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.
- package/README.md +351 -22
- package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +27 -8
- package/cpp/bindings/HybridMarkdownParser.cpp +216 -66
- package/cpp/bindings/HybridMarkdownParser.hpp +2 -0
- package/ios/HybridMarkdownSession.swift +33 -7
- package/lib/commonjs/headless.js +41 -5
- package/lib/commonjs/headless.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +107 -13
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +180 -25
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/code.js +1 -0
- package/lib/commonjs/renderers/code.js.map +1 -1
- package/lib/commonjs/renderers/table.js +116 -24
- package/lib/commonjs/renderers/table.js.map +1 -1
- package/lib/commonjs/utils/incremental-ast.js +153 -0
- package/lib/commonjs/utils/incremental-ast.js.map +1 -0
- package/lib/module/headless.js +37 -4
- package/lib/module/headless.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +108 -14
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +182 -27
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/code.js +1 -0
- package/lib/module/renderers/code.js.map +1 -1
- package/lib/module/renderers/table.js +116 -24
- package/lib/module/renderers/table.js.map +1 -1
- package/lib/module/utils/incremental-ast.js +147 -0
- package/lib/module/utils/incremental-ast.js.map +1 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts +2 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/headless.d.ts +13 -0
- package/lib/typescript/commonjs/headless.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +6 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts +53 -1
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/table.d.ts +1 -1
- package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +5 -2
- package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
- package/lib/typescript/module/Markdown.nitro.d.ts +2 -0
- package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/module/headless.d.ts +13 -0
- package/lib/typescript/module/headless.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +6 -1
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts +53 -1
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/code.d.ts.map +1 -1
- package/lib/typescript/module/renderers/table.d.ts +1 -1
- package/lib/typescript/module/renderers/table.d.ts.map +1 -1
- package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +5 -2
- package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
- package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
- package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
- package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +18 -6
- package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +4 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +11 -3
- package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.hpp +31 -0
- package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +20 -2
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +4 -2
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +34 -9
- package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +4 -2
- package/package.json +4 -3
- package/src/Markdown.nitro.ts +2 -0
- package/src/headless.ts +42 -4
- package/src/index.ts +7 -0
- package/src/markdown-stream.tsx +163 -15
- package/src/markdown.tsx +339 -24
- package/src/renderers/code.tsx +5 -1
- package/src/renderers/table.tsx +212 -66
- package/src/specs/MarkdownSession.nitro.ts +6 -2
- 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="
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
|
160
|
-
|
|
|
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
|
|
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
|
|
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
|
}
|