react-native-nitro-markdown 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +257 -682
  2. package/android/CMakeLists.txt +8 -1
  3. package/android/build.gradle +9 -2
  4. package/android/consumer-rules.pro +31 -0
  5. package/android/gradle.properties +2 -0
  6. package/android/src/main/cpp/cpp-adapter.cpp +4 -1
  7. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +61 -21
  8. package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +6 -18
  9. package/cpp/bindings/HybridMarkdownParser.cpp +38 -12
  10. package/cpp/bindings/HybridMarkdownParser.hpp +4 -4
  11. package/cpp/bindings/HybridMarkdownSession.cpp +2 -0
  12. package/cpp/core/MD4CParser.cpp +128 -85
  13. package/cpp/core/MarkdownSessionCore.cpp +2 -0
  14. package/ios/HybridMarkdownSession.swift +89 -46
  15. package/lib/commonjs/headless.js +33 -7
  16. package/lib/commonjs/headless.js.map +1 -1
  17. package/lib/commonjs/index.js +48 -38
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/markdown-stream.js +1 -1
  20. package/lib/commonjs/markdown-stream.js.map +1 -1
  21. package/lib/commonjs/markdown.js +47 -10
  22. package/lib/commonjs/markdown.js.map +1 -1
  23. package/lib/commonjs/renderers/code.js +1 -1
  24. package/lib/commonjs/renderers/code.js.map +1 -1
  25. package/lib/commonjs/renderers/image.js +6 -1
  26. package/lib/commonjs/renderers/image.js.map +1 -1
  27. package/lib/commonjs/renderers/link.js +7 -2
  28. package/lib/commonjs/renderers/link.js.map +1 -1
  29. package/lib/commonjs/renderers/list.js +2 -0
  30. package/lib/commonjs/renderers/list.js.map +1 -1
  31. package/lib/commonjs/renderers/math.js +4 -2
  32. package/lib/commonjs/renderers/math.js.map +1 -1
  33. package/lib/commonjs/renderers/table/cell-content.js +1 -1
  34. package/lib/commonjs/renderers/table/cell-content.js.map +1 -1
  35. package/lib/commonjs/renderers/table/index.js +10 -2
  36. package/lib/commonjs/renderers/table/index.js.map +1 -1
  37. package/lib/commonjs/theme.js +7 -7
  38. package/lib/commonjs/theme.js.map +1 -1
  39. package/lib/commonjs/utils/code-highlight.js +24 -25
  40. package/lib/commonjs/utils/code-highlight.js.map +1 -1
  41. package/lib/module/headless.js +34 -6
  42. package/lib/module/headless.js.map +1 -1
  43. package/lib/module/index.js +1 -1
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/markdown-stream.js +1 -1
  46. package/lib/module/markdown-stream.js.map +1 -1
  47. package/lib/module/markdown.js +48 -11
  48. package/lib/module/markdown.js.map +1 -1
  49. package/lib/module/renderers/code.js +1 -1
  50. package/lib/module/renderers/code.js.map +1 -1
  51. package/lib/module/renderers/image.js +6 -1
  52. package/lib/module/renderers/image.js.map +1 -1
  53. package/lib/module/renderers/link.js +7 -2
  54. package/lib/module/renderers/link.js.map +1 -1
  55. package/lib/module/renderers/list.js +2 -0
  56. package/lib/module/renderers/list.js.map +1 -1
  57. package/lib/module/renderers/math.js +4 -2
  58. package/lib/module/renderers/math.js.map +1 -1
  59. package/lib/module/renderers/table/cell-content.js +1 -1
  60. package/lib/module/renderers/table/cell-content.js.map +1 -1
  61. package/lib/module/renderers/table/index.js +10 -2
  62. package/lib/module/renderers/table/index.js.map +1 -1
  63. package/lib/module/theme.js +7 -7
  64. package/lib/module/theme.js.map +1 -1
  65. package/lib/module/utils/code-highlight.js +24 -25
  66. package/lib/module/utils/code-highlight.js.map +1 -1
  67. package/lib/typescript/commonjs/headless.d.ts +9 -1
  68. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/index.d.ts +3 -2
  70. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/markdown.d.ts +7 -2
  73. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +4 -3
  80. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/utils/code-highlight.d.ts +1 -1
  84. package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -1
  85. package/lib/typescript/module/headless.d.ts +9 -1
  86. package/lib/typescript/module/headless.d.ts.map +1 -1
  87. package/lib/typescript/module/index.d.ts +3 -2
  88. package/lib/typescript/module/index.d.ts.map +1 -1
  89. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  90. package/lib/typescript/module/markdown.d.ts +7 -2
  91. package/lib/typescript/module/markdown.d.ts.map +1 -1
  92. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  93. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  94. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  95. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  96. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  97. package/lib/typescript/module/renderers/table/cell-content.d.ts +4 -3
  98. package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -1
  99. package/lib/typescript/module/renderers/table/index.d.ts.map +1 -1
  100. package/lib/typescript/module/theme.d.ts.map +1 -1
  101. package/lib/typescript/module/utils/code-highlight.d.ts +1 -1
  102. package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -1
  103. package/package.json +5 -3
  104. package/src/headless.ts +57 -7
  105. package/src/index.ts +16 -2
  106. package/src/markdown-stream.tsx +1 -0
  107. package/src/markdown.tsx +98 -31
  108. package/src/renderers/code.tsx +23 -16
  109. package/src/renderers/image.tsx +9 -1
  110. package/src/renderers/link.tsx +8 -2
  111. package/src/renderers/list.tsx +2 -0
  112. package/src/renderers/math.tsx +6 -2
  113. package/src/renderers/table/cell-content.tsx +15 -4
  114. package/src/renderers/table/index.tsx +15 -3
  115. package/src/theme.ts +34 -14
  116. package/src/utils/code-highlight.ts +133 -44
package/README.md CHANGED
@@ -4,59 +4,45 @@
4
4
 
5
5
  # react-native-nitro-markdown
6
6
 
