react-native-nitro-markdown 0.4.2 → 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 +605 -318
- 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/MarkdownContext.js +2 -1
- package/lib/commonjs/MarkdownContext.js.map +1 -1
- 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 +109 -13
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +215 -44
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/code.js +4 -3
- package/lib/commonjs/renderers/code.js.map +1 -1
- package/lib/commonjs/renderers/heading.js +1 -1
- package/lib/commonjs/renderers/heading.js.map +1 -1
- package/lib/commonjs/renderers/image.js +7 -5
- package/lib/commonjs/renderers/image.js.map +1 -1
- package/lib/commonjs/renderers/link.js +15 -3
- package/lib/commonjs/renderers/link.js.map +1 -1
- package/lib/commonjs/renderers/list.js +2 -2
- package/lib/commonjs/renderers/list.js.map +1 -1
- package/lib/commonjs/renderers/table.js +126 -21
- package/lib/commonjs/renderers/table.js.map +1 -1
- package/lib/commonjs/use-markdown-stream.js +16 -14
- package/lib/commonjs/use-markdown-stream.js.map +1 -1
- package/lib/commonjs/utils/incremental-ast.js +153 -0
- package/lib/commonjs/utils/incremental-ast.js.map +1 -0
- package/lib/commonjs/utils/link-security.js +21 -0
- package/lib/commonjs/utils/link-security.js.map +1 -0
- package/lib/commonjs/utils/stream-timeline.js +62 -0
- package/lib/commonjs/utils/stream-timeline.js.map +1 -0
- package/lib/module/MarkdownContext.js +2 -1
- package/lib/module/MarkdownContext.js.map +1 -1
- package/lib/module/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 +110 -14
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +217 -46
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/blockquote.js.map +1 -1
- package/lib/module/renderers/code.js +4 -3
- package/lib/module/renderers/code.js.map +1 -1
- package/lib/module/renderers/heading.js +1 -1
- package/lib/module/renderers/heading.js.map +1 -1
- package/lib/module/renderers/image.js +7 -5
- package/lib/module/renderers/image.js.map +1 -1
- package/lib/module/renderers/link.js +15 -3
- package/lib/module/renderers/link.js.map +1 -1
- package/lib/module/renderers/list.js +2 -2
- package/lib/module/renderers/list.js.map +1 -1
- package/lib/module/renderers/paragraph.js.map +1 -1
- package/lib/module/renderers/table.js +127 -22
- package/lib/module/renderers/table.js.map +1 -1
- package/lib/module/use-markdown-stream.js +16 -14
- package/lib/module/use-markdown-stream.js.map +1 -1
- package/lib/module/utils/incremental-ast.js +147 -0
- package/lib/module/utils/incremental-ast.js.map +1 -0
- package/lib/module/utils/link-security.js +15 -0
- package/lib/module/utils/link-security.js.map +1 -0
- package/lib/module/utils/stream-timeline.js +56 -0
- package/lib/module/utils/stream-timeline.js.map +1 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts +5 -3
- package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
- package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/headless.d.ts +15 -2
- package/lib/typescript/commonjs/headless.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +7 -2
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts +62 -5
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
- package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
- package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
- package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
- package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
- package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
- package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +5 -2
- package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme.d.ts +2 -2
- package/lib/typescript/commonjs/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
- package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
- package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
- package/lib/typescript/module/Markdown.nitro.d.ts +5 -3
- package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/module/MarkdownContext.d.ts +26 -25
- package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
- package/lib/typescript/module/headless.d.ts +15 -2
- package/lib/typescript/module/headless.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +7 -2
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts +62 -5
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
- package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
- package/lib/typescript/module/renderers/code.d.ts +5 -5
- package/lib/typescript/module/renderers/code.d.ts.map +1 -1
- package/lib/typescript/module/renderers/heading.d.ts +3 -3
- package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
- package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
- package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
- package/lib/typescript/module/renderers/image.d.ts +2 -2
- package/lib/typescript/module/renderers/image.d.ts.map +1 -1
- package/lib/typescript/module/renderers/link.d.ts +3 -3
- package/lib/typescript/module/renderers/link.d.ts.map +1 -1
- package/lib/typescript/module/renderers/list.d.ts +7 -7
- package/lib/typescript/module/renderers/list.d.ts.map +1 -1
- package/lib/typescript/module/renderers/math.d.ts +4 -4
- package/lib/typescript/module/renderers/math.d.ts.map +1 -1
- package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
- package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
- package/lib/typescript/module/renderers/table.d.ts +3 -3
- package/lib/typescript/module/renderers/table.d.ts.map +1 -1
- package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +5 -2
- package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
- package/lib/typescript/module/theme.d.ts +2 -2
- package/lib/typescript/module/theme.d.ts.map +1 -1
- package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
- package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
- package/lib/typescript/module/utils/link-security.d.ts +3 -0
- package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
- package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
- package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
- package/nitrogen/generated/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.swift +0 -1
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +4 -3
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +34 -10
- 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 +7 -5
- package/src/Markdown.nitro.ts +7 -3
- package/src/MarkdownContext.ts +31 -25
- package/src/headless.ts +44 -6
- package/src/index.ts +8 -0
- package/src/markdown-stream.tsx +159 -15
- package/src/markdown.tsx +406 -50
- package/src/renderers/blockquote.tsx +4 -4
- package/src/renderers/code.tsx +16 -10
- package/src/renderers/heading.tsx +4 -4
- package/src/renderers/horizontal-rule.tsx +3 -3
- package/src/renderers/image.tsx +11 -9
- package/src/renderers/link.tsx +25 -7
- package/src/renderers/list.tsx +9 -12
- package/src/renderers/math.tsx +4 -4
- package/src/renderers/paragraph.tsx +3 -3
- package/src/renderers/table.tsx +270 -98
- package/src/specs/MarkdownSession.nitro.ts +6 -2
- package/src/theme.ts +3 -3
- package/src/use-markdown-stream.ts +22 -16
- package/src/utils/incremental-ast.ts +224 -0
- package/src/utils/link-security.ts +22 -0
- package/src/utils/stream-timeline.ts +72 -0
package/src/markdown.tsx
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
memo,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
type FC,
|
|
7
|
+
Fragment,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
11
|
import {
|
|
12
12
|
StyleSheet,
|
|
13
13
|
View,
|
|
14
14
|
Text,
|
|
15
|
+
FlatList,
|
|
15
16
|
Platform,
|
|
17
|
+
type ListRenderItemInfo,
|
|
18
|
+
type FlatListProps,
|
|
16
19
|
type StyleProp,
|
|
17
20
|
type ViewStyle,
|
|
18
21
|
} from "react-native";
|
|
@@ -28,21 +31,214 @@ import {
|
|
|
28
31
|
MarkdownContext,
|
|
29
32
|
useMarkdownContext,
|
|
30
33
|
type CustomRenderers,
|
|
34
|
+
type LinkPressHandler,
|
|
31
35
|
type NodeRendererProps,
|
|
32
36
|
} from "./MarkdownContext";
|
|
33
|
-
|
|
34
|
-
import { Heading } from "./renderers/heading";
|
|
35
|
-
import { Paragraph } from "./renderers/paragraph";
|
|
36
|
-
import { Link } from "./renderers/link";
|
|
37
37
|
import { Blockquote } from "./renderers/blockquote";
|
|
38
|
-
import { HorizontalRule } from "./renderers/horizontal-rule";
|
|
39
38
|
import { CodeBlock, InlineCode } from "./renderers/code";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
39
|
+
import { Heading } from "./renderers/heading";
|
|
40
|
+
import { HorizontalRule } from "./renderers/horizontal-rule";
|
|
42
41
|
import { Image } from "./renderers/image";
|
|
42
|
+
import { Link } from "./renderers/link";
|
|
43
|
+
import { List, ListItem, TaskListItem } from "./renderers/list";
|
|
43
44
|
import { MathInline, MathBlock } from "./renderers/math";
|
|
45
|
+
import { Paragraph } from "./renderers/paragraph";
|
|
46
|
+
import { TableRenderer } from "./renderers/table";
|
|
47
|
+
import {
|
|
48
|
+
defaultMarkdownTheme,
|
|
49
|
+
minimalMarkdownTheme,
|
|
50
|
+
mergeThemes,
|
|
51
|
+
type MarkdownTheme,
|
|
52
|
+
type PartialMarkdownTheme,
|
|
53
|
+
type NodeStyleOverrides,
|
|
54
|
+
type StylingStrategy,
|
|
55
|
+
} from "./theme";
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
|
|
58
|
+
const parseAstCache = new Map<string, MarkdownNode>();
|
|
59
|
+
const MAX_PARSE_CACHE_ENTRIES = 32;
|
|
60
|
+
const MAX_CACHEABLE_TEXT_LENGTH = 24_000;
|
|
61
|
+
|
|
62
|
+
export type AstTransform = (ast: MarkdownNode) => MarkdownNode;
|
|
63
|
+
export type MarkdownVirtualizationOptions = Pick<
|
|
64
|
+
FlatListProps<MarkdownNode>,
|
|
65
|
+
| "initialNumToRender"
|
|
66
|
+
| "maxToRenderPerBatch"
|
|
67
|
+
| "windowSize"
|
|
68
|
+
| "updateCellsBatchingPeriod"
|
|
69
|
+
| "removeClippedSubviews"
|
|
70
|
+
>;
|
|
71
|
+
|
|
72
|
+
export type MarkdownPlugin = {
|
|
73
|
+
/**
|
|
74
|
+
* Optional plugin name used for diagnostics and debugging.
|
|
75
|
+
*/
|
|
76
|
+
name?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Optional plugin version metadata for diagnostics.
|
|
79
|
+
*/
|
|
80
|
+
version?: string | number;
|
|
81
|
+
/**
|
|
82
|
+
* Optional text preprocessor executed before native parsing.
|
|
83
|
+
* Should return a full markdown string.
|
|
84
|
+
*/
|
|
85
|
+
beforeParse?: (markdown: string) => string;
|
|
86
|
+
/**
|
|
87
|
+
* Optional AST postprocessor executed after native parsing.
|
|
88
|
+
*/
|
|
89
|
+
afterParse?: AstTransform;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const isMarkdownNode = (value: unknown): value is MarkdownNode => {
|
|
93
|
+
if (typeof value !== "object" || value === null) return false;
|
|
94
|
+
return typeof Reflect.get(value, "type") === "string";
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const warnInDev = (message: string, error?: unknown): void => {
|
|
98
|
+
if (typeof __DEV__ === "undefined" || !__DEV__) return;
|
|
99
|
+
|
|
100
|
+
const runtimeConsole = Reflect.get(globalThis, "console");
|
|
101
|
+
if (
|
|
102
|
+
typeof runtimeConsole === "object" &&
|
|
103
|
+
runtimeConsole !== null &&
|
|
104
|
+
"warn" in runtimeConsole &&
|
|
105
|
+
typeof runtimeConsole.warn === "function"
|
|
106
|
+
) {
|
|
107
|
+
runtimeConsole.warn(message, error);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
|
|
112
|
+
return {
|
|
113
|
+
...node,
|
|
114
|
+
children: node.children?.map(cloneMarkdownNode),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getParserOptionsKey = (options?: ParserOptions): string => {
|
|
119
|
+
if (!options) return "gfm:default|math:default";
|
|
120
|
+
|
|
121
|
+
const gfm = options.gfm === undefined ? "default" : options.gfm ? "1" : "0";
|
|
122
|
+
const math =
|
|
123
|
+
options.math === undefined ? "default" : options.math ? "1" : "0";
|
|
124
|
+
return `gfm:${gfm}|math:${math}`;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const normalizeParserOptions = (
|
|
128
|
+
options?: ParserOptions,
|
|
129
|
+
): ParserOptions | undefined => {
|
|
130
|
+
if (!options) return undefined;
|
|
131
|
+
|
|
132
|
+
const gfm = options.gfm;
|
|
133
|
+
const math = options.math;
|
|
134
|
+
|
|
135
|
+
if (gfm === undefined && math === undefined) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
gfm,
|
|
141
|
+
math,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const parseWithNativeParser = (
|
|
146
|
+
text: string,
|
|
147
|
+
options?: ParserOptions,
|
|
148
|
+
): MarkdownNode => {
|
|
149
|
+
if (options) {
|
|
150
|
+
return parseMarkdownWithOptions(text, options);
|
|
151
|
+
}
|
|
152
|
+
return parseMarkdown(text);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const getCachedParsedAst = (
|
|
156
|
+
text: string,
|
|
157
|
+
options?: ParserOptions,
|
|
158
|
+
): MarkdownNode => {
|
|
159
|
+
if (text.length > MAX_CACHEABLE_TEXT_LENGTH) {
|
|
160
|
+
return parseWithNativeParser(text, options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const cacheKey = `${getParserOptionsKey(options)}|${text}`;
|
|
164
|
+
const cachedNode = parseAstCache.get(cacheKey);
|
|
165
|
+
if (cachedNode) {
|
|
166
|
+
parseAstCache.delete(cacheKey);
|
|
167
|
+
parseAstCache.set(cacheKey, cachedNode);
|
|
168
|
+
return cloneMarkdownNode(cachedNode);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const parsedNode = parseWithNativeParser(text, options);
|
|
172
|
+
parseAstCache.set(cacheKey, parsedNode);
|
|
173
|
+
if (parseAstCache.size > MAX_PARSE_CACHE_ENTRIES) {
|
|
174
|
+
const oldestCacheKey = parseAstCache.keys().next().value;
|
|
175
|
+
if (typeof oldestCacheKey === "string") {
|
|
176
|
+
parseAstCache.delete(oldestCacheKey);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return cloneMarkdownNode(parsedNode);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const applyBeforeParsePlugins = (
|
|
184
|
+
markdown: string,
|
|
185
|
+
plugins?: MarkdownPlugin[],
|
|
186
|
+
): string => {
|
|
187
|
+
if (!plugins || plugins.length === 0) {
|
|
188
|
+
return markdown;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let nextMarkdown = markdown;
|
|
192
|
+
for (const plugin of plugins) {
|
|
193
|
+
if (!plugin.beforeParse) continue;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const transformed = plugin.beforeParse(nextMarkdown);
|
|
197
|
+
if (typeof transformed === "string") {
|
|
198
|
+
nextMarkdown = transformed;
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const pluginLabel = plugin.name ? ` (${plugin.name})` : "";
|
|
202
|
+
warnInDev(
|
|
203
|
+
`[react-native-nitro-markdown] plugin beforeParse${pluginLabel} threw; using previous markdown.`,
|
|
204
|
+
error,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return nextMarkdown;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const applyAfterParsePlugins = (
|
|
213
|
+
ast: MarkdownNode,
|
|
214
|
+
plugins?: MarkdownPlugin[],
|
|
215
|
+
): MarkdownNode => {
|
|
216
|
+
if (!plugins || plugins.length === 0) {
|
|
217
|
+
return ast;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let nextAst = ast;
|
|
221
|
+
for (const plugin of plugins) {
|
|
222
|
+
if (!plugin.afterParse) continue;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const transformed = plugin.afterParse(nextAst);
|
|
226
|
+
if (isMarkdownNode(transformed)) {
|
|
227
|
+
nextAst = transformed;
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const pluginLabel = plugin.name ? ` (${plugin.name})` : "";
|
|
231
|
+
warnInDev(
|
|
232
|
+
`[react-native-nitro-markdown] plugin afterParse${pluginLabel} threw; using previous AST.`,
|
|
233
|
+
error,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return nextAst;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export type MarkdownProps = {
|
|
46
242
|
/**
|
|
47
243
|
* The markdown string to parse and render.
|
|
48
244
|
*/
|
|
@@ -51,6 +247,20 @@ export interface MarkdownProps {
|
|
|
51
247
|
* Parser options to enable GFM or Math support.
|
|
52
248
|
*/
|
|
53
249
|
options?: ParserOptions;
|
|
250
|
+
/**
|
|
251
|
+
* Optional parser plugins for preprocessing and AST postprocessing.
|
|
252
|
+
*/
|
|
253
|
+
plugins?: MarkdownPlugin[];
|
|
254
|
+
/**
|
|
255
|
+
* Optional pre-parsed AST.
|
|
256
|
+
* When provided, native parse is skipped and this tree is rendered instead.
|
|
257
|
+
*/
|
|
258
|
+
sourceAst?: MarkdownNode;
|
|
259
|
+
/**
|
|
260
|
+
* Optional transform applied after parsing and before rendering.
|
|
261
|
+
* The transformed AST is also returned in `onParseComplete`.
|
|
262
|
+
*/
|
|
263
|
+
astTransform?: AstTransform;
|
|
54
264
|
/**
|
|
55
265
|
* Callback fired when parsing begins.
|
|
56
266
|
*/
|
|
@@ -93,11 +303,36 @@ export interface MarkdownProps {
|
|
|
93
303
|
* Optional style for the container view.
|
|
94
304
|
*/
|
|
95
305
|
style?: StyleProp<ViewStyle>;
|
|
96
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Optional link press handler.
|
|
308
|
+
* Return false to prevent the default openURL behavior.
|
|
309
|
+
*/
|
|
310
|
+
onLinkPress?: LinkPressHandler;
|
|
311
|
+
/**
|
|
312
|
+
* Enables top-level block virtualization for very large markdown documents.
|
|
313
|
+
* Best used when Markdown is the primary scroll container on screen.
|
|
314
|
+
* - `true`: always virtualize when block threshold is met
|
|
315
|
+
* - `"auto"`: virtualize only when threshold is met (recommended for large docs)
|
|
316
|
+
* - `false`: disable virtualization (default)
|
|
317
|
+
*/
|
|
318
|
+
virtualize?: boolean | "auto";
|
|
319
|
+
/**
|
|
320
|
+
* Minimum number of top-level blocks before virtualization is activated.
|
|
321
|
+
* Helps avoid FlatList overhead on small documents.
|
|
322
|
+
*/
|
|
323
|
+
virtualizationMinBlocks?: number;
|
|
324
|
+
/**
|
|
325
|
+
* Optional FlatList tuning for virtualization.
|
|
326
|
+
*/
|
|
327
|
+
virtualization?: MarkdownVirtualizationOptions;
|
|
328
|
+
};
|
|
97
329
|
|
|
98
330
|
export const Markdown: FC<MarkdownProps> = ({
|
|
99
331
|
children,
|
|
100
332
|
options,
|
|
333
|
+
plugins,
|
|
334
|
+
sourceAst,
|
|
335
|
+
astTransform,
|
|
101
336
|
renderers = {},
|
|
102
337
|
theme: userTheme,
|
|
103
338
|
styles: nodeStyles,
|
|
@@ -105,34 +340,78 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
105
340
|
style,
|
|
106
341
|
onParsingInProgress,
|
|
107
342
|
onParseComplete,
|
|
343
|
+
onLinkPress,
|
|
344
|
+
virtualize = false,
|
|
345
|
+
virtualizationMinBlocks = 40,
|
|
346
|
+
virtualization,
|
|
108
347
|
}) => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
if (onParsingInProgress) {
|
|
112
|
-
onParsingInProgress();
|
|
113
|
-
}
|
|
348
|
+
const parserOptionGfm = options?.gfm;
|
|
349
|
+
const parserOptionMath = options?.math;
|
|
114
350
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
351
|
+
const parseResult = useMemo(() => {
|
|
352
|
+
try {
|
|
353
|
+
const markdownToParse = applyBeforeParsePlugins(children, plugins);
|
|
354
|
+
const parserOptions = normalizeParserOptions({
|
|
355
|
+
gfm: parserOptionGfm,
|
|
356
|
+
math: parserOptionMath,
|
|
357
|
+
});
|
|
358
|
+
let parsedAst = sourceAst
|
|
359
|
+
? cloneMarkdownNode(sourceAst)
|
|
360
|
+
: getCachedParsedAst(markdownToParse, parserOptions);
|
|
361
|
+
parsedAst = applyAfterParsePlugins(parsedAst, plugins);
|
|
362
|
+
|
|
363
|
+
let ast = parsedAst;
|
|
364
|
+
if (astTransform) {
|
|
365
|
+
try {
|
|
366
|
+
const nextAst = astTransform(parsedAst);
|
|
367
|
+
if (isMarkdownNode(nextAst)) {
|
|
368
|
+
ast = nextAst;
|
|
369
|
+
}
|
|
370
|
+
} catch (error) {
|
|
371
|
+
warnInDev(
|
|
372
|
+
"[react-native-nitro-markdown] astTransform threw; falling back to parsed AST.",
|
|
373
|
+
error,
|
|
374
|
+
);
|
|
375
|
+
ast = parsedAst;
|
|
376
|
+
}
|
|
128
377
|
}
|
|
129
378
|
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
379
|
+
return {
|
|
380
|
+
ast,
|
|
381
|
+
};
|
|
382
|
+
} catch {
|
|
383
|
+
return {
|
|
384
|
+
ast: null,
|
|
385
|
+
};
|
|
134
386
|
}
|
|
135
|
-
}, [
|
|
387
|
+
}, [
|
|
388
|
+
children,
|
|
389
|
+
parserOptionGfm,
|
|
390
|
+
parserOptionMath,
|
|
391
|
+
plugins,
|
|
392
|
+
sourceAst,
|
|
393
|
+
astTransform,
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
onParsingInProgress?.();
|
|
398
|
+
}, [
|
|
399
|
+
children,
|
|
400
|
+
parserOptionGfm,
|
|
401
|
+
parserOptionMath,
|
|
402
|
+
plugins,
|
|
403
|
+
onParsingInProgress,
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
if (!parseResult.ast || !onParseComplete) return;
|
|
408
|
+
|
|
409
|
+
onParseComplete({
|
|
410
|
+
raw: children,
|
|
411
|
+
ast: parseResult.ast,
|
|
412
|
+
text: getFlattenedText(parseResult.ast),
|
|
413
|
+
});
|
|
414
|
+
}, [children, onParseComplete, parseResult.ast]);
|
|
136
415
|
|
|
137
416
|
const theme = useMemo(() => {
|
|
138
417
|
const base =
|
|
@@ -142,9 +421,44 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
142
421
|
return mergeThemes(base, userTheme);
|
|
143
422
|
}, [userTheme, stylingStrategy]);
|
|
144
423
|
|
|
145
|
-
const baseStyles =
|
|
424
|
+
const baseStyles = getBaseStyles(theme);
|
|
425
|
+
const contextValue = useMemo(
|
|
426
|
+
() => ({
|
|
427
|
+
renderers,
|
|
428
|
+
theme,
|
|
429
|
+
styles: nodeStyles,
|
|
430
|
+
stylingStrategy,
|
|
431
|
+
onLinkPress,
|
|
432
|
+
}),
|
|
433
|
+
[renderers, theme, nodeStyles, stylingStrategy, onLinkPress],
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const topLevelBlocks =
|
|
437
|
+
parseResult.ast?.type === "document"
|
|
438
|
+
? (parseResult.ast.children ?? [])
|
|
439
|
+
: parseResult.ast
|
|
440
|
+
? [parseResult.ast]
|
|
441
|
+
: [];
|
|
442
|
+
const shouldVirtualizeBySetting =
|
|
443
|
+
virtualize === true ||
|
|
444
|
+
(virtualize === "auto" && topLevelBlocks.length >= virtualizationMinBlocks);
|
|
445
|
+
const shouldVirtualize =
|
|
446
|
+
parseResult.ast !== null && shouldVirtualizeBySetting;
|
|
447
|
+
|
|
448
|
+
const keyExtractor = useCallback((node: MarkdownNode, index: number) => {
|
|
449
|
+
const beg = typeof node.beg === "number" ? node.beg : index;
|
|
450
|
+
const end = typeof node.end === "number" ? node.end : index;
|
|
451
|
+
return `${node.type}:${beg}:${end}:${index}`;
|
|
452
|
+
}, []);
|
|
453
|
+
|
|
454
|
+
const renderVirtualizedItem = useCallback(
|
|
455
|
+
({ item }: ListRenderItemInfo<MarkdownNode>): ReactElement => (
|
|
456
|
+
<NodeRenderer node={item} depth={0} inListItem={false} />
|
|
457
|
+
),
|
|
458
|
+
[],
|
|
459
|
+
);
|
|
146
460
|
|
|
147
|
-
if (!ast) {
|
|
461
|
+
if (!parseResult.ast) {
|
|
148
462
|
return (
|
|
149
463
|
<View style={[baseStyles.container, style]}>
|
|
150
464
|
<Text style={baseStyles.errorText}>Error parsing markdown</Text>
|
|
@@ -153,11 +467,28 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
153
467
|
}
|
|
154
468
|
|
|
155
469
|
return (
|
|
156
|
-
<MarkdownContext.Provider
|
|
157
|
-
value={{ renderers, theme, styles: nodeStyles, stylingStrategy }}
|
|
158
|
-
>
|
|
470
|
+
<MarkdownContext.Provider value={contextValue}>
|
|
159
471
|
<View style={[baseStyles.container, style]}>
|
|
160
|
-
|
|
472
|
+
{shouldVirtualize ? (
|
|
473
|
+
<FlatList
|
|
474
|
+
data={topLevelBlocks}
|
|
475
|
+
renderItem={renderVirtualizedItem}
|
|
476
|
+
keyExtractor={keyExtractor}
|
|
477
|
+
style={baseStyles.virtualizedList}
|
|
478
|
+
initialNumToRender={virtualization?.initialNumToRender ?? 12}
|
|
479
|
+
maxToRenderPerBatch={virtualization?.maxToRenderPerBatch ?? 12}
|
|
480
|
+
windowSize={virtualization?.windowSize ?? 10}
|
|
481
|
+
updateCellsBatchingPeriod={
|
|
482
|
+
virtualization?.updateCellsBatchingPeriod ?? 16
|
|
483
|
+
}
|
|
484
|
+
removeClippedSubviews={
|
|
485
|
+
virtualization?.removeClippedSubviews ?? true
|
|
486
|
+
}
|
|
487
|
+
showsVerticalScrollIndicator={false}
|
|
488
|
+
/>
|
|
489
|
+
) : (
|
|
490
|
+
<NodeRenderer node={parseResult.ast} depth={0} inListItem={false} />
|
|
491
|
+
)}
|
|
161
492
|
</View>
|
|
162
493
|
</MarkdownContext.Provider>
|
|
163
494
|
);
|
|
@@ -178,14 +509,14 @@ const isInline = (type: MarkdownNode["type"]): boolean => {
|
|
|
178
509
|
);
|
|
179
510
|
};
|
|
180
511
|
|
|
181
|
-
const
|
|
512
|
+
const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
182
513
|
node,
|
|
183
514
|
depth,
|
|
184
515
|
inListItem,
|
|
185
516
|
parentIsText = false,
|
|
186
517
|
}) => {
|
|
187
518
|
const { renderers, theme, styles: nodeStyles } = useMarkdownContext();
|
|
188
|
-
const baseStyles =
|
|
519
|
+
const baseStyles = getBaseStyles(theme);
|
|
189
520
|
|
|
190
521
|
const renderChildren = (
|
|
191
522
|
children?: MarkdownNode[],
|
|
@@ -308,7 +639,9 @@ const NodeRenderer: FC<NodeRendererProps> = ({
|
|
|
308
639
|
};
|
|
309
640
|
|
|
310
641
|
const result = customRenderer(enhancedProps);
|
|
311
|
-
if (result !== undefined)
|
|
642
|
+
if (result !== undefined) {
|
|
643
|
+
return result as ReactElement | null;
|
|
644
|
+
}
|
|
312
645
|
}
|
|
313
646
|
|
|
314
647
|
const nodeStyleOverride = nodeStyles?.[node.type];
|
|
@@ -491,11 +824,34 @@ const NodeRenderer: FC<NodeRendererProps> = ({
|
|
|
491
824
|
}
|
|
492
825
|
};
|
|
493
826
|
|
|
827
|
+
const NodeRenderer = memo(NodeRendererComponent, (previousProps, nextProps) => {
|
|
828
|
+
return (
|
|
829
|
+
previousProps.node === nextProps.node &&
|
|
830
|
+
previousProps.depth === nextProps.depth &&
|
|
831
|
+
previousProps.inListItem === nextProps.inListItem &&
|
|
832
|
+
previousProps.parentIsText === nextProps.parentIsText
|
|
833
|
+
);
|
|
834
|
+
}) as FC<NodeRendererProps>;
|
|
835
|
+
|
|
836
|
+
type BaseStyles = ReturnType<typeof createBaseStyles>;
|
|
837
|
+
|
|
838
|
+
const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
|
|
839
|
+
const cached = baseStylesCache.get(theme);
|
|
840
|
+
if (cached) return cached;
|
|
841
|
+
|
|
842
|
+
const created = createBaseStyles(theme);
|
|
843
|
+
baseStylesCache.set(theme, created);
|
|
844
|
+
return created;
|
|
845
|
+
};
|
|
846
|
+
|
|
494
847
|
const createBaseStyles = (theme: MarkdownTheme) =>
|
|
495
848
|
StyleSheet.create({
|
|
496
849
|
container: {
|
|
497
850
|
flex: 1,
|
|
498
851
|
},
|
|
852
|
+
virtualizedList: {
|
|
853
|
+
flex: 1,
|
|
854
|
+
},
|
|
499
855
|
document: {
|
|
500
856
|
flex: 1,
|
|
501
857
|
},
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, type FC, type ReactNode } from "react";
|
|
2
2
|
import { View, StyleSheet, type ViewStyle } from "react-native";
|
|
3
3
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type BlockquoteProps = {
|
|
6
6
|
children: ReactNode;
|
|
7
7
|
style?: ViewStyle;
|
|
8
|
-
}
|
|
8
|
+
};
|
|
9
9
|
|
|
10
10
|
export const Blockquote: FC<BlockquoteProps> = ({ children, style }) => {
|
|
11
11
|
const { theme } = useMarkdownContext();
|
|
@@ -23,7 +23,7 @@ export const Blockquote: FC<BlockquoteProps> = ({ children, style }) => {
|
|
|
23
23
|
borderRadius: theme.borderRadius.s,
|
|
24
24
|
},
|
|
25
25
|
}),
|
|
26
|
-
[theme]
|
|
26
|
+
[theme],
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
return <View style={[styles.blockquote, style]}>{children}</View>;
|
package/src/renderers/code.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, type FC, type ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -8,16 +8,16 @@ import {
|
|
|
8
8
|
type ViewStyle,
|
|
9
9
|
type TextStyle,
|
|
10
10
|
} from "react-native";
|
|
11
|
+
import { getTextContent } from "../headless";
|
|
11
12
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
12
13
|
import type { MarkdownNode } from "../headless";
|
|
13
|
-
import { getTextContent } from "../headless";
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
type CodeBlockProps = {
|
|
16
16
|
language?: string;
|
|
17
17
|
content?: string;
|
|
18
18
|
node?: MarkdownNode;
|
|
19
19
|
style?: ViewStyle;
|
|
20
|
-
}
|
|
20
|
+
};
|
|
21
21
|
|
|
22
22
|
export const CodeBlock: FC<CodeBlockProps> = ({
|
|
23
23
|
language,
|
|
@@ -60,27 +60,33 @@ export const CodeBlock: FC<CodeBlockProps> = ({
|
|
|
60
60
|
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
61
61
|
},
|
|
62
62
|
}),
|
|
63
|
-
[theme]
|
|
63
|
+
[theme],
|
|
64
64
|
);
|
|
65
65
|
|
|
66
66
|
const showLanguage = theme.showCodeLanguage && language;
|
|
67
67
|
|
|
68
68
|
return (
|
|
69
69
|
<View style={[styles.codeBlock, style]}>
|
|
70
|
-
{showLanguage
|
|
71
|
-
|
|
70
|
+
{showLanguage ? (
|
|
71
|
+
<Text style={styles.codeLanguage}>{language}</Text>
|
|
72
|
+
) : null}
|
|
73
|
+
<ScrollView
|
|
74
|
+
horizontal
|
|
75
|
+
showsHorizontalScrollIndicator={false}
|
|
76
|
+
bounces={false}
|
|
77
|
+
>
|
|
72
78
|
<Text style={styles.codeBlockText}>{displayContent}</Text>
|
|
73
79
|
</ScrollView>
|
|
74
80
|
</View>
|
|
75
81
|
);
|
|
76
82
|
};
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
type InlineCodeProps = {
|
|
79
85
|
content?: string;
|
|
80
86
|
node?: MarkdownNode;
|
|
81
87
|
children?: ReactNode;
|
|
82
88
|
style?: TextStyle;
|
|
83
|
-
}
|
|
89
|
+
};
|
|
84
90
|
|
|
85
91
|
export const InlineCode: FC<InlineCodeProps> = ({
|
|
86
92
|
content,
|
|
@@ -109,7 +115,7 @@ export const InlineCode: FC<InlineCodeProps> = ({
|
|
|
109
115
|
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
110
116
|
},
|
|
111
117
|
}),
|
|
112
|
-
[theme]
|
|
118
|
+
[theme],
|
|
113
119
|
);
|
|
114
120
|
return <Text style={[styles.codeInline, style]}>{displayContent}</Text>;
|
|
115
121
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, type FC, type ReactNode } from "react";
|
|
2
2
|
import { Text, StyleSheet, Platform, type TextStyle } from "react-native";
|
|
3
3
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type HeadingProps = {
|
|
6
6
|
level: number;
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
style?: TextStyle;
|
|
9
|
-
}
|
|
9
|
+
};
|
|
10
10
|
|
|
11
11
|
const ANDROID_SYSTEM_FONTS = new Set([
|
|
12
12
|
"sans-serif",
|
|
@@ -71,7 +71,7 @@ export const Heading: FC<HeadingProps> = ({ level, children, style }) => {
|
|
|
71
71
|
color: theme.colors.textMuted,
|
|
72
72
|
},
|
|
73
73
|
}),
|
|
74
|
-
[theme]
|
|
74
|
+
[headingWeight, theme],
|
|
75
75
|
);
|
|
76
76
|
|
|
77
77
|
const headingStyles = [
|
|
@@ -2,9 +2,9 @@ import { useMemo, type FC } from "react";
|
|
|
2
2
|
import { View, StyleSheet, type ViewStyle } from "react-native";
|
|
3
3
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type HorizontalRuleProps = {
|
|
6
6
|
style?: ViewStyle;
|
|
7
|
-
}
|
|
7
|
+
};
|
|
8
8
|
|
|
9
9
|
export const HorizontalRule: FC<HorizontalRuleProps> = ({ style }) => {
|
|
10
10
|
const { theme } = useMarkdownContext();
|
|
@@ -17,7 +17,7 @@ export const HorizontalRule: FC<HorizontalRuleProps> = ({ style }) => {
|
|
|
17
17
|
marginVertical: theme.spacing.xl,
|
|
18
18
|
},
|
|
19
19
|
}),
|
|
20
|
-
[theme]
|
|
20
|
+
[theme],
|
|
21
21
|
);
|
|
22
22
|
return <View style={[styles.horizontalRule, style]} />;
|
|
23
23
|
};
|