react-native-nitro-markdown 0.4.2 → 0.5.0

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 (191) hide show
  1. package/README.md +605 -318
  2. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +27 -8
  3. package/cpp/bindings/HybridMarkdownParser.cpp +216 -66
  4. package/cpp/bindings/HybridMarkdownParser.hpp +2 -0
  5. package/ios/HybridMarkdownSession.swift +33 -7
  6. package/lib/commonjs/MarkdownContext.js +2 -1
  7. package/lib/commonjs/MarkdownContext.js.map +1 -1
  8. package/lib/commonjs/headless.js +41 -5
  9. package/lib/commonjs/headless.js.map +1 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/markdown-stream.js +109 -13
  12. package/lib/commonjs/markdown-stream.js.map +1 -1
  13. package/lib/commonjs/markdown.js +215 -44
  14. package/lib/commonjs/markdown.js.map +1 -1
  15. package/lib/commonjs/renderers/code.js +4 -3
  16. package/lib/commonjs/renderers/code.js.map +1 -1
  17. package/lib/commonjs/renderers/heading.js +1 -1
  18. package/lib/commonjs/renderers/heading.js.map +1 -1
  19. package/lib/commonjs/renderers/image.js +7 -5
  20. package/lib/commonjs/renderers/image.js.map +1 -1
  21. package/lib/commonjs/renderers/link.js +15 -3
  22. package/lib/commonjs/renderers/link.js.map +1 -1
  23. package/lib/commonjs/renderers/list.js +2 -2
  24. package/lib/commonjs/renderers/list.js.map +1 -1
  25. package/lib/commonjs/renderers/table.js +126 -21
  26. package/lib/commonjs/renderers/table.js.map +1 -1
  27. package/lib/commonjs/use-markdown-stream.js +16 -14
  28. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  29. package/lib/commonjs/utils/incremental-ast.js +153 -0
  30. package/lib/commonjs/utils/incremental-ast.js.map +1 -0
  31. package/lib/commonjs/utils/link-security.js +21 -0
  32. package/lib/commonjs/utils/link-security.js.map +1 -0
  33. package/lib/commonjs/utils/stream-timeline.js +62 -0
  34. package/lib/commonjs/utils/stream-timeline.js.map +1 -0
  35. package/lib/module/MarkdownContext.js +2 -1
  36. package/lib/module/MarkdownContext.js.map +1 -1
  37. package/lib/module/headless.js +37 -4
  38. package/lib/module/headless.js.map +1 -1
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/markdown-stream.js +110 -14
  41. package/lib/module/markdown-stream.js.map +1 -1
  42. package/lib/module/markdown.js +217 -46
  43. package/lib/module/markdown.js.map +1 -1
  44. package/lib/module/renderers/blockquote.js.map +1 -1
  45. package/lib/module/renderers/code.js +4 -3
  46. package/lib/module/renderers/code.js.map +1 -1
  47. package/lib/module/renderers/heading.js +1 -1
  48. package/lib/module/renderers/heading.js.map +1 -1
  49. package/lib/module/renderers/image.js +7 -5
  50. package/lib/module/renderers/image.js.map +1 -1
  51. package/lib/module/renderers/link.js +15 -3
  52. package/lib/module/renderers/link.js.map +1 -1
  53. package/lib/module/renderers/list.js +2 -2
  54. package/lib/module/renderers/list.js.map +1 -1
  55. package/lib/module/renderers/paragraph.js.map +1 -1
  56. package/lib/module/renderers/table.js +127 -22
  57. package/lib/module/renderers/table.js.map +1 -1
  58. package/lib/module/use-markdown-stream.js +16 -14
  59. package/lib/module/use-markdown-stream.js.map +1 -1
  60. package/lib/module/utils/incremental-ast.js +147 -0
  61. package/lib/module/utils/incremental-ast.js.map +1 -0
  62. package/lib/module/utils/link-security.js +15 -0
  63. package/lib/module/utils/link-security.js.map +1 -0
  64. package/lib/module/utils/stream-timeline.js +56 -0
  65. package/lib/module/utils/stream-timeline.js.map +1 -0
  66. package/lib/typescript/commonjs/Markdown.nitro.d.ts +5 -3
  67. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
  69. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/headless.d.ts +15 -2
  71. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/index.d.ts +3 -1
  73. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/markdown-stream.d.ts +7 -2
  75. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/markdown.d.ts +62 -5
  77. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
  79. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
  81. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
  83. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
  85. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
  87. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
  89. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
  91. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
  93. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
  95. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
  97. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +5 -2
  99. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/theme.d.ts +2 -2
  101. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
  104. package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
  106. package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
  108. package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
  109. package/lib/typescript/module/Markdown.nitro.d.ts +5 -3
  110. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  111. package/lib/typescript/module/MarkdownContext.d.ts +26 -25
  112. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  113. package/lib/typescript/module/headless.d.ts +15 -2
  114. package/lib/typescript/module/headless.d.ts.map +1 -1
  115. package/lib/typescript/module/index.d.ts +3 -1
  116. package/lib/typescript/module/index.d.ts.map +1 -1
  117. package/lib/typescript/module/markdown-stream.d.ts +7 -2
  118. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  119. package/lib/typescript/module/markdown.d.ts +62 -5
  120. package/lib/typescript/module/markdown.d.ts.map +1 -1
  121. package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
  122. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  123. package/lib/typescript/module/renderers/code.d.ts +5 -5
  124. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  125. package/lib/typescript/module/renderers/heading.d.ts +3 -3
  126. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  127. package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
  128. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  129. package/lib/typescript/module/renderers/image.d.ts +2 -2
  130. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  131. package/lib/typescript/module/renderers/link.d.ts +3 -3
  132. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  133. package/lib/typescript/module/renderers/list.d.ts +7 -7
  134. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  135. package/lib/typescript/module/renderers/math.d.ts +4 -4
  136. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  137. package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
  138. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  139. package/lib/typescript/module/renderers/table.d.ts +3 -3
  140. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  141. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +5 -2
  142. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
  143. package/lib/typescript/module/theme.d.ts +2 -2
  144. package/lib/typescript/module/theme.d.ts.map +1 -1
  145. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  146. package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
  147. package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
  148. package/lib/typescript/module/utils/link-security.d.ts +3 -0
  149. package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
  150. package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
  151. package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
  152. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -0
  153. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  154. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +18 -6
  155. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +4 -2
  156. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/Func_void_double_double.kt +80 -0
  157. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +11 -3
  158. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.cpp +8 -0
  159. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.hpp +31 -0
  160. package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +20 -2
  161. package/nitrogen/generated/ios/swift/Func_void.swift +0 -1
  162. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
  163. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +4 -3
  164. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +34 -10
  165. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
  166. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
  167. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +2 -0
  168. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +4 -2
  169. package/package.json +7 -5
  170. package/src/Markdown.nitro.ts +7 -3
  171. package/src/MarkdownContext.ts +31 -25
  172. package/src/headless.ts +44 -6
  173. package/src/index.ts +8 -0
  174. package/src/markdown-stream.tsx +159 -15
  175. package/src/markdown.tsx +406 -50
  176. package/src/renderers/blockquote.tsx +4 -4
  177. package/src/renderers/code.tsx +16 -10
  178. package/src/renderers/heading.tsx +4 -4
  179. package/src/renderers/horizontal-rule.tsx +3 -3
  180. package/src/renderers/image.tsx +11 -9
  181. package/src/renderers/link.tsx +25 -7
  182. package/src/renderers/list.tsx +9 -12
  183. package/src/renderers/math.tsx +4 -4
  184. package/src/renderers/paragraph.tsx +3 -3
  185. package/src/renderers/table.tsx +270 -98
  186. package/src/specs/MarkdownSession.nitro.ts +6 -2
  187. package/src/theme.ts +3 -3
  188. package/src/use-markdown-stream.ts +22 -16
  189. package/src/utils/incremental-ast.ts +224 -0
  190. package/src/utils/link-security.ts +22 -0
  191. package/src/utils/stream-timeline.ts +72 -0
