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