react-native-nitro-markdown 0.5.2 → 0.5.4

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 (178) hide show
  1. package/README.md +311 -669
  2. package/android/CMakeLists.txt +8 -1
  3. package/android/build.gradle +9 -2
  4. package/android/consumer-rules.pro +31 -0
  5. package/android/gradle.properties +2 -0
  6. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +68 -22
  7. package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +6 -18
  8. package/cpp/bindings/HybridMarkdownParser.cpp +40 -12
  9. package/cpp/bindings/HybridMarkdownParser.hpp +4 -4
  10. package/cpp/bindings/HybridMarkdownSession.cpp +2 -0
  11. package/cpp/core/MD4CParser.cpp +147 -86
  12. package/cpp/core/MarkdownSessionCore.cpp +2 -0
  13. package/cpp/core/MarkdownTypes.hpp +1 -1
  14. package/ios/HybridMarkdownSession.swift +89 -46
  15. package/lib/commonjs/headless.js +34 -8
  16. package/lib/commonjs/headless.js.map +1 -1
  17. package/lib/commonjs/index.js +48 -38
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/markdown-stream.js +1 -1
  20. package/lib/commonjs/markdown-stream.js.map +1 -1
  21. package/lib/commonjs/markdown.js +57 -16
  22. package/lib/commonjs/markdown.js.map +1 -1
  23. package/lib/commonjs/renderers/blockquote.js +15 -13
  24. package/lib/commonjs/renderers/blockquote.js.map +1 -1
  25. package/lib/commonjs/renderers/code.js +58 -54
  26. package/lib/commonjs/renderers/code.js.map +1 -1
  27. package/lib/commonjs/renderers/heading.js +48 -46
  28. package/lib/commonjs/renderers/heading.js.map +1 -1
  29. package/lib/commonjs/renderers/horizontal-rule.js +10 -8
  30. package/lib/commonjs/renderers/horizontal-rule.js.map +1 -1
  31. package/lib/commonjs/renderers/image.js +18 -4
  32. package/lib/commonjs/renderers/image.js.map +1 -1
  33. package/lib/commonjs/renderers/link.js +7 -2
  34. package/lib/commonjs/renderers/link.js.map +1 -1
  35. package/lib/commonjs/renderers/list.js +75 -68
  36. package/lib/commonjs/renderers/list.js.map +1 -1
  37. package/lib/commonjs/renderers/math.js +8 -5
  38. package/lib/commonjs/renderers/math.js.map +1 -1
  39. package/lib/commonjs/renderers/paragraph.js +15 -13
  40. package/lib/commonjs/renderers/paragraph.js.map +1 -1
  41. package/lib/commonjs/renderers/style-cache.js +14 -0
  42. package/lib/commonjs/renderers/style-cache.js.map +1 -0
  43. package/lib/commonjs/renderers/table/cell-content.js +1 -1
  44. package/lib/commonjs/renderers/table/cell-content.js.map +1 -1
  45. package/lib/commonjs/renderers/table/index.js +17 -6
  46. package/lib/commonjs/renderers/table/index.js.map +1 -1
  47. package/lib/commonjs/theme.js +7 -7
  48. package/lib/commonjs/theme.js.map +1 -1
  49. package/lib/commonjs/utils/code-highlight.js +24 -25
  50. package/lib/commonjs/utils/code-highlight.js.map +1 -1
  51. package/lib/module/headless.js +35 -7
  52. package/lib/module/headless.js.map +1 -1
  53. package/lib/module/index.js +1 -1
  54. package/lib/module/index.js.map +1 -1
  55. package/lib/module/markdown-stream.js +1 -1
  56. package/lib/module/markdown-stream.js.map +1 -1
  57. package/lib/module/markdown.js +58 -17
  58. package/lib/module/markdown.js.map +1 -1
  59. package/lib/module/renderers/blockquote.js +15 -13
  60. package/lib/module/renderers/blockquote.js.map +1 -1
  61. package/lib/module/renderers/code.js +58 -54
  62. package/lib/module/renderers/code.js.map +1 -1
  63. package/lib/module/renderers/heading.js +48 -46
  64. package/lib/module/renderers/heading.js.map +1 -1
  65. package/lib/module/renderers/horizontal-rule.js +10 -8
  66. package/lib/module/renderers/horizontal-rule.js.map +1 -1
  67. package/lib/module/renderers/image.js +19 -5
  68. package/lib/module/renderers/image.js.map +1 -1
  69. package/lib/module/renderers/link.js +7 -2
  70. package/lib/module/renderers/link.js.map +1 -1
  71. package/lib/module/renderers/list.js +75 -68
  72. package/lib/module/renderers/list.js.map +1 -1
  73. package/lib/module/renderers/math.js +8 -5
  74. package/lib/module/renderers/math.js.map +1 -1
  75. package/lib/module/renderers/paragraph.js +15 -13
  76. package/lib/module/renderers/paragraph.js.map +1 -1
  77. package/lib/module/renderers/style-cache.js +10 -0
  78. package/lib/module/renderers/style-cache.js.map +1 -0
  79. package/lib/module/renderers/table/cell-content.js +1 -1
  80. package/lib/module/renderers/table/cell-content.js.map +1 -1
  81. package/lib/module/renderers/table/index.js +17 -6
  82. package/lib/module/renderers/table/index.js.map +1 -1
  83. package/lib/module/theme.js +7 -7
  84. package/lib/module/theme.js.map +1 -1
  85. package/lib/module/utils/code-highlight.js +24 -25
  86. package/lib/module/utils/code-highlight.js.map +1 -1
  87. package/lib/typescript/commonjs/Markdown.nitro.d.ts +1 -0
  88. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/headless.d.ts +10 -2
  90. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/index.d.ts +3 -2
  92. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/markdown.d.ts +8 -3
  95. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/renderers/blockquote.d.ts +1 -1
  97. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/renderers/heading.d.ts +1 -1
  100. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +1 -1
  102. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/renderers/list.d.ts +1 -1
  106. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/renderers/math.d.ts +1 -1
  108. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/renderers/paragraph.d.ts +1 -1
  110. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/renderers/style-cache.d.ts +3 -0
  112. package/lib/typescript/commonjs/renderers/style-cache.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +4 -3
  114. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/utils/code-highlight.d.ts +1 -1
  118. package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -1
  119. package/lib/typescript/module/Markdown.nitro.d.ts +1 -0
  120. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  121. package/lib/typescript/module/headless.d.ts +10 -2
  122. package/lib/typescript/module/headless.d.ts.map +1 -1
  123. package/lib/typescript/module/index.d.ts +3 -2
  124. package/lib/typescript/module/index.d.ts.map +1 -1
  125. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  126. package/lib/typescript/module/markdown.d.ts +8 -3
  127. package/lib/typescript/module/markdown.d.ts.map +1 -1
  128. package/lib/typescript/module/renderers/blockquote.d.ts +1 -1
  129. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  130. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  131. package/lib/typescript/module/renderers/heading.d.ts +1 -1
  132. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  133. package/lib/typescript/module/renderers/horizontal-rule.d.ts +1 -1
  134. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  135. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  136. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  137. package/lib/typescript/module/renderers/list.d.ts +1 -1
  138. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  139. package/lib/typescript/module/renderers/math.d.ts +1 -1
  140. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  141. package/lib/typescript/module/renderers/paragraph.d.ts +1 -1
  142. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  143. package/lib/typescript/module/renderers/style-cache.d.ts +3 -0
  144. package/lib/typescript/module/renderers/style-cache.d.ts.map +1 -0
  145. package/lib/typescript/module/renderers/table/cell-content.d.ts +4 -3
  146. package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -1
  147. package/lib/typescript/module/renderers/table/index.d.ts.map +1 -1
  148. package/lib/typescript/module/theme.d.ts.map +1 -1
  149. package/lib/typescript/module/utils/code-highlight.d.ts +1 -1
  150. package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -1
  151. package/nitro.json +12 -3
  152. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -2
  153. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  154. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
  155. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +2 -2
  156. package/nitrogen/generated/ios/NitroMarkdown+autolinking.rb +2 -0
  157. package/nitrogen/generated/shared/c++/ParserOptions.hpp +6 -2
  158. package/package.json +8 -6
  159. package/react-native-nitro-markdown.podspec +3 -0
  160. package/src/Markdown.nitro.ts +1 -0
  161. package/src/headless.ts +58 -8
  162. package/src/index.ts +16 -2
  163. package/src/markdown-stream.tsx +1 -0
  164. package/src/markdown.tsx +108 -23
  165. package/src/renderers/blockquote.tsx +22 -17
  166. package/src/renderers/code.tsx +76 -57
  167. package/src/renderers/heading.tsx +60 -54
  168. package/src/renderers/horizontal-rule.tsx +17 -12
  169. package/src/renderers/image.tsx +24 -5
  170. package/src/renderers/link.tsx +8 -2
  171. package/src/renderers/list.tsx +93 -74
  172. package/src/renderers/math.tsx +14 -5
  173. package/src/renderers/paragraph.tsx +22 -17
  174. package/src/renderers/style-cache.ts +14 -0
  175. package/src/renderers/table/cell-content.tsx +15 -4
  176. package/src/renderers/table/index.tsx +30 -13
  177. package/src/theme.ts +34 -14
  178. package/src/utils/code-highlight.ts +133 -44
