Fenced code blocks are syntax-highlighted natively via tree-sitter. Highlighting is foreground-only (it recolors tokens and never changes text metrics), so a code block's measured height always matches its drawn height. It is enabled by default on iOS and Android with a curated set of languages, and can be trimmed or disabled to reduce binary size.
Highlighting activates automatically for a fenced block whose info string names a supported language:
<EnrichedMarkdownText
flavor="github"
markdown={String.raw`
\`\`\`python
def greet(name: str) -> str:
return f"Hello, {name}!" # a comment
\`\`\`
`}
markdownStyle={{
codeBlock: {
syntaxColors: {
keyword: '#C678DD',
string: '#98C379',
number: '#D19A66',
comment: '#7F848E',
function: '#61AFEF',
type: '#E5C07B',
// ...any of the 14 token types
},
},
}}
/>Token colors are set through codeBlock.syntaxColors. The 14 token types are: keyword,
operator, punctuation, string, number, constant, comment, function, type,
variable, property, tag, attribute, embedded. Any type left unset is drawn in the normal
code color.
With flavor="github", each code block's header shows a copy button (and a long-press context
menu with Copy / Copy as Markdown). The CommonMark flavor renders code blocks inline with
no header, so it has no copy button. To observe when a user copies code, pass
onCopyPress — it fires with the copied code and its
language for the header button, the context-menu Copy action, and the VoiceOver copy action.
The copy label shown to assistive technologies is configurable via
selectionMenuConfig.
Fence info strings map to a grammar (for example js, jsx -> JavaScript). The curated default
set is compiled in unless you override it. It is defined by default:true in
vendor/grammar-versions.json (the single source of truth the iOS podspec and Android build both
derive from), so the table below tracks that manifest:
| Default (on) | Opt-in (heavier) |
|---|---|
| json, html, css, markdown, yaml, go, java, javascript, python, c, rust, bash, typescript, tsx | cpp, swift, php, ruby, c-sharp |
The default set is the smaller-footprint tier (~32 MB of grammar C source across the whole set). The opt-in grammars are larger (17-29 MB each) and are only compiled when you list them explicitly. A block whose language is not compiled in simply renders as plain (uncolored) code.
Only the grammars you compile end up in your binary, so trimming the list is the main size lever. The seam degrades to plain code whenever a grammar is absent, so nothing breaks when you remove one.
Tip
To also skip the install-time grammar download (not just the build-time linking), set
"enriched-markdown": { "enableCodeHighlight": false } in your app's package.json. This
auto-disables highlighting at build time too, so it's an alternative to the build flags below.
See Skipping the download.
Add to your Podfile and re-run pod install:
# Compile a custom set (comma-separated; adds tsx to the trimmed set below):
ENV['ENRICHED_MARKDOWN_CODE_HIGHLIGHT_LANGUAGES'] = 'javascript,tsx,json,bash'
# ...or disable highlighting entirely (no tree-sitter code linked):
ENV['ENRICHED_MARKDOWN_ENABLE_CODE_HIGHLIGHT'] = '0'Add to your project's gradle.properties:
# Compile a custom set:
enrichedMarkdown.codeHighlightLanguages=javascript,tsx,json,bash
# ...or disable highlighting entirely:
enrichedMarkdown.enableCodeHighlight=falseRebuild the app after changing either value.
Configure both platforms at once in app.json / app.config.js:
{
"expo": {
"plugins": [
[
"react-native-enriched-markdown",
{
"codeHighlight": {
"enabled": true,
"languages": ["javascript", "tsx", "json", "bash"]
}
}
]
]
}
}Set "enabled": false to disable it. Changes are applied during npx expo prebuild; if you change
the set later, run npx expo prebuild --clean and rebuild.
Grammars are vendored into packages/core/cpp/highlight/vendor/ (only each grammar's
parser.c/scanner.c + highlights.scm, never whole npm packages), so the native build itself is
fully offline and deterministic. The stable tree-sitter runtime is vendored the same way and compiled
with WebAssembly support left out. A build-time codegen emits a registry for exactly the selected
languages, so the binary and link step only ever reference compiled grammars.
The entire vendor/ tree is gitignored to keep the repo and PRs small — nothing generated lives
in git. vendor/vendor-grammars.mjs restores all of it from the pins in vendor/grammar-versions.json:
the tree-sitter runtime (vendor/tree-sitter/) is fetched and sha256-verified from the pinned GitHub
release tarball, the ~178 MB of grammar parser.c tables (vendor/grammars/) are copied from the
pinned grammar devDependencies, and the default registry (vendor/generated/) is codegen'd from them.
It is wired into the package prepare script (so a plain yarn install restores everything, with
.stamp guards making repeats a no-op).
The published npm tarball ships only the small default registry, not the heavy grammar/runtime
source — that would bloat every install to ~230 MB. Instead a postinstall script downloads the
grammar sources (from the npm registry) and the tree-sitter runtime (from the pinned GitHub release)
into the installed package, sha256-verified and idempotent. See Native assets
for the install-time behavior, network requirements, and how to recover an offline/pnpm install.
Highlighting runs synchronously when a code block is applied and is cached per block, with a size cap
(~50 KB / ~2000 lines) that falls back to plain rendering for pathological inputs. Maintainers re-pin
by editing vendor/grammar-versions.json (for a runtime bump, also update runtime.sha256 — a full
node vendor/vendor-grammars.mjs --force run prints the correct digest on mismatch) and re-running
the script; there is nothing generated to commit. To vendor the runtime from a local tree-sitter
checkout instead of the network, pass --runtime-src <path to tree-sitter/lib> (or set
TREE_SITTER_SRC).