react-native-nitro-markdown 0.5.3 → 0.5.5

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 (125) hide show
  1. package/README.md +89 -10
  2. package/android/CMakeLists.txt +1 -1
  3. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +9 -3
  4. package/cpp/CMakeLists.txt +2 -1
  5. package/cpp/bindings/HybridMarkdownParser.cpp +4 -2
  6. package/cpp/core/MD4CParser.cpp +69 -2
  7. package/cpp/core/MD4CParser.hpp +17 -0
  8. package/cpp/core/MarkdownTypes.hpp +1 -1
  9. package/lib/commonjs/headless.js +2 -2
  10. package/lib/commonjs/markdown.js +56 -44
  11. package/lib/commonjs/markdown.js.map +1 -1
  12. package/lib/commonjs/renderers/blockquote.js +15 -13
  13. package/lib/commonjs/renderers/blockquote.js.map +1 -1
  14. package/lib/commonjs/renderers/code.js +57 -53
  15. package/lib/commonjs/renderers/code.js.map +1 -1
  16. package/lib/commonjs/renderers/heading.js +48 -46
  17. package/lib/commonjs/renderers/heading.js.map +1 -1
  18. package/lib/commonjs/renderers/horizontal-rule.js +10 -8
  19. package/lib/commonjs/renderers/horizontal-rule.js.map +1 -1
  20. package/lib/commonjs/renderers/image.js +12 -3
  21. package/lib/commonjs/renderers/image.js.map +1 -1
  22. package/lib/commonjs/renderers/list.js +75 -70
  23. package/lib/commonjs/renderers/list.js.map +1 -1
  24. package/lib/commonjs/renderers/math.js +4 -3
  25. package/lib/commonjs/renderers/math.js.map +1 -1
  26. package/lib/commonjs/renderers/paragraph.js +15 -13
  27. package/lib/commonjs/renderers/paragraph.js.map +1 -1
  28. package/lib/commonjs/renderers/style-cache.js +14 -0
  29. package/lib/commonjs/renderers/style-cache.js.map +1 -0
  30. package/lib/commonjs/renderers/table/index.js +7 -4
  31. package/lib/commonjs/renderers/table/index.js.map +1 -1
  32. package/lib/module/headless.js +2 -2
  33. package/lib/module/markdown.js +56 -44
  34. package/lib/module/markdown.js.map +1 -1
  35. package/lib/module/renderers/blockquote.js +15 -13
  36. package/lib/module/renderers/blockquote.js.map +1 -1
  37. package/lib/module/renderers/code.js +57 -53
  38. package/lib/module/renderers/code.js.map +1 -1
  39. package/lib/module/renderers/heading.js +48 -46
  40. package/lib/module/renderers/heading.js.map +1 -1
  41. package/lib/module/renderers/horizontal-rule.js +10 -8
  42. package/lib/module/renderers/horizontal-rule.js.map +1 -1
  43. package/lib/module/renderers/image.js +13 -4
  44. package/lib/module/renderers/image.js.map +1 -1
  45. package/lib/module/renderers/list.js +75 -70
  46. package/lib/module/renderers/list.js.map +1 -1
  47. package/lib/module/renderers/math.js +4 -3
  48. package/lib/module/renderers/math.js.map +1 -1
  49. package/lib/module/renderers/paragraph.js +15 -13
  50. package/lib/module/renderers/paragraph.js.map +1 -1
  51. package/lib/module/renderers/style-cache.js +10 -0
  52. package/lib/module/renderers/style-cache.js.map +1 -0
  53. package/lib/module/renderers/table/index.js +7 -4
  54. package/lib/module/renderers/table/index.js.map +1 -1
  55. package/lib/typescript/commonjs/Markdown.nitro.d.ts +1 -0
  56. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/headless.d.ts +2 -2
  58. package/lib/typescript/commonjs/markdown.d.ts +7 -1
  59. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/renderers/blockquote.d.ts +1 -1
  61. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/renderers/heading.d.ts +1 -1
  64. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +1 -1
  66. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/renderers/list.d.ts +1 -1
  69. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/renderers/math.d.ts +1 -1
  71. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/renderers/paragraph.d.ts +1 -1
  73. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/renderers/style-cache.d.ts +3 -0
  75. package/lib/typescript/commonjs/renderers/style-cache.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/theme.d.ts +2 -2
  78. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  79. package/lib/typescript/module/Markdown.nitro.d.ts +1 -0
  80. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  81. package/lib/typescript/module/headless.d.ts +2 -2
  82. package/lib/typescript/module/markdown.d.ts +7 -1
  83. package/lib/typescript/module/markdown.d.ts.map +1 -1
  84. package/lib/typescript/module/renderers/blockquote.d.ts +1 -1
  85. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  86. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  87. package/lib/typescript/module/renderers/heading.d.ts +1 -1
  88. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  89. package/lib/typescript/module/renderers/horizontal-rule.d.ts +1 -1
  90. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  91. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  92. package/lib/typescript/module/renderers/list.d.ts +1 -1
  93. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  94. package/lib/typescript/module/renderers/math.d.ts +1 -1
  95. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  96. package/lib/typescript/module/renderers/paragraph.d.ts +1 -1
  97. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  98. package/lib/typescript/module/renderers/style-cache.d.ts +3 -0
  99. package/lib/typescript/module/renderers/style-cache.d.ts.map +1 -0
  100. package/lib/typescript/module/renderers/table/index.d.ts.map +1 -1
  101. package/lib/typescript/module/theme.d.ts +2 -2
  102. package/lib/typescript/module/theme.d.ts.map +1 -1
  103. package/nitro.json +12 -3
  104. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -2
  105. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  106. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
  107. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +2 -2
  108. package/nitrogen/generated/ios/NitroMarkdown+autolinking.rb +2 -0
  109. package/nitrogen/generated/shared/c++/ParserOptions.hpp +6 -2
  110. package/package.json +6 -5
  111. package/react-native-nitro-markdown.podspec +3 -0
  112. package/src/Markdown.nitro.ts +1 -0
  113. package/src/headless.ts +2 -2
  114. package/src/markdown.tsx +102 -58
  115. package/src/renderers/blockquote.tsx +22 -17
  116. package/src/renderers/code.tsx +75 -63
  117. package/src/renderers/heading.tsx +60 -54
  118. package/src/renderers/horizontal-rule.tsx +17 -12
  119. package/src/renderers/image.tsx +15 -4
  120. package/src/renderers/list.tsx +93 -76
  121. package/src/renderers/math.tsx +8 -3
  122. package/src/renderers/paragraph.tsx +22 -17
  123. package/src/renderers/style-cache.ts +14 -0
  124. package/src/renderers/table/index.tsx +15 -10
  125. package/src/theme.ts +2 -2
