react-native-nitro-markdown 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +68 -5
  2. package/android/src/main/cpp/cpp-adapter.cpp +4 -1
  3. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +19 -0
  4. package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +4 -3
  5. package/ios/HybridMarkdownSession.swift +18 -0
  6. package/lib/commonjs/MarkdownContext.js.map +1 -1
  7. package/lib/commonjs/headless.js +20 -0
  8. package/lib/commonjs/headless.js.map +1 -1
  9. package/lib/commonjs/index.js +9 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/markdown.js +21 -11
  12. package/lib/commonjs/markdown.js.map +1 -1
  13. package/lib/commonjs/renderers/code.js +20 -2
  14. package/lib/commonjs/renderers/code.js.map +1 -1
  15. package/lib/commonjs/renderers/table/cell-content.js +32 -0
  16. package/lib/commonjs/renderers/table/cell-content.js.map +1 -0
  17. package/lib/commonjs/renderers/table/index.js +310 -0
  18. package/lib/commonjs/renderers/table/index.js.map +1 -0
  19. package/lib/commonjs/renderers/table/table-reducer.js +29 -0
  20. package/lib/commonjs/renderers/table/table-reducer.js.map +1 -0
  21. package/lib/commonjs/renderers/table/table-utils.js +68 -0
  22. package/lib/commonjs/renderers/table/table-utils.js.map +1 -0
  23. package/lib/commonjs/renderers/table/types.js +6 -0
  24. package/lib/commonjs/renderers/table/types.js.map +1 -0
  25. package/lib/commonjs/renderers/table.js +6 -398
  26. package/lib/commonjs/renderers/table.js.map +1 -1
  27. package/lib/commonjs/theme.js +10 -1
  28. package/lib/commonjs/theme.js.map +1 -1
  29. package/lib/commonjs/use-markdown-stream.js +9 -1
  30. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  31. package/lib/commonjs/utils/code-highlight.js +101 -0
  32. package/lib/commonjs/utils/code-highlight.js.map +1 -0
  33. package/lib/module/MarkdownContext.js.map +1 -1
  34. package/lib/module/headless.js +19 -0
  35. package/lib/module/headless.js.map +1 -1
  36. package/lib/module/index.js +1 -0
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/module/markdown.js +21 -11
  39. package/lib/module/markdown.js.map +1 -1
  40. package/lib/module/renderers/code.js +20 -2
  41. package/lib/module/renderers/code.js.map +1 -1
  42. package/lib/module/renderers/table/cell-content.js +27 -0
  43. package/lib/module/renderers/table/cell-content.js.map +1 -0
  44. package/lib/module/renderers/table/index.js +305 -0
  45. package/lib/module/renderers/table/index.js.map +1 -0
  46. package/lib/module/renderers/table/table-reducer.js +24 -0
  47. package/lib/module/renderers/table/table-reducer.js.map +1 -0
  48. package/lib/module/renderers/table/table-utils.js +62 -0
  49. package/lib/module/renderers/table/table-utils.js.map +1 -0
  50. package/lib/module/renderers/table/types.js +4 -0
  51. package/lib/module/renderers/table/types.js.map +1 -0
  52. package/lib/module/renderers/table.js +1 -397
  53. package/lib/module/renderers/table.js.map +1 -1
  54. package/lib/module/theme.js +10 -1
  55. package/lib/module/theme.js.map +1 -1
  56. package/lib/module/use-markdown-stream.js +9 -1
  57. package/lib/module/use-markdown-stream.js.map +1 -1
  58. package/lib/module/utils/code-highlight.js +97 -0
  59. package/lib/module/utils/code-highlight.js.map +1 -0
  60. package/lib/typescript/commonjs/MarkdownContext.d.ts +6 -0
  61. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/headless.d.ts +5 -0
  63. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/index.d.ts +2 -0
  65. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/markdown.d.ts +24 -0
  67. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +15 -0
  70. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/renderers/table/index.d.ts +11 -0
  72. package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/renderers/table/table-reducer.d.ts +5 -0
  74. package/lib/typescript/commonjs/renderers/table/table-reducer.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/renderers/table/table-utils.d.ts +10 -0
  76. package/lib/typescript/commonjs/renderers/table/table-utils.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/renderers/table/types.d.ts +24 -0
  78. package/lib/typescript/commonjs/renderers/table/types.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/renderers/table.d.ts +1 -11
  80. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +9 -0
  82. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/theme.d.ts +18 -2
  84. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/use-markdown-stream.d.ts +4 -0
  86. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/utils/code-highlight.d.ts +8 -0
  88. package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -0
  89. package/lib/typescript/module/MarkdownContext.d.ts +6 -0
  90. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  91. package/lib/typescript/module/headless.d.ts +5 -0
  92. package/lib/typescript/module/headless.d.ts.map +1 -1
  93. package/lib/typescript/module/index.d.ts +2 -0
  94. package/lib/typescript/module/index.d.ts.map +1 -1
  95. package/lib/typescript/module/markdown.d.ts +24 -0
  96. package/lib/typescript/module/markdown.d.ts.map +1 -1
  97. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  98. package/lib/typescript/module/renderers/table/cell-content.d.ts +15 -0
  99. package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -0
  100. package/lib/typescript/module/renderers/table/index.d.ts +11 -0
  101. package/lib/typescript/module/renderers/table/index.d.ts.map +1 -0
  102. package/lib/typescript/module/renderers/table/table-reducer.d.ts +5 -0
  103. package/lib/typescript/module/renderers/table/table-reducer.d.ts.map +1 -0
  104. package/lib/typescript/module/renderers/table/table-utils.d.ts +10 -0
  105. package/lib/typescript/module/renderers/table/table-utils.d.ts.map +1 -0
  106. package/lib/typescript/module/renderers/table/types.d.ts +24 -0
  107. package/lib/typescript/module/renderers/table/types.d.ts.map +1 -0
  108. package/lib/typescript/module/renderers/table.d.ts +1 -11
  109. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  110. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +9 -0
  111. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
  112. package/lib/typescript/module/theme.d.ts +18 -2
  113. package/lib/typescript/module/theme.d.ts.map +1 -1
  114. package/lib/typescript/module/use-markdown-stream.d.ts +4 -0
  115. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  116. package/lib/typescript/module/utils/code-highlight.d.ts +8 -0
  117. package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -0
  118. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +37 -27
  119. package/nitrogen/generated/android/NitroMarkdownOnLoad.hpp +13 -4
  120. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +35 -32
  121. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +21 -22
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +23 -18
  123. package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +14 -0
  124. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +2 -0
  125. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +23 -0
  126. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +2 -0
  127. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +2 -0
  128. package/package.json +6 -3
  129. package/react-native-nitro-markdown.podspec +1 -1
  130. package/src/MarkdownContext.ts +6 -0
  131. package/src/headless.ts +12 -0
  132. package/src/index.ts +3 -0
  133. package/src/markdown.tsx +47 -7
  134. package/src/renderers/code.tsx +27 -2
  135. package/src/renderers/table/cell-content.tsx +38 -0
  136. package/src/renderers/table/index.tsx +419 -0
  137. package/src/renderers/table/table-reducer.ts +36 -0
  138. package/src/renderers/table/table-utils.ts +81 -0
  139. package/src/renderers/table/types.ts +24 -0
  140. package/src/renderers/table.tsx +1 -547
  141. package/src/specs/MarkdownSession.nitro.ts +10 -0
  142. package/src/theme.ts +29 -1
  143. package/src/use-markdown-stream.ts +10 -0
  144. package/src/utils/code-highlight.ts +102 -0
