react-native-nitro-markdown 0.5.2 → 0.5.4
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 +311 -669
- package/android/CMakeLists.txt +8 -1
- package/android/build.gradle +9 -2
- package/android/consumer-rules.pro +31 -0
- package/android/gradle.properties +2 -0
- package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +68 -22
- package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +6 -18
- package/cpp/bindings/HybridMarkdownParser.cpp +40 -12
- package/cpp/bindings/HybridMarkdownParser.hpp +4 -4
- package/cpp/bindings/HybridMarkdownSession.cpp +2 -0
- package/cpp/core/MD4CParser.cpp +147 -86
- package/cpp/core/MarkdownSessionCore.cpp +2 -0
- package/cpp/core/MarkdownTypes.hpp +1 -1
- package/ios/HybridMarkdownSession.swift +89 -46
- package/lib/commonjs/headless.js +34 -8
- package/lib/commonjs/headless.js.map +1 -1
- package/lib/commonjs/index.js +48 -38
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +1 -1
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +57 -16
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/blockquote.js +15 -13
- package/lib/commonjs/renderers/blockquote.js.map +1 -1
- package/lib/commonjs/renderers/code.js +58 -54
- package/lib/commonjs/renderers/code.js.map +1 -1
- package/lib/commonjs/renderers/heading.js +48 -46
- package/lib/commonjs/renderers/heading.js.map +1 -1
- package/lib/commonjs/renderers/horizontal-rule.js +10 -8
- package/lib/commonjs/renderers/horizontal-rule.js.map +1 -1
- package/lib/commonjs/renderers/image.js +18 -4
- package/lib/commonjs/renderers/image.js.map +1 -1
- package/lib/commonjs/renderers/link.js +7 -2
- package/lib/commonjs/renderers/link.js.map +1 -1
- package/lib/commonjs/renderers/list.js +75 -68
- package/lib/commonjs/renderers/list.js.map +1 -1
- package/lib/commonjs/renderers/math.js +8 -5
- package/lib/commonjs/renderers/math.js.map +1 -1
- package/lib/commonjs/renderers/paragraph.js +15 -13
- package/lib/commonjs/renderers/paragraph.js.map +1 -1
- package/lib/commonjs/renderers/style-cache.js +14 -0
- package/lib/commonjs/renderers/style-cache.js.map +1 -0
- package/lib/commonjs/renderers/table/cell-content.js +1 -1
- package/lib/commonjs/renderers/table/cell-content.js.map +1 -1
- package/lib/commonjs/renderers/table/index.js +17 -6
- package/lib/commonjs/renderers/table/index.js.map +1 -1
- package/lib/commonjs/theme.js +7 -7
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/utils/code-highlight.js +24 -25
- package/lib/commonjs/utils/code-highlight.js.map +1 -1
- package/lib/module/headless.js +35 -7
- package/lib/module/headless.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 +1 -1
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +58 -17
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/blockquote.js +15 -13
- package/lib/module/renderers/blockquote.js.map +1 -1
- package/lib/module/renderers/code.js +58 -54
- package/lib/module/renderers/code.js.map +1 -1
- package/lib/module/renderers/heading.js +48 -46
- package/lib/module/renderers/heading.js.map +1 -1
- package/lib/module/renderers/horizontal-rule.js +10 -8
- package/lib/module/renderers/horizontal-rule.js.map +1 -1
- package/lib/module/renderers/image.js +19 -5
- package/lib/module/renderers/image.js.map +1 -1
- package/lib/module/renderers/link.js +7 -2
- package/lib/module/renderers/link.js.map +1 -1
- package/lib/module/renderers/list.js +75 -68
- package/lib/module/renderers/list.js.map +1 -1
- package/lib/module/renderers/math.js +8 -5
- package/lib/module/renderers/math.js.map +1 -1
- package/lib/module/renderers/paragraph.js +15 -13
- package/lib/module/renderers/paragraph.js.map +1 -1
- package/lib/module/renderers/style-cache.js +10 -0
- package/lib/module/renderers/style-cache.js.map +1 -0
- package/lib/module/renderers/table/cell-content.js +1 -1
- package/lib/module/renderers/table/cell-content.js.map +1 -1
- package/lib/module/renderers/table/index.js +17 -6
- package/lib/module/renderers/table/index.js.map +1 -1
- package/lib/module/theme.js +7 -7
- package/lib/module/theme.js.map +1 -1
- package/lib/module/utils/code-highlight.js +24 -25
- package/lib/module/utils/code-highlight.js.map +1 -1
- package/lib/typescript/commonjs/Markdown.nitro.d.ts +1 -0
- package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/headless.d.ts +10 -2
- package/lib/typescript/commonjs/headless.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts +8 -3
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/blockquote.d.ts +1 -1
- package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/heading.d.ts +1 -1
- package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +1 -1
- package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/list.d.ts +1 -1
- package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/math.d.ts +1 -1
- package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/paragraph.d.ts +1 -1
- package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/style-cache.d.ts +3 -0
- package/lib/typescript/commonjs/renderers/style-cache.d.ts.map +1 -0
- package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +4 -3
- package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/code-highlight.d.ts +1 -1
- package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -1
- package/lib/typescript/module/Markdown.nitro.d.ts +1 -0
- package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
- package/lib/typescript/module/headless.d.ts +10 -2
- package/lib/typescript/module/headless.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts +8 -3
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/blockquote.d.ts +1 -1
- package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
- package/lib/typescript/module/renderers/code.d.ts.map +1 -1
- package/lib/typescript/module/renderers/heading.d.ts +1 -1
- package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
- package/lib/typescript/module/renderers/horizontal-rule.d.ts +1 -1
- package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
- package/lib/typescript/module/renderers/image.d.ts.map +1 -1
- package/lib/typescript/module/renderers/link.d.ts.map +1 -1
- package/lib/typescript/module/renderers/list.d.ts +1 -1
- package/lib/typescript/module/renderers/list.d.ts.map +1 -1
- package/lib/typescript/module/renderers/math.d.ts +1 -1
- package/lib/typescript/module/renderers/math.d.ts.map +1 -1
- package/lib/typescript/module/renderers/paragraph.d.ts +1 -1
- package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
- package/lib/typescript/module/renderers/style-cache.d.ts +3 -0
- package/lib/typescript/module/renderers/style-cache.d.ts.map +1 -0
- package/lib/typescript/module/renderers/table/cell-content.d.ts +4 -3
- package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -1
- package/lib/typescript/module/renderers/table/index.d.ts.map +1 -1
- package/lib/typescript/module/theme.d.ts.map +1 -1
- package/lib/typescript/module/utils/code-highlight.d.ts +1 -1
- package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -1
- package/nitro.json +12 -3
- package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
- package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +2 -2
- package/nitrogen/generated/ios/NitroMarkdown+autolinking.rb +2 -0
- package/nitrogen/generated/shared/c++/ParserOptions.hpp +6 -2
- package/package.json +8 -6
- package/react-native-nitro-markdown.podspec +3 -0
- package/src/Markdown.nitro.ts +1 -0
- package/src/headless.ts +58 -8
- package/src/index.ts +16 -2
- package/src/markdown-stream.tsx +1 -0
- package/src/markdown.tsx +108 -23
- package/src/renderers/blockquote.tsx +22 -17
- package/src/renderers/code.tsx +76 -57
- package/src/renderers/heading.tsx +60 -54
- package/src/renderers/horizontal-rule.tsx +17 -12
- package/src/renderers/image.tsx +24 -5
- package/src/renderers/link.tsx +8 -2
- package/src/renderers/list.tsx +93 -74
- package/src/renderers/math.tsx +14 -5
- package/src/renderers/paragraph.tsx +22 -17
- package/src/renderers/style-cache.ts +14 -0
- package/src/renderers/table/cell-content.tsx +15 -4
- package/src/renderers/table/index.tsx +30 -13
- package/src/theme.ts +34 -14
- package/src/utils/code-highlight.ts +133 -44
package/src/markdown.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
5
|
useMemo,
|
|
6
|
+
useRef,
|
|
6
7
|
type FC,
|
|
7
8
|
Fragment,
|
|
8
9
|
type ReactElement,
|
|
@@ -56,10 +57,54 @@ import {
|
|
|
56
57
|
} from "./theme";
|
|
57
58
|
import type { CodeHighlighter } from "./utils/code-highlight";
|
|
58
59
|
|
|
60
|
+
function hashString(str: string): number {
|
|
61
|
+
let hash = 0;
|
|
62
|
+
for (let i = 0; i < str.length; i++) {
|
|
63
|
+
const char = str.charCodeAt(i);
|
|
64
|
+
hash = (hash << 5) - hash + char;
|
|
65
|
+
hash = hash & hash; // Convert to 32bit int
|
|
66
|
+
}
|
|
67
|
+
return hash;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ERROR_PHASE = {
|
|
71
|
+
PARSE: "parse",
|
|
72
|
+
BEFORE_PLUGIN: "before-plugin",
|
|
73
|
+
AFTER_PLUGIN: "after-plugin",
|
|
74
|
+
} as const;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Safely invoke the onError callback, preventing callback exceptions from
|
|
78
|
+
* propagating and breaking the render cycle.
|
|
79
|
+
*/
|
|
80
|
+
function safeOnError<P extends string>(
|
|
81
|
+
onError: ((error: Error, phase: P, pluginName?: string) => void) | undefined,
|
|
82
|
+
error: unknown,
|
|
83
|
+
phase: P,
|
|
84
|
+
pluginName?: string,
|
|
85
|
+
): void {
|
|
86
|
+
try {
|
|
87
|
+
onError?.(
|
|
88
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
89
|
+
phase,
|
|
90
|
+
pluginName,
|
|
91
|
+
);
|
|
92
|
+
} catch (callbackError) {
|
|
93
|
+
if (__DEV__) {
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.warn(
|
|
96
|
+
"[NitroMarkdown] onError callback threw an exception:",
|
|
97
|
+
callbackError,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
|
|
60
104
|
const parseAstCache = new Map<string, MarkdownNode>();
|
|
61
105
|
const MAX_PARSE_CACHE_ENTRIES = 32;
|
|
62
106
|
const MAX_CACHEABLE_TEXT_LENGTH = 24_000;
|
|
107
|
+
const EMPTY_RENDERERS: CustomRenderers = {};
|
|
63
108
|
|
|
64
109
|
export type AstTransform = (ast: MarkdownNode) => MarkdownNode;
|
|
65
110
|
export type MarkdownVirtualizationOptions = Pick<
|
|
@@ -122,12 +167,14 @@ const cloneMarkdownNode = (node: MarkdownNode): MarkdownNode => {
|
|
|
122
167
|
};
|
|
123
168
|
|
|
124
169
|
const getParserOptionsKey = (options?: ParserOptions): string => {
|
|
125
|
-
if (!options) return "gfm:default|math:default";
|
|
170
|
+
if (!options) return "gfm:default|math:default|html:default";
|
|
126
171
|
|
|
127
172
|
const gfm = options.gfm === undefined ? "default" : options.gfm ? "1" : "0";
|
|
128
173
|
const math =
|
|
129
174
|
options.math === undefined ? "default" : options.math ? "1" : "0";
|
|
130
|
-
|
|
175
|
+
const html =
|
|
176
|
+
options.html === undefined ? "default" : options.html ? "1" : "0";
|
|
177
|
+
return `gfm:${gfm}|math:${math}|html:${html}`;
|
|
131
178
|
};
|
|
132
179
|
|
|
133
180
|
const normalizeParserOptions = (
|
|
@@ -137,14 +184,16 @@ const normalizeParserOptions = (
|
|
|
137
184
|
|
|
138
185
|
const gfm = options.gfm;
|
|
139
186
|
const math = options.math;
|
|
187
|
+
const html = options.html;
|
|
140
188
|
|
|
141
|
-
if (gfm === undefined && math === undefined) {
|
|
189
|
+
if (gfm === undefined && math === undefined && html === undefined) {
|
|
142
190
|
return undefined;
|
|
143
191
|
}
|
|
144
192
|
|
|
145
193
|
return {
|
|
146
194
|
gfm,
|
|
147
195
|
math,
|
|
196
|
+
html,
|
|
148
197
|
};
|
|
149
198
|
};
|
|
150
199
|
|
|
@@ -166,7 +215,7 @@ const getCachedParsedAst = (
|
|
|
166
215
|
return parseWithNativeParser(text, options);
|
|
167
216
|
}
|
|
168
217
|
|
|
169
|
-
const cacheKey = `${getParserOptionsKey(options)}|${text}`;
|
|
218
|
+
const cacheKey = `${getParserOptionsKey(options)}|${text.length}|${hashString(text)}`;
|
|
170
219
|
const cachedNode = parseAstCache.get(cacheKey);
|
|
171
220
|
if (cachedNode) {
|
|
172
221
|
parseAstCache.delete(cacheKey);
|
|
@@ -189,13 +238,15 @@ const getCachedParsedAst = (
|
|
|
189
238
|
const applyBeforeParsePlugins = (
|
|
190
239
|
markdown: string,
|
|
191
240
|
plugins?: MarkdownPlugin[],
|
|
192
|
-
onError?: (error: Error, phase:
|
|
241
|
+
onError?: (error: Error, phase: "before-plugin", pluginName?: string) => void,
|
|
193
242
|
): string => {
|
|
194
243
|
if (!plugins || plugins.length === 0) {
|
|
195
244
|
return markdown;
|
|
196
245
|
}
|
|
197
246
|
|
|
198
|
-
const sorted = [...plugins].sort(
|
|
247
|
+
const sorted = [...plugins].sort(
|
|
248
|
+
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
|
|
249
|
+
);
|
|
199
250
|
let nextMarkdown = markdown;
|
|
200
251
|
for (const plugin of sorted) {
|
|
201
252
|
if (!plugin.beforeParse) continue;
|
|
@@ -211,7 +262,7 @@ const applyBeforeParsePlugins = (
|
|
|
211
262
|
`[react-native-nitro-markdown] plugin beforeParse${pluginLabel} threw; using previous markdown.`,
|
|
212
263
|
error,
|
|
213
264
|
);
|
|
214
|
-
onError
|
|
265
|
+
safeOnError(onError, error, ERROR_PHASE.BEFORE_PLUGIN, plugin.name);
|
|
215
266
|
}
|
|
216
267
|
}
|
|
217
268
|
|
|
@@ -221,13 +272,15 @@ const applyBeforeParsePlugins = (
|
|
|
221
272
|
const applyAfterParsePlugins = (
|
|
222
273
|
ast: MarkdownNode,
|
|
223
274
|
plugins?: MarkdownPlugin[],
|
|
224
|
-
onError?: (error: Error, phase:
|
|
275
|
+
onError?: (error: Error, phase: "after-plugin", pluginName?: string) => void,
|
|
225
276
|
): MarkdownNode => {
|
|
226
277
|
if (!plugins || plugins.length === 0) {
|
|
227
278
|
return ast;
|
|
228
279
|
}
|
|
229
280
|
|
|
230
|
-
const sorted = [...plugins].sort(
|
|
281
|
+
const sorted = [...plugins].sort(
|
|
282
|
+
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
|
|
283
|
+
);
|
|
231
284
|
let nextAst = ast;
|
|
232
285
|
for (const plugin of sorted) {
|
|
233
286
|
if (!plugin.afterParse) continue;
|
|
@@ -243,7 +296,7 @@ const applyAfterParsePlugins = (
|
|
|
243
296
|
`[react-native-nitro-markdown] plugin afterParse${pluginLabel} threw; using previous AST.`,
|
|
244
297
|
error,
|
|
245
298
|
);
|
|
246
|
-
onError
|
|
299
|
+
safeOnError(onError, error, ERROR_PHASE.AFTER_PLUGIN, plugin.name);
|
|
247
300
|
}
|
|
248
301
|
}
|
|
249
302
|
|
|
@@ -256,7 +309,7 @@ export type MarkdownProps = {
|
|
|
256
309
|
*/
|
|
257
310
|
children: string;
|
|
258
311
|
/**
|
|
259
|
-
* Parser options to enable GFM or
|
|
312
|
+
* Parser options to enable GFM, math, or raw HTML AST support.
|
|
260
313
|
*/
|
|
261
314
|
options?: ParserOptions;
|
|
262
315
|
/**
|
|
@@ -274,7 +327,12 @@ export type MarkdownProps = {
|
|
|
274
327
|
*/
|
|
275
328
|
astTransform?: AstTransform;
|
|
276
329
|
/**
|
|
277
|
-
* Callback fired
|
|
330
|
+
* Callback fired after the current parse cycle completes and the component
|
|
331
|
+
* has re-rendered with new content. Because the native parser runs
|
|
332
|
+
* synchronously inside `useMemo`, there is no observable "in-progress"
|
|
333
|
+
* window — this callback fires in the `useEffect` commit phase, after the
|
|
334
|
+
* new AST is already rendered. Use `onParseComplete` for post-parse
|
|
335
|
+
* inspection of results.
|
|
278
336
|
*/
|
|
279
337
|
onParsingInProgress?: () => void;
|
|
280
338
|
/**
|
|
@@ -291,7 +349,11 @@ export type MarkdownProps = {
|
|
|
291
349
|
* @param phase - Where the error occurred.
|
|
292
350
|
* @param pluginName - The plugin name, if applicable.
|
|
293
351
|
*/
|
|
294
|
-
onError?: (
|
|
352
|
+
onError?: (
|
|
353
|
+
error: Error,
|
|
354
|
+
phase: "parse" | "before-plugin" | "after-plugin",
|
|
355
|
+
pluginName?: string,
|
|
356
|
+
) => void;
|
|
295
357
|
/**
|
|
296
358
|
* Custom renderers for specific markdown node types.
|
|
297
359
|
* Each renderer receives { node, children, Renderer } plus type-specific props.
|
|
@@ -364,7 +426,7 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
364
426
|
plugins,
|
|
365
427
|
sourceAst,
|
|
366
428
|
astTransform,
|
|
367
|
-
renderers =
|
|
429
|
+
renderers = EMPTY_RENDERERS,
|
|
368
430
|
theme: userTheme,
|
|
369
431
|
styles: nodeStyles,
|
|
370
432
|
stylingStrategy = "opinionated",
|
|
@@ -381,18 +443,32 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
381
443
|
}) => {
|
|
382
444
|
const parserOptionGfm = options?.gfm;
|
|
383
445
|
const parserOptionMath = options?.math;
|
|
446
|
+
const parserOptionHtml = options?.html;
|
|
447
|
+
|
|
448
|
+
/* eslint-disable react-hooks/refs -- Refs updated/read intentionally to avoid re-parsing on callback identity changes */
|
|
449
|
+
const onErrorRef = useRef(onError);
|
|
450
|
+
onErrorRef.current = onError;
|
|
384
451
|
|
|
385
452
|
const parseResult = useMemo(() => {
|
|
386
453
|
try {
|
|
387
|
-
const markdownToParse = applyBeforeParsePlugins(
|
|
454
|
+
const markdownToParse = applyBeforeParsePlugins(
|
|
455
|
+
children,
|
|
456
|
+
plugins,
|
|
457
|
+
onErrorRef.current,
|
|
458
|
+
);
|
|
388
459
|
const parserOptions = normalizeParserOptions({
|
|
389
460
|
gfm: parserOptionGfm,
|
|
390
461
|
math: parserOptionMath,
|
|
462
|
+
html: parserOptionHtml,
|
|
391
463
|
});
|
|
392
464
|
let parsedAst = sourceAst
|
|
393
465
|
? cloneMarkdownNode(sourceAst)
|
|
394
466
|
: getCachedParsedAst(markdownToParse, parserOptions);
|
|
395
|
-
parsedAst = applyAfterParsePlugins(
|
|
467
|
+
parsedAst = applyAfterParsePlugins(
|
|
468
|
+
parsedAst,
|
|
469
|
+
plugins,
|
|
470
|
+
onErrorRef.current,
|
|
471
|
+
);
|
|
396
472
|
|
|
397
473
|
let ast = parsedAst;
|
|
398
474
|
if (astTransform) {
|
|
@@ -414,7 +490,7 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
414
490
|
ast,
|
|
415
491
|
};
|
|
416
492
|
} catch (parseError) {
|
|
417
|
-
|
|
493
|
+
safeOnError(onErrorRef.current, parseError, ERROR_PHASE.PARSE);
|
|
418
494
|
return {
|
|
419
495
|
ast: null,
|
|
420
496
|
};
|
|
@@ -423,11 +499,12 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
423
499
|
children,
|
|
424
500
|
parserOptionGfm,
|
|
425
501
|
parserOptionMath,
|
|
426
|
-
|
|
502
|
+
parserOptionHtml,
|
|
427
503
|
sourceAst,
|
|
428
504
|
astTransform,
|
|
429
|
-
|
|
505
|
+
plugins,
|
|
430
506
|
]);
|
|
507
|
+
/* eslint-enable react-hooks/refs */
|
|
431
508
|
|
|
432
509
|
useEffect(() => {
|
|
433
510
|
onParsingInProgress?.();
|
|
@@ -435,7 +512,7 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
435
512
|
children,
|
|
436
513
|
parserOptionGfm,
|
|
437
514
|
parserOptionMath,
|
|
438
|
-
|
|
515
|
+
parserOptionHtml,
|
|
439
516
|
onParsingInProgress,
|
|
440
517
|
]);
|
|
441
518
|
|
|
@@ -468,7 +545,15 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
468
545
|
tableOptions,
|
|
469
546
|
highlightCode,
|
|
470
547
|
}),
|
|
471
|
-
[
|
|
548
|
+
[
|
|
549
|
+
renderers,
|
|
550
|
+
theme,
|
|
551
|
+
nodeStyles,
|
|
552
|
+
stylingStrategy,
|
|
553
|
+
onLinkPress,
|
|
554
|
+
tableOptions,
|
|
555
|
+
highlightCode,
|
|
556
|
+
],
|
|
472
557
|
);
|
|
473
558
|
|
|
474
559
|
const topLevelBlocks =
|
|
@@ -887,13 +972,13 @@ const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
|
|
|
887
972
|
const createBaseStyles = (theme: MarkdownTheme) =>
|
|
888
973
|
StyleSheet.create({
|
|
889
974
|
container: {
|
|
890
|
-
|
|
975
|
+
flexShrink: 1,
|
|
891
976
|
},
|
|
892
977
|
virtualizedList: {
|
|
893
978
|
flex: 1,
|
|
894
979
|
},
|
|
895
980
|
document: {
|
|
896
|
-
|
|
981
|
+
flexShrink: 1,
|
|
897
982
|
},
|
|
898
983
|
errorText: {
|
|
899
984
|
color: "#f87171",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FC, ReactNode } from "react";
|
|
2
2
|
import { View, StyleSheet, type ViewStyle } from "react-native";
|
|
3
|
+
import { getCachedStyles } from "./style-cache";
|
|
3
4
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
5
|
+
import type { MarkdownTheme } from "../theme";
|
|
4
6
|
|
|
5
7
|
type BlockquoteProps = {
|
|
6
8
|
children: ReactNode;
|
|
@@ -9,22 +11,25 @@ type BlockquoteProps = {
|
|
|
9
11
|
|
|
10
12
|
export const Blockquote: FC<BlockquoteProps> = ({ children, style }) => {
|
|
11
13
|
const { theme } = useMarkdownContext();
|
|
12
|
-
const styles =
|
|
13
|
-
() =>
|
|
14
|
-
StyleSheet.create({
|
|
15
|
-
blockquote: {
|
|
16
|
-
borderLeftWidth: 4,
|
|
17
|
-
borderLeftColor: theme.colors.blockquote,
|
|
18
|
-
paddingLeft: theme.spacing.l,
|
|
19
|
-
marginVertical: theme.spacing.m,
|
|
20
|
-
backgroundColor: theme.colors.surfaceLight,
|
|
21
|
-
paddingVertical: theme.spacing.m,
|
|
22
|
-
paddingRight: theme.spacing.m,
|
|
23
|
-
borderRadius: theme.borderRadius.s,
|
|
24
|
-
},
|
|
25
|
-
}),
|
|
26
|
-
[theme],
|
|
27
|
-
);
|
|
14
|
+
const styles = getCachedStyles(stylesCache, theme, createStyles);
|
|
28
15
|
|
|
29
16
|
return <View style={[styles.blockquote, style]}>{children}</View>;
|
|
30
17
|
};
|
|
18
|
+
|
|
19
|
+
type BlockquoteStyles = ReturnType<typeof createStyles>;
|
|
20
|
+
|
|
21
|
+
const stylesCache = new WeakMap<MarkdownTheme, BlockquoteStyles>();
|
|
22
|
+
|
|
23
|
+
const createStyles = (theme: MarkdownTheme) =>
|
|
24
|
+
StyleSheet.create({
|
|
25
|
+
blockquote: {
|
|
26
|
+
borderLeftWidth: 4,
|
|
27
|
+
borderLeftColor: theme.colors.blockquote,
|
|
28
|
+
paddingLeft: theme.spacing.l,
|
|
29
|
+
marginVertical: theme.spacing.m,
|
|
30
|
+
backgroundColor: theme.colors.surfaceLight,
|
|
31
|
+
paddingVertical: theme.spacing.m,
|
|
32
|
+
paddingRight: theme.spacing.m,
|
|
33
|
+
borderRadius: theme.borderRadius.s,
|
|
34
|
+
},
|
|
35
|
+
});
|
package/src/renderers/code.tsx
CHANGED
|
@@ -8,10 +8,15 @@ import {
|
|
|
8
8
|
type ViewStyle,
|
|
9
9
|
type TextStyle,
|
|
10
10
|
} from "react-native";
|
|
11
|
+
import { getCachedStyles } from "./style-cache";
|
|
11
12
|
import { getTextContent } from "../headless";
|
|
12
13
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
14
|
+
import {
|
|
15
|
+
defaultHighlighter,
|
|
16
|
+
type HighlightedToken,
|
|
17
|
+
} from "../utils/code-highlight";
|
|
13
18
|
import type { MarkdownNode } from "../headless";
|
|
14
|
-
import {
|
|
19
|
+
import type { MarkdownTheme } from "../theme";
|
|
15
20
|
|
|
16
21
|
type CodeBlockProps = {
|
|
17
22
|
language?: string;
|
|
@@ -29,48 +34,22 @@ export const CodeBlock: FC<CodeBlockProps> = ({
|
|
|
29
34
|
const ctx = useMarkdownContext();
|
|
30
35
|
const { theme } = ctx;
|
|
31
36
|
|
|
32
|
-
const highlighter =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const highlighter =
|
|
38
|
+
ctx.highlightCode === true
|
|
39
|
+
? defaultHighlighter
|
|
40
|
+
: typeof ctx.highlightCode === "function"
|
|
41
|
+
? ctx.highlightCode
|
|
42
|
+
: null;
|
|
37
43
|
|
|
38
44
|
const displayContent = content ?? (node ? getTextContent(node) : "");
|
|
39
|
-
|
|
40
|
-
const styles = useMemo(
|
|
45
|
+
const highlightedTokens = useMemo(
|
|
41
46
|
() =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
backgroundColor: theme.colors.codeBackground,
|
|
45
|
-
borderRadius: theme.borderRadius.m,
|
|
46
|
-
padding: theme.spacing.l,
|
|
47
|
-
marginVertical: theme.spacing.m,
|
|
48
|
-
borderWidth: 1,
|
|
49
|
-
borderColor: theme.colors.border,
|
|
50
|
-
},
|
|
51
|
-
codeLanguage: {
|
|
52
|
-
color: theme.colors.codeLanguage,
|
|
53
|
-
fontSize: theme.fontSizes.xs,
|
|
54
|
-
fontWeight: "600",
|
|
55
|
-
marginBottom: theme.spacing.s,
|
|
56
|
-
textTransform: "uppercase",
|
|
57
|
-
letterSpacing: 0.5,
|
|
58
|
-
fontFamily: theme.fontFamilies.mono,
|
|
59
|
-
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
60
|
-
},
|
|
61
|
-
codeBlockText: {
|
|
62
|
-
fontFamily:
|
|
63
|
-
theme.fontFamilies.mono ??
|
|
64
|
-
Platform.select({ ios: "Courier", android: "monospace" }),
|
|
65
|
-
fontSize: theme.fontSizes.s,
|
|
66
|
-
color: theme.colors.text,
|
|
67
|
-
lineHeight: theme.fontSizes.s * 1.5,
|
|
68
|
-
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
69
|
-
},
|
|
70
|
-
}),
|
|
71
|
-
[theme],
|
|
47
|
+
highlighter && language ? highlighter(language, displayContent) : null,
|
|
48
|
+
[displayContent, highlighter, language],
|
|
72
49
|
);
|
|
73
50
|
|
|
51
|
+
const styles = getCachedStyles(codeBlockStylesCache, theme, createCodeStyles);
|
|
52
|
+
|
|
74
53
|
const showLanguage = theme.showCodeLanguage && language;
|
|
75
54
|
|
|
76
55
|
return (
|
|
@@ -83,9 +62,9 @@ export const CodeBlock: FC<CodeBlockProps> = ({
|
|
|
83
62
|
showsHorizontalScrollIndicator={false}
|
|
84
63
|
bounces={false}
|
|
85
64
|
>
|
|
86
|
-
{
|
|
65
|
+
{highlightedTokens ? (
|
|
87
66
|
<Text style={styles.codeBlockText} selectable>
|
|
88
|
-
{
|
|
67
|
+
{highlightedTokens.map((token: HighlightedToken, i: number) => {
|
|
89
68
|
const tokenColor = ctx.theme.colors.codeTokenColors?.[token.type];
|
|
90
69
|
return tokenColor ? (
|
|
91
70
|
<Text key={i} style={{ color: tokenColor }}>
|
|
@@ -124,23 +103,63 @@ export const InlineCode: FC<InlineCodeProps> = ({
|
|
|
124
103
|
const displayContent =
|
|
125
104
|
content ?? children ?? (node ? getTextContent(node) : "");
|
|
126
105
|
|
|
127
|
-
const styles =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
fontFamily:
|
|
132
|
-
theme.fontFamilies.mono ??
|
|
133
|
-
Platform.select({ ios: "Courier", android: "monospace" }),
|
|
134
|
-
fontSize: theme.fontSizes.s,
|
|
135
|
-
color: theme.colors.code,
|
|
136
|
-
backgroundColor: theme.colors.codeBackground,
|
|
137
|
-
paddingHorizontal: theme.spacing.xs,
|
|
138
|
-
paddingVertical: 2,
|
|
139
|
-
borderRadius: theme.borderRadius.s,
|
|
140
|
-
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
141
|
-
},
|
|
142
|
-
}),
|
|
143
|
-
[theme],
|
|
106
|
+
const styles = getCachedStyles(
|
|
107
|
+
inlineCodeStylesCache,
|
|
108
|
+
theme,
|
|
109
|
+
createInlineStyles,
|
|
144
110
|
);
|
|
145
111
|
return <Text style={[styles.codeInline, style]}>{displayContent}</Text>;
|
|
146
112
|
};
|
|
113
|
+
|
|
114
|
+
type CodeBlockStyles = ReturnType<typeof createCodeStyles>;
|
|
115
|
+
type InlineCodeStyles = ReturnType<typeof createInlineStyles>;
|
|
116
|
+
|
|
117
|
+
const codeBlockStylesCache = new WeakMap<MarkdownTheme, CodeBlockStyles>();
|
|
118
|
+
const inlineCodeStylesCache = new WeakMap<MarkdownTheme, InlineCodeStyles>();
|
|
119
|
+
|
|
120
|
+
const getMonoFontFamily = (theme: MarkdownTheme) =>
|
|
121
|
+
theme.fontFamilies.mono ??
|
|
122
|
+
Platform.select({ ios: "Courier", android: "monospace" });
|
|
123
|
+
|
|
124
|
+
const createCodeStyles = (theme: MarkdownTheme) =>
|
|
125
|
+
StyleSheet.create({
|
|
126
|
+
codeBlock: {
|
|
127
|
+
backgroundColor: theme.colors.codeBackground,
|
|
128
|
+
borderRadius: theme.borderRadius.m,
|
|
129
|
+
padding: theme.spacing.l,
|
|
130
|
+
marginVertical: theme.spacing.m,
|
|
131
|
+
borderWidth: 1,
|
|
132
|
+
borderColor: theme.colors.border,
|
|
133
|
+
},
|
|
134
|
+
codeLanguage: {
|
|
135
|
+
color: theme.colors.codeLanguage,
|
|
136
|
+
fontSize: theme.fontSizes.xs,
|
|
137
|
+
fontWeight: "600",
|
|
138
|
+
marginBottom: theme.spacing.s,
|
|
139
|
+
textTransform: "uppercase",
|
|
140
|
+
letterSpacing: 0.5,
|
|
141
|
+
fontFamily: theme.fontFamilies.mono,
|
|
142
|
+
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
143
|
+
},
|
|
144
|
+
codeBlockText: {
|
|
145
|
+
fontFamily: getMonoFontFamily(theme),
|
|
146
|
+
fontSize: theme.fontSizes.s,
|
|
147
|
+
color: theme.colors.text,
|
|
148
|
+
lineHeight: theme.fontSizes.s * 1.5,
|
|
149
|
+
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const createInlineStyles = (theme: MarkdownTheme) =>
|
|
154
|
+
StyleSheet.create({
|
|
155
|
+
codeInline: {
|
|
156
|
+
fontFamily: getMonoFontFamily(theme),
|
|
157
|
+
fontSize: theme.fontSizes.s,
|
|
158
|
+
color: theme.colors.code,
|
|
159
|
+
backgroundColor: theme.colors.codeBackground,
|
|
160
|
+
paddingHorizontal: theme.spacing.xs,
|
|
161
|
+
paddingVertical: 2,
|
|
162
|
+
borderRadius: theme.borderRadius.s,
|
|
163
|
+
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FC, ReactNode } from "react";
|
|
2
2
|
import { Text, StyleSheet, Platform, type TextStyle } from "react-native";
|
|
3
|
+
import { getCachedStyles } from "./style-cache";
|
|
3
4
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
5
|
+
import type { MarkdownTheme } from "../theme";
|
|
4
6
|
|
|
5
7
|
type HeadingProps = {
|
|
6
8
|
level: number;
|
|
@@ -20,59 +22,7 @@ const ANDROID_SYSTEM_FONTS = new Set([
|
|
|
20
22
|
|
|
21
23
|
export const Heading: FC<HeadingProps> = ({ level, children, style }) => {
|
|
22
24
|
const { theme } = useMarkdownContext();
|
|
23
|
-
const
|
|
24
|
-
theme.headingWeight ??
|
|
25
|
-
(Platform.OS === "android" &&
|
|
26
|
-
theme.fontFamilies.heading &&
|
|
27
|
-
!ANDROID_SYSTEM_FONTS.has(theme.fontFamilies.heading)
|
|
28
|
-
? "normal"
|
|
29
|
-
: "700");
|
|
30
|
-
const styles = useMemo(
|
|
31
|
-
() =>
|
|
32
|
-
StyleSheet.create({
|
|
33
|
-
heading: {
|
|
34
|
-
color: theme.colors.heading,
|
|
35
|
-
fontWeight: headingWeight,
|
|
36
|
-
marginTop: theme.spacing.xl,
|
|
37
|
-
marginBottom: theme.spacing.m,
|
|
38
|
-
fontFamily: theme.fontFamilies.heading,
|
|
39
|
-
letterSpacing: -0.2,
|
|
40
|
-
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
41
|
-
},
|
|
42
|
-
h1: {
|
|
43
|
-
fontSize: theme.fontSizes.h1,
|
|
44
|
-
lineHeight: theme.fontSizes.h1 * 1.3,
|
|
45
|
-
borderBottomWidth: 1,
|
|
46
|
-
borderBottomColor: theme.colors.border,
|
|
47
|
-
paddingBottom: theme.spacing.s,
|
|
48
|
-
letterSpacing: -0.6,
|
|
49
|
-
},
|
|
50
|
-
h2: {
|
|
51
|
-
fontSize: theme.fontSizes.h2,
|
|
52
|
-
lineHeight: theme.fontSizes.h2 * 1.3,
|
|
53
|
-
letterSpacing: -0.4,
|
|
54
|
-
},
|
|
55
|
-
h3: {
|
|
56
|
-
fontSize: theme.fontSizes.h3,
|
|
57
|
-
lineHeight: theme.fontSizes.h3 * 1.3,
|
|
58
|
-
letterSpacing: -0.2,
|
|
59
|
-
},
|
|
60
|
-
h4: {
|
|
61
|
-
fontSize: theme.fontSizes.h4,
|
|
62
|
-
lineHeight: theme.fontSizes.h4 * 1.3,
|
|
63
|
-
},
|
|
64
|
-
h5: {
|
|
65
|
-
fontSize: theme.fontSizes.h5,
|
|
66
|
-
lineHeight: theme.fontSizes.h5 * 1.3,
|
|
67
|
-
},
|
|
68
|
-
h6: {
|
|
69
|
-
fontSize: theme.fontSizes.h6,
|
|
70
|
-
lineHeight: theme.fontSizes.h6 * 1.3,
|
|
71
|
-
color: theme.colors.textMuted,
|
|
72
|
-
},
|
|
73
|
-
}),
|
|
74
|
-
[headingWeight, theme],
|
|
75
|
-
);
|
|
25
|
+
const styles = getCachedStyles(stylesCache, theme, createStyles);
|
|
76
26
|
|
|
77
27
|
const headingStyles = [
|
|
78
28
|
styles.heading,
|
|
@@ -86,3 +36,59 @@ export const Heading: FC<HeadingProps> = ({ level, children, style }) => {
|
|
|
86
36
|
|
|
87
37
|
return <Text style={[...headingStyles, style]}>{children}</Text>;
|
|
88
38
|
};
|
|
39
|
+
|
|
40
|
+
type HeadingStyles = ReturnType<typeof createStyles>;
|
|
41
|
+
|
|
42
|
+
const stylesCache = new WeakMap<MarkdownTheme, HeadingStyles>();
|
|
43
|
+
|
|
44
|
+
const getHeadingWeight = (theme: MarkdownTheme) =>
|
|
45
|
+
theme.headingWeight ??
|
|
46
|
+
(Platform.OS === "android" &&
|
|
47
|
+
theme.fontFamilies.heading &&
|
|
48
|
+
!ANDROID_SYSTEM_FONTS.has(theme.fontFamilies.heading)
|
|
49
|
+
? "normal"
|
|
50
|
+
: "700");
|
|
51
|
+
|
|
52
|
+
const createStyles = (theme: MarkdownTheme) =>
|
|
53
|
+
StyleSheet.create({
|
|
54
|
+
heading: {
|
|
55
|
+
color: theme.colors.heading,
|
|
56
|
+
fontWeight: getHeadingWeight(theme),
|
|
57
|
+
marginTop: theme.spacing.xl,
|
|
58
|
+
marginBottom: theme.spacing.m,
|
|
59
|
+
fontFamily: theme.fontFamilies.heading,
|
|
60
|
+
letterSpacing: -0.2,
|
|
61
|
+
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
62
|
+
},
|
|
63
|
+
h1: {
|
|
64
|
+
fontSize: theme.fontSizes.h1,
|
|
65
|
+
lineHeight: theme.fontSizes.h1 * 1.3,
|
|
66
|
+
borderBottomWidth: 1,
|
|
67
|
+
borderBottomColor: theme.colors.border,
|
|
68
|
+
paddingBottom: theme.spacing.s,
|
|
69
|
+
letterSpacing: -0.6,
|
|
70
|
+
},
|
|
71
|
+
h2: {
|
|
72
|
+
fontSize: theme.fontSizes.h2,
|
|
73
|
+
lineHeight: theme.fontSizes.h2 * 1.3,
|
|
74
|
+
letterSpacing: -0.4,
|
|
75
|
+
},
|
|
76
|
+
h3: {
|
|
77
|
+
fontSize: theme.fontSizes.h3,
|
|
78
|
+
lineHeight: theme.fontSizes.h3 * 1.3,
|
|
79
|
+
letterSpacing: -0.2,
|
|
80
|
+
},
|
|
81
|
+
h4: {
|
|
82
|
+
fontSize: theme.fontSizes.h4,
|
|
83
|
+
lineHeight: theme.fontSizes.h4 * 1.3,
|
|
84
|
+
},
|
|
85
|
+
h5: {
|
|
86
|
+
fontSize: theme.fontSizes.h5,
|
|
87
|
+
lineHeight: theme.fontSizes.h5 * 1.3,
|
|
88
|
+
},
|
|
89
|
+
h6: {
|
|
90
|
+
fontSize: theme.fontSizes.h6,
|
|
91
|
+
lineHeight: theme.fontSizes.h6 * 1.3,
|
|
92
|
+
color: theme.colors.textMuted,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FC } from "react";
|
|
2
2
|
import { View, StyleSheet, type ViewStyle } from "react-native";
|
|
3
|
+
import { getCachedStyles } from "./style-cache";
|
|
3
4
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
5
|
+
import type { MarkdownTheme } from "../theme";
|
|
4
6
|
|
|
5
7
|
type HorizontalRuleProps = {
|
|
6
8
|
style?: ViewStyle;
|
|
@@ -8,16 +10,19 @@ type HorizontalRuleProps = {
|
|
|
8
10
|
|
|
9
11
|
export const HorizontalRule: FC<HorizontalRuleProps> = ({ style }) => {
|
|
10
12
|
const { theme } = useMarkdownContext();
|
|
11
|
-
const styles =
|
|
12
|
-
() =>
|
|
13
|
-
StyleSheet.create({
|
|
14
|
-
horizontalRule: {
|
|
15
|
-
height: 1,
|
|
16
|
-
backgroundColor: theme.colors.border,
|
|
17
|
-
marginVertical: theme.spacing.xl,
|
|
18
|
-
},
|
|
19
|
-
}),
|
|
20
|
-
[theme],
|
|
21
|
-
);
|
|
13
|
+
const styles = getCachedStyles(stylesCache, theme, createStyles);
|
|
22
14
|
return <View style={[styles.horizontalRule, style]} />;
|
|
23
15
|
};
|
|
16
|
+
|
|
17
|
+
type HorizontalRuleStyles = ReturnType<typeof createStyles>;
|
|
18
|
+
|
|
19
|
+
const stylesCache = new WeakMap<MarkdownTheme, HorizontalRuleStyles>();
|
|
20
|
+
|
|
21
|
+
const createStyles = (theme: MarkdownTheme) =>
|
|
22
|
+
StyleSheet.create({
|
|
23
|
+
horizontalRule: {
|
|
24
|
+
height: 1,
|
|
25
|
+
backgroundColor: theme.colors.border,
|
|
26
|
+
marginVertical: theme.spacing.xl,
|
|
27
|
+
},
|
|
28
|
+
});
|