react-native-nitro-markdown 0.5.2 → 0.5.4

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 (178) hide show
  1. package/README.md +311 -669
  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/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +68 -22
  7. package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +6 -18
  8. package/cpp/bindings/HybridMarkdownParser.cpp +40 -12
  9. package/cpp/bindings/HybridMarkdownParser.hpp +4 -4
  10. package/cpp/bindings/HybridMarkdownSession.cpp +2 -0
  11. package/cpp/core/MD4CParser.cpp +147 -86
  12. package/cpp/core/MarkdownSessionCore.cpp +2 -0
  13. package/cpp/core/MarkdownTypes.hpp +1 -1
  14. package/ios/HybridMarkdownSession.swift +89 -46
  15. package/lib/commonjs/headless.js +34 -8
  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 +57 -16
  22. package/lib/commonjs/markdown.js.map +1 -1
  23. package/lib/commonjs/renderers/blockquote.js +15 -13
  24. package/lib/commonjs/renderers/blockquote.js.map +1 -1
  25. package/lib/commonjs/renderers/code.js +58 -54
  26. package/lib/commonjs/renderers/code.js.map +1 -1
  27. package/lib/commonjs/renderers/heading.js +48 -46
  28. package/lib/commonjs/renderers/heading.js.map +1 -1
  29. package/lib/commonjs/renderers/horizontal-rule.js +10 -8
  30. package/lib/commonjs/renderers/horizontal-rule.js.map +1 -1
  31. package/lib/commonjs/renderers/image.js +18 -4
  32. package/lib/commonjs/renderers/image.js.map +1 -1
  33. package/lib/commonjs/renderers/link.js +7 -2
  34. package/lib/commonjs/renderers/link.js.map +1 -1
  35. package/lib/commonjs/renderers/list.js +75 -68
  36. package/lib/commonjs/renderers/list.js.map +1 -1
  37. package/lib/commonjs/renderers/math.js +8 -5
  38. package/lib/commonjs/renderers/math.js.map +1 -1
  39. package/lib/commonjs/renderers/paragraph.js +15 -13
  40. package/lib/commonjs/renderers/paragraph.js.map +1 -1
  41. package/lib/commonjs/renderers/style-cache.js +14 -0
  42. package/lib/commonjs/renderers/style-cache.js.map +1 -0
  43. package/lib/commonjs/renderers/table/cell-content.js +1 -1
  44. package/lib/commonjs/renderers/table/cell-content.js.map +1 -1
  45. package/lib/commonjs/renderers/table/index.js +17 -6
  46. package/lib/commonjs/renderers/table/index.js.map +1 -1
  47. package/lib/commonjs/theme.js +7 -7
  48. package/lib/commonjs/theme.js.map +1 -1
  49. package/lib/commonjs/utils/code-highlight.js +24 -25
  50. package/lib/commonjs/utils/code-highlight.js.map +1 -1
  51. package/lib/module/headless.js +35 -7
  52. package/lib/module/headless.js.map +1 -1
  53. package/lib/module/index.js +1 -1
  54. package/lib/module/index.js.map +1 -1
  55. package/lib/module/markdown-stream.js +1 -1
  56. package/lib/module/markdown-stream.js.map +1 -1
  57. package/lib/module/markdown.js +58 -17
  58. package/lib/module/markdown.js.map +1 -1
  59. package/lib/module/renderers/blockquote.js +15 -13
  60. package/lib/module/renderers/blockquote.js.map +1 -1
  61. package/lib/module/renderers/code.js +58 -54
  62. package/lib/module/renderers/code.js.map +1 -1
  63. package/lib/module/renderers/heading.js +48 -46
  64. package/lib/module/renderers/heading.js.map +1 -1
  65. package/lib/module/renderers/horizontal-rule.js +10 -8
  66. package/lib/module/renderers/horizontal-rule.js.map +1 -1
  67. package/lib/module/renderers/image.js +19 -5
  68. package/lib/module/renderers/image.js.map +1 -1
  69. package/lib/module/renderers/link.js +7 -2
  70. package/lib/module/renderers/link.js.map +1 -1
  71. package/lib/module/renderers/list.js +75 -68
  72. package/lib/module/renderers/list.js.map +1 -1
  73. package/lib/module/renderers/math.js +8 -5
  74. package/lib/module/renderers/math.js.map +1 -1
  75. package/lib/module/renderers/paragraph.js +15 -13
  76. package/lib/module/renderers/paragraph.js.map +1 -1
  77. package/lib/module/renderers/style-cache.js +10 -0
  78. package/lib/module/renderers/style-cache.js.map +1 -0
  79. package/lib/module/renderers/table/cell-content.js +1 -1
  80. package/lib/module/renderers/table/cell-content.js.map +1 -1
  81. package/lib/module/renderers/table/index.js +17 -6
  82. package/lib/module/renderers/table/index.js.map +1 -1
  83. package/lib/module/theme.js +7 -7
  84. package/lib/module/theme.js.map +1 -1
  85. package/lib/module/utils/code-highlight.js +24 -25
  86. package/lib/module/utils/code-highlight.js.map +1 -1
  87. package/lib/typescript/commonjs/Markdown.nitro.d.ts +1 -0
  88. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/headless.d.ts +10 -2
  90. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/index.d.ts +3 -2
  92. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/markdown.d.ts +8 -3
  95. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/renderers/blockquote.d.ts +1 -1
  97. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/renderers/heading.d.ts +1 -1
  100. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +1 -1
  102. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/renderers/list.d.ts +1 -1
  106. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/renderers/math.d.ts +1 -1
  108. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/renderers/paragraph.d.ts +1 -1
  110. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/renderers/style-cache.d.ts +3 -0
  112. package/lib/typescript/commonjs/renderers/style-cache.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +4 -3
  114. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/utils/code-highlight.d.ts +1 -1
  118. package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -1
  119. package/lib/typescript/module/Markdown.nitro.d.ts +1 -0
  120. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  121. package/lib/typescript/module/headless.d.ts +10 -2
  122. package/lib/typescript/module/headless.d.ts.map +1 -1
  123. package/lib/typescript/module/index.d.ts +3 -2
  124. package/lib/typescript/module/index.d.ts.map +1 -1
  125. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  126. package/lib/typescript/module/markdown.d.ts +8 -3
  127. package/lib/typescript/module/markdown.d.ts.map +1 -1
  128. package/lib/typescript/module/renderers/blockquote.d.ts +1 -1
  129. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  130. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  131. package/lib/typescript/module/renderers/heading.d.ts +1 -1
  132. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  133. package/lib/typescript/module/renderers/horizontal-rule.d.ts +1 -1
  134. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  135. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  136. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  137. package/lib/typescript/module/renderers/list.d.ts +1 -1
  138. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  139. package/lib/typescript/module/renderers/math.d.ts +1 -1
  140. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  141. package/lib/typescript/module/renderers/paragraph.d.ts +1 -1
  142. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  143. package/lib/typescript/module/renderers/style-cache.d.ts +3 -0
  144. package/lib/typescript/module/renderers/style-cache.d.ts.map +1 -0
  145. package/lib/typescript/module/renderers/table/cell-content.d.ts +4 -3
  146. package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -1
  147. package/lib/typescript/module/renderers/table/index.d.ts.map +1 -1
  148. package/lib/typescript/module/theme.d.ts.map +1 -1
  149. package/lib/typescript/module/utils/code-highlight.d.ts +1 -1
  150. package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -1
  151. package/nitro.json +12 -3
  152. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -2
  153. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  154. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
  155. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +2 -2
  156. package/nitrogen/generated/ios/NitroMarkdown+autolinking.rb +2 -0
  157. package/nitrogen/generated/shared/c++/ParserOptions.hpp +6 -2
  158. package/package.json +8 -6
  159. package/react-native-nitro-markdown.podspec +3 -0
  160. package/src/Markdown.nitro.ts +1 -0
  161. package/src/headless.ts +58 -8
  162. package/src/index.ts +16 -2
  163. package/src/markdown-stream.tsx +1 -0
  164. package/src/markdown.tsx +108 -23
  165. package/src/renderers/blockquote.tsx +22 -17
  166. package/src/renderers/code.tsx +76 -57
  167. package/src/renderers/heading.tsx +60 -54
  168. package/src/renderers/horizontal-rule.tsx +17 -12
  169. package/src/renderers/image.tsx +24 -5
  170. package/src/renderers/link.tsx +8 -2
  171. package/src/renderers/list.tsx +93 -74
  172. package/src/renderers/math.tsx +14 -5
  173. package/src/renderers/paragraph.tsx +22 -17
  174. package/src/renderers/style-cache.ts +14 -0
  175. package/src/renderers/table/cell-content.tsx +15 -4
  176. package/src/renderers/table/index.tsx +30 -13
  177. package/src/theme.ts +34 -14
  178. package/src/utils/code-highlight.ts +133 -44
