react-native-nitro-markdown 0.4.3 → 0.5.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 (175) hide show
  1. package/README.md +417 -25
  2. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +46 -8
  3. package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +2 -1
  4. package/cpp/bindings/HybridMarkdownParser.cpp +216 -66
  5. package/cpp/bindings/HybridMarkdownParser.hpp +2 -0
  6. package/ios/HybridMarkdownSession.swift +51 -7
  7. package/lib/commonjs/MarkdownContext.js.map +1 -1
  8. package/lib/commonjs/headless.js +61 -5
  9. package/lib/commonjs/headless.js.map +1 -1
  10. package/lib/commonjs/index.js +9 -1
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/markdown-stream.js +107 -13
  13. package/lib/commonjs/markdown-stream.js.map +1 -1
  14. package/lib/commonjs/markdown.js +191 -26
  15. package/lib/commonjs/markdown.js.map +1 -1
  16. package/lib/commonjs/renderers/code.js +21 -2
  17. package/lib/commonjs/renderers/code.js.map +1 -1
  18. package/lib/commonjs/renderers/table/cell-content.js +32 -0
  19. package/lib/commonjs/renderers/table/cell-content.js.map +1 -0
  20. package/lib/commonjs/renderers/table/index.js +310 -0
  21. package/lib/commonjs/renderers/table/index.js.map +1 -0
  22. package/lib/commonjs/renderers/table/table-reducer.js +29 -0
  23. package/lib/commonjs/renderers/table/table-reducer.js.map +1 -0
  24. package/lib/commonjs/renderers/table/table-utils.js +68 -0
  25. package/lib/commonjs/renderers/table/table-utils.js.map +1 -0
  26. package/lib/commonjs/renderers/table/types.js +6 -0
  27. package/lib/commonjs/renderers/table/types.js.map +1 -0
  28. package/lib/commonjs/renderers/table.js +6 -306
  29. package/lib/commonjs/renderers/table.js.map +1 -1
  30. package/lib/commonjs/theme.js +10 -1
  31. package/lib/commonjs/theme.js.map +1 -1
  32. package/lib/commonjs/use-markdown-stream.js +9 -1
  33. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  34. package/lib/commonjs/utils/code-highlight.js +101 -0
  35. package/lib/commonjs/utils/code-highlight.js.map +1 -0
  36. package/lib/commonjs/utils/incremental-ast.js +153 -0
  37. package/lib/commonjs/utils/incremental-ast.js.map +1 -0
  38. package/lib/module/MarkdownContext.js.map +1 -1
  39. package/lib/module/headless.js +56 -4
  40. package/lib/module/headless.js.map +1 -1
  41. package/lib/module/index.js +1 -0
  42. package/lib/module/index.js.map +1 -1
  43. package/lib/module/markdown-stream.js +108 -14
  44. package/lib/module/markdown-stream.js.map +1 -1
  45. package/lib/module/markdown.js +193 -28
  46. package/lib/module/markdown.js.map +1 -1
  47. package/lib/module/renderers/code.js +21 -2
  48. package/lib/module/renderers/code.js.map +1 -1
  49. package/lib/module/renderers/table/cell-content.js +27 -0
  50. package/lib/module/renderers/table/cell-content.js.map +1 -0
  51. package/lib/module/renderers/table/index.js +305 -0
  52. package/lib/module/renderers/table/index.js.map +1 -0
  53. package/lib/module/renderers/table/table-reducer.js +24 -0
  54. package/lib/module/renderers/table/table-reducer.js.map +1 -0
  55. package/lib/module/renderers/table/table-utils.js +62 -0
  56. package/lib/module/renderers/table/table-utils.js.map +1 -0
  57. package/lib/module/renderers/table/types.js +4 -0
  58. package/lib/module/renderers/table/types.js.map +1 -0
  59. package/lib/module/renderers/table.js +1 -305
  60. package/lib/module/renderers/table.js.map +1 -1
  61. package/lib/module/theme.js +10 -1
  62. package/lib/module/theme.js.map +1 -1
  63. package/lib/module/use-markdown-stream.js +9 -1
  64. package/lib/module/use-markdown-stream.js.map +1 -1
  65. package/lib/module/utils/code-highlight.js +97 -0
  66. package/lib/module/utils/code-highlight.js.map +1 -0
  67. package/lib/module/utils/incremental-ast.js +147 -0
  68. package/lib/module/utils/incremental-ast.js.map +1 -0
  69. package/lib/typescript/commonjs/Markdown.nitro.d.ts +2 -0
  70. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/MarkdownContext.d.ts +6 -0
  72. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/headless.d.ts +18 -0
  74. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/index.d.ts +4 -0
  76. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/markdown-stream.d.ts +6 -1
  78. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/markdown.d.ts +77 -1
  80. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +15 -0
  83. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/renderers/table/index.d.ts +11 -0
  85. package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/renderers/table/table-reducer.d.ts +5 -0
  87. package/lib/typescript/commonjs/renderers/table/table-reducer.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/renderers/table/table-utils.d.ts +10 -0
  89. package/lib/typescript/commonjs/renderers/table/table-utils.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/renderers/table/types.d.ts +24 -0
  91. package/lib/typescript/commonjs/renderers/table/types.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/renderers/table.d.ts +1 -11
  93. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +14 -2
  95. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/theme.d.ts +18 -2
  97. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/use-markdown-stream.d.ts +4 -0
  99. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/utils/code-highlight.d.ts +8 -0
  101. package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
  103. package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
  104. package/lib/typescript/module/Markdown.nitro.d.ts +2 -0
  105. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  106. package/lib/typescript/module/MarkdownContext.d.ts +6 -0
  107. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  108. package/lib/typescript/module/headless.d.ts +18 -0
  109. package/lib/typescript/module/headless.d.ts.map +1 -1
  110. package/lib/typescript/module/index.d.ts +4 -0
  111. package/lib/typescript/module/index.d.ts.map +1 -1
  112. package/lib/typescript/module/markdown-stream.d.ts +6 -1
  113. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  114. package/lib/typescript/module/markdown.d.ts +77 -1
  115. package/lib/typescript/module/markdown.d.ts.map +1 -1
  116. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  117. package/lib/typescript/module/renderers/table/cell-content.d.ts +15 -0
  118. package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -0
  119. package/lib/typescript/module/renderers/table/index.d.ts +11 -0
  120. package/lib/typescript/module/renderers/table/index.d.ts.map +1 -0
  121. package/lib/typescript/module/renderers/table/table-reducer.d.ts +5 -0
  122. package/lib/typescript/module/renderers/table/table-reducer.d.ts.map +1 -0
  123. package/lib/typescript/module/renderers/table/table-utils.d.ts +10 -0
  124. package/lib/typescript/module/renderers/table/table-utils.d.ts.map +1 -0
  125. package/lib/typescript/module/renderers/table/types.d.ts +24 -0
  126. package/lib/typescript/module/renderers/table/types.d.ts.map +1 -0
  127. package/lib/typescript/module/renderers/table.d.ts +1 -11
  128. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  129. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +14 -2
  130. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
  131. package/lib/typescript/module/theme.d.ts +18 -2
  132. package/lib/typescript/module/theme.d.ts.map +1 -1
  133. package/lib/typescript/module/use-markdown-stream.d.ts +4 -0
  134. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  135. package/lib/typescript/module/utils/code-highlight.d.ts +8 -0
  136. package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -0
  137. package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
  138. package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
  139. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +38 -26
  140. package/nitrogen/generated/android/NitroMarkdownOnLoad.hpp +13 -4
  141. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  142. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +49 -34
  143. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +25 -24
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/Func_void_double_double.kt +80 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +34 -21
  146. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.cpp +8 -0
  147. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.hpp +31 -0
  148. package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +34 -2
  149. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
  150. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +6 -2
  151. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +57 -9
  152. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
  153. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
  154. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +4 -0
  155. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +6 -2
  156. package/package.json +9 -5
  157. package/react-native-nitro-markdown.podspec +1 -1
  158. package/src/Markdown.nitro.ts +2 -0
  159. package/src/MarkdownContext.ts +6 -0
  160. package/src/headless.ts +54 -4
  161. package/src/index.ts +10 -0
  162. package/src/markdown-stream.tsx +163 -15
  163. package/src/markdown.tsx +381 -26
  164. package/src/renderers/code.tsx +32 -3
  165. package/src/renderers/table/cell-content.tsx +38 -0
  166. package/src/renderers/table/index.tsx +419 -0
  167. package/src/renderers/table/table-reducer.ts +36 -0
  168. package/src/renderers/table/table-utils.ts +81 -0
  169. package/src/renderers/table/types.ts +24 -0
  170. package/src/renderers/table.tsx +1 -401
  171. package/src/specs/MarkdownSession.nitro.ts +16 -2
  172. package/src/theme.ts +29 -1
  173. package/src/use-markdown-stream.ts +10 -0
  174. package/src/utils/code-highlight.ts +102 -0
  175. package/src/utils/incremental-ast.ts +224 -0
