react-native-nitro-markdown 0.4.3 → 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.
- package/README.md +351 -22
- package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +27 -8
- package/cpp/bindings/HybridMarkdownParser.cpp +216 -66
- package/cpp/bindings/HybridMarkdownParser.hpp +2 -0
- package/ios/HybridMarkdownSession.swift +33 -7
- package/lib/commonjs/headless.js +41 -5
- package/lib/commonjs/headless.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +107 -13
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +180 -25
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/code.js +1 -0
- package/lib/commonjs/renderers/code.js.map +1 -1
- package/lib/commonjs/renderers/table.js +116 -24
- package/lib/commonjs/renderers/table.js.map +1 -1
- package/lib/commonjs/utils/incremental-ast.js +153 -0
- package/lib/commonjs/utils/incremental-ast.js.map +1 -0
- package/lib/module/headless.js +37 -4
- package/lib/module/headless.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +108 -14
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +182 -27
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/code.js +1 -0
- package/lib/module/renderers/code.js.map +1 -1
- package/lib/module/renderers/table.js +116 -24
- package/lib/module/renderers/table.js.map +1 -1
- package/lib/module/utils/incremental-ast.js +147 -0
- package/lib/module/utils/incremental-ast.js.map +1 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts +2 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/headless.d.ts +13 -0
- package/lib/typescript/commonjs/headless.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +6 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts +53 -1
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/table.d.ts +1 -1
- package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +5 -2
- package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
- package/lib/typescript/module/Markdown.nitro.d.ts +2 -0
- package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/module/headless.d.ts +13 -0
- package/lib/typescript/module/headless.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +6 -1
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts +53 -1
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/code.d.ts.map +1 -1
- package/lib/typescript/module/renderers/table.d.ts +1 -1
- package/lib/typescript/module/renderers/table.d.ts.map +1 -1
- package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +5 -2
- package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
- package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
- package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
- package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +18 -6
- package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +4 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +11 -3
- package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.hpp +31 -0
- package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +20 -2
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +4 -2
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +34 -9
- package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +4 -2
- package/package.json +4 -3
- package/src/Markdown.nitro.ts +2 -0
- package/src/headless.ts +42 -4
- package/src/index.ts +7 -0
- package/src/markdown-stream.tsx +163 -15
- package/src/markdown.tsx +339 -24
- package/src/renderers/code.tsx +5 -1
- package/src/renderers/table.tsx +212 -66
- package/src/specs/MarkdownSession.nitro.ts +6 -2
- package/src/utils/incremental-ast.ts +224 -0
package/src/renderers/table.tsx
CHANGED
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
type ViewStyle,
|
|
19
19
|
type LayoutChangeEvent,
|
|
20
20
|
} from "react-native";
|
|
21
|
+
import { getTextContent, type MarkdownNode } from "../headless";
|
|
21
22
|
import { useMarkdownContext, type NodeRendererProps } from "../MarkdownContext";
|
|
22
|
-
import type { MarkdownNode } from "../headless";
|
|
23
23
|
import type { MarkdownTheme } from "../theme";
|
|
24
24
|
|
|
25
25
|
type TableData = {
|
|
@@ -66,15 +66,86 @@ type TableRendererProps = {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
type ColumnWidthsAction = {
|
|
69
|
-
type: "
|
|
69
|
+
type: "RESET_WIDTHS" | "SET_MONOTONIC_WIDTHS";
|
|
70
70
|
payload: number[];
|
|
71
71
|
};
|
|
72
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
|
+
|
|
73
83
|
const columnWidthsReducer = (state: number[], action: ColumnWidthsAction) => {
|
|
74
|
-
if (action.type === "
|
|
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
|
+
|
|
75
109
|
return state;
|
|
76
110
|
};
|
|
77
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
|
+
|
|
78
149
|
export const TableRenderer: FC<TableRendererProps> = ({
|
|
79
150
|
node,
|
|
80
151
|
Renderer,
|
|
@@ -88,11 +159,27 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
88
159
|
|
|
89
160
|
const columnCount = headers.length;
|
|
90
161
|
const styles = useMemo(() => createTableStyles(theme), [theme]);
|
|
162
|
+
const estimatedColumnWidths = useMemo(
|
|
163
|
+
() => estimateColumnWidths(headers, rows, columnCount),
|
|
164
|
+
[headers, rows, columnCount],
|
|
165
|
+
);
|
|
91
166
|
|
|
92
|
-
const [columnWidths, dispatch] = useReducer(
|
|
167
|
+
const [columnWidths, dispatch] = useReducer(
|
|
168
|
+
columnWidthsReducer,
|
|
169
|
+
estimatedColumnWidths,
|
|
170
|
+
);
|
|
93
171
|
const measuredWidths = useRef<Map<string, number>>(new Map());
|
|
94
172
|
const measuredCells = useRef<Set<string>>(new Set());
|
|
95
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
|
+
);
|
|
96
183
|
|
|
97
184
|
const expectedCellKeys = useMemo(() => {
|
|
98
185
|
const keys: string[] = [];
|
|
@@ -110,16 +197,62 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
110
197
|
return keys;
|
|
111
198
|
}, [headers, rows]);
|
|
112
199
|
|
|
200
|
+
const expectedCellKeySignature = useMemo(
|
|
201
|
+
() => expectedCellKeys.join("|"),
|
|
202
|
+
[expectedCellKeys],
|
|
203
|
+
);
|
|
204
|
+
|
|
113
205
|
useEffect(() => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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]);
|
|
119
252
|
|
|
120
253
|
const onCellLayout = useCallback(
|
|
121
254
|
(cellKey: string, width: number) => {
|
|
122
|
-
if (widthsCalculated.current) return;
|
|
255
|
+
if (width <= 0 || widthsCalculated.current || !needsMeasurement) return;
|
|
123
256
|
|
|
124
257
|
measuredWidths.current.set(cellKey, width);
|
|
125
258
|
if (!measuredCells.current.has(cellKey)) {
|
|
@@ -133,7 +266,7 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
133
266
|
);
|
|
134
267
|
if (!allCellsMeasured) return;
|
|
135
268
|
|
|
136
|
-
const maxWidths: number[] =
|
|
269
|
+
const maxWidths: number[] = [...columnWidthsRef.current];
|
|
137
270
|
|
|
138
271
|
for (let col = 0; col < columnCount; col++) {
|
|
139
272
|
const headerWidth = measuredWidths.current.get(`header-${col}`);
|
|
@@ -149,13 +282,17 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
149
282
|
}
|
|
150
283
|
}
|
|
151
284
|
|
|
152
|
-
maxWidths[col] = Math.max(
|
|
285
|
+
maxWidths[col] = Math.max(
|
|
286
|
+
maxWidths[col] + COLUMN_MEASUREMENT_PADDING,
|
|
287
|
+
MIN_COLUMN_WIDTH,
|
|
288
|
+
);
|
|
153
289
|
}
|
|
154
290
|
|
|
155
291
|
widthsCalculated.current = true;
|
|
156
|
-
|
|
292
|
+
setNeedsMeasurement(false);
|
|
293
|
+
dispatch({ type: "RESET_WIDTHS", payload: maxWidths });
|
|
157
294
|
},
|
|
158
|
-
[columnCount, expectedCellKeys, rows],
|
|
295
|
+
[columnCount, expectedCellKeys, needsMeasurement, rows],
|
|
159
296
|
);
|
|
160
297
|
|
|
161
298
|
const getAlignment = (
|
|
@@ -170,10 +307,11 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
170
307
|
if (columnCount === 0) return null;
|
|
171
308
|
|
|
172
309
|
const hasWidths = columnWidths.length === columnCount;
|
|
310
|
+
const resolvedWidths = hasWidths ? columnWidths : estimatedColumnWidths;
|
|
173
311
|
|
|
174
312
|
return (
|
|
175
313
|
<View style={[styles.container, style]}>
|
|
176
|
-
{
|
|
314
|
+
{needsMeasurement ? (
|
|
177
315
|
<View style={styles.measurementContainer}>
|
|
178
316
|
<View style={styles.measurementRow}>
|
|
179
317
|
{headers.map((cell, colIndex) => (
|
|
@@ -214,24 +352,62 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
214
352
|
</View>
|
|
215
353
|
))}
|
|
216
354
|
</View>
|
|
217
|
-
)}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
355
|
+
) : null}
|
|
356
|
+
|
|
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
|
+
]}
|
|
225
371
|
>
|
|
226
|
-
<View style={styles.
|
|
227
|
-
|
|
228
|
-
|
|
372
|
+
<View style={styles.headerRow}>
|
|
373
|
+
{headers.map((cell, colIndex) => (
|
|
374
|
+
<View
|
|
375
|
+
key={`header-${colIndex}`}
|
|
376
|
+
style={[
|
|
377
|
+
styles.headerCell,
|
|
378
|
+
{
|
|
379
|
+
width: resolvedWidths[colIndex] ?? MIN_COLUMN_WIDTH,
|
|
380
|
+
alignItems: getAlignment(colIndex),
|
|
381
|
+
},
|
|
382
|
+
colIndex === columnCount - 1 && styles.lastCell,
|
|
383
|
+
]}
|
|
384
|
+
>
|
|
385
|
+
<CellContent
|
|
386
|
+
node={cell}
|
|
387
|
+
Renderer={Renderer}
|
|
388
|
+
styles={styles}
|
|
389
|
+
textStyle={styles.headerText}
|
|
390
|
+
/>
|
|
391
|
+
</View>
|
|
392
|
+
))}
|
|
393
|
+
</View>
|
|
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) => (
|
|
229
405
|
<View
|
|
230
|
-
key={`
|
|
406
|
+
key={`cell-${rowIndex}-${colIndex}`}
|
|
231
407
|
style={[
|
|
232
|
-
styles.
|
|
408
|
+
styles.bodyCell,
|
|
233
409
|
{
|
|
234
|
-
width:
|
|
410
|
+
width: resolvedWidths[colIndex] ?? MIN_COLUMN_WIDTH,
|
|
235
411
|
alignItems: getAlignment(colIndex),
|
|
236
412
|
},
|
|
237
413
|
colIndex === columnCount - 1 && styles.lastCell,
|
|
@@ -241,46 +417,14 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
241
417
|
node={cell}
|
|
242
418
|
Renderer={Renderer}
|
|
243
419
|
styles={styles}
|
|
244
|
-
textStyle={styles.
|
|
420
|
+
textStyle={styles.cellText}
|
|
245
421
|
/>
|
|
246
422
|
</View>
|
|
247
423
|
))}
|
|
248
424
|
</View>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
key={`row-${rowIndex}`}
|
|
253
|
-
style={[
|
|
254
|
-
styles.bodyRow,
|
|
255
|
-
rowIndex === rows.length - 1 && styles.lastRow,
|
|
256
|
-
rowIndex % 2 === 1 && styles.oddRow,
|
|
257
|
-
]}
|
|
258
|
-
>
|
|
259
|
-
{row.map((cell, colIndex) => (
|
|
260
|
-
<View
|
|
261
|
-
key={`cell-${rowIndex}-${colIndex}`}
|
|
262
|
-
style={[
|
|
263
|
-
styles.bodyCell,
|
|
264
|
-
{
|
|
265
|
-
width: columnWidths[colIndex],
|
|
266
|
-
alignItems: getAlignment(colIndex),
|
|
267
|
-
},
|
|
268
|
-
colIndex === columnCount - 1 && styles.lastCell,
|
|
269
|
-
]}
|
|
270
|
-
>
|
|
271
|
-
<CellContent
|
|
272
|
-
node={cell}
|
|
273
|
-
Renderer={Renderer}
|
|
274
|
-
styles={styles}
|
|
275
|
-
textStyle={styles.cellText}
|
|
276
|
-
/>
|
|
277
|
-
</View>
|
|
278
|
-
))}
|
|
279
|
-
</View>
|
|
280
|
-
))}
|
|
281
|
-
</View>
|
|
282
|
-
</ScrollView>
|
|
283
|
-
) : null}
|
|
425
|
+
))}
|
|
426
|
+
</View>
|
|
427
|
+
</ScrollView>
|
|
284
428
|
</View>
|
|
285
429
|
);
|
|
286
430
|
};
|
|
@@ -339,7 +483,6 @@ const createTableStyles = (theme: MarkdownTheme) => {
|
|
|
339
483
|
overflow: "hidden",
|
|
340
484
|
borderWidth: 1,
|
|
341
485
|
borderColor: colors.tableBorder || "#374151",
|
|
342
|
-
backgroundColor: colors.surface || "#111827",
|
|
343
486
|
},
|
|
344
487
|
headerRow: {
|
|
345
488
|
flexDirection: "row",
|
|
@@ -352,6 +495,9 @@ const createTableStyles = (theme: MarkdownTheme) => {
|
|
|
352
495
|
borderBottomWidth: 1,
|
|
353
496
|
borderBottomColor: colors.tableBorder || "#374151",
|
|
354
497
|
},
|
|
498
|
+
evenRow: {
|
|
499
|
+
backgroundColor: colors.tableRowEven || "transparent",
|
|
500
|
+
},
|
|
355
501
|
oddRow: {
|
|
356
502
|
backgroundColor: colors.tableRowOdd || "rgba(255,255,255,0.02)",
|
|
357
503
|
},
|
|
@@ -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):
|
|
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:
|
|
17
|
+
addListener(listener: MarkdownSessionListener): () => void;
|
|
14
18
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseMarkdown,
|
|
3
|
+
parseMarkdownWithOptions,
|
|
4
|
+
type MarkdownNode,
|
|
5
|
+
} from "../headless";
|
|
6
|
+
import type { ParserOptions } from "../Markdown.nitro";
|
|
7
|
+
|
|
8
|
+
const PLAIN_TEXT_APPEND_PATTERN = /[`*_~[\]#!<>()|$\n\r]/;
|
|
9
|
+
const FENCE_LINE_PATTERN = /^ {0,3}(```+|~~~+)/;
|
|
10
|
+
|
|
11
|
+
const parseAst = (text: string, options?: ParserOptions): MarkdownNode => {
|
|
12
|
+
if (options) {
|
|
13
|
+
return parseMarkdownWithOptions(text, options);
|
|
14
|
+
}
|
|
15
|
+
return parseMarkdown(text);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const isInsideFencedCodeBlock = (text: string): boolean => {
|
|
19
|
+
const lines = text.split(/\r?\n/);
|
|
20
|
+
let openFenceChar: "`" | "~" | null = null;
|
|
21
|
+
let openFenceLength = 0;
|
|
22
|
+
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const fenceMatch = line.match(FENCE_LINE_PATTERN);
|
|
25
|
+
if (!fenceMatch) continue;
|
|
26
|
+
|
|
27
|
+
const marker = fenceMatch[1];
|
|
28
|
+
const markerChar = marker[0] as "`" | "~";
|
|
29
|
+
const markerLength = marker.length;
|
|
30
|
+
|
|
31
|
+
if (!openFenceChar) {
|
|
32
|
+
openFenceChar = markerChar;
|
|
33
|
+
openFenceLength = markerLength;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (markerChar === openFenceChar && markerLength >= openFenceLength) {
|
|
38
|
+
openFenceChar = null;
|
|
39
|
+
openFenceLength = 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return openFenceChar !== null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const containsFenceLine = (text: string): boolean => {
|
|
47
|
+
return text.split(/\r?\n/).some((line) => FENCE_LINE_PATTERN.test(line));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getTrailingLine = (text: string): string => {
|
|
51
|
+
const lastLineBreak = Math.max(
|
|
52
|
+
text.lastIndexOf("\n"),
|
|
53
|
+
text.lastIndexOf("\r"),
|
|
54
|
+
);
|
|
55
|
+
if (lastLineBreak === -1) return text;
|
|
56
|
+
return text.slice(lastLineBreak + 1);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getLeadingLine = (text: string): string => {
|
|
60
|
+
const newlineIndex = text.indexOf("\n");
|
|
61
|
+
const carriageReturnIndex = text.indexOf("\r");
|
|
62
|
+
|
|
63
|
+
const hasNewline = newlineIndex !== -1;
|
|
64
|
+
const hasCarriageReturn = carriageReturnIndex !== -1;
|
|
65
|
+
|
|
66
|
+
if (!hasNewline && !hasCarriageReturn) return text;
|
|
67
|
+
if (!hasNewline) return text.slice(0, carriageReturnIndex);
|
|
68
|
+
if (!hasCarriageReturn) return text.slice(0, newlineIndex);
|
|
69
|
+
|
|
70
|
+
return text.slice(0, Math.min(newlineIndex, carriageReturnIndex));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const hasSplitFenceBoundary = (
|
|
74
|
+
previousText: string,
|
|
75
|
+
appendedChunk: string,
|
|
76
|
+
): boolean => {
|
|
77
|
+
if (appendedChunk.length === 0) return false;
|
|
78
|
+
|
|
79
|
+
const candidateLine = `${getTrailingLine(previousText)}${getLeadingLine(
|
|
80
|
+
appendedChunk,
|
|
81
|
+
)}`;
|
|
82
|
+
if (candidateLine.length === 0) return false;
|
|
83
|
+
|
|
84
|
+
return FENCE_LINE_PATTERN.test(candidateLine);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const findTrailingLeafPath = (
|
|
88
|
+
node: MarkdownNode,
|
|
89
|
+
path: number[] = [],
|
|
90
|
+
): number[] => {
|
|
91
|
+
const children = node.children;
|
|
92
|
+
if (!children || children.length === 0) {
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lastIndex = children.length - 1;
|
|
97
|
+
return findTrailingLeafPath(children[lastIndex], [...path, lastIndex]);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getNodeAtPath = (
|
|
101
|
+
node: MarkdownNode,
|
|
102
|
+
path: readonly number[],
|
|
103
|
+
): MarkdownNode | null => {
|
|
104
|
+
let current: MarkdownNode = node;
|
|
105
|
+
for (const index of path) {
|
|
106
|
+
const child = current.children?.[index];
|
|
107
|
+
if (!child) return null;
|
|
108
|
+
current = child;
|
|
109
|
+
}
|
|
110
|
+
return current;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const appendPlainTextToAst = (
|
|
114
|
+
ast: MarkdownNode,
|
|
115
|
+
appendedChunk: string,
|
|
116
|
+
previousTextLength: number,
|
|
117
|
+
): MarkdownNode | null => {
|
|
118
|
+
if (appendedChunk.length === 0) return ast;
|
|
119
|
+
const path = findTrailingLeafPath(ast);
|
|
120
|
+
const leaf = getNodeAtPath(ast, path);
|
|
121
|
+
if (leaf?.type !== "text") return null;
|
|
122
|
+
if (typeof leaf.end !== "number" || leaf.end !== previousTextLength) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const delta = appendedChunk.length;
|
|
127
|
+
const update = (node: MarkdownNode, depth: number): MarkdownNode => {
|
|
128
|
+
if (depth === path.length) {
|
|
129
|
+
return {
|
|
130
|
+
...node,
|
|
131
|
+
content: (node.content ?? "") + appendedChunk,
|
|
132
|
+
end: typeof node.end === "number" ? node.end + delta : node.end,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const childIndex = path[depth];
|
|
137
|
+
const children = node.children?.map((child, index) =>
|
|
138
|
+
index === childIndex ? update(child, depth + 1) : child,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
...node,
|
|
143
|
+
end: typeof node.end === "number" ? node.end + delta : node.end,
|
|
144
|
+
children,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return update(ast, 0);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const endsAtBlockBoundary = (text: string): boolean => {
|
|
152
|
+
return text.endsWith("\n") || text.endsWith("\r");
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type IncrementalAstInput = {
|
|
156
|
+
allowIncremental?: boolean;
|
|
157
|
+
nextText: string;
|
|
158
|
+
options?: ParserOptions;
|
|
159
|
+
previousAst: MarkdownNode;
|
|
160
|
+
previousText: string;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const getNextStreamAst = ({
|
|
164
|
+
allowIncremental = true,
|
|
165
|
+
nextText,
|
|
166
|
+
options,
|
|
167
|
+
previousAst,
|
|
168
|
+
previousText,
|
|
169
|
+
}: IncrementalAstInput): MarkdownNode => {
|
|
170
|
+
if (!allowIncremental || !nextText.startsWith(previousText)) {
|
|
171
|
+
return parseAst(nextText, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const appendedChunk = nextText.slice(previousText.length);
|
|
175
|
+
if (appendedChunk.length === 0) {
|
|
176
|
+
return previousAst;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const insideFencedCodeBlock = isInsideFencedCodeBlock(previousText);
|
|
180
|
+
const hasFenceBoundary =
|
|
181
|
+
containsFenceLine(appendedChunk) ||
|
|
182
|
+
hasSplitFenceBoundary(previousText, appendedChunk);
|
|
183
|
+
|
|
184
|
+
if (insideFencedCodeBlock && !hasFenceBoundary) {
|
|
185
|
+
const fencedTextAppendAst = appendPlainTextToAst(
|
|
186
|
+
previousAst,
|
|
187
|
+
appendedChunk,
|
|
188
|
+
previousText.length,
|
|
189
|
+
);
|
|
190
|
+
if (fencedTextAppendAst) {
|
|
191
|
+
return fencedTextAppendAst;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!PLAIN_TEXT_APPEND_PATTERN.test(appendedChunk)) {
|
|
196
|
+
if (endsAtBlockBoundary(previousText)) {
|
|
197
|
+
return parseAst(nextText, options);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const textAppendedAst = appendPlainTextToAst(
|
|
201
|
+
previousAst,
|
|
202
|
+
appendedChunk,
|
|
203
|
+
previousText.length,
|
|
204
|
+
);
|
|
205
|
+
if (textAppendedAst) {
|
|
206
|
+
return textAppendedAst;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (insideFencedCodeBlock) {
|
|
211
|
+
return parseAst(nextText, options);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Correctness-first fallback: full reparse for all non-trivial appends.
|
|
215
|
+
// Incremental append is only used for plain text chunks at the true trailing leaf.
|
|
216
|
+
return parseAst(nextText, options);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const parseMarkdownAst = (
|
|
220
|
+
text: string,
|
|
221
|
+
options?: ParserOptions,
|
|
222
|
+
): MarkdownNode => {
|
|
223
|
+
return parseAst(text, options);
|
|
224
|
+
};
|