package/README.md CHANGED
@@ -4,59 +4,57 @@
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
+ - **Opt-in HTML AST nodes** -- preserve raw HTML as `html_inline` / `html_block` for custom renderers
16
+ - **Large document support** -- top-level block virtualization and cached renderer styles
17
+ - **Code highlighting** -- built-in JS/TS, Python, and Bash tokenizer with custom highlighter support
18
+ - **Headless API** -- parse markdown and extract text without any UI
19
+ - **Streaming** -- incremental rendering for chat/LLM token streams
20
+ - **Customizable** -- themes, per-node style overrides, custom renderers, AST transforms, plugin pipeline
18
21
 
19
22
  ## Requirements
20
23
 
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`
24
+ | Dependency | Version |
25
+ |---|---|
26
+ | React Native | `>=0.75.0` |
27
+ | react-native-nitro-modules | `>=0.35.4` |
28
+ | react-native-mathjax-svg *(optional)* | `>=0.9.0` |
29
+ | react-native-svg *(optional, for math)* | `>=13.0.0` |
28
30
 
29
31
  ## Installation
30
32
 
31
- ### React Native
33
+ With npm:
32
34
 
33
35
  ```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
36
+ npm install react-native-nitro-markdown react-native-nitro-modules
37
+ cd ios && pod install
41
38
  ```
42
39
 
43
- iOS pods:
40
+ With Bun:
44
41
 
45
42
  ```bash
