react-native-nitro-markdown 0.7.1 → 0.8.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 (94) hide show
  1. package/README.md +179 -27
  2. package/ios/HybridMarkdownSession.swift +11 -4
  3. package/lib/commonjs/MarkdownContext.js +1 -3
  4. package/lib/commonjs/MarkdownContext.js.map +1 -1
  5. package/lib/commonjs/index.js +6 -0
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/markdown-stream.js +113 -29
  8. package/lib/commonjs/markdown-stream.js.map +1 -1
  9. package/lib/commonjs/markdown.js +96 -38
  10. package/lib/commonjs/markdown.js.map +1 -1
  11. package/lib/commonjs/renderers/image.js +11 -2
  12. package/lib/commonjs/renderers/image.js.map +1 -1
  13. package/lib/commonjs/renderers/math.js +22 -2
  14. package/lib/commonjs/renderers/math.js.map +1 -1
  15. package/lib/commonjs/renderers/table/index.js +4 -4
  16. package/lib/commonjs/renderers/table/index.js.map +1 -1
  17. package/lib/commonjs/renderers/table/table-utils.js +1 -1
  18. package/lib/commonjs/renderers/table/table-utils.js.map +1 -1
  19. package/lib/commonjs/use-markdown-stream.js +2 -6
  20. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  21. package/lib/commonjs/utils/code-highlight.js +1 -1
  22. package/lib/commonjs/utils/code-highlight.js.map +1 -1
  23. package/lib/commonjs/utils/incremental-ast.js +67 -14
  24. package/lib/commonjs/utils/incremental-ast.js.map +1 -1
  25. package/lib/commonjs/utils/link-security.js +2 -2
  26. package/lib/commonjs/utils/link-security.js.map +1 -1
  27. package/lib/commonjs/utils/stream-timeline.js +13 -7
  28. package/lib/commonjs/utils/stream-timeline.js.map +1 -1
  29. package/lib/module/MarkdownContext.js +1 -3
  30. package/lib/module/MarkdownContext.js.map +1 -1
  31. package/lib/module/index.js +1 -1
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/markdown-stream.js +113 -30
  34. package/lib/module/markdown-stream.js.map +1 -1
  35. package/lib/module/markdown.js +96 -38
  36. package/lib/module/markdown.js.map +1 -1
  37. package/lib/module/renderers/image.js +12 -3
  38. package/lib/module/renderers/image.js.map +1 -1
  39. package/lib/module/renderers/math.js +22 -2
  40. package/lib/module/renderers/math.js.map +1 -1
  41. package/lib/module/renderers/table/index.js +4 -4
  42. package/lib/module/renderers/table/index.js.map +1 -1
  43. package/lib/module/renderers/table/table-utils.js +1 -1
  44. package/lib/module/renderers/table/table-utils.js.map +1 -1
  45. package/lib/module/use-markdown-stream.js +2 -6
  46. package/lib/module/use-markdown-stream.js.map +1 -1
  47. package/lib/module/utils/code-highlight.js +1 -1
  48. package/lib/module/utils/code-highlight.js.map +1 -1
  49. package/lib/module/utils/incremental-ast.js +65 -13
  50. package/lib/module/utils/incremental-ast.js.map +1 -1
  51. package/lib/module/utils/link-security.js +2 -2
  52. package/lib/module/utils/link-security.js.map +1 -1
  53. package/lib/module/utils/stream-timeline.js +13 -7
  54. package/lib/module/utils/stream-timeline.js.map +1 -1
  55. package/lib/typescript/commonjs/MarkdownContext.d.ts +1 -0
  56. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/index.d.ts +3 -3
  58. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/markdown-stream.d.ts +28 -7
  60. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/utils/incremental-ast.d.ts +1 -0
  66. package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -1
  68. package/lib/typescript/module/MarkdownContext.d.ts +1 -0
  69. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  70. package/lib/typescript/module/index.d.ts +3 -3
  71. package/lib/typescript/module/index.d.ts.map +1 -1
  72. package/lib/typescript/module/markdown-stream.d.ts +28 -7
  73. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  74. package/lib/typescript/module/markdown.d.ts.map +1 -1
  75. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  76. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  77. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  78. package/lib/typescript/module/utils/incremental-ast.d.ts +1 -0
  79. package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -1
  80. package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -1
  81. package/package.json +1 -1
  82. package/src/MarkdownContext.ts +2 -2
  83. package/src/index.ts +10 -2
  84. package/src/markdown-stream.tsx +153 -36
  85. package/src/markdown.tsx +82 -42
  86. package/src/renderers/image.tsx +13 -2
  87. package/src/renderers/math.tsx +20 -2
  88. package/src/renderers/table/index.tsx +4 -4
  89. package/src/renderers/table/table-utils.ts +1 -1
  90. package/src/use-markdown-stream.ts +2 -6
  91. package/src/utils/code-highlight.ts +1 -1
  92. package/src/utils/incremental-ast.ts +104 -11
  93. package/src/utils/link-security.ts +3 -3
  94. package/src/utils/stream-timeline.ts +10 -7
