react-native-nitro-markdown 0.7.1 → 0.7.2
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 +114 -24
- package/lib/commonjs/MarkdownContext.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +43 -10
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +2 -1
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/image.js +9 -0
- package/lib/commonjs/renderers/image.js.map +1 -1
- package/lib/commonjs/renderers/math.js +16 -0
- package/lib/commonjs/renderers/math.js.map +1 -1
- package/lib/commonjs/use-markdown-stream.js +2 -6
- package/lib/commonjs/use-markdown-stream.js.map +1 -1
- package/lib/commonjs/utils/incremental-ast.js +45 -5
- package/lib/commonjs/utils/incremental-ast.js.map +1 -1
- package/lib/module/MarkdownContext.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +44 -11
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +2 -1
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/image.js +10 -1
- package/lib/module/renderers/image.js.map +1 -1
- package/lib/module/renderers/math.js +16 -0
- package/lib/module/renderers/math.js.map +1 -1
- package/lib/module/use-markdown-stream.js +2 -6
- package/lib/module/use-markdown-stream.js.map +1 -1
- package/lib/module/utils/incremental-ast.js +43 -4
- package/lib/module/utils/incremental-ast.js.map +1 -1
- package/lib/typescript/commonjs/MarkdownContext.d.ts +1 -0
- package/lib/typescript/commonjs/MarkdownContext.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.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts +1 -0
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -1
- package/lib/typescript/module/MarkdownContext.d.ts +1 -0
- package/lib/typescript/module/MarkdownContext.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.map +1 -1
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/image.d.ts.map +1 -1
- package/lib/typescript/module/renderers/math.d.ts.map +1 -1
- package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/utils/incremental-ast.d.ts +1 -0
- package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/MarkdownContext.ts +2 -0
- package/src/index.ts +1 -0
- package/src/markdown-stream.tsx +53 -12
- package/src/markdown.tsx +7 -1
- package/src/renderers/image.tsx +11 -0
- package/src/renderers/math.tsx +18 -0
- package/src/use-markdown-stream.ts +2 -6
- package/src/utils/incremental-ast.ts +81 -4
package/src/index.ts
CHANGED
package/src/markdown-stream.tsx
CHANGED
|
@@ -3,10 +3,12 @@ import {
|
|
|
3
3
|
useEffect,
|
|
4
4
|
useRef,
|
|
5
5
|
useCallback,
|
|
6
|
+
useMemo,
|
|
6
7
|
startTransition,
|
|
7
8
|
type FC,
|
|
8
9
|
} from "react";
|
|
9
10
|
import type { MarkdownNode } from "./headless";
|
|
11
|
+
import type { ParserOptions } from "./Markdown.nitro";
|
|
10
12
|
import { Markdown, type MarkdownProps } from "./markdown";
|
|
11
13
|
import type { MarkdownSession } from "./specs/MarkdownSession.nitro";
|
|
12
14
|
import {
|
|
@@ -21,6 +23,22 @@ const normalizeOffset = (value: number): number | null => {
|
|
|
21
23
|
return Math.floor(value);
|
|
22
24
|
};
|
|
23
25
|
|
|
26
|
+
const normalizeParserOptions = (
|
|
27
|
+
options?: ParserOptions,
|
|
28
|
+
): ParserOptions | undefined => {
|
|
29
|
+
if (!options) return undefined;
|
|
30
|
+
|
|
31
|
+
const gfm = options.gfm;
|
|
32
|
+
const math = options.math;
|
|
33
|
+
const html = options.html;
|
|
34
|
+
|
|
35
|
+
if (gfm === undefined && math === undefined && html === undefined) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { gfm, math, html };
|
|
40
|
+
};
|
|
41
|
+
|
|
24
42
|
const resolveStreamText = ({
|
|
25
43
|
forceFullSync,
|
|
26
44
|
pendingFrom,
|
|
@@ -115,9 +133,21 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
115
133
|
...props
|
|
116
134
|
}) => {
|
|
117
135
|
const activeSession = resolveMarkdownSession(session);
|
|
136
|
+
const parserOptionGfm = options?.gfm;
|
|
137
|
+
const parserOptionMath = options?.math;
|
|
138
|
+
const parserOptionHtml = options?.html;
|
|
139
|
+
const parserOptions = useMemo(
|
|
140
|
+
() =>
|
|
141
|
+
normalizeParserOptions({
|
|
142
|
+
gfm: parserOptionGfm,
|
|
143
|
+
math: parserOptionMath,
|
|
144
|
+
html: parserOptionHtml,
|
|
145
|
+
}),
|
|
146
|
+
[parserOptionGfm, parserOptionMath, parserOptionHtml],
|
|
147
|
+
);
|
|
118
148
|
const parseText = useCallback(
|
|
119
|
-
(text: string): MarkdownNode => parseMarkdownAst(text,
|
|
120
|
-
[
|
|
149
|
+
(text: string): MarkdownNode => parseMarkdownAst(text, parserOptions),
|
|
150
|
+
[parserOptions],
|
|
121
151
|
);
|
|
122
152
|
const createEmptyAst = (): MarkdownNode => ({
|
|
123
153
|
type: "document",
|
|
@@ -184,13 +214,19 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
184
214
|
pendingToRef.current = null;
|
|
185
215
|
forceFullSyncRef.current = false;
|
|
186
216
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
let latest: string;
|
|
218
|
+
try {
|
|
219
|
+
latest = resolveStreamText({
|
|
220
|
+
forceFullSync,
|
|
221
|
+
pendingFrom,
|
|
222
|
+
pendingTo,
|
|
223
|
+
previousText: previousState.text,
|
|
224
|
+
session: activeSession,
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
warnStreamError("[NitroMarkdown] Failed to read stream session:", error);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
194
230
|
if (latest === previousState.text) return;
|
|
195
231
|
|
|
196
232
|
const nextAst = hasBeforeParsePlugins
|
|
@@ -198,7 +234,7 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
198
234
|
: getNextStreamAst({
|
|
199
235
|
allowIncremental,
|
|
200
236
|
nextText: latest,
|
|
201
|
-
options,
|
|
237
|
+
options: parserOptions,
|
|
202
238
|
previousAst: previousState.ast,
|
|
203
239
|
previousText: previousState.text,
|
|
204
240
|
});
|
|
@@ -236,6 +272,8 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
236
272
|
|
|
237
273
|
try {
|
|
238
274
|
unsubscribe = activeSession.addListener((from, to) => {
|
|
275
|
+
if (!mountedRef.current) return;
|
|
276
|
+
|
|
239
277
|
const nextFrom = normalizeOffset(from);
|
|
240
278
|
const nextTo = normalizeOffset(to);
|
|
241
279
|
|
|
@@ -259,6 +297,10 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
259
297
|
}
|
|
260
298
|
|
|
261
299
|
return () => {
|
|
300
|
+
pendingUpdateRef.current = false;
|
|
301
|
+
pendingFromRef.current = null;
|
|
302
|
+
pendingToRef.current = null;
|
|
303
|
+
forceFullSyncRef.current = false;
|
|
262
304
|
try {
|
|
263
305
|
unsubscribe?.();
|
|
264
306
|
} catch (error) {
|
|
@@ -279,8 +321,7 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
279
321
|
}, [
|
|
280
322
|
allowIncremental,
|
|
281
323
|
hasBeforeParsePlugins,
|
|
282
|
-
|
|
283
|
-
plugins,
|
|
324
|
+
parserOptions,
|
|
284
325
|
activeSession,
|
|
285
326
|
updateIntervalMs,
|
|
286
327
|
updateStrategy,
|
package/src/markdown.tsx
CHANGED
|
@@ -483,8 +483,14 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
483
483
|
math: parserOptionMath,
|
|
484
484
|
html: parserOptionHtml,
|
|
485
485
|
});
|
|
486
|
+
const shouldCloneSourceAst =
|
|
487
|
+
sourceAst &&
|
|
488
|
+
(Boolean(astTransform) ||
|
|
489
|
+
sortedPlugins?.some((plugin) => plugin.afterParse) === true);
|
|
486
490
|
let parsedAst = sourceAst
|
|
487
|
-
?
|
|
491
|
+
? shouldCloneSourceAst
|
|
492
|
+
? cloneMarkdownNode(sourceAst)
|
|
493
|
+
: sourceAst
|
|
488
494
|
: parseCache
|
|
489
495
|
? getCachedParsedAst(markdownToParse, parserOptions)
|
|
490
496
|
: parseWithNativeParser(markdownToParse, parserOptions);
|
package/src/renderers/image.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
useState,
|
|
3
3
|
useEffect,
|
|
4
4
|
useMemo,
|
|
5
|
+
useRef,
|
|
5
6
|
type ReactNode,
|
|
6
7
|
type FC,
|
|
7
8
|
type ComponentType,
|
|
@@ -47,6 +48,7 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
|
|
|
47
48
|
const [loading, setLoading] = useState(true);
|
|
48
49
|
const [error, setError] = useState(false);
|
|
49
50
|
const [aspectRatio, setAspectRatio] = useState<number | undefined>(undefined);
|
|
51
|
+
const mountedRef = useRef(true);
|
|
50
52
|
const { theme, imageOptions } = useMarkdownContext();
|
|
51
53
|
const allowedImageHref = useMemo(
|
|
52
54
|
() => getAllowedImageHref(url, imageOptions),
|
|
@@ -115,6 +117,13 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
|
|
|
115
117
|
[theme, aspectRatio],
|
|
116
118
|
);
|
|
117
119
|
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
mountedRef.current = true;
|
|
122
|
+
return () => {
|
|
123
|
+
mountedRef.current = false;
|
|
124
|
+
};
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
118
127
|
useEffect(() => {
|
|
119
128
|
let isMounted = true;
|
|
120
129
|
setLoading(true);
|
|
@@ -236,9 +245,11 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
|
|
|
236
245
|
accessibilityRole={accessibilityLabel ? "image" : undefined}
|
|
237
246
|
accessibilityLabel={accessibilityLabel}
|
|
238
247
|
onLoad={() => {
|
|
248
|
+
if (!mountedRef.current) return;
|
|
239
249
|
setLoading(false);
|
|
240
250
|
}}
|
|
241
251
|
onError={() => {
|
|
252
|
+
if (!mountedRef.current) return;
|
|
242
253
|
setLoading(false);
|
|
243
254
|
setError(true);
|
|
244
255
|
}}
|
package/src/renderers/math.tsx
CHANGED
|
@@ -247,6 +247,14 @@ export const MathInline: FC<MathInlineProps> = ({ content, style }) => {
|
|
|
247
247
|
const { theme } = useMarkdownContext();
|
|
248
248
|
const styles = getCachedStyles(mathStylesCache, theme, createMathStyles);
|
|
249
249
|
const [hasRenderError, setHasRenderError] = useState(false);
|
|
250
|
+
const mountedRef = useRef(true);
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
mountedRef.current = true;
|
|
254
|
+
return () => {
|
|
255
|
+
mountedRef.current = false;
|
|
256
|
+
};
|
|
257
|
+
}, []);
|
|
250
258
|
|
|
251
259
|
if (!content) return null;
|
|
252
260
|
|
|
@@ -260,6 +268,7 @@ export const MathInline: FC<MathInlineProps> = ({ content, style }) => {
|
|
|
260
268
|
color={theme.colors.text}
|
|
261
269
|
style={styles.ratexInline}
|
|
262
270
|
onError={() => {
|
|
271
|
+
if (!mountedRef.current) return;
|
|
263
272
|
setHasRenderError(true);
|
|
264
273
|
}}
|
|
265
274
|
/>
|
|
@@ -283,6 +292,14 @@ export const MathBlock: FC<MathBlockProps> = ({ content, style }) => {
|
|
|
283
292
|
const { theme } = useMarkdownContext();
|
|
284
293
|
const styles = getCachedStyles(mathStylesCache, theme, createMathStyles);
|
|
285
294
|
const [hasRenderError, setHasRenderError] = useState(false);
|
|
295
|
+
const mountedRef = useRef(true);
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
mountedRef.current = true;
|
|
299
|
+
return () => {
|
|
300
|
+
mountedRef.current = false;
|
|
301
|
+
};
|
|
302
|
+
}, []);
|
|
286
303
|
|
|
287
304
|
if (!content) return null;
|
|
288
305
|
|
|
@@ -300,6 +317,7 @@ export const MathBlock: FC<MathBlockProps> = ({ content, style }) => {
|
|
|
300
317
|
color={theme.colors.text}
|
|
301
318
|
style={styles.ratexBlock}
|
|
302
319
|
onError={() => {
|
|
320
|
+
if (!mountedRef.current) return;
|
|
303
321
|
setHasRenderError(true);
|
|
304
322
|
}}
|
|
305
323
|
/>
|
|
@@ -21,13 +21,9 @@ export function useMarkdownSession(initialText?: string) {
|
|
|
21
21
|
const session = sessionRef.current!;
|
|
22
22
|
return () => {
|
|
23
23
|
try {
|
|
24
|
-
session.
|
|
24
|
+
session.dispose();
|
|
25
25
|
} finally {
|
|
26
|
-
|
|
27
|
-
session.dispose();
|
|
28
|
-
} finally {
|
|
29
|
-
sessionRef.current = null;
|
|
30
|
-
}
|
|
26
|
+
sessionRef.current = null;
|
|
31
27
|
}
|
|
32
28
|
};
|
|
33
29
|
}, []);
|
|
@@ -152,6 +152,83 @@ const endsAtBlockBoundary = (text: string): boolean => {
|
|
|
152
152
|
return text.endsWith("\n") || text.endsWith("\r");
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
+
const nodesHaveMatchingMetadata = (
|
|
156
|
+
previousNode: MarkdownNode,
|
|
157
|
+
nextNode: MarkdownNode,
|
|
158
|
+
): boolean => {
|
|
159
|
+
return (
|
|
160
|
+
previousNode.type === nextNode.type &&
|
|
161
|
+
previousNode.content === nextNode.content &&
|
|
162
|
+
previousNode.level === nextNode.level &&
|
|
163
|
+
previousNode.href === nextNode.href &&
|
|
164
|
+
previousNode.title === nextNode.title &&
|
|
165
|
+
previousNode.alt === nextNode.alt &&
|
|
166
|
+
previousNode.language === nextNode.language &&
|
|
167
|
+
previousNode.ordered === nextNode.ordered &&
|
|
168
|
+
previousNode.start === nextNode.start &&
|
|
169
|
+
previousNode.checked === nextNode.checked &&
|
|
170
|
+
previousNode.isHeader === nextNode.isHeader &&
|
|
171
|
+
previousNode.align === nextNode.align &&
|
|
172
|
+
previousNode.beg === nextNode.beg &&
|
|
173
|
+
previousNode.end === nextNode.end
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const reuseStableAstNodes = (
|
|
178
|
+
previousNode: MarkdownNode,
|
|
179
|
+
nextNode: MarkdownNode,
|
|
180
|
+
): MarkdownNode => {
|
|
181
|
+
if (previousNode.type !== nextNode.type) {
|
|
182
|
+
return nextNode;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const hasMatchingMetadata = nodesHaveMatchingMetadata(previousNode, nextNode);
|
|
186
|
+
const previousChildren = previousNode.children;
|
|
187
|
+
const nextChildren = nextNode.children;
|
|
188
|
+
|
|
189
|
+
if (!previousChildren || !nextChildren) {
|
|
190
|
+
return hasMatchingMetadata && previousChildren === nextChildren
|
|
191
|
+
? previousNode
|
|
192
|
+
: nextNode;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (previousChildren.length !== nextChildren.length) {
|
|
196
|
+
return {
|
|
197
|
+
...nextNode,
|
|
198
|
+
children: nextChildren.map((nextChild, index) => {
|
|
199
|
+
const previousChild = previousChildren[index];
|
|
200
|
+
return previousChild
|
|
201
|
+
? reuseStableAstNodes(previousChild, nextChild)
|
|
202
|
+
: nextChild;
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let hasChildChange = false;
|
|
208
|
+
const children = nextChildren.map((nextChild, index) => {
|
|
209
|
+
const child = reuseStableAstNodes(previousChildren[index], nextChild);
|
|
210
|
+
hasChildChange ||= child !== previousChildren[index];
|
|
211
|
+
return child;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (hasMatchingMetadata && !hasChildChange) {
|
|
215
|
+
return previousNode;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
...nextNode,
|
|
220
|
+
children,
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const parseAstWithStableNodes = (
|
|
225
|
+
previousAst: MarkdownNode,
|
|
226
|
+
text: string,
|
|
227
|
+
options?: ParserOptions,
|
|
228
|
+
): MarkdownNode => {
|
|
229
|
+
return reuseStableAstNodes(previousAst, parseAst(text, options));
|
|
230
|
+
};
|
|
231
|
+
|
|
155
232
|
export type IncrementalAstInput = {
|
|
156
233
|
allowIncremental?: boolean;
|
|
157
234
|
nextText: string;
|
|
@@ -168,7 +245,7 @@ export const getNextStreamAst = ({
|
|
|
168
245
|
previousText,
|
|
169
246
|
}: IncrementalAstInput): MarkdownNode => {
|
|
170
247
|
if (!allowIncremental || !nextText.startsWith(previousText)) {
|
|
171
|
-
return
|
|
248
|
+
return parseAstWithStableNodes(previousAst, nextText, options);
|
|
172
249
|
}
|
|
173
250
|
|
|
174
251
|
const appendedChunk = nextText.slice(previousText.length);
|
|
@@ -194,7 +271,7 @@ export const getNextStreamAst = ({
|
|
|
194
271
|
|
|
195
272
|
if (!PLAIN_TEXT_APPEND_PATTERN.test(appendedChunk)) {
|
|
196
273
|
if (endsAtBlockBoundary(previousText)) {
|
|
197
|
-
return
|
|
274
|
+
return parseAstWithStableNodes(previousAst, nextText, options);
|
|
198
275
|
}
|
|
199
276
|
|
|
200
277
|
const textAppendedAst = appendPlainTextToAst(
|
|
@@ -208,12 +285,12 @@ export const getNextStreamAst = ({
|
|
|
208
285
|
}
|
|
209
286
|
|
|
210
287
|
if (insideFencedCodeBlock) {
|
|
211
|
-
return
|
|
288
|
+
return parseAstWithStableNodes(previousAst, nextText, options);
|
|
212
289
|
}
|
|
213
290
|
|
|
214
291
|
// Correctness-first fallback: full reparse for all non-trivial appends.
|
|
215
292
|
// Incremental append is only used for plain text chunks at the true trailing leaf.
|
|
216
|
-
return
|
|
293
|
+
return parseAstWithStableNodes(previousAst, nextText, options);
|
|
217
294
|
};
|
|
218
295
|
|
|
219
296
|
export const parseMarkdownAst = (
|