react-native-nitro-markdown 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +329 -322
  2. package/lib/commonjs/MarkdownContext.js +2 -1
  3. package/lib/commonjs/MarkdownContext.js.map +1 -1
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/markdown-stream.js +3 -1
  6. package/lib/commonjs/markdown-stream.js.map +1 -1
  7. package/lib/commonjs/markdown.js +51 -35
  8. package/lib/commonjs/markdown.js.map +1 -1
  9. package/lib/commonjs/renderers/code.js +3 -3
  10. package/lib/commonjs/renderers/code.js.map +1 -1
  11. package/lib/commonjs/renderers/heading.js +1 -1
  12. package/lib/commonjs/renderers/heading.js.map +1 -1
  13. package/lib/commonjs/renderers/image.js +7 -5
  14. package/lib/commonjs/renderers/image.js.map +1 -1
  15. package/lib/commonjs/renderers/link.js +15 -3
  16. package/lib/commonjs/renderers/link.js.map +1 -1
  17. package/lib/commonjs/renderers/list.js +2 -2
  18. package/lib/commonjs/renderers/list.js.map +1 -1
  19. package/lib/commonjs/renderers/table.js +32 -15
  20. package/lib/commonjs/renderers/table.js.map +1 -1
  21. package/lib/commonjs/use-markdown-stream.js +16 -14
  22. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  23. package/lib/commonjs/utils/link-security.js +21 -0
  24. package/lib/commonjs/utils/link-security.js.map +1 -0
  25. package/lib/commonjs/utils/stream-timeline.js +62 -0
  26. package/lib/commonjs/utils/stream-timeline.js.map +1 -0
  27. package/lib/module/MarkdownContext.js +2 -1
  28. package/lib/module/MarkdownContext.js.map +1 -1
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/markdown-stream.js +3 -1
  31. package/lib/module/markdown-stream.js.map +1 -1
  32. package/lib/module/markdown.js +52 -36
  33. package/lib/module/markdown.js.map +1 -1
  34. package/lib/module/renderers/blockquote.js.map +1 -1
  35. package/lib/module/renderers/code.js +3 -3
  36. package/lib/module/renderers/code.js.map +1 -1
  37. package/lib/module/renderers/heading.js +1 -1
  38. package/lib/module/renderers/heading.js.map +1 -1
  39. package/lib/module/renderers/image.js +7 -5
  40. package/lib/module/renderers/image.js.map +1 -1
  41. package/lib/module/renderers/link.js +15 -3
  42. package/lib/module/renderers/link.js.map +1 -1
  43. package/lib/module/renderers/list.js +2 -2
  44. package/lib/module/renderers/list.js.map +1 -1
  45. package/lib/module/renderers/paragraph.js.map +1 -1
  46. package/lib/module/renderers/table.js +33 -16
  47. package/lib/module/renderers/table.js.map +1 -1
  48. package/lib/module/use-markdown-stream.js +16 -14
  49. package/lib/module/use-markdown-stream.js.map +1 -1
  50. package/lib/module/utils/link-security.js +15 -0
  51. package/lib/module/utils/link-security.js.map +1 -0
  52. package/lib/module/utils/stream-timeline.js +56 -0
  53. package/lib/module/utils/stream-timeline.js.map +1 -0
  54. package/lib/typescript/commonjs/Markdown.nitro.d.ts +3 -3
  55. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
  57. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/headless.d.ts +2 -2
  59. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/index.d.ts +1 -1
  61. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/markdown-stream.d.ts +2 -2
  63. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/markdown.d.ts +9 -4
  65. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
  67. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
  69. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
  71. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
  73. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
  75. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
  77. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
  79. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
  81. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
  83. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
  85. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/theme.d.ts +2 -2
  87. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
  90. package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
  92. package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
  93. package/lib/typescript/module/Markdown.nitro.d.ts +3 -3
  94. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  95. package/lib/typescript/module/MarkdownContext.d.ts +26 -25
  96. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  97. package/lib/typescript/module/headless.d.ts +2 -2
  98. package/lib/typescript/module/headless.d.ts.map +1 -1
  99. package/lib/typescript/module/index.d.ts +1 -1
  100. package/lib/typescript/module/index.d.ts.map +1 -1
  101. package/lib/typescript/module/markdown-stream.d.ts +2 -2
  102. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  103. package/lib/typescript/module/markdown.d.ts +9 -4
  104. package/lib/typescript/module/markdown.d.ts.map +1 -1
  105. package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
  106. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  107. package/lib/typescript/module/renderers/code.d.ts +5 -5
  108. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  109. package/lib/typescript/module/renderers/heading.d.ts +3 -3
  110. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  111. package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
  112. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  113. package/lib/typescript/module/renderers/image.d.ts +2 -2
  114. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  115. package/lib/typescript/module/renderers/link.d.ts +3 -3
  116. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  117. package/lib/typescript/module/renderers/list.d.ts +7 -7
  118. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  119. package/lib/typescript/module/renderers/math.d.ts +4 -4
  120. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  121. package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
  122. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  123. package/lib/typescript/module/renderers/table.d.ts +3 -3
  124. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  125. package/lib/typescript/module/theme.d.ts +2 -2
  126. package/lib/typescript/module/theme.d.ts.map +1 -1
  127. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  128. package/lib/typescript/module/utils/link-security.d.ts +3 -0
  129. package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
  130. package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
  131. package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
  132. package/nitrogen/generated/ios/swift/Func_void.swift +0 -1
  133. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +0 -1
  134. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +0 -1
  135. package/package.json +4 -3
  136. package/src/Markdown.nitro.ts +5 -3
  137. package/src/MarkdownContext.ts +31 -25
  138. package/src/headless.ts +2 -2
  139. package/src/index.ts +1 -0
  140. package/src/markdown-stream.tsx +6 -10
  141. package/src/markdown.tsx +86 -45
  142. package/src/renderers/blockquote.tsx +4 -4
  143. package/src/renderers/code.tsx +11 -9
  144. package/src/renderers/heading.tsx +4 -4
  145. package/src/renderers/horizontal-rule.tsx +3 -3
  146. package/src/renderers/image.tsx +11 -9
  147. package/src/renderers/link.tsx +25 -7
  148. package/src/renderers/list.tsx +9 -12
  149. package/src/renderers/math.tsx +4 -4
  150. package/src/renderers/paragraph.tsx +3 -3
  151. package/src/renderers/table.tsx +74 -46
  152. package/src/theme.ts +3 -3
  153. package/src/use-markdown-stream.ts +22 -16
  154. package/src/utils/link-security.ts +22 -0
  155. package/src/utils/stream-timeline.ts +72 -0