package/src/markdown.tsx CHANGED
@@ -1,4 +1,6 @@
1
1
  import {
2
+ memo,
3
+ useCallback,
2
4
  useEffect,
3
5
  useMemo,
4
6
  type FC,
@@ -10,8 +12,12 @@ import {
10
12
  StyleSheet,
11
13
  View,
12
14
  Text,
15
+ FlatList,
13
16
  Platform,
17
+ type ListRenderItemInfo,
18
+ type FlatListProps,
14
19
  type StyleProp,
20
+ type TextStyle,
15
21
  type ViewStyle,
16
22
  } from "react-native";
17
23
  import {
@@ -48,8 +54,201 @@ import {
48
54
  type NodeStyleOverrides,
49
55
  type StylingStrategy,
50
56
  } from "./theme";
57
+ import type { CodeHighlighter } from "./utils/code-highlight";
51
58
 
52
59
  const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
60
+ const parseAstCache = new Map<string, MarkdownNode>();
61
+ const MAX_PARSE_CACHE_ENTRIES = 32;
62
+ const MAX_CACHEABLE_TEXT_LENGTH = 24_000;
63
+
64
+ export type AstTransform = (ast: MarkdownNode) => MarkdownNode;
65
+ export type MarkdownVirtualizationOptions = Pick<
66
+ FlatListProps<MarkdownNode>,
67
+ | "initialNumToRender"
68
+ | "maxToRenderPerBatch"
69
+ | "windowSize"
70
+ | "updateCellsBatchingPeriod"
71
+ | "removeClippedSubviews"
72
+ >;
73
+
74
+ export type MarkdownPlugin = {
75
+ /**
76
+ * Optional plugin name used for diagnostics and debugging.
77
+ */
78
+ name?: string;
79
+ /**
80
+ * Optional plugin version metadata for diagnostics.
81
+ */
82
+ version?: string | number;
83
+ /**
84
+ * Execution priority. Higher values run first (default: 0).
85
+ */
86
+ priority?: number;
87
+ /**
88
+ * Optional text preprocessor executed before native parsing.
89
+ * Should return a full markdown string.
90
+ */
91
+ beforeParse?: (markdown: string) => string;
92
+ /**
93
+ * Optional AST postprocessor executed after native parsing.
94
+ */
95
+ afterParse?: AstTransform;
96
+ };
97
+
98
+ const isMarkdownNode = (value: unknown): value is MarkdownNode => {
99
+ if (typeof value !== "object" || value === null) return false;
100
+ return typeof Reflect.get(value, "type") === "string";
101
+ };
102
+
103
+ const warnInDev = (message: string, error?: unknown): void => {
104
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
105
+
106
+ const runtimeConsole = Reflect.get(globalThis, "console");
107
+ if (
108
+ typeof runtimeConsole === "object" &&
109
+ runtimeConsole !== null &&
110
+ "warn" in runtimeConsole &&
111
+ typeof runtimeConsole.warn === "function"
112
+ ) {
113
+ runtimeConsole.warn(message, error);
114
+ }
115
+ };
116
+
117
+ const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
118
+ return {
119
+ ...node,
120
+ children: node.children?.map(cloneMarkdownNode),
121
+ };
122
+ };
123
+
124
+ const getParserOptionsKey = (options?: ParserOptions): string => {
125
+ if (!options) return "gfm:default|math:default";
126
+
127
+ const gfm = options.gfm === undefined ? "default" : options.gfm ? "1" : "0";
128
+ const math =
129
+ options.math === undefined ? "default" : options.math ? "1" : "0";
130
+ return `gfm:${gfm}|math:${math}`;
131
+ };
132
+
133
+ const normalizeParserOptions = (
134
+ options?: ParserOptions,
135
+ ): ParserOptions | undefined => {
136
+ if (!options) return undefined;
137
+
138
+ const gfm = options.gfm;
139
+ const math = options.math;
140
+
141
+ if (gfm === undefined && math === undefined) {
142
+ return undefined;
143
+ }
144
+
145
+ return {
146
+ gfm,
147
+ math,
148
+ };
149
+ };
150
+
151
+ const parseWithNativeParser = (
152
+ text: string,
153
+ options?: ParserOptions,
154
+ ): MarkdownNode => {
155
+ if (options) {
156
+ return parseMarkdownWithOptions(text, options);
157
+ }
158
+ return parseMarkdown(text);
159
+ };
160
+
161
+ const getCachedParsedAst = (
162
+ text: string,
163
+ options?: ParserOptions,
164
+ ): MarkdownNode => {
165
+ if (text.length > MAX_CACHEABLE_TEXT_LENGTH) {
166
+ return parseWithNativeParser(text, options);
167
+ }
168
+
169
+ const cacheKey = `${getParserOptionsKey(options)}|${text}`;
170
+ const cachedNode = parseAstCache.get(cacheKey);
171
+ if (cachedNode) {
172
+ parseAstCache.delete(cacheKey);
173
+ parseAstCache.set(cacheKey, cachedNode);
174
+ return cloneMarkdownNode(cachedNode);
175
+ }
176
+
177
+ const parsedNode = parseWithNativeParser(text, options);
178
+ parseAstCache.set(cacheKey, parsedNode);
179
+ if (parseAstCache.size > MAX_PARSE_CACHE_ENTRIES) {
180
+ const oldestCacheKey = parseAstCache.keys().next().value;
181
+ if (typeof oldestCacheKey === "string") {
182
+ parseAstCache.delete(oldestCacheKey);
183
+ }
184
+ }
185
+
186
+ return cloneMarkdownNode(parsedNode);
187
+ };
188
+
189
+ const applyBeforeParsePlugins = (
190
+ markdown: string,
191
+ plugins?: MarkdownPlugin[],
192
+ onError?: (error: Error, phase: 'before-plugin', pluginName?: string) => void,
193
+ ): string => {
194
+ if (!plugins || plugins.length === 0) {
195
+ return markdown;
196
+ }
197
+
198
+ const sorted = [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
199
+ let nextMarkdown = markdown;
200
+ for (const plugin of sorted) {
201
+ if (!plugin.beforeParse) continue;
202
+
203
+ try {
204
+ const transformed = plugin.beforeParse(nextMarkdown);
205
+ if (typeof transformed === "string") {
206
+ nextMarkdown = transformed;
207
+ }
208
+ } catch (error) {
209
+ const pluginLabel = plugin.name ? ` (${plugin.name})` : "";
210
+ warnInDev(
211
+ `[react-native-nitro-markdown] plugin beforeParse${pluginLabel} threw; using previous markdown.`,
212
+ error,
213
+ );
214
+ onError?.(error instanceof Error ? error : new Error(String(error)), 'before-plugin', plugin.name);
215
+ }
216
+ }
217
+
218
+ return nextMarkdown;
219
+ };
220
+
221
+ const applyAfterParsePlugins = (
222
+ ast: MarkdownNode,
223
+ plugins?: MarkdownPlugin[],
224
+ onError?: (error: Error, phase: 'after-plugin', pluginName?: string) => void,
225
+ ): MarkdownNode => {
226
+ if (!plugins || plugins.length === 0) {
227
+ return ast;
228
+ }
229
+
230
+ const sorted = [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
231
+ let nextAst = ast;
232
+ for (const plugin of sorted) {
233
+ if (!plugin.afterParse) continue;
234
+
235
+ try {
236
+ const transformed = plugin.afterParse(nextAst);
237
+ if (isMarkdownNode(transformed)) {
238
+ nextAst = transformed;
239
+ }
240
+ } catch (error) {
241
+ const pluginLabel = plugin.name ? ` (${plugin.name})` : "";
242
+ warnInDev(
243
+ `[react-native-nitro-markdown] plugin afterParse${pluginLabel} threw; using previous AST.`,
244
+ error,
245
+ );
246
+ onError?.(error instanceof Error ? error : new Error(String(error)), 'after-plugin', plugin.name);
247
+ }
248
+ }
249
+
250
+ return nextAst;
251
+ };
53
252
 
54
253
  export type MarkdownProps = {
55
254
  /**
@@ -60,6 +259,20 @@ export type MarkdownProps = {
60
259
  * Parser options to enable GFM or Math support.
61
260
  */
62
261
  options?: ParserOptions;
262
+ /**
263
+ * Optional parser plugins for preprocessing and AST postprocessing.
264
+ */
265
+ plugins?: MarkdownPlugin[];
266
+ /**
267
+ * Optional pre-parsed AST.
268
+ * When provided, native parse is skipped and this tree is rendered instead.
269
+ */
270
+ sourceAst?: MarkdownNode;
271
+ /**
272
+ * Optional transform applied after parsing and before rendering.
273
+ * The transformed AST is also returned in `onParseComplete`.
274
+ */
275
+ astTransform?: AstTransform;
63
276
  /**
64
277
  * Callback fired when parsing begins.
65
278
  */
@@ -72,6 +285,13 @@ export type MarkdownProps = {
72
285
  ast: MarkdownNode;
73
286
  text: string;
74
287
  }) => void;
288
+ /**
289
+ * Called when a parse error or plugin error occurs.
290
+ * @param error - The thrown error.
291
+ * @param phase - Where the error occurred.
292
+ * @param pluginName - The plugin name, if applicable.
293
+ */
294
+ onError?: (error: Error, phase: 'parse' | 'before-plugin' | 'after-plugin', pluginName?: string) => void;
75
295
  /**
76
296
  * Custom renderers for specific markdown node types.
77
297
  * Each renderer receives { node, children, Renderer } plus type-specific props.
@@ -107,11 +327,43 @@ export type MarkdownProps = {
107
327
  * Return false to prevent the default openURL behavior.
108
328
  */
109
329
  onLinkPress?: LinkPressHandler;
330
+ /**
331
+ * Enables top-level block virtualization for very large markdown documents.
332
+ * Best used when Markdown is the primary scroll container on screen.
333
+ * - `true`: always virtualize when block threshold is met
334
+ * - `"auto"`: virtualize only when threshold is met (recommended for large docs)
335
+ * - `false`: disable virtualization (default)
336
+ */
337
+ virtualize?: boolean | "auto";
338
+ /**
339
+ * Minimum number of top-level blocks before virtualization is activated.
340
+ * Helps avoid FlatList overhead on small documents.
341
+ */
342
+ virtualizationMinBlocks?: number;
343
+ /**
344
+ * Optional FlatList tuning for virtualization.
345
+ */
346
+ virtualization?: MarkdownVirtualizationOptions;
347
+ /**
348
+ * Optional configuration for the table renderer.
349
+ */
350
+ tableOptions?: {
351
+ minColumnWidth?: number;
352
+ measurementStabilizeMs?: number;
353
+ };
354
+ /**
355
+ * Enable built-in syntax highlighting for code blocks.
356
+ * Pass `true` to use the built-in tokenizer, or a custom highlighter function.
357
+ */
358
+ highlightCode?: boolean | CodeHighlighter;
110
359
  };
111
360
 
112
361
  export const Markdown: FC<MarkdownProps> = ({
113
362
  children,
114
363
  options,
364
+ plugins,
365
+ sourceAst,
366
+ astTransform,
115
367
  renderers = {},
116
368
  theme: userTheme,
117
369
  styles: nodeStyles,
@@ -120,41 +372,82 @@ export const Markdown: FC<MarkdownProps> = ({
120
372
  onParsingInProgress,
121
373
  onParseComplete,
122
374
  onLinkPress,
375
+ onError,
376
+ virtualize = false,
377
+ virtualizationMinBlocks = 40,
378
+ virtualization,
379
+ tableOptions,
380
+ highlightCode,
123
381
  }) => {
382
+ const parserOptionGfm = options?.gfm;
383
+ const parserOptionMath = options?.math;
384
+
124
385
  const parseResult = useMemo(() => {
125
386
  try {
126
- let ast: MarkdownNode;
127
- if (options) {
128
- ast = parseMarkdownWithOptions(children, options);
129
- } else {
130
- ast = parseMarkdown(children);
387
+ const markdownToParse = applyBeforeParsePlugins(children, plugins, onError ? (e, phase, name) => onError(e, phase, name) : undefined);
388
+ const parserOptions = normalizeParserOptions({
389
+ gfm: parserOptionGfm,
390
+ math: parserOptionMath,
391
+ });
392
+ let parsedAst = sourceAst
393
+ ? cloneMarkdownNode(sourceAst)
394
+ : getCachedParsedAst(markdownToParse, parserOptions);
395
+ parsedAst = applyAfterParsePlugins(parsedAst, plugins, onError ? (e, phase, name) => onError(e, phase, name) : undefined);
396
+
397
+ let ast = parsedAst;
398
+ if (astTransform) {
399
+ try {
400
+ const nextAst = astTransform(parsedAst);
401
+ if (isMarkdownNode(nextAst)) {
402
+ ast = nextAst;
403
+ }
404
+ } catch (error) {
405
+ warnInDev(
406
+ "[react-native-nitro-markdown] astTransform threw; falling back to parsed AST.",
407
+ error,
408
+ );
409
+ ast = parsedAst;
410
+ }
131
411
  }
132
412
 
133
413
  return {
134
414
  ast,
135
- text: getFlattenedText(ast),
136
415
  };
137
- } catch {
416
+ } catch (parseError) {
417
+ onError?.(parseError instanceof Error ? parseError : new Error(String(parseError)), 'parse');
138
418
  return {
139
419
  ast: null,
140
- text: "",
141
420
  };
142
421
  }
143
- }, [children, options]);
422
+ }, [
423
+ children,
424
+ parserOptionGfm,
425
+ parserOptionMath,
426
+ plugins,
427
+ sourceAst,
428
+ astTransform,
429
+ onError,
430
+ ]);
144
431
 
145
432
  useEffect(() => {
146
433
  onParsingInProgress?.();
147
- }, [children, options, onParsingInProgress]);
434
+ }, [
435
+ children,
436
+ parserOptionGfm,
437
+ parserOptionMath,
438
+ plugins,
439
+ onParsingInProgress,
440
+ ]);
148
441
 
149
442
  useEffect(() => {
150
- if (!parseResult.ast) return;
443
+ if (!parseResult.ast || !onParseComplete) return;
151
444
 
152
- onParseComplete?.({
445
+ onParseComplete({
153
446
  raw: children,
154
447
  ast: parseResult.ast,
155
- text: parseResult.text,
448
+ text: getFlattenedText(parseResult.ast),
156
449
  });
157
- }, [children, onParseComplete, parseResult.ast, parseResult.text]);
450
+ }, [children, onParseComplete, parseResult.ast]);
158
451
 
159
452
  const theme = useMemo(() => {
160
453
  const base =
@@ -165,6 +458,43 @@ export const Markdown: FC<MarkdownProps> = ({
165
458
  }, [userTheme, stylingStrategy]);
166
459
 
167
460
  const baseStyles = getBaseStyles(theme);
461
+ const contextValue = useMemo(
462
+ () => ({
463
+ renderers,
464
+ theme,
465
+ styles: nodeStyles,
466
+ stylingStrategy,
467
+ onLinkPress,
468
+ tableOptions,
469
+ highlightCode,
470
+ }),
471
+ [renderers, theme, nodeStyles, stylingStrategy, onLinkPress, tableOptions, highlightCode],
472
+ );
473
+
474
+ const topLevelBlocks =
475
+ parseResult.ast?.type === "document"
476
+ ? (parseResult.ast.children ?? [])
477
+ : parseResult.ast
478
+ ? [parseResult.ast]
479
+ : [];
480
+ const shouldVirtualizeBySetting =
481
+ virtualize === true ||
482
+ (virtualize === "auto" && topLevelBlocks.length >= virtualizationMinBlocks);
483
+ const shouldVirtualize =
484
+ parseResult.ast !== null && shouldVirtualizeBySetting;
485
+
486
+ const keyExtractor = useCallback((node: MarkdownNode, index: number) => {
487
+ const beg = typeof node.beg === "number" ? node.beg : index;
488
+ const end = typeof node.end === "number" ? node.end : index;
489
+ return `${node.type}:${beg}:${end}:${index}`;
490
+ }, []);
491
+
492
+ const renderVirtualizedItem = useCallback(
493
+ ({ item }: ListRenderItemInfo<MarkdownNode>): ReactElement => (
494
+ <NodeRenderer node={item} depth={0} inListItem={false} />
495
+ ),
496
+ [],
497
+ );
168
498
 
169
499
  if (!parseResult.ast) {
170
500
  return (
@@ -175,17 +505,28 @@ export const Markdown: FC<MarkdownProps> = ({
175
505
  }
176
506
 
177
507
  return (
178
- <MarkdownContext.Provider
179
- value={{
180
- renderers,
181
- theme,
182
- styles: nodeStyles,
183
- stylingStrategy,
184
- onLinkPress,
185
- }}
186
- >
508
+ <MarkdownContext.Provider value={contextValue}>
187
509
  <View style={[baseStyles.container, style]}>
188
- <NodeRenderer node={parseResult.ast} depth={0} inListItem={false} />
510
+ {shouldVirtualize ? (
511
+ <FlatList
512
+ data={topLevelBlocks}
513
+ renderItem={renderVirtualizedItem}
514
+ keyExtractor={keyExtractor}
515
+ style={baseStyles.virtualizedList}
516
+ initialNumToRender={virtualization?.initialNumToRender ?? 12}
517
+ maxToRenderPerBatch={virtualization?.maxToRenderPerBatch ?? 12}
518
+ windowSize={virtualization?.windowSize ?? 10}
519
+ updateCellsBatchingPeriod={
520
+ virtualization?.updateCellsBatchingPeriod ?? 16
521
+ }
522
+ removeClippedSubviews={
523
+ virtualization?.removeClippedSubviews ?? true
524
+ }
525
+ showsVerticalScrollIndicator={false}
526
+ />
527
+ ) : (
528
+ <NodeRenderer node={parseResult.ast} depth={0} inListItem={false} />
529
+ )}
189
530
  </View>
190
531
  </MarkdownContext.Provider>
191
532
  );
@@ -206,7 +547,7 @@ const isInline = (type: MarkdownNode["type"]): boolean => {
206
547
  );
207
548
  };
208
549
 
209
- const NodeRenderer: FC<NodeRendererProps> = ({
550
+ const NodeRendererComponent: FC<NodeRendererProps> = ({
210
551
  node,
211
552
  depth,
212
553
  inListItem,
@@ -341,7 +682,9 @@ const NodeRenderer: FC<NodeRendererProps> = ({
341
682
  }
342
683
  }
343
684
 
344
- const nodeStyleOverride = nodeStyles?.[node.type];
685
+ const nodeStyleOverride = nodeStyles?.[node.type] as
686
+ | (ViewStyle & TextStyle)
687
+ | undefined;
345
688
 
346
689
  switch (node.type) {
347
690
  case "document":
@@ -521,6 +864,15 @@ const NodeRenderer: FC<NodeRendererProps> = ({
521
864
  }
522
865
  };
523
866
 
867
+ const NodeRenderer = memo(NodeRendererComponent, (previousProps, nextProps) => {
868
+ return (
869
+ previousProps.node === nextProps.node &&
870
+ previousProps.depth === nextProps.depth &&
871
+ previousProps.inListItem === nextProps.inListItem &&
872
+ previousProps.parentIsText === nextProps.parentIsText
873
+ );
874
+ }) as FC<NodeRendererProps>;
875
+
524
876
  type BaseStyles = ReturnType<typeof createBaseStyles>;
525
877
 
526
878
  const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
@@ -537,6 +889,9 @@ const createBaseStyles = (theme: MarkdownTheme) =>
537
889
  container: {
538
890
  flex: 1,
539
891
  },
892
+ virtualizedList: {
893
+ flex: 1,
894
+ },
540
895
  document: {
541
896
  flex: 1,
542
897
  },
@@ -11,6 +11,7 @@ import {
11
11
  import { getTextContent } from "../headless";
12
12
  import { useMarkdownContext } from "../MarkdownContext";
13
13
  import type { MarkdownNode } from "../headless";
14
+ import { defaultHighlighter, type HighlightedToken } from "../utils/code-highlight";
14
15
 
15
16
  type CodeBlockProps = {
16
17
  language?: string;
@@ -25,7 +26,14 @@ export const CodeBlock: FC<CodeBlockProps> = ({
25
26
  node,
26
27
  style,
27
28
  }) => {
28
- const { theme } = useMarkdownContext();
29
+ const ctx = useMarkdownContext();
30
+ const { theme } = ctx;
31
+
32
+ const highlighter = ctx.highlightCode === true
33
+ ? defaultHighlighter
34
+ : typeof ctx.highlightCode === 'function'
35
+ ? ctx.highlightCode
36
+ : null;
29
37
 
30
38
  const displayContent = content ?? (node ? getTextContent(node) : "");
31
39
 
@@ -70,8 +78,29 @@ export const CodeBlock: FC<CodeBlockProps> = ({
70
78
  {showLanguage ? (
71
79
  <Text style={styles.codeLanguage}>{language}</Text>
72
80
  ) : null}
73
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
74
- <Text style={styles.codeBlockText}>{displayContent}</Text>
81
+ <ScrollView
82
+ horizontal
83
+ showsHorizontalScrollIndicator={false}
84
+ bounces={false}
85
+ >
86
+ {highlighter && language ? (
87
+ <Text style={styles.codeBlockText} selectable>
88
+ {highlighter(language, displayContent).map((token: HighlightedToken, i: number) => {
89
+ const tokenColor = ctx.theme.colors.codeTokenColors?.[token.type];
90
+ return tokenColor ? (
91
+ <Text key={i} style={{ color: tokenColor }}>
92
+ {token.text}
93
+ </Text>
94
+ ) : (
95
+ <Text key={i}>{token.text}</Text>
96
+ );
97
+ })}
98
+ </Text>
99
+ ) : (
100
+ <Text style={styles.codeBlockText} selectable>
101
+ {displayContent}
102
+ </Text>
103
+ )}
75
104
  </ScrollView>
76
105
  </View>
77
106
  );
@@ -0,0 +1,38 @@
1
+ import { type FC, type ComponentType } from "react";
2
+ import { View, Text, type StyleProp, type TextStyle } from "react-native";
3
+ import type { MarkdownNode } from "../../headless";
4
+ import type { NodeRendererProps } from "../../MarkdownContext";
5
+
6
+ type CellContentProps = {
7
+ node: MarkdownNode;
8
+ Renderer: ComponentType<NodeRendererProps>;
9
+ styles: {
10
+ cellContentWrapper: object;
11
+ };
12
+ textStyle?: StyleProp<TextStyle>;
13
+ };
14
+
15
+ export const CellContent: FC<CellContentProps> = ({
16
+ node,
17
+ Renderer,
18
+ styles,
19
+ textStyle,
20
+ }) => {
21
+ if (!node.children || node.children.length === 0) {
22
+ return <Text style={textStyle}>{node.content ?? ""}</Text>;
23
+ }
24
+
25
+ return (
26
+ <View style={styles.cellContentWrapper}>
27
+ {node.children.map((child, idx) => (
28
+ <Renderer
29
+ key={idx}
30
+ node={child}
31
+ depth={0}
32
+ inListItem={false}
33
+ parentIsText={false}
34
+ />
35
+ ))}
36
+ </View>
37
+ );
38
+ };