package/src/headless.ts CHANGED
@@ -207,3 +207,15 @@ export const getFlattenedText = (node: MarkdownNode): string => {
207
207
  return childrenText;
208
208
  }
209
209
  };
210
+
211
+ /**
212
+ * Recursively removes `beg`/`end` source offset fields from an AST.
213
+ * Useful to reduce memory in environments that don't need source mapping.
214
+ */
215
+ export function stripSourceOffsets(node: MarkdownNode): MarkdownNode {
216
+ const { beg: _beg, end: _end, children, ...rest } = node;
217
+ return {
218
+ ...rest,
219
+ ...(children ? { children: children.map(stripSourceOffsets) } : {}),
220
+ };
221
+ }
package/src/index.ts CHANGED
@@ -55,3 +55,6 @@ export { MathInline, MathBlock } from "./renderers/math";
55
55
  export { createMarkdownSession } from "./MarkdownSession";
56
56
  export type { MarkdownSession } from "./MarkdownSession";
57
57
  export { useMarkdownSession, useStream } from "./use-markdown-stream";
58
+
59
+ export type { HighlightedToken, TokenType, CodeHighlighter } from "./utils/code-highlight";
60
+ export { defaultHighlighter } from "./utils/code-highlight";
package/src/markdown.tsx CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  type ListRenderItemInfo,
18
18
  type FlatListProps,