43
+ bun add react-native-nitro-markdown react-native-nitro-modules
46
44
  cd ios && pod install
47
45
  ```
48
46
 
49
- ### Expo (development build)
47
+ For math rendering:
50
48
 
51
49
  ```bash
52
- bunx expo install react-native-nitro-markdown react-native-nitro-modules
53
- bunx expo prebuild
50
+ npm install react-native-mathjax-svg react-native-svg
54
51
  ```
55
52
 
56
- Optional math support:
53
+ **Expo** (development build):
57
54
 
58
55
  ```bash
59
- bunx expo install react-native-mathjax-svg react-native-svg
56
+ npx expo install react-native-nitro-markdown react-native-nitro-modules
57
+ npx expo prebuild
60
58
  ```
61
59
 
62
60
  ## Quick Start
@@ -73,343 +71,248 @@ export function Example() {
73
71
  }
74
72
  ```
75
73
 
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 |
74
+ ## API Reference
148
75
 
149
- ## Package Exports
150
-
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`
76
+ ### Parser Options
200
77
 
201
- ```tsx
202
- import { Markdown } from "react-native-nitro-markdown";
203
- ```
78
+ `ParserOptions` controls native parser extensions. Defaults are conservative for HTML and feature-complete for Markdown extensions.
204
79
 
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)
80
+ | Option | Default | Enables | Notes |
81
+ |---|---:|---|---|
82
+ | `gfm` | `true` | Tables, strikethrough, task lists, permissive autolinks | Set `false` for stricter CommonMark-style parsing |
83
+ | `math` | `true` | Inline and display LaTeX spans | Rendering falls back to plain styled text unless optional math deps are installed |
84
+ | `html` | `false` | `html_inline` and `html_block` AST nodes | Raw HTML is not rendered by default; provide custom renderers |
246
85
 
247
86
  ```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
- >
87
+ <Markdown options={{ gfm: true, math: true, html: false }}>
259
88
  {content}
260
89
  </Markdown>
261
90
  ```
262
91
 
263
- Virtualization notes:
264
-
265
- - Keep `Markdown` as the primary vertical scroller when `virtualize` is enabled.
266
- - Avoid nesting it inside another vertical `ScrollView`, or virtualization effectiveness drops.
92
+ ### `<Markdown>`
267
93
 
268
- ### AST transform example
94
+ The main component. Parses a markdown string and renders it.
269
95
 
270
96
  ```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
