react-native-nitro-markdown 0.4.2 → 0.4.3

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 (155) hide show
  1. package/README.md +310 -352
  2. package/lib/commonjs/MarkdownContext.js +2 -1
  3. package/lib/commonjs/MarkdownContext.js.map +1 -1
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/markdown-stream.js +3 -1
  6. package/lib/commonjs/markdown-stream.js.map +1 -1
  7. package/lib/commonjs/markdown.js +51 -35
  8. package/lib/commonjs/markdown.js.map +1 -1
  9. package/lib/commonjs/renderers/code.js +3 -3
  10. package/lib/commonjs/renderers/code.js.map +1 -1
  11. package/lib/commonjs/renderers/heading.js +1 -1
  12. package/lib/commonjs/renderers/heading.js.map +1 -1
  13. package/lib/commonjs/renderers/image.js +7 -5
  14. package/lib/commonjs/renderers/image.js.map +1 -1
  15. package/lib/commonjs/renderers/link.js +15 -3
  16. package/lib/commonjs/renderers/link.js.map +1 -1
  17. package/lib/commonjs/renderers/list.js +2 -2
  18. package/lib/commonjs/renderers/list.js.map +1 -1
  19. package/lib/commonjs/renderers/table.js +36 -23
  20. package/lib/commonjs/renderers/table.js.map +1 -1
  21. package/lib/commonjs/use-markdown-stream.js +16 -14
  22. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  23. package/lib/commonjs/utils/link-security.js +21 -0
  24. package/lib/commonjs/utils/link-security.js.map +1 -0
  25. package/lib/commonjs/utils/stream-timeline.js +62 -0
  26. package/lib/commonjs/utils/stream-timeline.js.map +1 -0
  27. package/lib/module/MarkdownContext.js +2 -1
  28. package/lib/module/MarkdownContext.js.map +1 -1
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/markdown-stream.js +3 -1
  31. package/lib/module/markdown-stream.js.map +1 -1
  32. package/lib/module/markdown.js +52 -36
  33. package/lib/module/markdown.js.map +1 -1
  34. package/lib/module/renderers/blockquote.js.map +1 -1
  35. package/lib/module/renderers/code.js +3 -3
  36. package/lib/module/renderers/code.js.map +1 -1
  37. package/lib/module/renderers/heading.js +1 -1
  38. package/lib/module/renderers/heading.js.map +1 -1
  39. package/lib/module/renderers/image.js +7 -5
  40. package/lib/module/renderers/image.js.map +1 -1
  41. package/lib/module/renderers/link.js +15 -3
  42. package/lib/module/renderers/link.js.map +1 -1
  43. package/lib/module/renderers/list.js +2 -2
  44. package/lib/module/renderers/list.js.map +1 -1
  45. package/lib/module/renderers/paragraph.js.map +1 -1
  46. package/lib/module/renderers/table.js +37 -24
  47. package/lib/module/renderers/table.js.map +1 -1
  48. package/lib/module/use-markdown-stream.js +16 -14
  49. package/lib/module/use-markdown-stream.js.map +1 -1
  50. package/lib/module/utils/link-security.js +15 -0
  51. package/lib/module/utils/link-security.js.map +1 -0
  52. package/lib/module/utils/stream-timeline.js +56 -0
  53. package/lib/module/utils/stream-timeline.js.map +1 -0
  54. package/lib/typescript/commonjs/Markdown.nitro.d.ts +3 -3
  55. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
  57. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/headless.d.ts +2 -2
  59. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/index.d.ts +1 -1
  61. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/markdown-stream.d.ts +2 -2
  63. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/markdown.d.ts +9 -4
  65. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
  67. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
  69. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
  71. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
  73. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
  75. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
  77. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
  79. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
  81. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
  83. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
  85. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/theme.d.ts +2 -2
  87. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
  90. package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
  92. package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
  93. package/lib/typescript/module/Markdown.nitro.d.ts +3 -3
  94. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  95. package/lib/typescript/module/MarkdownContext.d.ts +26 -25
  96. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  97. package/lib/typescript/module/headless.d.ts +2 -2
  98. package/lib/typescript/module/headless.d.ts.map +1 -1
  99. package/lib/typescript/module/index.d.ts +1 -1
  100. package/lib/typescript/module/index.d.ts.map +1 -1
  101. package/lib/typescript/module/markdown-stream.d.ts +2 -2
  102. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  103. package/lib/typescript/module/markdown.d.ts +9 -4
  104. package/lib/typescript/module/markdown.d.ts.map +1 -1
  105. package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
  106. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  107. package/lib/typescript/module/renderers/code.d.ts +5 -5
  108. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  109. package/lib/typescript/module/renderers/heading.d.ts +3 -3
  110. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  111. package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
  112. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  113. package/lib/typescript/module/renderers/image.d.ts +2 -2
  114. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  115. package/lib/typescript/module/renderers/link.d.ts +3 -3
  116. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  117. package/lib/typescript/module/renderers/list.d.ts +7 -7
  118. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  119. package/lib/typescript/module/renderers/math.d.ts +4 -4
  120. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  121. package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
  122. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  123. package/lib/typescript/module/renderers/table.d.ts +3 -3
  124. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  125. package/lib/typescript/module/theme.d.ts +2 -2
  126. package/lib/typescript/module/theme.d.ts.map +1 -1
  127. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  128. package/lib/typescript/module/utils/link-security.d.ts +3 -0
  129. package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
  130. package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
  131. package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
  132. package/nitrogen/generated/ios/swift/Func_void.swift +0 -1
  133. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +0 -1
  134. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +0 -1
  135. package/package.json +4 -3
  136. package/src/Markdown.nitro.ts +5 -3
  137. package/src/MarkdownContext.ts +31 -25
  138. package/src/headless.ts +2 -2
  139. package/src/index.ts +1 -0
  140. package/src/markdown-stream.tsx +6 -10
  141. package/src/markdown.tsx +86 -45
  142. package/src/renderers/blockquote.tsx +4 -4
  143. package/src/renderers/code.tsx +11 -9
  144. package/src/renderers/heading.tsx +4 -4
  145. package/src/renderers/horizontal-rule.tsx +3 -3
  146. package/src/renderers/image.tsx +11 -9
  147. package/src/renderers/link.tsx +25 -7
  148. package/src/renderers/list.tsx +9 -12
  149. package/src/renderers/math.tsx +4 -4
  150. package/src/renderers/paragraph.tsx +3 -3
  151. package/src/renderers/table.tsx +77 -51
  152. package/src/theme.ts +3 -3
  153. package/src/use-markdown-stream.ts +22 -16
  154. package/src/utils/link-security.ts +22 -0
  155. package/src/utils/stream-timeline.ts +72 -0
