react-native-nitro-markdown 0.4.1 → 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.
- package/README.md +329 -322
- package/lib/commonjs/MarkdownContext.js +2 -1
- package/lib/commonjs/MarkdownContext.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +3 -1
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +51 -35
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/code.js +3 -3
- package/lib/commonjs/renderers/code.js.map +1 -1
- package/lib/commonjs/renderers/heading.js +1 -1
- package/lib/commonjs/renderers/heading.js.map +1 -1
- package/lib/commonjs/renderers/image.js +7 -5
- package/lib/commonjs/renderers/image.js.map +1 -1
- package/lib/commonjs/renderers/link.js +15 -3
- package/lib/commonjs/renderers/link.js.map +1 -1
- package/lib/commonjs/renderers/list.js +2 -2
- package/lib/commonjs/renderers/list.js.map +1 -1
- package/lib/commonjs/renderers/table.js +32 -15
- package/lib/commonjs/renderers/table.js.map +1 -1
- package/lib/commonjs/use-markdown-stream.js +16 -14
- package/lib/commonjs/use-markdown-stream.js.map +1 -1
- package/lib/commonjs/utils/link-security.js +21 -0
- package/lib/commonjs/utils/link-security.js.map +1 -0
- package/lib/commonjs/utils/stream-timeline.js +62 -0
- package/lib/commonjs/utils/stream-timeline.js.map +1 -0
- package/lib/module/MarkdownContext.js +2 -1
- package/lib/module/MarkdownContext.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +3 -1
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +52 -36
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/blockquote.js.map +1 -1
- package/lib/module/renderers/code.js +3 -3
- package/lib/module/renderers/code.js.map +1 -1
- package/lib/module/renderers/heading.js +1 -1
- package/lib/module/renderers/heading.js.map +1 -1
- package/lib/module/renderers/image.js +7 -5
- package/lib/module/renderers/image.js.map +1 -1
- package/lib/module/renderers/link.js +15 -3
- package/lib/module/renderers/link.js.map +1 -1
- package/lib/module/renderers/list.js +2 -2
- package/lib/module/renderers/list.js.map +1 -1
- package/lib/module/renderers/paragraph.js.map +1 -1
- package/lib/module/renderers/table.js +33 -16
- package/lib/module/renderers/table.js.map +1 -1
- package/lib/module/use-markdown-stream.js +16 -14
- package/lib/module/use-markdown-stream.js.map +1 -1
- package/lib/module/utils/link-security.js +15 -0
- package/lib/module/utils/link-security.js.map +1 -0
- package/lib/module/utils/stream-timeline.js +56 -0
- package/lib/module/utils/stream-timeline.js.map +1 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts +3 -3
- package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
- package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/headless.d.ts +2 -2
- package/lib/typescript/commonjs/headless.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +2 -2
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts +9 -4
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
- package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
- package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
- package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
- package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
- package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme.d.ts +2 -2
- package/lib/typescript/commonjs/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
- package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
- package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
- package/lib/typescript/module/Markdown.nitro.d.ts +3 -3
- package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/module/MarkdownContext.d.ts +26 -25
- package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
- package/lib/typescript/module/headless.d.ts +2 -2
- package/lib/typescript/module/headless.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +2 -2
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts +9 -4
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
- package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
- package/lib/typescript/module/renderers/code.d.ts +5 -5
- package/lib/typescript/module/renderers/code.d.ts.map +1 -1
- package/lib/typescript/module/renderers/heading.d.ts +3 -3
- package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
- package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
- package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
- package/lib/typescript/module/renderers/image.d.ts +2 -2
- package/lib/typescript/module/renderers/image.d.ts.map +1 -1
- package/lib/typescript/module/renderers/link.d.ts +3 -3
- package/lib/typescript/module/renderers/link.d.ts.map +1 -1
- package/lib/typescript/module/renderers/list.d.ts +7 -7
- package/lib/typescript/module/renderers/list.d.ts.map +1 -1
- package/lib/typescript/module/renderers/math.d.ts +4 -4
- package/lib/typescript/module/renderers/math.d.ts.map +1 -1
- package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
- package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
- package/lib/typescript/module/renderers/table.d.ts +3 -3
- package/lib/typescript/module/renderers/table.d.ts.map +1 -1
- package/lib/typescript/module/theme.d.ts +2 -2
- package/lib/typescript/module/theme.d.ts.map +1 -1
- package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/utils/link-security.d.ts +3 -0
- package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
- package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
- package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +0 -1
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +0 -1
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +0 -1
- package/package.json +4 -3
- package/src/Markdown.nitro.ts +5 -3
- package/src/MarkdownContext.ts +31 -25
- package/src/headless.ts +2 -2
- package/src/index.ts +1 -0
- package/src/markdown-stream.tsx +6 -10
- package/src/markdown.tsx +86 -45
- package/src/renderers/blockquote.tsx +4 -4
- package/src/renderers/code.tsx +11 -9
- package/src/renderers/heading.tsx +4 -4
- package/src/renderers/horizontal-rule.tsx +3 -3
- package/src/renderers/image.tsx +11 -9
- package/src/renderers/link.tsx +25 -7
- package/src/renderers/list.tsx +9 -12
- package/src/renderers/math.tsx +4 -4
- package/src/renderers/paragraph.tsx +3 -3
- package/src/renderers/table.tsx +74 -46
- package/src/theme.ts +3 -3
- package/src/use-markdown-stream.ts +22 -16
- package/src/utils/link-security.ts +22 -0
- package/src/utils/stream-timeline.ts +72 -0
package/src/renderers/list.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
139
|
+
{checked ? <Text style={styles.taskCheckboxText}>✓</Text> : null}
|
|
143
140
|
</View>
|
|
144
141
|
<View style={styles.taskContent}>{children}</View>
|
|
145
142
|
</View>
|
package/src/renderers/math.tsx
CHANGED
|
@@ -26,10 +26,10 @@ try {
|
|
|
26
26
|
// ignored
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|
package/src/renderers/table.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
measuredCells.current.add(cellKey);
|
|
122
|
+
if (widthsCalculated.current) return;
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
158
|
-
<View style={styles.
|
|
159
|
-
{
|
|
160
|
-
|
|
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-
|
|
181
|
+
key={`measure-header-${colIndex}`}
|
|
176
182
|
style={styles.measurementCell}
|
|
177
183
|
onLayout={(e: LayoutChangeEvent) => {
|
|
178
184
|
onCellLayout(
|
|
179
|
-
`
|
|
180
|
-
e.nativeEvent.layout.width
|
|
185
|
+
`header-${colIndex}`,
|
|
186
|
+
e.nativeEvent.layout.width,
|
|
181
187
|
);
|
|
182
188
|
}}
|
|
183
189
|
>
|
|
@@ -185,10 +191,32 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
185
191
|
</View>
|
|
186
192
|
))}
|
|
187
193
|
</View>
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
@@ -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
|
};
|
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
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
[
|
|
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
|
+
};
|