- });
97
+ import { Markdown } from "react-native-nitro-markdown";
98
+ ```
283
99
 
284
- return transformNode(ast);
285
- }, []);
100
+ | Prop | Type | Default | Description |
101
+ |---|---|---|---|
102
+ | `children` | `string` | required | Markdown input string |
103
+ | `options` | `ParserOptions` | -- | Parser flags (`gfm`, `math`, `html`) |
104
+ | `plugins` | `MarkdownPlugin[]` | -- | Plugin hooks (`beforeParse`, `afterParse`) |
105
+ | `sourceAst` | `MarkdownNode` | -- | Pre-parsed AST; skips native parse when provided |
106
+ | `astTransform` | `AstTransform` | -- | Post-parse AST rewrite before render |
107
+ | `renderers` | `CustomRenderers` | `{}` | Per-node custom renderer overrides |
108
+ | `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` | Theme token overrides |
109
+ | `styles` | `NodeStyleOverrides` | -- | Per-node style overrides |
110
+ | `stylingStrategy` | `"opinionated" \| "minimal"` | `"opinionated"` | Base styling preset |
111
+ | `style` | `StyleProp<ViewStyle>` | -- | Container style |
112
+ | `onLinkPress` | `LinkPressHandler` | -- | Intercept link presses; return `false` to block default open |
113
+ | `onParsingInProgress` | `() => void` | -- | Called when parse inputs change |
114
+ | `onParseComplete` | `(result) => void` | -- | Called with `{ raw, ast, text }` after parse |
115
+ | `onError` | `(error, phase, pluginName?) => void` | -- | Error handler for parse/plugin failures |
116
+ | `highlightCode` | `boolean \| CodeHighlighter` | -- | Enable syntax highlighting for code blocks |
117
+ | `tableOptions` | `{ minColumnWidth?; measurementStabilizeMs? }` | -- | Table layout tuning |
118
+ | `virtualize` | `boolean \| "auto"` | `false` | Top-level block virtualization |
119
+ | `virtualizationMinBlocks` | `number` | `40` | Block threshold for `"auto"` virtualization |
120
+ | `virtualization` | `MarkdownVirtualizationOptions` | -- | FlatList tuning (windowSize, batching, etc.) |
121
+
122
+ **Pipeline order:** `beforeParse` plugins (by priority desc) -> parse/sourceAst -> `afterParse` plugins (by priority desc) -> `astTransform` -> render.
123
+
124
+ ### `<MarkdownStream>`
125
+
126
+ Renders markdown from a streaming session. Extends `MarkdownProps` (minus `children`).
286
127
 
287
- <Markdown astTransform={astTransform}>{"Hello :wink:"}</Markdown>;
128
+ ```tsx
129
+ import { MarkdownStream } from "react-native-nitro-markdown";
288
130
  ```
289
131
 
290
- ### Plugin pipeline example (`beforeParse` + `afterParse`)
132
+ | Prop | Type | Default | Description |
133
+ |---|---|---|---|
134
+ | `session` | `MarkdownSession` | required | Session supplying streamed text |
135
+ | `updateIntervalMs` | `number` | `50` | Flush interval for `"interval"` strategy |
136
+ | `updateStrategy` | `"interval" \| "raf"` | `"interval"` | Update cadence |
137
+ | `useTransitionUpdates` | `boolean` | `false` | Wrap updates in `startTransition` |
138
+ | `incrementalParsing` | `boolean` | `true` | Append-optimized incremental AST updates |
291
139
 
292
- ```tsx
293
- import { Markdown, type MarkdownPlugin } from "react-native-nitro-markdown";
140
+ ### `MarkdownSession`
294
141
 
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
- ];
142
+ A native text buffer with change listeners, used for streaming.
316
143
 
317
- <Markdown plugins={plugins}>{"Launch :rocket:"}</Markdown>;
144
+ ```tsx
145
+ import { createMarkdownSession } from "react-native-nitro-markdown";
146
+
147
+ const session = createMarkdownSession();
148
+ session.append("# Hello\n");
149
+ session.append("Streaming content...");
318
150
  ```
319
151
 
320
- ### `onError` example
152
+ | Method | Signature | Description |
153
+ |---|---|---|
154
+ | `append` | `(chunk: string) => number` | Append text, returns new UTF-16 length |
155
+ | `clear` | `() => void` | Clear buffer, emit reset event |
156
+ | `reset` | `(text: string) => void` | Replace full buffer content |
157
+ | `replace` | `(from, to, text) => number` | Partial buffer mutation |
158
+ | `getAllText` | `() => string` | Get full session text |
159
+ | `getLength` | `() => number` | Get UTF-16 length without copy |
160
+ | `getTextRange` | `(from, to) => string` | Get substring range |
161
+ | `addListener` | `(listener) => () => void` | Subscribe to mutation events; returns unsubscribe |
162
+ | `highlightPosition` | `number` | Mutable cursor for stream highlight |
163
+
164
+ ### `useMarkdownSession()`
165
+
166
+ Creates and owns a `MarkdownSession` for a component lifecycle.
321
167
 
322
168
  ```tsx
323
- import { Markdown } from "react-native-nitro-markdown";
169
+ import { useMarkdownSession } from "react-native-nitro-markdown";
324
170
 
325
- <Markdown
326
- onError={(error, phase, pluginName) => {
327
- console.warn(`[markdown] ${phase} error`, { error, pluginName });
328
- }}
329
- plugins={plugins}
330
- >
331
- {content}
332
- </Markdown>;
171
+ const { getSession, isStreaming, setIsStreaming, stop, clear, setHighlight } =
172
+ useMarkdownSession();
333
173
  ```