package/README.md CHANGED
@@ -3,468 +3,475 @@
3
3
  <img src="./readme/stream-demo.gif" alt="react-native-nitro-markdown stream demo" width="300" />
4
4
  </p>
5
5
 
6
- # react-native-nitro-markdown 🚀
6
+ # react-native-nitro-markdown
7
7
 
8
- > The fastest Markdown parser for React Native. Period.
8
+ Native Markdown parsing and rendering for React Native.
9
9
 
10
- [![npm version](https://img.shields.io/npm/v/react-native-nitro-markdown?style=flat-square)](https://www.npmjs.com/package/react-native-nitro-markdown)
11
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
12
- [![Nitro Modules](https://img.shields.io/badge/Powered%20by-Nitro%20Modules-blueviolet?style=flat-square)](https://nitro.margelo.com)
10
+ `react-native-nitro-markdown` uses `md4c` (C++) through Nitro Modules (JSI) to parse Markdown synchronously into a typed AST, then render it with customizable React Native components.
13
11
 
14
- **react-native-nitro-markdown** is a high-performance Markdown parser built on **[md4c](https://github.com/mity/md4c)** (C++) and **[Nitro Modules](https://nitro.margelo.com)**. It parses complex Markdown, GFM, and LaTeX Math into a structured AST **synchronously** via JSI, bypassing the React Native Bridge entirely.
12
+ ## Why use it
15
13
 
16
- ---
14
+ - Native parser (`md4c`) for lower JS thread overhead on large documents
15
+ - End-to-end solution: parser + renderer + streaming session API
16
+ - Headless API for custom rendering and text processing
17
+ - GFM support (tables, strikethrough, task lists, autolinks)
18
+ - Optional math rendering with `react-native-mathjax-svg`
17
19
 
18
- ## ⚡ Why Nitro? (Benchmarks)
20
+ ## Requirements
19
21
 
20
- We benchmarked this library against the most popular JavaScript parsers on a real mobile device (iPhone 15 Pro, Release Mode) using a heavy **237KB** Markdown document.
22
+ - React Native `>=0.75.0`
23
+ - `react-native-nitro-modules`
21
24
 
22
- | Parser | Time (ms) | Speedup | Frame Drops (60fps) |
23
- | :-------------------------- | :--------- | :---------------- | :-------------------- |
24
- | **🚀 Nitro Markdown (C++)** | **~29 ms** | **1x (Baseline)** | **~1 frame** (Smooth) |
25
- | 📋 CommonMark (JS) | ~82 ms | 2.8x slower | ~5 frames (Jank) |
26
- | 🏗️ Markdown-It (JS) | ~118 ms | 4.0x slower | ~7 frames (Jank) |
27
- | 💨 Marked (JS) | ~400 ms | 13.5x slower | ~24 frames (Freeze) |
25
+ Optional for math rendering:
28
26
 
29
- > **Takeaway:** JavaScript parsers trigger Garbage Collection pauses. Nitro uses C++ to parse efficiently with zero-copy overhead, keeping your UI thread responsive.
27
+ - `react-native-mathjax-svg >=0.9.0`
28
+ - `react-native-svg >=13.0.0`
30
29
 
31
- ---
30
+ ## Installation
32
31
 
33
- ## 📦 Installation
34
-
35
- Choose your preferred package manager to install the package and its core dependency (`react-native-nitro-modules`).
36
-
37
- ### **1. Install Dependencies**
38
-
39
- **npm**
40
-
41
- ```bash
42
- npm install react-native-nitro-markdown react-native-nitro-modules
43
- ```
44
-
45
- > **Note:** If you want to use **Math** (LaTeX) or certain **Image** features, you should also install the optional peer dependencies:
46
- > `npm install react-native-svg react-native-mathjax-svg`
47
-
48
- **Yarn**
49
-
50
- ```bash
51
- yarn add react-native-nitro-markdown react-native-nitro-modules
52
- ```
53
-
54
- **Bun**
32
+ ### React Native
55
33
 
56
34
  ```bash
57
35
  bun add react-native-nitro-markdown react-native-nitro-modules
58
36
  ```
59
37
 
60
- **pnpm**
38
+ Optional math support:
61
39
 
62
40
  ```bash
63
- pnpm add react-native-nitro-markdown react-native-nitro-modules
41
+ bun add react-native-mathjax-svg react-native-svg
64
42
  ```
65
43
 
66
- ### **2. Install Native Pods (iOS)**
67
-
68
- **Standard**
44
+ iOS pods:
69
45
 
70
46
  ```bash
71
47
  cd ios && pod install
72
48
  ```
73
49
 
74
- ### **3. Expo Users**
75
-
76
- If you are using Expo, you must run a **Prebuild** (Development Build) because this package contains native C++ code.
50
+ ### Expo (development build)
77
51
 
78
52
  ```bash
79
53
  bunx expo install react-native-nitro-markdown react-native-nitro-modules
80
54
  bunx expo prebuild
81
55
  ```
82
56
 
83
- ---
84
-
85
- ## 💻 Usage
57
+ Optional math support:
86
58
 
87
- ### Option 1: Batteries Included (Simplest)
59
+ ```bash
60
+ bunx expo install react-native-mathjax-svg react-native-svg
61
+ ```
88
62
 
89
- Use the `Markdown` component with clean, neutral styling that stays out of the way:
63
+ ## Quick Start
90
64
 
91
65
  ```tsx
92
66
  import { Markdown } from "react-native-nitro-markdown";
93
67
 
94
- export function MyComponent() {
68
+ export function Example() {
95
69
  return (
96
70
  <Markdown options={{ gfm: true }}>
97
- {"# Hello World\nThis is **bold** text."}
71
+ {"# Hello\nThis is **native** markdown."}
98
72
  </Markdown>
99
73
  );
100
74
  }
101
75
  ```
102
76
 
103
- If you're rendering on a dark surface, override `theme.colors.text` (or use the `styles` prop) to match your app's palette.
104
-
105
- ### Option 2: Style Overrides per Node Type
106
-
107
- Apply quick style overrides to specific node types without writing custom renderers:
77
+ ## Package Exports
78
+
79
+ ### Main Entry (`react-native-nitro-markdown`)
80
+
81
+ - Parser and headless helpers:
82
+ - `parseMarkdown`
83
+ - `parseMarkdownWithOptions`
84
+ - `getTextContent`
85
+ - `getFlattenedText`
86
+ - `MarkdownParserModule`
87
+ - Components:
88
+ - `Markdown`
89
+ - `MarkdownStream`
90
+ - Hooks and sessions:
91
+ - `useMarkdownSession`
92
+ - `useStream`
93
+ - `createMarkdownSession`
94
+ - Context:
95
+ - `MarkdownContext`
96
+ - `useMarkdownContext`
97
+ - Theme:
98
+ - `defaultMarkdownTheme`
99
+ - `minimalMarkdownTheme`
100
+ - `mergeThemes`
101
+ - Built-in renderers:
102
+ - `Heading`, `Paragraph`, `Link`, `Blockquote`, `HorizontalRule`
103
+ - `CodeBlock`, `InlineCode`
104
+ - `List`, `ListItem`, `TaskListItem`
105
+ - `TableRenderer`, `Image`, `MathInline`, `MathBlock`
106
+ - Types:
107
+ - `MarkdownNode`, `ParserOptions`, `MarkdownParser`
108
+ - `CustomRenderers`, `CustomRenderer`, `CustomRendererProps`
109
+ - `NodeRendererProps`, `BaseCustomRendererProps`, `EnhancedRendererProps`
110
+ - `HeadingRendererProps`, `LinkRendererProps`, `ImageRendererProps`
111
+ - `CodeBlockRendererProps`, `InlineCodeRendererProps`
112
+ - `ListRendererProps`, `TaskListItemRendererProps`
113
+ - `LinkPressHandler`, `MarkdownContextValue`
114
+ - `MarkdownTheme`, `PartialMarkdownTheme`, `NodeStyleOverrides`, `StylingStrategy`
115
+ - `MarkdownSession`
116
+
117
+ ### Headless Entry (`react-native-nitro-markdown/headless`)
118
+
119
+ Exports only parser-related API (`parseMarkdown`, `parseMarkdownWithOptions`, `getTextContent`, `getFlattenedText`, types). Use this when you do not need built-in UI rendering.
120
+
121
+ ## Component API
122
+
123
+ ## `Markdown`
108
124
 
109
125
  ```tsx
110
- <Markdown
111
- styles={{
112
- heading: { color: "#0ea5e9", fontWeight: "900" },
113
- code_block: { backgroundColor: "#e2e8f0", borderRadius: 16 },
114
- blockquote: { borderLeftColor: "#0ea5e9" },
115
- }}
116
- >
117
- {markdown}
118
- </Markdown>
126
+ import { Markdown } from "react-native-nitro-markdown";
119
127
  ```
120
128
 
121
- ### Option 3: Custom Renderers
122
-
123
- Override specific node types with full control. Custom renderers receive **pre-mapped props** for common values:
129
+ | Prop | Type | Default | Description |
130
+ | --------------------- | ---------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------- | -------------------- |
131
+ | `children` | `string` | required | Markdown input string. |
132
+ | `options` | `ParserOptions` | `undefined` | Parser flags (`gfm`, `math`). |
133
+ | `renderers` | `CustomRenderers` | `{}` | Per-node custom renderers. |
134
+ | `theme` | `PartialMarkdownTheme` | `defaultMarkdownTheme` or `minimalMarkdownTheme` | Theme token overrides. |
135
+ | `styles` | `NodeStyleOverrides` | `undefined` | Per-node style overrides. |
136
+ | `stylingStrategy` | `"opinionated" | "minimal"` | `"opinionated"` | Base styling preset. |
137
+ | `style` | `StyleProp<ViewStyle>` | `undefined` | Container style for the root `View`. |
138
+ | `onParsingInProgress` | `() => void` | `undefined` | Called when parse inputs change. |
139
+ | `onParseComplete` | `(result) => void` | `undefined` | Called with `{ raw, ast, text }` after successful parse. |
140
+ | `onLinkPress` | `LinkPressHandler` | `undefined` | Intercepts link press before default open behavior. Return `false` to block default open. |
124
141
 
125
- ```tsx
126
- import {
127
- Markdown,
128
- CodeBlock,
129
- type HeadingRendererProps,
130
- type CodeBlockRendererProps,
131
- } from "react-native-nitro-markdown";
142
+ Notes:
132
143
 
133
- const renderers = {
134
- // Pre-mapped `level` prop - no need for node.level!
135
- heading: ({ level, children }: HeadingRendererProps) => (
136
- <MyHeading level={level}>{children}</MyHeading>
137
- ),
144
+ - Parse failures are caught and rendered as a fallback message (`Error parsing markdown`).
145
+ - `text` in `onParseComplete` is produced by `getFlattenedText(ast)`.
138
146
 
139
- // Pre-mapped `content` and `language` - no getTextContent() needed!
140
- code_block: ({ content, language }: CodeBlockRendererProps) => (
141
- <CodeBlock
142
- content={content}
143
- language={language}
144
- style={{ borderWidth: 2 }}
145
- />
146
- ),
147
- };
147
+ ## `MarkdownStream`
148
148
 
149
- <Markdown renderers={renderers} options={{ gfm: true }}>
150
- {markdown}
151
- </Markdown>;
149
+ ```tsx
150
+ import { MarkdownStream } from "react-native-nitro-markdown";
152
151
  ```
153
152
 
154
- **Pre-mapped Props by Node Type:**
155
-
156
- - `heading` → `level` (1-6)
157
- - `link` → `href`, `title`
158
- - `image` → `url`, `alt`, `title`
159
- - `code_block` → `content`, `language`
160
- - `code_inline` → `content`
161
- - `list` → `ordered`, `start`
162
- - `task_list_item` → `checked`
153
+ `MarkdownStreamProps` extends `MarkdownProps` except `children`.
163
154
 
164
- ### Option 4: Token Overrides (Theme)
155
+ | Prop | Type | Default | Description |
156
+ | ---------------------- | ----------------- | -------- | -------------------------------------------------- | --------------------------------------------------------- |
157
+ | `session` | `MarkdownSession` | required | Session object that supplies streamed text chunks. |
158
+ | `updateIntervalMs` | `number` | `50` | Flush interval when `updateStrategy="interval"`. |
159
+ | `updateStrategy` | `"interval" | "raf"` | `"interval"` | Update cadence (`setTimeout` vs `requestAnimationFrame`). |
160
+ | `useTransitionUpdates` | `boolean` | `false` | Applies `startTransition` to streamed UI updates. |
165
161
 
166
- Customize the look and feel by passing a partial `theme` object:
162
+ ### Streaming Example
167
163
 
168
164
  ```tsx
169
- import { Markdown } from "react-native-nitro-markdown";
170
-
171
- const myTheme = {
172
- colors: {
173
- text: "#0f172a",
174
- heading: "#0f172a",
175
- link: "#0ea5e9",
176
- codeBackground: "#e2e8f0",
177
- },
178
- showCodeLanguage: false,
179
- };
165
+ import { useEffect } from "react";
166
+ import {
167
+ MarkdownStream,
168
+ useMarkdownSession,
169
+ } from "react-native-nitro-markdown";
180
170
 
181
- <Markdown theme={myTheme}>{"# Custom Themed Markdown"}</Markdown>;
182
- ```
171
+ export function StreamingExample() {
172
+ const session = useMarkdownSession();
183
173
 
184
- Defaults live in `defaultMarkdownTheme` and are intentionally neutral so you can layer your own palette on top.
174
+ useEffect(() => {
175
+ const s = session.getSession();
176
+ s.append("# Streaming\n");
177
+ s.append("This text arrives in chunks.");
185
178
 
186
- **Theme Properties:**
179
+ return () => session.clear();
180
+ }, [session]);
187
181
 
188
- - `colors` - All color tokens (text, heading, link, code, codeBackground, codeLanguage, etc.)
189
- - `spacing` - Spacing tokens (xs, s, m, l, xl)
190
- - `fontSizes` - Font sizes (xs, s, m, l, xl, h1-h6)
191
- - `fontFamilies` - Font families for regular, heading, and mono text
192
- - `headingWeight` - Optional weight for headings (useful for Android custom fonts)
193
- - `borderRadius` - Border radius tokens (s, m, l)
194
- - `showCodeLanguage` - Show/hide code block language labels
182
+ return (
183
+ <MarkdownStream
184
+ session={session.getSession()}
185
+ options={{ gfm: true }}
186
+ updateStrategy="raf"
187
+ useTransitionUpdates
188
+ />
189
+ );
190
+ }
191
+ ```
195
192
 
196
- **Android custom fonts note:**
197
- If you use a custom heading font on Android and don’t load a bold variant, set
198
- `headingWeight: "normal"` (or use the `styles` prop) to avoid fallback to a
199
- system serif font.
193
+ ## Hooks and Session API
200
194
 
201
- ### Option 5: Minimal Styling Strategy
195
+ ## `createMarkdownSession()`
202
196
 
203
- Start with a clean slate using the `stylingStrategy` prop:
197
+ Creates and returns a native `MarkdownSession` instance.
204
198
 
205
199
  ```tsx
206
- <Markdown stylingStrategy="minimal" theme={myLightTheme}>
207
- {content}
208
- </Markdown>
200
+ import { createMarkdownSession } from "react-native-nitro-markdown";
201
+
202
+ const session = createMarkdownSession();
203
+ session.append("hello");
209
204
  ```
210
205
 
211
- This zeros out all spacing and removes opinionated colors, letting you build up from scratch.
206
+ ## `useMarkdownSession()`
212
207
 
213
- ### Option 6: Style Props on Individual Renderers
208
+ Creates and owns one `MarkdownSession` for a component lifecycle.
214
209
 
215
- All built-in renderers accept a `style` prop for fine-grained overrides:
210
+ Returns:
216
211
 
217
- ```tsx
218
- import { Heading, CodeBlock, InlineCode } from "react-native-nitro-markdown";
212
+ | Field | Type | Description |
213
+ | ---------------- | ---------------------------- | ------------------------------------------------------------- |
214
+ | `getSession` | `() => MarkdownSession` | Returns the stable native session instance. |
215
+ | `isStreaming` | `boolean` | Stateful flag for app-level streaming UI. |
216
+ | `setIsStreaming` | `(value: boolean) => void` | Setter for `isStreaming`. |
217
+ | `stop` | `() => void` | Sets `isStreaming` to `false`. |
218
+ | `clear` | `() => void` | Clears session content and resets `highlightPosition` to `0`. |
219
+ | `setHighlight` | `(position: number) => void` | Sets `session.highlightPosition`. |
219
220
 
220
- // Works in custom renderers
221
- <Heading level={1} style={{ color: "hotpink" }}>Title</Heading>
222
- <CodeBlock content={code} style={{ borderRadius: 0 }} />
223
- <InlineCode style={{ backgroundColor: "#ff0" }}>code</InlineCode>
224
- ```
221
+ ## `useStream(timestamps?)`
225
222
 
226
- ### Option 7: Auto Content Extraction for Code
223
+ Builds on `useMarkdownSession` and adds timeline sync helpers.
227
224
 
228
- The `CodeBlock` and `InlineCode` components now accept a `node` prop for automatic content extraction:
225
+ - `timestamps` type: `Record<number, number>` where key = word/token index, value = timestamp in ms.
226
+ - `sync(currentTimeMs)` computes highlight position from timestamp map.
227
+ - Uses optimized lookup for monotonic timelines and handles non-monotonic maps safely.
229
228
 
230
- ```tsx
231
- // Before: Manual extraction required
232
- code_block: ({ node }) => (
233
- <CodeBlock content={getTextContent(node)} language={node.language} />
234
- );
235
-
236
- // After: Just pass the node
237
- code_block: ({ node }) => <CodeBlock node={node} />;
238
-
239
- // Or use the pre-mapped content prop (recommended)
240
- code_block: ({ content, language }) => (
241
- <CodeBlock content={content} language={language} />
242
- );
243
- ```
229
+ Additional returned fields:
244
230
 
245
- ### Option 8: Headless (Minimal Bundle)
231
+ | Field | Type | Description |
232
+ | -------------- | --------------------------------- | ----------------------------------------- |
233
+ | `isPlaying` | `boolean` | Playback state for timed streaming. |
234
+ | `setIsPlaying` | `(value: boolean) => void` | Setter for `isPlaying`. |
235
+ | `sync` | `(currentTimeMs: number) => void` | Applies timeline-based highlight updates. |
246
236
 
247
- For maximum control, data processing, or minimal JS overhead:
237
+ ## Headless API
248
238
 
249
239
  ```tsx
250
240
  import {
251
241
  parseMarkdown,
242
+ parseMarkdownWithOptions,
252
243
  getTextContent,
253
244
  getFlattenedText,
254
245
  } from "react-native-nitro-markdown/headless";
255
-
256
- const ast = parseMarkdown("# Hello World");
257
- const text = getTextContent(ast); // "Hello World"
258
- const fullText = getFlattenedText(ast); // "Hello World\n\n" (Normalized with line breaks)
259
246
  ```
260
247
 
261
- ### Option 9: High-Performance Streaming (LLMs)
248
+ | Function | Signature | Description |
249
+ | -------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ |
250
+ | `parseMarkdown` | `(text: string) => MarkdownNode` | Parses markdown using default parser settings. |
251
+ | `parseMarkdownWithOptions` | `(text: string, options: ParserOptions) => MarkdownNode` | Parses markdown with `gfm` and/or `math` flags. |
252
+ | `getTextContent` | `(node: MarkdownNode) => string` | Concatenates text recursively without layout normalization. |
253
+ | `getFlattenedText` | `(node: MarkdownNode) => string` | Returns normalized plain text with paragraph and block separators. |
262
254
 
263
- When streaming text token-by-token (e.g., from ChatGPT or Gemini), you should batch UI updates to avoid re-rendering on every token:
255
+ ### Parser Options
264
256
 
265
- ```tsx
266
- import {
267
- MarkdownStream,
268
- useMarkdownSession,
269
- } from "react-native-nitro-markdown";
257
+ ```ts
258
+ type ParserOptions = {
259
+ gfm?: boolean;
260
+ math?: boolean;
261
+ };
262
+ ```
270
263
 
271
- export function AIResponseStream() {
272
- const session = useMarkdownSession();
264
+ ## Custom Renderer API
273
265
 
274
- useEffect(() => {
275
- session.getSession().append("Hello **Nitro**!");
276
- return () => session.clear();
277
- }, [session]);
266
+ ## `renderers` prop contract
278
267
 
279
- return (
280
- <MarkdownStream
281
- session={session.getSession()}
282
- options={{ gfm: true }}
283
- updateIntervalMs={60}
284
- updateStrategy="raf"
285
- useTransitionUpdates
286
- />
287
- );
288
- }
268
+ `CustomRenderers` is:
269
+
270
+ ```ts
271
+ type CustomRenderers = Partial<Record<MarkdownNode["type"], CustomRenderer>>;
289
272
  ```
290
273
 
291
- **Recommended defaults for token streaming:**
292
- - `updateStrategy="raf"` for smooth, frame-aligned updates
293
- - `updateIntervalMs={50–100}` when using `updateStrategy="interval"`
294
- - Avoid per-token UI updates by batching token appends
274
+ `CustomRenderer` receives `EnhancedRendererProps` and returns:
295
275
 
296
- **Streaming props:**
276
+ - React node to override default rendering
277
+ - `undefined` to fallback to built-in renderer
278
+ - `null` to render nothing
297
279
 
298
- | Prop | Type | Default | Description |
299
- | :-- | :-- | :-- | :-- |
300
- | `updateIntervalMs` | `number` | `50` | Throttle UI updates when using interval strategy |
301
- | `updateStrategy` | `"interval" \| "raf"` | `"interval"` | Interval batching or frame-aligned updates |
302
- | `useTransitionUpdates` | `boolean` | `false` | Use React transitions to keep input responsive |
280
+ `EnhancedRendererProps` always includes:
303
281
 
304
- ### Option 10: Extracting Plain Text
282
+ - `node`: current `MarkdownNode`
283
+ - `children`: pre-rendered node children
284
+ - `Renderer`: recursive renderer component for nested custom rendering
305
285
 
306
- You can extract the plain text representation (with proper line breaks) using the `onParseComplete` callback. This is useful for "Copy All" buttons or TTS.
286
+ And conditionally includes mapped fields by node type:
307
287
 
308
- ```tsx
309
- <Markdown
310
- onParseComplete={(result) => {
311
- console.log(result.text); // "Hello World\n\nThis is bold text."
312
- console.log(result.ast); // Full AST
313
- }}
314
- >
315
- {markdown}
316
- </Markdown>
317
- ```
318
-
319
- ---
320
-
321
- ## 🎨 Using Context in Custom Renderers
288
+ | Node type | Extra mapped fields |
289
+ | ---------------- | --------------------- |
290
+ | `heading` | `level` |
291
+ | `link` | `href`, `title` |
292
+ | `image` | `url`, `alt`, `title` |
293
+ | `code_block` | `content`, `language` |
294
+ | `code_inline` | `content` |
295
+ | `list` | `ordered`, `start` |
296
+ | `task_list_item` | `checked` |
322
297
 
323
- Access theme and context in custom renderers:
298
+ ### Example: Custom heading + code block
324
299
 
325
300
  ```tsx
326
301
  import {
327
- useMarkdownContext,
328
- MarkdownContext,
302
+ Markdown,
303
+ type HeadingRendererProps,
304
+ type CodeBlockRendererProps,
329
305
  } from "react-native-nitro-markdown";
330
306
 
331
- const MyCustomRenderer = ({ children }) => {
332
- const { theme, stylingStrategy } = useMarkdownContext();
333
-
334
- return <View style={{ padding: theme.spacing.m }}>{children}</View>;
307
+ const renderers = {
308
+ heading: ({ level, children }: HeadingRendererProps) => (
309
+ <MyHeading level={level}>{children}</MyHeading>
310
+ ),
311
+ code_block: ({ language, content }: CodeBlockRendererProps) => (
312
+ <MyCode language={language} content={content} />
313
+ ),
335
314
  };
315
+
316
+ <Markdown renderers={renderers}>{content}</Markdown>;
336
317
  ```
337
318
 
338
- ---
319
+ ## Link Handling Behavior
339
320
 
340
- ## 🛠️ Exported Utilities
321
+ Default link renderer behavior:
341
322
 
342
- ```tsx
343
- // Parser and utilities
344
- export {
345
- parseMarkdown,
346
- parseMarkdownWithOptions,
347
- getTextContent,
348
- getFlattenedText,
349
- } from "./headless";
323
+ 1. Trims incoming href.
324
+ 2. Calls `onLinkPress(href)` if provided.
325
+ 3. Stops if handler returns `false`.
326
+ 4. Allows only protocol-based links with these schemes:
327
+ - `http:`
328
+ - `https:`
329
+ - `mailto:`
330
+ - `tel:`
331
+ - `sms:`
332
+ 5. Uses `Linking.canOpenURL` before `Linking.openURL`.
350
333
 
351
- // Theme tokens
352
- export {
353
- defaultMarkdownTheme,
354
- minimalMarkdownTheme,
355
- mergeThemes,
356
- };
334
+ Relative URLs and anchors are ignored by default open behavior, but you can handle them in `onLinkPress`.
357
335
 
358
- // Context
359
- export { useMarkdownContext, MarkdownContext };
360
-
361
- // Individual renderers
362
- export {
363
- Heading,
364
- Paragraph,
365
- Link,
366
- Blockquote,
367
- HorizontalRule,
368
- CodeBlock,
369
- InlineCode,
370
- List,
371
- ListItem,
372
- TaskListItem,
373
- TableRenderer,
374
- Image,
375
- MathInline,
376
- MathBlock,
377
- };
378
- ```
336
+ ## Theme API
379
337
 
380
- ---
338
+ ## `MarkdownTheme`
381
339
 
382
- ## 🛠️ Headless vs. Non-Headless
340
+ ```tsx
341
+ import type {
342
+ MarkdownTheme,
343
+ PartialMarkdownTheme,
344
+ } from "react-native-nitro-markdown";
345
+ ```
383
346
 
384
- | Feature | **Headless** (`/headless`) | **Non-Headless** (`default`) |
385
- | :-------------- | :-------------------------- | :--------------------------------- |
386
- | **Logic** | Raw C++ md4c Parser | Parser + Full UI Renderer |
387
- | **Output** | JSON AST Tree | React Native Views |
388
- | **Best For** | Search Indexing, Custom UIs | Fast Implementation, Documentation |
389
- | **JS Overhead** | ~4 KB | ~60 KB |
347
+ `MarkdownTheme` fields:
390
348
 
391
- ---
349
+ - `colors`
350
+ - `text`, `textMuted`, `heading`, `link`, `code`, `codeBackground`, `codeLanguage`
351
+ - `blockquote`, `border`, `surface`, `surfaceLight`, `accent`
352
+ - `tableBorder`, `tableHeader`, `tableHeaderText`, `tableRowEven`, `tableRowOdd`
353
+ - `spacing`: `xs`, `s`, `m`, `l`, `xl`
354
+ - `fontSizes`: `xs`, `s`, `m`, `l`, `xl`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`
355
+ - `fontFamilies`: `regular`, `heading`, `mono`
356
+ - `headingWeight?`
357
+ - `borderRadius`: `s`, `m`, `l`
358
+ - `showCodeLanguage`
392
359
 
393
- ### Basic Parsing API
360
+ Helpers:
394
361
 
395
- The parsing is synchronous and instant. It returns a fully typed JSON AST:
362
+ - `defaultMarkdownTheme`
363
+ - `minimalMarkdownTheme`
364
+ - `mergeThemes(base, partial)`
396
365
 
397
- ```typescript
398
- import { parseMarkdown } from "react-native-nitro-markdown/headless";
366
+ `NodeStyleOverrides` lets you override per-node styles:
399
367
 
400
- const ast = parseMarkdown(`
401
- # Hello World
402
- This is **bold** text and a [link](https://github.com).
403
- `);
368
+ ```ts
369
+ type NodeStyleOverrides = Partial<
370
+ Record<MarkdownNode["type"], ViewStyle | TextStyle>
371
+ >;
404
372
  ```
405
373
 
406
- ### Options
374
+ ## Built-in Renderer Components
407
375
 
408
- | Option | Type | Default | Description |
409
- | :----- | :-------- | :------ | :----------------------------------------------------------------------------- |
410
- | `gfm` | `boolean` | `false` | Enable GitHub Flavored Markdown (Tables, Strikethrough, Autolinks, TaskLists). |
411
- | `math` | `boolean` | `false` | Enable LaTeX Math support (`$` and `$$`). |
376
+ Use these when composing custom renderer maps.
412
377
 
413
- ---
378
+ | Component | Key props |
379
+ | ---------------- | ------------------------------------------------ |
380
+ | `Heading` | `level`, `children`, `style` |
381
+ | `Paragraph` | `children`, `inListItem`, `style` |
382
+ | `Link` | `href`, `children`, `style` |
383
+ | `Blockquote` | `children`, `style` |
384
+ | `HorizontalRule` | `style` |
385
+ | `CodeBlock` | `language`, `content`, `node`, `style` |
386
+ | `InlineCode` | `content`, `node`, `children`, `style` |
387
+ | `List` | `ordered`, `start`, `depth`, `children`, `style` |
388
+ | `ListItem` | `children`, `index`, `ordered`, `start`, `style` |
389
+ | `TaskListItem` | `children`, `checked`, `style` |
390
+ | `TableRenderer` | `node`, `Renderer`, `style` |
391
+ | `Image` | `url`, `title`, `alt`, `Renderer`, `style` |
392
+ | `MathInline` | `content`, `style` |
393
+ | `MathBlock` | `content`, `style` |
414
394
 
415
- ## 📐 AST Structure
395
+ ## Supported AST Node Types
416
396
 
417
- The parser returns a `MarkdownNode` tree:
397
+ `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`
418
398
 
419
- ```typescript
420
- export interface MarkdownNode {
421
- type: NodeType;
422
- content?: string;
423
- children?: MarkdownNode[];
424
- level?: number;
425
- href?: string;
426
- checked?: boolean;
427
- language?: string;
428
- align?: "left" | "center" | "right";
429
- isHeader?: boolean;
430
- }
431
- ```
399
+ Notes:
400
+
401
+ - `html_inline` and `html_block` are parsed but not rendered by default.
402
+ - Table internals (`table_head`, `table_body`, `table_row`, `table_cell`) are renderer internals; override `table` for custom table UI.
403
+
404
+ ## Recipes
432
405
 
433
- ---
406
+ ### Intercept links with `onLinkPress`
434
407
 
435
- ## 🧮 LaTeX Math Support
408
+ ```tsx
409
+ import { Markdown } from "react-native-nitro-markdown";
436
410
 
437
- We parse math delimiters (`$` and `$$`) natively using the `MD_FLAG_LATEXMATHSPANS` flag in `md4c`.
411
+ <Markdown
412
+ onLinkPress={(href) => {
413
+ if (href.startsWith("/")) {
414
+ router.push(href);
415
+ return false;
416
+ }
417
+ }}
418
+ >
419
+ {content}
420
+ </Markdown>;
421
+ ```
438
422
 
439
- To render the math, use a library like `react-native-mathjax-svg`:
423
+ ### Use headless mode to build search index
440
424
 
441
425
  ```tsx
442
- case 'math_inline':
443
- return <MathView math={node.content} style={styles.math} />;
444
- case 'math_block':
445
- return <MathView math={node.content} style={styles.mathBlock} />;
426
+ import {
427
+ parseMarkdown,
428
+ getFlattenedText,
429
+ } from "react-native-nitro-markdown/headless";
430
+
431
+ const ast = parseMarkdown(content);
432
+ const searchableText = getFlattenedText(ast);
446
433
  ```
447
434
 
448
- ---
435
+ ### Minimal styling baseline
436
+
437
+ ```tsx
438
+ import { Markdown } from "react-native-nitro-markdown";
449
439
 
450
- ## 📊 Package Size
440
+ <Markdown
441
+ stylingStrategy="minimal"
442
+ theme={{
443
+ colors: {
444
+ text: "#0f172a",
445
+ link: "#1d4ed8",
446
+ },
447
+ }}
448
+ >
449
+ {content}
450
+ </Markdown>;
451
+ ```
451
452
 
452
- | Metric | Size |
453
- | :------------------- | :------ |
454
- | **Packed (tarball)** | ~75 kB |
455
- | **Unpacked** | ~325 kB |
456
- | **Total files** | 55 |
453
+ ## Performance Guidance
457
454
 
458
- ---
455
+ - For streaming text, prefer `updateStrategy="raf"`.
456
+ - If you use interval strategy, `updateIntervalMs` between `50` and `100` is a good baseline.
457
+ - Batch `session.append(...)` calls instead of appending one character at a time.
458
+ - Use the headless API if you do not need built-in renderers.
459
459
 
460
- ## 🤝 Contributing
460
+ ## Troubleshooting
461
461
 
462
- See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
462
+ - Math appears as plain code-style fallback:
463
+ - Install `react-native-mathjax-svg` and `react-native-svg`.
464
+ - iOS native build issues after install:
465
+ - Run `pod install` in your iOS project.
466
+ - Expo app does not load native module:
467
+ - Use a development build (`expo prebuild` + `expo run`), not Expo Go.
468
+ - Android heading font weight looks wrong:
469
+ - Set `theme.headingWeight` explicitly (for custom fonts without bold variants, use `"normal"`).
463
470
 
464
- ## 📄 License
471
+ ## Contributing
465
472
 
466
- MIT
473
+ See `/Users/jota/Workspace/Projects/RN-Packages/react-native-nitro-markdown/CONTRIBUTING.md`.
467
474
 
468
- ---
475
+ ## License
469
476
 
470
- Built with ❤️ using [Nitro Modules](https://nitro.margelo.com) and [md4c](https://github.com/mity/md4c).
477
+ MIT