package/src/markdown.tsx CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  type ListRenderItemInfo,
19
19
  type FlatListProps,
20
20
  type StyleProp,
21
- type TextStyle,
22
21
  type ViewStyle,
23
22
  } from "react-native";
24
23
  import {
@@ -101,9 +100,15 @@ function safeOnError<P extends string>(
101
100
  }
102
101
 
103
102
  const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
104
- const parseAstCache = new Map<string, MarkdownNode>();
103
+ type ParseAstCacheEntry = {
104
+ text: string;
105
+ ast: MarkdownNode;
106
+ };
107
+
108
+ const parseAstCache = new Map<string, ParseAstCacheEntry>();
105
109
  const MAX_PARSE_CACHE_ENTRIES = 32;
106
110
  const MAX_CACHEABLE_TEXT_LENGTH = 24_000;
111
+ const EMPTY_RENDERERS: CustomRenderers = {};
107
112
 
108
113
  export type AstTransform = (ast: MarkdownNode) => MarkdownNode;
109
114
  export type MarkdownVirtualizationOptions = Pick<
@@ -166,12 +171,14 @@ const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
166
171
  };
167
172
 
168
173
  const getParserOptionsKey = (options?: ParserOptions): string => {
169
- if (!options) return "gfm:default|math:default";
174
+ if (!options) return "gfm:default|math:default|html:default";
170
175
 
171
176
  const gfm = options.gfm === undefined ? "default" : options.gfm ? "1" : "0";
172
177
  const math =
173
178
  options.math === undefined ? "default" : options.math ? "1" : "0";
174
- return `gfm:${gfm}|math:${math}`;
179
+ const html =
180
+ options.html === undefined ? "default" : options.html ? "1" : "0";
181
+ return `gfm:${gfm}|math:${math}|html:${html}`;
175
182
  };