7
- Native Markdown parsing and rendering for React Native.
7
+ Native Markdown parsing and rendering for React Native, powered by [md4c](https://github.com/mity/md4c) (C++) through [Nitro Modules](https://github.com/mrousavy/nitro) (JSI).
8
8
 
9
- `react-native-nitro-markdown` uses `md4c` (C++) through Nitro Modules (JSI) to parse Markdown synchronously into a typed AST, then render it with customizable React Native components.
9
+ ## Features
10
10
 
11
- ## Why use it
12
-
13
- - Native parser (`md4c`) for lower JS thread overhead on large documents
14
- - End-to-end solution: parser + renderer + streaming session API
15
- - Headless API for custom rendering and text processing
16
- - GFM support (tables, strikethrough, task lists, autolinks)
17
- - Optional math rendering with `react-native-mathjax-svg`
11
+ - **Native C++ parser** -- synchronous parsing via JSI with minimal JS thread overhead
12
+ - **Full rendering pipeline** -- parser + renderer + streaming session in one package
13
+ - **GFM support** -- tables, strikethrough, task lists, autolinks
14
+ - **Math rendering** -- inline and block LaTeX via `react-native-mathjax-svg` (optional)
15
+ - **Headless API** -- parse markdown and extract text without any UI
16
+ - **Streaming** -- incremental rendering for chat/LLM token streams
17
+ - **Customizable** -- themes, per-node style overrides, custom renderers, AST transforms, plugin pipeline
18
18
 
19
19
  ## Requirements
20
20
 
21
- - React Native `>=0.75.0`
22
- - `react-native-nitro-modules >=0.35.0`
23
-
24
- Optional for math rendering:
25
-
26
- - `react-native-mathjax-svg >=0.9.0`
27
- - `react-native-svg >=13.0.0`
21
+ | Dependency | Version |
22
+ |---|---|
23
+ | React Native | `>=0.75.0` |
24
+ | react-native-nitro-modules | `>=0.35.0` |
25
+ | react-native-mathjax-svg *(optional)* | `>=0.9.0` |
26
+ | react-native-svg *(optional, for math)* | `>=13.0.0` |
28
27
 
29
28
  ## Installation
30
29
 
31
- ### React Native
32
-
33
- ```bash
34
- bun add react-native-nitro-markdown react-native-nitro-modules
35
- ```
36
-
37
- Optional math support:
38
-
39
- ```bash
40
- bun add react-native-mathjax-svg react-native-svg
41
- ```
42
-
43
- iOS pods:
44
-
45
30
  ```bash
31
+ npm install react-native-nitro-markdown react-native-nitro-modules
46
32
  cd ios && pod install
47
33
  ```
48
34
 
49
- ### Expo (development build)
35
+ For math rendering:
50
36
 
51
37
  ```bash
52
- bunx expo install react-native-nitro-markdown react-native-nitro-modules
53
- bunx expo prebuild
38
+ npm install react-native-mathjax-svg react-native-svg
54
39
  ```
55
40
 
56
- Optional math support:
41
+ **Expo** (development build):
57
42
 
58
43
  ```bash
59
- bunx expo install react-native-mathjax-svg react-native-svg
44
+ npx expo install react-native-nitro-markdown react-native-nitro-modules
45
+ npx expo prebuild
60
46
  ```
61
47
 
62
48
  ## Quick Start
@@ -73,343 +59,200 @@ export function Example() {
73
59
  }
74
60
  ```
75
61
 
76
- ## Demo App Tour
77
-
78
- The example app in `apps/example` now maps each major feature to a screen:
79
-
80
- - `Bench` (`app/index.tsx`)
81
- - Nitro benchmark + JS parser comparisons
82
- - `Default` (`app/render-default.tsx`)
83
- - Built-in renderer defaults
84
- - `Styles` (`app/render-default-styles.tsx`)
85
- - `styles` prop and theme token overrides
86
- - `Custom` (`app/render-custom.tsx`)
87
- - `renderers` overrides + `astTransform`
88
- - `Stream` (`app/render-stream.tsx`)
89
- - streaming UX with live token append
90
-
91
- ## Runtime API Coverage (Demo + Docs)
92
-
93
- This table maps each runtime API to where it is demonstrated.
94
-
95
- | API | Purpose | Demo usage |
96
- | --- | --- | --- |
97
- | `Markdown` | Parse + render markdown component | `apps/example/app/render-default.tsx` |
98
- | `Markdown` `options` | Enable parser flags (`gfm`, `math`) | `apps/example/app/render-default.tsx` |
99
- | `Markdown` `styles` | Per-node style overrides | `apps/example/app/render-default-styles.tsx` |
100
- | `Markdown` `renderers` | Custom node renderer overrides | `apps/example/app/render-custom.tsx` |
101
- | `Markdown` `astTransform` | Post-parse AST transform hook | `apps/example/app/render-custom.tsx` |
102
- | `Markdown` `virtualize` / `virtualization*` | Large-document block virtualization | README examples |
103
- | `MarkdownStream` | Stream rendering from session text | `apps/example/app/render-stream.tsx` |
104
- | `useMarkdownSession` | Own and reuse a native markdown session | `apps/example/app/render-stream.tsx` |
105
- | `createMarkdownSession` | Create a manual session instance | README examples |
106
- | `useStream` | Timed playback sync + highlighting | README examples |
107
- | `parseMarkdown` | Headless parse/benchmark pipeline | `apps/example/app/index.tsx` |
108
- | `parseMarkdownWithOptions` | Headless parse with parser flags | README examples |
109
- | `getTextContent` | Extract raw text from AST subtree | README examples |
110
- | `getFlattenedText` | Normalize AST text for indexing/search | README examples |
111
- | `MarkdownParserModule` | Low-level Nitro parser access | README examples |
112
- | `mergeThemes` / `defaultMarkdownTheme` / `minimalMarkdownTheme` | Theme composition and style presets | `apps/example/app/render-default-styles.tsx` + README examples |
113
- | `useMarkdownContext` / `MarkdownContext` | Access theme/renderer/link handlers inside custom trees | README examples |
114
- | Built-in renderer components (`CodeBlock`, `TableRenderer`, etc.) | Compose renderer overrides with built-ins | `apps/example/app/render-custom.tsx` |
115
- | `onLinkPress`, `onParsingInProgress`, `onParseComplete`, `plugins`, `sourceAst` | Advanced lifecycle/link/pipeline control | README examples |
116
- | `onError` | Structured error reporting for parse and plugin failures | README examples |
117
- | `highlightCode` / `defaultHighlighter` | Opt-in syntax highlighting for code blocks | README examples |
118
- | `tableOptions` | Per-instance table column and measurement tuning | README examples |
119
- | `stripSourceOffsets` | Remove source position data from parsed AST | README examples |
120
- | `MarkdownSession.reset` / `MarkdownSession.replace` | Full and partial buffer mutation | README examples |
121
-
122
- ## Feature Index
123
-
124
- Use this table as a quick map from feature -> API -> demo usage.
125
-
126
- | Feature | API | What it does | Demo |
127
- | ------------------------ | --------------------------------------------------- | --------------------------------------------------------- | ------------------------------------- |
128
- | Basic markdown rendering | `Markdown` | Parse and render markdown in one component | `app/render-default.tsx` |
129
- | Parser flags | `options` (`gfm`, `math`) | Enable GFM and math parsing | `app/render-default.tsx` |
130
- | Plugin pipeline | `plugins` (`beforeParse`, `afterParse`) | Rewrite markdown input or AST around parse | README examples |
131
- | AST transform | `astTransform` | Post-parse AST rewrite before render | `app/render-custom.tsx` |
132
- | Pre-parsed AST render | `sourceAst` | Skip parsing during render and render existing AST | README examples |
133
- | Parse lifecycle | `onParsingInProgress`, `onParseComplete` | Observe parse start/finish and consume normalized text | README examples |
134
- | Link interception | `onLinkPress` | Override default URL open behavior | README examples |
135
- | Large doc virtualization | `virtualize`, `virtualizationMinBlocks` | Virtualizes top-level blocks for very large markdown docs | README examples |
136
- | Streaming markdown | `MarkdownStream` + `createMarkdownSession` | Render incrementally appended markdown content | `app/render-stream.tsx` |
137
- | Timed highlight sync | `useStream(timestamps)` | Sync highlight position to playback timeline | README examples |
138
- | Headless parsing | `parseMarkdown`, `parseMarkdownWithOptions` | Parse markdown without built-in UI | `app/index.tsx` + README examples |
139
- | Custom node rendering | `renderers` + built-in renderer components | Replace specific node UI while preserving parser behavior | `app/render-custom.tsx` |
140
- | Styling and theme | `theme`, `styles`, `stylingStrategy`, `mergeThemes` | Control visual tokens and per-node styles | `app/render-default-styles.tsx` |
141
- | Syntax highlighting | `highlightCode`, `defaultHighlighter`, `codeTokenColors` | Opt-in token-colored code block rendering | README examples |
142
- | Plugin priority | `MarkdownPlugin.priority` | Control plugin execution order | README examples |
143
- | Error reporting | `onError` | Observe parse and plugin failures without crashing | README examples |
144
- | Table tuning | `tableOptions` | Configure column width and measurement debounce | README examples |
145
- | AST cleanup | `stripSourceOffsets` | Remove source positions from AST for compact storage | README examples |
146
- | Session mutation | `MarkdownSession.reset`, `MarkdownSession.replace` | Replace or partially edit the session text buffer | README examples |
147
- | Low-level parser access | `MarkdownParserModule` | Direct access to Nitro parser methods | README examples |
62
+ ## API Reference
148
63
 
149
- ## Package Exports
64
+ ### `<Markdown>`
150
65
 
151
- ### Main Entry (`react-native-nitro-markdown`)
152
-
153
- - Parser and headless helpers:
154
- - `parseMarkdown`
155
- - `parseMarkdownWithOptions`
156
- - `getTextContent`
157
- - `getFlattenedText`
158
- - `MarkdownParserModule`
159
- - Components:
160
- - `Markdown`
161
- - `MarkdownStream`
162
- - Hooks and sessions:
163
- - `useMarkdownSession`
164
- - `useStream`
165
- - `createMarkdownSession`
166
- - Context:
167
- - `MarkdownContext`
168
- - `useMarkdownContext`
169
- - Theme:
170
- - `defaultMarkdownTheme`
171
- - `minimalMarkdownTheme`
172
- - `mergeThemes`
173
- - Built-in renderers:
174
- - `Heading`, `Paragraph`, `Link`, `Blockquote`, `HorizontalRule`
175
- - `CodeBlock`, `InlineCode`
176
- - `List`, `ListItem`, `TaskListItem`
177
- - `TableRenderer`, `Image`, `MathInline`, `MathBlock`
178
- - Syntax highlighting:
179
- - `defaultHighlighter`
180
- - `CodeHighlighter`, `HighlightedToken`, `TokenType`
181
- - Types:
182
- - `MarkdownNode`, `ParserOptions`, `MarkdownParser`
183
- - `MarkdownProps`, `AstTransform`, `MarkdownPlugin`, `MarkdownStreamProps`, `MarkdownVirtualizationOptions`
184
- - `CustomRenderers`, `CustomRenderer`, `CustomRendererProps`
185
- - `NodeRendererProps`, `BaseCustomRendererProps`, `EnhancedRendererProps`
186
- - `HeadingRendererProps`, `LinkRendererProps`, `ImageRendererProps`
187
- - `CodeBlockRendererProps`, `InlineCodeRendererProps`
188
- - `ListRendererProps`, `TaskListItemRendererProps`
189
- - `LinkPressHandler`, `MarkdownContextValue`
190
- - `MarkdownTheme`, `PartialMarkdownTheme`, `NodeStyleOverrides`, `StylingStrategy`
191
- - `MarkdownSession`
192
-
193
- ### Headless Entry (`react-native-nitro-markdown/headless`)
194
-
195
- Exports only parser-related API (`parseMarkdown`, `parseMarkdownWithOptions`, `extractPlainText`, `extractPlainTextWithOptions`, `getTextContent`, `getFlattenedText`, `stripSourceOffsets`, types). Use this when you do not need built-in UI rendering.
196
-
197
- ## Component API
198
-
199
- ## `Markdown`
66
+ The main component. Parses a markdown string and renders it.
200
67
 
201
68
  ```tsx
202
69
  import { Markdown } from "react-native-nitro-markdown";
203
70
  ```
204
71
 
205
- Demo usage:
206
-
207
- - `apps/example/app/render-default.tsx`
208
- - `apps/example/app/render-default-styles.tsx`
209
- - `apps/example/app/render-custom.tsx`
210
-
211
- | Prop | Type | Default | Description |
212
- | --------------------- | ---------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
213
- | `children` | `string` | required | Markdown input string. |
214
- | `options` | `ParserOptions` | `undefined` | Parser flags (`gfm`, `math`). |
215
- | `plugins` | `MarkdownPlugin[]` | `undefined` | Optional parser plugin hooks (`beforeParse`, `afterParse`). |
216
- | `sourceAst` | `MarkdownNode` | `undefined` | Pre-parsed AST. When provided, native parse is skipped. |
217
- | `astTransform` | `AstTransform` | `undefined` | Transform hook applied after plugins, before rendering and `onParseComplete`. |
218
- | `renderers` | `CustomRenderers` | `{}` | Per-node custom renderers. |
219
- | `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` or `minimalMarkdownTheme` | Theme token overrides. |
220
- | `styles` | `NodeStyleOverrides` | `undefined` | Per-node style overrides. |
221
- | `stylingStrategy` | `"opinionated" \| "minimal"` | `"opinionated"` | Base styling preset. |
222
- | `style` | `StyleProp<ViewStyle>` | `undefined` | Container style for the root `View`. |
223
- | `onParsingInProgress` | `() => void` | `undefined` | Called when parse inputs change. |
224
- | `onParseComplete` | `(result) => void` | `undefined` | Called with `{ raw, ast, text }` after successful parse. |
225
- | `onLinkPress` | `LinkPressHandler` | `undefined` | Intercepts link press before default open behavior. Return `false` to block default open. |
226
- | `onError` | `(error, phase, pluginName?) => void` | `undefined` | Called when a parse or plugin error occurs. `phase` is `'parse'`, `'before-plugin'`, or `'after-plugin'`. |
227
- | `highlightCode` | `boolean \| CodeHighlighter` | `undefined` | Enables syntax highlighting for code blocks. Pass `true` for the built-in highlighter or a custom `CodeHighlighter` function. |
228
- | `tableOptions` | `{ minColumnWidth?: number; measurementStabilizeMs?: number }` | `undefined` | Table layout tuning. `minColumnWidth` defaults to `60`; `measurementStabilizeMs` defaults to `140`. |
229
- | `virtualize` | `boolean \| "auto"` | `false` | Enables top-level block virtualization. Use `"auto"` to activate by block threshold. |
230
- | `virtualizationMinBlocks` | `number` | `40` | Minimum top-level block count before virtualization activates. |
231
- | `virtualization` | `MarkdownVirtualizationOptions` | `undefined` | Optional FlatList tuning (`windowSize`, `initialNumToRender`, batching, clipping). |
232
-
233
- Notes:
234
-
235
- - Parse failures are caught and rendered as a fallback message (`Error parsing markdown`).
236
- - `text` in `onParseComplete` is produced by `getFlattenedText(ast)`.
237
- - `astTransform` should be wrapped with `useCallback` to avoid unnecessary re-parses.
238
- - `astTransform` is a post-parse AST rewrite hook. It does not add parser syntax support and is not a markdown-it plugin API.
239
- - Plugin pipeline order is: `beforeParse` plugins (sorted by `priority` desc) -> parse/sourceAst -> `afterParse` plugins (sorted by `priority` desc) -> `astTransform` -> render.
240
- - `onError` is called per-failure; individual plugin failures do not abort the full pipeline.
241
- - Tables render immediately with estimated column widths, then refine widths after layout measurement to improve reliability on slower layout cycles.
242
- - For very large markdown content, enable `virtualize` to avoid mounting all top-level blocks at once.
243
- - `virtualize="auto"` enables threshold-driven virtualization while keeping small markdown renders on plain `View` trees.
244
-
245
- ### Virtualization example (large docs)
72
+ | Prop | Type | Default | Description |
73
+ |---|---|---|---|
74
+ | `children` | `string` | required | Markdown input string |
75
+ | `options` | `ParserOptions` | -- | Parser flags (`gfm`, `math`) |
76
+ | `plugins` | `MarkdownPlugin[]` | -- | Plugin hooks (`beforeParse`, `afterParse`) |
77
+ | `sourceAst` | `MarkdownNode` | -- | Pre-parsed AST; skips native parse when provided |
78
+ | `astTransform` | `AstTransform` | -- | Post-parse AST rewrite before render |
79
+ | `renderers` | `CustomRenderers` | `{}` | Per-node custom renderer overrides |
80
+ | `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` | Theme token overrides |
81
+ | `styles` | `NodeStyleOverrides` | -- | Per-node style overrides |
82
+ | `stylingStrategy` | `"opinionated" \| "minimal"` | `"opinionated"` | Base styling preset |
83
+ | `style` | `StyleProp<ViewStyle>` | -- | Container style |
84
+ | `onLinkPress` | `LinkPressHandler` | -- | Intercept link presses; return `false` to block default open |
85
+ | `onParsingInProgress` | `() => void` | -- | Called when parse inputs change |
86
+ | `onParseComplete` | `(result) => void` | -- | Called with `{ raw, ast, text }` after parse |
87
+ | `onError` | `(error, phase, pluginName?) => void` | -- | Error handler for parse/plugin failures |
88
+ | `highlightCode` | `boolean \| CodeHighlighter` | -- | Enable syntax highlighting for code blocks |
89
+ | `tableOptions` | `{ minColumnWidth?; measurementStabilizeMs? }` | -- | Table layout tuning |
90
+ | `virtualize` | `boolean \| "auto"` | `false` | Top-level block virtualization |
91
+ | `virtualizationMinBlocks` | `number` | `40` | Block threshold for `"auto"` virtualization |
92
+ | `virtualization` | `MarkdownVirtualizationOptions` | -- | FlatList tuning (windowSize, batching, etc.) |
93
+
94
+ **Pipeline order:** `beforeParse` plugins (by priority desc) -> parse/sourceAst -> `afterParse` plugins (by priority desc) -> `astTransform` -> render.
95
+
96
+ ### `<MarkdownStream>`
97
+
98
+ Renders markdown from a streaming session. Extends `MarkdownProps` (minus `children`).
246
99
 
247
100
  ```tsx
248
- <Markdown
249
- virtualize="auto"
250
- virtualizationMinBlocks={30}
251
- virtualization={{
252
- initialNumToRender: 10,
253
- maxToRenderPerBatch: 10,
254
- windowSize: 8,
255
- updateCellsBatchingPeriod: 16,
256
- removeClippedSubviews: true,
257
- }}
258
- >
259
- {content}
260
- </Markdown>
101
+ import { MarkdownStream } from "react-native-nitro-markdown";
261
102
  ```
262
103
 
263
- Virtualization notes:
104
+ | Prop | Type | Default | Description |
105
+ |---|---|---|---|
106
+ | `session` | `MarkdownSession` | required | Session supplying streamed text |
107
+ | `updateIntervalMs` | `number` | `50` | Flush interval for `"interval"` strategy |
108
+ | `updateStrategy` | `"interval" \| "raf"` | `"interval"` | Update cadence |
109
+ | `useTransitionUpdates` | `boolean` | `false` | Wrap updates in `startTransition` |
110
+ | `incrementalParsing` | `boolean` | `true` | Append-optimized incremental AST updates |
264
111
 
265
- - Keep `Markdown` as the primary vertical scroller when `virtualize` is enabled.
266
- - Avoid nesting it inside another vertical `ScrollView`, or virtualization effectiveness drops.
112
+ ### `MarkdownSession`
267
113
 
268
- ### AST transform example
114
+ A native text buffer with change listeners, used for streaming.
269
115
 
270
116
  ```tsx
271
- import { useCallback } from "react";
272
- import { Markdown, type AstTransform } from "react-native-nitro-markdown";
273
-
274
- const astTransform = useCallback<AstTransform>((ast) => {
275
- const transformNode = (node: Parameters<AstTransform>[0]) => ({
276
- ...node,
277
- content:
278
- node.type === "text"
279
- ? (node.content ?? "").replace(/:wink:/g, "😉")
280
- : node.content,
281
- children: node.children?.map(transformNode),
282
- });
283
-
284
- return transformNode(ast);
285
- }, []);
117
+ import { createMarkdownSession } from "react-native-nitro-markdown";
286
118
 
287
- <Markdown astTransform={astTransform}>{"Hello :wink:"}</Markdown>;
119
+ const session = createMarkdownSession();
120
+ session.append("# Hello\n");
121
+ session.append("Streaming content...");
288
122
  ```
289
123
 
290
- ### Plugin pipeline example (`beforeParse` + `afterParse`)
291
-
292
- ```tsx
293
- import { Markdown, type MarkdownPlugin } from "react-native-nitro-markdown";
294
-
295
- const plugins: MarkdownPlugin[] = [
296
- {
297
- name: "rewrite-before-parse",
298
- priority: 10, // runs before lower-priority plugins
299
- beforeParse: (input) => input.replace(/:rocket:/g, "ROCKET_TOKEN"),
300
- },
301
- {
302
- name: "rewrite-after-parse",
303
- afterParse: (ast) => {
304
- const visit = (node: typeof ast): typeof ast => ({
305
- ...node,
306
- content:
307
- node.type === "text"
308
- ? (node.content ?? "").replace(/ROCKET_TOKEN/g, "🚀")
309
- : node.content,
310
- children: node.children?.map(visit),
311
- });
312
- return visit(ast);
313
- },
314
- },
315
- ];
124
+ | Method | Signature | Description |
125
+ |---|---|---|
126
+ | `append` | `(chunk: string) => number` | Append text, returns new UTF-16 length |
127
+ | `clear` | `() => void` | Clear buffer, emit reset event |
128
+ | `reset` | `(text: string) => void` | Replace full buffer content |
129
+ | `replace` | `(from, to, text) => number` | Partial buffer mutation |
130
+ | `getAllText` | `() => string` | Get full session text |
131
+ | `getLength` | `() => number` | Get UTF-16 length without copy |
132
+ | `getTextRange` | `(from, to) => string` | Get substring range |
133
+ | `addListener` | `(listener) => () => void` | Subscribe to mutation events; returns unsubscribe |
134
+ | `highlightPosition` | `number` | Mutable cursor for stream highlight |
316
135
 
317
- <Markdown plugins={plugins}>{"Launch :rocket:"}</Markdown>;
318
- ```
136
+ ### `useMarkdownSession()`
319
137
 
320
- ### `onError` example
138
+ Creates and owns a `MarkdownSession` for a component lifecycle.
321
139
 
322
140
  ```tsx
323
- import { Markdown } from "react-native-nitro-markdown";
141
+ import { useMarkdownSession } from "react-native-nitro-markdown";
324
142
 
325
- <Markdown
326
- onError={(error, phase, pluginName) => {
327
- console.warn(`[markdown] ${phase} error`, { error, pluginName });
328
- }}
329
- plugins={plugins}
330
- >
331
- {content}
332
- </Markdown>;
143
+ const { getSession, isStreaming, setIsStreaming, stop, clear, setHighlight } =
144
+ useMarkdownSession();
333
145
  ```
334
146
 
335
- ### Syntax highlighting example
147
+ ### `useStream(timestamps?)`
336
148
 
337
- ```tsx
338
- import { Markdown } from "react-native-nitro-markdown";
149
+ Extends `useMarkdownSession` with timeline sync for timed playback.
339
150
 
340
- // Built-in highlighter (JS/TS, Python, Bash)
341
- <Markdown highlightCode>{"```typescript\nconst x: number = 42;\n```"}</Markdown>;
342
-
343
- // Custom highlighter
344
- import type { CodeHighlighter } from "react-native-nitro-markdown";
151
+ ```tsx
152
+ import { useStream } from "react-native-nitro-markdown";
345
153
 
346
- const myHighlighter: CodeHighlighter = (language, code) => {
347
- // return an array of { text, type } tokens
348
- return [{ text: code, type: "default" }];
349
- };
154
+ const stream = useStream({ 0: 0, 1: 500, 2: 1000 });
155
+ stream.sync(currentTimeMs);
350
156
 
351
- <Markdown highlightCode={myHighlighter}>{content}</Markdown>;
157
+ <MarkdownStream session={stream.getSession()} />;
352
158
  ```
353
159
 
354
- ### `sourceAst` example (skip parsing in render)
160
+ ### Headless API
161
+
162
+ Parse markdown without any UI. Available from both entry points.
355
163
 
356
164
  ```tsx
357
165
  import {
358
- Markdown,
166
+ parseMarkdown,
359
167
  parseMarkdownWithOptions,
360
- } from "react-native-nitro-markdown";
168
+ extractPlainText,
169
+ getTextContent,
170
+ getFlattenedText,
171
+ stripSourceOffsets,
172
+ } from "react-native-nitro-markdown/headless";
173
+ ```
361
174
 
362
- const sourceAst = parseMarkdownWithOptions(content, { gfm: true, math: true });
175
+ | Function | Description |
176
+ |---|---|
177
+ | `parseMarkdown(text, options?)` | Parse to AST (options: `{ gfm?, math? }`) |
178
+ | `parseMarkdownWithOptions(text, options)` | Parse with explicit options |
179
+ | `extractPlainText(text)` | Parse and return plain text from native parser |
180
+ | `extractPlainTextWithOptions(text, options)` | Same with parser flags |
181
+ | `getTextContent(node)` | Concatenate text recursively (no normalization) |
182
+ | `getFlattenedText(node)` | Normalized plain text with block separators |
183
+ | `stripSourceOffsets(node)` | Remove `beg`/`end` fields from AST |
363
184
 
364
- <Markdown sourceAst={sourceAst}>
365
- {"children is ignored when sourceAst is provided"}
366
- </Markdown>;
367
- ```
185
+ ### Custom Renderers
368
186
 
369
- ### Parse lifecycle callbacks example
187
+ Override rendering for specific node types:
370
188
 
371
189
  ```tsx
372
- import { Markdown } from "react-native-nitro-markdown";
190
+ import {
191
+ Markdown,
192
+ type HeadingRendererProps,
193
+ type CodeBlockRendererProps,
194
+ } from "react-native-nitro-markdown";
373
195
 
374
196
  <Markdown
375
- onParsingInProgress={() => setIsParsing(true)}
376
- onParseComplete={({ raw, ast, text }) => {
377
- setIsParsing(false);
378
- setWordCount(text.trim().split(/\s+/).length);
379
- setLastRaw(raw);
380
- setLastAst(ast);
197
+ renderers={{
198
+ heading: ({ level, children }: HeadingRendererProps) => (
199
+ <MyHeading level={level}>{children}</MyHeading>
200
+ ),
201
+ code_block: ({ language, content }: CodeBlockRendererProps) => (
202
+ <MyCode language={language} content={content} />
203
+ ),
381
204
  }}
382
205
  >
383
206
  {content}
384
207
  </Markdown>;
385
208
  ```
386
209
 
387
- ## `MarkdownStream`
210
+ Renderers receive `EnhancedRendererProps` with `node`, `children`, and `Renderer` (for recursive rendering). Node-specific props are mapped automatically:
388
211
 
389
- ```tsx
390
- import { MarkdownStream } from "react-native-nitro-markdown";
391
- ```
212
+ | Node type | Extra props |
213
+ |---|---|
214
+ | `heading` | `level` |
215
+ | `link` | `href`, `title` |
216
+ | `image` | `url`, `alt`, `title` |
217
+ | `code_block` | `content`, `language` |
218
+ | `code_inline` | `content` |
219
+ | `list` | `ordered`, `start` |
220
+ | `task_list_item` | `checked` |
221
+
222
+ Return `undefined` to fall back to the built-in renderer, or `null` to render nothing.
392
223
 
393
- `MarkdownStreamProps` extends `MarkdownProps` except `children`.
224
+ ### Theme API
394
225
 
395
- Demo usage:
226
+ ```tsx
227
+ import {
228
+ Markdown,
229
+ defaultMarkdownTheme,
230
+ minimalMarkdownTheme,
231
+ mergeThemes,
232
+ } from "react-native-nitro-markdown";
233
+
234
+ const theme = mergeThemes(defaultMarkdownTheme, {
235
+ colors: { text: "#0f172a", link: "#1d4ed8" },
236
+ fontSizes: { m: 16 },
237
+ });
396
238
 
397
- - `apps/example/app/render-stream.tsx`
239
+ <Markdown theme={theme}>{content}</Markdown>;
240
+ ```
398
241
 
399
- | Prop | Type | Default | Description |
400
- | ---------------------- | --------------------- | ------------ | ---------------------------------------------------------------------------------------- |
401
- | `session` | `MarkdownSession` | required | Session object that supplies streamed text chunks. |
402
- | `updateIntervalMs` | `number` | `50` | Flush interval when `updateStrategy="interval"`. |
403
- | `updateStrategy` | `"interval" \| "raf"` | `"interval"` | Update cadence (`setTimeout` vs `requestAnimationFrame`). |
404
- | `useTransitionUpdates` | `boolean` | `false` | Applies `startTransition` to streamed UI updates. |
405
- | `incrementalParsing` | `boolean` | `true` | Enables append-optimized incremental AST updates (falls back to full parse when unsafe). |
242
+ `MarkdownTheme` includes:
243
+ - **colors** -- `text`, `heading`, `link`, `code`, `codeBackground`, `blockquote`, `border`, `surface`, table colors, and optional `codeTokenColors` for syntax highlighting
244
+ - **spacing** -- `xs`, `s`, `m`, `l`, `xl`
245
+ - **fontSizes** -- `xs` through `xl`, plus `h1`-`h6`
246
+ - **fontFamilies** -- `regular`, `heading`, `mono`
247
+ - **borderRadius** -- `s`, `m`, `l`
248
+ - **headingWeight** -- optional font weight override
249
+ - **showCodeLanguage** -- show/hide language label on code blocks
406
250
 
407
- Notes:
251
+ Use `stylingStrategy="minimal"` for a bare baseline, or `NodeStyleOverrides` for per-node style overrides (text nodes accept `TextStyle`, container nodes accept `ViewStyle`).
408
252
 
409
- - If any plugin defines `beforeParse`, `MarkdownStream` disables incremental AST mode for correctness.
410
- - `MarkdownStream` consumes native session change ranges (`from`, `to`) and uses `getTextRange()` for contiguous appends to avoid full-buffer copies during token streams.
253
+ ## Examples
411
254
 
412
- ### Streaming Example
255
+ ### Streaming
413
256
 
414
257
  ```tsx
415
258
  import { useEffect } from "react";
@@ -425,7 +268,6 @@ export function StreamingExample() {
425
268
  const s = session.getSession();
426
269
  s.append("# Streaming\n");
427
270
  s.append("This text arrives in chunks.");
428
-
429
271
  return () => session.clear();
430
272
  }, [session]);
431
273
 
@@ -434,371 +276,107 @@ export function StreamingExample() {
434
276
  session={session.getSession()}
435
277
  options={{ gfm: true }}
436
278
  updateStrategy="raf"
437
- useTransitionUpdates
438
279
  />
439
280
  );
440
281
  }
441
282
  ```
442
283
 
443
- ## Hooks and Session API
444
-
445
- ## `createMarkdownSession()`
446
-
447
- Creates and returns a native `MarkdownSession` instance.
448
-
449
- ```tsx
450
- import { createMarkdownSession } from "react-native-nitro-markdown";
451
-
452
- const session = createMarkdownSession();
453
- session.append("hello");
454
- ```
455
-
456
- `MarkdownSession` methods:
457
-
458
- | Method | Signature | Description |
459
- | --- | --- | --- |
460
- | `append` | `(chunk: string) => number` | Appends text and returns new UTF-16 length. |
461
- | `clear` | `() => void` | Clears buffer and emits a reset range event (`0, 0`). |
462
- | `reset` | `(text: string) => void` | Replaces full buffer content and emits a full-range change event. |
463
- | `replace` | `(from: number, to: number, text: string) => number` | Partial buffer mutation; returns new total UTF-16 length. |
464
- | `getAllText` | `() => string` | Returns full session text. |
465
- | `getLength` | `() => number` | Returns current UTF-16 text length without materializing a copy. |
466
- | `getTextRange` | `(from: number, to: number) => string` | Returns a substring range for delta-driven streaming updates. |
467
- | `addListener` | `(listener: (from: number, to: number) => void) => () => void` | Subscribes to mutation range events and returns an unsubscribe function. |
468
- | `highlightPosition` | `number` | Mutable cursor used by stream highlight workflows. |
469
-
470
- Demo usage:
471
-
472
- - Referenced in `apps/example/app/render-stream.tsx` sample markdown content and used directly in README examples.
473
-
474
- Manual session + stream rendering:
475
-
476
- ```tsx
477
- import {
478
- createMarkdownSession,
479
- MarkdownStream,
480
- } from "react-native-nitro-markdown";
481
-
482
- const session = createMarkdownSession();
483
- session.append("# Hello\n");
484
- session.append("Streaming content...");
485
-
486
- <MarkdownStream session={session} updateStrategy="raf" />;
487
- ```
488
-
489
- ## `useMarkdownSession()`
490
-
491
- Creates and owns one `MarkdownSession` for a component lifecycle.
492
-
493
- Returns:
494
-
495
- | Field | Type | Description |
496
- | ---------------- | ---------------------------- | ------------------------------------------------------------- |
497
- | `getSession` | `() => MarkdownSession` | Returns the stable native session instance. |
498
- | `isStreaming` | `boolean` | Stateful flag for app-level streaming UI. |
499
- | `setIsStreaming` | `(value: boolean) => void` | Setter for `isStreaming`. |
500
- | `stop` | `() => void` | Sets `isStreaming` to `false`. |
501
- | `clear` | `() => void` | Clears session content and resets `highlightPosition` to `0`. |
502
- | `setHighlight` | `(position: number) => void` | Sets `session.highlightPosition`. |
503
-
504
- Demo usage:
505
-
506
- - `apps/example/app/render-stream.tsx`
507
-
508
- ## `useStream(timestamps?)`
509
-
510
- Builds on `useMarkdownSession` and adds timeline sync helpers.
511
-
512
- - `timestamps` type: `Record<number, number>` where key = word/token index, value = timestamp in ms.
513
- - `sync(currentTimeMs)` computes highlight position from timestamp map.
514
- - Uses optimized lookup for monotonic timelines and handles non-monotonic maps safely.
515
-
516
- Additional returned fields:
517
-
518
- | Field | Type | Description |
519
- | -------------- | --------------------------------- | ----------------------------------------- |
520
- | `isPlaying` | `boolean` | Playback state for timed streaming. |
521
- | `setIsPlaying` | `(value: boolean) => void` | Setter for `isPlaying`. |
522
- | `sync` | `(currentTimeMs: number) => void` | Applies timeline-based highlight updates. |
523
-
524
- Example:
525
-
526
- ```tsx
527
- const stream = useStream({
528
- 0: 0,
529
- 1: 500,
530
- 2: 1000,
531
- });
532
-
533
- // e.g. in media time update callback:
534
- stream.sync(currentTimeMs);
535
-
536
- <MarkdownStream session={stream.getSession()} />;
537
- ```
538
-
539
- Demo usage:
540
-
541
- - README examples (timed playback scenario)
542
-
543
- ## Headless API
284
+ ### AST Transform
544
285
 
545
286
  ```tsx
546
- import {
547
- parseMarkdown,
548
- parseMarkdownWithOptions,
549
- extractPlainText,
550
- extractPlainTextWithOptions,
551
- getTextContent,
552
- getFlattenedText,
553
- stripSourceOffsets,
554
- } from "react-native-nitro-markdown/headless";
555
- ```
556
-
557
- | Function | Signature | Description |
558
- | -------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ |
559
- | `parseMarkdown` | `(text: string) => MarkdownNode` | Parses markdown using default parser settings. |
560
- | `parseMarkdownWithOptions` | `(text: string, options: ParserOptions) => MarkdownNode` | Parses markdown with `gfm` and/or `math` flags. |
561
- | `extractPlainText` | `(text: string) => string` | Parses and returns normalized plain text directly from native parser. |
562
- | `extractPlainTextWithOptions` | `(text: string, options: ParserOptions) => string` | Same as above with parser flags. |
563
- | `getTextContent` | `(node: MarkdownNode) => string` | Concatenates text recursively without layout normalization. |
564
- | `getFlattenedText` | `(node: MarkdownNode) => string` | Returns normalized plain text with paragraph and block separators. |
565
- | `stripSourceOffsets` | `(node: MarkdownNode) => MarkdownNode` | Recursively removes `beg`/`end` source position fields. Useful for compact serialization or snapshot testing. |
566
-
567
- ### Parser Options
568
-
569
- ```ts
570
- type ParserOptions = {
571
- gfm?: boolean;
572
- math?: boolean;
573
- };
574
- ```
287
+ import { useCallback } from "react";
288
+ import { Markdown, type AstTransform } from "react-native-nitro-markdown";
575
289
 
576
- Example:
290
+ const transform = useCallback<AstTransform>((ast) => {
291
+ const visit = (node: Parameters<AstTransform>[0]): typeof node => ({
292
+ ...node,
293
+ content:
294
+ node.type === "text"
295
+ ? (node.content ?? "").replace(/:wink:/g, "😉")
296
+ : node.content,
297
+ children: node.children?.map(visit),
298
+ });
299
+ return visit(ast);
300
+ }, []);
577
301
 
578
- ```tsx
579
- const ast = parseMarkdownWithOptions(markdown, {
580
- gfm: true, // tables, task lists, strikethrough
581
- math: true, // inline/block LaTeX
582
- });
302
+ <Markdown astTransform={transform}>{"Hello :wink:"}</Markdown>;
583
303
  ```
584
304
 
585
- ### `MarkdownParserModule` (low-level Nitro access)
586
-
587
- Use this only when you want direct method access (`parse`, `parseWithOptions`, `extractPlainText`, `extractPlainTextWithOptions`).
305
+ ### Plugin Pipeline
588
306
 
589
307
  ```tsx
590
- import {
591
- MarkdownParserModule,
592
- type ParserOptions,
593
- } from "react-native-nitro-markdown/headless";
594
-
595
- const options: ParserOptions = { gfm: true };
596
- const jsonAst = JSON.parse(
597
- MarkdownParserModule.parseWithOptions("# Hello", options),
598
- );
599
- ```
600
-
601
- ## Custom Renderer API
602
-
603
- ## `renderers` prop contract
308
+ import { Markdown, type MarkdownPlugin } from "react-native-nitro-markdown";
604
309
 
605
- `CustomRenderers` is:
310
+ const plugins: MarkdownPlugin[] = [
311
+ {
312
+ name: "rewrite-before-parse",
313
+ priority: 10,
314
+ beforeParse: (input) => input.replace(/:rocket:/g, "ROCKET_TOKEN"),
315
+ },
316
+ {
317
+ name: "rewrite-after-parse",
318
+ afterParse: (ast) => {
319
+ const visit = (node: typeof ast): typeof ast => ({
320
+ ...node,
321
+ content:
322
+ node.type === "text"
323
+ ? (node.content ?? "").replace(/ROCKET_TOKEN/g, "🚀")
324
+ : node.content,
325
+ children: node.children?.map(visit),
326
+ });
327
+ return visit(ast);
328
+ },
329
+ },
330
+ ];
606
331
 
607
- ```ts
608
- type CustomRenderers = Partial<Record<MarkdownNode["type"], CustomRenderer>>;
332
+ <Markdown plugins={plugins}>{"Launch :rocket:"}</Markdown>;
609
333
  ```
610
334
 
611
- `CustomRenderer` receives `EnhancedRendererProps` and returns:
612
-
613
- - React node to override default rendering
614
- - `undefined` to fallback to built-in renderer
615
- - `null` to render nothing
616
-
617
- `EnhancedRendererProps` always includes:
618
-
619
- - `node`: current `MarkdownNode`
620
- - `children`: pre-rendered node children
621
- - `Renderer`: recursive renderer component for nested custom rendering
622
-
623
- And conditionally includes mapped fields by node type:
624
-
625
- | Node type | Extra mapped fields |
626
- | ---------------- | --------------------- |
627
- | `heading` | `level` |
628
- | `link` | `href`, `title` |
629
- | `image` | `url`, `alt`, `title` |
630
- | `code_block` | `content`, `language` |
631
- | `code_inline` | `content` |
632
- | `list` | `ordered`, `start` |
633
- | `task_list_item` | `checked` |
634
-
635
- ### Example: Custom heading + code block
335
+ ### Pre-parsed AST
636
336
 
637
337
  ```tsx
638
- import {
639
- Markdown,
640
- type HeadingRendererProps,
641
- type CodeBlockRendererProps,
642
- } from "react-native-nitro-markdown";
338
+ import { Markdown, parseMarkdownWithOptions } from "react-native-nitro-markdown";
643
339
 
644
- const renderers = {
645
- heading: ({ level, children }: HeadingRendererProps) => (
646
- <MyHeading level={level}>{children}</MyHeading>
647
- ),
648
- code_block: ({ language, content }: CodeBlockRendererProps) => (
649
- <MyCode language={language} content={content} />
650
- ),
651
- };
340
+ const ast = parseMarkdownWithOptions(content, { gfm: true, math: true });
652
341
 
653
- <Markdown renderers={renderers}>{content}</Markdown>;
342
+ <Markdown sourceAst={ast}>{"ignored when sourceAst is provided"}</Markdown>;
654
343
  ```
655
344
 
656
- ### `useMarkdownContext` example (inside custom renderer tree)
345
+ ### Virtualization (large documents)
657
346
 
658
347
  ```tsx
659
- import { Text } from "react-native";
660
- import {
661
- Markdown,
662
- useMarkdownContext,
663
- type CustomRendererProps,
664
- } from "react-native-nitro-markdown";
665
-
666
- function ThemedParagraph({ children }: Pick<CustomRendererProps, "children">) {
667
- const { theme } = useMarkdownContext();
668
- return <Text style={{ color: theme.colors.text }}>{children}</Text>;
669
- }
670
-
671
348
  <Markdown
672
- renderers={{
673
- paragraph: ({ children }: CustomRendererProps) => (
674
- <ThemedParagraph>{children}</ThemedParagraph>
675
- ),
349
+ virtualize="auto"
350
+ virtualizationMinBlocks={30}
351
+ virtualization={{
352
+ initialNumToRender: 10,
353
+ maxToRenderPerBatch: 10,
354
+ windowSize: 8,
676
355
  }}
677
356
  >
678
357
  {content}
679
- </Markdown>;
680
- ```
681
-
682
- ## Link Handling Behavior
683
-
684
- Default link renderer behavior:
685
-
686
- 1. Trims incoming href.
687
- 2. Calls `onLinkPress(href)` if provided.
688
- 3. Stops if handler returns `false`.
689
- 4. Allows only protocol-based links with these schemes:
690
- - `http:`
691
- - `https:`
692
- - `mailto:`
693
- - `tel:`
694
- - `sms:`
695
- 5. Uses `Linking.canOpenURL` before `Linking.openURL`.
696
-
697
- Relative URLs and anchors are ignored by default open behavior, but you can handle them in `onLinkPress`.
698
-
699
- ## Theme API
700
-
701
- ## `MarkdownTheme`
702
-
703
- ```tsx
704
- import type {
705
- MarkdownTheme,
706
- PartialMarkdownTheme,
707
- } from "react-native-nitro-markdown";
358
+ </Markdown>
708
359
  ```
709
360
 
710
- `MarkdownTheme` fields:
711
-
712
- - `colors`
713
- - `text`, `textMuted`, `heading`, `link`, `code`, `codeBackground`, `codeLanguage`
714
- - `blockquote`, `border`, `surface`, `surfaceLight`, `accent`
715
- - `tableBorder`, `tableHeader`, `tableHeaderText`, `tableRowEven`, `tableRowOdd`
716
- - `codeTokenColors?`: `{ keyword?, string?, comment?, number?, operator?, punctuation?, type?, default? }` — per-token colors used by `highlightCode`
717
- - `spacing`: `xs`, `s`, `m`, `l`, `xl`
718
- - `fontSizes`: `xs`, `s`, `m`, `l`, `xl`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`
719
- - `fontFamilies`: `regular`, `heading`, `mono`
720
- - `headingWeight?`
721
- - `borderRadius`: `s`, `m`, `l`
722
- - `showCodeLanguage`
723
-
724
- Helpers:
725
-
726
- - `defaultMarkdownTheme`
727
- - `minimalMarkdownTheme`
728
- - `mergeThemes(base, partial)`
729
-
730
- `NodeStyleOverrides` lets you override per-node styles with type-safe shape checking:
731
-
732
- ```ts
733
- // Text-type nodes accept TextStyle; view-type nodes accept ViewStyle.
734
- // TypeScript will catch mismatched style shapes at compile time.
735
- type NodeStyleOverrides = Partial<
736
- {
737
- text: TextStyle; bold: TextStyle; italic: TextStyle; /* ... */
738
- document: ViewStyle; blockquote: ViewStyle; code_block: ViewStyle; /* ... */
739
- }
740
- >;
741
- ```
361
+ Keep `Markdown` as the primary vertical scroller when virtualization is enabled -- avoid nesting inside another `ScrollView`.
742
362
 
743
- Example with `mergeThemes`:
363
+ ### Syntax Highlighting
744
364
 
745
365
  ```tsx
746
- import {
747
- Markdown,
748
- defaultMarkdownTheme,
749
- mergeThemes,
750
- } from "react-native-nitro-markdown";
366
+ // Built-in highlighter (JS/TS, Python, Bash)
367
+ <Markdown highlightCode>{"```typescript\nconst x: number = 42;\n```"}</Markdown>
751
368
 
752
- const theme = mergeThemes(defaultMarkdownTheme, {
753
- colors: {
754
- text: "#0f172a",
755
- link: "#1d4ed8",
756
- },
757
- fontSizes: {
758
- m: 16,
759
- },
760
- });
369
+ // Custom highlighter
370
+ const myHighlighter: CodeHighlighter = (language, code) => {
371
+ return [{ text: code, type: "default" }];
372
+ };
761
373
 
762
- <Markdown theme={theme}>{content}</Markdown>;
374
+ <Markdown highlightCode={myHighlighter}>{content}</Markdown>
763
375
  ```
764
376
 
765
- ## Built-in Renderer Components
766
-
767
- Use these when composing custom renderer maps.
768
-
769
- | Component | Key props |
770
- | ---------------- | ------------------------------------------------ |
771
- | `Heading` | `level`, `children`, `style` |
772
- | `Paragraph` | `children`, `inListItem`, `style` |
773
- | `Link` | `href`, `children`, `style` |
774
- | `Blockquote` | `children`, `style` |
775
- | `HorizontalRule` | `style` |
776
- | `CodeBlock` | `language`, `content`, `node`, `style` |
777
- | `InlineCode` | `content`, `node`, `children`, `style` |
778
- | `List` | `ordered`, `start`, `depth`, `children`, `style` |
779
- | `ListItem` | `children`, `index`, `ordered`, `start`, `style` |
780
- | `TaskListItem` | `children`, `checked`, `style` |
781
- | `TableRenderer` | `node`, `Renderer`, `style` |
782
- | `Image` | `url`, `title`, `alt`, `Renderer`, `style` |
783
- | `MathInline` | `content`, `style` |
784
- | `MathBlock` | `content`, `style` |
785
-
786
- ## Supported AST Node Types
787
-
788
- `document`, `heading`, `paragraph`, `text`, `bold`, `italic`, `strikethrough`, `link`, `image`, `code_inline`, `code_block`, `blockquote`, `horizontal_rule`, `line_break`, `soft_break`, `table`, `table_head`, `table_body`, `table_row`, `table_cell`, `list`, `list_item`, `task_list_item`, `math_inline`, `math_block`, `html_block`, `html_inline`
789
-
790
- Notes:
791
-
792
- - `html_inline` and `html_block` are parsed but not rendered by default.
793
- - Table internals (`table_head`, `table_body`, `table_row`, `table_cell`) are renderer internals; override `table` for custom table UI.
794
-
795
- ## Recipes
796
-
797
- ### Intercept links with `onLinkPress`
377
+ ### Link Interception
798
378
 
799
379
  ```tsx
800
- import { Markdown } from "react-native-nitro-markdown";
801
-
802
380
  <Markdown
803
381
  onLinkPress={(href) => {
804
382
  if (href.startsWith("/")) {
@@ -808,61 +386,58 @@ import { Markdown } from "react-native-nitro-markdown";
808
386
  }}
809
387
  >
810
388
  {content}
811
- </Markdown>;
389
+ </Markdown>
812
390
  ```
813
391
 
814
- ### Use headless mode to build search index
392
+ Default link behavior: trims href, calls `onLinkPress`, validates scheme (`http:`, `https:`, `mailto:`, `tel:`, `sms:`), then opens via `Linking.openURL`. Relative URLs and anchors are ignored unless handled in `onLinkPress`.
815
393
 
816
- ```tsx
817
- import {
818
- parseMarkdown,
819
- getFlattenedText,
820
- } from "react-native-nitro-markdown/headless";
394
+ ## Supported Node Types
821
395
 
822
- const ast = parseMarkdown(content);
823
- const searchableText = getFlattenedText(ast);
824
- ```
396
+ `document`, `heading`, `paragraph`, `text`, `bold`, `italic`, `strikethrough`, `link`, `image`, `code_inline`, `code_block`, `blockquote`, `horizontal_rule`, `line_break`, `soft_break`, `table`, `table_head`, `table_body`, `table_row`, `table_cell`, `list`, `list_item`, `task_list_item`, `math_inline`, `math_block`, `html_block`, `html_inline`
825
397
 
826
- ### Minimal styling baseline
398
+ `html_inline` and `html_block` are parsed but not rendered by default.
827
399
 
828
- ```tsx
829
- import { Markdown } from "react-native-nitro-markdown";
400
+ ## Package Exports
830
401
 
831
- <Markdown
832
- stylingStrategy="minimal"
833
- theme={{
834
- colors: {
835
- text: "#0f172a",
836
- link: "#1d4ed8",
837
- },
838
- }}
839
- >
840
- {content}
841
- </Markdown>;
842
- ```
402
+ ### Main (`react-native-nitro-markdown`)
403
+
404
+ Components, hooks, sessions, themes, built-in renderers, syntax highlighting, and all headless APIs.
405
+
406
+ ### Headless (`react-native-nitro-markdown/headless`)
843
407
 
844
- ## Performance Guidance
408
+ Parser and text utilities only -- no React dependencies. Use this for server-side processing, search indexing, or custom renderers.
845
409
 
846
- - For streaming text, prefer `updateStrategy="raf"`.
847
- - If you use interval strategy, `updateIntervalMs` between `50` and `100` is a good baseline.
848
- - Batch `session.append(...)` calls instead of appending one character at a time.
849
- - For large markdown documents, enable `virtualize` and tune `virtualization.windowSize` / `maxToRenderPerBatch`.
850
- - Use the headless API if you do not need built-in renderers.
410
+ ## Performance Tips
411
+
412
+ - Use `updateStrategy="raf"` for streaming
413
+ - Batch `session.append()` calls (50-100ms intervals) rather than per-character
414
+ - Enable `virtualize` for large documents
415
+ - Use the headless API when you don't need built-in renderers
851
416
 
852
417
  ## Troubleshooting
853
418
 
854
- - Math appears as plain code-style fallback:
855
- - Install `react-native-mathjax-svg` and `react-native-svg`.
856
- - iOS native build issues after install:
857
- - Run `pod install` in your iOS project.
858
- - Expo app does not load native module:
859
- - Use a development build (`expo prebuild` + `expo run`), not Expo Go.
860
- - Android heading font weight looks wrong:
861
- - Set `theme.headingWeight` explicitly (for custom fonts without bold variants, use `"normal"`).
419
+ | Problem | Solution |
420
+ |---|---|
421
+ | Math renders as code-style fallback | Install `react-native-mathjax-svg` and `react-native-svg` |
422
+ | iOS build fails after install | Run `pod install` in your iOS directory |
423
+ | Expo app doesn't load native module | Use a development build (`expo prebuild` + `expo run`), not Expo Go |
424
+ | Android heading font weight looks wrong | Set `theme.headingWeight` explicitly |
425
+
426
+ ## Example App
427
+
428
+ The `apps/example` directory contains a full demo app with these screens:
429
+
430
+ | Screen | File | Demonstrates |
431
+ |---|---|---|
432
+ | Bench | `app/index.tsx` | Smoke tests + benchmark vs JS parsers |
433
+ | Default | `app/render-default.tsx` | Built-in renderer defaults |
434
+ | Styles | `app/render-default-styles.tsx` | Theme and style overrides |
435
+ | Custom | `app/render-custom.tsx` | Custom renderers + AST transform |
436
+ | Stream | `app/render-stream.tsx` | Live streaming with token append |
862
437
 
863
438
  ## Contributing
864
439
 
865
- See `CONTRIBUTING.md`.
440
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
866
441
 
867
442
  ## License
868
443