334
174
 
335
- ### Syntax highlighting example
175
+ ### `useStream(timestamps?)`
176
+
177
+ Extends `useMarkdownSession` with timeline sync for timed playback.
336
178
 
337
179
  ```tsx
338
- import { Markdown } from "react-native-nitro-markdown";
180
+ import { useStream } from "react-native-nitro-markdown";
339
181
 
340
- // Built-in highlighter (JS/TS, Python, Bash)
341
- <Markdown highlightCode>{"```typescript\nconst x: number = 42;\n```"}</Markdown>;
182
+ const stream = useStream({ 0: 0, 1: 500, 2: 1000 });
183
+ stream.sync(currentTimeMs);
342
184
 
343
- // Custom highlighter
344
- import type { CodeHighlighter } from "react-native-nitro-markdown";
185
+ <MarkdownStream session={stream.getSession()} />;
186
+ ```
345
187
 
346
- const myHighlighter: CodeHighlighter = (language, code) => {
347
- // return an array of { text, type } tokens
348
- return [{ text: code, type: "default" }];
349
- };
188
+ ### Headless API
189
+
190
+ Parse markdown without any UI. Available from both entry points.
350
191
 
351
- <Markdown highlightCode={myHighlighter}>{content}</Markdown>;
192
+ ```tsx
193
+ import {
194
+ parseMarkdown,
195
+ parseMarkdownWithOptions,
196
+ extractPlainText,
197
+ getTextContent,
198
+ getFlattenedText,
199
+ stripSourceOffsets,
200
+ } from "react-native-nitro-markdown/headless";
352
201
  ```
353
202
 
354
- ### `sourceAst` example (skip parsing in render)
203
+ | Function | Description |
204
+ |---|---|
205
+ | `parseMarkdown(text, options?)` | Parse to AST (options: `{ gfm?, math?, html? }`) |
206
+ | `parseMarkdownWithOptions(text, options)` | Parse with explicit options |
207
+ | `extractPlainText(text)` | Parse and return plain text from native parser |
208
+ | `extractPlainTextWithOptions(text, options)` | Same with parser flags |
209
+ | `getTextContent(node)` | Concatenate text recursively (no normalization) |
210
+ | `getFlattenedText(node)` | Normalized plain text with block separators |
211
+ | `stripSourceOffsets(node)` | Remove `beg`/`end` fields from AST |
212
+
213
+ ### Custom Renderers
214
+
215
+ Override rendering for specific node types:
355
216
 
356
217
  ```tsx
357
218
  import {
358
219
  Markdown,
359
- parseMarkdownWithOptions,
220
+ type HeadingRendererProps,
221
+ type CodeBlockRendererProps,
360
222
  } from "react-native-nitro-markdown";
361
223
 
362
- const sourceAst = parseMarkdownWithOptions(content, { gfm: true, math: true });
363
-
364
- <Markdown sourceAst={sourceAst}>
365
- {"children is ignored when sourceAst is provided"}
224
+ <Markdown
225
+ renderers={{
226
+ heading: ({ level, children }: HeadingRendererProps) => (
227
+ <MyHeading level={level}>{children}</MyHeading>
228
+ ),
229
+ code_block: ({ language, content }: CodeBlockRendererProps) => (
230
+ <MyCode language={language} content={content} />
231
+ ),
232
+ }}
233
+ >
234
+ {content}
366
235
  </Markdown>;
367
236
  ```
368
237
 
369
- ### Parse lifecycle callbacks example
238
+ Renderers receive `EnhancedRendererProps` with `node`, `children`, and `Renderer` (for recursive rendering). Node-specific props are mapped automatically:
239
+
240
+ | Node type | Extra props |
241
+ |---|---|
242
+ | `heading` | `level` |
243
+ | `link` | `href`, `title` |
244
+ | `image` | `url`, `alt`, `title` |
245
+ | `code_block` | `content`, `language` |
246
+ | `code_inline` | `content` |
247
+ | `list` | `ordered`, `start` |
248
+ | `task_list_item` | `checked` |
249
+ | `html_inline`, `html_block` | Read raw HTML from `node.content` |
250
+
251
+ Return `undefined` to fall back to the built-in renderer, or `null` to render nothing.
252
+
253
+ #### Rendering HTML Nodes
254
+
255
+ HTML parsing is opt-in and produces raw AST nodes. The built-in renderer intentionally renders nothing for `html_inline` and `html_block`; applications decide what is safe to display.
370
256
 