package/src/markdown.tsx CHANGED
@@ -1,18 +1,21 @@
1
1
  import {
2
- defaultMarkdownTheme,
3
- minimalMarkdownTheme,
4
- mergeThemes,
5
- type MarkdownTheme,
6
- type PartialMarkdownTheme,
7
- type NodeStyleOverrides,
8
- type StylingStrategy,
9
- } from "./theme";
10
- import { useMemo, type ReactNode, type FC, Fragment } from "react";
2
+ memo,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ type FC,
7
+ Fragment,
8
+ type ReactElement,
9
+ type ReactNode,
10
+ } from "react";
11
11
  import {
12
12
  StyleSheet,
13
13
  View,
14
14
  Text,
15
+ FlatList,
15
16
  Platform,
17
+ type ListRenderItemInfo,
18
+ type FlatListProps,
16
19
  type StyleProp,
17
20
  type ViewStyle,
18
21
  } from "react-native";
@@ -28,21 +31,214 @@ import {
28
31
  MarkdownContext,
29
32
  useMarkdownContext,
30
33
  type CustomRenderers,
34
+ type LinkPressHandler,
31
35
  type NodeRendererProps,
32
36
  } from "./MarkdownContext";
33
-
34
- import { Heading } from "./renderers/heading";
35
- import { Paragraph } from "./renderers/paragraph";
36
- import { Link } from "./renderers/link";
37
37
  import { Blockquote } from "./renderers/blockquote";
