react-native-nitro-markdown 0.4.3 → 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 (91) hide show
  1. package/README.md +351 -22
  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/headless.js +41 -5
  7. package/lib/commonjs/headless.js.map +1 -1
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/markdown-stream.js +107 -13
  10. package/lib/commonjs/markdown-stream.js.map +1 -1
  11. package/lib/commonjs/markdown.js +180 -25
  12. package/lib/commonjs/markdown.js.map +1 -1
  13. package/lib/commonjs/renderers/code.js +1 -0
  14. package/lib/commonjs/renderers/code.js.map +1 -1
  15. package/lib/commonjs/renderers/table.js +116 -24
  16. package/lib/commonjs/renderers/table.js.map +1 -1
  17. package/lib/commonjs/utils/incremental-ast.js +153 -0
  18. package/lib/commonjs/utils/incremental-ast.js.map +1 -0
  19. package/lib/module/headless.js +37 -4
  20. package/lib/module/headless.js.map +1 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/markdown-stream.js +108 -14
  23. package/lib/module/markdown-stream.js.map +1 -1
  24. package/lib/module/markdown.js +182 -27
  25. package/lib/module/markdown.js.map +1 -1
  26. package/lib/module/renderers/code.js +1 -0
  27. package/lib/module/renderers/code.js.map +1 -1
  28. package/lib/module/renderers/table.js +116 -24
  29. package/lib/module/renderers/table.js.map +1 -1
  30. package/lib/module/utils/incremental-ast.js +147 -0
  31. package/lib/module/utils/incremental-ast.js.map +1 -0
  32. package/lib/typescript/commonjs/Markdown.nitro.d.ts +2 -0
  33. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/headless.d.ts +13 -0
  35. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/index.d.ts +2 -0
  37. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/markdown-stream.d.ts +6 -1
  39. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/markdown.d.ts +53 -1
  41. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/renderers/table.d.ts +1 -1
  44. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +5 -2
  46. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
  48. package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
  49. package/lib/typescript/module/Markdown.nitro.d.ts +2 -0
  50. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  51. package/lib/typescript/module/headless.d.ts +13 -0
  52. package/lib/typescript/module/headless.d.ts.map +1 -1
  53. package/lib/typescript/module/index.d.ts +2 -0
  54. package/lib/typescript/module/index.d.ts.map +1 -1
  55. package/lib/typescript/module/markdown-stream.d.ts +6 -1
  56. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  57. package/lib/typescript/module/markdown.d.ts +53 -1
  58. package/lib/typescript/module/markdown.d.ts.map +1 -1
  59. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  60. package/lib/typescript/module/renderers/table.d.ts +1 -1
  61. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  62. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +5 -2
  63. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
  64. package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
  65. package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
  66. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -0
  67. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  68. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +18 -6
  69. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +4 -2
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/Func_void_double_double.kt +80 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +11 -3
  72. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.cpp +8 -0
  73. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.hpp +31 -0
  74. package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +20 -2
  75. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
  76. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +4 -2
  77. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +34 -9
  78. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
  79. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
  80. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +2 -0
  81. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +4 -2
  82. package/package.json +4 -3
  83. package/src/Markdown.nitro.ts +2 -0
  84. package/src/headless.ts +42 -4
  85. package/src/index.ts +7 -0
  86. package/src/markdown-stream.tsx +163 -15
  87. package/src/markdown.tsx +339 -24
  88. package/src/renderers/code.tsx +5 -1
  89. package/src/renderers/table.tsx +212 -66
  90. package/src/specs/MarkdownSession.nitro.ts +6 -2
  91. package/src/utils/incremental-ast.ts +224 -0
package/README.md CHANGED
@@ -1,6 +1,5 @@
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
@@ -74,6 +73,68 @@ export function Example() {
74
73
  }
