react-native-nitro-markdown 0.4.2 → 0.5.0

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 (191) hide show
  1. package/README.md +605 -318
  2. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +27 -8
  3. package/cpp/bindings/HybridMarkdownParser.cpp +216 -66
  4. package/cpp/bindings/HybridMarkdownParser.hpp +2 -0
  5. package/ios/HybridMarkdownSession.swift +33 -7
  6. package/lib/commonjs/MarkdownContext.js +2 -1
  7. package/lib/commonjs/MarkdownContext.js.map +1 -1
  8. package/lib/commonjs/headless.js +41 -5
  9. package/lib/commonjs/headless.js.map +1 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/markdown-stream.js +109 -13
  12. package/lib/commonjs/markdown-stream.js.map +1 -1
  13. package/lib/commonjs/markdown.js +215 -44
  14. package/lib/commonjs/markdown.js.map +1 -1
  15. package/lib/commonjs/renderers/code.js +4 -3
  16. package/lib/commonjs/renderers/code.js.map +1 -1
  17. package/lib/commonjs/renderers/heading.js +1 -1
  18. package/lib/commonjs/renderers/heading.js.map +1 -1
  19. package/lib/commonjs/renderers/image.js +7 -5
  20. package/lib/commonjs/renderers/image.js.map +1 -1
  21. package/lib/commonjs/renderers/link.js +15 -3
  22. package/lib/commonjs/renderers/link.js.map +1 -1
  23. package/lib/commonjs/renderers/list.js +2 -2
  24. package/lib/commonjs/renderers/list.js.map +1 -1
  25. package/lib/commonjs/renderers/table.js +126 -21
  26. package/lib/commonjs/renderers/table.js.map +1 -1
  27. package/lib/commonjs/use-markdown-stream.js +16 -14
  28. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  29. package/lib/commonjs/utils/incremental-ast.js +153 -0
  30. package/lib/commonjs/utils/incremental-ast.js.map +1 -0
  31. package/lib/commonjs/utils/link-security.js +21 -0
  32. package/lib/commonjs/utils/link-security.js.map +1 -0
  33. package/lib/commonjs/utils/stream-timeline.js +62 -0
  34. package/lib/commonjs/utils/stream-timeline.js.map +1 -0
  35. package/lib/module/MarkdownContext.js +2 -1
  36. package/lib/module/MarkdownContext.js.map +1 -1
  37. package/lib/module/headless.js +37 -4
  38. package/lib/module/headless.js.map +1 -1
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/markdown-stream.js +110 -14
  41. package/lib/module/markdown-stream.js.map +1 -1
  42. package/lib/module/markdown.js +217 -46
  43. package/lib/module/markdown.js.map +1 -1
  44. package/lib/module/renderers/blockquote.js.map +1 -1
  45. package/lib/module/renderers/code.js +4 -3
  46. package/lib/module/renderers/code.js.map +1 -1
  47. package/lib/module/renderers/heading.js +1 -1
  48. package/lib/module/renderers/heading.js.map +1 -1
  49. package/lib/module/renderers/image.js +7 -5
  50. package/lib/module/renderers/image.js.map +1 -1
  51. package/lib/module/renderers/link.js +15 -3
  52. package/lib/module/renderers/link.js.map +1 -1
  53. package/lib/module/renderers/list.js +2 -2
  54. package/lib/module/renderers/list.js.map +1 -1
  55. package/lib/module/renderers/paragraph.js.map +1 -1
  56. package/lib/module/renderers/table.js +127 -22
  57. package/lib/module/renderers/table.js.map +1 -1
  58. package/lib/module/use-markdown-stream.js +16 -14
  59. package/lib/module/use-markdown-stream.js.map +1 -1
  60. package/lib/module/utils/incremental-ast.js +147 -0
  61. package/lib/module/utils/incremental-ast.js.map +1 -0
  62. package/lib/module/utils/link-security.js +15 -0
  63. package/lib/module/utils/link-security.js.map +1 -0
  64. package/lib/module/utils/stream-timeline.js +56 -0
  65. package/lib/module/utils/stream-timeline.js.map +1 -0
  66. package/lib/typescript/commonjs/Markdown.nitro.d.ts +5 -3
  67. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
  69. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/headless.d.ts +15 -2
  71. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/index.d.ts +3 -1
  73. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/markdown-stream.d.ts +7 -2
  75. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/markdown.d.ts +62 -5
  77. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
  79. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
  81. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
  83. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
  85. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
  87. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
  89. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
  91. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
  93. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
  95. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
  97. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +5 -2
  99. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/theme.d.ts +2 -2
  101. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
  104. package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
  106. package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
  108. package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
  109. package/lib/typescript/module/Markdown.nitro.d.ts +5 -3
  110. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  111. package/lib/typescript/module/MarkdownContext.d.ts +26 -25
  112. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  113. package/lib/typescript/module/headless.d.ts +15 -2
  114. package/lib/typescript/module/headless.d.ts.map +1 -1
  115. package/lib/typescript/module/index.d.ts +3 -1
  116. package/lib/typescript/module/index.d.ts.map +1 -1
  117. package/lib/typescript/module/markdown-stream.d.ts +7 -2
  118. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  119. package/lib/typescript/module/markdown.d.ts +62 -5
  120. package/lib/typescript/module/markdown.d.ts.map +1 -1
  121. package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
  122. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  123. package/lib/typescript/module/renderers/code.d.ts +5 -5
  124. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  125. package/lib/typescript/module/renderers/heading.d.ts +3 -3
  126. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  127. package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
  128. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  129. package/lib/typescript/module/renderers/image.d.ts +2 -2
  130. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  131. package/lib/typescript/module/renderers/link.d.ts +3 -3
  132. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  133. package/lib/typescript/module/renderers/list.d.ts +7 -7
  134. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  135. package/lib/typescript/module/renderers/math.d.ts +4 -4
  136. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  137. package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
  138. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  139. package/lib/typescript/module/renderers/table.d.ts +3 -3
  140. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  141. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +5 -2
  142. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
  143. package/lib/typescript/module/theme.d.ts +2 -2
  144. package/lib/typescript/module/theme.d.ts.map +1 -1
  145. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  146. package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
  147. package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
  148. package/lib/typescript/module/utils/link-security.d.ts +3 -0
  149. package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
  150. package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
  151. package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
  152. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -0
  153. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  154. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +18 -6
  155. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +4 -2
  156. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/Func_void_double_double.kt +80 -0
  157. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +11 -3
  158. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.cpp +8 -0
  159. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.hpp +31 -0
  160. package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +20 -2
  161. package/nitrogen/generated/ios/swift/Func_void.swift +0 -1
  162. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
  163. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +4 -3
  164. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +34 -10
  165. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
  166. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
  167. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +2 -0
  168. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +4 -2
  169. package/package.json +7 -5
  170. package/src/Markdown.nitro.ts +7 -3
  171. package/src/MarkdownContext.ts +31 -25
  172. package/src/headless.ts +44 -6
  173. package/src/index.ts +8 -0
  174. package/src/markdown-stream.tsx +159 -15
  175. package/src/markdown.tsx +406 -50
  176. package/src/renderers/blockquote.tsx +4 -4
  177. package/src/renderers/code.tsx +16 -10
  178. package/src/renderers/heading.tsx +4 -4
  179. package/src/renderers/horizontal-rule.tsx +3 -3
  180. package/src/renderers/image.tsx +11 -9
  181. package/src/renderers/link.tsx +25 -7
  182. package/src/renderers/list.tsx +9 -12
  183. package/src/renderers/math.tsx +4 -4
  184. package/src/renderers/paragraph.tsx +3 -3
  185. package/src/renderers/table.tsx +270 -98
  186. package/src/specs/MarkdownSession.nitro.ts +6 -2
  187. package/src/theme.ts +3 -3
  188. package/src/use-markdown-stream.ts +22 -16
  189. package/src/utils/incremental-ast.ts +224 -0
  190. package/src/utils/link-security.ts +22 -0
  191. package/src/utils/stream-timeline.ts +72 -0
