react-native-nitro-markdown 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +605 -318
  2. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +27 -8
  3. package/cpp/bindings/HybridMarkdownParser.cpp +216 -66
  4. package/cpp/bindings/HybridMarkdownParser.hpp +2 -0
  5. package/ios/HybridMarkdownSession.swift +33 -7
  6. package/lib/commonjs/MarkdownContext.js +2 -1
  7. package/lib/commonjs/MarkdownContext.js.map +1 -1
  8. package/lib/commonjs/headless.js +41 -5
  9. package/lib/commonjs/headless.js.map +1 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/markdown-stream.js +109 -13
  12. package/lib/commonjs/markdown-stream.js.map +1 -1
  13. package/lib/commonjs/markdown.js +215 -44
  14. package/lib/commonjs/markdown.js.map +1 -1
  15. package/lib/commonjs/renderers/code.js +4 -3
  16. package/lib/commonjs/renderers/code.js.map +1 -1
  17. package/lib/commonjs/renderers/heading.js +1 -1
  18. package/lib/commonjs/renderers/heading.js.map +1 -1
  19. package/lib/commonjs/renderers/image.js +7 -5
  20. package/lib/commonjs/renderers/image.js.map +1 -1
  21. package/lib/commonjs/renderers/link.js +15 -3
  22. package/lib/commonjs/renderers/link.js.map +1 -1
  23. package/lib/commonjs/renderers/list.js +2 -2
  24. package/lib/commonjs/renderers/list.js.map +1 -1
  25. package/lib/commonjs/renderers/table.js +126 -21
  26. package/lib/commonjs/renderers/table.js.map +1 -1
  27. package/lib/commonjs/use-markdown-stream.js +16 -14
  28. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  29. package/lib/commonjs/utils/incremental-ast.js +153 -0
  30. package/lib/commonjs/utils/incremental-ast.js.map +1 -0
  31. package/lib/commonjs/utils/link-security.js +21 -0
  32. package/lib/commonjs/utils/link-security.js.map +1 -0
  33. package/lib/commonjs/utils/stream-timeline.js +62 -0
  34. package/lib/commonjs/utils/stream-timeline.js.map +1 -0
  35. package/lib/module/MarkdownContext.js +2 -1
  36. package/lib/module/MarkdownContext.js.map +1 -1
  37. package/lib/module/headless.js +37 -4
  38. package/lib/module/headless.js.map +1 -1
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/markdown-stream.js +110 -14
  41. package/lib/module/markdown-stream.js.map +1 -1
  42. package/lib/module/markdown.js +217 -46
  43. package/lib/module/markdown.js.map +1 -1
  44. package/lib/module/renderers/blockquote.js.map +1 -1
  45. package/lib/module/renderers/code.js +4 -3
  46. package/lib/module/renderers/code.js.map +1 -1
  47. package/lib/module/renderers/heading.js +1 -1
  48. package/lib/module/renderers/heading.js.map +1 -1
  49. package/lib/module/renderers/image.js +7 -5
  50. package/lib/module/renderers/image.js.map +1 -1
  51. package/lib/module/renderers/link.js +15 -3
  52. package/lib/module/renderers/link.js.map +1 -1
  53. package/lib/module/renderers/list.js +2 -2
  54. package/lib/module/renderers/list.js.map +1 -1
  55. package/lib/module/renderers/paragraph.js.map +1 -1
  56. package/lib/module/renderers/table.js +127 -22
  57. package/lib/module/renderers/table.js.map +1 -1
  58. package/lib/module/use-markdown-stream.js +16 -14
  59. package/lib/module/use-markdown-stream.js.map +1 -1
  60. package/lib/module/utils/incremental-ast.js +147 -0
  61. package/lib/module/utils/incremental-ast.js.map +1 -0
  62. package/lib/module/utils/link-security.js +15 -0
  63. package/lib/module/utils/link-security.js.map +1 -0
  64. package/lib/module/utils/stream-timeline.js +56 -0
  65. package/lib/module/utils/stream-timeline.js.map +1 -0
  66. package/lib/typescript/commonjs/Markdown.nitro.d.ts +5 -3
  67. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
  69. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/headless.d.ts +15 -2
  71. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/index.d.ts +3 -1
  73. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/markdown-stream.d.ts +7 -2
  75. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/markdown.d.ts +62 -5
  77. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
  79. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
  81. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
  83. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
  85. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
  87. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
  89. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
  91. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
  93. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
  95. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
  97. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +5 -2
  99. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/theme.d.ts +2 -2
  101. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
  104. package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
  106. package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
  108. package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
  109. package/lib/typescript/module/Markdown.nitro.d.ts +5 -3
  110. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  111. package/lib/typescript/module/MarkdownContext.d.ts +26 -25
  112. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  113. package/lib/typescript/module/headless.d.ts +15 -2
  114. package/lib/typescript/module/headless.d.ts.map +1 -1
  115. package/lib/typescript/module/index.d.ts +3 -1
  116. package/lib/typescript/module/index.d.ts.map +1 -1
  117. package/lib/typescript/module/markdown-stream.d.ts +7 -2
  118. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  119. package/lib/typescript/module/markdown.d.ts +62 -5
  120. package/lib/typescript/module/markdown.d.ts.map +1 -1
  121. package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
  122. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  123. package/lib/typescript/module/renderers/code.d.ts +5 -5
  124. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  125. package/lib/typescript/module/renderers/heading.d.ts +3 -3
  126. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  127. package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
  128. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  129. package/lib/typescript/module/renderers/image.d.ts +2 -2
  130. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  131. package/lib/typescript/module/renderers/link.d.ts +3 -3
  132. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  133. package/lib/typescript/module/renderers/list.d.ts +7 -7
  134. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  135. package/lib/typescript/module/renderers/math.d.ts +4 -4
  136. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  137. package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
  138. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  139. package/lib/typescript/module/renderers/table.d.ts +3 -3
  140. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  141. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +5 -2
  142. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
  143. package/lib/typescript/module/theme.d.ts +2 -2
  144. package/lib/typescript/module/theme.d.ts.map +1 -1
  145. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  146. package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
  147. package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
  148. package/lib/typescript/module/utils/link-security.d.ts +3 -0
  149. package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
  150. package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
  151. package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
  152. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -0
  153. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  154. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +18 -6
  155. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +4 -2
  156. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/Func_void_double_double.kt +80 -0
  157. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +11 -3
  158. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.cpp +8 -0
  159. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.hpp +31 -0
  160. package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +20 -2
  161. package/nitrogen/generated/ios/swift/Func_void.swift +0 -1
  162. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
  163. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +4 -3
  164. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +34 -10
  165. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
  166. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
  167. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +2 -0
  168. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +4 -2
  169. package/package.json +7 -5
  170. package/src/Markdown.nitro.ts +7 -3
  171. package/src/MarkdownContext.ts +31 -25
  172. package/src/headless.ts +44 -6
  173. package/src/index.ts +8 -0
  174. package/src/markdown-stream.tsx +159 -15
  175. package/src/markdown.tsx +406 -50
  176. package/src/renderers/blockquote.tsx +4 -4
  177. package/src/renderers/code.tsx +16 -10
  178. package/src/renderers/heading.tsx +4 -4
  179. package/src/renderers/horizontal-rule.tsx +3 -3
  180. package/src/renderers/image.tsx +11 -9
  181. package/src/renderers/link.tsx +25 -7
  182. package/src/renderers/list.tsx +9 -12
  183. package/src/renderers/math.tsx +4 -4
  184. package/src/renderers/paragraph.tsx +3 -3
  185. package/src/renderers/table.tsx +270 -98
  186. package/src/specs/MarkdownSession.nitro.ts +6 -2
  187. package/src/theme.ts +3 -3
  188. package/src/use-markdown-stream.ts +22 -16
  189. package/src/utils/incremental-ast.ts +224 -0
  190. package/src/utils/link-security.ts +22 -0
  191. package/src/utils/stream-timeline.ts +72 -0