75
74
  ```
76
75
 
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
+
77
138
  ## Package Exports
78
139
 
79
140
  ### Main Entry (`react-native-nitro-markdown`)
@@ -105,6 +166,7 @@ export function Example() {
105
166
  - `TableRenderer`, `Image`, `MathInline`, `MathBlock`
106
167
  - Types:
107
168
  - `MarkdownNode`, `ParserOptions`, `MarkdownParser`
169
+ - `MarkdownProps`, `AstTransform`, `MarkdownPlugin`, `MarkdownStreamProps`, `MarkdownVirtualizationOptions`
108
170
  - `CustomRenderers`, `CustomRenderer`, `CustomRendererProps`
109
171
  - `NodeRendererProps`, `BaseCustomRendererProps`, `EnhancedRendererProps`
110
172
  - `HeadingRendererProps`, `LinkRendererProps`, `ImageRendererProps`
@@ -116,7 +178,7 @@ export function Example() {
116
178
 
117
179
  ### Headless Entry (`react-native-nitro-markdown/headless`)
118
180
 
119
- Exports only parser-related API (`parseMarkdown`, `parseMarkdownWithOptions`, `getTextContent`, `getFlattenedText`, types). Use this when you do not need built-in UI rendering.
181
+ Exports only parser-related API (`parseMarkdown`, `parseMarkdownWithOptions`, `extractPlainText`, `extractPlainTextWithOptions`, `getTextContent`, `getFlattenedText`, types). Use this when you do not need built-in UI rendering.
120
182
 
121
183
  ## Component API
122
184
 
@@ -126,23 +188,148 @@ Exports only parser-related API (`parseMarkdown`, `parseMarkdownWithOptions`, `g
126
188
  import { Markdown } from "react-native-nitro-markdown";
127
189
  ```
128
190
 
129
- | Prop | Type | Default | Description |
130
- | --------------------- | ---------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------- | -------------------- |
131
- | `children` | `string` | required | Markdown input string. |
132
- | `options` | `ParserOptions` | `undefined` | Parser flags (`gfm`, `math`). |
133
- | `renderers` | `CustomRenderers` | `{}` | Per-node custom renderers. |
134
- | `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` or `minimalMarkdownTheme` | Theme token overrides. |
135
- | `styles` | `NodeStyleOverrides` | `undefined` | Per-node style overrides. |
136
- | `stylingStrategy` | `"opinionated" | "minimal"` | `"opinionated"` | Base styling preset. |
137
- | `style` | `StyleProp<ViewStyle>` | `undefined` | Container style for the root `View`. |
138
- | `onParsingInProgress` | `() => void` | `undefined` | Called when parse inputs change. |
139
- | `onParseComplete` | `(result) => void` | `undefined` | Called with `{ raw, ast, text }` after successful parse. |
140
- | `onLinkPress` | `LinkPressHandler` | `undefined` | Intercepts link press before default open behavior. Return `false` to block default open. |
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). |
141
215
 
142
216
  Notes:
143
217
 
144
218
  - Parse failures are caught and rendered as a fallback message (`Error parsing markdown`).
145
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)
228
+
229
+ ```tsx
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>
243
+ ```
244
+
245
+ Virtualization notes:
246
+
247
+ - Keep `Markdown` as the primary vertical scroller when `virtualize` is enabled.
248
+ - Avoid nesting it inside another vertical `ScrollView`, or virtualization effectiveness drops.
249
+
250
+ ### AST transform example
251
+
252
+ ```tsx
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>;
270
+ ```
271
+
272
+ ### Plugin pipeline example (`beforeParse` + `afterParse`)
273
+
274
+ ```tsx
275
+ import { Markdown, type MarkdownPlugin } from "react-native-nitro-markdown";
276
+
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
+ ];
297
+
298
+ <Markdown plugins={plugins}>{"Launch :rocket:"}</Markdown>;
299
+ ```
300
+
301
+ ### `sourceAst` example (skip parsing in render)
302
+
303
+ ```tsx
304
+ import {
305
+ Markdown,
306
+ parseMarkdownWithOptions,
307
+ } from "react-native-nitro-markdown";
308
+
309
+ const sourceAst = parseMarkdownWithOptions(content, { gfm: true, math: true });
310
+
311
+ <Markdown sourceAst={sourceAst}>
312
+ {"children is ignored when sourceAst is provided"}
313
+ </Markdown>;
314
+ ```
315
+
316
+ ### Parse lifecycle callbacks example
317
+
318
+ ```tsx
319
+ import { Markdown } from "react-native-nitro-markdown";
320
+
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
+ ```
146
333
 
147
334
  ## `MarkdownStream`
148
335
 
@@ -152,12 +339,22 @@ import { MarkdownStream } from "react-native-nitro-markdown";
152
339
 
153
340
  `MarkdownStreamProps` extends `MarkdownProps` except `children`.
154
341
 
155
- | Prop | Type | Default | Description |
156
- | ---------------------- | ----------------- | -------- | -------------------------------------------------- | --------------------------------------------------------- |
157
- | `session` | `MarkdownSession` | required | Session object that supplies streamed text chunks. |
158
- | `updateIntervalMs` | `number` | `50` | Flush interval when `updateStrategy="interval"`. |
159
- | `updateStrategy` | `"interval" | "raf"` | `"interval"` | Update cadence (`setTimeout` vs `requestAnimationFrame`). |
160
- | `useTransitionUpdates` | `boolean` | `false` | Applies `startTransition` to streamed UI updates. |
342
+ Demo usage:
343
+
344
+ - `apps/example/app/render-stream.tsx`
345
+
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.
161
358
 
162
359
  ### Streaming Example
163
360
 
@@ -203,6 +400,37 @@ const session = createMarkdownSession();
203
400
  session.append("hello");
204
401
  ```
