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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  useState,
3
- useLayoutEffect,
3
+ useEffect,
4
4
  useMemo,
5
5
  type ReactNode,
6
6
  type FC,
@@ -110,26 +110,45 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
110
110
  [theme, aspectRatio],
111
111
  );
112
112
 
113
- useLayoutEffect(() => {
114
- // Fast path for consistent aspect ratios if checking picsum
113
+ useEffect(() => {
114
+ let isMounted = true;
115
+ setLoading(true);
116
+ setError(false);
117
+ setAspectRatio(undefined);
118
+
115
119
  const picsumMatch = url.match(/picsum\.photos\/.*\/(\d+)\/(\d+)/);
116
120
  if (picsumMatch) {
117
121
  const w = parseInt(picsumMatch[1], 10);
118
122
  const h = parseInt(picsumMatch[2], 10);
119
123
  if (!isNaN(w) && !isNaN(h) && h !== 0) {
120
124
  setAspectRatio(w / h);
125
+ return () => {
126
+ isMounted = false;
127
+ };
121
128
  }
122
129
  }
123
130
 
124
131
  RNImage.getSize(
125
132
  url,
126
133
  (width, height) => {
127
- if (width > 0 && height > 0) {
134
+ if (isMounted && width > 0 && height > 0) {
128
135
  setAspectRatio(width / height);
129
136
  }
130
137
  },
131
- () => {},
138
+ (error) => {
139
+ if (__DEV__) {
140
+ // eslint-disable-next-line no-console
141
+ console.warn(
142
+ "[NitroMarkdown] Failed to get image dimensions:",
143
+ error,
144
+ );
145
+ }
146
+ },
132
147
  );
148
+
149
+ return () => {
150
+ isMounted = false;
151
+ };
133
152
  }, [url]);
134
153
 
135
154
  const altContent = useMemo(() => {
@@ -39,7 +39,8 @@ export const Link: FC<LinkProps> = ({ href, children, style }) => {
39
39
  if (!normalizedHref) return;
40
40
 
41
41
  try {
42
- const shouldOpen = (await onLinkPress?.(normalizedHref)) !== false;
42
+ const shouldOpen =
43
+ (await Promise.resolve(onLinkPress?.(normalizedHref))) !== false;
43
44
  if (!shouldOpen) return;
44
45
 
45
46
  const allowedExternalHref = getAllowedExternalHref(normalizedHref);
@@ -49,7 +50,12 @@ export const Link: FC<LinkProps> = ({ href, children, style }) => {
49
50
  if (!canOpen) return;
50
51
 
51
52
  await Linking.openURL(allowedExternalHref);
52
- } catch {}
53
+ } catch (error) {
54
+ if (__DEV__) {
55
+ // eslint-disable-next-line no-console
56
+ console.warn("[NitroMarkdown] Link press handler failed:", error);
57
+ }
58
+ }
53
59
  };
54
60
 
55
61
  return (
@@ -1,6 +1,8 @@
1
- import { useMemo, type FC, type ReactNode } from "react";
1
+ import type { FC, ReactNode } from "react";
2
2
  import { View, Text, StyleSheet, Platform, 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 ListProps = {
6
8
  ordered: boolean;
@@ -10,21 +12,10 @@ type ListProps = {
10
12
  style?: ViewStyle;
11
13
  };
12
14
 
15
+ // ordered/start stay on List for renderer API parity; markers render in ListItem.
13
16
  export const List: FC<ListProps> = ({ depth, children, style }) => {
14
17
  const { theme } = useMarkdownContext();
15
- const styles = useMemo(
16
- () =>
17
- StyleSheet.create({
18
- list: {
19
- marginBottom: theme.spacing.m,
20
- },
21
- listNested: {
22
- marginLeft: theme.spacing.s,
23
- marginBottom: 0,
24
- },
25
- }),
26
- [theme],
27
- );
18
+ const styles = getCachedStyles(listStylesCache, theme, createListStyles);
28
19
  return (
29
20
  <View style={[styles.list, depth > 0 && styles.listNested, style]}>
30
21
  {children}
@@ -48,30 +39,10 @@ export const ListItem: FC<ListItemProps> = ({
48
39
  style,
49
40
  }) => {
50
41
  const { theme } = useMarkdownContext();
51
- const styles = useMemo(
52
- () =>
53
- StyleSheet.create({
54
- listItem: {
55
- flexDirection: "row",
56
- marginBottom: theme.spacing.s,
57
- alignItems: "flex-start",
58
- },
59
- listBullet: {
60
- color: theme.colors.accent,
61
- fontSize: theme.fontSizes.m,
62
- lineHeight: theme.fontSizes.m * 1.6,
63
- marginRight: theme.spacing.s,
64
- minWidth: 22,
65
- textAlign: "center",
66
- fontFamily: theme.fontFamilies.regular,
67
- ...(Platform.OS === "android" && { includeFontPadding: false }),
68
- },
69
- listItemContent: {
70
- flex: 1,
71
- minWidth: 0,
72
- },
73
- }),
74
- [theme],
42
+ const styles = getCachedStyles(
43
+ listItemStylesCache,
44
+ theme,
45
+ createListItemStyles,
75
46
  );
76
47
  const bullet = ordered ? `${start + index}.` : "•";
77
48
  return (
@@ -94,42 +65,10 @@ export const TaskListItem: FC<TaskListItemProps> = ({
94
65
  style,
95
66
  }) => {
96
67
  const { theme } = useMarkdownContext();
97
- const styles = useMemo(
98
- () =>
99
- StyleSheet.create({
100
- taskListItem: {
101
- flexDirection: "row",
102
- alignItems: "flex-start",
103
- marginBottom: theme.spacing.s,
104
- },
105
- taskCheckbox: {
106
- width: 18,
107
- height: 18,
108
- borderRadius: 4,
109
- borderWidth: 2,
110
- borderColor: theme.colors.accent,
111
- alignItems: "center",
112
- justifyContent: "center",
113
- marginRight: theme.spacing.s,
114
- marginTop: 2,
115
- },
116
- taskCheckboxChecked: {
117
- backgroundColor: theme.colors.accent,
118
- },
119
- taskCheckboxText: {
120
- color: theme.colors.surface,
121
- fontSize: 12,
122
- lineHeight: 12,
123
- fontWeight: "700",
124
- fontFamily: theme.fontFamilies.regular,
125
- ...(Platform.OS === "android" && { includeFontPadding: false }),
126
- },
127
- taskContent: {
128
- flex: 1,
129
- minWidth: 0,
130
- },
131
- }),
132
- [theme],
68
+ const styles = getCachedStyles(
69
+ taskListItemStylesCache,
70
+ theme,
71
+ createTaskListItemStyles,
133
72
  );
134
73
  return (
135
74
  <View style={[styles.taskListItem, style]}>
@@ -142,3 +81,83 @@ export const TaskListItem: FC<TaskListItemProps> = ({
142
81
  </View>
143
82
  );
144
83
  };
84
+
85
+ type ListStyles = ReturnType<typeof createListStyles>;
86
+ type ListItemStyles = ReturnType<typeof createListItemStyles>;
87
+ type TaskListItemStyles = ReturnType<typeof createTaskListItemStyles>;
88
+
89
+ const listStylesCache = new WeakMap<MarkdownTheme, ListStyles>();
90
+ const listItemStylesCache = new WeakMap<MarkdownTheme, ListItemStyles>();
91
+ const taskListItemStylesCache = new WeakMap<
92
+ MarkdownTheme,
93
+ TaskListItemStyles
94
+ >();
95
+
96
+ const createListStyles = (theme: MarkdownTheme) =>
97
+ StyleSheet.create({
98
+ list: {
99
+ marginBottom: theme.spacing.m,
100
+ },
101
+ listNested: {
102
+ marginLeft: theme.spacing.s,
103
+ marginBottom: 0,
104
+ },
105
+ });
106
+
107
+ const createListItemStyles = (theme: MarkdownTheme) =>
108
+ StyleSheet.create({
109
+ listItem: {
110
+ flexDirection: "row",
111
+ marginBottom: theme.spacing.s,
112
+ alignItems: "flex-start",
113
+ },
114
+ listBullet: {
115
+ color: theme.colors.accent,
116
+ fontSize: theme.fontSizes.m,
117
+ lineHeight: theme.fontSizes.m * 1.6,
118
+ marginRight: theme.spacing.s,
119
+ minWidth: 22,
120
+ textAlign: "center",
121
+ fontFamily: theme.fontFamilies.regular,
122
+ ...(Platform.OS === "android" && { includeFontPadding: false }),
123
+ },
124
+ listItemContent: {
125
+ flex: 1,
126
+ minWidth: 0,
127
+ },
128
+ });
129
+
130
+ const createTaskListItemStyles = (theme: MarkdownTheme) =>
131
+ StyleSheet.create({
132
+ taskListItem: {
133
+ flexDirection: "row",
134
+ alignItems: "flex-start",
135
+ marginBottom: theme.spacing.s,
136
+ },
137
+ taskCheckbox: {
138
+ width: 18,
139
+ height: 18,
140
+ borderRadius: 4,
141
+ borderWidth: 2,
142
+ borderColor: theme.colors.accent,
143
+ alignItems: "center",
144
+ justifyContent: "center",
145
+ marginRight: theme.spacing.s,
146
+ marginTop: 2,
147
+ },
148
+ taskCheckboxChecked: {
149
+ backgroundColor: theme.colors.accent,
150
+ },
151
+ taskCheckboxText: {
152
+ color: theme.colors.surface,
153
+ fontSize: 12,
154
+ lineHeight: 12,
155
+ fontWeight: "700",
156
+ fontFamily: theme.fontFamilies.regular,
157
+ ...(Platform.OS === "android" && { includeFontPadding: false }),
158
+ },
159
+ taskContent: {
160
+ flex: 1,
161
+ minWidth: 0,
162
+ },
163
+ });
@@ -1,4 +1,4 @@
1
- import { useMemo, type FC, type ComponentType } from "react";
1
+ import type { FC, ComponentType } from "react";
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -7,6 +7,7 @@ import {
7
7
  type StyleProp,
8
8
  type ViewStyle,
9
9
  } from "react-native";
10
+ import { getCachedStyles } from "./style-cache";
10
11
  import { useMarkdownContext } from "../MarkdownContext";
11
12
  import type { MarkdownTheme } from "../theme";
12
13
 
@@ -23,7 +24,12 @@ try {
23
24
  const mathJaxModule = require("react-native-mathjax-svg");
24
25
  MathJaxComponent = mathJaxModule.default || mathJaxModule;
25
26
  } catch {
26
- // ignored
27
+ if (__DEV__) {
28
+ // eslint-disable-next-line no-console
29
+ console.warn(
30
+ "[NitroMarkdown] react-native-mathjax-svg not found — math will render as plain text.",
31
+ );
32
+ }
27
33
  }
28
34
 
29
35
  type MathInlineProps = {
@@ -31,11 +37,14 @@ type MathInlineProps = {
31
37
  style?: ViewStyle;
32
38
  };
33
39
 
40
+ type MathStyles = ReturnType<typeof createMathStyles>;
41
+
42
+ const mathStylesCache = new WeakMap<MarkdownTheme, MathStyles>();
43
+
34
44
  const createMathStyles = (theme: MarkdownTheme) =>
35
45
  StyleSheet.create({
36
46
  mathInlineContainer: {
37
47
  marginHorizontal: 2,
38
- // Ensure the inline view has layout alignment
39
48
  justifyContent: "center",
40
49
  },
41
50
  mathInlineFallbackContainer: {
@@ -88,7 +97,7 @@ const createMathStyles = (theme: MarkdownTheme) =>
88
97
 
89
98
  export const MathInline: FC<MathInlineProps> = ({ content, style }) => {
90
99
  const { theme } = useMarkdownContext();
91
- const styles = useMemo(() => createMathStyles(theme), [theme]);
100
+ const styles = getCachedStyles(mathStylesCache, theme, createMathStyles);
92
101
 
93
102
  if (!content) return null;
94
103
 
@@ -122,7 +131,7 @@ type MathBlockProps = {
122
131
 
123
132
  export const MathBlock: FC<MathBlockProps> = ({ content, style }) => {
124
133
  const { theme } = useMarkdownContext();
125
- const styles = useMemo(() => createMathStyles(theme), [theme]);
134
+ const styles = getCachedStyles(mathStylesCache, theme, createMathStyles);
126
135
 
127
136
  if (!content) return null;
128
137
 
@@ -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 StyleProp, 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 ParagraphProps = {
6
8
  children: ReactNode;
@@ -14,22 +16,7 @@ export const Paragraph: FC<ParagraphProps> = ({
14
16
  style,
15
17
  }) => {
16
18
  const { theme } = useMarkdownContext();
17
- const styles = useMemo(
18
- () =>
19
- StyleSheet.create({
20
- paragraph: {
21
- flexDirection: "row",
22
- flexWrap: "wrap",
23
- marginBottom: theme.spacing.l,
24
- gap: 0,
25
- },
26
- paragraphInListItem: {
27
- marginBottom: 0,
28
- marginTop: 0,
29
- },
30
- }),
31
- [theme],
32
- );
19
+ const styles = getCachedStyles(stylesCache, theme, createStyles);
33
20
 
34
21
  return (
35
22
  <View
@@ -43,3 +30,21 @@ export const Paragraph: FC<ParagraphProps> = ({
43
30
  </View>
44
31
  );
45
32
  };
33
+
34
+ type ParagraphStyles = ReturnType<typeof createStyles>;
35
+
36
+ const stylesCache = new WeakMap<MarkdownTheme, ParagraphStyles>();
37
+
38
+ const createStyles = (theme: MarkdownTheme) =>
39
+ StyleSheet.create({
40
+ paragraph: {
41
+ flexDirection: "row",
42
+ flexWrap: "wrap",
43
+ marginBottom: theme.spacing.l,
44
+ gap: 0,
45
+ },
46
+ paragraphInListItem: {
47
+ marginBottom: 0,
48
+ marginTop: 0,
49
+ },
50
+ });
@@ -0,0 +1,14 @@
1
+ import type { MarkdownTheme } from "../theme";
2
+
3
+ export function getCachedStyles<T>(
4
+ cache: WeakMap<MarkdownTheme, T>,
5
+ theme: MarkdownTheme,
6
+ createStyles: (theme: MarkdownTheme) => T,
7
+ ): T {
8
+ const cached = cache.get(theme);
9
+ if (cached) return cached;
10
+
11
+ const styles = createStyles(theme);
12
+ cache.set(theme, styles);
13
+ return styles;
14
+ }
@@ -1,5 +1,11 @@
1
- import { type FC, type ComponentType } from "react";
2
- import { View, Text, type StyleProp, type TextStyle } from "react-native";
1
+ import type { FC, ComponentType } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type TextStyle,
7
+ type ViewStyle,
8
+ } from "react-native";
3
9
  import type { MarkdownNode } from "../../headless";
4
10
  import type { NodeRendererProps } from "../../MarkdownContext";
5
11
 
@@ -7,7 +13,8 @@ type CellContentProps = {
7
13
  node: MarkdownNode;
8
14
  Renderer: ComponentType<NodeRendererProps>;
9
15
  styles: {
10
- cellContentWrapper: object;
16
+ cellContentWrapper: StyleProp<ViewStyle>;
17
+ [key: string]: StyleProp<ViewStyle | TextStyle> | undefined;
11
18
  };
12
19
  textStyle?: StyleProp<TextStyle>;
13
20
  };
@@ -26,7 +33,11 @@ export const CellContent: FC<CellContentProps> = ({
26
33
  <View style={styles.cellContentWrapper}>
27
34
  {node.children.map((child, idx) => (
28
35
  <Renderer
29
- key={idx}
36
+ key={
37
+ child.beg != null
38
+ ? `${child.type}-${child.beg}`
39
+ : `${child.type}-${idx}`
40
+ }
30
41
  node={child}
31
42
  depth={0}
32
43
  inListItem={false}
@@ -15,15 +15,19 @@ import {
15
15
  type ViewStyle,
16
16
  type LayoutChangeEvent,
17
17
  } from "react-native";
18
- import { useMarkdownContext, type NodeRendererProps } from "../../MarkdownContext";
19
- import type { MarkdownTheme } from "../../theme";
20
18
  import { CellContent } from "./cell-content";
21
- import { extractTableData, estimateColumnWidths } from "./table-utils";
22
19
  import {
23
20
  columnWidthsReducer,
24
21
  DEFAULT_MIN_COLUMN_WIDTH,
25
22
  DEFAULT_MEASUREMENT_STABILIZE_MS,
26
23
  } from "./table-reducer";
24
+ import { extractTableData, estimateColumnWidths } from "./table-utils";
25
+ import {
26
+ useMarkdownContext,
27
+ type NodeRendererProps,
28
+ } from "../../MarkdownContext";
29
+ import { getCachedStyles } from "../style-cache";
30
+ import type { MarkdownTheme } from "../../theme";
27
31
 
28
32
  type TableRendererProps = {
29
33
  node: import("../../headless").MarkdownNode;
@@ -55,7 +59,7 @@ export const TableRenderer: FC<TableRendererProps> = ({
55
59
  );
56
60
 
57
61
  const columnCount = headers.length;
58
- const styles = useMemo(() => createTableStyles(theme), [theme]);
62
+ const styles = getCachedStyles(tableStylesCache, theme, createTableStyles);
59
63
  const estimatedColumnWidths = useMemo(
60
64
  () => estimateColumnWidths(headers, rows, columnCount, minColumnWidth),
61
65
  [headers, rows, columnCount, minColumnWidth],
@@ -147,6 +151,15 @@ export const TableRenderer: FC<TableRendererProps> = ({
147
151
  };
148
152
  }, [estimatedColumnWidths, expectedCellKeySignature, measurementStabilizeMs]);
149
153
 
154
+ useEffect(() => {
155
+ const widthsMap = measuredWidths.current;
156
+ const cellsSet = measuredCells.current;
157
+ return () => {
158
+ widthsMap.clear();
159
+ cellsSet.clear();
160
+ };
161
+ }, []);
162
+
150
163
  const onCellLayout = useCallback(
151
164
  (cellKey: string, width: number) => {
152
165
  if (width <= 0 || widthsCalculated.current || !needsMeasurement) return;
@@ -201,6 +214,14 @@ export const TableRenderer: FC<TableRendererProps> = ({
201
214
  return "flex-start";
202
215
  };
203
216
 
217
+ const tableBackgroundStyle = useMemo(
218
+ () => ({
219
+ backgroundColor:
220
+ style?.backgroundColor ?? theme.colors.surface ?? "#111827",
221
+ }),
222
+ [style, theme.colors.surface],
223
+ );
224
+
204
225
  if (columnCount === 0) return null;
205
226
 
206
227
  const hasWidths = columnWidths.length === columnCount;
@@ -257,15 +278,7 @@ export const TableRenderer: FC<TableRendererProps> = ({
257
278
  style={styles.tableScroll}
258
279
  bounces={false}
259
280
  >
260
- <View
261
- style={[
262
- styles.table,
263
- {
264
- backgroundColor:
265
- style?.backgroundColor ?? theme.colors.surface ?? "#111827",
266
- },
267
- ]}
268
- >
281
+ <View style={[styles.table, tableBackgroundStyle]}>
269
282
  <View style={styles.headerRow}>
270
283
  {headers.map((cell, colIndex) => (
271
284
  <View
@@ -326,6 +339,10 @@ export const TableRenderer: FC<TableRendererProps> = ({
326
339
  );
327
340
  };
328
341
 
342
+ type TableStyles = ReturnType<typeof createTableStyles>;
343
+
344
+ const tableStylesCache = new WeakMap<MarkdownTheme, TableStyles>();
345
+
329
346
  const createTableStyles = (theme: MarkdownTheme) => {
330
347
  const colors = theme?.colors || {};
331
348
  const borderRadius = theme?.borderRadius || { m: 8 };
package/src/theme.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Platform, type TextStyle, type ViewStyle } from "react-native";
2
- import type { MarkdownNode } from "./headless";
3
2
 
4
3
  export type MarkdownTheme = {
5
4
  colors: {
@@ -85,13 +84,13 @@ export const defaultMarkdownTheme: MarkdownTheme = {
85
84
  tableRowEven: "transparent",
86
85
  tableRowOdd: "#f8fafc",
87
86
  codeTokenColors: {
88
- keyword: '#c792ea',
89
- string: '#c3e88d',
90
- comment: '#546e7a',
91
- number: '#f78c6c',
92
- operator: '#89ddff',
93
- punctuation: '#89ddff',
94
- type: '#ffcb6b',
87
+ keyword: "#c792ea",
88
+ string: "#c3e88d",
89
+ comment: "#546e7a",
90
+ number: "#f78c6c",
91
+ operator: "#89ddff",
92
+ punctuation: "#89ddff",
93
+ type: "#ffcb6b",
95
94
  },
96
95
  },
97
96
  spacing: {
@@ -149,13 +148,34 @@ export type PartialMarkdownTheme = {
149
148
  };
150
149
 
151
150
  type TextNodeType =
152
- | "text" | "bold" | "italic" | "strikethrough" | "link"
153
- | "code_inline" | "heading" | "paragraph" | "math_inline" | "html_inline";
151
+ | "text"
152
+ | "bold"
153
+ | "italic"
154
+ | "strikethrough"
155
+ | "link"
156
+ | "code_inline"
157
+ | "heading"
158
+ | "paragraph"
159
+ | "math_inline"
160
+ | "html_inline";
154
161
  type ViewNodeType =
155
- | "document" | "blockquote" | "code_block" | "horizontal_rule"
156
- | "image" | "list" | "list_item" | "task_list_item" | "table"
157
- | "table_head" | "table_body" | "table_row" | "table_cell"
158
- | "math_block" | "html_block" | "line_break" | "soft_break";
162
+ | "document"
163
+ | "blockquote"
164
+ | "code_block"
165
+ | "horizontal_rule"
166
+ | "image"
167
+ | "list"
168
+ | "list_item"
169
+ | "task_list_item"
170
+ | "table"
171
+ | "table_head"
172
+ | "table_body"
173
+ | "table_row"
174
+ | "table_cell"
175
+ | "math_block"
176
+ | "html_block"
177
+ | "line_break"
178
+ | "soft_break";
159
179
 
160
180
  export type NodeStyleOverrides = Partial<
161
181
  { [K in TextNodeType]: TextStyle } & { [K in ViewNodeType]: ViewStyle }