Skip to content

Latest commit

 

History

History
157 lines (125 loc) · 6.56 KB

File metadata and controls

157 lines (125 loc) · 6.56 KB

Code-block syntax highlighting

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.

Usage

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.

Copy button

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.

Supported languages

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.

Choosing languages / reducing binary size

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.

iOS

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'

Android

Add to your project's gradle.properties:

# Compile a custom set:
enrichedMarkdown.codeHighlightLanguages=javascript,tsx,json,bash

# ...or disable highlighting entirely:
enrichedMarkdown.enableCodeHighlight=false

Rebuild the app after changing either value.

Expo config plugin

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.

How it works

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).