205
402
 
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. |
414
+
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:
420
+
421
+ ```tsx
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" />;
432
+ ```
433
+
206
434
  ## `useMarkdownSession()`
207
435
 
208
436
  Creates and owns one `MarkdownSession` for a component lifecycle.
@@ -218,6 +446,10 @@ Returns:
218
446
  | `clear` | `() => void` | Clears session content and resets `highlightPosition` to `0`. |
219
447
  | `setHighlight` | `(position: number) => void` | Sets `session.highlightPosition`. |
220
448
 
449
+ Demo usage:
450
+
451
+ - `apps/example/app/render-stream.tsx`
452
+
221
453
  ## `useStream(timestamps?)`
222
454
 
223
455
  Builds on `useMarkdownSession` and adds timeline sync helpers.
@@ -234,12 +466,33 @@ Additional returned fields:
234
466
  | `setIsPlaying` | `(value: boolean) => void` | Setter for `isPlaying`. |
235
467
  | `sync` | `(currentTimeMs: number) => void` | Applies timeline-based highlight updates. |
236
468
 
469
+ Example:
470
+
471
+ ```tsx
472
+ const stream = useStream({
473
+ 0: 0,
474
+ 1: 500,
475
+ 2: 1000,
476
+ });
477
+
478
+ // e.g. in media time update callback:
479
+ stream.sync(currentTimeMs);
480
+
481
+ <MarkdownStream session={stream.getSession()} />;
482
+ ```
483
+
484
+ Demo usage:
485
+
486
+ - README examples (timed playback scenario)
487
+
237
488
  ## Headless API
238
489
 
239
490
  ```tsx
240
491
  import {
241
492
  parseMarkdown,
242
493
  parseMarkdownWithOptions,
494
+ extractPlainText,
495
+ extractPlainTextWithOptions,
243
496
  getTextContent,
244
497
  getFlattenedText,
245
498
  } from "react-native-nitro-markdown/headless";
@@ -249,6 +502,8 @@ import {
249
502
  | -------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ |
250
503
  | `parseMarkdown` | `(text: string) => MarkdownNode` | Parses markdown using default parser settings. |
251
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. |
252
507
  | `getTextContent` | `(node: MarkdownNode) => string` | Concatenates text recursively without layout normalization. |
253
508
  | `getFlattenedText` | `(node: MarkdownNode) => string` | Returns normalized plain text with paragraph and block separators. |
254
509
 
@@ -261,6 +516,31 @@ type ParserOptions = {
261
516
  };
262
517
  ```
263
518
 
519
+ Example:
520
+
521
+ ```tsx
522
+ const ast = parseMarkdownWithOptions(markdown, {
523
+ gfm: true, // tables, task lists, strikethrough
524
+ math: true, // inline/block LaTeX
525
+ });
526
+ ```
527
+
528
+ ### `MarkdownParserModule` (low-level Nitro access)
529
+
530
+ Use this only when you want direct method access (`parse`, `parseWithOptions`, `extractPlainText`, `extractPlainTextWithOptions`).
531
+
532
+ ```tsx
533
+ import {
534
+ MarkdownParserModule,
535
+ type ParserOptions,
536
+ } from "react-native-nitro-markdown/headless";
537
+
538
+ const options: ParserOptions = { gfm: true };
539
+ const jsonAst = JSON.parse(
540
+ MarkdownParserModule.parseWithOptions("# Hello", options),
541
+ );
542
+ ```
543
+
264
544
  ## Custom Renderer API
265
545
 
266
546
  ## `renderers` prop contract