package/src/markdown.tsx CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  useCallback,
4
4
  useEffect,
5
5
  useMemo,
6
+ useRef,
6
7
  type FC,
7
8
  Fragment,
8
9
  type ReactElement,
@@ -56,10 +57,54 @@ import {
56
57
  } from "./theme";
57
58
  import type { CodeHighlighter } from "./utils/code-highlight";
58
59
 
60
+ function hashString(str: string): number {
61
+ let hash = 0;
62
+ for (let i = 0; i < str.length; i++) {
63
+ const char = str.charCodeAt(i);
64
+ hash = (hash << 5) - hash + char;
65
+ hash = hash & hash; // Convert to 32bit int
66
+ }
67
+ return hash;
68
+ }
69
+
70
+ const ERROR_PHASE = {
71
+ PARSE: "parse",
72
+ BEFORE_PLUGIN: "before-plugin",
73
+ AFTER_PLUGIN: "after-plugin",
74
+ } as const;
75
+
76
+ /**
77
+ * Safely invoke the onError callback, preventing callback exceptions from
78
+ * propagating and breaking the render cycle.
79
+ */
80
+ function safeOnError<P extends string>(
81
+ onError: ((error: Error, phase: P, pluginName?: string) => void) | undefined,
82
+ error: unknown,
83
+ phase: P,
84
+ pluginName?: string,
85
+ ): void {
86
+ try {
87
+ onError?.(
88
+ error instanceof Error ? error : new Error(String(error)),
89
+ phase,
90
+ pluginName,
91
+ );
92
+ } catch (callbackError) {
93
+ if (__DEV__) {
94
+ // eslint-disable-next-line no-console
95
+ console.warn(
96
+ "[NitroMarkdown] onError callback threw an exception:",
97
+ callbackError,
98
+ );
99
+ }
100
+ }
101
+ }
102
+
59
103
  const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