package/README.md CHANGED
@@ -1,58 +1,40 @@
1
1
  <p align="center">
2
- <img src="./readme/demo.gif" alt="react-native-nitro-markdown demo" width="300" />
3
- <img src="./readme/stream-demo.gif" alt="react-native-nitro-markdown stream demo" width="300" />
2
+ <img src="./readme/demo.gif" alt="react-native-nitro-markdown demo" width="400" />
4
3
  </p>
5
4
 
6
5
  # react-native-nitro-markdown
7
6
 
8
- Fast, native Markdown parsing and rendering for React Native. Built on md4c (C++) and Nitro Modules (JSI), it turns Markdown into a typed AST synchronously and renders it with a batteries-included React Native renderer.
7
+ Native Markdown parsing and rendering for React Native.
9
8
 
10
- ---
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.
11
10
 
12
- ## Why this package exists
11
+ ## Why use it
13
12
 
14
- JavaScript Markdown parsers do a lot of work on the JS thread and often trigger GC pauses on large documents. This package moves parsing to native C++ and uses JSI to return the AST without going through the RN bridge.
15
-
16
- **How it works:**
17
-
18
- Markdown string -> md4c C++ parser -> JSON AST -> React Native renderers
19
-
20
- ---
21
-
22
- ## Features
23
-
24
- - Native C++ parser with JSI access (fast, synchronous parsing)
25
- - Full renderer included (Markdown component)
26
- - Headless API for custom renderers or processing
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
27
16
  - GFM support (tables, strikethrough, task lists, autolinks)
28
- - LaTeX math parsing (inline and block)
29
- - Streaming support for token-by-token updates
30
- - Theming and per-node style overrides
31
- - Built-in renderers exposed for reuse
32
-
33
- ---
17
+ - Optional math rendering with `react-native-mathjax-svg`
34
18
 
35
19
  ## Requirements
36
20
 
37
- - React Native >= 0.75
38
- - react-native-nitro-modules
39
-
40
- Optional (for math rendering):
21
+ - React Native `>=0.75.0`
22
+ - `react-native-nitro-modules`
41
23
 
42
- - react-native-mathjax-svg
43
- - react-native-svg
24
+ Optional for math rendering:
44
25
 
45
- ---
26
+ - `react-native-mathjax-svg >=0.9.0`
27
+ - `react-native-svg >=13.0.0`
46
28
 
47
29
  ## Installation
48
30
 
49
- Install the package and Nitro Modules:
31
+ ### React Native
50
32
 
51
33
  ```bash
52
34
  bun add react-native-nitro-markdown react-native-nitro-modules
53
35
  ```
54
36
 
55
- Optional math dependencies:
37
+ Optional math support:
56
38
 
57
39
  ```bash
58
40
  bun add react-native-mathjax-svg react-native-svg
@@ -64,168 +46,333 @@ iOS pods:
64
46
  cd ios && pod install
65
47
  ```
66
48
 
67
- Expo (requires a development build):
49
+ ### Expo (development build)
68
50
 
69
51
  ```bash
70
52
  bunx expo install react-native-nitro-markdown react-native-nitro-modules
71
53
  bunx expo prebuild
72
54
  ```
73
55
 
74
- ---
56
+ Optional math support:
57
+
58
+ ```bash
59
+ bunx expo install react-native-mathjax-svg react-native-svg
60
+ ```
75
61
 
76
62
  ## Quick Start
77
63
 
78
64
  ```tsx
79
65
  import { Markdown } from "react-native-nitro-markdown";
80
66
 
81
- export function MyComponent() {
67
+ export function Example() {
82
68
  return (
83
69
  <Markdown options={{ gfm: true }}>
84
- {"# Hello World\nThis is **bold** text."}
70
+ {"# Hello\nThis is **native** markdown."}
85
71
  </Markdown>
86
72
  );
87
73
  }
88
74
  ```
89
75
 