@@ -316,6 +596,32 @@ const renderers = {
316
596
  <Markdown renderers={renderers}>{content}</Markdown>;
317
597
  ```
318
598
 
599
+ ### `useMarkdownContext` example (inside custom renderer tree)
600
+
601
+ ```tsx
602
+ import { Text } from "react-native";
603
+ import {
604
+ Markdown,
605
+ useMarkdownContext,
606
+ type CustomRendererProps,
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>;
623
+ ```
624
+
319
625
  ## Link Handling Behavior
320
626
 
321
627
  Default link renderer behavior:
@@ -371,6 +677,28 @@ type NodeStyleOverrides = Partial<
371
677
  >;
372
678
  ```
373
679
 
680
+ Example with `mergeThemes`:
681
+
682
+ ```tsx
683
+ import {
684
+ Markdown,
685
+ defaultMarkdownTheme,
686
+ mergeThemes,
687
+ } from "react-native-nitro-markdown";
688
+
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
+
374
702
  ## Built-in Renderer Components
375
703
 
376
704
  Use these when composing custom renderer maps.
@@ -455,6 +783,7 @@ import { Markdown } from "react-native-nitro-markdown";
455
783
  - For streaming text, prefer `updateStrategy="raf"`.
456
784
  - If you use interval strategy, `updateIntervalMs` between `50` and `100` is a good baseline.
457
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`.
458
787
  - Use the headless API if you do not need built-in renderers.
459
788
 
460
789
  ## Troubleshooting
@@ -470,7 +799,7 @@ import { Markdown } from "react-native-nitro-markdown";
470
799
 
471
800
  ## Contributing
472
801
 
473
- See `/Users/jota/Workspace/Projects/RN-Packages/react-native-nitro-markdown/CONTRIBUTING.md`.
802
+ See `CONTRIBUTING.md`.
474
803
 
475
804
  ## License
476
805
 
@@ -2,7 +2,7 @@ package com.margelo.nitro.com.nitromarkdown
2
2
 
3
3
  class HybridMarkdownSession : HybridMarkdownSessionSpec() {
4
4
  private var buffer = StringBuilder()
5
- private val listeners = mutableMapOf<Long, () -> Unit>()
5
+ private val listeners = mutableMapOf<Long, (Double, Double) -> Unit>()
6
6
  private var nextListenerId = 0L
7
7
  private val lock = Any()
8
8
 
@@ -17,11 +17,16 @@ class HybridMarkdownSession : HybridMarkdownSessionSpec() {
17
17
  override val memorySize: Long
18
18
  get() = buffer.length.toLong()
19
19
 
20
- override fun append(chunk: String) {
20
+ override fun append(chunk: String): Double {
21
+ val from: Int
22
+ val to: Int
21
23
  synchronized(lock) {
24
+ from = buffer.length
22
25
  buffer.append(chunk)
26
+ to = buffer.length
23
27
  }
24
- notifyListeners()
28
+ notifyListeners(from.toDouble(), to.toDouble())
29
+ return to.toDouble()
25
30
  }
26
31
 
27
32
  override fun clear() {
@@ -29,7 +34,7 @@ class HybridMarkdownSession : HybridMarkdownSessionSpec() {
29
34
  buffer.clear()
30
35
  highlightPosition = 0.0
31
36
  }
32
- notifyListeners()
37
+ notifyListeners(0.0, 0.0)
33
38
  }
34
39
 
35
40
  override fun getAllText(): String {
@@ -38,7 +43,21 @@ class HybridMarkdownSession : HybridMarkdownSessionSpec() {
38
43
  }
39
44
  }
40
45
 
41
- override fun addListener(listener: () -> Unit): () -> Unit {
46
+ override fun getLength(): Double {
47
+ synchronized(lock) {
48
+ return buffer.length.toDouble()
49
+ }
50
+ }
51
+
52
+ override fun getTextRange(from: Double, to: Double): String {
53
+ synchronized(lock) {
54
+ val start = from.toInt().coerceIn(0, buffer.length)
55
+ val end = to.toInt().coerceIn(start, buffer.length)
56
+ return buffer.substring(start, end)
57
+ }
58
+ }
59
+
60
+ override fun addListener(listener: (Double, Double) -> Unit): () -> Unit {
42
61
  val id: Long
43
62
  synchronized(lock) {
44
63
  id = nextListenerId++
@@ -51,11 +70,11 @@ class HybridMarkdownSession : HybridMarkdownSessionSpec() {
51
70
  }
52
71
  }
53
72
 
54
- private fun notifyListeners() {
55
- val currentListeners: Collection<() -> Unit>
73
+ private fun notifyListeners(from: Double, to: Double) {
74
+ val currentListeners: Collection<(Double, Double) -> Unit>
56
75
  synchronized(lock) {
57
76
  currentListeners = listeners.values.toList()
58
77
  }
59
- currentListeners.forEach { it() }
78
+ currentListeners.forEach { it(from, to) }
60
79
  }
61
80
  }