react-native-nitro-markdown 0.4.1 → 0.4.3
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 +329 -322
- package/lib/commonjs/MarkdownContext.js +2 -1
- package/lib/commonjs/MarkdownContext.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +3 -1
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +51 -35
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/code.js +3 -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 +32 -15
- 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/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/index.js.map +1 -1
- package/lib/module/markdown-stream.js +3 -1
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +52 -36
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/blockquote.js.map +1 -1
- package/lib/module/renderers/code.js +3 -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 +33 -16
- 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/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 +3 -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 +2 -2
- package/lib/typescript/commonjs/headless.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +2 -2
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts +9 -4
- 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/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/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 +3 -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 +2 -2
- package/lib/typescript/module/headless.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +2 -2
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts +9 -4
- 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/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/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/ios/swift/Func_void.swift +0 -1
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +0 -1
- package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +0 -1
- package/package.json +4 -3
- package/src/Markdown.nitro.ts +5 -3
- package/src/MarkdownContext.ts +31 -25
- package/src/headless.ts +2 -2
- package/src/index.ts +1 -0
- package/src/markdown-stream.tsx +6 -10
- package/src/markdown.tsx +86 -45
- package/src/renderers/blockquote.tsx +4 -4
- package/src/renderers/code.tsx +11 -9
- 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 +74 -46
- package/src/theme.ts +3 -3
- package/src/use-markdown-stream.ts +22 -16
- package/src/utils/link-security.ts +22 -0
- package/src/utils/stream-timeline.ts +72 -0
package/src/MarkdownContext.ts
CHANGED
|
@@ -4,28 +4,28 @@ import {
|
|
|
4
4
|
type ReactNode,
|
|
5
5
|
type ComponentType,
|
|
6
6
|
} from "react";
|
|
7
|
+
import type { MarkdownNode } from "./headless";
|
|
7
8
|
import {
|
|
8
9
|
defaultMarkdownTheme,
|
|
9
10
|
type MarkdownTheme,
|
|
10
11
|
type NodeStyleOverrides,
|
|
11
12
|
type StylingStrategy,
|
|
12
13
|
} from "./theme";
|
|
13
|
-
import type { MarkdownNode } from "./headless";
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export type NodeRendererProps = {
|
|
16
16
|
node: MarkdownNode;
|
|
17
17
|
depth: number;
|
|
18
18
|
inListItem: boolean;
|
|
19
19
|
parentIsText?: boolean;
|
|
20
|
-
}
|
|
20
|
+
};
|
|
21
21
|
|
|
22
|
-
export
|
|
22
|
+
export type BaseCustomRendererProps = {
|
|
23
23
|
node: MarkdownNode;
|
|
24
24
|
children: ReactNode;
|
|
25
25
|
Renderer: ComponentType<NodeRendererProps>;
|
|
26
|
-
}
|
|
26
|
+
};
|
|
27
27
|
|
|
28
|
-
export
|
|
28
|
+
export type EnhancedRendererProps = {
|
|
29
29
|
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
30
30
|
href?: string;
|
|
31
31
|
title?: string;
|
|
@@ -36,63 +36,69 @@ export interface EnhancedRendererProps extends BaseCustomRendererProps {
|
|
|
36
36
|
ordered?: boolean;
|
|
37
37
|
start?: number;
|
|
38
38
|
checked?: boolean;
|
|
39
|
-
}
|
|
39
|
+
} & BaseCustomRendererProps;
|
|
40
40
|
|
|
41
|
-
export
|
|
41
|
+
export type HeadingRendererProps = {
|
|
42
42
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
43
|
-
}
|
|
43
|
+
} & BaseCustomRendererProps;
|
|
44
44
|
|
|
45
|
-
export
|
|
45
|
+
export type LinkRendererProps = {
|
|
46
46
|
href: string;
|
|
47
47
|
title?: string;
|
|
48
|
-
}
|
|
48
|
+
} & BaseCustomRendererProps;
|
|
49
49
|
|
|
50
|
-
export
|
|
50
|
+
export type ImageRendererProps = {
|
|
51
51
|
url: string;
|
|
52
52
|
alt?: string;
|
|
53
53
|
title?: string;
|
|
54
|
-
}
|
|
54
|
+
} & BaseCustomRendererProps;
|
|
55
55
|
|
|
56
|
-
export
|
|
56
|
+
export type CodeBlockRendererProps = {
|
|
57
57
|
content: string;
|
|
58
58
|
language?: string;
|
|
59
|
-
}
|
|
59
|
+
} & BaseCustomRendererProps;
|
|
60
60
|
|
|
61
|
-
export
|
|
61
|
+
export type InlineCodeRendererProps = {
|
|
62
62
|
content: string;
|
|
63
|
-
}
|
|
63
|
+
} & BaseCustomRendererProps;
|
|
64
64
|
|
|
65
|
-
export
|
|
65
|
+
export type ListRendererProps = {
|
|
66
66
|
ordered: boolean;
|
|
67
67
|
start?: number;
|
|
68
|
-
}
|
|
68
|
+
} & BaseCustomRendererProps;
|
|
69
69
|
|
|
70
|
-
export
|
|
70
|
+
export type TaskListItemRendererProps = {
|
|
71
71
|
checked: boolean;
|
|
72
|
-
}
|
|
72
|
+
} & BaseCustomRendererProps;
|
|
73
|
+
|
|
74
|
+
export type CustomRendererProps = {} & EnhancedRendererProps;
|
|
73
75
|
|
|
74
|
-
export
|
|
76
|
+
export type LinkPressHandler = (
|
|
77
|
+
href: string,
|
|
78
|
+
) => void | boolean | Promise<void | boolean>;
|
|
75
79
|
|
|
76
80
|
export type CustomRenderer = (
|
|
77
|
-
props: EnhancedRendererProps
|
|
81
|
+
props: EnhancedRendererProps,
|
|
78
82
|
) => ReactNode | undefined;
|
|
79
83
|
|
|
80
84
|
export type CustomRenderers = Partial<
|
|
81
85
|
Record<MarkdownNode["type"], CustomRenderer>
|
|
82
86
|
>;
|
|
83
87
|
|
|
84
|
-
export
|
|
88
|
+
export type MarkdownContextValue = {
|
|
85
89
|
renderers: CustomRenderers;
|
|
86
90
|
theme: MarkdownTheme;
|
|
87
91
|
styles?: NodeStyleOverrides;
|
|
88
92
|
stylingStrategy: StylingStrategy;
|
|
89
|
-
|
|
93
|
+
onLinkPress?: LinkPressHandler;
|
|
94
|
+
};
|
|
90
95
|
|
|
91
96
|
export const MarkdownContext = createContext<MarkdownContextValue>({
|
|
92
97
|
renderers: {},
|
|
93
98
|
theme: defaultMarkdownTheme,
|
|
94
99
|
styles: undefined,
|
|
95
100
|
stylingStrategy: "opinionated",
|
|
101
|
+
onLinkPress: undefined,
|
|
96
102
|
});
|
|
97
103
|
|
|
98
104
|
export const useMarkdownContext = () => useContext(MarkdownContext);
|
package/src/headless.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type { ParserOptions } from "./Markdown.nitro";
|
|
|
19
19
|
* Represents a node in the Markdown AST (Abstract Syntax Tree).
|
|
20
20
|
* Each node has a type and optional properties depending on the node type.
|
|
21
21
|
*/
|
|
22
|
-
export
|
|
22
|
+
export type MarkdownNode = {
|
|
23
23
|
/** The type of markdown element this node represents. Used to decide how to render the node. */
|
|
24
24
|
type:
|
|
25
25
|
| "document"
|
|
@@ -73,7 +73,7 @@ export interface MarkdownNode {
|
|
|
73
73
|
align?: string;
|
|
74
74
|
/** Nested child nodes for hierarchical elements like paragraphs, lists, and tables. */
|
|
75
75
|
children?: MarkdownNode[];
|
|
76
|
-
}
|
|
76
|
+
};
|
|
77
77
|
|
|
78
78
|
export const MarkdownParserModule =
|
|
79
79
|
NitroModules.createHybridObject<MarkdownParser>("MarkdownParser");
|
package/src/index.ts
CHANGED
package/src/markdown-stream.tsx
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useState,
|
|
3
|
-
useEffect,
|
|
4
|
-
useRef,
|
|
5
|
-
startTransition,
|
|
6
|
-
type FC,
|
|
7
|
-
} from "react";
|
|
1
|
+
import { useState, useEffect, useRef, startTransition, type FC } from "react";
|
|
8
2
|
import { Markdown, type MarkdownProps } from "./markdown";
|
|
9
3
|
import type { MarkdownSession } from "./specs/MarkdownSession.nitro";
|
|
10
4
|
|
|
11
|
-
export
|
|
5
|
+
export type MarkdownStreamProps = {
|
|
12
6
|
/**
|
|
13
7
|
* The active MarkdownSession to stream content from.
|
|
14
8
|
*/
|
|
@@ -29,7 +23,7 @@ export interface MarkdownStreamProps extends Omit<MarkdownProps, "children"> {
|
|
|
29
23
|
* Useful when you want to prioritize user interactions over stream renders.
|
|
30
24
|
*/
|
|
31
25
|
useTransitionUpdates?: boolean;
|
|
32
|
-
}
|
|
26
|
+
} & Omit<MarkdownProps, "children">;
|
|
33
27
|
|
|
34
28
|
/**
|
|
35
29
|
* A component that renders streaming Markdown from a MarkdownSession.
|
|
@@ -68,7 +62,9 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
|
|
|
68
62
|
lastEmittedRef.current = latest;
|
|
69
63
|
|
|
70
64
|
if (useTransitionUpdates) {
|
|
71
|
-
startTransition(() =>
|
|
65
|
+
startTransition(() => {
|
|
66
|
+
setText(latest);
|
|
67
|
+
});
|
|
72
68
|
} else {
|
|
73
69
|
setText(latest);
|
|
74
70
|
}
|
package/src/markdown.tsx
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
} from "./theme";
|
|
10
|
-
import { useMemo, type ReactNode, type FC, Fragment } from "react";
|
|
2
|
+
useEffect,
|
|
3
|
+
useMemo,
|
|
4
|
+
type FC,
|
|
5
|
+
Fragment,
|
|
6
|
+
type ReactElement,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
11
9
|
import {
|
|
12
10
|
StyleSheet,
|
|
13
11
|
View,
|
|
@@ -28,21 +26,32 @@ import {
|
|
|
28
26
|
MarkdownContext,
|
|
29
27
|
useMarkdownContext,
|
|
30
28
|
type CustomRenderers,
|
|
29
|
+
type LinkPressHandler,
|
|
31
30
|
type NodeRendererProps,
|
|
32
31
|
} from "./MarkdownContext";
|
|
33
|
-
|
|
34
|
-
import { Heading } from "./renderers/heading";
|
|
35
|
-
import { Paragraph } from "./renderers/paragraph";
|
|
36
|
-
import { Link } from "./renderers/link";
|
|
37
32
|
import { Blockquote } from "./renderers/blockquote";
|
|
38
|
-
import { HorizontalRule } from "./renderers/horizontal-rule";
|
|
39
33
|
import { CodeBlock, InlineCode } from "./renderers/code";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
34
|
+
import { Heading } from "./renderers/heading";
|
|
35
|
+
import { HorizontalRule } from "./renderers/horizontal-rule";
|
|
42
36
|
import { Image } from "./renderers/image";
|
|
37
|
+
import { Link } from "./renderers/link";
|
|
38
|
+
import { List, ListItem, TaskListItem } from "./renderers/list";
|
|
43
39
|
import { MathInline, MathBlock } from "./renderers/math";
|
|
40
|
+
import { Paragraph } from "./renderers/paragraph";
|
|
41
|
+
import { TableRenderer } from "./renderers/table";
|
|
42
|
+
import {
|
|
43
|
+
defaultMarkdownTheme,
|
|
44
|
+
minimalMarkdownTheme,
|
|
45
|
+
mergeThemes,
|
|
46
|
+
type MarkdownTheme,
|
|
47
|
+
type PartialMarkdownTheme,
|
|
48
|
+
type NodeStyleOverrides,
|
|
49
|
+
type StylingStrategy,
|
|
50
|
+
} from "./theme";
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
const baseStylesCache = new WeakMap<MarkdownTheme, BaseStyles>();
|
|
53
|
+
|
|
54
|
+
export type MarkdownProps = {
|
|
46
55
|
/**
|
|
47
56
|
* The markdown string to parse and render.
|
|
48
57
|
*/
|
|
@@ -93,7 +102,12 @@ export interface MarkdownProps {
|
|
|
93
102
|
* Optional style for the container view.
|
|
94
103
|
*/
|
|
95
104
|
style?: StyleProp<ViewStyle>;
|
|
96
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Optional link press handler.
|
|
107
|
+
* Return false to prevent the default openURL behavior.
|
|
108
|
+
*/
|
|
109
|
+
onLinkPress?: LinkPressHandler;
|
|
110
|
+
};
|
|
97
111
|
|
|
98
112
|
export const Markdown: FC<MarkdownProps> = ({
|
|
99
113
|
children,
|
|
@@ -105,34 +119,42 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
105
119
|
style,
|
|
106
120
|
onParsingInProgress,
|
|
107
121
|
onParseComplete,
|
|
122
|
+
onLinkPress,
|
|
108
123
|
}) => {
|
|
109
|
-
const
|
|
124
|
+
const parseResult = useMemo(() => {
|
|
110
125
|
try {
|
|
111
|
-
|
|
112
|
-
onParsingInProgress();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let result: MarkdownNode;
|
|
126
|
+
let ast: MarkdownNode;
|
|
116
127
|
if (options) {
|
|
117
|
-
|
|
128
|
+
ast = parseMarkdownWithOptions(children, options);
|
|
118
129
|
} else {
|
|
119
|
-
|
|
130
|
+
ast = parseMarkdown(children);
|
|
120
131
|
}
|
|
121
132
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error("Failed to parse markdown:", error);
|
|
133
|
-
return null;
|
|
133
|
+
return {
|
|
134
|
+
ast,
|
|
135
|
+
text: getFlattenedText(ast),
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
return {
|
|
139
|
+
ast: null,
|
|
140
|
+
text: "",
|
|
141
|
+
};
|
|
134
142
|
}
|
|
135
|
-
}, [children, options
|
|
143
|
+
}, [children, options]);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
onParsingInProgress?.();
|
|
147
|
+
}, [children, options, onParsingInProgress]);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!parseResult.ast) return;
|
|
151
|
+
|
|
152
|
+
onParseComplete?.({
|
|
153
|
+
raw: children,
|
|
154
|
+
ast: parseResult.ast,
|
|
155
|
+
text: parseResult.text,
|
|
156
|
+
});
|
|
157
|
+
}, [children, onParseComplete, parseResult.ast, parseResult.text]);
|
|
136
158
|
|
|
137
159
|
const theme = useMemo(() => {
|
|
138
160
|
const base =
|
|
@@ -142,9 +164,9 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
142
164
|
return mergeThemes(base, userTheme);
|
|
143
165
|
}, [userTheme, stylingStrategy]);
|
|
144
166
|
|
|
145
|
-
const baseStyles =
|
|
167
|
+
const baseStyles = getBaseStyles(theme);
|
|
146
168
|
|
|
147
|
-
if (!ast) {
|
|
169
|
+
if (!parseResult.ast) {
|
|
148
170
|
return (
|
|
149
171
|
<View style={[baseStyles.container, style]}>
|
|
150
172
|
<Text style={baseStyles.errorText}>Error parsing markdown</Text>
|
|
@@ -154,10 +176,16 @@ export const Markdown: FC<MarkdownProps> = ({
|
|
|
154
176
|
|
|
155
177
|
return (
|
|
156
178
|
<MarkdownContext.Provider
|
|
157
|
-
value={{
|
|
179
|
+
value={{
|
|
180
|
+
renderers,
|
|
181
|
+
theme,
|
|
182
|
+
styles: nodeStyles,
|
|
183
|
+
stylingStrategy,
|
|
184
|
+
onLinkPress,
|
|
185
|
+
}}
|
|
158
186
|
>
|
|
159
187
|
<View style={[baseStyles.container, style]}>
|
|
160
|
-
<NodeRenderer node={ast} depth={0} inListItem={false} />
|
|
188
|
+
<NodeRenderer node={parseResult.ast} depth={0} inListItem={false} />
|
|
161
189
|
</View>
|
|
162
190
|
</MarkdownContext.Provider>
|
|
163
191
|
);
|
|
@@ -185,7 +213,7 @@ const NodeRenderer: FC<NodeRendererProps> = ({
|
|
|
185
213
|
parentIsText = false,
|
|
186
214
|
}) => {
|
|
187
215
|
const { renderers, theme, styles: nodeStyles } = useMarkdownContext();
|
|
188
|
-
const baseStyles =
|
|
216
|
+
const baseStyles = getBaseStyles(theme);
|
|
189
217
|
|
|
190
218
|
const renderChildren = (
|
|
191
219
|
children?: MarkdownNode[],
|
|
@@ -308,7 +336,9 @@ const NodeRenderer: FC<NodeRendererProps> = ({
|
|
|
308
336
|
};
|
|
309
337
|
|
|
310
338
|
const result = customRenderer(enhancedProps);
|
|
311
|
-
if (result !== undefined)
|
|
339
|
+
if (result !== undefined) {
|
|
340
|
+
return result as ReactElement | null;
|
|
341
|
+
}
|
|
312
342
|
}
|
|
313
343
|
|
|
314
344
|
const nodeStyleOverride = nodeStyles?.[node.type];
|
|
@@ -491,6 +521,17 @@ const NodeRenderer: FC<NodeRendererProps> = ({
|
|
|
491
521
|
}
|
|
492
522
|
};
|
|
493
523
|
|
|
524
|
+
type BaseStyles = ReturnType<typeof createBaseStyles>;
|
|
525
|
+
|
|
526
|
+
const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
|
|
527
|
+
const cached = baseStylesCache.get(theme);
|
|
528
|
+
if (cached) return cached;
|
|
529
|
+
|
|
530
|
+
const created = createBaseStyles(theme);
|
|
531
|
+
baseStylesCache.set(theme, created);
|
|
532
|
+
return created;
|
|
533
|
+
};
|
|
534
|
+
|
|
494
535
|
const createBaseStyles = (theme: MarkdownTheme) =>
|
|
495
536
|
StyleSheet.create({
|
|
496
537
|
container: {
|
|
@@ -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,14 +60,16 @@ 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
|
|
70
|
+
{showLanguage ? (
|
|
71
|
+
<Text style={styles.codeLanguage}>{language}</Text>
|
|
72
|
+
) : null}
|
|
71
73
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
72
74
|
<Text style={styles.codeBlockText}>{displayContent}</Text>
|
|
73
75
|
</ScrollView>
|
|
@@ -75,12 +77,12 @@ export const CodeBlock: FC<CodeBlockProps> = ({
|
|
|
75
77
|
);
|
|
76
78
|
};
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
type InlineCodeProps = {
|
|
79
81
|
content?: string;
|
|
80
82
|
node?: MarkdownNode;
|
|
81
83
|
children?: ReactNode;
|
|
82
84
|
style?: TextStyle;
|
|
83
|
-
}
|
|
85
|
+
};
|
|
84
86
|
|
|
85
87
|
export const InlineCode: FC<InlineCodeProps> = ({
|
|
86
88
|
content,
|
|
@@ -109,7 +111,7 @@ export const InlineCode: FC<InlineCodeProps> = ({
|
|
|
109
111
|
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
110
112
|
},
|
|
111
113
|
}),
|
|
112
|
-
[theme]
|
|
114
|
+
[theme],
|
|
113
115
|
);
|
|
114
116
|
return <Text style={[styles.codeInline, style]}>{displayContent}</Text>;
|
|
115
117
|
};
|
|
@@ -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
|
};
|
package/src/renderers/image.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useState,
|
|
3
|
-
useEffect,
|
|
4
3
|
useLayoutEffect,
|
|
5
4
|
useMemo,
|
|
6
5
|
type ReactNode,
|
|
@@ -15,10 +14,9 @@ import {
|
|
|
15
14
|
Platform,
|
|
16
15
|
type ViewStyle,
|
|
17
16
|
} from "react-native";
|
|
18
|
-
|
|
19
17
|
import { parseMarkdownWithOptions, type MarkdownNode } from "../headless";
|
|
20
|
-
import type { NodeRendererProps } from "../MarkdownContext";
|
|
21
18
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
19
|
+
import type { NodeRendererProps } from "../MarkdownContext";
|
|
22
20
|
|
|
23
21
|
const renderInlineContent = (
|
|
24
22
|
node: MarkdownNode,
|
|
@@ -36,13 +34,13 @@ const renderInlineContent = (
|
|
|
36
34
|
return null;
|
|
37
35
|
};
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
type ImageProps = {
|
|
40
38
|
url: string;
|
|
41
39
|
title?: string;
|
|
42
40
|
alt?: string;
|
|
43
41
|
Renderer?: ComponentType<NodeRendererProps>;
|
|
44
42
|
style?: ViewStyle;
|
|
45
|
-
}
|
|
43
|
+
};
|
|
46
44
|
|
|
47
45
|
export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
|
|
48
46
|
const [loading, setLoading] = useState(true);
|
|
@@ -187,22 +185,26 @@ export const Image: FC<ImageProps> = ({ url, title, alt, Renderer, style }) => {
|
|
|
187
185
|
|
|
188
186
|
return (
|
|
189
187
|
<View style={[styles.imageContainer, style]}>
|
|
190
|
-
{loading && !aspectRatio
|
|
188
|
+
{loading && !aspectRatio ? (
|
|
191
189
|
<View style={styles.imageLoading}>
|
|
192
190
|
<Text style={styles.imageLoadingText}>Loading image...</Text>
|
|
193
191
|
</View>
|
|
194
|
-
)}
|
|
192
|
+
) : null}
|
|
195
193
|
<RNImage
|
|
196
194
|
source={{ uri: url }}
|
|
197
195
|
style={[styles.image, loading && !aspectRatio && styles.imageHidden]}
|
|
198
196
|
resizeMode="contain"
|
|
199
|
-
onLoad={() =>
|
|
197
|
+
onLoad={() => {
|
|
198
|
+
setLoading(false);
|
|
199
|
+
}}
|
|
200
200
|
onError={() => {
|
|
201
201
|
setLoading(false);
|
|
202
202
|
setError(true);
|
|
203
203
|
}}
|
|
204
204
|
/>
|
|
205
|
-
{title && !loading
|
|
205
|
+
{title && !loading ? (
|
|
206
|
+
<Text style={styles.imageCaption}>{title}</Text>
|
|
207
|
+
) : null}
|
|
206
208
|
</View>
|
|
207
209
|
);
|
|
208
210
|
};
|
package/src/renderers/link.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, type FC, type ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Text,
|
|
4
4
|
StyleSheet,
|
|
@@ -7,15 +7,19 @@ import {
|
|
|
7
7
|
type TextStyle,
|
|
8
8
|
} from "react-native";
|
|
9
9
|
import { useMarkdownContext } from "../MarkdownContext";
|
|
10
|
+
import {
|
|
11
|
+
getAllowedExternalHref,
|
|
12
|
+
normalizeLinkHref,
|
|
13
|
+
} from "../utils/link-security";
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
type LinkProps = {
|
|
12
16
|
href: string;
|
|
13
17
|
children: ReactNode;
|
|
14
18
|
style?: TextStyle;
|
|
15
|
-
}
|
|
19
|
+
};
|
|
16
20
|
|
|
17
21
|
export const Link: FC<LinkProps> = ({ href, children, style }) => {
|
|
18
|
-
const { theme } = useMarkdownContext();
|
|
22
|
+
const { theme, onLinkPress } = useMarkdownContext();
|
|
19
23
|
const styles = useMemo(
|
|
20
24
|
() =>
|
|
21
25
|
StyleSheet.create({
|
|
@@ -27,11 +31,25 @@ export const Link: FC<LinkProps> = ({ href, children, style }) => {
|
|
|
27
31
|
...(Platform.OS === "android" && { includeFontPadding: false }),
|
|
28
32
|
},
|
|
29
33
|
}),
|
|
30
|
-
[theme]
|
|
34
|
+
[theme],
|
|
31
35
|
);
|
|
32
36
|
|
|
33
|
-
const handlePress = () => {
|
|
34
|
-
|
|
37
|
+
const handlePress = async () => {
|
|
38
|
+
const normalizedHref = normalizeLinkHref(href);
|
|
39
|
+
if (!normalizedHref) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const shouldOpen = (await onLinkPress?.(normalizedHref)) !== false;
|
|
43
|
+
if (!shouldOpen) return;
|
|
44
|
+
|
|
45
|
+
const allowedExternalHref = getAllowedExternalHref(normalizedHref);
|
|
46
|
+
if (!allowedExternalHref) return;
|
|
47
|
+
|
|
48
|
+
const canOpen = await Linking.canOpenURL(allowedExternalHref);
|
|
49
|
+
if (!canOpen) return;
|
|
50
|
+
|
|
51
|
+
await Linking.openURL(allowedExternalHref);
|
|
52
|
+
} catch {}
|
|
35
53
|
};
|
|
36
54
|
|
|
37
55
|
return (
|