react-native-nitro-markdown 0.7.1 → 0.8.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 +179 -27
- package/ios/HybridMarkdownSession.swift +11 -4
- package/lib/commonjs/MarkdownContext.js +1 -3
- package/lib/commonjs/MarkdownContext.js.map +1 -1
- package/lib/commonjs/index.js +6 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +113 -29
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +96 -38
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/image.js +11 -2
- package/lib/commonjs/renderers/image.js.map +1 -1
- package/lib/commonjs/renderers/math.js +22 -2
- package/lib/commonjs/renderers/math.js.map +1 -1
- package/lib/commonjs/renderers/table/index.js +4 -4
- package/lib/commonjs/renderers/table/index.js.map +1 -1
- package/lib/commonjs/renderers/table/table-utils.js +1 -1
- package/lib/commonjs/renderers/table/table-utils.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/code-highlight.js +1 -1
- package/lib/commonjs/utils/code-highlight.js.map +1 -1
- package/lib/commonjs/utils/incremental-ast.js +67 -14
- package/lib/commonjs/utils/incremental-ast.js.map +1 -1
- package/lib/commonjs/utils/link-security.js +2 -2
- package/lib/commonjs/utils/link-security.js.map +1 -1
- package/lib/commonjs/utils/stream-timeline.js +13 -7
- package/lib/commonjs/utils/stream-timeline.js.map +1 -1
- package/lib/module/MarkdownContext.js +1 -3
- package/lib/module/MarkdownContext.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +113 -30
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +96 -38
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/image.js +12 -3
- package/lib/module/renderers/image.js.map +1 -1
- package/lib/module/renderers/math.js +22 -2
- package/lib/module/renderers/math.js.map +1 -1
- package/lib/module/renderers/table/index.js +4 -4
- package/lib/module/renderers/table/index.js.map +1 -1
- package/lib/module/renderers/table/table-utils.js +1 -1
- package/lib/module/renderers/table/table-utils.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/code-highlight.js +1 -1
- package/lib/module/utils/code-highlight.js.map +1 -1
- package/lib/module/utils/incremental-ast.js +65 -13
- package/lib/module/utils/incremental-ast.js.map +1 -1
- package/lib/module/utils/link-security.js +2 -2
- package/lib/module/utils/link-security.js.map +1 -1
- package/lib/module/utils/stream-timeline.js +13 -7
- package/lib/module/utils/stream-timeline.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 +3 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +28 -7
- 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/commonjs/utils/stream-timeline.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 +3 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +28 -7
- 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/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/MarkdownContext.ts +2 -2
- package/src/index.ts +10 -2
- package/src/markdown-stream.tsx +153 -36
- package/src/markdown.tsx +82 -42
- package/src/renderers/image.tsx +13 -2
- package/src/renderers/math.tsx +20 -2
- package/src/renderers/table/index.tsx +4 -4
- package/src/renderers/table/table-utils.ts +1 -1
- package/src/use-markdown-stream.ts +2 -6
- package/src/utils/code-highlight.ts +1 -1
- package/src/utils/incremental-ast.ts +104 -11
- package/src/utils/link-security.ts +3 -3
- package/src/utils/stream-timeline.ts +10 -7
package/src/markdown-stream.tsx
CHANGED
|
@@ -3,11 +3,14 @@ import {
|
|
|
3
3
|
useEffect,
|
|
4
4
|
useRef,
|
|
5
5
|
useCallback,
|
|
6
|
+
useMemo,
|
|
6
7
|
startTransition,
|
|
7
8
|
type FC,
|
|
9
|
+
type ReactNode,
|
|
8
10
|
} from "react";
|
|
9
11
|
import type { MarkdownNode } from "./headless";
|
|
10
|
-
import {
|
|
12
|
+
import type { ParserOptions } from "./Markdown.nitro";
|
|
13
|
+
import { Markdown, type MarkdownPlugin, type MarkdownProps } from "./markdown";
|
|
11
14
|
import type { MarkdownSession } from "./specs/MarkdownSession.nitro";
|
|
12
15
|
import {
|
|
13
16
|
resolveMarkdownSession,
|
|
@@ -21,6 +24,26 @@ const normalizeOffset = (value: number): number | null => {
|
|
|
21
24
|
return Math.floor(value);
|
|
22
25
|
};
|
|
23
26
|
|
|
27
|
+
const normalizeParserOptions = (
|
|
28
|
+
options?: ParserOptions,
|
|
29
|
+
): ParserOptions | undefined => {
|
|
30
|
+
if (!options) return undefined;
|
|
31
|
+
|
|
32
|
+
const gfm = options.gfm;
|
|
33
|
+
const math = options.math;
|
|
34
|
+
const html = options.html;
|
|
35
|
+
|
|
36
|
+
if (gfm === undefined && math === undefined && html === undefined) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const normalized: ParserOptions = {};
|
|
41
|
+
if (gfm !== undefined) normalized.gfm = gfm;
|
|
42
|
+
if (math !== undefined) normalized.math = math;
|
|
43
|
+
if (html !== undefined) normalized.html = html;
|
|
44
|
+
return normalized;
|
|
45
|
+
};
|
|
46
|
+
|
|
24
47
|
const resolveStreamText = ({
|
|
25
48
|
forceFullSync,
|
|
26
49
|
pendingFrom,
|
|
@@ -71,7 +94,11 @@ function warnStreamError(message: string, error: unknown): void {
|
|
|
71
94
|
}
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
export type
|
|
97
|
+
export type MarkdownStreamSourceAstStatus = "available" | "disabled";
|
|
98
|
+
|
|
99
|
+
export type MarkdownStreamSourceAstDisabledReason = "beforeParse-plugin";
|
|
100
|
+
|
|
101
|
+
export type UseMarkdownStreamStateOptions = {
|
|
75
102
|
/**
|
|
76
103
|
* The active MarkdownSession to stream content from.
|
|
77
104
|
*/
|
|
@@ -98,13 +125,32 @@ export type MarkdownStreamProps = {
|
|
|
98
125
|
* Automatically falls back to full parse when updates are not safely mergeable.
|
|
99
126
|
*/
|
|
100
127
|
incrementalParsing?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Parser options used for the stream source AST.
|
|
130
|
+
*/
|
|
131
|
+
options?: ParserOptions;
|
|
132
|
+
/**
|
|
133
|
+
* Plugins determine whether an optimized source AST can be passed through.
|
|
134
|
+
*/
|
|
135
|
+
plugins?: MarkdownPlugin[];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type MarkdownStreamState = {
|
|
139
|
+
text: string;
|
|
140
|
+
sourceAst?: MarkdownNode;
|
|
141
|
+
sourceAstStatus: MarkdownStreamSourceAstStatus;
|
|
142
|
+
sourceAstDisabledReason?: MarkdownStreamSourceAstDisabledReason;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type MarkdownStreamRenderProps = MarkdownStreamState & {
|
|
146
|
+
markdownProps: MarkdownProps;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type MarkdownStreamProps = UseMarkdownStreamStateOptions & {
|
|
150
|
+
renderMarkdown?: (props: MarkdownStreamRenderProps) => ReactNode;
|
|
101
151
|
} & Omit<MarkdownProps, "children" | "sourceAst">;
|
|
102
152
|
|
|
103
|
-
|
|
104
|
-
* A component that renders streaming Markdown from a MarkdownSession.
|
|
105
|
-
* It efficiently subscribes to session updates to minimize parent re-renders.
|
|
106
|
-
*/
|
|
107
|
-
export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
153
|
+
export function useMarkdownStreamState({
|
|
108
154
|
session,
|
|
109
155
|
updateIntervalMs = 50,
|
|
110
156
|
updateStrategy = "interval",
|
|
@@ -112,26 +158,43 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
112
158
|
incrementalParsing = true,
|
|
113
159
|
options,
|
|
114
160
|
plugins,
|
|
115
|
-
|
|
116
|
-
}) => {
|
|
161
|
+
}: UseMarkdownStreamStateOptions): MarkdownStreamState {
|
|
117
162
|
const activeSession = resolveMarkdownSession(session);
|
|
163
|
+
const parserOptionGfm = options?.gfm;
|
|
164
|
+
const parserOptionMath = options?.math;
|
|
165
|
+
const parserOptionHtml = options?.html;
|
|
166
|
+
const parserOptions = useMemo(
|
|
167
|
+
() =>
|
|
168
|
+
normalizeParserOptions(
|
|
169
|
+
Object.assign(
|
|
170
|
+
{},
|
|
171
|
+
parserOptionGfm === undefined ? null : { gfm: parserOptionGfm },
|
|
172
|
+
parserOptionMath === undefined ? null : { math: parserOptionMath },
|
|
173
|
+
parserOptionHtml === undefined ? null : { html: parserOptionHtml },
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
[parserOptionGfm, parserOptionMath, parserOptionHtml],
|
|
177
|
+
);
|
|
118
178
|
const parseText = useCallback(
|
|
119
|
-
(text: string): MarkdownNode => parseMarkdownAst(text,
|
|
120
|
-
[
|
|
179
|
+
(text: string): MarkdownNode => parseMarkdownAst(text, parserOptions),
|
|
180
|
+
[parserOptions],
|
|
121
181
|
);
|
|
122
182
|
const createEmptyAst = (): MarkdownNode => ({
|
|
123
183
|
type: "document",
|
|
124
184
|
children: [],
|
|
125
185
|
});
|
|
126
|
-
const initialText = activeSession.getAllText();
|
|
127
186
|
const hasBeforeParsePlugins =
|
|
128
187
|
plugins?.some((plugin) => typeof plugin.beforeParse === "function") ??
|
|
129
188
|
false;
|
|
130
|
-
const [renderState, setRenderState] = useState(() =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
189
|
+
const [renderState, setRenderState] = useState(() => {
|
|
190
|
+
const initialText = activeSession.getAllText();
|
|
191
|
+
return {
|
|
192
|
+
text: initialText,
|
|
193
|
+
ast: hasBeforeParsePlugins ? createEmptyAst() : parseText(initialText),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
134
196
|
const renderStateRef = useRef(renderState);
|
|
197
|
+
const didMountRef = useRef(false);
|
|
135
198
|
const pendingUpdateRef = useRef(false);
|
|
136
199
|
const pendingFromRef = useRef<number | null>(null);
|
|
137
200
|
const pendingToRef = useRef<number | null>(null);
|
|
@@ -153,6 +216,11 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
153
216
|
}, []);
|
|
154
217
|
|
|
155
218
|
useEffect(() => {
|
|
219
|
+
if (!didMountRef.current) {
|
|
220
|
+
didMountRef.current = true;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
156
224
|
const initialText = activeSession.getAllText();
|
|
157
225
|
const initialState = {
|
|
158
226
|
text: initialText,
|
|
@@ -184,13 +252,19 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
184
252
|
pendingToRef.current = null;
|
|
185
253
|
forceFullSyncRef.current = false;
|
|
186
254
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
255
|
+
let latest: string;
|
|
256
|
+
try {
|
|
257
|
+
latest = resolveStreamText({
|
|
258
|
+
forceFullSync,
|
|
259
|
+
pendingFrom,
|
|
260
|
+
pendingTo,
|
|
261
|
+
previousText: previousState.text,
|
|
262
|
+
session: activeSession,
|
|
263
|
+
});
|
|
264
|
+
} catch (error) {
|
|
265
|
+
warnStreamError("[NitroMarkdown] Failed to read stream session:", error);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
194
268
|
if (latest === previousState.text) return;
|
|
195
269
|
|
|
196
270
|
const nextAst = hasBeforeParsePlugins
|
|
@@ -198,9 +272,9 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
198
272
|
: getNextStreamAst({
|
|
199
273
|
allowIncremental,
|
|
200
274
|
nextText: latest,
|
|
201
|
-
options,
|
|
202
275
|
previousAst: previousState.ast,
|
|
203
276
|
previousText: previousState.text,
|
|
277
|
+
...(parserOptions ? { options: parserOptions } : {}),
|
|
204
278
|
});
|
|
205
279
|
const nextState = {
|
|
206
280
|
text: latest,
|
|
@@ -236,6 +310,8 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
236
310
|
|
|
237
311
|
try {
|
|
238
312
|
unsubscribe = activeSession.addListener((from, to) => {
|
|
313
|
+
if (!mountedRef.current) return;
|
|
314
|
+
|
|
239
315
|
const nextFrom = normalizeOffset(from);
|
|
240
316
|
const nextTo = normalizeOffset(to);
|
|
241
317
|
|
|
@@ -259,6 +335,10 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
259
335
|
}
|
|
260
336
|
|
|
261
337
|
return () => {
|
|
338
|
+
pendingUpdateRef.current = false;
|
|
339
|
+
pendingFromRef.current = null;
|
|
340
|
+
pendingToRef.current = null;
|
|
341
|
+
forceFullSyncRef.current = false;
|
|
262
342
|
try {
|
|
263
343
|
unsubscribe?.();
|
|
264
344
|
} catch (error) {
|
|
@@ -279,22 +359,59 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
279
359
|
}, [
|
|
280
360
|
allowIncremental,
|
|
281
361
|
hasBeforeParsePlugins,
|
|
282
|
-
|
|
283
|
-
plugins,
|
|
362
|
+
parserOptions,
|
|
284
363
|
activeSession,
|
|
285
364
|
updateIntervalMs,
|
|
286
365
|
updateStrategy,
|
|
287
366
|
useTransitionUpdates,
|
|
288
367
|
]);
|
|
289
368
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
369
|
+
const streamState: MarkdownStreamState = {
|
|
370
|
+
text: renderState.text,
|
|
371
|
+
sourceAstStatus: hasBeforeParsePlugins ? "disabled" : "available",
|
|
372
|
+
};
|
|
373
|
+
if (hasBeforeParsePlugins) {
|
|
374
|
+
streamState.sourceAstDisabledReason = "beforeParse-plugin";
|
|
375
|
+
} else {
|
|
376
|
+
streamState.sourceAst = renderState.ast;
|
|
377
|
+
}
|
|
378
|
+
return streamState;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
382
|
+
session,
|
|
383
|
+
updateIntervalMs = 50,
|
|
384
|
+
updateStrategy = "interval",
|
|
385
|
+
useTransitionUpdates = false,
|
|
386
|
+
incrementalParsing = true,
|
|
387
|
+
options,
|
|
388
|
+
plugins,
|
|
389
|
+
renderMarkdown,
|
|
390
|
+
...props
|
|
391
|
+
}) => {
|
|
392
|
+
const streamState = useMarkdownStreamState({
|
|
393
|
+
session,
|
|
394
|
+
updateIntervalMs,
|
|
395
|
+
updateStrategy,
|
|
396
|
+
useTransitionUpdates,
|
|
397
|
+
incrementalParsing,
|
|
398
|
+
...(options ? { options } : {}),
|
|
399
|
+
...(plugins ? { plugins } : {}),
|
|
400
|
+
});
|
|
401
|
+
const markdownProps: MarkdownProps = {
|
|
402
|
+
...props,
|
|
403
|
+
children: streamState.text,
|
|
404
|
+
};
|
|
405
|
+
if (options) markdownProps.options = options;
|
|
406
|
+
if (plugins) markdownProps.plugins = plugins;
|
|
407
|
+
if (streamState.sourceAst) markdownProps.sourceAst = streamState.sourceAst;
|
|
408
|
+
|
|
409
|
+
if (renderMarkdown) {
|
|
410
|
+
return renderMarkdown({
|
|
411
|
+
...streamState,
|
|
412
|
+
markdownProps,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return <Markdown {...markdownProps} />;
|
|
300
417
|
};
|
package/src/markdown.tsx
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type CustomRenderer,
|
|
35
35
|
type CustomRenderers,
|
|
36
36
|
type LinkPressHandler,
|
|
37
|
+
type MarkdownContextValue,
|
|
37
38
|
type NodeRendererProps,
|
|
38
39
|
type TableOptions,
|
|
39
40
|
} from "./MarkdownContext";
|
|
@@ -174,10 +175,8 @@ const warnInDev = (message: string, error?: unknown): void => {
|
|
|
174
175
|
};
|
|
175
176
|
|
|
176
177
|
const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
children: node.children?.map(cloneMarkdownNode),
|
|
180
|
-
};
|
|
178
|
+
const children = node.children?.map(cloneMarkdownNode);
|
|
179
|
+
return children ? { ...node, children } : { ...node };
|
|
181
180
|
};
|
|
182
181
|
|
|
183
182
|
const getParserOptionsKey = (options?: ParserOptions): string => {
|
|
@@ -204,11 +203,11 @@ const normalizeParserOptions = (
|
|
|
204
203
|
return undefined;
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
const normalized: ParserOptions = {};
|
|
207
|
+
if (gfm !== undefined) normalized.gfm = gfm;
|
|
208
|
+
if (math !== undefined) normalized.math = math;
|
|
209
|
+
if (html !== undefined) normalized.html = html;
|
|
210
|
+
return normalized;
|
|
212
211
|
};
|
|
213
212
|
|
|
214
213
|
const parseWithNativeParser = (
|
|
@@ -478,13 +477,22 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
478
477
|
const markdownToParse = sourceAst
|
|
479
478
|
? children
|
|
480
479
|
: applyBeforeParsePlugins(children, sortedPlugins, onErrorRef.current);
|
|
481
|
-
const parserOptions = normalizeParserOptions(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
480
|
+
const parserOptions = normalizeParserOptions(
|
|
481
|
+
Object.assign(
|
|
482
|
+
{},
|
|
483
|
+
parserOptionGfm === undefined ? null : { gfm: parserOptionGfm },
|
|
484
|
+
parserOptionMath === undefined ? null : { math: parserOptionMath },
|
|
485
|
+
parserOptionHtml === undefined ? null : { html: parserOptionHtml },
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
const shouldCloneSourceAst =
|
|
489
|
+
sourceAst &&
|
|
490
|
+
(Boolean(astTransform) ||
|
|
491
|
+
sortedPlugins?.some((plugin) => plugin.afterParse) === true);
|
|
486
492
|
let parsedAst = sourceAst
|
|
487
|
-
?
|
|
493
|
+
? shouldCloneSourceAst
|
|
494
|
+
? cloneMarkdownNode(sourceAst)
|
|
495
|
+
: sourceAst
|
|
488
496
|
: parseCache
|
|
489
497
|
? getCachedParsedAst(markdownToParse, parserOptions)
|
|
490
498
|
: parseWithNativeParser(markdownToParse, parserOptions);
|
|
@@ -560,16 +568,16 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
560
568
|
}, [userTheme, stylingStrategy]);
|
|
561
569
|
|
|
562
570
|
const baseStyles = getBaseStyles(theme);
|
|
563
|
-
const contextValue = useMemo(
|
|
571
|
+
const contextValue = useMemo<MarkdownContextValue>(
|
|
564
572
|
() => ({
|
|
565
573
|
renderers,
|
|
566
574
|
theme,
|
|
567
|
-
styles: nodeStyles,
|
|
568
575
|
stylingStrategy,
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
576
|
+
...(nodeStyles ? { styles: nodeStyles } : {}),
|
|
577
|
+
...(onLinkPress ? { onLinkPress } : {}),
|
|
578
|
+
...(tableOptions ? { tableOptions } : {}),
|
|
579
|
+
...(imageOptions ? { imageOptions } : {}),
|
|
580
|
+
...(highlightCode === undefined ? {} : { highlightCode }),
|
|
573
581
|
}),
|
|
574
582
|
[
|
|
575
583
|
renderers,
|
|
@@ -773,15 +781,18 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
773
781
|
...(node.type === "heading" && {
|
|
774
782
|
level: (node.level ?? 1) as 1 | 2 | 3 | 4 | 5 | 6,
|
|
775
783
|
}),
|
|
776
|
-
...(node.type === "link" && {
|
|
784
|
+
...(node.type === "link" && {
|
|
785
|
+
href: node.href ?? "",
|
|
786
|
+
...(node.title ? { title: node.title } : {}),
|
|
787
|
+
}),
|
|
777
788
|
...(node.type === "image" && {
|
|
778
789
|
url: node.href ?? "",
|
|
779
|
-
alt: node.alt,
|
|
780
|
-
title: node.title,
|
|
790
|
+
...(node.alt ? { alt: node.alt } : {}),
|
|
791
|
+
...(node.title ? { title: node.title } : {}),
|
|
781
792
|
}),
|
|
782
793
|
...(node.type === "code_block" && {
|
|
783
794
|
content: getTextContent(node),
|
|
784
|
-
language: node.language,
|
|
795
|
+
...(node.language ? { language: node.language } : {}),
|
|
785
796
|
}),
|
|
786
797
|
...(node.type === "code_inline" && { content: node.content ?? "" }),
|
|
787
798
|
...((node.type === "math_inline" || node.type === "math_block") && {
|
|
@@ -789,7 +800,7 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
789
800
|
}),
|
|
790
801
|
...(node.type === "list" && {
|
|
791
802
|
ordered: node.ordered ?? false,
|
|
792
|
-
start: node.start,
|
|
803
|
+
...(node.start === undefined ? {} : { start: node.start }),
|
|
793
804
|
}),
|
|
794
805
|
...(node.type === "task_list_item" && { checked: node.checked ?? false }),
|
|
795
806
|
};
|
|
@@ -810,7 +821,10 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
810
821
|
|
|
811
822
|
case "heading":
|
|
812
823
|
return (
|
|
813
|
-
<Heading
|
|
824
|
+
<Heading
|
|
825
|
+
level={node.level ?? 1}
|
|
826
|
+
{...(nodeStyles?.heading ? { style: nodeStyles.heading } : {})}
|
|
827
|
+
>
|
|
814
828
|
{renderChildren(node.children, inListItem, true)}
|
|
815
829
|
</Heading>
|
|
816
830
|
);
|
|
@@ -853,7 +867,10 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
853
867
|
|
|
854
868
|
case "link":
|
|
855
869
|
return (
|
|
856
|
-
<Link
|
|
870
|
+
<Link
|
|
871
|
+
href={node.href ?? ""}
|
|
872
|
+
{...(nodeStyles?.link ? { style: nodeStyles.link } : {})}
|
|
873
|
+
>
|
|
857
874
|
{renderChildren(node.children, inListItem, true)}
|
|
858
875
|
</Link>
|
|
859
876
|
);
|
|
@@ -862,36 +879,52 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
862
879
|
return (
|
|
863
880
|
<Image
|
|
864
881
|
url={node.href ?? ""}
|
|
865
|
-
title={node.title}
|
|
866
|
-
alt={node.alt}
|
|
867
882
|
Renderer={NodeRenderer}
|
|
868
|
-
|
|
883
|
+
{...(node.title ? { title: node.title } : {})}
|
|
884
|
+
{...(node.alt ? { alt: node.alt } : {})}
|
|
885
|
+
{...(nodeStyles?.image ? { style: nodeStyles.image } : {})}
|
|
869
886
|
/>
|
|
870
887
|
);
|
|
871
888
|
|
|
872
889
|
case "code_inline":
|
|
873
890
|
return (
|
|
874
|
-
<InlineCode
|
|
891
|
+
<InlineCode
|
|
892
|
+
{...(nodeStyles?.code_inline
|
|
893
|
+
? { style: nodeStyles.code_inline }
|
|
894
|
+
: {})}
|
|
895
|
+
>
|
|
896
|
+
{node.content}
|
|
897
|
+
</InlineCode>
|
|
875
898
|
);
|
|
876
899
|
|
|
877
900
|
case "code_block":
|
|
878
901
|
return (
|
|
879
902
|
<CodeBlock
|
|
880
|
-
language={node.language}
|
|
881
903
|
content={getTextContent(node)}
|
|
882
|
-
|
|
904
|
+
{...(node.language ? { language: node.language } : {})}
|
|
905
|
+
{...(nodeStyles?.code_block ? { style: nodeStyles.code_block } : {})}
|
|
883
906
|
/>
|
|
884
907
|
);
|
|
885
908
|
|
|
886
909
|
case "blockquote":
|
|
887
910
|
return (
|
|
888
|
-
<Blockquote
|
|
911
|
+
<Blockquote
|
|
912
|
+
{...(nodeStyles?.blockquote
|
|
913
|
+
? { style: nodeStyles.blockquote }
|
|
914
|
+
: {})}
|
|
915
|
+
>
|
|
889
916
|
{renderChildren(node.children, inListItem, false)}
|
|
890
917
|
</Blockquote>
|
|
891
918
|
);
|
|
892
919
|
|
|
893
920
|
case "horizontal_rule":
|
|
894
|
-
return
|
|
921
|
+
return (
|
|
922
|
+
<HorizontalRule
|
|
923
|
+
{...(nodeStyles?.horizontal_rule
|
|
924
|
+
? { style: nodeStyles.horizontal_rule }
|
|
925
|
+
: {})}
|
|
926
|
+
/>
|
|
927
|
+
);
|
|
895
928
|
|
|
896
929
|
case "line_break":
|
|
897
930
|
return <Text>{"\n"}</Text>;
|
|
@@ -904,7 +937,12 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
904
937
|
if (!mathContent) return null;
|
|
905
938
|
mathContent = mathContent.replace(/^\$+|\$+$/g, "").trim();
|
|
906
939
|
return (
|
|
907
|
-
<MathInline
|
|
940
|
+
<MathInline
|
|
941
|
+
content={mathContent}
|
|
942
|
+
{...(nodeStyles?.math_inline
|
|
943
|
+
? { style: nodeStyles.math_inline }
|
|
944
|
+
: {})}
|
|
945
|
+
/>
|
|
908
946
|
);
|
|
909
947
|
}
|
|
910
948
|
|
|
@@ -912,7 +950,7 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
912
950
|
return (
|
|
913
951
|
<MathBlock
|
|
914
952
|
content={getTextContent(node)}
|
|
915
|
-
|
|
953
|
+
{...(nodeStyles?.math_block ? { style: nodeStyles.math_block } : {})}
|
|
916
954
|
/>
|
|
917
955
|
);
|
|
918
956
|
|
|
@@ -920,9 +958,9 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
920
958
|
return (
|
|
921
959
|
<List
|
|
922
960
|
ordered={node.ordered ?? false}
|
|
923
|
-
start={node.start}
|
|
924
961
|
depth={depth}
|
|
925
|
-
|
|
962
|
+
{...(node.start === undefined ? {} : { start: node.start })}
|
|
963
|
+
{...(nodeStyles?.list ? { style: nodeStyles.list } : {})}
|
|
926
964
|
>
|
|
927
965
|
{node.children?.map((child, index) => {
|
|
928
966
|
if (child.type === "task_list_item") {
|
|
@@ -962,7 +1000,9 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
962
1000
|
return (
|
|
963
1001
|
<TaskListItem
|
|
964
1002
|
checked={node.checked ?? false}
|
|
965
|
-
|
|
1003
|
+
{...(nodeStyles?.task_list_item
|
|
1004
|
+
? { style: nodeStyles.task_list_item }
|
|
1005
|
+
: {})}
|
|
966
1006
|
>
|
|
967
1007
|
{renderChildren(node.children, true, false)}
|
|
968
1008
|
</TaskListItem>
|
|
@@ -973,7 +1013,7 @@ const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
|
973
1013
|
<TableRenderer
|
|
974
1014
|
node={node}
|
|
975
1015
|
Renderer={NodeRenderer}
|
|
976
|
-
|
|
1016
|
+
{...(nodeStyles?.table ? { style: nodeStyles.table } : {})}
|
|
977
1017
|
/>
|
|
978
1018
|
);
|
|
979
1019
|
|
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);
|
|
@@ -131,8 +140,8 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
|
|
|
131
140
|
/picsum\.photos\/.*\/(\d+)\/(\d+)/,
|
|
132
141
|
);
|
|
133
142
|
if (picsumMatch) {
|
|
134
|
-
const w = parseInt(picsumMatch[1], 10);
|
|
135
|
-
const h = parseInt(picsumMatch[2], 10);
|
|
143
|
+
const w = parseInt(picsumMatch[1] ?? "", 10);
|
|
144
|
+
const h = parseInt(picsumMatch[2] ?? "", 10);
|
|
136
145
|
if (!isNaN(w) && !isNaN(h) && h !== 0) {
|
|
137
146
|
setAspectRatio(w / h);
|
|
138
147
|
return () => {
|
|
@@ -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
|
|
|
@@ -257,9 +265,10 @@ export const MathInline: FC<MathInlineProps> = ({ content, style }) => {
|
|
|
257
265
|
latex={content}
|
|
258
266
|
fontSize={getInlineMathFontSize(content, theme)}
|
|
259
267
|
displayMode={false}
|
|
260
|
-
color={theme.colors.text}
|
|
261
268
|
style={styles.ratexInline}
|
|
269
|
+
{...(theme.colors.text ? { color: theme.colors.text } : {})}
|
|
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
|
|
|
@@ -297,9 +314,10 @@ export const MathBlock: FC<MathBlockProps> = ({ content, style }) => {
|
|
|
297
314
|
latex={content}
|
|
298
315
|
fontSize={theme.fontSizes.xl}
|
|
299
316
|
displayMode
|
|
300
|
-
color={theme.colors.text}
|
|
301
317
|
style={styles.ratexBlock}
|
|
318
|
+
{...(theme.colors.text ? { color: theme.colors.text } : {})}
|
|
302
319
|
onError={() => {
|
|
320
|
+
if (!mountedRef.current) return;
|
|
303
321
|
setHasRenderError(true);
|
|
304
322
|
}}
|
|
305
323
|
/>
|
|
@@ -181,19 +181,19 @@ export const TableRenderer: FC<TableRendererProps> = ({
|
|
|
181
181
|
for (let col = 0; col < columnCount; col++) {
|
|
182
182
|
const headerWidth = measuredWidths.current.get(`header-${col}`);
|
|
183
183
|
if (headerWidth && headerWidth > 0) {
|
|
184
|
-
maxWidths[col] = Math.max(maxWidths[col], headerWidth);
|
|
184
|
+
maxWidths[col] = Math.max(maxWidths[col] ?? 0, headerWidth);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
for (let row = 0; row < rows.length; row++) {
|
|
188
|
-
if (col >= rows[row]
|
|
188
|
+
if (col >= (rows[row]?.length ?? 0)) continue;
|
|
189
189
|
const cellWidth = measuredWidths.current.get(`cell-${row}-${col}`);
|
|
190
190
|
if (cellWidth && cellWidth > 0) {
|
|
191
|
-
maxWidths[col] = Math.max(maxWidths[col], cellWidth);
|
|
191
|
+
maxWidths[col] = Math.max(maxWidths[col] ?? 0, cellWidth);
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
maxWidths[col] = Math.max(
|
|
196
|
-
maxWidths[col] + COLUMN_MEASUREMENT_PADDING,
|
|
196
|
+
(maxWidths[col] ?? 0) + COLUMN_MEASUREMENT_PADDING,
|
|
197
197
|
minColumnWidth,
|
|
198
198
|
);
|
|
199
199
|
}
|
|
@@ -59,7 +59,7 @@ export const estimateColumnWidths = (
|
|
|
59
59
|
let maxChars = headerChars;
|
|
60
60
|
|
|
61
61
|
for (let row = 0; row < rows.length; row++) {
|
|
62
|
-
const cell = rows[row][col];
|
|
62
|
+
const cell = rows[row]?.[col];
|
|
63
63
|
if (!cell) continue;
|
|
64
64
|
const cellChars = Math.min(
|
|
65
65
|
getTextContent(cell).trim().length,
|