90
- ---
91
-
92
- ## Feature Guide
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
+
117
+ ## Feature Index
118
+
119
+ Use this table as a quick map from feature -> API -> demo usage.
120
+
121
+ | Feature | API | What it does | Demo |
122
+ | ------------------------ | --------------------------------------------------- | --------------------------------------------------------- | ------------------------------------- |
123
+ | Basic markdown rendering | `Markdown` | Parse and render markdown in one component | `app/render-default.tsx` |
124
+ | Parser flags | `options` (`gfm`, `math`) | Enable GFM and math parsing | `app/render-default.tsx` |
125
+ | Plugin pipeline | `plugins` (`beforeParse`, `afterParse`) | Rewrite markdown input or AST around parse | README examples |
126
+ | AST transform | `astTransform` | Post-parse AST rewrite before render | `app/render-custom.tsx` |
127
+ | Pre-parsed AST render | `sourceAst` | Skip parsing during render and render existing AST | README examples |
128
+ | Parse lifecycle | `onParsingInProgress`, `onParseComplete` | Observe parse start/finish and consume normalized text | README examples |
129
+ | Link interception | `onLinkPress` | Override default URL open behavior | README examples |
130
+ | Large doc virtualization | `virtualize`, `virtualizationMinBlocks` | Virtualizes top-level blocks for very large markdown docs | README examples |
131
+ | Streaming markdown | `MarkdownStream` + `createMarkdownSession` | Render incrementally appended markdown content | `app/render-stream.tsx` |
132
+ | Timed highlight sync | `useStream(timestamps)` | Sync highlight position to playback timeline | README examples |
133
+ | Headless parsing | `parseMarkdown`, `parseMarkdownWithOptions` | Parse markdown without built-in UI | `app/index.tsx` + README examples |
134
+ | Custom node rendering | `renderers` + built-in renderer components | Replace specific node UI while preserving parser behavior | `app/render-custom.tsx` |
135
+ | Styling and theme | `theme`, `styles`, `stylingStrategy`, `mergeThemes` | Control visual tokens and per-node styles | `app/render-default-styles.tsx` |
136
+ | Low-level parser access | `MarkdownParserModule` | Direct access to Nitro parser methods | README examples |
137
+
138
+ ## Package Exports
139
+
140
+ ### Main Entry (`react-native-nitro-markdown`)
141
+
142
+ - Parser and headless helpers:
143
+ - `parseMarkdown`
144
+ - `parseMarkdownWithOptions`
145
+ - `getTextContent`
146
+ - `getFlattenedText`
147
+ - `MarkdownParserModule`
148
+ - Components:
149
+ - `Markdown`
150
+ - `MarkdownStream`
151
+ - Hooks and sessions:
152
+ - `useMarkdownSession`
153
+ - `useStream`
154
+ - `createMarkdownSession`
155
+ - Context:
156
+ - `MarkdownContext`
157
+ - `useMarkdownContext`
158
+ - Theme:
159
+ - `defaultMarkdownTheme`
160
+ - `minimalMarkdownTheme`
161
+ - `mergeThemes`
162
+ - Built-in renderers:
163
+ - `Heading`, `Paragraph`, `Link`, `Blockquote`, `HorizontalRule`
164
+ - `CodeBlock`, `InlineCode`
165
+ - `List`, `ListItem`, `TaskListItem`
166
+ - `TableRenderer`, `Image`, `MathInline`, `MathBlock`
167
+ - Types:
168
+ - `MarkdownNode`, `ParserOptions`, `MarkdownParser`
169
+ - `MarkdownProps`, `AstTransform`, `MarkdownPlugin`, `MarkdownStreamProps`, `MarkdownVirtualizationOptions`
170
+ - `CustomRenderers`, `CustomRenderer`, `CustomRendererProps`
171
+ - `NodeRendererProps`, `BaseCustomRendererProps`, `EnhancedRendererProps`
172
+ - `HeadingRendererProps`, `LinkRendererProps`, `ImageRendererProps`
173
+ - `CodeBlockRendererProps`, `InlineCodeRendererProps`
174
+ - `ListRendererProps`, `TaskListItemRendererProps`
175
+ - `LinkPressHandler`, `MarkdownContextValue`
176
+ - `MarkdownTheme`, `PartialMarkdownTheme`, `NodeStyleOverrides`, `StylingStrategy`
177
+ - `MarkdownSession`
178
+
179
+ ### Headless Entry (`react-native-nitro-markdown/headless`)
180
+
181
+ Exports only parser-related API (`parseMarkdown`, `parseMarkdownWithOptions`, `extractPlainText`, `extractPlainTextWithOptions`, `getTextContent`, `getFlattenedText`, types). Use this when you do not need built-in UI rendering.
182
+
183
+ ## Component API
184
+
185
+ ## `Markdown`
93
186
 
94
- Start here (pick the approach that matches your use case):
95
-
96
- - `Markdown` for static or preloaded content
97
- - `MarkdownStream` + `useMarkdownSession` for streaming tokens
98
- - `headless` API for custom rendering or data processing
99
-
100
- ### 1) Parsing options (GFM and Math)
187
+ ```tsx
188
+ import { Markdown } from "react-native-nitro-markdown";
189
+ ```
101
190
 
102
- Parsing options are passed using the `options` prop.
191
+ Demo usage:
192
+
193
+ - `apps/example/app/render-default.tsx`
194
+ - `apps/example/app/render-default-styles.tsx`
195
+ - `apps/example/app/render-custom.tsx`
196
+
197
+ | Prop | Type | Default | Description |
198
+ | --------------------- | ---------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
199
+ | `children` | `string` | required | Markdown input string. |
200
+ | `options` | `ParserOptions` | `undefined` | Parser flags (`gfm`, `math`). |
201
+ | `plugins` | `MarkdownPlugin[]` | `undefined` | Optional parser plugin hooks (`beforeParse`, `afterParse`). |
202
+ | `sourceAst` | `MarkdownNode` | `undefined` | Pre-parsed AST. When provided, native parse is skipped. |
203
+ | `astTransform` | `AstTransform` | `undefined` | Transform hook applied after plugins, before rendering and `onParseComplete`. |
204
+ | `renderers` | `CustomRenderers` | `{}` | Per-node custom renderers. |
205
+ | `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` or `minimalMarkdownTheme` | Theme token overrides. |
206
+ | `styles` | `NodeStyleOverrides` | `undefined` | Per-node style overrides. |
207
+ | `stylingStrategy` | `"opinionated" \| "minimal"` | `"opinionated"` | Base styling preset. |
208
+ | `style` | `StyleProp<ViewStyle>` | `undefined` | Container style for the root `View`. |
209
+ | `onParsingInProgress` | `() => void` | `undefined` | Called when parse inputs change. |
210
+ | `onParseComplete` | `(result) => void` | `undefined` | Called with `{ raw, ast, text }` after successful parse. |
211
+ | `onLinkPress` | `LinkPressHandler` | `undefined` | Intercepts link press before default open behavior. Return `false` to block default open. |
212
+ | `virtualize` | `boolean \| "auto"` | `false` | Enables top-level block virtualization. Use `"auto"` to activate by block threshold. |
213
+ | `virtualizationMinBlocks` | `number` | `40` | Minimum top-level block count before virtualization activates. |
214
+ | `virtualization` | `MarkdownVirtualizationOptions` | `undefined` | Optional FlatList tuning (`windowSize`, `initialNumToRender`, batching, clipping). |
215
+
216
+ Notes:
217
+
218
+ - Parse failures are caught and rendered as a fallback message (`Error parsing markdown`).
219
+ - `text` in `onParseComplete` is produced by `getFlattenedText(ast)`.
220
+ - `astTransform` should be wrapped with `useCallback` to avoid unnecessary re-parses.
221
+ - `astTransform` is a post-parse AST rewrite hook. It does not add parser syntax support and is not a markdown-it plugin API.
222
+ - Plugin pipeline order is: `beforeParse` -> parse/sourceAst -> `afterParse` -> `astTransform` -> render.
223
+ - Tables render immediately with estimated column widths, then refine widths after layout measurement to improve reliability on slower layout cycles.
224
+ - For very large markdown content, enable `virtualize` to avoid mounting all top-level blocks at once.
225
+ - `virtualize="auto"` enables threshold-driven virtualization while keeping small markdown renders on plain `View` trees.
226
+
227
+ ### Virtualization example (large docs)
103
228
 