19
19
  type StyleProp,
20
+ type TextStyle,
20
21
  type ViewStyle,
21
22
  } from "react-native";
22
23
  import {
@@ -53,6 +54,7 @@ import {
53
54
  type NodeStyleOverrides,
54
55
  type StylingStrategy,
55
56
  } from "./theme";
57
+ import type { CodeHighlighter } from "./utils/code-highlight";
56
58
 
57
59
  const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
58
60
  const parseAstCache = new Map<string, MarkdownNode>();
@@ -78,6 +80,10 @@ export type MarkdownPlugin = {
78
80
  * Optional plugin version metadata for diagnostics.
79
81
  */
80
82
  version?: string | number;
83
+ /**
84
+ * Execution priority. Higher values run first (default: 0).
85
+ */
86
+ priority?: number;
81
87
  /**
82
88
  * Optional text preprocessor executed before native parsing.
83
89
  * Should return a full markdown string.
@@ -183,13 +189,15 @@ const getCachedParsedAst = (
183
189
  const applyBeforeParsePlugins = (
184
190
  markdown: string,
185
191
  plugins?: MarkdownPlugin[],
192
+ onError?: (error: Error, phase: 'before-plugin', pluginName?: string) => void,
186
193
  ): string => {
187
194
  if (!plugins || plugins.length === 0) {
188
195
  return markdown;
189
196
  }
190
197
 
198
+ const sorted = [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
191
199
  let nextMarkdown = markdown;
192
- for (const plugin of plugins) {
200
+ for (const plugin of sorted) {
193
201
  if (!plugin.beforeParse) continue;
194
202
 
195
203
  try {
@@ -203,6 +211,7 @@ const applyBeforeParsePlugins = (
203
211
  `[react-native-nitro-markdown] plugin beforeParse${pluginLabel} threw; using previous markdown.`,
204
212
  error,
205
213
  );
214
+ onError?.(error instanceof Error ? error : new Error(String(error)), 'before-plugin', plugin.name);
206
215
  }
207
216
  }
208
217
 
@@ -212,13 +221,15 @@ const applyBeforeParsePlugins = (
212
221
  const applyAfterParsePlugins = (
213
222
  ast: MarkdownNode,
214
223
  plugins?: MarkdownPlugin[],
224
+ onError?: (error: Error, phase: 'after-plugin', pluginName?: string) => void,
215
225
  ): MarkdownNode => {
216
226
  if (!plugins || plugins.length === 0) {
217
227
  return ast;
218
228
  }
219
229
 
230
+ const sorted = [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
220
231
  let nextAst = ast;
221
- for (const plugin of plugins) {
232
+ for (const plugin of sorted) {
222
233
  if (!plugin.afterParse) continue;
223
234
 
224
235
  try {
@@ -232,6 +243,7 @@ const applyAfterParsePlugins = (
232
243
  `[react-native-nitro-markdown] plugin afterParse${pluginLabel} threw; using previous AST.`,
233
244
  error,
234
245
  );
246
+ onError?.(error instanceof Error ? error : new Error(String(error)), 'after-plugin', plugin.name);
235
247
  }
236
248
  }
237
249
 
@@ -273,6 +285,13 @@ export type MarkdownProps = {
273
285
  ast: MarkdownNode;
274
286
  text: string;
275
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;
276
295
  /**
277
296
  * Custom renderers for specific markdown node types.
278
297
  * Each renderer receives { node, children, Renderer } plus type-specific props.
@@ -325,6 +344,18 @@ export type MarkdownProps = {
325
344
  * Optional FlatList tuning for virtualization.
326
345
  */
327
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;
328
359
  };
329
360
 
330
361
  export const Markdown: FC<MarkdownProps> = ({
@@ -341,16 +372,19 @@ export const Markdown: FC<MarkdownProps> = ({
341
372
  onParsingInProgress,
342
373
  onParseComplete,
343
374
  onLinkPress,
375
+ onError,
344
376
  virtualize = false,
345
377
  virtualizationMinBlocks = 40,
346
378
  virtualization,
379
+ tableOptions,
380
+ highlightCode,
347
381
  }) => {
348
382
  const parserOptionGfm = options?.gfm;
349
383
  const parserOptionMath = options?.math;
350
384
 
351
385
  const parseResult = useMemo(() => {
352
386
  try {
353
- const markdownToParse = applyBeforeParsePlugins(children, plugins);
387
+ const markdownToParse = applyBeforeParsePlugins(children, plugins, onError ? (e, phase, name) => onError(e, phase, name) : undefined);
354
388
  const parserOptions = normalizeParserOptions({
355
389
  gfm: parserOptionGfm,
356
390
  math: parserOptionMath,
@@ -358,7 +392,7 @@ export const Markdown: FC<MarkdownProps> = ({
358
392
  let parsedAst = sourceAst
359
393
  ? cloneMarkdownNode(sourceAst)
360
394
  : getCachedParsedAst(markdownToParse, parserOptions);
361
- parsedAst = applyAfterParsePlugins(parsedAst, plugins);
395
+ parsedAst = applyAfterParsePlugins(parsedAst, plugins, onError ? (e, phase, name) => onError(e, phase, name) : undefined);
362
396
 
363
397
  let ast = parsedAst;
364
398
  if (astTransform) {
@@ -379,7 +413,8 @@ export const Markdown: FC<MarkdownProps> = ({
379
413
  return {
380
414
  ast,
381
415
  };
382
- } catch {
416
+ } catch (parseError) {
417
+ onError?.(parseError instanceof Error ? parseError : new Error(String(parseError)), 'parse');
383
418
  return {
384
419
  ast: null,
385
420
  };
@@ -391,6 +426,7 @@ export const Markdown: FC<MarkdownProps> = ({
391
426
  plugins,
392
427
  sourceAst,
393
428
  astTransform,
429
+ onError,
394
430
  ]);
395
431
 
396
432
  useEffect(() => {
@@ -429,8 +465,10 @@ export const Markdown: FC<MarkdownProps> = ({
429
465
  styles: nodeStyles,
430
466
  stylingStrategy,
431
467
  onLinkPress,
468
+ tableOptions,
469
+ highlightCode,
432
470
  }),
433
- [renderers, theme, nodeStyles, stylingStrategy, onLinkPress],
471
+ [renderers, theme, nodeStyles, stylingStrategy, onLinkPress, tableOptions, highlightCode],
434
472
  );
435
473
 
436
474
  const topLevelBlocks =
@@ -644,7 +682,9 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
644
682
  }
645
683
  }
646
684
 
647
- const nodeStyleOverride = nodeStyles?.[node.type];
685
+ const nodeStyleOverride = nodeStyles?.[node.type] as
686
+ | (ViewStyle & TextStyle)
687
+ | undefined;
648
688
 
649
689
  switch (node.type) {
650
690
  case "document":
@@ -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
 
@@ -75,7 +83,24 @@ export const CodeBlock: FC<CodeBlockProps> = ({
75
83
  showsHorizontalScrollIndicator={false}
76
84
  bounces={false}
77
85
  >
78
- <Text style={styles.codeBlockText}>{displayContent}</Text>
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
+ )}
79
104
  </ScrollView>
80
105
  </View>
81
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
+ };
@@ -0,0 +1,419 @@
1
+ import {
2
+ useEffect,
3
+ useMemo,
4
+ useRef,
5
+ useReducer,
6
+ useCallback,
7
+ type FC,
8
+ type ComponentType,
9
+ } from "react";
10
+ import {
11
+ View,
12
+ StyleSheet,
13
+ ScrollView,
14
+ Platform,
15
+ type ViewStyle,
16
+ type LayoutChangeEvent,
17
+ } from "react-native";
18
+ import { useMarkdownContext, type NodeRendererProps } from "../../MarkdownContext";
19
+ import type { MarkdownTheme } from "../../theme";
20
+ import { CellContent } from "./cell-content";
21
+ import { extractTableData, estimateColumnWidths } from "./table-utils";
22
+ import {
23
+ columnWidthsReducer,
24
+ DEFAULT_MIN_COLUMN_WIDTH,
25
+ DEFAULT_MEASUREMENT_STABILIZE_MS,
26
+ } from "./table-reducer";
27
+
28
+ type TableRendererProps = {
29
+ node: import("../../headless").MarkdownNode;
30
+ Renderer: ComponentType<NodeRendererProps>;
31
+ style?: ViewStyle;
32
+ };
33
+
34
+ const COLUMN_MEASUREMENT_PADDING = 8;
35
+
36
+ const IS_ACT_TEST_ENVIRONMENT =
37
+ Reflect.get(globalThis, "IS_REACT_ACT_ENVIRONMENT") === true;
38
+ const SHOULD_DEBOUNCE_MEASUREMENT = !IS_ACT_TEST_ENVIRONMENT;
39
+
40
+ export const TableRenderer: FC<TableRendererProps> = ({
41
+ node,
42
+ Renderer,
43
+ style,
44
+ }) => {
45
+ const { theme, tableOptions } = useMarkdownContext();
46
+
47
+ const minColumnWidth =
48
+ tableOptions?.minColumnWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
49
+ const measurementStabilizeMs =
50
+ tableOptions?.measurementStabilizeMs ?? DEFAULT_MEASUREMENT_STABILIZE_MS;
51
+
52
+ const { headers, rows, alignments } = useMemo(
53
+ () => extractTableData(node),
54
+ [node],
55
+ );
56
+
57
+ const columnCount = headers.length;
58
+ const styles = useMemo(() => createTableStyles(theme), [theme]);
59
+ const estimatedColumnWidths = useMemo(
60
+ () => estimateColumnWidths(headers, rows, columnCount, minColumnWidth),
61
+ [headers, rows, columnCount, minColumnWidth],
62
+ );
63
+
64
+ const [columnWidths, dispatch] = useReducer(
65
+ columnWidthsReducer,
66
+ estimatedColumnWidths,
67
+ );
68
+ const measuredWidths = useRef<Map<string, number>>(new Map());
69
+ const measuredCells = useRef<Set<string>>(new Set());
70
+ const widthsCalculated = useRef(false);
71
+ const columnWidthsRef = useRef(columnWidths);
72
+ const lastCellKeySignatureRef = useRef("");
73
+ const measurementTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
74
+ null,
75
+ );
76
+ const [needsMeasurement, setNeedsMeasurement] = useReducer(
77
+ (_previous: boolean, nextValue: boolean) => nextValue,
78
+ false,
79
+ );
80
+
81
+ const expectedCellKeys = useMemo(() => {
82
+ const keys: string[] = [];
83
+
84
+ headers.forEach((_, colIndex) => {
85
+ keys.push(`header-${colIndex}`);
86
+ });
87
+
88
+ rows.forEach((row, rowIndex) => {
89
+ row.forEach((_, colIndex) => {
90
+ keys.push(`cell-${rowIndex}-${colIndex}`);
91
+ });
92
+ });
93
+
94
+ return keys;
95
+ }, [headers, rows]);
96
+
97
+ const expectedCellKeySignature = useMemo(
98
+ () => expectedCellKeys.join("|"),
99
+ [expectedCellKeys],
100
+ );
101
+
102
+ useEffect(() => {
103
+ columnWidthsRef.current = columnWidths;
104
+ }, [columnWidths]);
105
+
106
+ useEffect(() => {
107
+ const structureChanged =
108
+ lastCellKeySignatureRef.current !== expectedCellKeySignature;
109
+ lastCellKeySignatureRef.current = expectedCellKeySignature;
110
+
111
+ if (measurementTimerRef.current) {
112
+ clearTimeout(measurementTimerRef.current);
113
+ measurementTimerRef.current = null;
114
+ }
115
+
116
+ if (structureChanged) {
117
+ measuredWidths.current.clear();
118
+ measuredCells.current.clear();
119
+ widthsCalculated.current = false;
120
+ setNeedsMeasurement(false);
121
+ dispatch({ type: "RESET_WIDTHS", widths: estimatedColumnWidths });
122
+ } else {
123
+ dispatch({
124
+ type: "SET_MONOTONIC_WIDTHS",
125
+ widths: estimatedColumnWidths,
126
+ });
127
+ if (widthsCalculated.current) {
128
+ return;
129
+ }
130
+ }
131
+
132
+ if (!SHOULD_DEBOUNCE_MEASUREMENT) {
133
+ setNeedsMeasurement(true);
134
+ return;
135
+ }
136
+
137
+ measurementTimerRef.current = setTimeout(() => {
138
+ measurementTimerRef.current = null;
139
+ setNeedsMeasurement(true);
140
+ }, measurementStabilizeMs);
141
+
142
+ return () => {
143
+ if (measurementTimerRef.current) {
144
+ clearTimeout(measurementTimerRef.current);
145
+ measurementTimerRef.current = null;
146
+ }
147
+ };
148
+ }, [estimatedColumnWidths, expectedCellKeySignature, measurementStabilizeMs]);
149
+
150
+ const onCellLayout = useCallback(
151
+ (cellKey: string, width: number) => {
152
+ if (width <= 0 || widthsCalculated.current || !needsMeasurement) return;
153
+
154
+ measuredWidths.current.set(cellKey, width);
155
+ if (!measuredCells.current.has(cellKey)) {
156
+ measuredCells.current.add(cellKey);
157
+ }
158
+
159
+ if (measuredCells.current.size < expectedCellKeys.length) return;
160
+
161
+ const allCellsMeasured = expectedCellKeys.every((key) =>
162
+ measuredCells.current.has(key),
163
+ );
164
+ if (!allCellsMeasured) return;
165
+
166
+ const maxWidths: number[] = [...columnWidthsRef.current];
167
+
168
+ for (let col = 0; col < columnCount; col++) {
169
+ const headerWidth = measuredWidths.current.get(`header-${col}`);
170
+ if (headerWidth && headerWidth > 0) {
171
+ maxWidths[col] = Math.max(maxWidths[col], headerWidth);
172
+ }
173
+
174
+ for (let row = 0; row < rows.length; row++) {
175
+ if (col >= rows[row].length) continue;
176
+ const cellWidth = measuredWidths.current.get(`cell-${row}-${col}`);
177
+ if (cellWidth && cellWidth > 0) {
178
+ maxWidths[col] = Math.max(maxWidths[col], cellWidth);
179
+ }
180
+ }
181
+
182
+ maxWidths[col] = Math.max(
183
+ maxWidths[col] + COLUMN_MEASUREMENT_PADDING,
184
+ minColumnWidth,
185
+ );
186
+ }
187
+
188
+ widthsCalculated.current = true;
189
+ setNeedsMeasurement(false);
190
+ dispatch({ type: "RESET_WIDTHS", widths: maxWidths });
191
+ },
192
+ [columnCount, expectedCellKeys, needsMeasurement, rows, minColumnWidth],
193
+ );
194
+
195
+ const getAlignment = (
196
+ index: number,
197
+ ): "flex-start" | "center" | "flex-end" => {
198
+ const align = alignments[index];
199
+ if (align === "center") return "center";
200
+ if (align === "right") return "flex-end";
201
+ return "flex-start";
202
+ };
203
+
204
+ if (columnCount === 0) return null;
205
+
206
+ const hasWidths = columnWidths.length === columnCount;
207
+ const resolvedWidths = hasWidths ? columnWidths : estimatedColumnWidths;
208
+
209
+ return (
210
+ <View style={[styles.container, style]}>
211
+ {needsMeasurement ? (
212
+ <View style={styles.measurementContainer}>
213
+ <View style={styles.measurementRow}>
214
+ {headers.map((cell, colIndex) => (
215
+ <View
216
+ key={`measure-header-${colIndex}`}
217
+ style={styles.measurementCell}
218
+ onLayout={(e: LayoutChangeEvent) => {
219
+ onCellLayout(
220
+ `header-${colIndex}`,
221
+ e.nativeEvent.layout.width,
222
+ );
223
+ }}
224
+ >
225
+ <CellContent node={cell} Renderer={Renderer} styles={styles} />
226
+ </View>
227
+ ))}
228
+ </View>
229
+ {rows.map((row, rowIndex) => (
230
+ <View key={`measure-row-${rowIndex}`} style={styles.measurementRow}>
231
+ {row.map((cell, colIndex) => (
232
+ <View
233
+ key={`measure-cell-${rowIndex}-${colIndex}`}
234
+ style={styles.measurementCell}
235
+ onLayout={(e: LayoutChangeEvent) => {
236
+ onCellLayout(
237
+ `cell-${rowIndex}-${colIndex}`,
238
+ e.nativeEvent.layout.width,
239
+ );
240
+ }}
241
+ >
242
+ <CellContent
243
+ node={cell}
244
+ Renderer={Renderer}
245
+ styles={styles}
246
+ />
247
+ </View>
248
+ ))}
249
+ </View>
250
+ ))}
251
+ </View>
252
+ ) : null}
253
+
254
+ <ScrollView
255
+ horizontal
256
+ showsHorizontalScrollIndicator
257
+ style={styles.tableScroll}
258
+ bounces={false}
259
+ >
260
+ <View
261
+ style={[
262
+ styles.table,
263
+ {
264
+ backgroundColor:
265
+ style?.backgroundColor ?? theme.colors.surface ?? "#111827",
266
+ },
267
+ ]}
268
+ >
269
+ <View style={styles.headerRow}>
270
+ {headers.map((cell, colIndex) => (
271
+ <View
272
+ key={`header-${colIndex}`}
273
+ style={[
274
+ styles.headerCell,
275
+ {
276
+ width: resolvedWidths[colIndex] ?? minColumnWidth,
277
+ alignItems: getAlignment(colIndex),
278
+ },
279
+ colIndex === columnCount - 1 && styles.lastCell,
280
+ ]}
281
+ >
282
+ <CellContent
283
+ node={cell}
284
+ Renderer={Renderer}
285
+ styles={styles}
286
+ textStyle={styles.headerText}
287
+ />
288
+ </View>
289
+ ))}
290
+ </View>
291
+
292
+ {rows.map((row, rowIndex) => (
293
+ <View
294
+ key={`row-${rowIndex}`}
295
+ style={[
296
+ styles.bodyRow,
297
+ rowIndex === rows.length - 1 && styles.lastRow,
298
+ rowIndex % 2 === 0 ? styles.evenRow : styles.oddRow,
299
+ ]}
300
+ >
301
+ {row.map((cell, colIndex) => (
302
+ <View
303
+ key={`cell-${rowIndex}-${colIndex}`}
304
+ style={[
305
+ styles.bodyCell,
306
+ {
307
+ width: resolvedWidths[colIndex] ?? minColumnWidth,
308
+ alignItems: getAlignment(colIndex),
309
+ },
310
+ colIndex === columnCount - 1 && styles.lastCell,
311
+ ]}
312
+ >
313
+ <CellContent
314
+ node={cell}
315
+ Renderer={Renderer}
316
+ styles={styles}
317
+ textStyle={styles.cellText}
318
+ />
319
+ </View>
320
+ ))}
321
+ </View>
322
+ ))}
323
+ </View>
324
+ </ScrollView>
325
+ </View>
326
+ );
327
+ };
328
+
329
+ const createTableStyles = (theme: MarkdownTheme) => {
330
+ const colors = theme?.colors || {};
331
+ const borderRadius = theme?.borderRadius || { m: 8 };
332
+
333
+ return StyleSheet.create({
334
+ container: {
335
+ marginVertical: theme.spacing.s,
336
+ },
337
+ measurementContainer: {
338
+ position: "absolute",
339
+ opacity: 0,
340
+ pointerEvents: "none",
341
+ left: -9999,
342
+ },
343
+ measurementRow: {
344
+ flexDirection: "row",
345
+ },
346
+ measurementCell: {
347
+ paddingVertical: 10,
348
+ paddingHorizontal: 12,
349
+ },
350
+ tableScroll: {
351
+ flexGrow: 0,
352
+ },
353
+ table: {
354
+ borderRadius: borderRadius.m,
355
+ overflow: "hidden",
356
+ borderWidth: 1,
357
+ borderColor: colors.tableBorder || "#374151",
358
+ },
359
+ headerRow: {
360
+ flexDirection: "row",
361
+ backgroundColor: colors.tableHeader || "#1f2937",
362
+ borderBottomWidth: 1,
363
+ borderBottomColor: colors.tableBorder || "#374151",
364
+ },
365
+ bodyRow: {
366
+ flexDirection: "row",
367
+ borderBottomWidth: 1,
368
+ borderBottomColor: colors.tableBorder || "#374151",
369
+ },
370
+ evenRow: {
371
+ backgroundColor: colors.tableRowEven || "transparent",
372
+ },
373
+ oddRow: {
374
+ backgroundColor: colors.tableRowOdd || "rgba(255,255,255,0.02)",
375
+ },
376
+ lastRow: {
377
+ borderBottomWidth: 0,
378
+ },
379
+ headerCell: {
380
+ flexShrink: 0,
381
+ paddingVertical: 10,
382
+ paddingHorizontal: 12,
383
+ minWidth: 60,
384
+ borderRightWidth: 1,
385
+ borderRightColor: colors.tableBorder || "#374151",
386
+ },
387
+ bodyCell: {
388
+ flexShrink: 0,
389
+ paddingVertical: 10,
390
+ paddingHorizontal: 12,
391
+ minWidth: 60,
392
+ borderRightWidth: 1,
393
+ borderRightColor: colors.tableBorder || "#374151",
394
+ justifyContent: "center",
395
+ },
396
+ lastCell: {
397
+ borderRightWidth: 0,
398
+ },
399
+ headerText: {
400
+ color: colors.tableHeaderText || "#9ca3af",
401
+ fontSize: 12,
402
+ fontWeight: "600",
403
+ fontFamily: theme.fontFamilies?.regular,
404
+ ...(Platform.OS === "android" && { includeFontPadding: false }),
405
+ },
406
+ cellText: {
407
+ color: colors.text || "#e5e7eb",
408
+ fontSize: 14,
409
+ lineHeight: 20,
410
+ fontFamily: theme.fontFamilies?.regular,
411
+ ...(Platform.OS === "android" && { includeFontPadding: false }),
412
+ },
413
+ cellContentWrapper: {
414
+ flexDirection: "row",
415
+ flexWrap: "wrap",
416
+ alignItems: "center",
417
+ },
418
+ });
419
+ };