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.
Files changed (73) hide show
  1. package/README.md +188 -488
  2. package/lib/commonjs/MarkdownContext.js.map +1 -1
  3. package/lib/commonjs/MarkdownSession.js +6 -2
  4. package/lib/commonjs/MarkdownSession.js.map +1 -1
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/markdown-stream.js +49 -14
  7. package/lib/commonjs/markdown-stream.js.map +1 -1
  8. package/lib/commonjs/markdown.js +2 -1
  9. package/lib/commonjs/markdown.js.map +1 -1
  10. package/lib/commonjs/renderers/image.js +9 -0
  11. package/lib/commonjs/renderers/image.js.map +1 -1
  12. package/lib/commonjs/renderers/math.js +16 -0
  13. package/lib/commonjs/renderers/math.js.map +1 -1
  14. package/lib/commonjs/use-markdown-stream.js +25 -10
  15. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  16. package/lib/commonjs/utils/incremental-ast.js +45 -5
  17. package/lib/commonjs/utils/incremental-ast.js.map +1 -1
  18. package/lib/module/MarkdownContext.js.map +1 -1
  19. package/lib/module/MarkdownSession.js +6 -2
  20. package/lib/module/MarkdownSession.js.map +1 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/markdown-stream.js +50 -15
  23. package/lib/module/markdown-stream.js.map +1 -1
  24. package/lib/module/markdown.js +2 -1
  25. package/lib/module/markdown.js.map +1 -1
  26. package/lib/module/renderers/image.js +10 -1
  27. package/lib/module/renderers/image.js.map +1 -1
  28. package/lib/module/renderers/math.js +16 -0
  29. package/lib/module/renderers/math.js.map +1 -1
  30. package/lib/module/use-markdown-stream.js +24 -11
  31. package/lib/module/use-markdown-stream.js.map +1 -1
  32. package/lib/module/utils/incremental-ast.js +43 -4
  33. package/lib/module/utils/incremental-ast.js.map +1 -1
  34. package/lib/typescript/commonjs/MarkdownContext.d.ts +1 -0
  35. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/MarkdownSession.d.ts +1 -1
  37. package/lib/typescript/commonjs/MarkdownSession.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/index.d.ts +2 -1
  39. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/markdown-stream.d.ts +2 -1
  41. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/use-markdown-stream.d.ts +4 -1
  46. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/utils/incremental-ast.d.ts +1 -0
  48. package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -1
  49. package/lib/typescript/module/MarkdownContext.d.ts +1 -0
  50. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  51. package/lib/typescript/module/MarkdownSession.d.ts +1 -1
  52. package/lib/typescript/module/MarkdownSession.d.ts.map +1 -1
  53. package/lib/typescript/module/index.d.ts +2 -1
  54. package/lib/typescript/module/index.d.ts.map +1 -1
  55. package/lib/typescript/module/markdown-stream.d.ts +2 -1
  56. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  57. package/lib/typescript/module/markdown.d.ts.map +1 -1
  58. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  59. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  60. package/lib/typescript/module/use-markdown-stream.d.ts +4 -1
  61. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  62. package/lib/typescript/module/utils/incremental-ast.d.ts +1 -0
  63. package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -1
  64. package/package.json +2 -1
  65. package/src/MarkdownContext.ts +2 -0
  66. package/src/MarkdownSession.ts +9 -2
  67. package/src/index.ts +2 -0
  68. package/src/markdown-stream.tsx +64 -18
  69. package/src/markdown.tsx +7 -1
  70. package/src/renderers/image.tsx +11 -0
  71. package/src/renderers/math.tsx +18 -0
  72. package/src/use-markdown-stream.ts +55 -19
  73. 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