60
104
  const parseAstCache = new Map<string, MarkdownNode>();
61
105
  const MAX_PARSE_CACHE_ENTRIES = 32;
62
106
  const MAX_CACHEABLE_TEXT_LENGTH = 24_000;
107
+ const EMPTY_RENDERERS: CustomRenderers = {};
63
108
 
64
109
  export type AstTransform = (ast: MarkdownNode) => MarkdownNode;
65
110
  export type MarkdownVirtualizationOptions = Pick<
@@ -122,12 +167,14 @@ const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
122
167
  };
123
168
 
124
169
  const getParserOptionsKey = (options?: ParserOptions): string => {
125
- if (!options) return "gfm:default|math:default";
170
+ if (!options) return "gfm:default|math:default|html:default";
126
171
 
127
172
  const gfm = options.gfm === undefined ? "default" : options.gfm ? "1" : "0";
128
173
  const math =
129
174
  options.math === undefined ? "default" : options.math ? "1" : "0";
130
- return `gfm:${gfm}|math:${math}`;
175
+ const html =
176
+ options.html === undefined ? "default" : options.html ? "1" : "0";
177
+ return `gfm:${gfm}|math:${math}|html:${html}`;
131
178
  };
132
179
 
133
180
  const normalizeParserOptions = (
@@ -137,14 +184,16 @@ const normalizeParserOptions = (
137
184
 
138
185
  const gfm = options.gfm;
139
186
  const math = options.math;
187
+ const html = options.html;
140
188
 
141
- if (gfm === undefined && math === undefined) {
189
+ if (gfm === undefined && math === undefined && html === undefined) {
142
190
  return undefined;
143
191
  }
144
192
 
145
193
  return {
146
194
  gfm,
147
195
  math,
196
+ html,
148
197
  };
149
198
  };
150
199
 
@@ -166,7 +215,7 @@ const getCachedParsedAst = (
166
215
  return parseWithNativeParser(text, options);
167
216
  }
168
217
 
169
- const cacheKey = `${getParserOptionsKey(options)}|${text}`;
218
+ const cacheKey = `${getParserOptionsKey(options)}|${text.length}|${hashString(text)}`;
170
219
  const cachedNode = parseAstCache.get(cacheKey);
171
220
  if (cachedNode) {
172
221
  parseAstCache.delete(cacheKey);
@@ -189,13 +238,15 @@ const getCachedParsedAst = (
189
238
  const applyBeforeParsePlugins = (
190
239
  markdown: string,
191
240
  plugins?: MarkdownPlugin[],
192
- onError?: (error: Error, phase: 'before-plugin', pluginName?: string) => void,
241
+ onError?: (error: Error, phase: "before-plugin", pluginName?: string) => void,
193
242
  ): string => {
194
243
  if (!plugins || plugins.length === 0) {
195
244
  return markdown;
196
245
  }
197
246
 
198
- const sorted = [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
247
+ const sorted = [...plugins].sort(
248
+ (a, b) => (b.priority ?? 0) - (a.priority ?? 0),
249
+ );
199
250
  let nextMarkdown = markdown;
200
251
  for (const plugin of sorted) {
201
252
  if (!plugin.beforeParse) continue;
@@ -211,7 +262,7 @@ const applyBeforeParsePlugins = (
211
262
  `[react-native-nitro-markdown] plugin beforeParse${pluginLabel} threw; using previous markdown.`,
212
263
  error,
213
264
  );
214
- onError?.(error instanceof Error ? error : new Error(String(error)), 'before-plugin', plugin.name);
265
+ safeOnError(onError, error, ERROR_PHASE.BEFORE_PLUGIN, plugin.name);
215
266
  }
216
267
  }
217
268
 
@@ -221,13 +272,15 @@ const applyBeforeParsePlugins = (
221
272
  const applyAfterParsePlugins = (
222
273
  ast: MarkdownNode,
223
274
  plugins?: MarkdownPlugin[],
224
- onError?: (error: Error, phase: 'after-plugin', pluginName?: string) => void,
275
+ onError?: (error: Error, phase: "after-plugin", pluginName?: string) => void,
225
276
  ): MarkdownNode => {
226
277
  if (!plugins || plugins.length === 0) {
227
278
  return ast;
228
279
  }
229
280
 
230
- const sorted = [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
281
+ const sorted = [...plugins].sort(
282
+ (a, b) => (b.priority ?? 0) - (a.priority ?? 0),
283
+ );
231
284
  let nextAst = ast;
232
285
  for (const plugin of sorted) {
233
286
  if (!plugin.afterParse) continue;
@@ -243,7 +296,7 @@ const applyAfterParsePlugins = (
243
296
  `[react-native-nitro-markdown] plugin afterParse${pluginLabel} threw; using previous AST.`,
244
297
  error,
245
298
  );
246
- onError?.(error instanceof Error ? error : new Error(String(error)), 'after-plugin', plugin.name);
299
+ safeOnError(onError, error, ERROR_PHASE.AFTER_PLUGIN, plugin.name);
247
300
  }
248
301
  }
249
302
 
@@ -256,7 +309,7 @@ export type MarkdownProps = {
256
309
  */
257
310
  children: string;
258
311
  /**
259
- * Parser options to enable GFM or Math support.
312
+ * Parser options to enable GFM, math, or raw HTML AST support.
260
313
  */
261
314
  options?: ParserOptions;
262
315
  /**
@@ -274,7 +327,12 @@ export type MarkdownProps = {
274
327
  */
275
328
  astTransform?: AstTransform;
276
329
  /**
277
- * Callback fired when parsing begins.
330
+ * Callback fired after the current parse cycle completes and the component
331
+ * has re-rendered with new content. Because the native parser runs
332
+ * synchronously inside `useMemo`, there is no observable "in-progress"
333
+ * window — this callback fires in the `useEffect` commit phase, after the
334
+ * new AST is already rendered. Use `onParseComplete` for post-parse
335
+ * inspection of results.
278
336
  */
279
337
  onParsingInProgress?: () => void;
280
338
  /**
@@ -291,7 +349,11 @@ export type MarkdownProps = {
291
349
  * @param phase - Where the error occurred.
292
350
  * @param pluginName - The plugin name, if applicable.
293
351
  */
294
- onError?: (error: Error, phase: 'parse' | 'before-plugin' | 'after-plugin', pluginName?: string) => void;
352
+ onError?: (
353
+ error: Error,
354
+ phase: "parse" | "before-plugin" | "after-plugin",
355
+ pluginName?: string,
356
+ ) => void;
295
357
  /**
296
358
  * Custom renderers for specific markdown node types.
297
359
  * Each renderer receives { node, children, Renderer } plus type-specific props.
@@ -364,7 +426,7 @@ export const Markdown: FC<MarkdownProps> = ({
364
426
  plugins,
365
427
  sourceAst,
366
428
  astTransform,
367
- renderers = {},
429
+ renderers = EMPTY_RENDERERS,
368
430
  theme: userTheme,
369
431
  styles: nodeStyles,
370
432
  stylingStrategy = "opinionated",
@@ -381,18 +443,32 @@ export const Markdown: FC<MarkdownProps> = ({
381
443
  }) => {
382
444
  const parserOptionGfm = options?.gfm;
383
445
  const parserOptionMath = options?.math;
446
+ const parserOptionHtml = options?.html;
447
+
448
+ /* eslint-disable react-hooks/refs -- Refs updated/read intentionally to avoid re-parsing on callback identity changes */
449
+ const onErrorRef = useRef(onError);
450
+ onErrorRef.current = onError;
384
451
 
385
452
  const parseResult = useMemo(() => {
386
453
  try {
387
- const markdownToParse = applyBeforeParsePlugins(children, plugins, onError ? (e, phase, name) => onError(e, phase, name) : undefined);
454
+ const markdownToParse = applyBeforeParsePlugins(
455
+ children,
456
+ plugins,
457
+ onErrorRef.current,
458
+ );
388
459
  const parserOptions = normalizeParserOptions({
389
460
  gfm: parserOptionGfm,
390
461
  math: parserOptionMath,
462
+ html: parserOptionHtml,
391
463
  });
392
464
  let parsedAst = sourceAst
393
465
  ? cloneMarkdownNode(sourceAst)
394
466
  : getCachedParsedAst(markdownToParse, parserOptions);
395
- parsedAst = applyAfterParsePlugins(parsedAst, plugins, onError ? (e, phase, name) => onError(e, phase, name) : undefined);
467
+ parsedAst = applyAfterParsePlugins(
468
+ parsedAst,
469
+ plugins,
470
+ onErrorRef.current,
471
+ );
396
472
 
397
473
  let ast = parsedAst;
398
474
  if (astTransform) {
@@ -414,7 +490,7 @@ export const Markdown: FC<MarkdownProps> = ({
414
490
  ast,
415
491
  };
416
492
  } catch (parseError) {
417
- onError?.(parseError instanceof Error ? parseError : new Error(String(parseError)), 'parse');
493
+ safeOnError(onErrorRef.current, parseError, ERROR_PHASE.PARSE);
418
494
  return {
419
495
  ast: null,
420
496
  };
@@ -423,11 +499,12 @@ export const Markdown: FC<MarkdownProps> = ({
423
499
  children,
424
500
  parserOptionGfm,
425
501
  parserOptionMath,
426
- plugins,
502
+ parserOptionHtml,
427
503
  sourceAst,
428
504
  astTransform,
429
- onError,
505
+ plugins,
430
506
  ]);
507
+ /* eslint-enable react-hooks/refs */
431
508
 
432
509
  useEffect(() => {
433
510
  onParsingInProgress?.();
@@ -435,7 +512,7 @@ export const Markdown: FC<MarkdownProps> = ({
435
512
  children,
436
513
  parserOptionGfm,
437
514
  parserOptionMath,
438
- plugins,
515
+ parserOptionHtml,
439
516
  onParsingInProgress,
440
517
  ]);
441
518
 
@@ -468,7 +545,15 @@ export const Markdown: FC<MarkdownProps> = ({
468
545
  tableOptions,
469
546
  highlightCode,
470
547
  }),
471
- [renderers, theme, nodeStyles, stylingStrategy, onLinkPress, tableOptions, highlightCode],
548
+ [
549
+ renderers,
550
+ theme,
551
+ nodeStyles,
552
+ stylingStrategy,
553
+ onLinkPress,
554
+ tableOptions,
555
+ highlightCode,
556
+ ],
472
557
  );
473
558
 
474
559
  const topLevelBlocks =
@@ -887,13 +972,13 @@ const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
887
972
  const createBaseStyles = (theme: MarkdownTheme) =>
888
973
  StyleSheet.create({
889
974
  container: {
890
- flex: 1,
975
+ flexShrink: 1,
891
976
  },
892
977
  virtualizedList: {
893
978
  flex: 1,
894
979
  },
895
980
  document: {
896
- flex: 1,
981
+ flexShrink: 1,
897
982
  },
898
983
  errorText: {
899
984
  color: "#f87171",
@@ -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,10 +8,15 @@ 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";
14
+ import {
15
+ defaultHighlighter,
16
+ type HighlightedToken,
17
+ } from "../utils/code-highlight";
13
18
  import type { MarkdownNode } from "../headless";
14
- import { defaultHighlighter, type HighlightedToken } from "../utils/code-highlight";
19
+ import type { MarkdownTheme } from "../theme";
15
20
 
16
21
  type CodeBlockProps = {
17
22
  language?: string;
@@ -29,48 +34,22 @@ export const CodeBlock: FC<CodeBlockProps> = ({
29
34
  const ctx = useMarkdownContext();
30
35
  const { theme } = ctx;
31
36
 
32
- const highlighter = ctx.highlightCode === true
33
- ? defaultHighlighter
34
- : typeof ctx.highlightCode === 'function'
35
- ? ctx.highlightCode
36
- : null;
37
+ const highlighter =
38
+ ctx.highlightCode === true
39
+ ? defaultHighlighter
40
+ : typeof ctx.highlightCode === "function"
41
+ ? ctx.highlightCode
42
+ : null;
37
43
 
38
44
  const displayContent = content ?? (node ? getTextContent(node) : "");
39
-
40
- const styles = useMemo(
45
+ const highlightedTokens = useMemo(
41
46
  () =>
42
- StyleSheet.create({
43
- codeBlock: {
44
- backgroundColor: theme.colors.codeBackground,
45
- borderRadius: theme.borderRadius.m,
46
- padding: theme.spacing.l,
47
- marginVertical: theme.spacing.m,
48
- borderWidth: 1,
49
- borderColor: theme.colors.border,
50
- },
51
- codeLanguage: {
52
- color: theme.colors.codeLanguage,
53
- fontSize: theme.fontSizes.xs,
54
- fontWeight: "600",
55
- marginBottom: theme.spacing.s,
56
- textTransform: "uppercase",
57
- letterSpacing: 0.5,
58
- fontFamily: theme.fontFamilies.mono,
59
- ...(Platform.OS === "android" && { includeFontPadding: false }),
60
- },
61
- codeBlockText: {
62
- fontFamily:
63
- theme.fontFamilies.mono ??
64
- Platform.select({ ios: "Courier", android: "monospace" }),
65
- fontSize: theme.fontSizes.s,
66
- color: theme.colors.text,
67
- lineHeight: theme.fontSizes.s * 1.5,
68
- ...(Platform.OS === "android" && { includeFontPadding: false }),
69
- },
70
- }),
71
- [theme],
47
+ highlighter && language ? highlighter(language, displayContent) : null,
48
+ [displayContent, highlighter, language],
72
49
  );
73
50
 
51
+ const styles = getCachedStyles(codeBlockStylesCache, theme, createCodeStyles);
52
+
74
53
  const showLanguage = theme.showCodeLanguage && language;
75
54
 
76
55
  return (
@@ -83,9 +62,9 @@ export const CodeBlock: FC<CodeBlockProps> = ({
83
62
  showsHorizontalScrollIndicator={false}
84
63
  bounces={false}
85
64
  >
86
- {highlighter && language ? (
65
+ {highlightedTokens ? (
87
66
  <Text style={styles.codeBlockText} selectable>
88
- {highlighter(language, displayContent).map((token: HighlightedToken, i: number) => {
67
+ {highlightedTokens.map((token: HighlightedToken, i: number) => {
89
68
  const tokenColor = ctx.theme.colors.codeTokenColors?.[token.type];
90
69
  return tokenColor ? (
91
70
  <Text key={i} style={{ color: tokenColor }}>
@@ -124,23 +103,63 @@ export const InlineCode: FC<InlineCodeProps> = ({
124
103
  const displayContent =
125
104
  content ?? children ?? (node ? getTextContent(node) : "");
126
105
 
127
- const styles = useMemo(
128
- () =>
129
- StyleSheet.create({
130
- codeInline: {
131
- fontFamily:
132
- theme.fontFamilies.mono ??
133
- Platform.select({ ios: "Courier", android: "monospace" }),
134
- fontSize: theme.fontSizes.s,
135
- color: theme.colors.code,
136
- backgroundColor: theme.colors.codeBackground,
137
- paddingHorizontal: theme.spacing.xs,
138
- paddingVertical: 2,
139
- borderRadius: theme.borderRadius.s,
140
- ...(Platform.OS === "android" && { includeFontPadding: false }),
141
- },
142
- }),
143
- [theme],
106
+ const styles = getCachedStyles(
107
+ inlineCodeStylesCache,
108
+ theme,
109
+ createInlineStyles,
144
110
  );
145
111
  return <Text style={[styles.codeInline, style]}>{displayContent}</Text>;
146
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
+ });
@@ -1,6 +1,8 @@
1
- import { useMemo, type FC, type ReactNode } from "react";
1
+ import type { FC, ReactNode } from "react";
2
2
  import { Text, StyleSheet, Platform, type TextStyle } 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 HeadingProps = {
6
8
  level: number;
@@ -20,59 +22,7 @@ const ANDROID_SYSTEM_FONTS = new Set([
20
22
 
21
23
  export const Heading: FC<HeadingProps> = ({ level, children, style }) => {
22
24
  const { theme } = useMarkdownContext();
23
- const headingWeight =
24
- theme.headingWeight ??
25
- (Platform.OS === "android" &&
26
- theme.fontFamilies.heading &&
27
- !ANDROID_SYSTEM_FONTS.has(theme.fontFamilies.heading)
28
- ? "normal"
29
- : "700");
30
- const styles = useMemo(
31
- () =>
32
- StyleSheet.create({
33
- heading: {
34
- color: theme.colors.heading,
35
- fontWeight: headingWeight,
36
- marginTop: theme.spacing.xl,
37
- marginBottom: theme.spacing.m,
38
- fontFamily: theme.fontFamilies.heading,
39
- letterSpacing: -0.2,
40
- ...(Platform.OS === "android" && { includeFontPadding: false }),
41
- },
42
- h1: {
43
- fontSize: theme.fontSizes.h1,
44
- lineHeight: theme.fontSizes.h1 * 1.3,
45
- borderBottomWidth: 1,
46
- borderBottomColor: theme.colors.border,
47
- paddingBottom: theme.spacing.s,
48
- letterSpacing: -0.6,
49
- },
50
- h2: {
51
- fontSize: theme.fontSizes.h2,
52
- lineHeight: theme.fontSizes.h2 * 1.3,
53
- letterSpacing: -0.4,
54
- },
55
- h3: {
56
- fontSize: theme.fontSizes.h3,
57
- lineHeight: theme.fontSizes.h3 * 1.3,
58
- letterSpacing: -0.2,
59
- },
60
- h4: {
61
- fontSize: theme.fontSizes.h4,
62
- lineHeight: theme.fontSizes.h4 * 1.3,
63
- },
64
- h5: {
65
- fontSize: theme.fontSizes.h5,
66
- lineHeight: theme.fontSizes.h5 * 1.3,
67
- },
68
- h6: {
69
- fontSize: theme.fontSizes.h6,
70
- lineHeight: theme.fontSizes.h6 * 1.3,
71
- color: theme.colors.textMuted,
72
- },
73
- }),
74
- [headingWeight, theme],
75
- );
25
+ const styles = getCachedStyles(stylesCache, theme, createStyles);
76
26
 
77
27
  const headingStyles = [
78
28
  styles.heading,
@@ -86,3 +36,59 @@ export const Heading: FC<HeadingProps> = ({ level, children, style }) => {
86
36
 
87
37
  return <Text style={[...headingStyles, style]}>{children}</Text>;
88
38
  };
39
+
40
+ type HeadingStyles = ReturnType<typeof createStyles>;
41
+
42
+ const stylesCache = new WeakMap<MarkdownTheme, HeadingStyles>();
43
+
44
+ const getHeadingWeight = (theme: MarkdownTheme) =>
45
+ theme.headingWeight ??
46
+ (Platform.OS === "android" &&
47
+ theme.fontFamilies.heading &&
48
+ !ANDROID_SYSTEM_FONTS.has(theme.fontFamilies.heading)
49
+ ? "normal"
50
+ : "700");
51
+
52
+ const createStyles = (theme: MarkdownTheme) =>
53
+ StyleSheet.create({
54
+ heading: {
55
+ color: theme.colors.heading,
56
+ fontWeight: getHeadingWeight(theme),
57
+ marginTop: theme.spacing.xl,
58
+ marginBottom: theme.spacing.m,
59
+ fontFamily: theme.fontFamilies.heading,
60
+ letterSpacing: -0.2,
61
+ ...(Platform.OS === "android" && { includeFontPadding: false }),
62
+ },
63
+ h1: {
64
+ fontSize: theme.fontSizes.h1,
65
+ lineHeight: theme.fontSizes.h1 * 1.3,
66
+ borderBottomWidth: 1,
67
+ borderBottomColor: theme.colors.border,
68
+ paddingBottom: theme.spacing.s,
69
+ letterSpacing: -0.6,
70
+ },
71
+ h2: {
72
+ fontSize: theme.fontSizes.h2,
73
+ lineHeight: theme.fontSizes.h2 * 1.3,
74
+ letterSpacing: -0.4,
75
+ },
76
+ h3: {
77
+ fontSize: theme.fontSizes.h3,
78
+ lineHeight: theme.fontSizes.h3 * 1.3,
79
+ letterSpacing: -0.2,
80
+ },
81
+ h4: {
82
+ fontSize: theme.fontSizes.h4,
83
+ lineHeight: theme.fontSizes.h4 * 1.3,
84
+ },
85
+ h5: {
86
+ fontSize: theme.fontSizes.h5,
87
+ lineHeight: theme.fontSizes.h5 * 1.3,
88
+ },
89
+ h6: {
90
+ fontSize: theme.fontSizes.h6,
91
+ lineHeight: theme.fontSizes.h6 * 1.3,
92
+ color: theme.colors.textMuted,
93
+ },
94
+ });
@@ -1,6 +1,8 @@
1
- import { useMemo, type FC } from "react";
1
+ import type { FC } 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 HorizontalRuleProps = {
6
8
  style?: ViewStyle;
@@ -8,16 +10,19 @@ type HorizontalRuleProps = {
8
10
 
9
11
  export const HorizontalRule: FC<HorizontalRuleProps> = ({ style }) => {
10
12
  const { theme } = useMarkdownContext();
11
- const styles = useMemo(
12
- () =>
13
- StyleSheet.create({
14
- horizontalRule: {
15
- height: 1,
16
- backgroundColor: theme.colors.border,
17
- marginVertical: theme.spacing.xl,
18
- },
19
- }),
20
- [theme],
21
- );
13
+ const styles = getCachedStyles(stylesCache, theme, createStyles);
22
14
  return <View style={[styles.horizontalRule, style]} />;
23
15
  };
16
+
17
+ type HorizontalRuleStyles = ReturnType<typeof createStyles>;
18
+
19
+ const stylesCache = new WeakMap<MarkdownTheme, HorizontalRuleStyles>();
20
+
21
+ const createStyles = (theme: MarkdownTheme) =>
22
+ StyleSheet.create({
23
+ horizontalRule: {
24
+ height: 1,
25
+ backgroundColor: theme.colors.border,
26
+ marginVertical: theme.spacing.xl,
27
+ },
28
+ });