react-native-nitro-markdown 0.7.0 → 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 +188 -488
- package/lib/commonjs/MarkdownContext.js.map +1 -1
- package/lib/commonjs/MarkdownSession.js +6 -2
- package/lib/commonjs/MarkdownSession.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +49 -14
- 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 +25 -10
- 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/MarkdownSession.js +6 -2
- package/lib/module/MarkdownSession.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +50 -15
- 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 +24 -11
- 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/MarkdownSession.d.ts +1 -1
- package/lib/typescript/commonjs/MarkdownSession.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +2 -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 +4 -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/MarkdownSession.d.ts +1 -1
- package/lib/typescript/module/MarkdownSession.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +2 -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 +4 -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 +2 -1
- package/src/MarkdownContext.ts +2 -0
- package/src/MarkdownSession.ts +9 -2
- package/src/index.ts +2 -0
- package/src/markdown-stream.tsx +64 -18
- 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 +55 -19
- package/src/utils/incremental-ast.ts +81 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createMarkdownSession } from "./MarkdownSession";
|
|
2
2
|
export type MarkdownSession = ReturnType<typeof createMarkdownSession>;
|
|
3
|
-
export declare function useMarkdownSession(): {
|
|
3
|
+
export declare function useMarkdownSession(initialText?: string): {
|
|
4
4
|
getSession: () => import("./specs/MarkdownSession.nitro").MarkdownSession;
|
|
5
5
|
isStreaming: boolean;
|
|
6
6
|
setIsStreaming: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
@@ -10,6 +10,9 @@ export declare function useMarkdownSession(): {
|
|
|
10
10
|
reset: (text: string) => void;
|
|
11
11
|
replace: (from: number, to: number, text: string) => number;
|
|
12
12
|
};
|
|
13
|
+
export type MarkdownSessionController = ReturnType<typeof useMarkdownSession>;
|
|
14
|
+
export declare function isMarkdownSessionController(value: MarkdownSession | MarkdownSessionController): value is MarkdownSessionController;
|
|
15
|
+
export declare function resolveMarkdownSession(session: MarkdownSession | MarkdownSessionController): MarkdownSession;
|
|
13
16
|
export declare function useStream(timestamps?: Record<number, number>): {
|
|
14
17
|
isPlaying: boolean;
|
|
15
18
|
setIsPlaying: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-markdown-stream.d.ts","sourceRoot":"","sources":["../../../src/use-markdown-stream.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAO1D,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEvE,wBAAgB,kBAAkB;;;;;;
|
|
1
|
+
{"version":3,"file":"use-markdown-stream.d.ts","sourceRoot":"","sources":["../../../src/use-markdown-stream.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAO1D,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEvE,wBAAgB,kBAAkB,CAAC,WAAW,CAAC,EAAE,MAAM;;;;;;6BAuCT,MAAM;kBAMjB,MAAM;oBAIJ,MAAM,MAAM,MAAM,QAAQ,MAAM;EA0BpE;AAED,MAAM,MAAM,yBAAyB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9E,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,eAAe,GAAG,yBAAyB,GACjD,KAAK,IAAI,yBAAyB,CAEpC;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,eAAe,GAAG,yBAAyB,GACnD,eAAe,CAMjB;AAED,wBAAgB,SAAS,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;;;0BAgBzC,MAAM;;;;;;6BAxEoB,MAAM;kBAMjB,MAAM;oBAIJ,MAAM,MAAM,MAAM,QAAQ,MAAM;EAmFpE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type MarkdownNode } from "../headless";
|
|
2
2
|
import type { ParserOptions } from "../Markdown.nitro";
|
|
3
|
+
export declare const reuseStableAstNodes: (previousNode: MarkdownNode, nextNode: MarkdownNode) => MarkdownNode;
|
|
3
4
|
export type IncrementalAstInput = {
|
|
4
5
|
allowIncremental?: boolean;
|
|
5
6
|
nextText: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"incremental-ast.d.ts","sourceRoot":"","sources":["../../../../src/utils/incremental-ast.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"incremental-ast.d.ts","sourceRoot":"","sources":["../../../../src/utils/incremental-ast.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AA2KvD,eAAO,MAAM,mBAAmB,GAC9B,cAAc,YAAY,EAC1B,UAAU,YAAY,KACrB,YA0CF,CAAC;AAUF,MAAM,MAAM,mBAAmB,GAAG;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,WAAW,EAAE,YAAY,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,qEAM9B,mBAAmB,KAAG,YAgDxB,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,MAAM,MAAM,EACZ,UAAU,aAAa,KACtB,YAEF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-markdown",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "High-performance Markdown parser for React Native using Nitro Modules and md4c",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"clean": "rimraf lib nitrogen/generated",
|
|
58
58
|
"codegen": "nitrogen --logLevel=\"debug\"",
|
|
59
59
|
"lint": "bunx eslint src --max-warnings=0 --ignore-pattern 'src/**/__tests__/**' --ignore-pattern 'src/**/*.nitro.ts'",
|
|
60
|
+
"lint:fix": "bunx eslint src --fix --ignore-pattern 'src/**/__tests__/**' --ignore-pattern 'src/**/*.nitro.ts'",
|
|
60
61
|
"typecheck": "tsc --noEmit",
|
|
61
62
|
"test": "jest",
|
|
62
63
|
"test:coverage": "jest --coverage",
|
package/src/MarkdownContext.ts
CHANGED
package/src/MarkdownSession.ts
CHANGED
|
@@ -3,6 +3,13 @@ import type { MarkdownSession as MarkdownSessionSpec } from "./specs/MarkdownSes
|
|
|
3
3
|
|
|
4
4
|
export type MarkdownSession = MarkdownSessionSpec;
|
|
5
5
|
|
|
6
|
-
export function createMarkdownSession(): MarkdownSession {
|
|
7
|
-
|
|
6
|
+
export function createMarkdownSession(initialText?: string): MarkdownSession {
|
|
7
|
+
const session =
|
|
8
|
+
NitroModules.createHybridObject<MarkdownSession>("MarkdownSession");
|
|
9
|
+
|
|
10
|
+
if (initialText !== undefined) {
|
|
11
|
+
session.reset(initialText);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return session;
|
|
8
15
|
}
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ export type {
|
|
|
48
48
|
LinkPressHandler,
|
|
49
49
|
MarkdownContextValue,
|
|
50
50
|
CustomRendererPropsByNode,
|
|
51
|
+
MarkdownRenderers,
|
|
51
52
|
TableOptions,
|
|
52
53
|
} from "./MarkdownContext";
|
|
53
54
|
|
|
@@ -77,6 +78,7 @@ export { MathInline, MathBlock } from "./renderers/math";
|
|
|
77
78
|
export { createMarkdownSession } from "./MarkdownSession";
|
|
78
79
|
export type { MarkdownSession } from "./MarkdownSession";
|
|
79
80
|
export { useMarkdownSession, useStream } from "./use-markdown-stream";
|
|
81
|
+
export type { MarkdownSessionController } from "./use-markdown-stream";
|
|
80
82
|
|
|
81
83
|
export type {
|
|
82
84
|
HighlightedToken,
|
package/src/markdown-stream.tsx
CHANGED
|
@@ -3,12 +3,18 @@ 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";
|
|
14
|
+
import {
|
|
15
|
+
resolveMarkdownSession,
|
|
16
|
+
type MarkdownSessionController,
|
|
17
|
+
} from "./use-markdown-stream";
|
|
12
18
|
import { getNextStreamAst, parseMarkdownAst } from "./utils/incremental-ast";
|
|
13
19
|
|
|
14
20
|
const normalizeOffset = (value: number): number | null => {
|
|
@@ -17,6 +23,22 @@ const normalizeOffset = (value: number): number | null => {
|
|
|
17
23
|
return Math.floor(value);
|
|
18
24
|
};
|
|
19
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
|
+
|
|
20
42
|
const resolveStreamText = ({
|
|
21
43
|
forceFullSync,
|
|
22
44
|
pendingFrom,
|
|
@@ -71,7 +93,7 @@ export type MarkdownStreamProps = {
|
|
|
71
93
|
/**
|
|
72
94
|
* The active MarkdownSession to stream content from.
|
|
73
95
|
*/
|
|
74
|
-
session: MarkdownSession;
|
|
96
|
+
session: MarkdownSession | MarkdownSessionController;
|
|
75
97
|
/**
|
|
76
98
|
* Throttle UI updates when updateStrategy is "interval".
|
|
77
99
|
* Ignored when updateStrategy is "raf".
|
|
@@ -110,15 +132,28 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
110
132
|
plugins,
|
|
111
133
|
...props
|
|
112
134
|
}) => {
|
|
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
|
+
);
|
|
113
148
|
const parseText = useCallback(
|
|
114
|
-
(text: string): MarkdownNode => parseMarkdownAst(text,
|
|
115
|
-
[
|
|
149
|
+
(text: string): MarkdownNode => parseMarkdownAst(text, parserOptions),
|
|
150
|
+
[parserOptions],
|
|
116
151
|
);
|
|
117
152
|
const createEmptyAst = (): MarkdownNode => ({
|
|
118
153
|
type: "document",
|
|
119
154
|
children: [],
|
|
120
155
|
});
|
|
121
|
-
const initialText =
|
|
156
|
+
const initialText = activeSession.getAllText();
|
|
122
157
|
const hasBeforeParsePlugins =
|
|
123
158
|
plugins?.some((plugin) => typeof plugin.beforeParse === "function") ??
|
|
124
159
|
false;
|
|
@@ -148,7 +183,7 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
148
183
|
}, []);
|
|
149
184
|
|
|
150
185
|
useEffect(() => {
|
|
151
|
-
const initialText =
|
|
186
|
+
const initialText = activeSession.getAllText();
|
|
152
187
|
const initialState = {
|
|
153
188
|
text: initialText,
|
|
154
189
|
ast: hasBeforeParsePlugins ? createEmptyAst() : parseText(initialText),
|
|
@@ -159,7 +194,7 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
159
194
|
forceFullSyncRef.current = false;
|
|
160
195
|
renderStateRef.current = initialState;
|
|
161
196
|
setRenderState(initialState);
|
|
162
|
-
}, [hasBeforeParsePlugins, parseText
|
|
197
|
+
}, [activeSession, hasBeforeParsePlugins, parseText]);
|
|
163
198
|
|
|
164
199
|
useEffect(() => {
|
|
165
200
|
const flushUpdate = () => {
|
|
@@ -179,13 +214,19 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
179
214
|
pendingToRef.current = null;
|
|
180
215
|
forceFullSyncRef.current = false;
|
|
181
216
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
}
|
|
189
230
|
if (latest === previousState.text) return;
|
|
190
231
|
|
|
191
232
|
const nextAst = hasBeforeParsePlugins
|
|
@@ -193,7 +234,7 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
193
234
|
: getNextStreamAst({
|
|
194
235
|
allowIncremental,
|
|
195
236
|
nextText: latest,
|
|
196
|
-
options,
|
|
237
|
+
options: parserOptions,
|
|
197
238
|
previousAst: previousState.ast,
|
|
198
239
|
previousText: previousState.text,
|
|
199
240
|
});
|
|
@@ -230,7 +271,9 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
230
271
|
let unsubscribe: (() => void) | null = null;
|
|
231
272
|
|
|
232
273
|
try {
|
|
233
|
-
unsubscribe =
|
|
274
|
+
unsubscribe = activeSession.addListener((from, to) => {
|
|
275
|
+
if (!mountedRef.current) return;
|
|
276
|
+
|
|
234
277
|
const nextFrom = normalizeOffset(from);
|
|
235
278
|
const nextTo = normalizeOffset(to);
|
|
236
279
|
|
|
@@ -254,6 +297,10 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
254
297
|
}
|
|
255
298
|
|
|
256
299
|
return () => {
|
|
300
|
+
pendingUpdateRef.current = false;
|
|
301
|
+
pendingFromRef.current = null;
|
|
302
|
+
pendingToRef.current = null;
|
|
303
|
+
forceFullSyncRef.current = false;
|
|
257
304
|
try {
|
|
258
305
|
unsubscribe?.();
|
|
259
306
|
} catch (error) {
|
|
@@ -274,9 +321,8 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
274
321
|
}, [
|
|
275
322
|
allowIncremental,
|
|
276
323
|
hasBeforeParsePlugins,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
session,
|
|
324
|
+
parserOptions,
|
|
325
|
+
activeSession,
|
|
280
326
|
updateIntervalMs,
|
|
281
327
|
updateStrategy,
|
|
282
328
|
useTransitionUpdates,
|
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
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useCallback, useState, useEffect } from "react";
|
|
1
|
+
import { useRef, useCallback, useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { createMarkdownSession } from "./MarkdownSession";
|
|
3
3
|
import {
|
|
4
4
|
createTimestampTimeline,
|
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export type MarkdownSession = ReturnType<typeof createMarkdownSession>;
|
|
10
10
|
|
|
11
|
-
export function useMarkdownSession() {
|
|
11
|
+
export function useMarkdownSession(initialText?: string) {
|
|
12
12
|
const sessionRef = useRef<MarkdownSession | null>(null);
|
|
13
|
+
const initialTextRef = useRef(initialText);
|
|
13
14
|
if (sessionRef.current === null) {
|
|
14
|
-
sessionRef.current = createMarkdownSession();
|
|
15
|
+
sessionRef.current = createMarkdownSession(initialText);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
@@ -20,17 +21,22 @@ export function useMarkdownSession() {
|
|
|
20
21
|
const session = sessionRef.current!;
|
|
21
22
|
return () => {
|
|
22
23
|
try {
|
|
23
|
-
session.
|
|
24
|
+
session.dispose();
|
|
24
25
|
} finally {
|
|
25
|
-
|
|
26
|
-
session.dispose();
|
|
27
|
-
} finally {
|
|
28
|
-
sessionRef.current = null;
|
|
29
|
-
}
|
|
26
|
+
sessionRef.current = null;
|
|
30
27
|
}
|
|
31
28
|
};
|
|
32
29
|
}, []);
|
|
33
30
|
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (initialText === undefined || initialTextRef.current === initialText) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
initialTextRef.current = initialText;
|
|
37
|
+
sessionRef.current!.reset(initialText);
|
|
38
|
+
}, [initialText]);
|
|
39
|
+
|
|
34
40
|
const stop = useCallback(() => {
|
|
35
41
|
setIsStreaming(false);
|
|
36
42
|
}, []);
|
|
@@ -55,16 +61,46 @@ export function useMarkdownSession() {
|
|
|
55
61
|
return sessionRef.current!.replace(from, to, text);
|
|
56
62
|
}, []);
|
|
57
63
|
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
return useMemo(
|
|
65
|
+
() => ({
|
|
66
|
+
getSession,
|
|
67
|
+
isStreaming,
|
|
68
|
+
setIsStreaming,
|
|
69
|
+
stop,
|
|
70
|
+
clear,
|
|
71
|
+
setHighlight,
|
|
72
|
+
reset,
|
|
73
|
+
replace,
|
|
74
|
+
}),
|
|
75
|
+
[
|
|
76
|
+
clear,
|
|
77
|
+
getSession,
|
|
78
|
+
isStreaming,
|
|
79
|
+
replace,
|
|
80
|
+
reset,
|
|
81
|
+
setHighlight,
|
|
82
|
+
setIsStreaming,
|
|
83
|
+
stop,
|
|
84
|
+
],
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type MarkdownSessionController = ReturnType<typeof useMarkdownSession>;
|
|
89
|
+
|
|
90
|
+
export function isMarkdownSessionController(
|
|
91
|
+
value: MarkdownSession | MarkdownSessionController,
|
|
92
|
+
): value is MarkdownSessionController {
|
|
93
|
+
return typeof Reflect.get(value, "getSession") === "function";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function resolveMarkdownSession(
|
|
97
|
+
session: MarkdownSession | MarkdownSessionController,
|
|
98
|
+
): MarkdownSession {
|
|
99
|
+
if (isMarkdownSessionController(session)) {
|
|
100
|
+
return session.getSession();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return session;
|
|
68
104
|
}
|
|
69
105
|
|
|
70
106
|
export function useStream(timestamps?: Record<number, number>) {
|
|
@@ -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 = (
|