- [![npm](https://img.shields.io/npm/v/react-native-nitro-markdown?style=flat-square&color=orange)](https://www.npmjs.com/package/react-native-nitro-markdown)
4
- [![license](https://img.shields.io/npm/l/react-native-nitro-markdown?style=flat-square&color=blue)](https://github.com/JoaoPauloCMarra/react-native-nitro-markdown/blob/main/LICENSE)
5
- [![react-native](https://img.shields.io/badge/react--native-%3E%3D0.75-1677a4?style=flat-square)](https://reactnative.dev/docs/environment-setup)
6
- [![nitro-modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.7-black?style=flat-square)](https://nitro.margelo.com/)
3
+ [![npm version](https://img.shields.io/npm/v/react-native-nitro-markdown?color=f97316&label=npm)](https://www.npmjs.com/package/react-native-nitro-markdown)
4
+ [![license](https://img.shields.io/npm/l/react-native-nitro-markdown?color=007ec6)](https://github.com/JoaoPauloCMarra/react-native-nitro-markdown/blob/main/LICENSE)
5
+ [![React Native](https://img.shields.io/badge/react--native-%3E%3D0.75-61dafb)](https://reactnative.dev/)
6
+ [![Expo](https://img.shields.io/badge/expo-SDK%2056-000020)](https://expo.dev/)
7
+ [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.7-black)](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
- 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).
13
-
14
- ## Features
20
+ ## Install
15
21
 
16
- - **Native C++ parser** -- synchronous parsing via JSI with minimal JS thread overhead
17
- - **Full rendering pipeline** -- parser + renderer + streaming session in one package
18
- - **GFM support** -- tables, strikethrough, task lists, autolinks
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
- | Dependency | Version |
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
- ## Installation
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
- With npm:
33
+ For bare React Native apps:
38
34
 
39
- ```bash
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
- With Bun:
39
+ Expo Go cannot load Nitro native modules. Use an Expo development build or a
40
+ bare app.
45
41
 
46
- ```bash
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
- **Expo** (development build):
44
+ No package config plugin is required for `react-native-nitro-markdown`.
52
45
 
53
- ```bash
54
- bunx expo install react-native-nitro-markdown react-native-nitro-modules ratex-react-native
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 Example() {
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
- ## API Reference
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
- <Markdown options={{ gfm: true, math: true, html: false }}>
86
- {content}
87
- </Markdown>
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
- Creates and owns a `MarkdownSession` for a component lifecycle.
72
+ export function ChatMessage({ text }: { text: string }) {
73
+ const session = useMarkdownSession();
172
74
 
173
- ```tsx
174
- import { useMarkdownSession } from "react-native-nitro-markdown";
75
+ useEffect(() => {
76
+ session.reset(text);
77
+ }, [session, text]);
175
78
 
176
- const { getSession, isStreaming, setIsStreaming, stop, clear, setHighlight } =
177
- useMarkdownSession();
79
+ return (
80
+ <MarkdownStream session={session} updateStrategy="raf" incrementalParsing />
81
+ );
82
+ }
178
83
  ```
179
84
 
180
- ### `useStream(timestamps?)`
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
- import { useStream } from "react-native-nitro-markdown";
186
-
187
- const stream = useStream({ 0: 0, 1: 500, 2: 1000 });
188
- stream.sync(currentTimeMs);
89
+ const session = useMarkdownSession();
189
90
 
190
- <MarkdownStream session={stream.getSession()} />;
91
+ session.getSession().append("Hello ");
92
+ session.getSession().append("**world**");
191
93
  ```
192
94
 
193
- ### Headless API
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
- Parse markdown without any UI. Available from both entry points.
103
+ ## Headless Parsing
196
104
 
197
- ```tsx
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
- | Function | Description |
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
- ### Custom Renderers
122
+ ## Source AST Rendering
219
123
 
220
- Override rendering for specific node types:
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
- type HeadingRendererProps,
226
- type CodeBlockRendererProps,
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 renderers: CustomRenderers = {
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
- Do not pass untrusted HTML directly into a WebView from a renderer. Sanitize or map known tags/attributes to native components.
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
- Math parsing is enabled by default. Rendering LaTeX uses the native RaTeX renderer:
142
+ ## Common Options
330
143
 
331
- ```bash
332
- bun add ratex-react-native
333
- ```
334
-
335
- If `ratex-react-native` is unavailable at runtime, `math_inline` and `math_block` render as styled plain text fallback.
336
-
337
- Inline math (`$...$`) stays inside text flow. Use block math (`$$...$$`) for large formulas; the default `math_block` renderer is horizontally scrollable when the equation is wider than the screen. Custom `math_block` renderers should preserve the same behavior for long display equations.
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
- ### Theme API
154
+ ## Custom Rendering
340
155
 
341
156
  ```tsx
342
- import {
343
- Markdown,
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 theme = mergeThemes(defaultMarkdownTheme, {
350
- colors: { text: "#0f172a", link: "#1d4ed8" },
351
- fontSizes: { m: 16 },
352
- });
160
+ const renderers: MarkdownRenderers = {
161
+ paragraph({ children }) {
162
+ return <Text style={{ lineHeight: 22 }}>{children}</Text>;
163
+ },
164
+ };
353
165
 
354
- <Markdown theme={theme}>{content}</Markdown>;
166
+ <Markdown renderers={renderers}>{"Custom paragraph renderer"}</Markdown>;
355
167
  ```
356
168
 
357
- `MarkdownTheme` includes:
358
- - **colors** -- `text`, `heading`, `link`, `code`, `codeBackground`, `blockquote`, `border`, `surface`, table colors, and optional `codeTokenColors` for syntax highlighting
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
- ## Examples
369
-
370
- ### Streaming
172
+ For stronger component-local typing, use the node-specific renderer props:
371
173
 
372
174
  ```tsx
373
- import { useEffect } from "react";
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
- return (
390
- <MarkdownStream
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
- ### AST Transform
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
- ### Plugin Pipeline
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: "rewrite-before-parse",
189
+ name: "mentions",
428
190
  priority: 10,
429
- beforeParse: (input) => input.replace(/:rocket:/g, "ROCKET_TOKEN"),
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
- ### Pre-parsed AST
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
- ```tsx
453
- import { Markdown, parseMarkdownWithOptions } from "react-native-nitro-markdown";
203
+ ## TypeScript Guidance
454
204
 
455
- const ast = parseMarkdownWithOptions(content, {
456
- gfm: true,
457
- math: true,
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
- <Markdown
470
- virtualize="auto"
471
- virtualizationMinBlocks={30}
472
- virtualization={{
473
- initialNumToRender: 10,
474
- maxToRenderPerBatch: 10,
475
- windowSize: 8,
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
- ### Syntax Highlighting
220
+ const options: ParserOptions = { gfm: true, math: true, html: false };
485
221
 
486
- ```tsx
487
- import type { CodeHighlighter } from "react-native-nitro-markdown";
488
-
489
- // Built-in highlighter (JS/TS, Python, Bash)
490
- <Markdown highlightCode>{"```typescript\nconst x: number = 42;\n```"}</Markdown>
222
+ function HeadingRenderer({
223
+ children,
224
+ level,
225
+ }: CustomRendererPropsByNode["heading"]) {
226
+ return <Text accessibilityRole="header">{`${level}. ${children}`}</Text>;
227
+ }
491
228
 
492
- // Custom highlighter
493
- const myHighlighter: CodeHighlighter = (language, code) => {
494
- return [{ text: code, type: "default" }];
229
+ const renderers: MarkdownRenderers = {
230
+ heading: HeadingRenderer,
495
231
  };
496
232
 
497
- <Markdown highlightCode={myHighlighter}>{content}</Markdown>
498
- ```
499
-
500
- ### Link Interception
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
- <Markdown imageOptions={imageOptions}>{content}</Markdown>;
240
+ const streamProps: Pick<MarkdownStreamProps, "updateStrategy"> = {
241
+ updateStrategy: "raf",
242
+ };
530
243
  ```
531
244
 
532
- Relative image URLs are not loaded by the default renderer. Map them in a custom `image` renderer when your app has a trusted asset resolver.
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
- `html_inline` and `html_block` are parsed only when `options.html` is `true`; they are not rendered by default. Use a custom renderer to handle them, and read raw HTML from `node.content`.
247
+ Main export:
551
248
 
552
- ## Package Exports
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
- ### Main (`react-native-nitro-markdown`)
260
+ Headless export:
555
261
 
556
- Components, hooks, sessions, themes, built-in renderers, syntax highlighting, and all headless APIs.
262
+ - `parseMarkdown`.
263
+ - `parseMarkdownWithOptions`.
264
+ - `extractPlainText`.
265
+ - `extractPlainTextWithOptions`.
266
+ - AST helpers such as `getTextContent`, `getFlattenedText`, and
267
+ `stripSourceOffsets`.
557
268
 
558
- ### Headless (`react-native-nitro-markdown/headless`)
269
+ ## Platform Support
559
270
 
560
- Parser and text utilities only -- no React dependencies. Use this for server-side processing, search indexing, or custom renderers.
561
-
562
- ## Performance Tips
563
-
564
- - Use `updateStrategy="raf"` for frame-aligned streaming UI updates; `updateIntervalMs` is ignored by `"raf"`.
565
- - Batch `session.append()` calls at 50-100ms intervals rather than per-character.
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
- | Problem | Solution |
573
- |---|---|
574
- | Math renders as code-style fallback | Install `ratex-react-native` |
575
- | Large inline math overflows text | Use block math (`$$...$$`) for wide formulas; inline math intentionally stays in text flow |
576
- | iOS build fails after install | Run `pod install` in your iOS directory |
577
- | Expo app doesn't load native module | Use a development build (`expo prebuild` + `expo run`), not Expo Go |
578
- | Android heading font weight looks wrong | Set `theme.headingWeight` explicitly |
579
-
580
- ## Example App
581
-
582
- The `apps/example` directory contains a full demo app with these screens:
583
-
584
- | Screen | File | Demonstrates |
585
- |---|---|---|
586
- | Bench | `app/index.tsx` | Smoke tests + benchmark vs JS parsers |
587
- | Default | `app/render-default.tsx` | Built-in renderer defaults |
588
- | Styles | `app/render-default-styles.tsx` | Theme and style overrides |
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
- See [CONTRIBUTING.md](https://github.com/JoaoPauloCMarra/react-native-nitro-markdown/blob/main/CONTRIBUTING.md).
298
+ Run native example builds before release when changing native, Nitro, rendering,
299
+ or packaging files.
600
300
 
601
301
  ## License
602
302