104
229
  ```tsx
105
- <Markdown options={{ gfm: true, math: true }}>{content}</Markdown>
230
+ <Markdown
231
+ virtualize="auto"
232
+ virtualizationMinBlocks={30}
233
+ virtualization={{
234
+ initialNumToRender: 10,
235
+ maxToRenderPerBatch: 10,
236
+ windowSize: 8,
237
+ updateCellsBatchingPeriod: 16,
238
+ removeClippedSubviews: true,
239
+ }}
240
+ >
241
+ {content}
242
+ </Markdown>
106
243
  ```
107
244
 
108
- - `gfm` enables GitHub Flavored Markdown features supported by md4c.
109
- - `math` enables `$...$` and `$$...$$` parsing into math nodes.
245
+ Virtualization notes:
110
246
 
111
- ### 2) Styling and themes
247
+ - Keep `Markdown` as the primary vertical scroller when `virtualize` is enabled.
248
+ - Avoid nesting it inside another vertical `ScrollView`, or virtualization effectiveness drops.
112
249
 
113
- You can override tokens using the `theme` prop. Only provide the tokens you want to change.
250
+ ### AST transform example
114
251
 
115
252
  ```tsx
116
- import { Markdown } from "react-native-nitro-markdown";
117
-
118
- const theme = {
119
- colors: {
120
- text: "#0f172a",
121
- heading: "#0f172a",
122
- link: "#2563eb",
123
- codeBackground: "#f1f5f9",
124
- },
125
- showCodeLanguage: true,
126
- };
127
-
128
- <Markdown theme={theme}>{content}</Markdown>;
253
+ import { useCallback } from "react";
254
+ import { Markdown, type AstTransform } from "react-native-nitro-markdown";
255
+
256
+ const astTransform = useCallback<AstTransform>((ast) => {
257
+ const transformNode = (node: Parameters<AstTransform>[0]) => ({
258
+ ...node,
259
+ content:
260
+ node.type === "text"
261
+ ? (node.content ?? "").replace(/:wink:/g, "😉")
262
+ : node.content,
263
+ children: node.children?.map(transformNode),
264
+ });
265
+
266
+ return transformNode(ast);
267
+ }, []);
268
+
269
+ <Markdown astTransform={astTransform}>{"Hello :wink:"}</Markdown>;
129
270
  ```
130
271
 
131
- If you use a custom heading font on Android and do not load a bold variant, set `headingWeight: "normal"` to avoid font fallback.
272
+ ### Plugin pipeline example (`beforeParse` + `afterParse`)
132
273
 
133
- ### 3) Per-node style overrides
274
+ ```tsx
275
+ import { Markdown, type MarkdownPlugin } from "react-native-nitro-markdown";
134
276
 
135
- Override the styles for specific node types with `styles`.
277
+ const plugins: MarkdownPlugin[] = [
278
+ {
279
+ name: "rewrite-before-parse",
280
+ beforeParse: (input) => input.replace(/:rocket:/g, "ROCKET_TOKEN"),
281
+ },
282
+ {
283
+ name: "rewrite-after-parse",
284
+ afterParse: (ast) => {
285
+ const visit = (node: typeof ast): typeof ast => ({
286
+ ...node,
287
+ content:
288
+ node.type === "text"
289
+ ? (node.content ?? "").replace(/ROCKET_TOKEN/g, "🚀")
290
+ : node.content,
291
+ children: node.children?.map(visit),
292
+ });
293
+ return visit(ast);
294
+ },
295
+ },
296
+ ];
136
297
 
137
- ```tsx
138
- <Markdown
139
- styles={{
140
- heading: { color: "#0ea5e9", fontWeight: "900" },
141
- code_block: { backgroundColor: "#e2e8f0", borderRadius: 16 },
142
- blockquote: { borderLeftColor: "#0ea5e9" },
143
- }}
144
- >
145
- {content}
146
- </Markdown>
298
+ <Markdown plugins={plugins}>{"Launch :rocket:"}</Markdown>;
147
299
  ```
148
300
 
149
- ### 4) Custom renderers
150
-
151
- Provide a custom renderer for any node type. You get pre-mapped props for common values.
301
+ ### `sourceAst` example (skip parsing in render)
152
302
 
153
303
  ```tsx
154
304
  import {
155
305
  Markdown,
156
- CodeBlock,
157
- type HeadingRendererProps,
158
- type CodeBlockRendererProps,
306
+ parseMarkdownWithOptions,
159
307
  } from "react-native-nitro-markdown";
160
308
 
161
- const renderers = {
162
- heading: ({ level, children }: HeadingRendererProps) => (
163
- <MyHeading level={level}>{children}</MyHeading>
164
- ),
165
- code_block: ({ content, language }: CodeBlockRendererProps) => (
166
- <CodeBlock content={content} language={language} />
167
- ),
168
- };
309
+ const sourceAst = parseMarkdownWithOptions(content, { gfm: true, math: true });
169
310
 
170
- <Markdown renderers={renderers}>{content}</Markdown>;
311
+ <Markdown sourceAst={sourceAst}>
312
+ {"children is ignored when sourceAst is provided"}
313
+ </Markdown>;
171
314
  ```
172
315
 
173
- Custom renderer behavior:
174
-
175
- - Return `undefined` to fall back to the built-in renderer.
176
- - Return `null` to render nothing for that node.
177
- - The `Renderer` prop lets you render nested children the same way the default renderer does.
316
+ ### Parse lifecycle callbacks example
178
317
 
179
- Pre-mapped props by node type:
180
-
181
- | Node type | Extra props |
182
- | --- | --- |
183
- | `heading` | `level` |
184
- | `link` | `href`, `title` |
185
- | `image` | `url`, `alt`, `title` |
186
- | `code_block` | `content`, `language` |
187
- | `code_inline` | `content` |
188
- | `list` | `ordered`, `start` |
189
- | `task_list_item` | `checked` |
318
+ ```tsx
319
+ import { Markdown } from "react-native-nitro-markdown";
190
320
 
191
- ### 5) Built-in renderers
321
+ <Markdown
322
+ onParsingInProgress={() => setIsParsing(true)}
323
+ onParseComplete={({ raw, ast, text }) => {
324
+ setIsParsing(false);
325
+ setWordCount(text.trim().split(/\s+/).length);
326
+ setLastRaw(raw);
327
+ setLastAst(ast);
328
+ }}
329
+ >
330
+ {content}
331
+ </Markdown>;
332
+ ```
192
333
 
193
- All built-in renderers are exported so you can reuse them in custom renderers.
334
+ ## `MarkdownStream`
194
335
 
195
336
  ```tsx