371
257
  ```tsx
258
+ import { Text, View } from "react-native";
372
259
  import { Markdown } from "react-native-nitro-markdown";
373
260
 
374
261
  <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);
262
+ options={{ html: true }}
263
+ renderers={{
264
+ html_inline: ({ node }) => {
265
+ if (node.content === "<br>") return <Text>{"\n"}</Text>;
266
+ return null;
267
+ },
268
+ html_block: ({ node }) => {
269
+ if (!node.content?.includes('data-kind="release-note"')) return null;
270
+ return (
271
+ <View>
272
+ <Text>Release note</Text>
273
+ </View>
274
+ );
275
+ },
381
276
  }}
382
277
  >
383
278
  {content}
384
279
  </Markdown>;
385
280
  ```
386
281
 
387
- ## `MarkdownStream`
282
+ Do not pass untrusted HTML directly into a WebView from a renderer. Sanitize or map known tags/attributes to native components.
388
283
 
389
- ```tsx
390
- import { MarkdownStream } from "react-native-nitro-markdown";
391
- ```
284
+ ### Theme API
392
285
 
393
- `MarkdownStreamProps` extends `MarkdownProps` except `children`.
286
+ ```tsx
287
+ import {
288
+ Markdown,
289
+ defaultMarkdownTheme,
290
+ minimalMarkdownTheme,
291
+ mergeThemes,
292
+ } from "react-native-nitro-markdown";
394
293
 
395
- Demo usage:
294
+ const theme = mergeThemes(defaultMarkdownTheme, {
295
+ colors: { text: "#0f172a", link: "#1d4ed8" },
296
+ fontSizes: { m: 16 },
297
+ });
396
298
 
397
- - `apps/example/app/render-stream.tsx`
299
+ <Markdown theme={theme}>{content}</Markdown>;
300
+ ```
398
301
 
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). |
302
+ `MarkdownTheme` includes:
303
+ - **colors** -- `text`, `heading`, `link`, `code`, `codeBackground`, `blockquote`, `border`, `surface`, table colors, and optional `codeTokenColors` for syntax highlighting
304
+ - **spacing** -- `xs`, `s`, `m`, `l`, `xl`
305
+ - **fontSizes** -- `xs` through `xl`, plus `h1`-`h6`
306
+ - **fontFamilies** -- `regular`, `heading`, `mono`
307
+ - **borderRadius** -- `s`, `m`, `l`
308
+ - **headingWeight** -- optional font weight override
309
+ - **showCodeLanguage** -- show/hide language label on code blocks
406
310
 
407
- Notes:
311
+ Use `stylingStrategy="minimal"` for a bare baseline, or `NodeStyleOverrides` for per-node style overrides (text nodes accept `TextStyle`, container nodes accept `ViewStyle`).
408
312
 
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.
313
+ ## Examples
411
314
 
412
- ### Streaming Example
315
+ ### Streaming
413
316
 
414
317
  ```tsx
415
318
  import { useEffect } from "react";
@@ -425,7 +328,6 @@ export function StreamingExample() {
425
328
  const s = session.getSession();
426
329
  s.append("# Streaming\n");
427
330
  s.append("This text arrives in chunks.");
428
-
429
331
  return () => session.clear();
430
332
  }, [session]);
431
333
 
@@ -434,371 +336,113 @@ export function StreamingExample() {
434
336
  session={session.getSession()}
435
337
  options={{ gfm: true }}
436
338
  updateStrategy="raf"
437
- useTransitionUpdates
438
339
  />
439
340
  );
440
341
  }
441
342
  ```
442
343
 
443
- ## Hooks and Session API
444
-
445
- ## `createMarkdownSession()`
446
-
447
- Creates and returns a native `MarkdownSession` instance.
344
+ ### AST Transform
448
345
 
449
346
  ```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";
347
+ import { useCallback } from "react";
348
+ import { Markdown, type AstTransform } from "react-native-nitro-markdown";
481
349
 
482
- const session = createMarkdownSession();
483
- session.append("# Hello\n");
484
- session.append("Streaming content...");
350
+ const transform = useCallback<AstTransform>((ast) => {
351
+ const visit = (node: Parameters<AstTransform>[0]): typeof node => ({
352
+ ...node,
353
+ content:
354
+ node.type === "text"
355
+ ? (node.content ?? "").replace(/:wink:/g, "😉")
356
+ : node.content,
357
+ children: node.children?.map(visit),
358
+ });
359
+ return visit(ast);
360
+ }, []);
485
361
 
486
- <MarkdownStream session={session} updateStrategy="raf" />;
362
+ <Markdown astTransform={transform}>{"Hello :wink:"}</Markdown>;
487
363
  ```
