react-native-nitro-markdown 0.4.3 → 0.5.1
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 +417 -25
- package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +46 -8
- package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +2 -1
- package/cpp/bindings/HybridMarkdownParser.cpp +216 -66
- package/cpp/bindings/HybridMarkdownParser.hpp +2 -0
- package/ios/HybridMarkdownSession.swift +51 -7
- package/lib/commonjs/MarkdownContext.js.map +1 -1
- package/lib/commonjs/headless.js +61 -5
- package/lib/commonjs/headless.js.map +1 -1
- package/lib/commonjs/index.js +9 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +107 -13
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +191 -26
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/code.js +21 -2
- package/lib/commonjs/renderers/code.js.map +1 -1
- package/lib/commonjs/renderers/table/cell-content.js +32 -0
- package/lib/commonjs/renderers/table/cell-content.js.map +1 -0
- package/lib/commonjs/renderers/table/index.js +310 -0
- package/lib/commonjs/renderers/table/index.js.map +1 -0
- package/lib/commonjs/renderers/table/table-reducer.js +29 -0
- package/lib/commonjs/renderers/table/table-reducer.js.map +1 -0
- package/lib/commonjs/renderers/table/table-utils.js +68 -0
- package/lib/commonjs/renderers/table/table-utils.js.map +1 -0
- package/lib/commonjs/renderers/table/types.js +6 -0
- package/lib/commonjs/renderers/table/types.js.map +1 -0
- package/lib/commonjs/renderers/table.js +6 -306
- package/lib/commonjs/renderers/table.js.map +1 -1
- package/lib/commonjs/theme.js +10 -1
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/use-markdown-stream.js +9 -1
- package/lib/commonjs/use-markdown-stream.js.map +1 -1
- package/lib/commonjs/utils/code-highlight.js +101 -0
- package/lib/commonjs/utils/code-highlight.js.map +1 -0
- package/lib/commonjs/utils/incremental-ast.js +153 -0
- package/lib/commonjs/utils/incremental-ast.js.map +1 -0
- package/lib/module/MarkdownContext.js.map +1 -1
- package/lib/module/headless.js +56 -4
- package/lib/module/headless.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +108 -14
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +193 -28
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/code.js +21 -2
- package/lib/module/renderers/code.js.map +1 -1
- package/lib/module/renderers/table/cell-content.js +27 -0
- package/lib/module/renderers/table/cell-content.js.map +1 -0
- package/lib/module/renderers/table/index.js +305 -0
- package/lib/module/renderers/table/index.js.map +1 -0
- package/lib/module/renderers/table/table-reducer.js +24 -0
- package/lib/module/renderers/table/table-reducer.js.map +1 -0
- package/lib/module/renderers/table/table-utils.js +62 -0
- package/lib/module/renderers/table/table-utils.js.map +1 -0
- package/lib/module/renderers/table/types.js +4 -0
- package/lib/module/renderers/table/types.js.map +1 -0
- package/lib/module/renderers/table.js +1 -305
- package/lib/module/renderers/table.js.map +1 -1
- package/lib/module/theme.js +10 -1
- package/lib/module/theme.js.map +1 -1
- package/lib/module/use-markdown-stream.js +9 -1
- package/lib/module/use-markdown-stream.js.map +1 -1
- package/lib/module/utils/code-highlight.js +97 -0
- package/lib/module/utils/code-highlight.js.map +1 -0
- package/lib/module/utils/incremental-ast.js +147 -0
- package/lib/module/utils/incremental-ast.js.map +1 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts +2 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/MarkdownContext.d.ts +6 -0
- package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/headless.d.ts +18 -0
- package/lib/typescript/commonjs/headless.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +6 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts +77 -1
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +15 -0
- package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -0
- package/lib/typescript/commonjs/renderers/table/index.d.ts +11 -0
- package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/renderers/table/table-reducer.d.ts +5 -0
- package/lib/typescript/commonjs/renderers/table/table-reducer.d.ts.map +1 -0
- package/lib/typescript/commonjs/renderers/table/table-utils.d.ts +10 -0
- package/lib/typescript/commonjs/renderers/table/table-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/renderers/table/types.d.ts +24 -0
- package/lib/typescript/commonjs/renderers/table/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/renderers/table.d.ts +1 -11
- package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +14 -2
- package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme.d.ts +18 -2
- package/lib/typescript/commonjs/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-markdown-stream.d.ts +4 -0
- package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/code-highlight.d.ts +8 -0
- package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
- package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
- package/lib/typescript/module/Markdown.nitro.d.ts +2 -0
- package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/module/MarkdownContext.d.ts +6 -0
- package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
- package/lib/typescript/module/headless.d.ts +18 -0
- package/lib/typescript/module/headless.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +6 -1
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts +77 -1
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/code.d.ts.map +1 -1
- package/lib/typescript/module/renderers/table/cell-content.d.ts +15 -0
- package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -0
- package/lib/typescript/module/renderers/table/index.d.ts +11 -0
- package/lib/typescript/module/renderers/table/index.d.ts.map +1 -0
- package/lib/typescript/module/renderers/table/table-reducer.d.ts +5 -0
- package/lib/typescript/module/renderers/table/table-reducer.d.ts.map +1 -0
- package/lib/typescript/module/renderers/table/table-utils.d.ts +10 -0
- package/lib/typescript/module/renderers/table/table-utils.d.ts.map +1 -0
- package/lib/typescript/module/renderers/table/types.d.ts +24 -0
- package/lib/typescript/module/renderers/table/types.d.ts.map +1 -0
- package/lib/typescript/module/renderers/table.d.ts +1 -11
- package/lib/typescript/module/renderers/table.d.ts.map +1 -1
- package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +14 -2
- package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
- package/lib/typescript/module/theme.d.ts +18 -2
- package/lib/typescript/module/theme.d.ts.map +1 -1
- package/lib/typescript/module/use-markdown-stream.d.ts +4 -0
- package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/utils/code-highlight.d.ts +8 -0
- package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -0
- package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
- package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
- package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +38 -26
- package/nitrogen/generated/android/NitroMarkdownOnLoad.hpp +13 -4
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +49 -34
- package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +25 -24
- 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 +34 -21
- 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 +34 -2
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +6 -2
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +57 -9
- package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +6 -2
- package/package.json +9 -5
- package/react-native-nitro-markdown.podspec +1 -1
- package/src/Markdown.nitro.ts +2 -0
- package/src/MarkdownContext.ts +6 -0
- package/src/headless.ts +54 -4
- package/src/index.ts +10 -0
- package/src/markdown-stream.tsx +163 -15
- package/src/markdown.tsx +381 -26
- package/src/renderers/code.tsx +32 -3
- package/src/renderers/table/cell-content.tsx +38 -0
- package/src/renderers/table/index.tsx +419 -0
- package/src/renderers/table/table-reducer.ts +36 -0
- package/src/renderers/table/table-utils.ts +81 -0
- package/src/renderers/table/types.ts +24 -0
- package/src/renderers/table.tsx +1 -401
- package/src/specs/MarkdownSession.nitro.ts +16 -2
- package/src/theme.ts +29 -1
- package/src/use-markdown-stream.ts +10 -0
- package/src/utils/code-highlight.ts +102 -0
- package/src/utils/incremental-ast.ts +224 -0
package/src/markdown.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
memo,
|
|
3
|
+
useCallback,
|
|
2
4
|
useEffect,
|
|
3
5
|
useMemo,
|
|
4
6
|
type FC,
|
|
@@ -10,8 +12,12 @@ import {
|
|
|
10
12
|
StyleSheet,
|
|
11
13
|
View,
|
|
12
14
|
Text,
|
|
15
|
+
FlatList,
|
|
13
16
|
Platform,
|
|
17
|
+
type ListRenderItemInfo,
|
|
18
|
+
type FlatListProps,
|
|
14
19
|
type StyleProp,
|
|
20
|
+
type TextStyle,
|
|
15
21
|
type ViewStyle,
|
|
16
22
|
} from "react-native";
|
|
17
23
|
import {
|
|
@@ -48,8 +54,201 @@ import {
|
|
|
48
54
|
type NodeStyleOverrides,
|
|
49
55
|
type StylingStrategy,
|
|
50
56
|
} from "./theme";
|
|
57
|
+
import type { CodeHighlighter } from "./utils/code-highlight";
|
|
51
58
|
|
|
52
59
|
const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
|
|
60
|
+
const parseAstCache = new Map<string, MarkdownNode>();
|
|
61
|
+
const MAX_PARSE_CACHE_ENTRIES = 32;
|
|
62
|
+
const MAX_CACHEABLE_TEXT_LENGTH = 24_000;
|
|
63
|
+
|
|
64
|
+
export type AstTransform = (ast: MarkdownNode) => MarkdownNode;
|
|
65
|
+
export type MarkdownVirtualizationOptions = Pick<
|
|
66
|
+
FlatListProps<MarkdownNode>,
|
|
67
|
+
| "initialNumToRender"
|
|
68
|
+
| "maxToRenderPerBatch"
|
|
69
|
+
| "windowSize"
|
|
70
|
+
| "updateCellsBatchingPeriod"
|
|
71
|
+
| "removeClippedSubviews"
|
|
72
|
+
>;
|
|
73
|
+
|
|
74
|
+
export type MarkdownPlugin = {
|
|
75
|
+
/**
|
|
76
|
+
* Optional plugin name used for diagnostics and debugging.
|
|
77
|
+
*/
|
|
78
|
+
name?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Optional plugin version metadata for diagnostics.
|
|
81
|
+
*/
|
|
82
|
+
version?: string | number;
|
|
83
|
+
/**
|
|
84
|
+
* Execution priority. Higher values run first (default: 0).
|
|
85
|
+
*/
|
|
86
|
+
priority?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Optional text preprocessor executed before native parsing.
|
|
89
|
+
* Should return a full markdown string.
|
|
90
|
+
*/
|
|
91
|
+
beforeParse?: (markdown: string) => string;
|
|
92
|
+
/**
|
|
93
|
+
* Optional AST postprocessor executed after native parsing.
|
|
94
|
+
*/
|
|
95
|
+
afterParse?: AstTransform;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const isMarkdownNode = (value: unknown): value is MarkdownNode => {
|
|
99
|
+
if (typeof value !== "object" || value === null) return false;
|
|
100
|
+
return typeof Reflect.get(value, "type") === "string";
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const warnInDev = (message: string, error?: unknown): void => {
|
|
104
|
+
if (typeof __DEV__ === "undefined" || !__DEV__) return;
|
|
105
|
+
|
|
106
|
+
const runtimeConsole = Reflect.get(globalThis, "console");
|
|
107
|
+
if (
|
|
108
|
+
typeof runtimeConsole === "object" &&
|
|
109
|
+
runtimeConsole !== null &&
|
|
110
|
+
"warn" in runtimeConsole &&
|
|
111
|
+
typeof runtimeConsole.warn === "function"
|
|
112
|
+
) {
|
|
113
|
+
runtimeConsole.warn(message, error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
|
|
118
|
+
return {
|
|
119
|
+
...node,
|
|
120
|
+
children: node.children?.map(cloneMarkdownNode),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const getParserOptionsKey = (options?: ParserOptions): string => {
|
|
125
|
+
if (!options) return "gfm:default|math:default";
|
|
126
|
+
|
|
127
|
+
const gfm = options.gfm === undefined ? "default" : options.gfm ? "1" : "0";
|
|
128
|
+
const math =
|
|
129
|
+
options.math === undefined ? "default" : options.math ? "1" : "0";
|
|
130
|
+
return `gfm:${gfm}|math:${math}`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const normalizeParserOptions = (
|
|
134
|
+
options?: ParserOptions,
|
|
135
|
+
): ParserOptions | undefined => {
|
|
136
|
+
if (!options) return undefined;
|
|
137
|
+
|
|
138
|
+
const gfm = options.gfm;
|
|
139
|
+
const math = options.math;
|
|
140
|
+
|
|
141
|
+
if (gfm === undefined && math === undefined) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
gfm,
|
|
147
|
+
math,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const parseWithNativeParser = (
|
|
152
|
+
text: string,
|
|
153
|
+
options?: ParserOptions,
|
|
154
|
+
): MarkdownNode => {
|
|
155
|
+
if (options) {
|
|
156
|
+
return parseMarkdownWithOptions(text, options);
|
|
157
|
+
}
|
|
158
|
+
return parseMarkdown(text);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const getCachedParsedAst = (
|
|
162
|
+
text: string,
|
|
163
|
+
options?: ParserOptions,
|
|
164
|
+
): MarkdownNode => {
|
|
165
|
+
if (text.length > MAX_CACHEABLE_TEXT_LENGTH) {
|
|
166
|
+
return parseWithNativeParser(text, options);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cacheKey = `${getParserOptionsKey(options)}|${text}`;
|
|
170
|
+
const cachedNode = parseAstCache.get(cacheKey);
|
|
171
|
+
if (cachedNode) {
|
|
172
|
+
parseAstCache.delete(cacheKey);
|
|
173
|
+
parseAstCache.set(cacheKey, cachedNode);
|
|
174
|
+
return cloneMarkdownNode(cachedNode);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const parsedNode = parseWithNativeParser(text, options);
|
|
178
|
+
parseAstCache.set(cacheKey, parsedNode);
|
|
179
|
+
if (parseAstCache.size > MAX_PARSE_CACHE_ENTRIES) {
|
|
180
|
+
const oldestCacheKey = parseAstCache.keys().next().value;
|
|
181
|
+
if (typeof oldestCacheKey === "string") {
|
|
182
|
+
parseAstCache.delete(oldestCacheKey);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return cloneMarkdownNode(parsedNode);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const applyBeforeParsePlugins = (
|
|
190
|
+
markdown: string,
|
|
191
|
+
plugins?: MarkdownPlugin[],
|
|
192
|
+
onError?: (error: Error, phase: 'before-plugin', pluginName?: string) => void,
|
|
193
|
+
): string => {
|
|
194
|
+
if (!plugins || plugins.length === 0) {
|
|
195
|
+
return markdown;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sorted = [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
199
|
+
let nextMarkdown = markdown;
|
|
200
|
+
for (const plugin of sorted) {
|
|
201
|
+
if (!plugin.beforeParse) continue;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const transformed = plugin.beforeParse(nextMarkdown);
|
|
205
|
+
if (typeof transformed === "string") {
|
|
206
|
+
nextMarkdown = transformed;
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const pluginLabel = plugin.name ? ` (${plugin.name})` : "";
|
|
210
|
+
warnInDev(
|
|
211
|
+
`[react-native-nitro-markdown] plugin beforeParse${pluginLabel} threw; using previous markdown.`,
|
|
212
|
+
error,
|
|
213
|
+
);
|
|
214
|
+
onError?.(error instanceof Error ? error : new Error(String(error)), 'before-plugin', plugin.name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return nextMarkdown;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const applyAfterParsePlugins = (
|
|
222
|
+
ast: MarkdownNode,
|
|
223
|
+
plugins?: MarkdownPlugin[],
|
|
224
|
+
onError?: (error: Error, phase: 'after-plugin', pluginName?: string) => void,
|
|
225
|
+
): MarkdownNode => {
|
|
226
|
+
if (!plugins || plugins.length === 0) {
|
|
227
|
+
return ast;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const sorted = [...plugins].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
231
|
+
let nextAst = ast;
|
|
232
|
+
for (const plugin of sorted) {
|
|
233
|
+
if (!plugin.afterParse) continue;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const transformed = plugin.afterParse(nextAst);
|
|
237
|
+
if (isMarkdownNode(transformed)) {
|
|
238
|
+
nextAst = transformed;
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const pluginLabel = plugin.name ? ` (${plugin.name})` : "";
|
|
242
|
+
warnInDev(
|
|
243
|
+
`[react-native-nitro-markdown] plugin afterParse${pluginLabel} threw; using previous AST.`,
|
|
244
|
+
error,
|
|
245
|
+
);
|
|
246
|
+
onError?.(error instanceof Error ? error : new Error(String(error)), 'after-plugin', plugin.name);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return nextAst;
|
|
251
|
+
};
|
|
53
252
|
|
|
54
253
|
export type MarkdownProps = {
|
|
55
254
|
/**
|
|
@@ -60,6 +259,20 @@ export type MarkdownProps = {
|
|
|
60
259
|
* Parser options to enable GFM or Math support.
|
|
61
260
|
*/
|
|
62
261
|
options?: ParserOptions;
|
|
262
|
+
/**
|
|
263
|
+
* Optional parser plugins for preprocessing and AST postprocessing.
|
|
264
|
+
*/
|
|
265
|
+
plugins?: MarkdownPlugin[];
|
|
266
|
+
/**
|
|
267
|
+
* Optional pre-parsed AST.
|
|
268
|
+
* When provided, native parse is skipped and this tree is rendered instead.
|
|
269
|
+
*/
|
|
270
|
+
sourceAst?: MarkdownNode;
|
|
271
|
+
/**
|
|
272
|
+
* Optional transform applied after parsing and before rendering.
|
|
273
|
+
* The transformed AST is also returned in `onParseComplete`.
|
|
274
|
+
*/
|
|
275
|
+
astTransform?: AstTransform;
|
|
63
276
|
/**
|
|
64
277
|
* Callback fired when parsing begins.
|
|
65
278
|
*/
|
|
@@ -72,6 +285,13 @@ export type MarkdownProps = {
|
|
|
72
285
|
ast: MarkdownNode;
|
|
73
286
|
text: string;
|
|
74
287
|
}) => void;
|
|
288
|
+
/**
|
|
289
|
+
* Called when a parse error or plugin error occurs.
|
|
290
|
+
* @param error - The thrown error.
|
|
291
|
+
* @param phase - Where the error occurred.
|
|
292
|
+
* @param pluginName - The plugin name, if applicable.
|
|
293
|
+
*/
|
|
294
|
+
onError?: (error: Error, phase: 'parse' | 'before-plugin' | 'after-plugin', pluginName?: string) => void;
|
|
75
295
|
/**
|
|
76
296
|
* Custom renderers for specific markdown node types.
|
|
77
297
|
* Each renderer receives { node, children, Renderer } plus type-specific props.
|
|
@@ -107,11 +327,43 @@ export type MarkdownProps = {
|
|
|
107
327
|
* Return false to prevent the default openURL behavior.
|
|
108
328
|
*/
|
|
109
329
|
onLinkPress?: LinkPressHandler;
|
|
330
|
+
/**
|
|
331
|
+
* Enables top-level block virtualization for very large markdown documents.
|
|
332
|
+
* Best used when Markdown is the primary scroll container on screen.
|
|
333
|
+
* - `true`: always virtualize when block threshold is met
|
|
334
|
+
* - `"auto"`: virtualize only when threshold is met (recommended for large docs)
|
|
335
|
+
* - `false`: disable virtualization (default)
|
|
336
|
+
*/
|
|
337
|
+
virtualize?: boolean | "auto";
|
|
338
|
+
/**
|
|
339
|
+
* Minimum number of top-level blocks before virtualization is activated.
|
|
340
|
+
* Helps avoid FlatList overhead on small documents.
|
|
341
|
+
*/
|
|
342
|
+
virtualizationMinBlocks?: number;
|
|
343
|
+
/**
|
|
344
|
+
* Optional FlatList tuning for virtualization.
|
|
345
|
+
*/
|
|
346
|
+
virtualization?: MarkdownVirtualizationOptions;
|
|
347
|
+
/**
|
|
348
|
+
* Optional configuration for the table renderer.
|
|
349
|
+
*/
|
|
350
|
+
tableOptions?: {
|
|
351
|
+
minColumnWidth?: number;
|
|
352
|
+
measurementStabilizeMs?: number;
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* Enable built-in syntax highlighting for code blocks.
|
|
356
|
+
* Pass `true` to use the built-in tokenizer, or a custom highlighter function.
|
|
357
|
+
*/
|
|
358
|
+
highlightCode?: boolean | CodeHighlighter;
|
|
110
359
|
};
|
|
111
360
|
|
|
112
361
|
export const Markdown: FC<MarkdownProps> = ({
|
|
113
362
|
children,
|
|
114
363
|
options,
|
|
364
|
+
plugins,
|
|
365
|
+
sourceAst,
|
|
366
|
+
astTransform,
|
|
115
367
|
renderers = {},
|
|
116
368
|
theme: userTheme,
|
|
117
369
|
styles: nodeStyles,
|
|
@@ -120,41 +372,82 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
120
372
|
onParsingInProgress,
|
|
121
373
|
onParseComplete,
|
|
122
374
|
onLinkPress,
|
|
375
|
+
onError,
|
|
376
|
+
virtualize = false,
|
|
377
|
+
virtualizationMinBlocks = 40,
|
|
378
|
+
virtualization,
|
|
379
|
+
tableOptions,
|
|
380
|
+
highlightCode,
|
|
123
381
|
}) => {
|
|
382
|
+
const parserOptionGfm = options?.gfm;
|
|
383
|
+
const parserOptionMath = options?.math;
|
|
384
|
+
|
|
124
385
|
const parseResult = useMemo(() => {
|
|
125
386
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
387
|
+
const markdownToParse = applyBeforeParsePlugins(children, plugins, onError ? (e, phase, name) => onError(e, phase, name) : undefined);
|
|
388
|
+
const parserOptions = normalizeParserOptions({
|
|
389
|
+
gfm: parserOptionGfm,
|
|
390
|
+
math: parserOptionMath,
|
|
391
|
+
});
|
|
392
|
+
let parsedAst = sourceAst
|
|
393
|
+
? cloneMarkdownNode(sourceAst)
|
|
394
|
+
: getCachedParsedAst(markdownToParse, parserOptions);
|
|
395
|
+
parsedAst = applyAfterParsePlugins(parsedAst, plugins, onError ? (e, phase, name) => onError(e, phase, name) : undefined);
|
|
396
|
+
|
|
397
|
+
let ast = parsedAst;
|
|
398
|
+
if (astTransform) {
|
|
399
|
+
try {
|
|
400
|
+
const nextAst = astTransform(parsedAst);
|
|
401
|
+
if (isMarkdownNode(nextAst)) {
|
|
402
|
+
ast = nextAst;
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
warnInDev(
|
|
406
|
+
"[react-native-nitro-markdown] astTransform threw; falling back to parsed AST.",
|
|
407
|
+
error,
|
|
408
|
+
);
|
|
409
|
+
ast = parsedAst;
|
|
410
|
+
}
|
|
131
411
|
}
|
|
132
412
|
|
|
133
413
|
return {
|
|
134
414
|
ast,
|
|
135
|
-
text: getFlattenedText(ast),
|
|
136
415
|
};
|
|
137
|
-
} catch {
|
|
416
|
+
} catch (parseError) {
|
|
417
|
+
onError?.(parseError instanceof Error ? parseError : new Error(String(parseError)), 'parse');
|
|
138
418
|
return {
|
|
139
419
|
ast: null,
|
|
140
|
-
text: "",
|
|
141
420
|
};
|
|
142
421
|
}
|
|
143
|
-
}, [
|
|
422
|
+
}, [
|
|
423
|
+
children,
|
|
424
|
+
parserOptionGfm,
|
|
425
|
+
parserOptionMath,
|
|
426
|
+
plugins,
|
|
427
|
+
sourceAst,
|
|
428
|
+
astTransform,
|
|
429
|
+
onError,
|
|
430
|
+
]);
|
|
144
431
|
|
|
145
432
|
useEffect(() => {
|
|
146
433
|
onParsingInProgress?.();
|
|
147
|
-
}, [
|
|
434
|
+
}, [
|
|
435
|
+
children,
|
|
436
|
+
parserOptionGfm,
|
|
437
|
+
parserOptionMath,
|
|
438
|
+
plugins,
|
|
439
|
+
onParsingInProgress,
|
|
440
|
+
]);
|
|
148
441
|
|
|
149
442
|
useEffect(() => {
|
|
150
|
-
if (!parseResult.ast) return;
|
|
443
|
+
if (!parseResult.ast || !onParseComplete) return;
|
|
151
444
|
|
|
152
|
-
onParseComplete
|
|
445
|
+
onParseComplete({
|
|
153
446
|
raw: children,
|
|
154
447
|
ast: parseResult.ast,
|
|
155
|
-
text: parseResult.
|
|
448
|
+
text: getFlattenedText(parseResult.ast),
|
|
156
449
|
});
|
|
157
|
-
}, [children, onParseComplete, parseResult.ast
|
|
450
|
+
}, [children, onParseComplete, parseResult.ast]);
|
|
158
451
|
|
|
159
452
|
const theme = useMemo(() => {
|
|
160
453
|
const base =
|
|
@@ -165,6 +458,43 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
165
458
|
}, [userTheme, stylingStrategy]);
|
|
166
459
|
|
|
167
460
|
const baseStyles = getBaseStyles(theme);
|
|
461
|
+
const contextValue = useMemo(
|
|
462
|
+
() => ({
|
|
463
|
+
renderers,
|
|
464
|
+
theme,
|
|
465
|
+
styles: nodeStyles,
|
|
466
|
+
stylingStrategy,
|
|
467
|
+
onLinkPress,
|
|
468
|
+
tableOptions,
|
|
469
|
+
highlightCode,
|
|
470
|
+
}),
|
|
471
|
+
[renderers, theme, nodeStyles, stylingStrategy, onLinkPress, tableOptions, highlightCode],
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const topLevelBlocks =
|
|
475
|
+
parseResult.ast?.type === "document"
|
|
476
|
+
? (parseResult.ast.children ?? [])
|
|
477
|
+
: parseResult.ast
|
|
478
|
+
? [parseResult.ast]
|
|
479
|
+
: [];
|
|
480
|
+
const shouldVirtualizeBySetting =
|
|
481
|
+
virtualize === true ||
|
|
482
|
+
(virtualize === "auto" && topLevelBlocks.length >= virtualizationMinBlocks);
|
|
483
|
+
const shouldVirtualize =
|
|
484
|
+
parseResult.ast !== null && shouldVirtualizeBySetting;
|
|
485
|
+
|
|
486
|
+
const keyExtractor = useCallback((node: MarkdownNode, index: number) => {
|
|
487
|
+
const beg = typeof node.beg === "number" ? node.beg : index;
|
|
488
|
+
const end = typeof node.end === "number" ? node.end : index;
|
|
489
|
+
return `${node.type}:${beg}:${end}:${index}`;
|
|
490
|
+
}, []);
|
|
491
|
+
|
|
492
|
+
const renderVirtualizedItem = useCallback(
|
|
493
|
+
({ item }: ListRenderItemInfo<MarkdownNode>): ReactElement => (
|
|
494
|
+
<NodeRenderer node={item} depth={0} inListItem={false} />
|
|
495
|
+
),
|
|
496
|
+
[],
|
|
497
|
+
);
|
|
168
498
|
|
|
169
499
|
if (!parseResult.ast) {
|
|
170
500
|
return (
|
|
@@ -175,17 +505,28 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
175
505
|
}
|
|
176
506
|
|
|
177
507
|
return (
|
|
178
|
-
<MarkdownContext.Provider
|
|
179
|
-
value={{
|
|
180
|
-
renderers,
|
|
181
|
-
theme,
|
|
182
|
-
styles: nodeStyles,
|
|
183
|
-
stylingStrategy,
|
|
184
|
-
onLinkPress,
|
|
185
|
-
}}
|
|
186
|
-
>
|
|
508
|
+
<MarkdownContext.Provider value={contextValue}>
|
|
187
509
|
<View style={[baseStyles.container, style]}>
|
|
188
|
-
|
|
510
|
+
{shouldVirtualize ? (
|
|
511
|
+
<FlatList
|
|
512
|
+
data={topLevelBlocks}
|
|
513
|
+
renderItem={renderVirtualizedItem}
|
|
514
|
+
keyExtractor={keyExtractor}
|
|
515
|
+
style={baseStyles.virtualizedList}
|
|
516
|
+
initialNumToRender={virtualization?.initialNumToRender ?? 12}
|
|
517
|
+
maxToRenderPerBatch={virtualization?.maxToRenderPerBatch ?? 12}
|
|
518
|
+
windowSize={virtualization?.windowSize ?? 10}
|
|
519
|
+
updateCellsBatchingPeriod={
|
|
520
|
+
virtualization?.updateCellsBatchingPeriod ?? 16
|
|
521
|
+
}
|
|
522
|
+
removeClippedSubviews={
|
|
523
|
+
virtualization?.removeClippedSubviews ?? true
|
|
524
|
+
}
|
|
525
|
+
showsVerticalScrollIndicator={false}
|
|
526
|
+
/>
|
|
527
|
+
) : (
|
|
528
|
+
<NodeRenderer node={parseResult.ast} depth={0} inListItem={false} />
|
|
529
|
+
)}
|
|
189
530
|
</View>
|
|
190
531
|
</MarkdownContext.Provider>
|
|
191
532
|
);
|
|
@@ -206,7 +547,7 @@ const isInline = (type: MarkdownNode["type"]): boolean => {
|
|
|
206
547
|
);
|
|
207
548
|
};
|
|
208
549
|
|
|
209
|
-
const
|
|
550
|
+
const NodeRendererComponent: FC<NodeRendererProps> = ({
|
|
210
551
|
node,
|
|
211
552
|
depth,
|
|
212
553
|
inListItem,
|
|
@@ -341,7 +682,9 @@ const NodeRenderer: FC<NodeRendererProps> = ({
|
|
|
341
682
|
}
|
|
342
683
|
}
|
|
343
684
|
|
|
344
|
-
const nodeStyleOverride = nodeStyles?.[node.type]
|
|
685
|
+
const nodeStyleOverride = nodeStyles?.[node.type] as
|
|
686
|
+
| (ViewStyle & TextStyle)
|
|
687
|
+
| undefined;
|
|
345
688
|
|
|
346
689
|
switch (node.type) {
|
|
347
690
|
case "document":
|
|
@@ -521,6 +864,15 @@ const NodeRenderer: FC<NodeRendererProps> = ({
|
|
|
521
864
|
}
|
|
522
865
|
};
|
|
523
866
|
|
|
867
|
+
const NodeRenderer = memo(NodeRendererComponent, (previousProps, nextProps) => {
|
|
868
|
+
return (
|
|
869
|
+
previousProps.node === nextProps.node &&
|
|
870
|
+
previousProps.depth === nextProps.depth &&
|
|
871
|
+
previousProps.inListItem === nextProps.inListItem &&
|
|
872
|
+
previousProps.parentIsText === nextProps.parentIsText
|
|
873
|
+
);
|
|
874
|
+
}) as FC<NodeRendererProps>;
|
|
875
|
+
|
|
524
876
|
type BaseStyles = ReturnType<typeof createBaseStyles>;
|
|
525
877
|
|
|
526
878
|
const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
|
|
@@ -537,6 +889,9 @@ const createBaseStyles = (theme: MarkdownTheme) =>
|
|
|
537
889
|
container: {
|
|
538
890
|
flex: 1,
|
|
539
891
|
},
|
|
892
|
+
virtualizedList: {
|
|
893
|
+
flex: 1,
|
|
894
|
+
},
|
|
540
895
|
document: {
|
|
541
896
|
flex: 1,
|
|
542
897
|
},
|
package/src/renderers/code.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { getTextContent } from "../headless";
|
|
12
12
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
13
13
|
import type { MarkdownNode } from "../headless";
|
|
14
|
+
import { defaultHighlighter, type HighlightedToken } from "../utils/code-highlight";
|
|
14
15
|
|
|
15
16
|
type CodeBlockProps = {
|
|
16
17
|
language?: string;
|
|
@@ -25,7 +26,14 @@ export const CodeBlock: FC<CodeBlockProps> = ({
|
|
|
25
26
|
node,
|
|
26
27
|
style,
|
|
27
28
|
}) => {
|
|
28
|
-
const
|
|
29
|
+
const ctx = useMarkdownContext();
|
|
30
|
+
const { theme } = ctx;
|
|
31
|
+
|
|
32
|
+
const highlighter = ctx.highlightCode === true
|
|
33
|
+
? defaultHighlighter
|
|
34
|
+
: typeof ctx.highlightCode === 'function'
|
|
35
|
+
? ctx.highlightCode
|
|
36
|
+
: null;
|
|
29
37
|
|
|
30
38
|
const displayContent = content ?? (node ? getTextContent(node) : "");
|
|
31
39
|
|
|
@@ -70,8 +78,29 @@ export const CodeBlock: FC<CodeBlockProps> = ({
|
|
|
70
78
|
{showLanguage ? (
|
|
71
79
|
<Text style={styles.codeLanguage}>{language}</Text>
|
|
72
80
|
) : null}
|
|
73
|
-
<ScrollView
|
|
74
|
-
|
|
81
|
+
<ScrollView
|
|
82
|
+
horizontal
|
|
83
|
+
showsHorizontalScrollIndicator={false}
|
|
84
|
+
bounces={false}
|
|
85
|
+
>
|
|
86
|
+
{highlighter && language ? (
|
|
87
|
+
<Text style={styles.codeBlockText} selectable>
|
|
88
|
+
{highlighter(language, displayContent).map((token: HighlightedToken, i: number) => {
|
|
89
|
+
const tokenColor = ctx.theme.colors.codeTokenColors?.[token.type];
|
|
90
|
+
return tokenColor ? (
|
|
91
|
+
<Text key={i} style={{ color: tokenColor }}>
|
|
92
|
+
{token.text}
|
|
93
|
+
</Text>
|
|
94
|
+
) : (
|
|
95
|
+
<Text key={i}>{token.text}</Text>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</Text>
|
|
99
|
+
) : (
|
|
100
|
+
<Text style={styles.codeBlockText} selectable>
|
|
101
|
+
{displayContent}
|
|
102
|
+
</Text>
|
|
103
|
+
)}
|
|
75
104
|
</ScrollView>
|
|
76
105
|
</View>
|
|
77
106
|
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type FC, type ComponentType } from "react";
|
|
2
|
+
import { View, Text, type StyleProp, type TextStyle } from "react-native";
|
|
3
|
+
import type { MarkdownNode } from "../../headless";
|
|
4
|
+
import type { NodeRendererProps } from "../../MarkdownContext";
|
|
5
|
+
|
|
6
|
+
type CellContentProps = {
|
|
7
|
+
node: MarkdownNode;
|
|
8
|
+
Renderer: ComponentType<NodeRendererProps>;
|
|
9
|
+
styles: {
|
|
10
|
+
cellContentWrapper: object;
|
|
11
|
+
};
|
|
12
|
+
textStyle?: StyleProp<TextStyle>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const CellContent: FC<CellContentProps> = ({
|
|
16
|
+
node,
|
|
17
|
+
Renderer,
|
|
18
|
+
styles,
|
|
19
|
+
textStyle,
|
|
20
|
+
}) => {
|
|
21
|
+
if (!node.children || node.children.length === 0) {
|
|
22
|
+
return <Text style={textStyle}>{node.content ?? ""}</Text>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<View style={styles.cellContentWrapper}>
|
|
27
|
+
{node.children.map((child, idx) => (
|
|
28
|
+
<Renderer
|
|
29
|
+
key={idx}
|
|
30
|
+
node={child}
|
|
31
|
+
depth={0}
|
|
32
|
+
inListItem={false}
|
|
33
|
+
parentIsText={false}
|
|
34
|
+
/>
|
|
35
|
+
))}
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
};
|