@@ -1,14 +1,14 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import { View, Text, StyleSheet, Platform, type ViewStyle } from "react-native";
3
3
  import { useMarkdownContext } from "../MarkdownContext";
4
4
 
5
- interface ListProps {
5
+ type ListProps = {
6
6
  ordered: boolean;
7
7
  start?: number;
8
8
  depth: number;
9
9
  children: ReactNode;
10
10
  style?: ViewStyle;
11
- }
11
+ };
12
12
 
13
13
  export const List: FC<ListProps> = ({ depth, children, style }) => {
14
14
  const { theme } = useMarkdownContext();
@@ -32,13 +32,13 @@ export const List: FC<ListProps> = ({ depth, children, style }) => {
32
32
  );
33
33
  };
34
34
 
35
- interface ListItemProps {
35
+ type ListItemProps = {
36
36
  children: ReactNode;
37
37
  index: number;
38
38
  ordered: boolean;
39
39
  start: number;
40
40
  style?: ViewStyle;
41
- }
41
+ };
42
42
 
43
43
  export const ListItem: FC<ListItemProps> = ({
44
44
  children,
@@ -82,11 +82,11 @@ export const ListItem: FC<ListItemProps> = ({
82
82
  );
83
83
  };
84
84
 
85
- interface TaskListItemProps {
85
+ type TaskListItemProps = {
86
86
  children: ReactNode;
87
87
  checked: boolean;
88
88
  style?: ViewStyle;
89
- }
89
+ };
90
90
 
91
91
  export const TaskListItem: FC<TaskListItemProps> = ({
92
92
  children,
@@ -134,12 +134,9 @@ export const TaskListItem: FC<TaskListItemProps> = ({
134
134
  return (
135
135
  <View style={[styles.taskListItem, style]}>
136
136
  <View
137
- style={[
138
- styles.taskCheckbox,
139
- checked && styles.taskCheckboxChecked,
140
- ]}
137
+ style={[styles.taskCheckbox, checked && styles.taskCheckboxChecked]}
141
138
  >
142
- {checked && <Text style={styles.taskCheckboxText}>✓</Text>}
139
+ {checked ? <Text style={styles.taskCheckboxText}>✓</Text> : null}
143
140
  </View>
144
141
  <View style={styles.taskContent}>{children}</View>
145
142
  </View>
@@ -26,10 +26,10 @@ try {
26
26
  // ignored
27
27
  }
28
28
 
29
- interface MathInlineProps {
29
+ type MathInlineProps = {
30
30
  content?: string;
31
31
  style?: ViewStyle;
32
- }
32
+ };
33
33
 
34
34
  const createMathStyles = (theme: MarkdownTheme) =>
35
35
  StyleSheet.create({
@@ -115,10 +115,10 @@ export const MathInline: FC<MathInlineProps> = ({ content, style }) => {
115
115
  );
116
116
  };
117
117
 
118
- interface MathBlockProps {
118
+ type MathBlockProps = {
119
119
  content?: string;
120
120
  style?: ViewStyle;
121
- }
121
+ };
122
122
 
123
123
  export const MathBlock: FC<MathBlockProps> = ({ content, style }) => {
124
124
  const { theme } = useMarkdownContext();
@@ -1,12 +1,12 @@
1
- import { ReactNode, useMemo, type FC } from "react";
1
+ import { useMemo, type FC, type ReactNode } from "react";
2
2
  import { View, StyleSheet, type StyleProp, type ViewStyle } from "react-native";
3
3
  import { useMarkdownContext } from "../MarkdownContext";
4
4
 
5
- interface ParagraphProps {
5
+ type ParagraphProps = {
6
6
  children: ReactNode;
7
7
  inListItem?: boolean;
8
8
  style?: StyleProp<ViewStyle>;
9
- }
9
+ };
10
10
 
11
11
  export const Paragraph: FC<ParagraphProps> = ({
12
12
  children,
@@ -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";
23
21
  import { useMarkdownContext, type NodeRendererProps } from "../MarkdownContext";
24
- import { MarkdownTheme } from "../theme";
22
+ import type { MarkdownNode } from "../headless";
23
+ import type { MarkdownTheme } from "../theme";
25
24
 
26
25
  type TableData = {
27
26
  headers: MarkdownNode[];
@@ -60,11 +59,11 @@ 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
69
  type: "SET_WIDTHS";
@@ -84,7 +83,7 @@ export const TableRenderer: FC<TableRendererProps> = ({
84
83
  const { theme } = useMarkdownContext();
85
84
  const { headers, rows, alignments } = useMemo(
86
85
  () => extractTableData(node),
87
- [node]
86
+ [node],
88
87
  );
89
88
 
90
89
  const columnCount = headers.length;
@@ -95,25 +94,44 @@ export const TableRenderer: FC<TableRendererProps> = ({
95
94
  const measuredCells = useRef<Set<string>>(new Set());
96
95
  const widthsCalculated = useRef(false);
97
96
 
97
+ const expectedCellKeys = useMemo(() => {
98
+ const keys: string[] = [];
99
+
100
+ headers.forEach((_, colIndex) => {
101
+ keys.push(`header-${colIndex}`);
102
+ });
103
+
104
+ rows.forEach((row, rowIndex) => {
105
+ row.forEach((_, colIndex) => {
106
+ keys.push(`cell-${rowIndex}-${colIndex}`);
107
+ });
108
+ });
109
+
110
+ return keys;
111
+ }, [headers, rows]);
112
+
113
+ useEffect(() => {
114
+ measuredWidths.current.clear();
115
+ measuredCells.current.clear();
116
+ widthsCalculated.current = false;
117
+ dispatch({ type: "SET_WIDTHS", payload: [] });
118
+ }, [expectedCellKeys]);
119
+
98
120
  const onCellLayout = useCallback(
99
121
  (cellKey: string, width: number) => {
100
- measuredWidths.current.set(cellKey, width);
101
- measuredCells.current.add(cellKey);
122
+ if (widthsCalculated.current) return;
102
123
 
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
- }
124
+ measuredWidths.current.set(cellKey, width);
125
+ if (!measuredCells.current.has(cellKey)) {
126
+ measuredCells.current.add(cellKey);
111
127
  }
112
128
 
113
- const allCellsMeasured = [...expectedCells].every((key) =>
114
- measuredCells.current.has(key)
129
+ if (measuredCells.current.size < expectedCellKeys.length) return;
130
+
131
+ const allCellsMeasured = expectedCellKeys.every((key) =>
132
+ measuredCells.current.has(key),
115
133
  );
116
- if (!allCellsMeasured || widthsCalculated.current) return;
134
+ if (!allCellsMeasured) return;
117
135
 
118
136
  const maxWidths: number[] = new Array(columnCount).fill(0);
119
137
 
@@ -124,6 +142,7 @@ export const TableRenderer: FC<TableRendererProps> = ({
124
142
  }
125
143
 
126
144
  for (let row = 0; row < rows.length; row++) {
145
+ if (col >= rows[row].length) continue;
127
146
  const cellWidth = measuredWidths.current.get(`cell-${row}-${col}`);
128
147
  if (cellWidth && cellWidth > 0) {
129
148
  maxWidths[col] = Math.max(maxWidths[col], cellWidth);
@@ -136,11 +155,11 @@ export const TableRenderer: FC<TableRendererProps> = ({
136
155
  widthsCalculated.current = true;
137
156
  dispatch({ type: "SET_WIDTHS", payload: maxWidths });
138
157
  },
139
- [columnCount, rows.length]
158
+ [columnCount, expectedCellKeys, rows],
140
159
  );
141
160
 
142
161
  const getAlignment = (
143
- index: number
162
+ index: number,
144
163
  ): "flex-start" | "center" | "flex-end" => {
145
164
  const align = alignments[index];
146
165
  if (align === "center") return "center";
@@ -154,30 +173,17 @@ export const TableRenderer: FC<TableRendererProps> = ({
154
173
 
155
174
  return (
156
175
  <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) => (
176
+ {!hasWidths && (
177
+ <View style={styles.measurementContainer}>
178
+ <View style={styles.measurementRow}>
179
+ {headers.map((cell, colIndex) => (
174
180
  <View
175
- key={`measure-cell-${rowIndex}-${colIndex}`}
181
+ key={`measure-header-${colIndex}`}
176
182
  style={styles.measurementCell}
177
183
  onLayout={(e: LayoutChangeEvent) => {
178
184
  onCellLayout(
179
- `cell-${rowIndex}-${colIndex}`,
180
- e.nativeEvent.layout.width
185
+ `header-${colIndex}`,
186
+ e.nativeEvent.layout.width,
181
187
  );
182
188
  }}
183
189
  >
@@ -185,17 +191,39 @@ export const TableRenderer: FC<TableRendererProps> = ({
185
191
  </View>
186
192
  ))}
187
193
  </View>
188
- ))}
189
- </View>
194
+ {rows.map((row, rowIndex) => (
195
+ <View key={`measure-row-${rowIndex}`} style={styles.measurementRow}>
196
+ {row.map((cell, colIndex) => (
197
+ <View
198
+ key={`measure-cell-${rowIndex}-${colIndex}`}
199
+ style={styles.measurementCell}
200
+ onLayout={(e: LayoutChangeEvent) => {
201
+ onCellLayout(
202
+ `cell-${rowIndex}-${colIndex}`,
203
+ e.nativeEvent.layout.width,
204
+ );
205
+ }}
206
+ >
207
+ <CellContent
208
+ node={cell}
209
+ Renderer={Renderer}
210
+ styles={styles}
211
+ />
212
+ </View>
213
+ ))}
214
+ </View>
215
+ ))}
216
+ </View>
217
+ )}
190
218
 
191
- {hasWidths && (
219
+ {hasWidths ? (
192
220
  <ScrollView
193
221
  horizontal
194
222
  showsHorizontalScrollIndicator
195
223
  style={styles.tableScroll}
196
224
  bounces={false}
197
225
  >
198
- <View style={[styles.table, { backgroundColor: style?.backgroundColor ?? theme.colors.surface ?? "#111827" }]}>
226
+ <View style={styles.table}>
199
227
  <View style={styles.headerRow}>
200
228
  {headers.map((cell, colIndex) => (
201
229
  <View
@@ -225,7 +253,7 @@ export const TableRenderer: FC<TableRendererProps> = ({
225
253
  style={[
226
254
  styles.bodyRow,
227
255
  rowIndex === rows.length - 1 && styles.lastRow,
228
- rowIndex % 2 === 0 ? styles.evenRow : styles.oddRow,
256
+ rowIndex % 2 === 1 && styles.oddRow,
229
257
  ]}
230
258
  >
231
259
  {row.map((cell, colIndex) => (
@@ -252,7 +280,7 @@ export const TableRenderer: FC<TableRendererProps> = ({
252
280
  ))}
253
281
  </View>
254
282
  </ScrollView>
255
- )}
283
+ ) : null}
256
284
  </View>
257
285
  );
258
286
  };
@@ -311,6 +339,7 @@ const createTableStyles = (theme: MarkdownTheme) => {
311
339
  overflow: "hidden",
312
340
  borderWidth: 1,
313
341
  borderColor: colors.tableBorder || "#374151",
342
+ backgroundColor: colors.surface || "#111827",
314
343
  },
315
344
  headerRow: {
316
345
  flexDirection: "row",
@@ -323,9 +352,6 @@ const createTableStyles = (theme: MarkdownTheme) => {
323
352
  borderBottomWidth: 1,
324
353
  borderBottomColor: colors.tableBorder || "#374151",
325
354
  },
326
- evenRow: {
327
- backgroundColor: colors.tableRowEven || "transparent",
328
- },
329
355
  oddRow: {
330
356
  backgroundColor: colors.tableRowOdd || "rgba(255,255,255,0.02)",
331
357
  },
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 {
@@ -0,0 +1,22 @@
1
+ const ALLOWED_LINK_PROTOCOLS = new Set([
2
+ "http:",
3
+ "https:",
4
+ "mailto:",
5
+ "tel:",
6
+ "sms:",
7
+ ]);
8
+
9
+ export const normalizeLinkHref = (href: string): string | null => {
10
+ const normalizedHref = href.trim();
11
+ return normalizedHref.length > 0 ? normalizedHref : null;
12
+ };
13
+
14
+ export const getAllowedExternalHref = (href: string): string | null => {
15
+ const protocolMatch = href.match(/^([a-z][a-z0-9+.-]*):/i);
16
+ if (!protocolMatch) return null;
17
+
18
+ const protocol = `${protocolMatch[1].toLowerCase()}:`;
19
+ if (!ALLOWED_LINK_PROTOCOLS.has(protocol)) return null;
20
+
21
+ return href;
22
+ };
@@ -0,0 +1,72 @@
1
+ export type TimestampEntry = {
2
+ index: number;
3
+ time: number;
4
+ };
5
+
6
+ export type TimestampTimeline = {
7
+ entries: TimestampEntry[];
8
+ monotonic: boolean;
9
+ };
10
+
11
+ export const createTimestampTimeline = (
12
+ timestamps?: Record<number, number>,
13
+ ): TimestampTimeline => {
14
+ if (!timestamps) {
15
+ return { entries: [], monotonic: true };
16
+ }
17
+
18
+ const entries = Object.keys(timestamps)
19
+ .map(Number)
20
+ .filter((index) => Number.isFinite(index))
21
+ .sort((a, b) => a - b)
22
+ .map((index) => ({
23
+ index,
24
+ time: timestamps[index],
25
+ }))
26
+ .filter((entry) => Number.isFinite(entry.time));
27
+
28
+ let monotonic = true;
29
+ for (let i = 1; i < entries.length; i += 1) {
30
+ if (entries[i].time < entries[i - 1].time) {
31
+ monotonic = false;
32
+ break;
33
+ }
34
+ }
35
+
36
+ return { entries, monotonic };
37
+ };
38
+
39
+ export const resolveHighlightPosition = (
40
+ timeline: TimestampTimeline,
41
+ currentTimeMs: number,
42
+ ): number => {
43
+ const { entries, monotonic } = timeline;
44
+ if (entries.length === 0) return 0;
45
+
46
+ if (!monotonic) {
47
+ let wordIndex = 0;
48
+ for (const entry of entries) {
49
+ if (currentTimeMs >= entry.time) {
50
+ wordIndex = entry.index + 1;
51
+ }
52
+ }
53
+ return wordIndex;
54
+ }
55
+
56
+ let left = 0;
57
+ let right = entries.length - 1;
58
+ let resolvedIndex = -1;
59
+
60
+ while (left <= right) {
61
+ const mid = (left + right) >> 1;
62
+ if (entries[mid].time <= currentTimeMs) {
63
+ resolvedIndex = mid;
64
+ left = mid + 1;
65
+ } else {
66
+ right = mid - 1;
67
+ }
68
+ }
69
+
70
+ if (resolvedIndex === -1) return 0;
71
+ return entries[resolvedIndex].index + 1;
72
+ };