488
364
 
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:
365
+ ### Plugin Pipeline
525
366
 
526
367
  ```tsx
527
- const stream = useStream({
528
- 0: 0,
529
- 1: 500,
530
- 2: 1000,
531
- });
368
+ import { Markdown, type MarkdownPlugin } from "react-native-nitro-markdown";
532
369
 
533
- // e.g. in media time update callback:
534
- stream.sync(currentTimeMs);
370
+ const plugins: MarkdownPlugin[] = [
371
+ {
372
+ name: "rewrite-before-parse",
373
+ priority: 10,
374
+ beforeParse: (input) => input.replace(/:rocket:/g, "ROCKET_TOKEN"),
375
+ },
376
+ {
377
+ name: "rewrite-after-parse",
378
+ afterParse: (ast) => {
379
+ const visit = (node: typeof ast): typeof ast => ({
380
+ ...node,
381
+ content:
382
+ node.type === "text"
383
+ ? (node.content ?? "").replace(/ROCKET_TOKEN/g, "🚀")
384
+ : node.content,
385
+ children: node.children?.map(visit),
386
+ });
387
+ return visit(ast);
388
+ },
389
+ },
390
+ ];
535
391
 
536
- <MarkdownStream session={stream.getSession()} />;
392
+ <Markdown plugins={plugins}>{"Launch :rocket:"}</Markdown>;
537
393
  ```
538
394
 
539
- Demo usage:
540
-
541
- - README examples (timed playback scenario)
542
-
543
- ## Headless API
395
+ ### Pre-parsed AST
544
396
 
545
397
  ```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
- ```
575
-
576
- Example:
398
+ import { Markdown, parseMarkdownWithOptions } from "react-native-nitro-markdown";
577
399
 
578
- ```tsx
579
- const ast = parseMarkdownWithOptions(markdown, {
580
- gfm: true, // tables, task lists, strikethrough
581
- math: true, // inline/block LaTeX
400
+ const ast = parseMarkdownWithOptions(content, {
401
+ gfm: true,
402
+ math: true,
403
+ html: false,
582
404
  });
583
- ```
584
-
585
- ### `MarkdownParserModule` (low-level Nitro access)
586
-
587
- Use this only when you want direct method access (`parse`, `parseWithOptions`, `extractPlainText`, `extractPlainTextWithOptions`).
588
-
589
- ```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
604
-
605
- `CustomRenderers` is:
606
-
607
- ```ts
608
- type CustomRenderers = Partial<Record<MarkdownNode["type"], CustomRenderer>>;
609
- ```
610
-
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
636
-
637
- ```tsx
638
- import {
639
- Markdown,
640
- type HeadingRendererProps,
641
- type CodeBlockRendererProps,
642
- } from "react-native-nitro-markdown";
643
-
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
- };
652
405
 
653
- <Markdown renderers={renderers}>{content}</Markdown>;
406
+ <Markdown sourceAst={ast}>{"ignored when sourceAst is provided"}</Markdown>;
654
407
  ```
655
408
 
656
- ### `useMarkdownContext` example (inside custom renderer tree)
409
+ ### Virtualization (large documents)
657
410
 
658
411
  ```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
412
  <Markdown