38
- import { HorizontalRule } from "./renderers/horizontal-rule";
39
38
  import { CodeBlock, InlineCode } from "./renderers/code";
40
- import { List, ListItem, TaskListItem } from "./renderers/list";
41
- import { TableRenderer } from "./renderers/table";
39
+ import { Heading } from "./renderers/heading";
40
+ import { HorizontalRule } from "./renderers/horizontal-rule";
42
41
  import { Image } from "./renderers/image";
42
+ import { Link } from "./renderers/link";
43
+ import { List, ListItem, TaskListItem } from "./renderers/list";
43
44
  import { MathInline, MathBlock } from "./renderers/math";
45
+ import { Paragraph } from "./renderers/paragraph";
46
+ import { TableRenderer } from "./renderers/table";
47
+ import {
48
+ defaultMarkdownTheme,
49
+ minimalMarkdownTheme,
50
+ mergeThemes,
51
+ type MarkdownTheme,
52
+ type PartialMarkdownTheme,
53
+ type NodeStyleOverrides,
54
+ type StylingStrategy,
55
+ } from "./theme";
44
56
 
45
- export interface MarkdownProps {
57
+ const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
58
+ const parseAstCache = new Map<string, MarkdownNode>();
59
+ const MAX_PARSE_CACHE_ENTRIES = 32;
60
+ const MAX_CACHEABLE_TEXT_LENGTH = 24_000;
61
+
62
+ export type AstTransform = (ast: MarkdownNode) => MarkdownNode;
63
+ export type MarkdownVirtualizationOptions = Pick<
64
+ FlatListProps<MarkdownNode>,
65
+ | "initialNumToRender"
66
+ | "maxToRenderPerBatch"
67
+ | "windowSize"
68
+ | "updateCellsBatchingPeriod"
69
+ | "removeClippedSubviews"
70
+ >;
71
+
72
+ export type MarkdownPlugin = {
73
+ /**
74
+ * Optional plugin name used for diagnostics and debugging.
75
+ */
76
+ name?: string;
77
+ /**
78
+ * Optional plugin version metadata for diagnostics.
79
+ */
80
+ version?: string | number;
81
+ /**
82
+ * Optional text preprocessor executed before native parsing.
83
+ * Should return a full markdown string.
84
+ */
85
+ beforeParse?: (markdown: string) => string;
86
+ /**
87
+ * Optional AST postprocessor executed after native parsing.
88
+ */
89
+ afterParse?: AstTransform;
90
+ };
91
+
92
+ const isMarkdownNode = (value: unknown): value is MarkdownNode => {
93
+ if (typeof value !== "object" || value === null) return false;
94
+ return typeof Reflect.get(value, "type") === "string";
95
+ };
96
+
97
+ const warnInDev = (message: string, error?: unknown): void => {
98
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
99
+
100
+ const runtimeConsole = Reflect.get(globalThis, "console");
101
+ if (
102
+ typeof runtimeConsole === "object" &&
103
+ runtimeConsole !== null &&
104
+ "warn" in runtimeConsole &&
105
+ typeof runtimeConsole.warn === "function"
106
+ ) {
107
+ runtimeConsole.warn(message, error);
108
+ }
109
+ };
110
+
111
+ const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
112
+ return {
113
+ ...node,
114
+ children: node.children?.map(cloneMarkdownNode),
115
+ };
116
+ };
117
+
118
+ const getParserOptionsKey = (options?: ParserOptions): string => {
119
+ if (!options) return "gfm:default|math:default";
120
+
121
+ const gfm = options.gfm === undefined ? "default" : options.gfm ? "1" : "0";
122
+ const math =
123
+ options.math === undefined ? "default" : options.math ? "1" : "0";
124
+ return `gfm:${gfm}|math:${math}`;
125
+ };
126
+
127
+ const normalizeParserOptions = (
128
+ options?: ParserOptions,
129
+ ): ParserOptions | undefined => {
130
+ if (!options) return undefined;
131
+
132
+ const gfm = options.gfm;
133
+ const math = options.math;
134
+
135
+ if (gfm === undefined && math === undefined) {
136
+ return undefined;
137
+ }
138
+
139
+ return {
140
+ gfm,
141
+ math,
142
+ };
143
+ };
144
+
145
+ const parseWithNativeParser = (
146
+ text: string,
147
+ options?: ParserOptions,
148
+ ): MarkdownNode => {
149
+ if (options) {
150
+ return parseMarkdownWithOptions(text, options);
151
+ }
152
+ return parseMarkdown(text);
153
+ };
154
+
155
+ const getCachedParsedAst = (
156
+ text: string,
157
+ options?: ParserOptions,
158
+ ): MarkdownNode => {
159
+ if (text.length > MAX_CACHEABLE_TEXT_LENGTH) {
160
+ return parseWithNativeParser(text, options);
161
+ }
162
+
163
+ const cacheKey = `${getParserOptionsKey(options)}|${text}`;
164
+ const cachedNode = parseAstCache.get(cacheKey);
165
+ if (cachedNode) {
166
+ parseAstCache.delete(cacheKey);
167
+ parseAstCache.set(cacheKey, cachedNode);
168
+ return cloneMarkdownNode(cachedNode);
169
+ }
170
+
171
+ const parsedNode = parseWithNativeParser(text, options);
172
+ parseAstCache.set(cacheKey, parsedNode);
173
+ if (parseAstCache.size > MAX_PARSE_CACHE_ENTRIES) {
174
+ const oldestCacheKey = parseAstCache.keys().next().value;
175
+ if (typeof oldestCacheKey === "string") {
176
+ parseAstCache.delete(oldestCacheKey);
177
+ }
178
+ }
179
+
180
+ return cloneMarkdownNode(parsedNode);
181
+ };
182
+
183
+ const applyBeforeParsePlugins = (
184
+ markdown: string,
185
+ plugins?: MarkdownPlugin[],
186
+ ): string => {
187
+ if (!plugins || plugins.length === 0) {
188
+ return markdown;
189
+ }
190
+
191
+ let nextMarkdown = markdown;
192
+ for (const plugin of plugins) {
193
+ if (!plugin.beforeParse) continue;
194
+
195
+ try {
196
+ const transformed = plugin.beforeParse(nextMarkdown);
197
+ if (typeof transformed === "string") {
198
+ nextMarkdown = transformed;
199
+ }
200
+ } catch (error) {
201
+ const pluginLabel = plugin.name ? ` (${plugin.name})` : "";
202
+ warnInDev(
203
+ `[react-native-nitro-markdown] plugin beforeParse${pluginLabel} threw; using previous markdown.`,
204
+ error,
205
+ );
206
+ }
207
+ }
208
+
209
+ return nextMarkdown;
210
+ };
211
+
212
+ const applyAfterParsePlugins = (
213
+ ast: MarkdownNode,
214
+ plugins?: MarkdownPlugin[],
215
+ ): MarkdownNode => {
216
+ if (!plugins || plugins.length === 0) {
217
+ return ast;
218
+ }
219
+
220
+ let nextAst = ast;
221
+ for (const plugin of plugins) {
222
+ if (!plugin.afterParse) continue;
223
+
224
+ try {
225
+ const transformed = plugin.afterParse(nextAst);
226
+ if (isMarkdownNode(transformed)) {
227
+ nextAst = transformed;
228
+ }
229
+ } catch (error) {
230
+ const pluginLabel = plugin.name ? ` (${plugin.name})` : "";
231
+ warnInDev(
232
+ `[react-native-nitro-markdown] plugin afterParse${pluginLabel} threw; using previous AST.`,
233
+ error,
234
+ );
235
+ }
236
+ }
237
+
238
+ return nextAst;
239
+ };
240
+
241
+ export type MarkdownProps = {
46
242
  /**
47
243
  * The markdown string to parse and render.
48
244
  */
@@ -51,6 +247,20 @@ export interface MarkdownProps {
51
247
  * Parser options to enable GFM or Math support.
52
248
  */
53
249
  options?: ParserOptions;
250
+ /**
251
+ * Optional parser plugins for preprocessing and AST postprocessing.
252
+ */
253
+ plugins?: MarkdownPlugin[];
254
+ /**
255
+ * Optional pre-parsed AST.
256
+ * When provided, native parse is skipped and this tree is rendered instead.
257
+ */
258
+ sourceAst?: MarkdownNode;
259
+ /**
260
+ * Optional transform applied after parsing and before rendering.
261
+ * The transformed AST is also returned in `onParseComplete`.
262
+ */
263
+ astTransform?: AstTransform;
54
264
  /**
55
265
  * Callback fired when parsing begins.
56
266
  */
@@ -93,11 +303,36 @@ export interface MarkdownProps {
93
303
  * Optional style for the container view.
94
304
  */
95
305
  style?: StyleProp<ViewStyle>;
96
- }
306
+ /**
307
+ * Optional link press handler.
308
+ * Return false to prevent the default openURL behavior.
309
+ */
310
+ onLinkPress?: LinkPressHandler;
311
+ /**
312
+ * Enables top-level block virtualization for very large markdown documents.
313
+ * Best used when Markdown is the primary scroll container on screen.
314
+ * - `true`: always virtualize when block threshold is met
315
+ * - `"auto"`: virtualize only when threshold is met (recommended for large docs)
316
+ * - `false`: disable virtualization (default)
317
+ */
318
+ virtualize?: boolean | "auto";
319
+ /**
320
+ * Minimum number of top-level blocks before virtualization is activated.
321
+ * Helps avoid FlatList overhead on small documents.
322
+ */
323
+ virtualizationMinBlocks?: number;
324
+ /**
325
+ * Optional FlatList tuning for virtualization.
326
+ */
327
+ virtualization?: MarkdownVirtualizationOptions;
328
+ };
97
329
 
98
330
  export const Markdown: FC<MarkdownProps> = ({
99
331
  children,
100
332
  options,
333
+ plugins,
334
+ sourceAst,
335
+ astTransform,
101
336
  renderers = {},
102
337
  theme: userTheme,
103
338
  styles: nodeStyles,
@@ -105,34 +340,78 @@ export const Markdown: FC<MarkdownProps> = ({
105
340
  style,
106
341
  onParsingInProgress,
107
342
  onParseComplete,
343
+ onLinkPress,
344
+ virtualize = false,
345
+ virtualizationMinBlocks = 40,
346
+ virtualization,
108
347
  }) => {
109
- const ast = useMemo(() => {
110
- try {
111
- if (onParsingInProgress) {
112
- onParsingInProgress();
113
- }
348
+ const parserOptionGfm = options?.gfm;
349
+ const parserOptionMath = options?.math;
114
350
 
115
- let result: MarkdownNode;
116
- if (options) {
117
- result = parseMarkdownWithOptions(children, options);
118
- } else {
119
- result = parseMarkdown(children);
120
- }
121
-
122
- if (onParseComplete) {
123
- onParseComplete({
124
- raw: children,
125
- ast: result,
126
- text: getFlattenedText(result),
127
- });
351
+ const parseResult = useMemo(() => {
352
+ try {
353
+ const markdownToParse = applyBeforeParsePlugins(children, plugins);
354
+ const parserOptions = normalizeParserOptions({
355
+ gfm: parserOptionGfm,
356
+ math: parserOptionMath,
357
+ });
358
+ let parsedAst = sourceAst
359
+ ? cloneMarkdownNode(sourceAst)
360
+ : getCachedParsedAst(markdownToParse, parserOptions);
361
+ parsedAst = applyAfterParsePlugins(parsedAst, plugins);
362
+
363
+ let ast = parsedAst;
364
+ if (astTransform) {
365
+ try {
366
+ const nextAst = astTransform(parsedAst);
367
+ if (isMarkdownNode(nextAst)) {
368
+ ast = nextAst;
369
+ }
370
+ } catch (error) {
371
+ warnInDev(
372
+ "[react-native-nitro-markdown] astTransform threw; falling back to parsed AST.",
373
+ error,
374
+ );
375
+ ast = parsedAst;
376
+ }
128
377
  }
129
378
 
130
- return result;
131
- } catch (error) {
132
- console.error("Failed to parse markdown:", error);
133
- return null;
379
+ return {
380
+ ast,
381
+ };
382
+ } catch {
383
+ return {
384
+ ast: null,
385
+ };
134
386
  }
135
- }, [children, options, onParsingInProgress, onParseComplete]);
387
+ }, [
388
+ children,
389
+ parserOptionGfm,
390
+ parserOptionMath,
391
+ plugins,
392
+ sourceAst,
393
+ astTransform,
394
+ ]);
395
+
396
+ useEffect(() => {
397
+ onParsingInProgress?.();
398
+ }, [
399
+ children,
400
+ parserOptionGfm,
401
+ parserOptionMath,
402
+ plugins,
403
+ onParsingInProgress,
404
+ ]);
405
+
406
+ useEffect(() => {
407
+ if (!parseResult.ast || !onParseComplete) return;
408
+
409
+ onParseComplete({
410
+ raw: children,
411
+ ast: parseResult.ast,
412
+ text: getFlattenedText(parseResult.ast),
413
+ });
414
+ }, [children, onParseComplete, parseResult.ast]);
136
415
 
137
416
  const theme = useMemo(() => {
138
417
  const base =
@@ -142,9 +421,44 @@ export const Markdown: FC<MarkdownProps> = ({
142
421
  return mergeThemes(base, userTheme);
143
422
  }, [userTheme, stylingStrategy]);
144
423
 
145
- const baseStyles = useMemo(() => createBaseStyles(theme), [theme]);
424
+ const baseStyles = getBaseStyles(theme);
425
+ const contextValue = useMemo(
426
+ () => ({
427
+ renderers,
428
+ theme,
429
+ styles: nodeStyles,
430
+ stylingStrategy,
431
+ onLinkPress,
432
+ }),
433
+ [renderers, theme, nodeStyles, stylingStrategy, onLinkPress],
434
+ );
435
+
436
+ const topLevelBlocks =
437
+ parseResult.ast?.type === "document"
438
+ ? (parseResult.ast.children ?? [])
439
+ : parseResult.ast
440
+ ? [parseResult.ast]
441
+ : [];
442
+ const shouldVirtualizeBySetting =
443
+ virtualize === true ||
444
+ (virtualize === "auto" && topLevelBlocks.length >= virtualizationMinBlocks);
445
+ const shouldVirtualize =
446
+ parseResult.ast !== null && shouldVirtualizeBySetting;
447
+
448
+ const keyExtractor = useCallback((node: MarkdownNode, index: number) => {
449
+ const beg = typeof node.beg === "number" ? node.beg : index;
450
+ const end = typeof node.end === "number" ? node.end : index;
451
+ return `${node.type}:${beg}:${end}:${index}`;
452
+ }, []);
453
+
454
+ const renderVirtualizedItem = useCallback(
455
+ ({ item }: ListRenderItemInfo<MarkdownNode>): ReactElement => (
456
+ <NodeRenderer node={item} depth={0} inListItem={false} />
457
+ ),
458
+ [],
459
+ );
146
460
 
147
- if (!ast) {
461
+ if (!parseResult.ast) {
148
462
  return (
149
463
  <View style={[baseStyles.container, style]}>
150
464
  <Text style={baseStyles.errorText}>Error parsing markdown</Text>
@@ -153,11 +467,28 @@ export const Markdown: FC<MarkdownProps> = ({
153
467
  }
154
468
 
155
469
  return (
156
- <MarkdownContext.Provider
157
- value={{ renderers, theme, styles: nodeStyles, stylingStrategy }}
158
- >
470
+ <MarkdownContext.Provider value={contextValue}>
159
471
  <View style={[baseStyles.container, style]}>
160
- <NodeRenderer node={ast} depth={0} inListItem={false} />
472
+ {shouldVirtualize ? (
473
+ <FlatList
474
+ data={topLevelBlocks}
475
+ renderItem={renderVirtualizedItem}
476
+ keyExtractor={keyExtractor}
477
+ style={baseStyles.virtualizedList}
478
+ initialNumToRender={virtualization?.initialNumToRender ?? 12}
479
+ maxToRenderPerBatch={virtualization?.maxToRenderPerBatch ?? 12}
480
+ windowSize={virtualization?.windowSize ?? 10}
481
+ updateCellsBatchingPeriod={
482
+ virtualization?.updateCellsBatchingPeriod ?? 16
483
+ }
484
+ removeClippedSubviews={
485
+ virtualization?.removeClippedSubviews ?? true
486
+ }
487
+ showsVerticalScrollIndicator={false}
488
+ />
489
+ ) : (
490
+ <NodeRenderer node={parseResult.ast} depth={0} inListItem={false} />
491
+ )}
161
492
  </View>
162
493
  </MarkdownContext.Provider>
163
494
  );
@@ -178,14 +509,14 @@ const isInline = (type: MarkdownNode["type"]): boolean => {
178
509
  );
179
510
  };
180
511
 
181
- const NodeRenderer: FC<NodeRendererProps> = ({
512
+ const NodeRendererComponent: FC<NodeRendererProps> = ({
182
513
  node,
183
514
  depth,
184
515
  inListItem,
185
516
  parentIsText = false,
186
517
  }) => {
187
518
  const { renderers, theme, styles: nodeStyles } = useMarkdownContext();
188
- const baseStyles = useMemo(() => createBaseStyles(theme), [theme]);
519
+ const baseStyles = getBaseStyles(theme);
189
520
 
190
521
  const renderChildren = (
191
522
  children?: MarkdownNode[],
@@ -308,7 +639,9 @@ const NodeRenderer: FC<NodeRendererProps> = ({
308
639
  };
309
640
 
310
641
  const result = customRenderer(enhancedProps);
311
- if (result !== undefined) return <>{result}</>;
642
+ if (result !== undefined) {
643
+ return result as ReactElement | null;
644
+ }
312
645
  }
313
646
 
314
647
  const nodeStyleOverride = nodeStyles?.[node.type];
@@ -491,11 +824,34 @@ const NodeRenderer: FC<NodeRendererProps> = ({
491
824
  }
492
825
  };
493
826
 
827
+ const NodeRenderer = memo(NodeRendererComponent, (previousProps, nextProps) => {
828
+ return (
829
+ previousProps.node === nextProps.node &&
830
+ previousProps.depth === nextProps.depth &&
831
+ previousProps.inListItem === nextProps.inListItem &&
832
+ previousProps.parentIsText === nextProps.parentIsText
833
+ );
834
+ }) as FC<NodeRendererProps>;
835
+
836
+ type BaseStyles = ReturnType<typeof createBaseStyles>;
837
+
838
+ const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
839
+ const cached = baseStylesCache.get(theme);
840
+ if (cached) return cached;
841
+
842
+ const created = createBaseStyles(theme);
843
+ baseStylesCache.set(theme, created);
844
+ return created;
845
+ };
846
+
494
847
  const createBaseStyles = (theme: MarkdownTheme) =>
495
848
  StyleSheet.create({
496
849
  container: {
497
850
  flex: 1,
498
851
  },
852
+ virtualizedList: {
853
+ flex: 1,
854
+ },
499
855
  document: {
500
856
  flex: 1,
501
857
  },
@@ -1,11 +1,11 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import { View, StyleSheet, type ViewStyle } from "react-native";
3
3
  import { useMarkdownContext } from "../MarkdownContext";
4
4
 
5
- interface BlockquoteProps {
5
+ type BlockquoteProps = {
6
6
  children: ReactNode;
7
7
  style?: ViewStyle;
8
- }
8
+ };
9
9
 
10
10
  export const Blockquote: FC<BlockquoteProps> = ({ children, style }) => {
11
11
  const { theme } = useMarkdownContext();
@@ -23,7 +23,7 @@ export const Blockquote: FC<BlockquoteProps> = ({ children, style }) => {
23
23
  borderRadius: theme.borderRadius.s,
24
24
  },
25
25
  }),
26
- [theme]
26
+ [theme],
27
27
  );
28
28
 
29
29
  return <View style={[styles.blockquote, style]}>{children}</View>;
@@ -1,4 +1,4 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -8,16 +8,16 @@ import {
8
8
  type ViewStyle,
9
9
  type TextStyle,
10
10
  } from "react-native";
11
+ import { getTextContent } from "../headless";
11
12
  import { useMarkdownContext } from "../MarkdownContext";
12
13
  import type { MarkdownNode } from "../headless";
13
- import { getTextContent } from "../headless";
14
14
 
15
- interface CodeBlockProps {
15
+ type CodeBlockProps = {
16
16
  language?: string;
17
17
  content?: string;
18
18
  node?: MarkdownNode;
19
19
  style?: ViewStyle;
20
- }
20
+ };
21
21
 
22
22
  export const CodeBlock: FC<CodeBlockProps> = ({
23
23
  language,
@@ -60,27 +60,33 @@ export const CodeBlock: FC<CodeBlockProps> = ({
60
60
  ...(Platform.OS === "android" && { includeFontPadding: false }),
61
61
  },
62
62
  }),
63
- [theme]
63
+ [theme],
64
64
  );
65
65
 
66
66
  const showLanguage = theme.showCodeLanguage && language;
67
67
 
68
68
  return (
69
69
  <View style={[styles.codeBlock, style]}>
70
- {showLanguage && <Text style={styles.codeLanguage}>{language}</Text>}
71
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
70
+ {showLanguage ? (
71
+ <Text style={styles.codeLanguage}>{language}</Text>
72
+ ) : null}
73
+ <ScrollView
74
+ horizontal
75
+ showsHorizontalScrollIndicator={false}
76
+ bounces={false}
77
+ >
72
78
  <Text style={styles.codeBlockText}>{displayContent}</Text>
73
79
  </ScrollView>
74
80
  </View>
75
81
  );
76
82
  };
77
83
 
78
- interface InlineCodeProps {
84
+ type InlineCodeProps = {
79
85
  content?: string;
80
86
  node?: MarkdownNode;
81
87
  children?: ReactNode;
82
88
  style?: TextStyle;
83
- }
89
+ };
84
90
 
85
91
  export const InlineCode: FC<InlineCodeProps> = ({
86
92
  content,
@@ -109,7 +115,7 @@ export const InlineCode: FC<InlineCodeProps> = ({
109
115
  ...(Platform.OS === "android" && { includeFontPadding: false }),
110
116
  },
111
117
  }),
112
- [theme]
118
+ [theme],
113
119
  );
114
120
  return <Text style={[styles.codeInline, style]}>{displayContent}</Text>;
115
121
  };
@@ -1,12 +1,12 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import { Text, StyleSheet, Platform, type TextStyle } from "react-native";
3
3
  import { useMarkdownContext } from "../MarkdownContext";
4
4
 
5
- interface HeadingProps {
5
+ type HeadingProps = {
6
6
  level: number;
7
7
  children: ReactNode;
8
8
  style?: TextStyle;
9
- }
9
+ };
10
10
 
11
11
  const ANDROID_SYSTEM_FONTS = new Set([
12
12
  "sans-serif",
@@ -71,7 +71,7 @@ export const Heading: FC<HeadingProps> = ({ level, children, style }) => {
71
71
  color: theme.colors.textMuted,
72
72
  },
73
73
  }),
74
- [theme]
74
+ [headingWeight, theme],
75
75
  );
76
76
 
77
77
  const headingStyles = [
@@ -2,9 +2,9 @@ import { useMemo, type FC } from "react";
2
2
  import { View, StyleSheet, type ViewStyle } from "react-native";
3
3
  import { useMarkdownContext } from "../MarkdownContext";
4
4
 
5
- interface HorizontalRuleProps {
5
+ type HorizontalRuleProps = {
6
6
  style?: ViewStyle;
7
- }
7
+ };
8
8
 
9
9
  export const HorizontalRule: FC<HorizontalRuleProps> = ({ style }) => {
10
10
  const { theme } = useMarkdownContext();
@@ -17,7 +17,7 @@ export const HorizontalRule: FC<HorizontalRuleProps> = ({ style }) => {
17
17
  marginVertical: theme.spacing.xl,
18
18
  },
19
19
  }),
20
- [theme]
20
+ [theme],
21
21
  );
22
22
  return <View style={[styles.horizontalRule, style]} />;
23
23
  };