react-native-nitro-markdown 0.6.2 → 0.7.1

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 (75) hide show
  1. package/README.md +128 -503
  2. package/android/gradle.properties +2 -2
  3. package/lib/commonjs/MarkdownSession.js +6 -2
  4. package/lib/commonjs/MarkdownSession.js.map +1 -1
  5. package/lib/commonjs/headless.js +0 -4
  6. package/lib/commonjs/headless.js.map +1 -1
  7. package/lib/commonjs/index.js.map +1 -1
  8. package/lib/commonjs/markdown-stream.js +8 -6
  9. package/lib/commonjs/markdown-stream.js.map +1 -1
  10. package/lib/commonjs/markdown.js +0 -1
  11. package/lib/commonjs/markdown.js.map +1 -1
  12. package/lib/commonjs/renderers/image.js +0 -1
  13. package/lib/commonjs/renderers/image.js.map +1 -1
  14. package/lib/commonjs/renderers/link.js +0 -1
  15. package/lib/commonjs/renderers/link.js.map +1 -1
  16. package/lib/commonjs/renderers/math.js +0 -1
  17. package/lib/commonjs/renderers/math.js.map +1 -1
  18. package/lib/commonjs/use-markdown-stream.js +23 -4
  19. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  20. package/lib/module/MarkdownSession.js +6 -2
  21. package/lib/module/MarkdownSession.js.map +1 -1
  22. package/lib/module/headless.js +0 -4
  23. package/lib/module/headless.js.map +1 -1
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/markdown-stream.js +8 -6
  26. package/lib/module/markdown-stream.js.map +1 -1
  27. package/lib/module/markdown.js +0 -1
  28. package/lib/module/markdown.js.map +1 -1
  29. package/lib/module/renderers/image.js +0 -1
  30. package/lib/module/renderers/image.js.map +1 -1
  31. package/lib/module/renderers/link.js +0 -1
  32. package/lib/module/renderers/link.js.map +1 -1
  33. package/lib/module/renderers/math.js +0 -1
  34. package/lib/module/renderers/math.js.map +1 -1
  35. package/lib/module/use-markdown-stream.js +22 -5
  36. package/lib/module/use-markdown-stream.js.map +1 -1
  37. package/lib/typescript/commonjs/MarkdownSession.d.ts +1 -1
  38. package/lib/typescript/commonjs/MarkdownSession.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/headless.d.ts +6 -3
  40. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/index.d.ts +2 -1
  42. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/markdown-stream.d.ts +2 -1
  44. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/use-markdown-stream.d.ts +4 -1
  50. package/lib/typescript/commonjs/use-markdown-stream.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/headless.d.ts +6 -3
  54. package/lib/typescript/module/headless.d.ts.map +1 -1
  55. package/lib/typescript/module/index.d.ts +2 -1
  56. package/lib/typescript/module/index.d.ts.map +1 -1
  57. package/lib/typescript/module/markdown-stream.d.ts +2 -1
  58. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  59. package/lib/typescript/module/markdown.d.ts.map +1 -1
  60. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  61. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  62. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  63. package/lib/typescript/module/use-markdown-stream.d.ts +4 -1
  64. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  65. package/package.json +6 -5
  66. package/react-native-nitro-markdown.podspec +1 -1
  67. package/src/MarkdownSession.ts +9 -2
  68. package/src/headless.ts +35 -34
  69. package/src/index.ts +9 -1
  70. package/src/markdown-stream.tsx +12 -7
  71. package/src/markdown.tsx +0 -1
  72. package/src/renderers/image.tsx +0 -1
  73. package/src/renderers/link.tsx +0 -1
  74. package/src/renderers/math.tsx +0 -1
  75. package/src/use-markdown-stream.ts +53 -13
package/README.md CHANGED
@@ -1,66 +1,57 @@
1
1
  # react-native-nitro-markdown
2
2
 