@@ -3,11 +3,14 @@ import {
3
3
  useEffect,
4
4
  useRef,
5
5
  useCallback,
6
+ useMemo,
6
7
  startTransition,
7
8
  type FC,
9
+ type ReactNode,
8
10
  } from "react";
9
11
  import type { MarkdownNode } from "./headless";
10
- import { Markdown, type MarkdownProps } from "./markdown";
12
+ import type { ParserOptions } from "./Markdown.nitro";
13
+ import { Markdown, type MarkdownPlugin, type MarkdownProps } from "./markdown";
11
14
  import type { MarkdownSession } from "./specs/MarkdownSession.nitro";
12
15
  import {
13
16
  resolveMarkdownSession,
@@ -21,6 +24,26 @@ const normalizeOffset = (value: number): number | null => {
21
24
  return Math.floor(value);
22
25
  };
23
26
 
27
+ const normalizeParserOptions = (
28
+ options?: ParserOptions,
29
+ ): ParserOptions | undefined => {
30
+ if (!options) return undefined;
31
+
32
+ const gfm = options.gfm;
33
+ const math = options.math;
34
+ const html = options.html;
35
+
36
+ if (gfm === undefined && math === undefined && html === undefined) {
37
+ return undefined;
38
+ }
39
+
40
+ const normalized: ParserOptions = {};
41
+ if (gfm !== undefined) normalized.gfm = gfm;
42
+ if (math !== undefined) normalized.math = math;
43
+ if (html !== undefined) normalized.html = html;
44
+ return normalized;
45
+ };
46
+
24
47
  const resolveStreamText = ({
25
48
  forceFullSync,
26
49
  pendingFrom,
@@ -71,7 +94,11 @@ function warnStreamError(message: string, error: unknown): void {
71
94
  }
72
95
  }
73
96
 
74
- export type MarkdownStreamProps = {
97
+ export type MarkdownStreamSourceAstStatus = "available" | "disabled";
98
+
99
+ export type MarkdownStreamSourceAstDisabledReason = "beforeParse-plugin";
100
+
101
+ export type UseMarkdownStreamStateOptions = {
75
102
  /**
76
103
  * The active MarkdownSession to stream content from.
77
104
  */
@@ -98,13 +125,32 @@ export type MarkdownStreamProps = {
98
125
  * Automatically falls back to full parse when updates are not safely mergeable.
99
126
  */
100
127
  incrementalParsing?: boolean;
128
+ /**
129
+ * Parser options used for the stream source AST.
130
+ */
131
+ options?: ParserOptions;
132
+ /**
133
+ * Plugins determine whether an optimized source AST can be passed through.
134
+ */
135
+ plugins?: MarkdownPlugin[];
136
+ };
137
+
138
+ export type MarkdownStreamState = {
139
+ text: string;
140
+ sourceAst?: MarkdownNode;
141
+ sourceAstStatus: MarkdownStreamSourceAstStatus;
142
+ sourceAstDisabledReason?: MarkdownStreamSourceAstDisabledReason;
143
+ };
144
+
145
+ export type MarkdownStreamRenderProps = MarkdownStreamState & {
146
+ markdownProps: MarkdownProps;
147
+ };
148
+
149
+ export type MarkdownStreamProps = UseMarkdownStreamStateOptions & {
150
+ renderMarkdown?: (props: MarkdownStreamRenderProps) => ReactNode;
101
151
  } & Omit<MarkdownProps, "children" | "sourceAst">;
102
152
 
103
- /**
104
- * A component that renders streaming Markdown from a MarkdownSession.
105
- * It efficiently subscribes to session updates to minimize parent re-renders.
106
- */
107
- export const MarkdownStream: FC<MarkdownStreamProps> = ({
153
+ export function useMarkdownStreamState({
108
154
  session,
109
155
  updateIntervalMs = 50,
110
156
  updateStrategy = "interval",
@@ -112,26 +158,43 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
112
158
  incrementalParsing = true,
113
159
  options,
114
160
  plugins,
115
- ...props
116
- }) => {
161
+ }: UseMarkdownStreamStateOptions): MarkdownStreamState {
117
162
  const activeSession = resolveMarkdownSession(session);
163
+ const parserOptionGfm = options?.gfm;
164
+ const parserOptionMath = options?.math;
165
+ const parserOptionHtml = options?.html;
166
+ const parserOptions = useMemo(
167
+ () =>
168
+ normalizeParserOptions(
169
+ Object.assign(
170
+ {},
171
+ parserOptionGfm === undefined ? null : { gfm: parserOptionGfm },
172
+ parserOptionMath === undefined ? null : { math: parserOptionMath },
173
+ parserOptionHtml === undefined ? null : { html: parserOptionHtml },
174
+ ),
175
+ ),
176
+ [parserOptionGfm, parserOptionMath, parserOptionHtml],
177
+ );
118
178
  const parseText = useCallback(
119
- (text: string): MarkdownNode => parseMarkdownAst(text, options),
120
- [options],
179
+ (text: string): MarkdownNode => parseMarkdownAst(text, parserOptions),
180
+ [parserOptions],
121
181
  );
122
182
  const createEmptyAst = (): MarkdownNode => ({
123
183
  type: "document",
124
184
  children: [],
125
185
  });
126
- const initialText = activeSession.getAllText();
127
186
  const hasBeforeParsePlugins =
128
187
  plugins?.some((plugin) => typeof plugin.beforeParse === "function") ??
129
188
  false;
130
- const [renderState, setRenderState] = useState(() => ({
131
- text: initialText,
132
- ast: hasBeforeParsePlugins ? createEmptyAst() : parseText(initialText),
133
- }));
189
+ const [renderState, setRenderState] = useState(() => {
190
+ const initialText = activeSession.getAllText();
191
+ return {
192
+ text: initialText,
193
+ ast: hasBeforeParsePlugins ? createEmptyAst() : parseText(initialText),
194
+ };
195
+ });
134
196
  const renderStateRef = useRef(renderState);
197
+ const didMountRef = useRef(false);
135
198
  const pendingUpdateRef = useRef(false);
136
199
  const pendingFromRef = useRef<number | null>(null);
137
200
  const pendingToRef = useRef<number | null>(null);
@@ -153,6 +216,11 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
153
216
  }, []);
154
217
 
155
218
  useEffect(() => {
219
+ if (!didMountRef.current) {
220
+ didMountRef.current = true;
221
+ return;
222
+ }
223
+
156
224
  const initialText = activeSession.getAllText();
157
225
  const initialState = {
158
226
  text: initialText,
@@ -184,13 +252,19 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
184
252
  pendingToRef.current = null;
185
253
  forceFullSyncRef.current = false;
186
254
 
187
- const latest = resolveStreamText({
188
- forceFullSync,
189
- pendingFrom,
190
- pendingTo,
191
- previousText: previousState.text,
192
- session: activeSession,
193
- });
255
+ let latest: string;
256
+ try {
257
+ latest = resolveStreamText({
258
+ forceFullSync,
259
+ pendingFrom,
260
+ pendingTo,
261
+ previousText: previousState.text,
262
+ session: activeSession,
263
+ });
264
+ } catch (error) {
265
+ warnStreamError("[NitroMarkdown] Failed to read stream session:", error);
266
+ return;
267
+ }
194
268
  if (latest === previousState.text) return;
195
269
 
196
270
  const nextAst = hasBeforeParsePlugins
@@ -198,9 +272,9 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
198
272
  : getNextStreamAst({
199
273
  allowIncremental,
200
274
  nextText: latest,
201
- options,
202
275
  previousAst: previousState.ast,
203
276
  previousText: previousState.text,
277
+ ...(parserOptions ? { options: parserOptions } : {}),
204
278
  });
205
279
  const nextState = {
206
280
  text: latest,
@@ -236,6 +310,8 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
236
310
 
237
311
  try {
238
312
  unsubscribe = activeSession.addListener((from, to) => {
313
+ if (!mountedRef.current) return;
314
+
239
315
  const nextFrom = normalizeOffset(from);
240
316
  const nextTo = normalizeOffset(to);
241
317
 
@@ -259,6 +335,10 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
259
335
  }
260
336
 
261
337
  return () => {
338
+ pendingUpdateRef.current = false;
339
+ pendingFromRef.current = null;
340
+ pendingToRef.current = null;
341
+ forceFullSyncRef.current = false;
262
342
  try {
263
343
  unsubscribe?.();
264
344
  } catch (error) {
@@ -279,22 +359,59 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
279
359
  }, [
280
360
  allowIncremental,
281
361
  hasBeforeParsePlugins,
282
- options,
283
- plugins,
362
+ parserOptions,
284
363
  activeSession,
285
364
  updateIntervalMs,
286
365
  updateStrategy,
287
366
  useTransitionUpdates,
288
367
  ]);
289
368
 
290
- return (
291
- <Markdown
292
- {...props}
293
- options={options}
294
- plugins={plugins}
295
- sourceAst={hasBeforeParsePlugins ? undefined : renderState.ast}
296
- >
297
- {renderState.text}
298
- </Markdown>
299
- );
369
+ const streamState: MarkdownStreamState = {
370
+ text: renderState.text,
371
+ sourceAstStatus: hasBeforeParsePlugins ? "disabled" : "available",
372
+ };
373
+ if (hasBeforeParsePlugins) {
374
+ streamState.sourceAstDisabledReason = "beforeParse-plugin";
375
+ } else {
376
+ streamState.sourceAst = renderState.ast;
377
+ }
378
+ return streamState;
379
+ }
380
+
381
+ export const MarkdownStream: FC<MarkdownStreamProps> = ({
382
+ session,
383
+ updateIntervalMs = 50,
384
+ updateStrategy = "interval",
385
+ useTransitionUpdates = false,
386
+ incrementalParsing = true,
387
+ options,
388
+ plugins,
389
+ renderMarkdown,
390
+ ...props
391
+ }) => {
392
+ const streamState = useMarkdownStreamState({
393
+ session,
394
+ updateIntervalMs,
395
+ updateStrategy,
396
+ useTransitionUpdates,
397
+ incrementalParsing,
398
+ ...(options ? { options } : {}),
399
+ ...(plugins ? { plugins } : {}),
400
+ });
401
+ const markdownProps: MarkdownProps = {
402
+ ...props,
403
+ children: streamState.text,
404
+ };
405
+ if (options) markdownProps.options = options;
406
+ if (plugins) markdownProps.plugins = plugins;
407
+ if (streamState.sourceAst) markdownProps.sourceAst = streamState.sourceAst;
408
+
409
+ if (renderMarkdown) {
410
+ return renderMarkdown({
411
+ ...streamState,
412
+ markdownProps,
413
+ });
414
+ }
415
+
416
+ return <Markdown {...markdownProps} />;
300
417
  };
package/src/markdown.tsx CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  type CustomRenderer,
35
35
  type CustomRenderers,
36
36
  type LinkPressHandler,
37
+ type MarkdownContextValue,
37
38
  type NodeRendererProps,
38
39
  type TableOptions,
39
40
  } from "./MarkdownContext";
@@ -174,10 +175,8 @@ const warnInDev = (message: string, error?: unknown): void => {
174
175
  };
175
176
 
176
177
  const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
177
- return {
178
- ...node,
179
- children: node.children?.map(cloneMarkdownNode),
180
- };
178
+ const children = node.children?.map(cloneMarkdownNode);
179
+ return children ? { ...node, children } : { ...node };
181
180
  };
182
181
 
183
182
  const getParserOptionsKey = (options?: ParserOptions): string => {
@@ -204,11 +203,11 @@ const normalizeParserOptions = (
204
203
  return undefined;
205
204
  }
206
205
 
207
- return {
208
- gfm,
209
- math,
210
- html,
211
- };
206
+ const normalized: ParserOptions = {};
207
+ if (gfm !== undefined) normalized.gfm = gfm;
208
+ if (math !== undefined) normalized.math = math;
209
+ if (html !== undefined) normalized.html = html;
210
+ return normalized;
212
211
  };
213
212
 
214
213
  const parseWithNativeParser = (
@@ -478,13 +477,22 @@ export const Markdown: FC<MarkdownProps> = ({
478
477
  const markdownToParse = sourceAst
479
478
  ? children
480
479
  : applyBeforeParsePlugins(children, sortedPlugins, onErrorRef.current);
481
- const parserOptions = normalizeParserOptions({
482
- gfm: parserOptionGfm,
483
- math: parserOptionMath,
484
- html: parserOptionHtml,
485
- });
480
+ const parserOptions = normalizeParserOptions(
481
+ Object.assign(
482
+ {},
483
+ parserOptionGfm === undefined ? null : { gfm: parserOptionGfm },
484
+ parserOptionMath === undefined ? null : { math: parserOptionMath },
485
+ parserOptionHtml === undefined ? null : { html: parserOptionHtml },
486
+ ),
487
+ );
488
+ const shouldCloneSourceAst =
489
+ sourceAst &&
490
+ (Boolean(astTransform) ||
491
+ sortedPlugins?.some((plugin) => plugin.afterParse) === true);
486
492
  let parsedAst = sourceAst
487
- ? cloneMarkdownNode(sourceAst)
493
+ ? shouldCloneSourceAst
494
+ ? cloneMarkdownNode(sourceAst)
495
+ : sourceAst
488
496
  : parseCache
489
497
  ? getCachedParsedAst(markdownToParse, parserOptions)
490
498
  : parseWithNativeParser(markdownToParse, parserOptions);
@@ -560,16 +568,16 @@ export const Markdown: FC<MarkdownProps> = ({
560
568
  }, [userTheme, stylingStrategy]);
561
569
 
562
570
  const baseStyles = getBaseStyles(theme);
563
- const contextValue = useMemo(
571
+ const contextValue = useMemo<MarkdownContextValue>(
564
572
  () => ({
565
573
  renderers,
566
574
  theme,
567
- styles: nodeStyles,
568
575
  stylingStrategy,
569
- onLinkPress,
570
- tableOptions,
571
- imageOptions,
572
- highlightCode,
576
+ ...(nodeStyles ? { styles: nodeStyles } : {}),
577
+ ...(onLinkPress ? { onLinkPress } : {}),
578
+ ...(tableOptions ? { tableOptions } : {}),
579
+ ...(imageOptions ? { imageOptions } : {}),
580
+ ...(highlightCode === undefined ? {} : { highlightCode }),
573
581
  }),
574
582
  [
575
583
  renderers,
@@ -773,15 +781,18 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
773
781
  ...(node.type === "heading" && {
774
782
  level: (node.level ?? 1) as 1 | 2 | 3 | 4 | 5 | 6,
775
783
  }),
776
- ...(node.type === "link" && { href: node.href ?? "", title: node.title }),
784
+ ...(node.type === "link" && {
785
+ href: node.href ?? "",
786
+ ...(node.title ? { title: node.title } : {}),
787
+ }),
777
788
  ...(node.type === "image" && {
778
789
  url: node.href ?? "",
779
- alt: node.alt,
780
- title: node.title,
790
+ ...(node.alt ? { alt: node.alt } : {}),
791
+ ...(node.title ? { title: node.title } : {}),
781
792
  }),
782
793
  ...(node.type === "code_block" && {
783
794
  content: getTextContent(node),
784
- language: node.language,
795
+ ...(node.language ? { language: node.language } : {}),
785
796
  }),
786
797
  ...(node.type === "code_inline" && { content: node.content ?? "" }),
787
798
  ...((node.type === "math_inline" || node.type === "math_block") && {
@@ -789,7 +800,7 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
789
800
  }),
790
801
  ...(node.type === "list" && {
791
802
  ordered: node.ordered ?? false,
792
- start: node.start,
803
+ ...(node.start === undefined ? {} : { start: node.start }),
793
804
  }),
794
805
  ...(node.type === "task_list_item" && { checked: node.checked ?? false }),
795
806
  };
@@ -810,7 +821,10 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
810
821
 
811
822
  case "heading":
812
823
  return (
813
- <Heading level={node.level ?? 1} style={nodeStyles?.heading}>
824
+ <Heading
825
+ level={node.level ?? 1}
826
+ {...(nodeStyles?.heading ? { style: nodeStyles.heading } : {})}
827
+ >
814
828
  {renderChildren(node.children, inListItem, true)}
815
829
  </Heading>
816
830
  );
@@ -853,7 +867,10 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
853
867
 
854
868
  case "link":
855
869
  return (
856
- <Link href={node.href ?? ""} style={nodeStyles?.link}>
870
+ <Link
871
+ href={node.href ?? ""}
872
+ {...(nodeStyles?.link ? { style: nodeStyles.link } : {})}
873
+ >
857
874
  {renderChildren(node.children, inListItem, true)}
858
875
  </Link>
859
876
  );
@@ -862,36 +879,52 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
862
879
  return (
863
880
  <Image
864
881
  url={node.href ?? ""}
865
- title={node.title}
866
- alt={node.alt}
867
882
  Renderer={NodeRenderer}
868
- style={nodeStyles?.image}
883
+ {...(node.title ? { title: node.title } : {})}
884
+ {...(node.alt ? { alt: node.alt } : {})}
885
+ {...(nodeStyles?.image ? { style: nodeStyles.image } : {})}
869
886
  />
870
887
  );
871
888
 
872
889
  case "code_inline":
873
890
  return (
874
- <InlineCode style={nodeStyles?.code_inline}>{node.content}</InlineCode>
891
+ <InlineCode
892
+ {...(nodeStyles?.code_inline
893
+ ? { style: nodeStyles.code_inline }
894
+ : {})}
895
+ >
896
+ {node.content}
897
+ </InlineCode>
875
898
  );
876
899
 
877
900
  case "code_block":
878
901
  return (
879
902
  <CodeBlock
880
- language={node.language}
881
903
  content={getTextContent(node)}
882
- style={nodeStyles?.code_block}
904
+ {...(node.language ? { language: node.language } : {})}
905
+ {...(nodeStyles?.code_block ? { style: nodeStyles.code_block } : {})}
883
906
  />
884
907
  );
885
908
 
886
909
  case "blockquote":
887
910
  return (
888
- <Blockquote style={nodeStyles?.blockquote}>
911
+ <Blockquote
912
+ {...(nodeStyles?.blockquote
913
+ ? { style: nodeStyles.blockquote }
914
+ : {})}
915
+ >
889
916
  {renderChildren(node.children, inListItem, false)}
890
917
  </Blockquote>
891
918
  );
892
919
 
893
920
  case "horizontal_rule":
894
- return <HorizontalRule style={nodeStyles?.horizontal_rule} />;
921
+ return (
922
+ <HorizontalRule
923
+ {...(nodeStyles?.horizontal_rule
924
+ ? { style: nodeStyles.horizontal_rule }
925
+ : {})}
926
+ />
927
+ );
895
928
 
896
929
  case "line_break":
897
930
  return <Text>{"\n"}</Text>;
@@ -904,7 +937,12 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
904
937
  if (!mathContent) return null;
905
938
  mathContent = mathContent.replace(/^\$+|\$+$/g, "").trim();
906
939
  return (
907
- <MathInline content={mathContent} style={nodeStyles?.math_inline} />
940
+ <MathInline
941
+ content={mathContent}
942
+ {...(nodeStyles?.math_inline
943
+ ? { style: nodeStyles.math_inline }
944
+ : {})}
945
+ />
908
946
  );
909
947
  }
910
948
 
@@ -912,7 +950,7 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
912
950
  return (
913
951
  <MathBlock
914
952
  content={getTextContent(node)}
915
- style={nodeStyles?.math_block}
953
+ {...(nodeStyles?.math_block ? { style: nodeStyles.math_block } : {})}
916
954
  />
917
955
  );
918
956
 
@@ -920,9 +958,9 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
920
958
  return (
921
959
  <List
922
960
  ordered={node.ordered ?? false}
923
- start={node.start}
924
961
  depth={depth}
925
- style={nodeStyles?.list}
962
+ {...(node.start === undefined ? {} : { start: node.start })}
963
+ {...(nodeStyles?.list ? { style: nodeStyles.list } : {})}
926
964
  >
927
965
  {node.children?.map((child, index) => {
928
966
  if (child.type === "task_list_item") {
@@ -962,7 +1000,9 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
962
1000
  return (
963
1001
  <TaskListItem
964
1002
  checked={node.checked ?? false}
965
- style={nodeStyles?.task_list_item}
1003
+ {...(nodeStyles?.task_list_item
1004
+ ? { style: nodeStyles.task_list_item }
1005
+ : {})}
966
1006
  >
967
1007
  {renderChildren(node.children, true, false)}
968
1008
  </TaskListItem>
@@ -973,7 +1013,7 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
973
1013
  <TableRenderer
974
1014
  node={node}
975
1015
  Renderer={NodeRenderer}
976
- style={nodeStyles?.table}
1016
+ {...(nodeStyles?.table ? { style: nodeStyles.table } : {})}
977
1017
  />
978
1018
  );
979
1019
 
@@ -2,6 +2,7 @@ import {
2
2
  useState,
3
3
  useEffect,
4
4
  useMemo,
5
+ useRef,
5
6
  type ReactNode,
6
7
  type FC,
7
8
  type ComponentType,
@@ -47,6 +48,7 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
47
48
  const [loading, setLoading] = useState(true);
48
49
  const [error, setError] = useState(false);
49
50
  const [aspectRatio, setAspectRatio] = useState<number | undefined>(undefined);
51
+ const mountedRef = useRef(true);
50
52
  const { theme, imageOptions } = useMarkdownContext();
51
53
  const allowedImageHref = useMemo(
52
54
  () => getAllowedImageHref(url, imageOptions),
@@ -115,6 +117,13 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
115
117
  [theme, aspectRatio],
116
118
  );
117
119
 
120
+ useEffect(() => {
121
+ mountedRef.current = true;
122
+ return () => {
123
+ mountedRef.current = false;
124
+ };
125
+ }, []);
126
+
118
127
  useEffect(() => {
119
128
  let isMounted = true;
120
129
  setLoading(true);
@@ -131,8 +140,8 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
131
140
  /picsum\.photos\/.*\/(\d+)\/(\d+)/,
132
141
  );
133
142
  if (picsumMatch) {
134
- const w = parseInt(picsumMatch[1], 10);
135
- const h = parseInt(picsumMatch[2], 10);
143
+ const w = parseInt(picsumMatch[1] ?? "", 10);
144
+ const h = parseInt(picsumMatch[2] ?? "", 10);
136
145
  if (!isNaN(w) && !isNaN(h) && h !== 0) {
137
146
  setAspectRatio(w / h);
138
147
  return () => {
@@ -236,9 +245,11 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
236
245
  accessibilityRole={accessibilityLabel ? "image" : undefined}
237
246
  accessibilityLabel={accessibilityLabel}
238
247
  onLoad={() => {
248
+ if (!mountedRef.current) return;
239
249
  setLoading(false);
240
250
  }}
241
251
  onError={() => {
252
+ if (!mountedRef.current) return;
242
253
  setLoading(false);
243
254
  setError(true);
244
255
  }}
@@ -247,6 +247,14 @@ export const MathInline: FC<MathInlineProps> = ({ content, style }) => {
247
247
  const { theme } = useMarkdownContext();
248
248
  const styles = getCachedStyles(mathStylesCache, theme, createMathStyles);
249
249
  const [hasRenderError, setHasRenderError] = useState(false);
250
+ const mountedRef = useRef(true);
251
+
252
+ useEffect(() => {
253
+ mountedRef.current = true;
254
+ return () => {
255
+ mountedRef.current = false;
256
+ };
257
+ }, []);
250
258
 
251
259
  if (!content) return null;
252
260
 
@@ -257,9 +265,10 @@ export const MathInline: FC<MathInlineProps> = ({ content, style }) => {
257
265
  latex={content}
258
266
  fontSize={getInlineMathFontSize(content, theme)}
259
267
  displayMode={false}
260
- color={theme.colors.text}
261
268
  style={styles.ratexInline}
269
+ {...(theme.colors.text ? { color: theme.colors.text } : {})}
262
270
  onError={() => {
271
+ if (!mountedRef.current) return;
263
272
  setHasRenderError(true);
264
273
  }}
265
274
  />
@@ -283,6 +292,14 @@ export const MathBlock: FC<MathBlockProps> = ({ content, style }) => {
283
292
  const { theme } = useMarkdownContext();
284
293
  const styles = getCachedStyles(mathStylesCache, theme, createMathStyles);
285
294
  const [hasRenderError, setHasRenderError] = useState(false);
295
+ const mountedRef = useRef(true);
296
+
297
+ useEffect(() => {
298
+ mountedRef.current = true;
299
+ return () => {
300
+ mountedRef.current = false;
301
+ };
302
+ }, []);
286
303
 
287
304
  if (!content) return null;
288
305
 
@@ -297,9 +314,10 @@ export const MathBlock: FC<MathBlockProps> = ({ content, style }) => {
297
314
  latex={content}
298
315
  fontSize={theme.fontSizes.xl}
299
316
  displayMode
300
- color={theme.colors.text}
301
317
  style={styles.ratexBlock}
318
+ {...(theme.colors.text ? { color: theme.colors.text } : {})}
302
319
  onError={() => {
320
+ if (!mountedRef.current) return;
303
321
  setHasRenderError(true);
304
322
  }}
305
323
  />
@@ -181,19 +181,19 @@ export const TableRenderer: FC<TableRendererProps> = ({
181
181
  for (let col = 0; col < columnCount; col++) {
182
182
  const headerWidth = measuredWidths.current.get(`header-${col}`);
183
183
  if (headerWidth && headerWidth > 0) {
184
- maxWidths[col] = Math.max(maxWidths[col], headerWidth);
184
+ maxWidths[col] = Math.max(maxWidths[col] ?? 0, headerWidth);
185
185
  }
186
186
 
187
187
  for (let row = 0; row < rows.length; row++) {
188
- if (col >= rows[row].length) continue;
188
+ if (col >= (rows[row]?.length ?? 0)) continue;
189
189
  const cellWidth = measuredWidths.current.get(`cell-${row}-${col}`);
190
190
  if (cellWidth && cellWidth > 0) {
191
- maxWidths[col] = Math.max(maxWidths[col], cellWidth);
191
+ maxWidths[col] = Math.max(maxWidths[col] ?? 0, cellWidth);
192
192
  }
193
193
  }
194
194
 
195
195
  maxWidths[col] = Math.max(
196
- maxWidths[col] + COLUMN_MEASUREMENT_PADDING,
196
+ (maxWidths[col] ?? 0) + COLUMN_MEASUREMENT_PADDING,
197
197
  minColumnWidth,
198
198
  );
199
199
  }
@@ -59,7 +59,7 @@ export const estimateColumnWidths = (
59
59
  let maxChars = headerChars;
60
60
 
61
61
  for (let row = 0; row < rows.length; row++) {
62
- const cell = rows[row][col];
62
+ const cell = rows[row]?.[col];
63
63
  if (!cell) continue;
64
64
  const cellChars = Math.min(
65
65
  getTextContent(cell).trim().length,