672
- renderers={{
673
- paragraph: ({ children }: CustomRendererProps) => (
674
- <ThemedParagraph>{children}</ThemedParagraph>
675
- ),
413
+ virtualize="auto"
414
+ virtualizationMinBlocks={30}
415
+ virtualization={{
416
+ initialNumToRender: 10,
417
+ maxToRenderPerBatch: 10,
418
+ windowSize: 8,
676
419
  }}
677
420
  >
678
421
  {content}
679
- </Markdown>;
422
+ </Markdown>
680
423
  ```
681
424
 
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
425
+ Keep `Markdown` as the primary vertical scroller when virtualization is enabled -- avoid nesting inside another `ScrollView`.
700
426
 
701
- ## `MarkdownTheme`
427
+ ### Syntax Highlighting
702
428
 
703
429
  ```tsx
704
- import type {
705
- MarkdownTheme,
706
- PartialMarkdownTheme,
707
- } from "react-native-nitro-markdown";
708
- ```
709
-
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
- ```
742
-
743
- Example with `mergeThemes`:
430
+ import type { CodeHighlighter } from "react-native-nitro-markdown";
744
431
 
745
- ```tsx
746
- import {
747
- Markdown,
748
- defaultMarkdownTheme,
749
- mergeThemes,
750
- } from "react-native-nitro-markdown";
432
+ // Built-in highlighter (JS/TS, Python, Bash)
433
+ <Markdown highlightCode>{"```typescript\nconst x: number = 42;\n```"}</Markdown>
751
434
 
752
- const theme = mergeThemes(defaultMarkdownTheme, {
753
- colors: {
754
- text: "#0f172a",
755
- link: "#1d4ed8",
756
- },
757
- fontSizes: {
758
- m: 16,
759
- },
760
- });
435
+ // Custom highlighter
436
+ const myHighlighter: CodeHighlighter = (language, code) => {
437
+ return [{ text: code, type: "default" }];
438
+ };
761
439
 
762
- <Markdown theme={theme}>{content}</Markdown>;
440
+ <Markdown highlightCode={myHighlighter}>{content}</Markdown>
763
441
  ```
764
442
 
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`
443
+ ### Link Interception
798
444
 
799
445
  ```tsx
800
- import { Markdown } from "react-native-nitro-markdown";
801
-
802
446
  <Markdown
803
447
  onLinkPress={(href) => {
804
448
  if (href.startsWith("/")) {
@@ -808,61 +452,59 @@ import { Markdown } from "react-native-nitro-markdown";
808
452
  }}
809
453
  >
810
454
  {content}
811
- </Markdown>;
455
+ </Markdown>
812
456
  ```
813
457
 
814
- ### Use headless mode to build search index
458
+ 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
459
 
816
- ```tsx
817
- import {
818
- parseMarkdown,
819
- getFlattenedText,
820
- } from "react-native-nitro-markdown/headless";
460
+ ## Supported Node Types
821
461
 
822
- const ast = parseMarkdown(content);
823
- const searchableText = getFlattenedText(ast);
824
- ```
462
+ `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
463
 
826
- ### Minimal styling baseline
464
+ `html_inline` and `html_block` are parsed only when `options.html` is `true`; they are not rendered by default. Use a custom renderer to handle them, and read raw HTML from `node.content`.
827
465
 
828
- ```tsx
829
- import { Markdown } from "react-native-nitro-markdown";
466
+ ## Package Exports
830
467
 
831
- <Markdown
832
- stylingStrategy="minimal"
833
- theme={{
834
- colors: {
835
- text: "#0f172a",
836
- link: "#1d4ed8",
837
- },
838
- }}
839
- >
840
- {content}
841
- </Markdown>;
842
- ```
468
+ ### Main (`react-native-nitro-markdown`)
469
+
470
+ Components, hooks, sessions, themes, built-in renderers, syntax highlighting, and all headless APIs.
843
471
 
844
- ## Performance Guidance
472
+ ### Headless (`react-native-nitro-markdown/headless`)
845
473
 
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.
474
+ Parser and text utilities only -- no React dependencies. Use this for server-side processing, search indexing, or custom renderers.
475
+
476
+ ## Performance Tips
477
+
478
+ - Use `updateStrategy="raf"` for streaming UI updates.
479
+ - Batch `session.append()` calls at 50-100ms intervals rather than per-character.
480
+ - Enable `virtualize="auto"` for long documents and keep `Markdown` as the primary vertical scroller.
481
+ - Memoize custom `plugins`, `renderers`, `theme`, and `styles` objects when they are created inside a component.
482
+ - Use the headless API when you only need parsing, plain text extraction, search indexing, or a custom renderer.
851
483
 
852
484
  ## Troubleshooting
853
485
 
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"`).
486
+ | Problem | Solution |
487
+ |---|---|
488
+ | Math renders as code-style fallback | Install `react-native-mathjax-svg` and `react-native-svg` |
489
+ | iOS build fails after install | Run `pod install` in your iOS directory |
490
+ | Expo app doesn't load native module | Use a development build (`expo prebuild` + `expo run`), not Expo Go |
491
+ | Android heading font weight looks wrong | Set `theme.headingWeight` explicitly |
492
+
493
+ ## Example App
494
+
495
+ The `apps/example` directory contains a full demo app with these screens:
496
+
497
+ | Screen | File | Demonstrates |
498
+ |---|---|---|
499
+ | Bench | `app/index.tsx` | Smoke tests + benchmark vs JS parsers |
500
+ | Default | `app/render-default.tsx` | Built-in renderer defaults |
501
+ | Styles | `app/render-default-styles.tsx` | Theme and style overrides |
502
+ | Custom | `app/render-custom.tsx` | HTML AST rendering, custom renderers, AST transform |
503
+ | Stream | `app/render-stream.tsx` | Live streaming with token append |
862
504
 
863
505
  ## Contributing
864
506
 
865
- See `CONTRIBUTING.md`.
507
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
866
508
 
867
509
  ## License
868
510