@@ -1,11 +1,11 @@
1
1
  import {
2
+ useEffect,
2
3
  useMemo,
3
4
  useRef,
4
5
  useReducer,
5
6
  useCallback,
6
7
  type FC,
7
8
  type ComponentType,
8
- type ReactNode,
9
9
  } from "react";
10
10
  import {
11
11
  View,
@@ -18,10 +18,9 @@ import {
18
18
  type ViewStyle,
19
19
  type LayoutChangeEvent,
20
20
  } from "react-native";
21
-
22
- import type { MarkdownNode } from "../headless";
21
+ import { getTextContent, type MarkdownNode } from "../headless";
23
22
  import { useMarkdownContext, type NodeRendererProps } from "../MarkdownContext";
24
- import { MarkdownTheme } from "../theme";
23
+ import type { MarkdownTheme } from "../theme";
25
24
 
26
25
  type TableData = {
27
26
  headers: MarkdownNode[];
@@ -60,22 +59,93 @@ const extractTableData = (node: MarkdownNode): TableData => {
60
59
  return { headers, rows, alignments };
61
60
  };
62
61
 
63
- interface TableRendererProps {
62
+ type TableRendererProps = {
64
63
  node: MarkdownNode;
65
64
  Renderer: ComponentType<NodeRendererProps>;
66
65
  style?: ViewStyle;
67
- }
66
+ };
68
67
 
69
68
  type ColumnWidthsAction = {
70
- type: "SET_WIDTHS";
69
+ type: "RESET_WIDTHS" | "SET_MONOTONIC_WIDTHS";
71
70
  payload: number[];
72
71
  };
73
72
 
73
+ const MIN_COLUMN_WIDTH = 60;
74
+ const COLUMN_MEASUREMENT_PADDING = 8;
75
+ const APPROX_CHAR_WIDTH = 7;
76
+ const MAX_ESTIMATED_CHARS = 120;
77
+ const ESTIMATED_WIDTH_STEP = 24;
78
+ const MEASUREMENT_STABILIZE_MS = 140;
79
+ const IS_ACT_TEST_ENVIRONMENT =
80
+ Reflect.get(globalThis, "IS_REACT_ACT_ENVIRONMENT") === true;
81
+ const SHOULD_DEBOUNCE_MEASUREMENT = !IS_ACT_TEST_ENVIRONMENT;
82
+
74
83
  const columnWidthsReducer = (state: number[], action: ColumnWidthsAction) => {
75
- if (action.type === "SET_WIDTHS") return action.payload;
84
+ if (action.type === "RESET_WIDTHS") {
85
+ if (
86
+ state.length === action.payload.length &&
87
+ state.every((width, index) => width === action.payload[index])
88
+ ) {
89
+ return state;
90
+ }
91
+ return action.payload;
92
+ }
93
+
94
+ if (action.type === "SET_MONOTONIC_WIDTHS") {
95
+ if (state.length !== action.payload.length) {
96
+ return action.payload;
97
+ }
98
+
99
+ const merged = action.payload.map((width, index) =>
100
+ Math.max(width, state[index] ?? width),
101
+ );
102
+
103
+ if (state.every((width, index) => width === merged[index])) {
104
+ return state;
105
+ }
106
+ return merged;
107
+ }
108
+
76
109
  return state;
77
110
  };
78
111
 
112
+ const estimateColumnWidths = (
113
+ headers: MarkdownNode[],
114
+ rows: MarkdownNode[][],
115
+ columnCount: number,
116
+ ) => {
117
+ const widths = new Array<number>(columnCount).fill(MIN_COLUMN_WIDTH);
118
+
119
+ for (let col = 0; col < columnCount; col++) {
120
+ const headerChars = Math.min(
121
+ getTextContent(headers[col] ?? { type: "text", content: "" }).trim()
122
+ .length,
123
+ MAX_ESTIMATED_CHARS,
124
+ );
125
+ let maxChars = headerChars;
126
+
127
+ for (let row = 0; row < rows.length; row++) {
128
+ const cell = rows[row][col];
129
+ if (!cell) continue;
130
+ const cellChars = Math.min(
131
+ getTextContent(cell).trim().length,
132
+ MAX_ESTIMATED_CHARS,
133
+ );
134
+ if (cellChars > maxChars) {
135
+ maxChars = cellChars;
136
+ }
137
+ }
138
+
139
+ const estimatedWidth =
140
+ maxChars * APPROX_CHAR_WIDTH + COLUMN_MEASUREMENT_PADDING;
141
+ const steppedEstimatedWidth =
142
+ Math.ceil(estimatedWidth / ESTIMATED_WIDTH_STEP) * ESTIMATED_WIDTH_STEP;
143
+ widths[col] = Math.max(MIN_COLUMN_WIDTH, steppedEstimatedWidth);
144
+ }
145
+
146
+ return widths;
147
+ };
148
+
79
149
  export const TableRenderer: FC<TableRendererProps> = ({
80
150
  node,
81
151
  Renderer,
@@ -84,38 +154,119 @@ export const TableRenderer: FC<TableRendererProps> = ({
84
154
  const { theme } = useMarkdownContext();
85
155
  const { headers, rows, alignments } = useMemo(
86
156
  () => extractTableData(node),
87
- [node]
157
+ [node],
88
158
  );
89
159
 
90
160
  const columnCount = headers.length;
91
161
  const styles = useMemo(() => createTableStyles(theme), [theme]);
162
+ const estimatedColumnWidths = useMemo(
163
+ () => estimateColumnWidths(headers, rows, columnCount),
164
+ [headers, rows, columnCount],
165
+ );
92
166
 
93
- const [columnWidths, dispatch] = useReducer(columnWidthsReducer, []);
167
+ const [columnWidths, dispatch] = useReducer(
168
+ columnWidthsReducer,
169
+ estimatedColumnWidths,
170
+ );
94
171
  const measuredWidths = useRef<Map<string, number>>(new Map());
95
172
  const measuredCells = useRef<Set<string>>(new Set());
96
173
  const widthsCalculated = useRef(false);
174
+ const columnWidthsRef = useRef(columnWidths);
175
+ const lastCellKeySignatureRef = useRef("");
176
+ const measurementTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
177
+ null,
178
+ );
179
+ const [needsMeasurement, setNeedsMeasurement] = useReducer(
180
+ (_previous: boolean, nextValue: boolean) => nextValue,
181
+ false,
182
+ );
183
+
184
+ const expectedCellKeys = useMemo(() => {
185
+ const keys: string[] = [];
186
+
187
+ headers.forEach((_, colIndex) => {
188
+ keys.push(`header-${colIndex}`);
189
+ });
190
+
191
+ rows.forEach((row, rowIndex) => {
192
+ row.forEach((_, colIndex) => {
193
+ keys.push(`cell-${rowIndex}-${colIndex}`);
194
+ });
195
+ });
196
+
197
+ return keys;
198
+ }, [headers, rows]);
199
+
200
+ const expectedCellKeySignature = useMemo(
201
+ () => expectedCellKeys.join("|"),
202
+ [expectedCellKeys],
203
+ );
204
+
205
+ useEffect(() => {
206
+ columnWidthsRef.current = columnWidths;
207
+ }, [columnWidths]);
208
+
209
+ useEffect(() => {
210
+ const structureChanged =
211
+ lastCellKeySignatureRef.current !== expectedCellKeySignature;
212
+ lastCellKeySignatureRef.current = expectedCellKeySignature;
213
+
214
+ if (measurementTimerRef.current) {
215
+ clearTimeout(measurementTimerRef.current);
216
+ measurementTimerRef.current = null;
217
+ }
218
+
219
+ if (structureChanged) {
220
+ measuredWidths.current.clear();
221
+ measuredCells.current.clear();
222
+ widthsCalculated.current = false;
223
+ setNeedsMeasurement(false);
224
+ dispatch({ type: "RESET_WIDTHS", payload: estimatedColumnWidths });
225
+ } else {
226
+ dispatch({
227
+ type: "SET_MONOTONIC_WIDTHS",
228
+ payload: estimatedColumnWidths,
229
+ });
230
+ if (widthsCalculated.current) {
231
+ return;
232
+ }
233
+ }
234
+
235
+ if (!SHOULD_DEBOUNCE_MEASUREMENT) {
236
+ setNeedsMeasurement(true);
237
+ return;
238
+ }
239
+
240
+ measurementTimerRef.current = setTimeout(() => {
241
+ measurementTimerRef.current = null;
242
+ setNeedsMeasurement(true);
243
+ }, MEASUREMENT_STABILIZE_MS);
244
+
245
+ return () => {
246
+ if (measurementTimerRef.current) {
247
+ clearTimeout(measurementTimerRef.current);
248
+ measurementTimerRef.current = null;
249
+ }
250
+ };
251
+ }, [estimatedColumnWidths, expectedCellKeySignature]);
97
252
 
98
253
  const onCellLayout = useCallback(
99
254
  (cellKey: string, width: number) => {
100
- measuredWidths.current.set(cellKey, width);
101
- measuredCells.current.add(cellKey);
255
+ if (width <= 0 || widthsCalculated.current || !needsMeasurement) return;
102
256
 
103
- const expectedCells = new Set<string>();
104
- for (let col = 0; col < columnCount; col++) {
105
- expectedCells.add(`header-${col}`);
106
- }
107
- for (let row = 0; row < rows.length; row++) {
108
- for (let col = 0; col < columnCount; col++) {
109
- expectedCells.add(`cell-${row}-${col}`);
110
- }
257
+ measuredWidths.current.set(cellKey, width);
258
+ if (!measuredCells.current.has(cellKey)) {
259
+ measuredCells.current.add(cellKey);
111
260
  }
112
261
 
113
- const allCellsMeasured = [...expectedCells].every((key) =>
114
- measuredCells.current.has(key)
262
+ if (measuredCells.current.size < expectedCellKeys.length) return;
263
+
264
+ const allCellsMeasured = expectedCellKeys.every((key) =>
265
+ measuredCells.current.has(key),
115
266
  );
116
- if (!allCellsMeasured || widthsCalculated.current) return;
267
+ if (!allCellsMeasured) return;
117
268
 
118
- const maxWidths: number[] = new Array(columnCount).fill(0);
269
+ const maxWidths: number[] = [...columnWidthsRef.current];
119
270
 
120
271
  for (let col = 0; col < columnCount; col++) {
121
272
  const headerWidth = measuredWidths.current.get(`header-${col}`);
@@ -124,23 +275,28 @@ export const TableRenderer: FC<TableRendererProps> = ({
124
275
  }
125
276
 
126
277
  for (let row = 0; row < rows.length; row++) {
278
+ if (col >= rows[row].length) continue;
127
279
  const cellWidth = measuredWidths.current.get(`cell-${row}-${col}`);
128
280
  if (cellWidth && cellWidth > 0) {
129
281
  maxWidths[col] = Math.max(maxWidths[col], cellWidth);
130
282
  }
131
283
  }
132
284
 
133
- maxWidths[col] = Math.max(maxWidths[col] + 8, 60);
285
+ maxWidths[col] = Math.max(
286
+ maxWidths[col] + COLUMN_MEASUREMENT_PADDING,
287
+ MIN_COLUMN_WIDTH,
288
+ );
134
289
  }
135
290
 
136
291
  widthsCalculated.current = true;
137
- dispatch({ type: "SET_WIDTHS", payload: maxWidths });
292
+ setNeedsMeasurement(false);
293
+ dispatch({ type: "RESET_WIDTHS", payload: maxWidths });
138
294
  },
139
- [columnCount, rows.length]
295
+ [columnCount, expectedCellKeys, needsMeasurement, rows],
140
296
  );
141
297
 
142
298
  const getAlignment = (
143
- index: number
299
+ index: number,
144
300
  ): "flex-start" | "center" | "flex-end" => {
145
301
  const align = alignments[index];
146
302
  if (align === "center") return "center";
@@ -151,33 +307,21 @@ export const TableRenderer: FC<TableRendererProps> = ({
151
307
  if (columnCount === 0) return null;
152
308
 
153
309
  const hasWidths = columnWidths.length === columnCount;
310
+ const resolvedWidths = hasWidths ? columnWidths : estimatedColumnWidths;
154
311
 
155
312
  return (
156
313
  <View style={[styles.container, style]}>
157
- <View style={styles.measurementContainer}>
158
- <View style={styles.measurementRow}>
159
- {headers.map((cell, colIndex) => (
160
- <View
161
- key={`measure-header-${colIndex}`}
162
- style={styles.measurementCell}
163
- onLayout={(e: LayoutChangeEvent) => {
164
- onCellLayout(`header-${colIndex}`, e.nativeEvent.layout.width);
165
- }}
166
- >
167
- <CellContent node={cell} Renderer={Renderer} styles={styles} />
168
- </View>
169
- ))}
170
- </View>
171
- {rows.map((row, rowIndex) => (
172
- <View key={`measure-row-${rowIndex}`} style={styles.measurementRow}>
173
- {row.map((cell, colIndex) => (
314
+ {needsMeasurement ? (
315
+ <View style={styles.measurementContainer}>
316
+ <View style={styles.measurementRow}>
317
+ {headers.map((cell, colIndex) => (
174
318
  <View
175
- key={`measure-cell-${rowIndex}-${colIndex}`}
319
+ key={`measure-header-${colIndex}`}
176
320
  style={styles.measurementCell}
177
321
  onLayout={(e: LayoutChangeEvent) => {
178
322
  onCellLayout(
179
- `cell-${rowIndex}-${colIndex}`,
180
- e.nativeEvent.layout.width
323
+ `header-${colIndex}`,
324
+ e.nativeEvent.layout.width,
181
325
  );
182
326
  }}
183
327
  >
@@ -185,74 +329,102 @@ export const TableRenderer: FC<TableRendererProps> = ({
185
329
  </View>
186
330
  ))}
187
331
  </View>
188
- ))}
189
- </View>
190
-
191
- {hasWidths && (
192
- <ScrollView
193
- horizontal
194
- showsHorizontalScrollIndicator
195
- style={styles.tableScroll}
196
- bounces={false}
197
- >
198
- <View style={[styles.table, { backgroundColor: style?.backgroundColor ?? theme.colors.surface ?? "#111827" }]}>
199
- <View style={styles.headerRow}>
200
- {headers.map((cell, colIndex) => (
332
+ {rows.map((row, rowIndex) => (
333
+ <View key={`measure-row-${rowIndex}`} style={styles.measurementRow}>
334
+ {row.map((cell, colIndex) => (
201
335
  <View
202
- key={`header-${colIndex}`}
203
- style={[
204
- styles.headerCell,
205
- {
206
- width: columnWidths[colIndex],
207
- alignItems: getAlignment(colIndex),
208
- },
209
- colIndex === columnCount - 1 && styles.lastCell,
210
- ]}
336
+ key={`measure-cell-${rowIndex}-${colIndex}`}
337
+ style={styles.measurementCell}
338
+ onLayout={(e: LayoutChangeEvent) => {
339
+ onCellLayout(
340
+ `cell-${rowIndex}-${colIndex}`,
341
+ e.nativeEvent.layout.width,
342
+ );
343
+ }}
211
344
  >
212
345
  <CellContent
213
346
  node={cell}
214
347
  Renderer={Renderer}
215
348
  styles={styles}
216
- textStyle={styles.headerText}
217
349
  />
218
350
  </View>
219
351
  ))}
220
352
  </View>
353
+ ))}
354
+ </View>
355
+ ) : null}
221
356
 
222
- {rows.map((row, rowIndex) => (
357
+ <ScrollView
358
+ horizontal
359
+ showsHorizontalScrollIndicator
360
+ style={styles.tableScroll}
361
+ bounces={false}
362
+ >
363
+ <View
364
+ style={[
365
+ styles.table,
366
+ {
367
+ backgroundColor:
368
+ style?.backgroundColor ?? theme.colors.surface ?? "#111827",
369
+ },
370
+ ]}
371
+ >
372
+ <View style={styles.headerRow}>
373
+ {headers.map((cell, colIndex) => (
223
374
  <View
224
- key={`row-${rowIndex}`}
375
+ key={`header-${colIndex}`}
225
376
  style={[
226
- styles.bodyRow,
227
- rowIndex === rows.length - 1 && styles.lastRow,
228
- rowIndex % 2 === 0 ? styles.evenRow : styles.oddRow,
377
+ styles.headerCell,
378
+ {
379
+ width: resolvedWidths[colIndex] ?? MIN_COLUMN_WIDTH,
380
+ alignItems: getAlignment(colIndex),
381
+ },
382
+ colIndex === columnCount - 1 && styles.lastCell,
229
383
  ]}
230
384
  >
231
- {row.map((cell, colIndex) => (
232
- <View
233
- key={`cell-${rowIndex}-${colIndex}`}
234
- style={[
235
- styles.bodyCell,
236
- {
237
- width: columnWidths[colIndex],
238
- alignItems: getAlignment(colIndex),
239
- },
240
- colIndex === columnCount - 1 && styles.lastCell,
241
- ]}
242
- >
243
- <CellContent
244
- node={cell}
245
- Renderer={Renderer}
246
- styles={styles}
247
- textStyle={styles.cellText}
248
- />
249
- </View>
250
- ))}
385
+ <CellContent
386
+ node={cell}
387
+ Renderer={Renderer}
388
+ styles={styles}
389
+ textStyle={styles.headerText}
390
+ />
251
391
  </View>
252
392
  ))}
253
393
  </View>
254
- </ScrollView>
255
- )}
394
+
395
+ {rows.map((row, rowIndex) => (
396
+ <View
397
+ key={`row-${rowIndex}`}
398
+ style={[
399
+ styles.bodyRow,
400
+ rowIndex === rows.length - 1 && styles.lastRow,
401
+ rowIndex % 2 === 0 ? styles.evenRow : styles.oddRow,
402
+ ]}
403
+ >
404
+ {row.map((cell, colIndex) => (
405
+ <View
406
+ key={`cell-${rowIndex}-${colIndex}`}
407
+ style={[
408
+ styles.bodyCell,
409
+ {
410
+ width: resolvedWidths[colIndex] ?? MIN_COLUMN_WIDTH,
411
+ alignItems: getAlignment(colIndex),
412
+ },
413
+ colIndex === columnCount - 1 && styles.lastCell,
414
+ ]}
415
+ >
416
+ <CellContent
417
+ node={cell}
418
+ Renderer={Renderer}
419
+ styles={styles}
420
+ textStyle={styles.cellText}
421
+ />
422
+ </View>
423
+ ))}
424
+ </View>
425
+ ))}
426
+ </View>
427
+ </ScrollView>
256
428
  </View>
257
429
  );
258
430
  };
@@ -1,14 +1,18 @@
1
1
  import type { HybridObject } from "react-native-nitro-modules";
2
2
 
3
+ export type MarkdownSessionListener = (from: number, to: number) => void;
4
+
3
5
  export interface MarkdownSession extends HybridObject<{
4
6
  ios: "swift";
5
7
  android: "kotlin";
6
8
  }> {
7
- append(chunk: string): void;
9
+ append(chunk: string): number;
8
10
  clear(): void;
9
11
  getAllText(): string;
12
+ getLength(): number;
13
+ getTextRange(from: number, to: number): string;
10
14
 
11
15
  highlightPosition: number;
12
16
 
13
- addListener(listener: () => void): () => void;
17
+ addListener(listener: MarkdownSessionListener): () => void;
14
18
  }
package/src/theme.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Platform, type TextStyle, type ViewStyle } from "react-native";
2
2
  import type { MarkdownNode } from "./headless";
3
3
 
4
- export interface MarkdownTheme {
4
+ export type MarkdownTheme = {
5
5
  colors: {
6
6
  text: string | undefined;
7
7
  textMuted: string | undefined;
@@ -53,7 +53,7 @@ export interface MarkdownTheme {
53
53
  l: number;
54
54
  };
55
55
  showCodeLanguage: boolean;
56
- }
56
+ };
57
57
 
58
58
  export const defaultMarkdownTheme: MarkdownTheme = {
59
59
  colors: {
@@ -191,7 +191,7 @@ export const minimalMarkdownTheme: MarkdownTheme = {
191
191
 
192
192
  export const mergeThemes = (
193
193
  base: MarkdownTheme,
194
- partial?: PartialMarkdownTheme
194
+ partial?: PartialMarkdownTheme,
195
195
  ): MarkdownTheme => {
196
196
  if (!partial) return base;
197
197
  return {
@@ -1,5 +1,10 @@
1
1
  import { useRef, useCallback, useState, useEffect } from "react";
2
2
  import { createMarkdownSession } from "./MarkdownSession";
3
+ import {
4
+ createTimestampTimeline,
5
+ resolveHighlightPosition,
6
+ type TimestampTimeline,
7
+ } from "./utils/stream-timeline";
3
8
 
4
9
  export type MarkdownSession = ReturnType<typeof createMarkdownSession>;
5
10
 
@@ -46,32 +51,33 @@ export function useMarkdownSession() {
46
51
 
47
52
  export function useStream(timestamps?: Record<number, number>) {
48
53
  const engine = useMarkdownSession();
54
+ const { setHighlight } = engine;
49
55
  const [isPlaying, setIsPlaying] = useState(false);
56
+ const timelineRef = useRef<TimestampTimeline>({
57
+ entries: [],
58
+ monotonic: true,
59
+ });
60
+ const lastHighlightRef = useRef<number>(-1);
50
61
 
51
- const sortedKeys = useRef<number[]>([]);
52
62
  useEffect(() => {
53
- if (timestamps) {
54
- sortedKeys.current = Object.keys(timestamps)
55
- .map(Number)
56
- .sort((a, b) => a - b);
57
- }
63
+ timelineRef.current = createTimestampTimeline(timestamps);
64
+ lastHighlightRef.current = -1;
58
65
  }, [timestamps]);
59
66
 
60
67
  const sync = useCallback(
61
68
  (currentTimeMs: number) => {
62
69
  if (!timestamps) return;
63
70
 
64
- let wordIdx = 0;
65
- for (const idx of sortedKeys.current) {
66
- if (currentTimeMs >= timestamps[idx]) {
67
- wordIdx = idx + 1;
68
- } else {
69
- break;
70
- }
71
- }
72
- engine.setHighlight(wordIdx);
71
+ const nextHighlight = resolveHighlightPosition(
72
+ timelineRef.current,
73
+ currentTimeMs,
74
+ );
75
+ if (nextHighlight === lastHighlightRef.current) return;
76
+
77
+ lastHighlightRef.current = nextHighlight;
78
+ setHighlight(nextHighlight);
73
79
  },
74
- [timestamps, engine]
80
+ [setHighlight, timestamps],
75
81
  );
76
82
 
77
83
  return {