3
- [![npm](https://img.shields.io/badge/npm-v0.6.2-orange?style=flat-square)](https://www.npmjs.com/package/react-native-nitro-markdown)
4
- [![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](./LICENSE)
5
- [![react-native](https://img.shields.io/badge/react--native-%3E%3D0.75-1677a4?style=flat-square)](https://reactnative.dev/)
6
- [![nitro-modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.6-black?style=flat-square)](https://github.com/mrousavy/nitro)
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 renderers, streaming
13
+ chat/LLM output, syntax highlighting, math rendering, or native headless parsing
14
+ without building your own renderer 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.6` |
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
- npx expo install react-native-nitro-markdown react-native-nitro-modules ratex-react-native
55
- npx 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
56
  <Markdown options={{ gfm: true }}>
66
57
  {"# Hello\nThis is **native** markdown."}
@@ -69,519 +60,153 @@ export function Example() {
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 |
83
-
84
- ```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()`
170
-
171
- Creates and owns a `MarkdownSession` for a component lifecycle.
172
-
173
- ```tsx
174
- import { useMarkdownSession } from "react-native-nitro-markdown";
175
-
176
- const { getSession, isStreaming, setIsStreaming, stop, clear, setHighlight } =
177
- useMarkdownSession();
178
- ```
179
-
180
- ### `useStream(timestamps?)`
181
-
182
- Extends `useMarkdownSession` with timeline sync for timed playback.
63
+ ## Streaming
183
64
 
184
65
  ```tsx
185
- import { useStream } from "react-native-nitro-markdown";
66
+ import {
67
+ MarkdownStream,
68
+ useMarkdownSession,
69
+ } from "react-native-nitro-markdown";
186
70
 
187
- const stream = useStream({ 0: 0, 1: 500, 2: 1000 });
188
- stream.sync(currentTimeMs);
71
+ export function ChatMessage({ text }: { text: string }) {
72
+ const session = useMarkdownSession(text);
189
73
 
190
- <MarkdownStream session={stream.getSession()} />;
74
+ return (
75
+ <MarkdownStream session={session} updateStrategy="raf" incrementalParsing />
76
+ );
77
+ }
191
78
  ```
192
79
 
193
- ### Headless API
80
+ `MarkdownStream` batches updates for append-only text. If any plugin uses
81
+ `beforeParse`, incremental AST optimization is disabled so the full pipeline can
82
+ run correctly. `MarkdownStream` accepts the controller returned by
83
+ `useMarkdownSession()`. Pass `session.getSession()` only when another API needs
84
+ direct access to the native session object.
194
85
 
195
- Parse markdown without any UI. Available from both entry points.
86
+ ## Headless Parsing
196
87
 
197
- ```tsx
88
+ ```ts
198
89
  import {
90
+ extractPlainText,
199
91
  parseMarkdown,
200
92
  parseMarkdownWithOptions,
201
- extractPlainText,
202
- getTextContent,
203
- getFlattenedText,
204
- stripSourceOffsets,
205
93
  } from "react-native-nitro-markdown/headless";
206
- ```
207
-
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 |
217
-
218
- ### Custom Renderers
219
-
220
- Override rendering for specific node types:
221
-
222
- ```tsx
223
- import {
224
- 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,
266
- } from "react-native-nitro-markdown";
267
94
 
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
- #### Rendering HTML Nodes
282
-
283
- 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.
284
-
285
- ```tsx
286
- import { Text, View } from "react-native";
287
- import { Markdown } from "react-native-nitro-markdown";
288
-
289
- <Markdown
290
- options={{ html: true }}
291
- renderers={{
292
- html_inline: ({ node }) => {
293
- if (node.content === "<br>") return <Text>{"\n"}</Text>;
294
- return null;
295
- },
296
- html_block: ({ node }) => {
297
- if (!node.content?.includes('data-kind="release-note"')) return null;
298
- return (
299
- <View>
300
- <Text>Release note</Text>
301
- </View>
302
- );
303
- },
304
- }}
305
- >
306
- {content}
307
- </Markdown>;
308
- ```
309
-
310
- Do not pass untrusted HTML directly into a WebView from a renderer. Sanitize or map known tags/attributes to native components.
311
-
312
- ### Math Rendering
313
-
314
- Math parsing is enabled by default. Rendering LaTeX uses the native RaTeX renderer:
315
-
316
- ```bash
317
- bun add ratex-react-native
318
- ```
319
-
320
- If `ratex-react-native` is unavailable at runtime, `math_inline` and `math_block` render as styled plain text fallback.
321
-
322
- 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.
323
-
324
- ### Theme API
325
-
326
- ```tsx
327
- import {
328
- Markdown,
329
- defaultMarkdownTheme,
330
- minimalMarkdownTheme,
331
- mergeThemes,
332
- } from "react-native-nitro-markdown";
333
-
334
- const theme = mergeThemes(defaultMarkdownTheme, {
335
- colors: { text: "#0f172a", link: "#1d4ed8" },
336
- fontSizes: { m: 16 },
95
+ const ast = parseMarkdown("# Title");
96
+ const astWithOffsets = parseMarkdownWithOptions("# Title", {
97
+ sourceAst: true,
337
98
  });
338
-
339
- <Markdown theme={theme}>{content}</Markdown>;
99
+ const text = extractPlainText("Hello **world**");
340
100
  ```
341
101
 
342
- `MarkdownTheme` includes:
343
- - **colors** -- `text`, `heading`, `link`, `code`, `codeBackground`, `blockquote`, `border`, `surface`, table colors, and optional `codeTokenColors` for syntax highlighting
344
- - **spacing** -- `xs`, `s`, `m`, `l`, `xl`
345
- - **fontSizes** -- `xs` through `xl`, plus `h1`-`h6`
346
- - **fontFamilies** -- `regular`, `heading`, `mono`
347
- - **borderRadius** -- `s`, `m`, `l`
348
- - **headingWeight** -- optional font weight override
349
- - **showCodeLanguage** -- show/hide language label on code blocks
102
+ Use the `react-native-nitro-markdown/headless` export when you need AST data,
103
+ plain text extraction, indexing, validation, or tests without rendering UI.
350
104
 
351
- Use `stylingStrategy="minimal"` for a bare baseline, or `NodeStyleOverrides` for per-node style overrides (text nodes accept `TextStyle`, container nodes accept `ViewStyle`).
105
+ ## Common Options
352
106
 
353
- ## Examples
107
+ | Option | Default | What it does |
108
+ | --------------- | ----------------- | --------------------------------------------------------- |
109
+ | `gfm` | `true` | Enables tables, strikethrough, task lists, and autolinks. |
110
+ | `parseCache` | `true` | Reuses parsed ASTs for repeated content. |
111
+ | `sourceAst` | `false` | Includes source offsets for tooling and editor features. |
112
+ | `allowHtml` | `false` | Preserves raw HTML nodes for custom renderers. |
113
+ | `highlightCode` | `false` | Enables built-in syntax highlighting. |
114
+ | `tableOptions` | Built-in defaults | Controls table measurement and minimum widths. |
354
115
 
355
- ### Streaming
116
+ ## Custom Rendering
356
117
 
357
118
  ```tsx
358
- import { useEffect } from "react";
359
- import {
360
- MarkdownStream,
361
- useMarkdownSession,
362
- } from "react-native-nitro-markdown";
363
-
364
- export function StreamingExample() {
365
- const session = useMarkdownSession();
119
+ import type { MarkdownRenderers } from "react-native-nitro-markdown";
120
+ import { Markdown } from "react-native-nitro-markdown";
366
121
 
367
- useEffect(() => {
368
- const s = session.getSession();
369
- s.append("# Streaming\n");
370
- s.append("This text arrives in chunks.");
371
- return () => session.clear();
372
- }, [session]);
122
+ const renderers: MarkdownRenderers = {
123
+ paragraph({ children }) {
124
+ return <Text style={{ lineHeight: 22 }}>{children}</Text>;
125
+ },
126
+ };
373
127
 
374
- return (
375
- <MarkdownStream
376
- session={session.getSession()}
377
- options={{ gfm: true }}
378
- updateStrategy="raf"
379
- />
380
- );
381
- }
128
+ <Markdown renderers={renderers}>{"Custom paragraph renderer"}</Markdown>;
382
129
  ```
383
130
 
384
- ### AST Transform
131
+ Custom renderers receive parsed nodes and pre-mapped props for common node
132
+ types. For `html_inline` and `html_block`, read `node.content` directly.
385
133
 
386
- ```tsx
387
- import { useCallback } from "react";
388
- import { Markdown, type AstTransform } from "react-native-nitro-markdown";
389
-
390
- const transform = useCallback<AstTransform>((ast) => {
391
- const visit = (node: Parameters<AstTransform>[0]): typeof node => ({
392
- ...node,
393
- content:
394
- node.type === "text"
395
- ? (node.content ?? "").replace(/:wink:/g, "😉")
396
- : node.content,
397
- children: node.children?.map(visit),
398
- });
399
- return visit(ast);
400
- }, []);
401
-
402
- <Markdown astTransform={transform}>{"Hello :wink:"}</Markdown>;
403
- ```
134
+ ## Plugin Pipeline
404
135
 
405
- ### Plugin Pipeline
406
-
407
- ```tsx
408
- import { Markdown, type MarkdownPlugin } from "react-native-nitro-markdown";
136
+ ```ts
137
+ import type { MarkdownPlugin } from "react-native-nitro-markdown";
409
138
 
410
139
  const plugins: MarkdownPlugin[] = [
411
140
  {
412
- name: "rewrite-before-parse",
141
+ name: "mentions",
413
142
  priority: 10,
414
- beforeParse: (input) => input.replace(/:rocket:/g, "ROCKET_TOKEN"),
415
- },
416
- {
417
- name: "rewrite-after-parse",
418
- afterParse: (ast) => {
419
- const visit = (node: typeof ast): typeof ast => ({
420
- ...node,
421
- content:
422
- node.type === "text"
423
- ? (node.content ?? "").replace(/ROCKET_TOKEN/g, "🚀")
424
- : node.content,
425
- children: node.children?.map(visit),
426
- });
427
- return visit(ast);
143
+ beforeParse(source) {
144
+ return source.replaceAll("@team", "**@team**");
428
145
  },
429
146
  },
430
147
  ];
431
-
432
- <Markdown plugins={plugins}>{"Launch :rocket:"}</Markdown>;
433
148
  ```
434
149
 
435
- ### Pre-parsed AST
436
-
437
- ```tsx
438
- import { Markdown, parseMarkdownWithOptions } from "react-native-nitro-markdown";
150
+ Pipeline order: `beforeParse` plugins, parse or `sourceAst`, `afterParse`
151
+ plugins, `astTransform`, then render. When `sourceAst` is provided, `beforeParse` plugins are skipped
152
+ because parsing already happened. Higher `priority` values run first, and
153
+ sorting is stable.
439
154
 
440
- const ast = parseMarkdownWithOptions(content, {
441
- gfm: true,
442
- math: true,
443
- html: false,
444
- });
155
+ ## API
445
156
 
446
- <Markdown sourceAst={ast}>{"ignored when sourceAst is provided"}</Markdown>;
447
- ```
157
+ Main export:
448
158
 
449
- With `sourceAst`, `beforeParse` plugins are skipped, while `afterParse` and `astTransform` still apply.
159
+ - `Markdown` for rendering complete markdown strings.
160
+ - `MarkdownStream` for incremental rendering.
161
+ - `MarkdownSession` and `useMarkdownSession()` for append/replace/reset flows.
162
+ - `useStream()` for timestamped stream state.
163
+ - `defaultMarkdownTheme` and theme types.
164
+ - Renderer components such as `Paragraph`, `Heading`, `Link`, `CodeBlock`,
165
+ `List`, `Table`, and `Image`.
166
+ - Types including `MarkdownNode`, `MarkdownPlugin`, `MarkdownRenderers`,
167
+ `ParserOptions`, `MarkdownTheme`, `MarkdownSessionController`, and
168
+ `MarkdownStreamProps`.
450
169
 
451
- ### Virtualization (large documents)
170
+ Headless export:
452
171
 
453
- ```tsx
454
- <Markdown
455
- virtualize="auto"
456
- virtualizationMinBlocks={30}
457
- virtualization={{
458
- initialNumToRender: 10,
459
- maxToRenderPerBatch: 10,
460
- windowSize: 8,
461
- }}
462
- >
463
- {content}
464
- </Markdown>
465
- ```
172
+ - `parseMarkdown`.
173
+ - `parseMarkdownWithOptions`.
174
+ - `extractPlainText`.
175
+ - `extractPlainTextWithOptions`.
176
+ - AST helpers such as `getTextContent`, `getFlattenedText`, and
177
+ `stripSourceOffsets`.
466
178
 
467
- Keep `Markdown` as the primary vertical scroller when virtualization is enabled -- avoid nesting inside another `ScrollView`.
179
+ ## Platform Support
468
180
 
469
- ### Syntax Highlighting
470
-
471
- ```tsx
472
- import type { CodeHighlighter } from "react-native-nitro-markdown";
473
-
474
- // Built-in highlighter (JS/TS, Python, Bash)
475
- <Markdown highlightCode>{"```typescript\nconst x: number = 42;\n```"}</Markdown>
476
-
477
- // Custom highlighter
478
- const myHighlighter: CodeHighlighter = (language, code) => {
479
- return [{ text: code, type: "default" }];
480
- };
481
-
482
- <Markdown highlightCode={myHighlighter}>{content}</Markdown>
483
- ```
484
-
485
- ### Link Interception
486
-
487
- ```tsx
488
- <Markdown
489
- onLinkPress={(href) => {
490
- if (href.startsWith("/")) {
491
- router.push(href);
492
- return false;
493
- }
494
- }}
495
- >
496
- {content}
497
- </Markdown>
498
- ```
499
-
500
- 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`.
501
-
502
- ### Image URL Safety
503
-
504
- 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.
505
-
506
- ```tsx
507
- import { Markdown, type UrlSafetyOptions } from "react-native-nitro-markdown";
508
-
509
- const imageOptions: UrlSafetyOptions = {
510
- allowedProtocols: ["https:"],
511
- allowedHosts: ["cdn.example.com", "images.example.com"],
512
- };
513
-
514
- <Markdown imageOptions={imageOptions}>{content}</Markdown>;
515
- ```
516
-
517
- 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.
518
-
519
- ### TypeScript Surface
520
-
521
- The package exports public types for every commonly customized surface:
522
-
523
- - `MarkdownProps`, `MarkdownStreamProps`, `MarkdownParseCompleteResult`, `MarkdownErrorPhase`
524
- - `MarkdownNode`, `ParserOptions`, `AstTransform`, `MarkdownPlugin`
525
- - `CustomRenderers`, `CustomRendererPropsByNode`, and node-specific renderer props
526
- - `MarkdownTheme`, `PartialMarkdownTheme`, `NodeStyleOverrides`, `TableOptions`, `UrlSafetyOptions`
527
- - `MarkdownSession`, `CodeHighlighter`, `HighlightedToken`, `TokenType`
528
-
529
- 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.
530
-
531
- ## Supported Node Types
532
-
533
- `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`
534
-
535
- `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`.
536
-
537
- ## Package Exports
538
-
539
- ### Main (`react-native-nitro-markdown`)
540
-
541
- Components, hooks, sessions, themes, built-in renderers, syntax highlighting, and all headless APIs.
542
-
543
- ### Headless (`react-native-nitro-markdown/headless`)
544
-
545
- Parser and text utilities only -- no React dependencies. Use this for server-side processing, search indexing, or custom renderers.
546
-
547
- ## Performance Tips
548
-
549
- - Use `updateStrategy="raf"` for frame-aligned streaming UI updates; `updateIntervalMs` is ignored by `"raf"`.
550
- - Batch `session.append()` calls at 50-100ms intervals rather than per-character.
551
- - Enable `virtualize="auto"` for long documents and keep `Markdown` as the primary vertical scroller.
552
- - Memoize custom `plugins`, `renderers`, `theme`, and `styles` objects when they are created inside a component.
553
- - Use the headless API when you only need parsing, plain text extraction, search indexing, or a custom renderer.
181
+ | Platform | Status |
182
+ | -------- | ------------------------------------------ |
183
+ | iOS | Native parser through Nitro and md4c. |
184
+ | Android | Native parser through Nitro and md4c. |
185
+ | Expo | Development builds. |
186
+ | Web | Not the primary target for native parsing. |
554
187
 
555
188
  ## Troubleshooting
556
189
 
557
- | Problem | Solution |
558
- |---|---|
559
- | Math renders as code-style fallback | Install `ratex-react-native` |
560
- | Large inline math overflows text | Use block math (`$$...$$`) for wide formulas; inline math intentionally stays in text flow |
561
- | iOS build fails after install | Run `pod install` in your iOS directory |
562
- | Expo app doesn't load native module | Use a development build (`expo prebuild` + `expo run`), not Expo Go |
563
- | Android heading font weight looks wrong | Set `theme.headingWeight` explicitly |
564
-
565
- ## Example App
190
+ - **Expo Go error:** build a dev client; Expo Go cannot load Nitro modules.
191
+ - **Streaming updates too often:** use `updateStrategy="raf"` or an interval
192
+ around 50-100ms.
193
+ - **Plugin changes do not appear incremental:** `beforeParse` plugins force a
194
+ full parse by design.
195
+ - **Math does not render:** ensure `ratex-react-native` is installed and native
196
+ code has been rebuilt.
566
197
 
567
- The `apps/example` directory contains a full demo app with these screens:
198
+ ## Development
568
199
 
569
- | Screen | File | Demonstrates |
570
- |---|---|---|
571
- | Bench | `app/index.tsx` | Smoke tests + benchmark vs JS parsers |
572
- | Default | `app/render-default.tsx` | Built-in renderer defaults |
573
- | Styles | `app/render-default-styles.tsx` | Theme and style overrides |
574
- | Custom | `app/render-custom.tsx` | HTML AST rendering, custom renderers, AST transform |
575
- | Stream | `app/render-stream.tsx` | Live streaming with token append |
576
-
577
- ## Release Checks
578
-
579
- - `bun run harness` runs lint, typecheck, JS coverage, benchmark checks, and C++ coverage.
580
- - `bun run publish-package -- --dry-run --yes` validates release docs, runs publish verification, builds the package, and performs an npm dry run.
581
-
582
- ## Contributing
200
+ ```sh
201
+ bun install
202
+ bun run check
203
+ bun run release:preflight
204
+ bun run example:android
205
+ bun run example:ios
206
+ ```
583
207
 
584
- See [CONTRIBUTING.md](./CONTRIBUTING.md).
208
+ Run native example builds before release when changing native, Nitro, rendering,
209
+ or packaging files.
585
210
 
586
211
  ## License
587
212