196
- import { Heading, CodeBlock, InlineCode } from "react-native-nitro-markdown";
337
+ import { MarkdownStream } from "react-native-nitro-markdown";
197
338
  ```
198
339
 
199
- Available renderers:
340
+ `MarkdownStreamProps` extends `MarkdownProps` except `children`.
200
341
 
201
- - `Heading`
202
- - `Paragraph`
203
- - `Link`
204
- - `Blockquote`
205
- - `HorizontalRule`
206
- - `CodeBlock`
207
- - `InlineCode`
208
- - `List`
209
- - `ListItem`
210
- - `TaskListItem`
211
- - `TableRenderer`
212
- - `Image`
213
- - `MathInline`
214
- - `MathBlock`
342
+ Demo usage:
215
343
 
216
- ### 6) Streaming (LLM tokens)
344
+ - `apps/example/app/render-stream.tsx`
217
345
 
218
- Use `MarkdownStream` plus `useMarkdownSession` to stream updates efficiently.
346
+ | Prop | Type | Default | Description |
347
+ | ---------------------- | --------------------- | ------------ | ---------------------------------------------------------------------------------------- |
348
+ | `session` | `MarkdownSession` | required | Session object that supplies streamed text chunks. |
349
+ | `updateIntervalMs` | `number` | `50` | Flush interval when `updateStrategy="interval"`. |
350
+ | `updateStrategy` | `"interval" \| "raf"` | `"interval"` | Update cadence (`setTimeout` vs `requestAnimationFrame`). |
351
+ | `useTransitionUpdates` | `boolean` | `false` | Applies `startTransition` to streamed UI updates. |
352
+ | `incrementalParsing` | `boolean` | `true` | Enables append-optimized incremental AST updates (falls back to full parse when unsafe). |
353
+
354
+ Notes:
355
+
356
+ - If any plugin defines `beforeParse`, `MarkdownStream` disables incremental AST mode for correctness.
357
+ - `MarkdownStream` consumes native session change ranges (`from`, `to`) and uses `getTextRange()` for contiguous appends to avoid full-buffer copies during token streams.
358
+
359
+ ### Streaming Example
219
360
 
220
361
  ```tsx
221
362
  import { useEffect } from "react";
222
- import { MarkdownStream, useMarkdownSession } from "react-native-nitro-markdown";
363
+ import {
364
+ MarkdownStream,
365
+ useMarkdownSession,
366
+ } from "react-native-nitro-markdown";
223
367
 
