react-native-nitro-markdown 0.7.0 → 0.7.2
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 +188 -488
- package/lib/commonjs/MarkdownContext.js.map +1 -1
- package/lib/commonjs/MarkdownSession.js +6 -2
- package/lib/commonjs/MarkdownSession.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +49 -14
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +2 -1
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/image.js +9 -0
- package/lib/commonjs/renderers/image.js.map +1 -1
- package/lib/commonjs/renderers/math.js +16 -0
- package/lib/commonjs/renderers/math.js.map +1 -1
- package/lib/commonjs/use-markdown-stream.js +25 -10
- package/lib/commonjs/use-markdown-stream.js.map +1 -1
- package/lib/commonjs/utils/incremental-ast.js +45 -5
- package/lib/commonjs/utils/incremental-ast.js.map +1 -1
- package/lib/module/MarkdownContext.js.map +1 -1
- package/lib/module/MarkdownSession.js +6 -2
- package/lib/module/MarkdownSession.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +50 -15
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +2 -1
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/image.js +10 -1
- package/lib/module/renderers/image.js.map +1 -1
- package/lib/module/renderers/math.js +16 -0
- package/lib/module/renderers/math.js.map +1 -1
- package/lib/module/use-markdown-stream.js +24 -11
- package/lib/module/use-markdown-stream.js.map +1 -1
- package/lib/module/utils/incremental-ast.js +43 -4
- package/lib/module/utils/incremental-ast.js.map +1 -1
- package/lib/typescript/commonjs/MarkdownContext.d.ts +1 -0
- package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/MarkdownSession.d.ts +1 -1
- package/lib/typescript/commonjs/MarkdownSession.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +2 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-markdown-stream.d.ts +4 -1
- package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts +1 -0
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -1
- package/lib/typescript/module/MarkdownContext.d.ts +1 -0
- package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
- package/lib/typescript/module/MarkdownSession.d.ts +1 -1
- package/lib/typescript/module/MarkdownSession.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +2 -1
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/image.d.ts.map +1 -1
- package/lib/typescript/module/renderers/math.d.ts.map +1 -1
- package/lib/typescript/module/use-markdown-stream.d.ts +4 -1
- package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/utils/incremental-ast.d.ts +1 -0
- package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/MarkdownContext.ts +2 -0
- package/src/MarkdownSession.ts +9 -2
- package/src/index.ts +2 -0
- package/src/markdown-stream.tsx +64 -18
- package/src/markdown.tsx +7 -1
- package/src/renderers/image.tsx +11 -0
- package/src/renderers/math.tsx +18 -0
- package/src/use-markdown-stream.ts +55 -19
- package/src/utils/incremental-ast.ts +81 -4
package/README.md
CHANGED
|
@@ -1,602 +1,302 @@
|
|
|
1
1
|
# react-native-nitro-markdown
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/react-native-nitro-markdown)
|
|
4
|
+
[](https://github.com/JoaoPauloCMarra/react-native-nitro-markdown/blob/main/LICENSE)
|
|
5
|
+
[](https://reactnative.dev/)
|
|
6
|
+
[](https://expo.dev/)
|
|
7
|
+
[](https://nitro.margelo.com/)
|
|
8
|
+
|
|
9
|
+
Markdown parsing, rendering, streaming, and headless AST access for React
|
|
10
|
+
Native, powered by md4c and Nitro Modules.
|
|
11
|
+
|
|
12
|
+
Use it when you need GitHub-flavored Markdown, custom native renderers,
|
|
13
|
+
streaming chat or LLM output, syntax highlighting, math rendering, or headless
|
|
14
|
+
AST access without building your own parser pipeline.
|
|
7
15
|
|
|
8
16
|
<p align="center">
|
|
9
17
|
<img src="https://raw.githubusercontent.com/JoaoPauloCMarra/react-native-nitro-markdown/main/readme/demo.gif" alt="react-native-nitro-markdown demo" width="400" />
|
|
10
18
|
</p>
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## Features
|
|
20
|
+
## Install
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **Math rendering** -- inline and block LaTeX via native RaTeX
|
|
20
|
-
- **Opt-in HTML AST nodes** -- preserve raw HTML as `html_inline` / `html_block` for custom renderers
|
|
21
|
-
- **Large document support** -- top-level block virtualization and cached renderer styles
|
|
22
|
-
- **Code highlighting** -- built-in JS/TS, Python, and Bash tokenizer with custom highlighter support
|
|
23
|
-
- **Headless API** -- parse markdown and extract text without any UI
|
|
24
|
-
- **Streaming** -- incremental rendering for chat/LLM token streams
|
|
25
|
-
- **Customizable** -- themes, per-node style overrides, custom renderers, AST transforms, plugin pipeline
|
|
26
|
-
|
|
27
|
-
## Requirements
|
|
22
|
+
```sh
|
|
23
|
+
bun add react-native-nitro-markdown react-native-nitro-modules ratex-react-native
|
|
24
|
+
```
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|---|---|
|
|
31
|
-
| React Native | `>=0.75.0` |
|
|
32
|
-
| react-native-nitro-modules | `>=0.35.7` |
|
|
33
|
-
| ratex-react-native | `>=0.1.4` |
|
|
26
|
+
For Expo development builds:
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
```sh
|
|
29
|
+
bunx expo install react-native-nitro-markdown react-native-nitro-modules ratex-react-native
|
|
30
|
+
bunx expo prebuild
|
|
31
|
+
```
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
For bare React Native apps:
|
|
38
34
|
|
|
39
|
-
```
|
|
40
|
-
npm install react-native-nitro-markdown react-native-nitro-modules ratex-react-native
|
|
35
|
+
```sh
|
|
41
36
|
cd ios && pod install
|
|
42
37
|
```
|
|
43
38
|
|
|
44
|
-
|
|
39
|
+
Expo Go cannot load Nitro native modules. Use an Expo development build or a
|
|
40
|
+
bare app.
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
bun add react-native-nitro-markdown react-native-nitro-modules ratex-react-native
|
|
48
|
-
cd ios && pod install
|
|
49
|
-
```
|
|
42
|
+
## Expo Config
|
|
50
43
|
|
|
51
|
-
|
|
44
|
+
No package config plugin is required for `react-native-nitro-markdown`.
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
bunx expo
|
|
55
|
-
bunx expo prebuild
|
|
56
|
-
```
|
|
46
|
+
Use your normal Expo app config, install the native dependencies, then run
|
|
47
|
+
`bunx expo prebuild` after adding or upgrading the package.
|
|
57
48
|
|
|
58
49
|
## Quick Start
|
|
59
50
|
|
|
60
51
|
```tsx
|
|
61
52
|
import { Markdown } from "react-native-nitro-markdown";
|
|
62
53
|
|
|
63
|
-
export function
|
|
54
|
+
export function Article() {
|
|
64
55
|
return (
|
|
65
|
-
<Markdown options={{ gfm: true }}>
|
|
56
|
+
<Markdown options={{ gfm: true, math: true }}>
|
|
66
57
|
{"# Hello\nThis is **native** markdown."}
|
|
67
58
|
</Markdown>
|
|
68
59
|
);
|
|
69
60
|
}
|
|
70
61
|
```
|
|
71
62
|
|
|
72
|
-
##
|
|
73
|
-
|
|
74
|
-
### Parser Options
|
|
75
|
-
|
|
76
|
-
`ParserOptions` controls native parser extensions. Defaults are conservative for HTML and feature-complete for Markdown extensions.
|
|
77
|
-
|
|
78
|
-
| Option | Default | Enables | Notes |
|
|
79
|
-
|---|---:|---|---|
|
|
80
|
-
| `gfm` | `true` | Tables, strikethrough, task lists, permissive autolinks | Set `false` for stricter CommonMark-style parsing |
|
|
81
|
-
| `math` | `true` | Inline and display LaTeX spans | Rendered with RaTeX; falls back to styled plain text if the peer dependency is unavailable at runtime |
|
|
82
|
-
| `html` | `false` | `html_inline` and `html_block` AST nodes | Raw HTML is not rendered by default; provide custom renderers |
|
|
63
|
+
## Streaming
|
|
83
64
|
|
|
84
65
|
```tsx
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
### `<Markdown>`
|
|
91
|
-
|
|
92
|
-
The main component. Parses a markdown string and renders it.
|
|
93
|
-
|
|
94
|
-
```tsx
|
|
95
|
-
import { Markdown } from "react-native-nitro-markdown";
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
| Prop | Type | Default | Description |
|
|
99
|
-
|---|---|---|---|
|
|
100
|
-
| `children` | `string` | required | Markdown input string |
|
|
101
|
-
| `options` | `ParserOptions` | -- | Parser flags (`gfm`, `math`, `html`) |
|
|
102
|
-
| `plugins` | `MarkdownPlugin[]` | -- | Plugin hooks (`beforeParse`, `afterParse`) |
|
|
103
|
-
| `sourceAst` | `MarkdownNode` | -- | Pre-parsed AST; skips native parse when provided |
|
|
104
|
-
| `parseCache` | `boolean` | `true` | Enable internal parse result caching for repeated inputs |
|
|
105
|
-
| `astTransform` | `AstTransform` | -- | Post-parse AST rewrite before render |
|
|
106
|
-
| `renderers` | `CustomRenderers` | `{}` | Per-node custom renderer overrides |
|
|
107
|
-
| `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` | Theme token overrides |
|
|
108
|
-
| `styles` | `NodeStyleOverrides` | -- | Per-node style overrides |
|
|
109
|
-
| `stylingStrategy` | `"opinionated" \| "minimal"` | `"opinionated"` | Base styling preset |
|
|
110
|
-
| `style` | `StyleProp<ViewStyle>` | -- | Container style |
|
|
111
|
-
| `onLinkPress` | `LinkPressHandler` | -- | Intercept link presses; return `false` to block default open |
|
|
112
|
-
| `onParsingInProgress` | `() => void` | -- | Called when parse inputs change |
|
|
113
|
-
| `onParseComplete` | `(result) => void` | -- | Called with `{ raw, ast, text }` after parse |
|
|
114
|
-
| `onError` | `(error, phase, pluginName?) => void` | -- | Error handler for parse/plugin failures |
|
|
115
|
-
| `highlightCode` | `boolean \| CodeHighlighter` | -- | Enable syntax highlighting for code blocks |
|
|
116
|
-
| `tableOptions` | `{ minColumnWidth?; measurementStabilizeMs? }` | -- | Table layout tuning |
|
|
117
|
-
| `imageOptions` | `UrlSafetyOptions` | `{ allowedProtocols: ["http:", "https:"] }` | Built-in image URL allowlist |
|
|
118
|
-
| `virtualize` | `boolean \| "auto"` | `false` | Top-level block virtualization |
|
|
119
|
-
| `virtualizationMinBlocks` | `number` | `40` | Block threshold for `"auto"` virtualization |
|
|
120
|
-
| `virtualization` | `MarkdownVirtualizationOptions` | -- | FlatList tuning (windowSize, batching, etc.) |
|
|
121
|
-
|
|
122
|
-
**Pipeline order:** `beforeParse` plugins (by priority desc) -> parse/sourceAst -> `afterParse` plugins (by priority desc) -> `astTransform` -> render.
|
|
123
|
-
|
|
124
|
-
When `sourceAst` is provided, `beforeParse` plugins are skipped because no source string is parsed. `afterParse` plugins still run on the provided AST.
|
|
125
|
-
|
|
126
|
-
`parseCache` defaults to `true` and caches parse results for repeated markdown inputs. Set `parseCache={false}` to force a fresh native parse on each input change. This flag has no effect when `sourceAst` is provided.
|
|
127
|
-
|
|
128
|
-
### `<MarkdownStream>`
|
|
129
|
-
|
|
130
|
-
Renders markdown from a streaming session. Extends `MarkdownProps` (minus `children`).
|
|
131
|
-
|
|
132
|
-
```tsx
|
|
133
|
-
import { MarkdownStream } from "react-native-nitro-markdown";
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
| Prop | Type | Default | Description |
|
|
137
|
-
|---|---|---|---|
|
|
138
|
-
| `session` | `MarkdownSession` | required | Session supplying streamed text |
|
|
139
|
-
| `updateIntervalMs` | `number` | `50` | Flush interval for `"interval"` strategy; ignored by `"raf"` |
|
|
140
|
-
| `updateStrategy` | `"interval" \| "raf"` | `"interval"` | `"interval"` uses `updateIntervalMs`; `"raf"` schedules at most once per animation frame |
|
|
141
|
-
| `useTransitionUpdates` | `boolean` | `false` | Wrap updates in `startTransition` |
|
|
142
|
-
| `incrementalParsing` | `boolean` | `true` | Append-optimized AST updates; disabled automatically when a `beforeParse` plugin is present |
|
|
143
|
-
|
|
144
|
-
### `MarkdownSession`
|
|
145
|
-
|
|
146
|
-
A native text buffer with change listeners, used for streaming.
|
|
147
|
-
|
|
148
|
-
```tsx
|
|
149
|
-
import { createMarkdownSession } from "react-native-nitro-markdown";
|
|
150
|
-
|
|
151
|
-
const session = createMarkdownSession();
|
|
152
|
-
session.append("# Hello\n");
|
|
153
|
-
session.append("Streaming content...");
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
| Method | Signature | Description |
|
|
157
|
-
|---|---|---|
|
|
158
|
-
| `append` | `(chunk: string) => number` | Append text, returns new UTF-16 length |
|
|
159
|
-
| `clear` | `() => void` | Clear buffer, emit reset event |
|
|
160
|
-
| `reset` | `(text: string) => void` | Replace full buffer content |
|
|
161
|
-
| `replace` | `(from, to, text) => number` | Partial buffer mutation; out-of-bounds ranges are clamped and invalid ranges throw |
|
|
162
|
-
| `getAllText` | `() => string` | Get full session text |
|
|
163
|
-
| `getLength` | `() => number` | Get UTF-16 length without copy |
|
|
164
|
-
| `getTextRange` | `(from, to) => string` | Get substring range |
|
|
165
|
-
| `addListener` | `(listener) => () => void` | Subscribe to mutation events; returns unsubscribe |
|
|
166
|
-
| `highlightPosition` | `number` | Mutable cursor for stream highlight |
|
|
167
|
-
| `dispose` | `() => void` | Release native listener and buffer storage; called automatically by `useMarkdownSession` on unmount |
|
|
168
|
-
|
|
169
|
-
### `useMarkdownSession()`
|
|
66
|
+
import { useEffect } from "react";
|
|
67
|
+
import {
|
|
68
|
+
MarkdownStream,
|
|
69
|
+
useMarkdownSession,
|
|
70
|
+
} from "react-native-nitro-markdown";
|
|
170
71
|
|
|
171
|
-
|
|
72
|
+
export function ChatMessage({ text }: { text: string }) {
|
|
73
|
+
const session = useMarkdownSession();
|
|
172
74
|
|
|
173
|
-
|
|
174
|
-
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
session.reset(text);
|
|
77
|
+
}, [session, text]);
|
|
175
78
|
|
|
176
|
-
|
|
177
|
-
|
|
79
|
+
return (
|
|
80
|
+
<MarkdownStream session={session} updateStrategy="raf" incrementalParsing />
|
|
81
|
+
);
|
|
82
|
+
}
|
|
178
83
|
```
|
|
179
84
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Extends `useMarkdownSession` with timeline sync for timed playback.
|
|
85
|
+
For token-by-token output, append to the hook-owned session and let
|
|
86
|
+
`MarkdownStream` subscribe to range updates:
|
|
183
87
|
|
|
184
88
|
```tsx
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const stream = useStream({ 0: 0, 1: 500, 2: 1000 });
|
|
188
|
-
stream.sync(currentTimeMs);
|
|
89
|
+
const session = useMarkdownSession();
|
|
189
90
|
|
|
190
|
-
|
|
91
|
+
session.getSession().append("Hello ");
|
|
92
|
+
session.getSession().append("**world**");
|
|
191
93
|
```
|
|
192
94
|
|
|
193
|
-
|
|
95
|
+
`MarkdownStream` batches updates for append-only text. Use
|
|
96
|
+
`updateStrategy="raf"` for visual streaming, or `updateStrategy="interval"` with
|
|
97
|
+
`updateIntervalMs={50}` to bound update frequency. If any plugin uses
|
|
98
|
+
`beforeParse`, incremental AST optimization is disabled so the full pipeline can
|
|
99
|
+
run correctly. `MarkdownStream` accepts the controller returned by
|
|
100
|
+
`useMarkdownSession()`. Pass `session.getSession()` only when another API needs
|
|
101
|
+
direct access to the native session object.
|
|
194
102
|
|
|
195
|
-
|
|
103
|
+
## Headless Parsing
|
|
196
104
|
|
|
197
|
-
```
|
|
105
|
+
```ts
|
|
198
106
|
import {
|
|
107
|
+
extractPlainText,
|
|
199
108
|
parseMarkdown,
|
|
200
109
|
parseMarkdownWithOptions,
|
|
201
|
-
extractPlainText,
|
|
202
|
-
getTextContent,
|
|
203
|
-
getFlattenedText,
|
|
204
|
-
stripSourceOffsets,
|
|
205
110
|
} from "react-native-nitro-markdown/headless";
|
|
111
|
+
|
|
112
|
+
const ast = parseMarkdown("# Title");
|
|
113
|
+
const astWithMath = parseMarkdownWithOptions("Inline $x^2$", {
|
|
114
|
+
math: true,
|
|
115
|
+
});
|
|
116
|
+
const text = extractPlainText("Hello **world**");
|
|
206
117
|
```
|
|
207
118
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
| `parseMarkdown(text, options?)` | Parse to AST (options: `{ gfm?, math?, html? }`) |
|
|
211
|
-
| `parseMarkdownWithOptions(text, options)` | Parse with explicit options |
|
|
212
|
-
| `extractPlainText(text)` | Parse and return plain text from native parser |
|
|
213
|
-
| `extractPlainTextWithOptions(text, options)` | Same with parser flags |
|
|
214
|
-
| `getTextContent(node)` | Concatenate text recursively (no normalization) |
|
|
215
|
-
| `getFlattenedText(node)` | Normalized plain text with block separators |
|
|
216
|
-
| `stripSourceOffsets(node)` | Remove `beg`/`end` fields from AST |
|
|
119
|
+
Use the `react-native-nitro-markdown/headless` export when you need AST data,
|
|
120
|
+
plain text extraction, indexing, validation, or tests without rendering UI.
|
|
217
121
|
|
|
218
|
-
|
|
122
|
+
## Source AST Rendering
|
|
219
123
|
|
|
220
|
-
|
|
124
|
+
If you already have a `MarkdownNode`, pass it through the `sourceAst` prop to
|
|
125
|
+
skip native parsing during render:
|
|
221
126
|
|
|
222
127
|
```tsx
|
|
223
128
|
import {
|
|
224
129
|
Markdown,
|
|
225
|
-
|
|
226
|
-
type
|
|
227
|
-
} from "react-native-nitro-markdown";
|
|
228
|
-
|
|
229
|
-
<Markdown
|
|
230
|
-
renderers={{
|
|
231
|
-
heading: ({ level, children }: HeadingRendererProps) => (
|
|
232
|
-
<MyHeading level={level}>{children}</MyHeading>
|
|
233
|
-
),
|
|
234
|
-
code_block: ({ language, content }: CodeBlockRendererProps) => (
|
|
235
|
-
<MyCode language={language} content={content} />
|
|
236
|
-
),
|
|
237
|
-
}}
|
|
238
|
-
>
|
|
239
|
-
{content}
|
|
240
|
-
</Markdown>;
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
Renderers receive `EnhancedRendererProps` with `node`, `children`, and `Renderer` (for recursive rendering). Node-specific props are mapped automatically:
|
|
244
|
-
|
|
245
|
-
| Node type | Extra props |
|
|
246
|
-
|---|---|
|
|
247
|
-
| `heading` | `level` |
|
|
248
|
-
| `link` | `href`, `title` |
|
|
249
|
-
| `image` | `url`, `alt`, `title` |
|
|
250
|
-
| `code_block` | `content`, `language` |
|
|
251
|
-
| `code_inline` | `content` |
|
|
252
|
-
| `list` | `ordered`, `start` |
|
|
253
|
-
| `task_list_item` | `checked` |
|
|
254
|
-
| `html_inline`, `html_block` | Read raw HTML from `node.content` |
|
|
255
|
-
|
|
256
|
-
Return `undefined` to fall back to the built-in renderer, or `null` to render nothing.
|
|
257
|
-
|
|
258
|
-
TypeScript users can import specific renderer props for stronger IDE feedback:
|
|
259
|
-
|
|
260
|
-
```tsx
|
|
261
|
-
import type {
|
|
262
|
-
CustomRenderers,
|
|
263
|
-
ImageRendererProps,
|
|
264
|
-
LinkRendererProps,
|
|
265
|
-
MathRendererProps,
|
|
130
|
+
parseMarkdown,
|
|
131
|
+
type MarkdownNode,
|
|
266
132
|
} from "react-native-nitro-markdown";
|
|
267
133
|
|
|
268
|
-
const
|
|
269
|
-
link: ({ href, children }: LinkRendererProps) => (
|
|
270
|
-
<MyLink href={href}>{children}</MyLink>
|
|
271
|
-
),
|
|
272
|
-
image: ({ url, alt }: ImageRendererProps) => (
|
|
273
|
-
<MyImage source={{ uri: url }} accessibilityLabel={alt} />
|
|
274
|
-
),
|
|
275
|
-
math_block: ({ content }: MathRendererProps) => (
|
|
276
|
-
<MyMathBlock latex={content} />
|
|
277
|
-
),
|
|
278
|
-
};
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
Use `satisfies CustomRenderers` when you want excess-property checking while preserving the exact function types for each renderer:
|
|
282
|
-
|
|
283
|
-
```tsx
|
|
284
|
-
import type { CustomRenderers } from "react-native-nitro-markdown";
|
|
285
|
-
|
|
286
|
-
const renderers = {
|
|
287
|
-
heading: ({ level, children }) => (
|
|
288
|
-
<MyHeading variant={`h${level}`}>{children}</MyHeading>
|
|
289
|
-
),
|
|
290
|
-
code_block: ({ language, content }) => (
|
|
291
|
-
<MyCodeBlock language={language ?? "text"} code={content} />
|
|
292
|
-
),
|
|
293
|
-
} satisfies CustomRenderers;
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
#### Rendering HTML Nodes
|
|
297
|
-
|
|
298
|
-
HTML parsing is opt-in and produces raw AST nodes. The built-in renderer intentionally renders nothing for `html_inline` and `html_block`; applications decide what is safe to display.
|
|
299
|
-
|
|
300
|
-
```tsx
|
|
301
|
-
import { Text, View } from "react-native";
|
|
302
|
-
import { Markdown } from "react-native-nitro-markdown";
|
|
134
|
+
const ast: MarkdownNode = parseMarkdown("# Cached AST", { gfm: true });
|
|
303
135
|
|
|
304
|
-
<Markdown
|
|
305
|
-
options={{ html: true }}
|
|
306
|
-
renderers={{
|
|
307
|
-
html_inline: ({ node }) => {
|
|
308
|
-
if (node.content === "<br>") return <Text>{"\n"}</Text>;
|
|
309
|
-
return null;
|
|
310
|
-
},
|
|
311
|
-
html_block: ({ node }) => {
|
|
312
|
-
if (!node.content?.includes('data-kind="release-note"')) return null;
|
|
313
|
-
return (
|
|
314
|
-
<View>
|
|
315
|
-
<Text>Release note</Text>
|
|
316
|
-
</View>
|
|
317
|
-
);
|
|
318
|
-
},
|
|
319
|
-
}}
|
|
320
|
-
>
|
|
321
|
-
{content}
|
|
322
|
-
</Markdown>;
|
|
136
|
+
<Markdown sourceAst={ast}>{"# Cached AST"}</Markdown>;
|
|
323
137
|
```
|
|
324
138
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
### Math Rendering
|
|
139
|
+
When `sourceAst` is provided, `beforeParse` plugins are skipped because parsing
|
|
140
|
+
already happened. `afterParse` plugins and `astTransform` still run.
|
|
328
141
|
|
|
329
|
-
|
|
142
|
+
## Common Options
|
|
330
143
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
144
|
+
| Prop or parser option | Default | What it does |
|
|
145
|
+
| --------------------- | ----------------- | --------------------------------------------------------- |
|
|
146
|
+
| `options.gfm` | `true` | Enables tables, strikethrough, task lists, and autolinks. |
|
|
147
|
+
| `options.math` | `true` | Parses inline and block math nodes. |
|
|
148
|
+
| `options.html` | `false` | Preserves raw HTML nodes for custom renderers. |
|
|
149
|
+
| `parseCache` | `true` | Reuses parsed ASTs for repeated content. |
|
|
150
|
+
| `sourceAst` | `undefined` | Renders a pre-parsed AST instead of parsing `children`. |
|
|
151
|
+
| `highlightCode` | `false` | Enables built-in syntax highlighting. |
|
|
152
|
+
| `tableOptions` | Built-in defaults | Controls table measurement and minimum widths. |
|
|
338
153
|
|
|
339
|
-
|
|
154
|
+
## Custom Rendering
|
|
340
155
|
|
|
341
156
|
```tsx
|
|
342
|
-
import {
|
|
343
|
-
|
|
344
|
-
defaultMarkdownTheme,
|
|
345
|
-
minimalMarkdownTheme,
|
|
346
|
-
mergeThemes,
|
|
347
|
-
} from "react-native-nitro-markdown";
|
|
157
|
+
import { Text } from "react-native";
|
|
158
|
+
import { Markdown, type MarkdownRenderers } from "react-native-nitro-markdown";
|
|
348
159
|
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
160
|
+
const renderers: MarkdownRenderers = {
|
|
161
|
+
paragraph({ children }) {
|
|
162
|
+
return <Text style={{ lineHeight: 22 }}>{children}</Text>;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
353
165
|
|
|
354
|
-
<Markdown
|
|
166
|
+
<Markdown renderers={renderers}>{"Custom paragraph renderer"}</Markdown>;
|
|
355
167
|
```
|
|
356
168
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
- **spacing** -- `xs`, `s`, `m`, `l`, `xl`
|
|
360
|
-
- **fontSizes** -- `xs` through `xl`, plus `h1`-`h6`
|
|
361
|
-
- **fontFamilies** -- `regular`, `heading`, `mono`
|
|
362
|
-
- **borderRadius** -- `s`, `m`, `l`
|
|
363
|
-
- **headingWeight** -- optional font weight override
|
|
364
|
-
- **showCodeLanguage** -- show/hide language label on code blocks
|
|
365
|
-
|
|
366
|
-
Use `stylingStrategy="minimal"` for a bare baseline, or `NodeStyleOverrides` for per-node style overrides (text nodes accept `TextStyle`, container nodes accept `ViewStyle`).
|
|
169
|
+
Custom renderers receive parsed nodes and pre-mapped props for common node
|
|
170
|
+
types. For `html_inline` and `html_block`, read `node.content` directly.
|
|
367
171
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
### Streaming
|
|
172
|
+
For stronger component-local typing, use the node-specific renderer props:
|
|
371
173
|
|
|
372
174
|
```tsx
|
|
373
|
-
import {
|
|
374
|
-
import {
|
|
375
|
-
MarkdownStream,
|
|
376
|
-
useMarkdownSession,
|
|
377
|
-
} from "react-native-nitro-markdown";
|
|
378
|
-
|
|
379
|
-
export function StreamingExample() {
|
|
380
|
-
const session = useMarkdownSession();
|
|
381
|
-
|
|
382
|
-
useEffect(() => {
|
|
383
|
-
const s = session.getSession();
|
|
384
|
-
s.append("# Streaming\n");
|
|
385
|
-
s.append("This text arrives in chunks.");
|
|
386
|
-
return () => session.clear();
|
|
387
|
-
}, [session]);
|
|
175
|
+
import type { CodeBlockRendererProps } from "react-native-nitro-markdown";
|
|
388
176
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
session={session.getSession()}
|
|
392
|
-
options={{ gfm: true }}
|
|
393
|
-
updateStrategy="raf"
|
|
394
|
-
/>
|
|
395
|
-
);
|
|
177
|
+
function CodeBlock({ content, language }: CodeBlockRendererProps) {
|
|
178
|
+
return <Text>{`${language ?? "text"}: ${content}`}</Text>;
|
|
396
179
|
}
|
|
397
180
|
```
|
|
398
181
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
```tsx
|
|
402
|
-
import { useCallback } from "react";
|
|
403
|
-
import { Markdown, type AstTransform } from "react-native-nitro-markdown";
|
|
404
|
-
|
|
405
|
-
const transform = useCallback<AstTransform>((ast) => {
|
|
406
|
-
const visit = (node: Parameters<AstTransform>[0]): typeof node => ({
|
|
407
|
-
...node,
|
|
408
|
-
content:
|
|
409
|
-
node.type === "text"
|
|
410
|
-
? (node.content ?? "").replace(/:wink:/g, "😉")
|
|
411
|
-
: node.content,
|
|
412
|
-
children: node.children?.map(visit),
|
|
413
|
-
});
|
|
414
|
-
return visit(ast);
|
|
415
|
-
}, []);
|
|
416
|
-
|
|
417
|
-
<Markdown astTransform={transform}>{"Hello :wink:"}</Markdown>;
|
|
418
|
-
```
|
|
182
|
+
## Plugin Pipeline
|
|
419
183
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
```tsx
|
|
423
|
-
import { Markdown, type MarkdownPlugin } from "react-native-nitro-markdown";
|
|
184
|
+
```ts
|
|
185
|
+
import type { MarkdownPlugin } from "react-native-nitro-markdown";
|
|
424
186
|
|
|
425
187
|
const plugins: MarkdownPlugin[] = [
|
|
426
188
|
{
|
|
427
|
-
name: "
|
|
189
|
+
name: "mentions",
|
|
428
190
|
priority: 10,
|
|
429
|
-
beforeParse
|
|
430
|
-
|
|
431
|
-
{
|
|
432
|
-
name: "rewrite-after-parse",
|
|
433
|
-
afterParse: (ast) => {
|
|
434
|
-
const visit = (node: typeof ast): typeof ast => ({
|
|
435
|
-
...node,
|
|
436
|
-
content:
|
|
437
|
-
node.type === "text"
|
|
438
|
-
? (node.content ?? "").replace(/ROCKET_TOKEN/g, "🚀")
|
|
439
|
-
: node.content,
|
|
440
|
-
children: node.children?.map(visit),
|
|
441
|
-
});
|
|
442
|
-
return visit(ast);
|
|
191
|
+
beforeParse(source) {
|
|
192
|
+
return source.replaceAll("@team", "**@team**");
|
|
443
193
|
},
|
|
444
194
|
},
|
|
445
195
|
];
|
|
446
|
-
|
|
447
|
-
<Markdown plugins={plugins}>{"Launch :rocket:"}</Markdown>;
|
|
448
196
|
```
|
|
449
197
|
|
|
450
|
-
|
|
198
|
+
Pipeline order: `beforeParse` plugins, parse or `sourceAst`, `afterParse`
|
|
199
|
+
plugins, `astTransform`, then render. Higher `priority` values run first, and
|
|
200
|
+
sorting is stable. `onError` receives `(error, phase, pluginName?)` for parser
|
|
201
|
+
and plugin failures.
|
|
451
202
|
|
|
452
|
-
|
|
453
|
-
import { Markdown, parseMarkdownWithOptions } from "react-native-nitro-markdown";
|
|
203
|
+
## TypeScript Guidance
|
|
454
204
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
html: false,
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
<Markdown sourceAst={ast}>{"ignored when sourceAst is provided"}</Markdown>;
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
With `sourceAst`, `beforeParse` plugins are skipped, while `afterParse` and `astTransform` still apply.
|
|
465
|
-
|
|
466
|
-
### Virtualization (large documents)
|
|
205
|
+
The public types are exported from the root package and the headless subpath.
|
|
206
|
+
Prefer package types over local object shapes so editors and AI tools can catch
|
|
207
|
+
invalid parser options, node names, renderer props, and stream session usage.
|
|
467
208
|
|
|
468
209
|
```tsx
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
{content}
|
|
479
|
-
</Markdown>
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
Keep `Markdown` as the primary vertical scroller when virtualization is enabled -- avoid nesting inside another `ScrollView`.
|
|
210
|
+
import { Text } from "react-native";
|
|
211
|
+
import type {
|
|
212
|
+
CustomRendererPropsByNode,
|
|
213
|
+
MarkdownNode,
|
|
214
|
+
MarkdownPlugin,
|
|
215
|
+
MarkdownRenderers,
|
|
216
|
+
MarkdownStreamProps,
|
|
217
|
+
ParserOptions,
|
|
218
|
+
} from "react-native-nitro-markdown";
|
|
483
219
|
|
|
484
|
-
|
|
220
|
+
const options: ParserOptions = { gfm: true, math: true, html: false };
|
|
485
221
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
<
|
|
222
|
+
function HeadingRenderer({
|
|
223
|
+
children,
|
|
224
|
+
level,
|
|
225
|
+
}: CustomRendererPropsByNode["heading"]) {
|
|
226
|
+
return <Text accessibilityRole="header">{`${level}. ${children}`}</Text>;
|
|
227
|
+
}
|
|
491
228
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return [{ text: code, type: "default" }];
|
|
229
|
+
const renderers: MarkdownRenderers = {
|
|
230
|
+
heading: HeadingRenderer,
|
|
495
231
|
};
|
|
496
232
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
```tsx
|
|
503
|
-
<Markdown
|
|
504
|
-
onLinkPress={(href) => {
|
|
505
|
-
if (href.startsWith("/")) {
|
|
506
|
-
router.push(href);
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
|
-
}}
|
|
510
|
-
>
|
|
511
|
-
{content}
|
|
512
|
-
</Markdown>
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
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`.
|
|
516
|
-
|
|
517
|
-
### Image URL Safety
|
|
518
|
-
|
|
519
|
-
Built-in image rendering only loads `http:` and `https:` URLs by default. Use `imageOptions` to narrow the allowed hosts or, when your app owns the source content, add another protocol explicitly.
|
|
520
|
-
|
|
521
|
-
```tsx
|
|
522
|
-
import { Markdown, type UrlSafetyOptions } from "react-native-nitro-markdown";
|
|
523
|
-
|
|
524
|
-
const imageOptions: UrlSafetyOptions = {
|
|
525
|
-
allowedProtocols: ["https:"],
|
|
526
|
-
allowedHosts: ["cdn.example.com", "images.example.com"],
|
|
233
|
+
const plugin: MarkdownPlugin = {
|
|
234
|
+
name: "strip-tracking",
|
|
235
|
+
afterParse(ast: MarkdownNode) {
|
|
236
|
+
return ast;
|
|
237
|
+
},
|
|
527
238
|
};
|
|
528
239
|
|
|
529
|
-
<
|
|
240
|
+
const streamProps: Pick<MarkdownStreamProps, "updateStrategy"> = {
|
|
241
|
+
updateStrategy: "raf",
|
|
242
|
+
};
|
|
530
243
|
```
|
|
531
244
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
### TypeScript Surface
|
|
535
|
-
|
|
536
|
-
The package exports public types for every commonly customized surface:
|
|
537
|
-
|
|
538
|
-
- `MarkdownProps`, `MarkdownStreamProps`, `MarkdownParseCompleteResult`, `MarkdownErrorPhase`
|
|
539
|
-
- `MarkdownNode`, `MarkdownNodeType`, `HeadingLevel`, `TableCellAlign`, `ParserOptions`, `AstTransform`, `MarkdownPlugin`
|
|
540
|
-
- `CustomRenderers`, `CustomRendererPropsByNode`, and node-specific renderer props
|
|
541
|
-
- `MarkdownTheme`, `PartialMarkdownTheme`, `NodeStyleOverrides`, `TableOptions`, `UrlSafetyOptions`
|
|
542
|
-
- `MarkdownSession`, `CodeHighlighter`, `HighlightedToken`, `TokenType`
|
|
543
|
-
|
|
544
|
-
Prefer typing shared `renderers`, `plugins`, `theme`, `tableOptions`, and `imageOptions` constants instead of relying on inference from inline JSX props. That keeps mistakes visible in IDEs and gives AI-assisted edits a stricter contract to follow.
|
|
545
|
-
|
|
546
|
-
## Supported Node Types
|
|
547
|
-
|
|
548
|
-
`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`
|
|
245
|
+
## API
|
|
549
246
|
|
|
550
|
-
|
|
247
|
+
Main export:
|
|
551
248
|
|
|
552
|
-
|
|
249
|
+
- `Markdown` for rendering complete markdown strings.
|
|
250
|
+
- `MarkdownStream` for incremental rendering.
|
|
251
|
+
- `MarkdownSession` and `useMarkdownSession()` for append/replace/reset flows.
|
|
252
|
+
- `useStream()` for timestamped stream state.
|
|
253
|
+
- `defaultMarkdownTheme` and theme types.
|
|
254
|
+
- Renderer components such as `Paragraph`, `Heading`, `Link`, `CodeBlock`,
|
|
255
|
+
`List`, `Table`, and `Image`.
|
|
256
|
+
- Types including `MarkdownNode`, `MarkdownPlugin`, `CustomRenderers`,
|
|
257
|
+
`MarkdownRenderers`, `CustomRendererPropsByNode`, `ParserOptions`,
|
|
258
|
+
`MarkdownTheme`, `MarkdownSessionController`, and `MarkdownStreamProps`.
|
|
553
259
|
|
|
554
|
-
|
|
260
|
+
Headless export:
|
|
555
261
|
|
|
556
|
-
|
|
262
|
+
- `parseMarkdown`.
|
|
263
|
+
- `parseMarkdownWithOptions`.
|
|
264
|
+
- `extractPlainText`.
|
|
265
|
+
- `extractPlainTextWithOptions`.
|
|
266
|
+
- AST helpers such as `getTextContent`, `getFlattenedText`, and
|
|
267
|
+
`stripSourceOffsets`.
|
|
557
268
|
|
|
558
|
-
|
|
269
|
+
## Platform Support
|
|
559
270
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
- Enable `virtualize="auto"` for long documents and keep `Markdown` as the primary vertical scroller.
|
|
567
|
-
- Memoize custom `plugins`, `renderers`, `theme`, and `styles` objects when they are created inside a component.
|
|
568
|
-
- Use the headless API when you only need parsing, plain text extraction, search indexing, or a custom renderer.
|
|
271
|
+
| Platform | Status |
|
|
272
|
+
| -------- | ------------------------------------------ |
|
|
273
|
+
| iOS | Native parser through Nitro and md4c. |
|
|
274
|
+
| Android | Native parser through Nitro and md4c. |
|
|
275
|
+
| Expo | Development builds. |
|
|
276
|
+
| Web | Not the primary target for native parsing. |
|
|
569
277
|
|
|
570
278
|
## Troubleshooting
|
|
571
279
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
##
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
| Custom | `app/render-custom.tsx` | HTML AST rendering, custom renderers, AST transform |
|
|
590
|
-
| Stream | `app/render-stream.tsx` | Live streaming with token append |
|
|
591
|
-
|
|
592
|
-
## Release Checks
|
|
593
|
-
|
|
594
|
-
- `bun run harness` runs lint, typecheck, JS coverage, benchmark checks, and C++ coverage.
|
|
595
|
-
- `bun run publish-package -- --dry-run --yes` validates release docs, runs publish verification, builds the package, and performs an npm dry run.
|
|
596
|
-
|
|
597
|
-
## Contributing
|
|
280
|
+
- **Expo Go error:** build a dev client; Expo Go cannot load Nitro modules.
|
|
281
|
+
- **Streaming updates too often:** use `updateStrategy="raf"` or an interval
|
|
282
|
+
around 50-100ms.
|
|
283
|
+
- **Plugin changes do not appear incremental:** `beforeParse` plugins force a
|
|
284
|
+
full parse by design.
|
|
285
|
+
- **Math does not render:** ensure `ratex-react-native` is installed and native
|
|
286
|
+
code has been rebuilt.
|
|
287
|
+
|
|
288
|
+
## Development
|
|
289
|
+
|
|
290
|
+
```sh
|
|
291
|
+
bun install
|
|
292
|
+
bun run check
|
|
293
|
+
bun run release:preflight
|
|
294
|
+
bun run example:android
|
|
295
|
+
bun run example:ios
|
|
296
|
+
```
|
|
598
297
|
|
|
599
|
-
|
|
298
|
+
Run native example builds before release when changing native, Nitro, rendering,
|
|
299
|
+
or packaging files.
|
|
600
300
|
|
|
601
301
|
## License
|
|
602
302
|
|