176
183
 
177
184
  const normalizeParserOptions = (
@@ -181,14 +188,16 @@ const normalizeParserOptions = (
181
188
 
182
189
  const gfm = options.gfm;
183
190
  const math = options.math;
191
+ const html = options.html;
184
192
 
185
- if (gfm === undefined && math === undefined) {
193
+ if (gfm === undefined && math === undefined && html === undefined) {
186
194
  return undefined;
187
195
  }
188
196
 
189
197
  return {
190
198
  gfm,
191
199
  math,
200
+ html,
192
201
  };
193
202
  };
194
203
 
@@ -211,15 +220,18 @@ const getCachedParsedAst = (
211
220
  }
212
221
 
213
222
  const cacheKey = `${getParserOptionsKey(options)}|${text.length}|${hashString(text)}`;
214
- const cachedNode = parseAstCache.get(cacheKey);
215
- if (cachedNode) {
223
+ const cachedEntry = parseAstCache.get(cacheKey);
224
+ if (cachedEntry?.text === text) {
216
225
  parseAstCache.delete(cacheKey);
217
- parseAstCache.set(cacheKey, cachedNode);
218
- return cloneMarkdownNode(cachedNode);
226
+ parseAstCache.set(cacheKey, cachedEntry);
227
+ return cloneMarkdownNode(cachedEntry.ast);
219
228
  }
220
229
 
221
230
  const parsedNode = parseWithNativeParser(text, options);
222
- parseAstCache.set(cacheKey, parsedNode);
231
+ parseAstCache.set(cacheKey, {
232
+ text,
233
+ ast: parsedNode,
234
+ });
223
235
  if (parseAstCache.size > MAX_PARSE_CACHE_ENTRIES) {
224
236
  const oldestCacheKey = parseAstCache.keys().next().value;
225
237
  if (typeof oldestCacheKey === "string") {
@@ -230,20 +242,27 @@ const getCachedParsedAst = (
230
242
  return cloneMarkdownNode(parsedNode);
231
243
  };
232
244
 
245
+ const sortPluginsByPriority = (
246
+ plugins?: MarkdownPlugin[],
247
+ ): MarkdownPlugin[] | undefined => {
248
+ if (!plugins || plugins.length === 0) {
249
+ return undefined;
250
+ }
251
+
252
+ return [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
253
+ };
254
+
233
255
  const applyBeforeParsePlugins = (
234
256
  markdown: string,
235
- plugins?: MarkdownPlugin[],
257
+ sortedPlugins?: MarkdownPlugin[],
236
258
  onError?: (error: Error, phase: "before-plugin", pluginName?: string) => void,
237
259
  ): string => {
238
- if (!plugins || plugins.length === 0) {
260
+ if (!sortedPlugins || sortedPlugins.length === 0) {
239
261
  return markdown;
240
262
  }
241
263
 
242
- const sorted = [...plugins].sort(
243
- (a, b) => (b.priority ?? 0) - (a.priority ?? 0),
244
- );
245
264
  let nextMarkdown = markdown;
246
- for (const plugin of sorted) {
265
+ for (const plugin of sortedPlugins) {
247
266
  if (!plugin.beforeParse) continue;
248
267
 
249
268
  try {
@@ -266,18 +285,15 @@ const applyBeforeParsePlugins = (
266
285
 
267
286
  const applyAfterParsePlugins = (
268
287
  ast: MarkdownNode,
269
- plugins?: MarkdownPlugin[],
288
+ sortedPlugins?: MarkdownPlugin[],
270
289
  onError?: (error: Error, phase: "after-plugin", pluginName?: string) => void,
271
290
  ): MarkdownNode => {
272
- if (!plugins || plugins.length === 0) {
291
+ if (!sortedPlugins || sortedPlugins.length === 0) {
273
292
  return ast;
274
293
  }
275
294
 
276
- const sorted = [...plugins].sort(
277
- (a, b) => (b.priority ?? 0) - (a.priority ?? 0),
278
- );
279
295
  let nextAst = ast;
280
- for (const plugin of sorted) {
296
+ for (const plugin of sortedPlugins) {
281
297
  if (!plugin.afterParse) continue;
282
298
 
283
299
  try {
@@ -304,7 +320,7 @@ export type MarkdownProps = {
304
320
  */
305
321
  children: string;
306
322
  /**
307
- * Parser options to enable GFM or Math support.
323
+ * Parser options to enable GFM, math, or raw HTML AST support.
308
324
  */
309
325
  options?: ParserOptions;
310
326
  /**
@@ -316,6 +332,12 @@ export type MarkdownProps = {
316
332
  * When provided, native parse is skipped and this tree is rendered instead.
317
333
  */
318
334
  sourceAst?: MarkdownNode;
335
+ /**
336
+ * Enables internal parse AST cache keyed by parser options and markdown.
337
+ * Disable to force native parse on each parse cycle.
338
+ * @default true
339
+ */
340
+ parseCache?: boolean;
319
341
  /**
320
342
  * Optional transform applied after parsing and before rendering.
321
343
  * The transformed AST is also returned in `onParseComplete`.
@@ -420,8 +442,9 @@ export const Markdown: FC<MarkdownProps> = ({
420
442
  options,
421
443
  plugins,
422
444
  sourceAst,
445
+ parseCache = true,
423
446
  astTransform,
424
- renderers = {},
447
+ renderers = EMPTY_RENDERERS,
425
448
  theme: userTheme,
426
449
  styles: nodeStyles,
427
450
  stylingStrategy = "opinionated",
@@ -438,31 +461,31 @@ export const Markdown: FC<MarkdownProps> = ({
438
461
  }) => {
439
462
  const parserOptionGfm = options?.gfm;
440
463
  const parserOptionMath = options?.math;
464
+ const parserOptionHtml = options?.html;
441
465
 
442
466
  /* eslint-disable react-hooks/refs -- Refs updated/read intentionally to avoid re-parsing on callback identity changes */
443
467
  const onErrorRef = useRef(onError);
444
468
  onErrorRef.current = onError;
445
469
 
446
- const pluginsRef = useRef(plugins);
447
- pluginsRef.current = plugins;
448
-
449
470
  const parseResult = useMemo(() => {
450
471
  try {
451
- const markdownToParse = applyBeforeParsePlugins(
452
- children,
453
- pluginsRef.current,
454
- onErrorRef.current,
455
- );
472
+ const sortedPlugins = sortPluginsByPriority(plugins);
473
+ const markdownToParse = sourceAst
474
+ ? children
475
+ : applyBeforeParsePlugins(children, sortedPlugins, onErrorRef.current);
456
476
  const parserOptions = normalizeParserOptions({
457
477
  gfm: parserOptionGfm,
458
478
  math: parserOptionMath,
479
+ html: parserOptionHtml,
459
480
  });
460
481
  let parsedAst = sourceAst
461
482
  ? cloneMarkdownNode(sourceAst)
462
- : getCachedParsedAst(markdownToParse, parserOptions);
483
+ : parseCache
484
+ ? getCachedParsedAst(markdownToParse, parserOptions)
485
+ : parseWithNativeParser(markdownToParse, parserOptions);
463
486
  parsedAst = applyAfterParsePlugins(
464
487
  parsedAst,
465
- pluginsRef.current,
488
+ sortedPlugins,
466
489
  onErrorRef.current,
467
490
  );
468
491
 
@@ -491,12 +514,27 @@ export const Markdown: FC<MarkdownProps> = ({
491
514
  ast: null,
492
515
  };
493
516
  }
494
- }, [children, parserOptionGfm, parserOptionMath, sourceAst, astTransform]);
517
+ }, [
518
+ children,
519
+ parserOptionGfm,
520
+ parserOptionMath,
521
+ parserOptionHtml,
522
+ sourceAst,
523
+ parseCache,
524
+ astTransform,
525
+ plugins,
526
+ ]);
495
527
  /* eslint-enable react-hooks/refs */
496
528
 
497
529
  useEffect(() => {
498
530
  onParsingInProgress?.();
499
- }, [children, parserOptionGfm, parserOptionMath, onParsingInProgress]);
531
+ }, [
532
+ children,
533
+ parserOptionGfm,
534
+ parserOptionMath,
535
+ parserOptionHtml,
536
+ onParsingInProgress,
537
+ ]);
500
538
 
501
539
  useEffect(() => {
502
540
  if (!parseResult.ast || !onParseComplete) return;
@@ -749,28 +787,24 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
749
787
  }
750
788
  }
751
789
 
752
- const nodeStyleOverride = nodeStyles?.[node.type] as
753
- | (ViewStyle & TextStyle)
754
- | undefined;
755
-
756
790
  switch (node.type) {
757
791
  case "document":
758
792
  return (
759
- <View style={[baseStyles.document, nodeStyleOverride]}>
793
+ <View style={[baseStyles.document, nodeStyles?.document]}>
760
794
  {renderChildren(node.children, false, false)}
761
795
  </View>
762
796
  );
763
797
 
764
798
  case "heading":
765
799
  return (
766
- <Heading level={node.level ?? 1} style={nodeStyleOverride}>
800
+ <Heading level={node.level ?? 1} style={nodeStyles?.heading}>
767
801
  {renderChildren(node.children, inListItem, true)}
768
802
  </Heading>
769
803
  );
770
804
 
771
805
  case "paragraph":
772
806
  return (
773
- <Paragraph inListItem={inListItem} style={nodeStyleOverride}>
807
+ <Paragraph inListItem={inListItem} style={nodeStyles?.paragraph}>
774
808
  {renderChildren(node.children, inListItem, false)}
775
809
  </Paragraph>
776
810
  );
@@ -780,33 +814,33 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
780
814
  return <Text>{node.content}</Text>;
781
815
  }
782
816
  return (
783
- <Text style={[baseStyles.text, nodeStyleOverride]}>{node.content}</Text>
817
+ <Text style={[baseStyles.text, nodeStyles?.text]}>{node.content}</Text>
784
818
  );
785
819
 
786
820
  case "bold":
787
821
  return (
788
- <Text style={[baseStyles.bold, nodeStyleOverride]}>
822
+ <Text style={[baseStyles.bold, nodeStyles?.bold]}>
789
823
  {renderChildren(node.children, inListItem, true)}
790
824
  </Text>
791
825
  );
792
826
 
793
827
  case "italic":
794
828
  return (
795
- <Text style={[baseStyles.italic, nodeStyleOverride]}>
829
+ <Text style={[baseStyles.italic, nodeStyles?.italic]}>
796
830
  {renderChildren(node.children, inListItem, true)}
797
831
  </Text>
798
832
  );
799
833
 
800
834
  case "strikethrough":
801
835
  return (
802
- <Text style={[baseStyles.strikethrough, nodeStyleOverride]}>
836
+ <Text style={[baseStyles.strikethrough, nodeStyles?.strikethrough]}>
803
837
  {renderChildren(node.children, inListItem, true)}
804
838
  </Text>
805
839
  );
806
840
 
807
841
  case "link":
808
842
  return (
809
- <Link href={node.href ?? ""} style={nodeStyleOverride}>
843
+ <Link href={node.href ?? ""} style={nodeStyles?.link}>
810
844
  {renderChildren(node.children, inListItem, true)}
811
845
  </Link>
812
846
  );
@@ -818,31 +852,33 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
818
852
  title={node.title}
819
853
  alt={node.alt}
820
854
  Renderer={NodeRenderer}
821
- style={nodeStyleOverride}
855
+ style={nodeStyles?.image}
822
856
  />
823
857
  );
824
858
 
825
859
  case "code_inline":
826
- return <InlineCode style={nodeStyleOverride}>{node.content}</InlineCode>;
860
+ return (
861
+ <InlineCode style={nodeStyles?.code_inline}>{node.content}</InlineCode>
862
+ );
827
863
 
828
864
  case "code_block":
829
865
  return (
830
866
  <CodeBlock
831
867
  language={node.language}
832
868
  content={getTextContent(node)}
833
- style={nodeStyleOverride}
869
+ style={nodeStyles?.code_block}
834
870
  />
835
871
  );
836
872
 
837
873
  case "blockquote":
838
874
  return (
839
- <Blockquote style={nodeStyleOverride}>
875
+ <Blockquote style={nodeStyles?.blockquote}>
840
876
  {renderChildren(node.children, inListItem, false)}
841
877
  </Blockquote>
842
878
  );
843
879
 
844
880
  case "horizontal_rule":
845
- return <HorizontalRule style={nodeStyleOverride} />;
881
+ return <HorizontalRule style={nodeStyles?.horizontal_rule} />;
846
882
 
847
883
  case "line_break":
848
884
  return <Text>{"\n"}</Text>;
@@ -854,12 +890,17 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
854
890
  let mathContent = getTextContent(node);
855
891
  if (!mathContent) return null;
856
892
  mathContent = mathContent.replace(/^\$+|\$+$/g, "").trim();
857
- return <MathInline content={mathContent} style={nodeStyleOverride} />;
893
+ return (
894
+ <MathInline content={mathContent} style={nodeStyles?.math_inline} />
895
+ );
858
896
  }
859
897
 
860
898
  case "math_block":
861
899
  return (
862
- <MathBlock content={getTextContent(node)} style={nodeStyleOverride} />
900
+ <MathBlock
901
+ content={getTextContent(node)}
902
+ style={nodeStyles?.math_block}
903
+ />
863
904
  );
864
905
 
865
906
  case "list":
@@ -868,7 +909,7 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
868
909
  ordered={node.ordered ?? false}
869
910
  start={node.start}
870
911
  depth={depth}
871
- style={nodeStyleOverride}
912
+ style={nodeStyles?.list}
872
913
  >
873
914
  {node.children?.map((child, index) => {
874
915
  if (child.type === "task_list_item") {
@@ -906,7 +947,10 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
906
947
 
907
948
  case "task_list_item":
908
949
  return (
909
- <TaskListItem checked={node.checked ?? false} style={nodeStyleOverride}>
950
+ <TaskListItem
951
+ checked={node.checked ?? false}
952
+ style={nodeStyles?.task_list_item}
953
+ >
910
954
  {renderChildren(node.children, true, false)}
911
955
  </TaskListItem>
912
956
  );
@@ -916,7 +960,7 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
916
960
  <TableRenderer
917
961
  node={node}
918
962
  Renderer={NodeRenderer}
919
- style={nodeStyleOverride}
963
+ style={nodeStyles?.table}
920
964
  />
921
965
  );
922
966
 
@@ -1,6 +1,8 @@
1
- import { useMemo, type FC, type ReactNode } from "react";
1
+ import type { FC, ReactNode } from "react";
2
2
  import { View, StyleSheet, type ViewStyle } from "react-native";
3
+ import { getCachedStyles } from "./style-cache";
3
4
  import { useMarkdownContext } from "../MarkdownContext";
5
+ import type { MarkdownTheme } from "../theme";
4
6
 
5
7
  type BlockquoteProps = {
6
8
  children: ReactNode;
@@ -9,22 +11,25 @@ type BlockquoteProps = {
9
11
 
10
12
  export const Blockquote: FC<BlockquoteProps> = ({ children, style }) => {
11
13
  const { theme } = useMarkdownContext();
12
- const styles = useMemo(
13
- () =>
14
- StyleSheet.create({
15
- blockquote: {
16
- borderLeftWidth: 4,
17
- borderLeftColor: theme.colors.blockquote,
18
- paddingLeft: theme.spacing.l,
19
- marginVertical: theme.spacing.m,
20
- backgroundColor: theme.colors.surfaceLight,
21
- paddingVertical: theme.spacing.m,
22
- paddingRight: theme.spacing.m,
23
- borderRadius: theme.borderRadius.s,
24
- },
25
- }),
26
- [theme],
27
- );
14
+ const styles = getCachedStyles(stylesCache, theme, createStyles);
28
15
 
29
16
  return <View style={[styles.blockquote, style]}>{children}</View>;
30
17
  };
18
+
19
+ type BlockquoteStyles = ReturnType<typeof createStyles>;
20
+
21
+ const stylesCache = new WeakMap<MarkdownTheme, BlockquoteStyles>();
22
+
23
+ const createStyles = (theme: MarkdownTheme) =>
24
+ StyleSheet.create({
25
+ blockquote: {
26
+ borderLeftWidth: 4,
27
+ borderLeftColor: theme.colors.blockquote,
28
+ paddingLeft: theme.spacing.l,
29
+ marginVertical: theme.spacing.m,
30
+ backgroundColor: theme.colors.surfaceLight,
31
+ paddingVertical: theme.spacing.m,
32
+ paddingRight: theme.spacing.m,
33
+ borderRadius: theme.borderRadius.s,
34
+ },
35
+ });
@@ -8,6 +8,7 @@ import {
8
8
  type ViewStyle,
9
9
  type TextStyle,
10
10
  } from "react-native";
11
+ import { getCachedStyles } from "./style-cache";
11
12
  import { getTextContent } from "../headless";
12
13
  import { useMarkdownContext } from "../MarkdownContext";
13
14
  import {
@@ -15,6 +16,7 @@ import {
15
16
  type HighlightedToken,
16
17
  } from "../utils/code-highlight";
17
18
  import type { MarkdownNode } from "../headless";
19
+ import type { MarkdownTheme } from "../theme";
18
20
 
19
21
  type CodeBlockProps = {
20
22
  language?: string;
@@ -40,41 +42,14 @@ export const CodeBlock: FC<CodeBlockProps> = ({
40
42
  : null;
41
43
 
42
44
  const displayContent = content ?? (node ? getTextContent(node) : "");
43
-
44
- const styles = useMemo(
45
+ const highlightedTokens = useMemo(
45
46
  () =>
46
- StyleSheet.create({
47
- codeBlock: {
48
- backgroundColor: theme.colors.codeBackground,
49
- borderRadius: theme.borderRadius.m,
50
- padding: theme.spacing.l,
51
- marginVertical: theme.spacing.m,
52
- borderWidth: 1,
53
- borderColor: theme.colors.border,
54
- },
55
- codeLanguage: {
56
- color: theme.colors.codeLanguage,
57
- fontSize: theme.fontSizes.xs,
58
- fontWeight: "600",
59
- marginBottom: theme.spacing.s,
60
- textTransform: "uppercase",
61
- letterSpacing: 0.5,
62
- fontFamily: theme.fontFamilies.mono,
63
- ...(Platform.OS === "android" && { includeFontPadding: false }),
64
- },
65
- codeBlockText: {
66
- fontFamily:
67
- theme.fontFamilies.mono ??
68
- Platform.select({ ios: "Courier", android: "monospace" }),
69
- fontSize: theme.fontSizes.s,
70
- color: theme.colors.text,
71
- lineHeight: theme.fontSizes.s * 1.5,
72
- ...(Platform.OS === "android" && { includeFontPadding: false }),
73
- },
74
- }),
75
- [theme],
47
+ highlighter && language ? highlighter(language, displayContent) : null,
48
+ [displayContent, highlighter, language],
76
49
  );
77
50
 
51
+ const styles = getCachedStyles(codeBlockStylesCache, theme, createCodeStyles);
52
+
78
53
  const showLanguage = theme.showCodeLanguage && language;
79
54
 
80
55
  return (
@@ -87,21 +62,18 @@ export const CodeBlock: FC<CodeBlockProps> = ({
87
62
  showsHorizontalScrollIndicator={false}
88
63
  bounces={false}
89
64
  >
90
- {highlighter && language ? (
65
+ {highlightedTokens ? (
91
66
  <Text style={styles.codeBlockText} selectable>
92
- {highlighter(language, displayContent).map(
93
- (token: HighlightedToken, i: number) => {
94
- const tokenColor =
95
- ctx.theme.colors.codeTokenColors?.[token.type];
96
- return tokenColor ? (
97
- <Text key={i} style={{ color: tokenColor }}>
98
- {token.text}
99
- </Text>
100
- ) : (
101
- <Text key={i}>{token.text}</Text>
102
- );
103
- },
104
- )}
67
+ {highlightedTokens.map((token: HighlightedToken, i: number) => {
68
+ const tokenColor = ctx.theme.colors.codeTokenColors?.[token.type];
69
+ return tokenColor ? (
70
+ <Text key={i} style={{ color: tokenColor }}>
71
+ {token.text}
72
+ </Text>
73
+ ) : (
74
+ <Text key={i}>{token.text}</Text>
75
+ );
76
+ })}
105
77
  </Text>
106
78
  ) : (
107
79
  <Text style={styles.codeBlockText} selectable>
@@ -131,23 +103,63 @@ export const InlineCode: FC<InlineCodeProps> = ({
131
103
  const displayContent =
132
104
  content ?? children ?? (node ? getTextContent(node) : "");
133
105
 
134
- const styles = useMemo(
135
- () =>
136
- StyleSheet.create({
137
- codeInline: {
138
- fontFamily:
139
- theme.fontFamilies.mono ??
140
- Platform.select({ ios: "Courier", android: "monospace" }),
141
- fontSize: theme.fontSizes.s,
142
- color: theme.colors.code,
143
- backgroundColor: theme.colors.codeBackground,
144
- paddingHorizontal: theme.spacing.xs,
145
- paddingVertical: 2,
146
- borderRadius: theme.borderRadius.s,
147
- ...(Platform.OS === "android" && { includeFontPadding: false }),
148
- },
149
- }),
150
- [theme],
106
+ const styles = getCachedStyles(
107
+ inlineCodeStylesCache,
108
+ theme,
109
+ createInlineStyles,
151
110
  );
152
111
  return <Text style={[styles.codeInline, style]}>{displayContent}</Text>;
153
112
  };
113
+
114
+ type CodeBlockStyles = ReturnType<typeof createCodeStyles>;
115
+ type InlineCodeStyles = ReturnType<typeof createInlineStyles>;
116
+
117
+ const codeBlockStylesCache = new WeakMap<MarkdownTheme, CodeBlockStyles>();
118
+ const inlineCodeStylesCache = new WeakMap<MarkdownTheme, InlineCodeStyles>();
119
+
120
+ const getMonoFontFamily = (theme: MarkdownTheme) =>
121
+ theme.fontFamilies.mono ??
122
+ Platform.select({ ios: "Courier", android: "monospace" });
123
+
124
+ const createCodeStyles = (theme: MarkdownTheme) =>
125
+ StyleSheet.create({
126
+ codeBlock: {
127
+ backgroundColor: theme.colors.codeBackground,
128
+ borderRadius: theme.borderRadius.m,
129
+ padding: theme.spacing.l,
130
+ marginVertical: theme.spacing.m,
131
+ borderWidth: 1,
132
+ borderColor: theme.colors.border,
133
+ },
134
+ codeLanguage: {
135
+ color: theme.colors.codeLanguage,
136
+ fontSize: theme.fontSizes.xs,
137
+ fontWeight: "600",
138
+ marginBottom: theme.spacing.s,
139
+ textTransform: "uppercase",
140
+ letterSpacing: 0.5,
141
+ fontFamily: theme.fontFamilies.mono,
142
+ ...(Platform.OS === "android" && { includeFontPadding: false }),
143
+ },
144
+ codeBlockText: {
145
+ fontFamily: getMonoFontFamily(theme),
146
+ fontSize: theme.fontSizes.s,
147
+ color: theme.colors.text,
148
+ lineHeight: theme.fontSizes.s * 1.5,
149
+ ...(Platform.OS === "android" && { includeFontPadding: false }),
150
+ },
151
+ });
152
+
153
+ const createInlineStyles = (theme: MarkdownTheme) =>
154
+ StyleSheet.create({
155
+ codeInline: {
156
+ fontFamily: getMonoFontFamily(theme),
157
+ fontSize: theme.fontSizes.s,
158
+ color: theme.colors.code,
159
+ backgroundColor: theme.colors.codeBackground,
160
+ paddingHorizontal: theme.spacing.xs,
161
+ paddingVertical: 2,
162
+ borderRadius: theme.borderRadius.s,
163
+ ...(Platform.OS === "android" && { includeFontPadding: false }),
164
+ },
165
+ });