224
- export function AIResponseStream() {
368
+ export function StreamingExample() {
225
369
  const session = useMarkdownSession();
226
370
 
227
371
  useEffect(() => {
228
- session.getSession().append("Hello **Nitro**!");
372
+ const s = session.getSession();
373
+ s.append("# Streaming\n");
374
+ s.append("This text arrives in chunks.");
375
+
229
376
  return () => session.clear();
230
377
  }, [session]);
231
378
 
@@ -240,279 +387,419 @@ export function AIResponseStream() {
240
387
  }
241
388
  ```
242
389
 
243
- Recommended streaming defaults:
244
-
245
- - `updateStrategy="raf"` for frame-aligned updates
246
- - `updateIntervalMs` between `50` and `100` when using interval strategy
247
- - Avoid per-token UI updates by batching appends
390
+ ## Hooks and Session API
248
391
 
249
- ### 7) Headless parsing
392
+ ## `createMarkdownSession()`
250
393
 
251
- Use the headless entry when you only need the AST or want to build your own renderer.
394
+ Creates and returns a native `MarkdownSession` instance.
252
395
 
253
396
  ```tsx
254
- import {
255
- parseMarkdown,
256
- parseMarkdownWithOptions,
257
- getTextContent,
258
- getFlattenedText,
259
- } from "react-native-nitro-markdown/headless";
397
+ import { createMarkdownSession } from "react-native-nitro-markdown";
260
398
 
261
- const ast = parseMarkdown("# Hello World");
262
- const text = getTextContent(ast);
263
- const normalized = getFlattenedText(ast);
399
+ const session = createMarkdownSession();
400
+ session.append("hello");
264
401
  ```
265
402
 
266
- ### 8) Plain text extraction
403
+ `MarkdownSession` methods:
404
+
405
+ | Method | Signature | Description |
406
+ | --- | --- | --- |
407
+ | `append` | `(chunk: string) => number` | Appends text and returns new UTF-16 length. |
408
+ | `clear` | `() => void` | Clears buffer and emits a reset range event (`0, 0`). |
409
+ | `getAllText` | `() => string` | Returns full session text. |
410
+ | `getLength` | `() => number` | Returns current UTF-16 text length without materializing a copy. |
411
+ | `getTextRange` | `(from: number, to: number) => string` | Returns a substring range for delta-driven streaming updates. |
412
+ | `addListener` | `(listener: (from: number, to: number) => void) => () => void` | Subscribes to mutation range events and returns an unsubscribe function. |
413
+ | `highlightPosition` | `number` | Mutable cursor used by stream highlight workflows. |
267
414
 
268
- If you are already using the `Markdown` component, you can get the plain text during parse.
415
+ Demo usage:
416
+
417
+ - Referenced in `apps/example/app/render-stream.tsx` sample markdown content and used directly in README examples.
418
+
419
+ Manual session + stream rendering:
269
420
 
270
421
  ```tsx
271
- <Markdown
272
- onParseComplete={(result) => {
273
- console.log(result.text);
274
- }}
275
- >
276
- {content}
277
- </Markdown>
422
+ import {
423
+ createMarkdownSession,
424
+ MarkdownStream,
425
+ } from "react-native-nitro-markdown";
426
+
427
+ const session = createMarkdownSession();
428
+ session.append("# Hello\n");
429
+ session.append("Streaming content...");
430
+
431
+ <MarkdownStream session={session} updateStrategy="raf" />;
278
432
  ```
279
433
 
280
- ### 9) Tables, images, and math
434
+ ## `useMarkdownSession()`
281
435
 
282
- - Tables are rendered with a horizontal scroll view and measured column widths.
283
- - Images use React Native `Image` and try to preserve the real aspect ratio.
284
- - Math nodes render with `react-native-mathjax-svg` if installed; otherwise they fall back to a code-style look.
436
+ Creates and owns one `MarkdownSession` for a component lifecycle.
285
437
 
286
- ---
438
+ Returns:
287
439
 
288
- ## Common Recipes
440
+ | Field | Type | Description |
441
+ | ---------------- | ---------------------------- | ------------------------------------------------------------- |
442
+ | `getSession` | `() => MarkdownSession` | Returns the stable native session instance. |
443
+ | `isStreaming` | `boolean` | Stateful flag for app-level streaming UI. |
444
+ | `setIsStreaming` | `(value: boolean) => void` | Setter for `isStreaming`. |
445
+ | `stop` | `() => void` | Sets `isStreaming` to `false`. |
446
+ | `clear` | `() => void` | Clears session content and resets `highlightPosition` to `0`. |
447
+ | `setHighlight` | `(position: number) => void` | Sets `session.highlightPosition`. |
289
448
 
290
- ### Open links with custom behavior
449
+ Demo usage:
291
450
 
292
- ```tsx
293
- import { Markdown, type LinkRendererProps } from "react-native-nitro-markdown";
294
- import { Text, Linking } from "react-native";
451
+ - `apps/example/app/render-stream.tsx`
295
452
 
296
- const renderers = {
297
- link: ({ href, children }: LinkRendererProps) => (
298
- <Text
299
- style={{ textDecorationLine: "underline" }}
300
- onPress={async () => {
301
- if (href && (await Linking.canOpenURL(href))) {
302
- Linking.openURL(href);
303
- }
304
- }}
305
- >
306
- {children}
307
- </Text>
308
- ),
309
- };
453
+ ## `useStream(timestamps?)`
310
454
 
311
- <Markdown renderers={renderers}>{content}</Markdown>;
312
- ```
455
+ Builds on `useMarkdownSession` and adds timeline sync helpers.
456
+
457
+ - `timestamps` type: `Record<number, number>` where key = word/token index, value = timestamp in ms.
458
+ - `sync(currentTimeMs)` computes highlight position from timestamp map.
459
+ - Uses optimized lookup for monotonic timelines and handles non-monotonic maps safely.
460
+
461
+ Additional returned fields:
462
+
463
+ | Field | Type | Description |
464
+ | -------------- | --------------------------------- | ----------------------------------------- |
465
+ | `isPlaying` | `boolean` | Playback state for timed streaming. |
466
+ | `setIsPlaying` | `(value: boolean) => void` | Setter for `isPlaying`. |
467
+ | `sync` | `(currentTimeMs: number) => void` | Applies timeline-based highlight updates. |
313
468
 
314
- ### Custom image renderer (placeholder + fixed height)
469
+ Example:
315
470
 
316
471
  ```tsx
317
- import { Markdown, type ImageRendererProps } from "react-native-nitro-markdown";
318
- import { View, Image, Text } from "react-native";
472
+ const stream = useStream({
473
+ 0: 0,
474
+ 1: 500,
475
+ 2: 1000,
476
+ });
319
477
 
320
- const renderers = {
321
- image: ({ url, title, alt }: ImageRendererProps) => (
322
- <View>
323
- <Image source={{ uri: url }} style={{ height: 220, borderRadius: 12 }} />
324
- {(title || alt) && (
325
- <Text style={{ marginTop: 6, opacity: 0.6 }}>{title || alt}</Text>
326
- )}
327
- </View>
328
- ),
329
- };
478
+ // e.g. in media time update callback:
479
+ stream.sync(currentTimeMs);
480
+
481
+ <MarkdownStream session={stream.getSession()} />;
330
482
  ```
331
483
 
332
- ### Render HTML nodes as code (opt-in)
484
+ Demo usage:
485
+
486
+ - README examples (timed playback scenario)
487
+
488
+ ## Headless API
333
489
 
334
490
  ```tsx
335
- import { Markdown, CodeBlock, InlineCode } from "react-native-nitro-markdown";
491
+ import {
492
+ parseMarkdown,
493
+ parseMarkdownWithOptions,
494
+ extractPlainText,
495
+ extractPlainTextWithOptions,
496
+ getTextContent,
497
+ getFlattenedText,
498
+ } from "react-native-nitro-markdown/headless";
499
+ ```
336
500
 
337
- const renderers = {
338
- html_block: ({ node }) => <CodeBlock content={node.content ?? \"\"} />,
339
- html_inline: ({ node }) => <InlineCode content={node.content ?? \"\"} />,
501
+ | Function | Signature | Description |
502
+ | -------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ |
503
+ | `parseMarkdown` | `(text: string) => MarkdownNode` | Parses markdown using default parser settings. |
504
+ | `parseMarkdownWithOptions` | `(text: string, options: ParserOptions) => MarkdownNode` | Parses markdown with `gfm` and/or `math` flags. |
505
+ | `extractPlainText` | `(text: string) => string` | Parses and returns normalized plain text directly from native parser. |
506
+ | `extractPlainTextWithOptions` | `(text: string, options: ParserOptions) => string` | Same as above with parser flags. |
507
+ | `getTextContent` | `(node: MarkdownNode) => string` | Concatenates text recursively without layout normalization. |
508
+ | `getFlattenedText` | `(node: MarkdownNode) => string` | Returns normalized plain text with paragraph and block separators. |
509
+
510
+ ### Parser Options
511
+
512
+ ```ts
513
+ type ParserOptions = {
514
+ gfm?: boolean;
515
+ math?: boolean;
340
516
  };
341
517
  ```
342
518
 
343
- ### Minimal styling + custom palette
519
+ Example:
344
520
 
345
521
  ```tsx
346
- import { Markdown } from "react-native-nitro-markdown";
347
-
348
- <Markdown
349
- stylingStrategy="minimal"
350
- theme={{ colors: { text: "#e2e8f0", link: "#38bdf8" } }}
351
- >
352
- {content}
353
- </Markdown>;
522
+ const ast = parseMarkdownWithOptions(markdown, {
523
+ gfm: true, // tables, task lists, strikethrough
524
+ math: true, // inline/block LaTeX
525
+ });
354
526
  ```
355
527
 
356
- ### Build a search index (headless)
528
+ ### `MarkdownParserModule` (low-level Nitro access)
529
+
530
+ Use this only when you want direct method access (`parse`, `parseWithOptions`, `extractPlainText`, `extractPlainTextWithOptions`).
357
531
 
358
532
  ```tsx
359
- import { parseMarkdown, getFlattenedText } from "react-native-nitro-markdown/headless";
533
+ import {
534
+ MarkdownParserModule,
535
+ type ParserOptions,
536
+ } from "react-native-nitro-markdown/headless";
360
537
 
361
- const ast = parseMarkdown(content);
362
- const plainText = getFlattenedText(ast);
538
+ const options: ParserOptions = { gfm: true };
539
+ const jsonAst = JSON.parse(
540
+ MarkdownParserModule.parseWithOptions("# Hello", options),
541
+ );
363
542
  ```
364
543
 
365
- ---
544
+ ## Custom Renderer API
366
545
 
367
- ## API Reference
546
+ ## `renderers` prop contract
368
547
 
369
- ### Markdown component
548
+ `CustomRenderers` is:
370
549
 
371
- ```tsx
372
- import { Markdown } from "react-native-nitro-markdown";
550
+ ```ts
551
+ type CustomRenderers = Partial<Record<MarkdownNode["type"], CustomRenderer>>;
373
552
  ```
374
553
 
375
- Props:
554
+ `CustomRenderer` receives `EnhancedRendererProps` and returns:
376
555
 
377
- | Prop | Type | Default | Description |
378
- | --- | --- | --- | --- |
379
- | `children` | `string` | required | Markdown string to parse and render |
380
- | `options` | `{ gfm?: boolean; math?: boolean }` | `undefined` | Parser options |
381
- | `renderers` | `CustomRenderers` | `{}` | Custom renderers by node type |
382
- | `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` | Theme token overrides |
383
- | `styles` | `NodeStyleOverrides` | `undefined` | Per-node style overrides |
384
- | `stylingStrategy` | `"opinionated" \| "minimal"` | `"opinionated"` | Styling baseline |
385
- | `style` | `StyleProp<ViewStyle>` | `undefined` | Container style |
386
- | `onParsingInProgress` | `() => void` | `undefined` | Called when parsing starts |
387
- | `onParseComplete` | `(result) => void` | `undefined` | Called with `{ raw, ast, text }` |
556
+ - React node to override default rendering
557
+ - `undefined` to fallback to built-in renderer
558
+ - `null` to render nothing
388
559
 
389
- ### MarkdownStream
560
+ `EnhancedRendererProps` always includes:
390
561
 
391
- ```tsx
392
- import { MarkdownStream } from "react-native-nitro-markdown";
393
- ```
562
+ - `node`: current `MarkdownNode`
563
+ - `children`: pre-rendered node children
564
+ - `Renderer`: recursive renderer component for nested custom rendering
394
565
 
395
- Props (in addition to all `Markdown` props except `children`):
566
+ And conditionally includes mapped fields by node type:
396
567
 
397
- | Prop | Type | Default | Description |
398
- | --- | --- | --- | --- |
399
- | `session` | `MarkdownSession` | required | Active session for streaming text |
400
- | `updateIntervalMs` | `number` | `50` | Throttle interval for updates |
401
- | `updateStrategy` | `"interval" \| "raf"` | `"interval"` | Update strategy |
402
- | `useTransitionUpdates` | `boolean` | `false` | Use React transitions |
568
+ | Node type | Extra mapped fields |
569
+ | ---------------- | --------------------- |
570
+ | `heading` | `level` |
571
+ | `link` | `href`, `title` |
572
+ | `image` | `url`, `alt`, `title` |
573
+ | `code_block` | `content`, `language` |
574
+ | `code_inline` | `content` |
575
+ | `list` | `ordered`, `start` |
576
+ | `task_list_item` | `checked` |
403
577
 
404
- ### Hooks and sessions
578
+ ### Example: Custom heading + code block
405
579
 
406
580
  ```tsx
407
- import { useMarkdownSession, createMarkdownSession, useStream } from "react-native-nitro-markdown";
408
- ```
409
-
410
- - `useMarkdownSession()` returns a managed session and helpers: `getSession`, `setIsStreaming`, `stop`, `clear`, `setHighlight`.
411
- - `createMarkdownSession()` creates a session without React hooks.
412
- - `useStream(timestamps)` adds a simple sync helper for time-based streaming.
581
+ import {
582
+ Markdown,
583
+ type HeadingRendererProps,
584
+ type CodeBlockRendererProps,
585
+ } from "react-native-nitro-markdown";
413
586
 
414
- ### Headless API
587
+ const renderers = {
588
+ heading: ({ level, children }: HeadingRendererProps) => (
589
+ <MyHeading level={level}>{children}</MyHeading>
590
+ ),
591
+ code_block: ({ language, content }: CodeBlockRendererProps) => (
592
+ <MyCode language={language} content={content} />
593
+ ),
594
+ };
415
595
 
416
- ```tsx
417
- import { parseMarkdown, parseMarkdownWithOptions, getTextContent, getFlattenedText } from "react-native-nitro-markdown/headless";
596
+ <Markdown renderers={renderers}>{content}</Markdown>;
418
597
  ```
419
598
 
420
- ### Theme utilities
599
+ ### `useMarkdownContext` example (inside custom renderer tree)
421
600
 
422
601
  ```tsx
602
+ import { Text } from "react-native";
423
603
  import {
424
- defaultMarkdownTheme,
425
- minimalMarkdownTheme,
426
- mergeThemes,
604
+ Markdown,
605
+ useMarkdownContext,
606
+ type CustomRendererProps,
427
607
  } from "react-native-nitro-markdown";
608
+
609
+ function ThemedParagraph({ children }: Pick<CustomRendererProps, "children">) {
610
+ const { theme } = useMarkdownContext();
611
+ return <Text style={{ color: theme.colors.text }}>{children}</Text>;
612
+ }
613
+
614
+ <Markdown
615
+ renderers={{
616
+ paragraph: ({ children }: CustomRendererProps) => (
617
+ <ThemedParagraph>{children}</ThemedParagraph>
618
+ ),
619
+ }}
620
+ >
621
+ {content}
622
+ </Markdown>;
428
623
  ```
429
624
 
430
- ### Types
625
+ ## Link Handling Behavior
626
+
627
+ Default link renderer behavior:
628
+
629
+ 1. Trims incoming href.
630
+ 2. Calls `onLinkPress(href)` if provided.
631
+ 3. Stops if handler returns `false`.
632
+ 4. Allows only protocol-based links with these schemes:
633
+ - `http:`
634
+ - `https:`
635
+ - `mailto:`
636
+ - `tel:`
637
+ - `sms:`
638
+ 5. Uses `Linking.canOpenURL` before `Linking.openURL`.
639
+
640
+ Relative URLs and anchors are ignored by default open behavior, but you can handle them in `onLinkPress`.
641
+
642
+ ## Theme API
643
+
644
+ ## `MarkdownTheme`
431
645
 
432
646
  ```tsx
433
647
  import type {
434
- CustomRenderers,
435
- NodeStyleOverrides,
436
648
  MarkdownTheme,
437
649
  PartialMarkdownTheme,
438
650
  } from "react-native-nitro-markdown";
439
651
  ```
440
652
 
441
- ---
653
+ `MarkdownTheme` fields:
654
+
655
+ - `colors`
656
+ - `text`, `textMuted`, `heading`, `link`, `code`, `codeBackground`, `codeLanguage`
657
+ - `blockquote`, `border`, `surface`, `surfaceLight`, `accent`
658
+ - `tableBorder`, `tableHeader`, `tableHeaderText`, `tableRowEven`, `tableRowOdd`
659
+ - `spacing`: `xs`, `s`, `m`, `l`, `xl`
660
+ - `fontSizes`: `xs`, `s`, `m`, `l`, `xl`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`
661
+ - `fontFamilies`: `regular`, `heading`, `mono`
662
+ - `headingWeight?`
663
+ - `borderRadius`: `s`, `m`, `l`
664
+ - `showCodeLanguage`
665
+
666
+ Helpers:
667
+
668
+ - `defaultMarkdownTheme`
669
+ - `minimalMarkdownTheme`
670
+ - `mergeThemes(base, partial)`
671
+
672
+ `NodeStyleOverrides` lets you override per-node styles:
673
+
674
+ ```ts
675
+ type NodeStyleOverrides = Partial<
676
+ Record<MarkdownNode["type"], ViewStyle | TextStyle>
677
+ >;
678
+ ```
679
+
680
+ Example with `mergeThemes`:
442
681
 
443
- ## Supported Node Types
682
+ ```tsx
683
+ import {
684
+ Markdown,
685
+ defaultMarkdownTheme,
686
+ mergeThemes,
687
+ } from "react-native-nitro-markdown";
444
688
 
445
- You can customize any of these node types with `renderers` or `styles`.
689
+ const theme = mergeThemes(defaultMarkdownTheme, {
690
+ colors: {
691
+ text: "#0f172a",
692
+ link: "#1d4ed8",
693
+ },
694
+ fontSizes: {
695
+ m: 16,
696
+ },
697
+ });
698
+
699
+ <Markdown theme={theme}>{content}</Markdown>;
700
+ ```
701
+
702
+ ## Built-in Renderer Components
703
+
704
+ Use these when composing custom renderer maps.
705
+
706
+ | Component | Key props |
707
+ | ---------------- | ------------------------------------------------ |
708
+ | `Heading` | `level`, `children`, `style` |
709
+ | `Paragraph` | `children`, `inListItem`, `style` |
710
+ | `Link` | `href`, `children`, `style` |
711
+ | `Blockquote` | `children`, `style` |
712
+ | `HorizontalRule` | `style` |
713
+ | `CodeBlock` | `language`, `content`, `node`, `style` |
714
+ | `InlineCode` | `content`, `node`, `children`, `style` |
715
+ | `List` | `ordered`, `start`, `depth`, `children`, `style` |
716
+ | `ListItem` | `children`, `index`, `ordered`, `start`, `style` |
717
+ | `TaskListItem` | `children`, `checked`, `style` |
718
+ | `TableRenderer` | `node`, `Renderer`, `style` |
719
+ | `Image` | `url`, `title`, `alt`, `Renderer`, `style` |
720
+ | `MathInline` | `content`, `style` |
721
+ | `MathBlock` | `content`, `style` |
722
+
723
+ ## Supported AST Node Types
446
724
 
447
725
  `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`
448
726
 
449
- Note: `html_inline` and `html_block` are parsed but not rendered by default.
727
+ Notes:
450
728
 
451
- ---
729
+ - `html_inline` and `html_block` are parsed but not rendered by default.
730
+ - Table internals (`table_head`, `table_body`, `table_row`, `table_cell`) are renderer internals; override `table` for custom table UI.
452
731
 
453
- ## AST Shape
732
+ ## Recipes
454
733
 
455
- The headless API returns a typed AST. This is the core shape used by the renderer.
734
+ ### Intercept links with `onLinkPress`
456
735
 
457
- ```ts
458
- export interface MarkdownNode {
459
- type:
460
- | "document"
461
- | "heading"
462
- | "paragraph"
463
- | "text"
464
- | "bold"
465
- | "italic"
466
- | "strikethrough"
467
- | "link"
468
- | "image"
469
- | "code_inline"
470
- | "code_block"
471
- | "blockquote"
472
- | "horizontal_rule"
473
- | "line_break"
474
- | "soft_break"
475
- | "table"
476
- | "table_head"
477
- | "table_body"
478
- | "table_row"
479
- | "table_cell"
480
- | "list"
481
- | "list_item"
482
- | "task_list_item"
483
- | "math_inline"
484
- | "math_block"
485
- | "html_block"
486
- | "html_inline";
487
- content?: string;
488
- children?: MarkdownNode[];
489
- level?: number;
490
- href?: string;
491
- title?: string;
492
- alt?: string;
493
- language?: string;
494
- ordered?: boolean;
495
- start?: number;
496
- checked?: boolean;
497
- align?: string;
498
- isHeader?: boolean;
499
- }
736
+ ```tsx
737
+ import { Markdown } from "react-native-nitro-markdown";
738
+
739
+ <Markdown
740
+ onLinkPress={(href) => {
741
+ if (href.startsWith("/")) {
742
+ router.push(href);
743
+ return false;
744
+ }
745
+ }}
746
+ >
747
+ {content}
748
+ </Markdown>;
500
749
  ```
501
750
 
502
- ---
751
+ ### Use headless mode to build search index
503
752
 
504
- ## Troubleshooting
753
+ ```tsx
754
+ import {
755
+ parseMarkdown,
756
+ getFlattenedText,
757
+ } from "react-native-nitro-markdown/headless";
505
758
 
506
- - Math renders as plain text: install `react-native-mathjax-svg` and `react-native-svg`.
507
- - iOS build errors: run `pod install` after installing dependencies.
508
- - Expo: you must use a development build (`expo prebuild` + `expo run`), not Expo Go.
509
- - Android heading font looks wrong: set `headingWeight: "normal"` when your font has no bold variant.
759
+ const ast = parseMarkdown(content);
760
+ const searchableText = getFlattenedText(ast);
761
+ ```
762
+
763
+ ### Minimal styling baseline
764
+
765
+ ```tsx
766
+ import { Markdown } from "react-native-nitro-markdown";
767
+
768
+ <Markdown
769
+ stylingStrategy="minimal"
770
+ theme={{
771
+ colors: {
772
+ text: "#0f172a",
773
+ link: "#1d4ed8",
774
+ },
775
+ }}
776
+ >
777
+ {content}
778
+ </Markdown>;
779
+ ```
780
+
781
+ ## Performance Guidance
782
+
783
+ - For streaming text, prefer `updateStrategy="raf"`.
784
+ - If you use interval strategy, `updateIntervalMs` between `50` and `100` is a good baseline.
785
+ - Batch `session.append(...)` calls instead of appending one character at a time.
786
+ - For large markdown documents, enable `virtualize` and tune `virtualization.windowSize` / `maxToRenderPerBatch`.
787
+ - Use the headless API if you do not need built-in renderers.
788
+
789
+ ## Troubleshooting
510
790
 
511
- ---
791
+ - Math appears as plain code-style fallback:
792
+ - Install `react-native-mathjax-svg` and `react-native-svg`.
793
+ - iOS native build issues after install:
794
+ - Run `pod install` in your iOS project.
795
+ - Expo app does not load native module:
796
+ - Use a development build (`expo prebuild` + `expo run`), not Expo Go.
797
+ - Android heading font weight looks wrong:
798
+ - Set `theme.headingWeight` explicitly (for custom fonts without bold variants, use `"normal"`).
512
799
 
513
800
  ## Contributing
514
801
 
515
- See `CONTRIBUTING.md` for the workflow and development commands.
802
+ See `CONTRIBUTING.md`.
516
803
 
517
804
  ## License
518
805