@idealyst/markdown 1.2.28
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/package.json +81 -0
- package/src/Markdown/Markdown.native.tsx +92 -0
- package/src/Markdown/Markdown.styles.tsx +302 -0
- package/src/Markdown/Markdown.web.tsx +102 -0
- package/src/Markdown/index.native.ts +3 -0
- package/src/Markdown/index.ts +6 -0
- package/src/Markdown/types.ts +198 -0
- package/src/index.native.ts +26 -0
- package/src/index.ts +41 -0
- package/src/renderers/native/createNativeStyles.ts +224 -0
- package/src/renderers/web/index.tsx +230 -0
- package/src/types/react-native-markdown-display.d.ts +64 -0
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idealyst/markdown",
|
|
3
|
+
"version": "1.2.28",
|
|
4
|
+
"description": "Cross-platform markdown renderer for React and React Native with theme integration",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"module": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"react-native": "src/index.native.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/IdealystIO/idealyst-framework.git",
|
|
12
|
+
"directory": "packages/markdown"
|
|
13
|
+
},
|
|
14
|
+
"author": "Idealyst",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"react-native": "./src/index.native.ts",
|
|
22
|
+
"import": "./src/index.ts",
|
|
23
|
+
"require": "./src/index.ts",
|
|
24
|
+
"types": "./src/index.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prepublishOnly": "echo 'Publishing TypeScript source directly'",
|
|
29
|
+
"publish:npm": "npm publish"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@idealyst/theme": "^1.2.28",
|
|
33
|
+
"react": ">=16.8.0",
|
|
34
|
+
"react-markdown": ">=9.0.0",
|
|
35
|
+
"react-native": ">=0.60.0",
|
|
36
|
+
"react-native-markdown-display": ">=7.0.0",
|
|
37
|
+
"react-native-unistyles": ">=3.0.0",
|
|
38
|
+
"remark-gfm": ">=4.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"@idealyst/theme": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"react-markdown": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"react-native": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"react-native-markdown-display": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"react-native-unistyles": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"remark-gfm": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@idealyst/theme": "^1.2.28",
|
|
62
|
+
"@types/react": "^19.1.0",
|
|
63
|
+
"react": "^19.1.0",
|
|
64
|
+
"react-markdown": "^9.0.0",
|
|
65
|
+
"react-native": "^0.80.1",
|
|
66
|
+
"react-native-unistyles": "^3.0.10",
|
|
67
|
+
"remark-gfm": "^4.0.0",
|
|
68
|
+
"typescript": "^5.0.0"
|
|
69
|
+
},
|
|
70
|
+
"files": [
|
|
71
|
+
"src",
|
|
72
|
+
"README.md"
|
|
73
|
+
],
|
|
74
|
+
"keywords": [
|
|
75
|
+
"react",
|
|
76
|
+
"react-native",
|
|
77
|
+
"markdown",
|
|
78
|
+
"cross-platform",
|
|
79
|
+
"idealyst"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { forwardRef, useMemo, ComponentRef } from 'react';
|
|
2
|
+
import { View, Linking } from 'react-native';
|
|
3
|
+
import MarkdownDisplay from 'react-native-markdown-display';
|
|
4
|
+
import { markdownStyles } from './Markdown.styles';
|
|
5
|
+
import { createNativeStyles } from '../renderers/native/createNativeStyles';
|
|
6
|
+
import type { MarkdownProps } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cross-platform Markdown renderer for React Native.
|
|
10
|
+
*
|
|
11
|
+
* Uses react-native-markdown-display for native rendering.
|
|
12
|
+
* Integrates with @idealyst/theme for consistent styling.
|
|
13
|
+
*/
|
|
14
|
+
const Markdown = forwardRef<ComponentRef<typeof View>, MarkdownProps>(
|
|
15
|
+
(
|
|
16
|
+
{
|
|
17
|
+
children,
|
|
18
|
+
size = 'md',
|
|
19
|
+
linkIntent = 'primary',
|
|
20
|
+
styleOverrides,
|
|
21
|
+
linkHandler,
|
|
22
|
+
imageHandler,
|
|
23
|
+
codeOptions: _codeOptions,
|
|
24
|
+
gfm: _gfm = true,
|
|
25
|
+
allowHtml: _allowHtml = false,
|
|
26
|
+
components: _customComponents,
|
|
27
|
+
style,
|
|
28
|
+
testID,
|
|
29
|
+
id,
|
|
30
|
+
accessibilityLabel,
|
|
31
|
+
},
|
|
32
|
+
ref
|
|
33
|
+
) => {
|
|
34
|
+
// Apply variants for size and linkIntent
|
|
35
|
+
markdownStyles.useVariants({
|
|
36
|
+
size,
|
|
37
|
+
linkIntent,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Create native style rules from theme styles
|
|
41
|
+
const nativeStyles = useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
createNativeStyles({
|
|
44
|
+
styles: markdownStyles,
|
|
45
|
+
dynamicProps: { size, linkIntent },
|
|
46
|
+
styleOverrides,
|
|
47
|
+
}),
|
|
48
|
+
[size, linkIntent, styleOverrides]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Handle link presses
|
|
52
|
+
const handleLinkPress = (url: string): boolean => {
|
|
53
|
+
if (linkHandler?.onLinkPress) {
|
|
54
|
+
const preventDefault = linkHandler.onLinkPress(url);
|
|
55
|
+
if (preventDefault) {
|
|
56
|
+
return false; // Don't open the link
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Open external links if enabled (default)
|
|
61
|
+
if (linkHandler?.openExternalLinks ?? true) {
|
|
62
|
+
Linking.openURL(url).catch(console.warn);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false; // We handle it ourselves
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View
|
|
70
|
+
ref={ref}
|
|
71
|
+
nativeID={id}
|
|
72
|
+
testID={testID}
|
|
73
|
+
accessibilityLabel={accessibilityLabel}
|
|
74
|
+
style={[
|
|
75
|
+
(markdownStyles.container as any)({ size, linkIntent }),
|
|
76
|
+
style,
|
|
77
|
+
]}
|
|
78
|
+
>
|
|
79
|
+
<MarkdownDisplay
|
|
80
|
+
style={nativeStyles}
|
|
81
|
+
onLinkPress={handleLinkPress}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
</MarkdownDisplay>
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
Markdown.displayName = 'Markdown';
|
|
91
|
+
|
|
92
|
+
export default Markdown;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown styles using defineStyle with theme integration.
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent styling for all markdown elements that
|
|
5
|
+
* integrates with the Idealyst theme system.
|
|
6
|
+
*/
|
|
7
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
8
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
9
|
+
import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
|
|
10
|
+
|
|
11
|
+
// Required: Unistyles must see StyleSheet usage in original source to process this file
|
|
12
|
+
void StyleSheet;
|
|
13
|
+
|
|
14
|
+
// Wrap theme for $iterator support
|
|
15
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
16
|
+
|
|
17
|
+
export type MarkdownVariants = {
|
|
18
|
+
size: Size;
|
|
19
|
+
linkIntent: Intent;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dynamic props passed to markdown style functions.
|
|
24
|
+
*/
|
|
25
|
+
export type MarkdownDynamicProps = {
|
|
26
|
+
linkIntent?: Intent;
|
|
27
|
+
size?: Size;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Markdown styles with theme integration.
|
|
32
|
+
*
|
|
33
|
+
* All elements use theme colors, typography, and spacing
|
|
34
|
+
* for consistent styling across the application.
|
|
35
|
+
*/
|
|
36
|
+
// @ts-expect-error - Markdown is not in the ComponentName union yet
|
|
37
|
+
export const markdownStyles = defineStyle('Markdown', (theme: Theme) => ({
|
|
38
|
+
// Container
|
|
39
|
+
container: (_props: MarkdownDynamicProps) => ({
|
|
40
|
+
flexShrink: 1,
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
// Body/paragraph text - uses theme typography
|
|
44
|
+
body: (_props: MarkdownDynamicProps) => ({
|
|
45
|
+
color: theme.colors.text.primary,
|
|
46
|
+
marginVertical: 8,
|
|
47
|
+
fontSize: theme.sizes.typography.body1.fontSize,
|
|
48
|
+
lineHeight: theme.sizes.typography.body1.lineHeight,
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
// Headings - use theme typography for each level
|
|
52
|
+
heading1: (_props: MarkdownDynamicProps) => ({
|
|
53
|
+
color: theme.colors.text.primary,
|
|
54
|
+
fontWeight: '700',
|
|
55
|
+
marginTop: 24,
|
|
56
|
+
marginBottom: 16,
|
|
57
|
+
fontSize: theme.sizes.typography.h1.fontSize,
|
|
58
|
+
lineHeight: theme.sizes.typography.h1.lineHeight,
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
heading2: (_props: MarkdownDynamicProps) => ({
|
|
62
|
+
color: theme.colors.text.primary,
|
|
63
|
+
fontWeight: '600',
|
|
64
|
+
marginTop: 20,
|
|
65
|
+
marginBottom: 12,
|
|
66
|
+
borderBottomWidth: 1,
|
|
67
|
+
borderBottomColor: theme.colors.border.primary,
|
|
68
|
+
paddingBottom: 8,
|
|
69
|
+
fontSize: theme.sizes.typography.h2.fontSize,
|
|
70
|
+
lineHeight: theme.sizes.typography.h2.lineHeight,
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
heading3: (_props: MarkdownDynamicProps) => ({
|
|
74
|
+
color: theme.colors.text.primary,
|
|
75
|
+
fontWeight: '600',
|
|
76
|
+
marginTop: 16,
|
|
77
|
+
marginBottom: 8,
|
|
78
|
+
fontSize: theme.sizes.typography.h3.fontSize,
|
|
79
|
+
lineHeight: theme.sizes.typography.h3.lineHeight,
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
heading4: (_props: MarkdownDynamicProps) => ({
|
|
83
|
+
color: theme.colors.text.primary,
|
|
84
|
+
fontWeight: '600',
|
|
85
|
+
marginTop: 12,
|
|
86
|
+
marginBottom: 8,
|
|
87
|
+
fontSize: theme.sizes.typography.h4.fontSize,
|
|
88
|
+
lineHeight: theme.sizes.typography.h4.lineHeight,
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
heading5: (_props: MarkdownDynamicProps) => ({
|
|
92
|
+
color: theme.colors.text.primary,
|
|
93
|
+
fontWeight: '600',
|
|
94
|
+
marginTop: 10,
|
|
95
|
+
marginBottom: 6,
|
|
96
|
+
fontSize: theme.sizes.typography.h5.fontSize,
|
|
97
|
+
lineHeight: theme.sizes.typography.h5.lineHeight,
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
heading6: (_props: MarkdownDynamicProps) => ({
|
|
101
|
+
color: theme.colors.text.secondary,
|
|
102
|
+
fontWeight: '600',
|
|
103
|
+
marginTop: 10,
|
|
104
|
+
marginBottom: 6,
|
|
105
|
+
fontSize: theme.sizes.typography.h6.fontSize,
|
|
106
|
+
lineHeight: theme.sizes.typography.h6.lineHeight,
|
|
107
|
+
}),
|
|
108
|
+
|
|
109
|
+
// Emphasis
|
|
110
|
+
strong: (_props: MarkdownDynamicProps) => ({
|
|
111
|
+
fontWeight: '700',
|
|
112
|
+
}),
|
|
113
|
+
|
|
114
|
+
em: (_props: MarkdownDynamicProps) => ({
|
|
115
|
+
fontStyle: 'italic',
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
strikethrough: (_props: MarkdownDynamicProps) => ({
|
|
119
|
+
textDecorationLine: 'line-through',
|
|
120
|
+
color: theme.colors.text.tertiary,
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
// Links
|
|
124
|
+
link: ({ linkIntent = 'primary' }: MarkdownDynamicProps) => ({
|
|
125
|
+
color: theme.intents[linkIntent].primary,
|
|
126
|
+
textDecorationLine: 'underline',
|
|
127
|
+
_web: {
|
|
128
|
+
cursor: 'pointer',
|
|
129
|
+
transition: 'color 0.15s ease',
|
|
130
|
+
_hover: {
|
|
131
|
+
opacity: 0.8,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
// Blockquote
|
|
137
|
+
blockquote: (_props: MarkdownDynamicProps) => ({
|
|
138
|
+
borderLeftWidth: 4,
|
|
139
|
+
borderLeftColor: theme.colors.border.secondary,
|
|
140
|
+
paddingLeft: 16,
|
|
141
|
+
paddingVertical: 8,
|
|
142
|
+
marginVertical: 12,
|
|
143
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
144
|
+
borderRadius: theme.radii.sm,
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
blockquoteText: (_props: MarkdownDynamicProps) => ({
|
|
148
|
+
color: theme.colors.text.secondary,
|
|
149
|
+
fontStyle: 'italic',
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
// Code inline
|
|
153
|
+
codeInline: (_props: MarkdownDynamicProps) => ({
|
|
154
|
+
fontFamily: 'monospace',
|
|
155
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
156
|
+
paddingHorizontal: 6,
|
|
157
|
+
paddingVertical: 2,
|
|
158
|
+
borderRadius: theme.radii.xs,
|
|
159
|
+
color: theme.colors.text.primary,
|
|
160
|
+
fontSize: theme.sizes.typography.caption.fontSize,
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
// Code block
|
|
164
|
+
codeBlock: (_props: MarkdownDynamicProps) => ({
|
|
165
|
+
fontFamily: 'monospace',
|
|
166
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
167
|
+
padding: 16,
|
|
168
|
+
borderRadius: theme.radii.md,
|
|
169
|
+
marginVertical: 12,
|
|
170
|
+
_web: {
|
|
171
|
+
overflow: 'auto' as const,
|
|
172
|
+
maxHeight: 500,
|
|
173
|
+
},
|
|
174
|
+
fontSize: theme.sizes.typography.caption.fontSize,
|
|
175
|
+
lineHeight: theme.sizes.typography.body1.lineHeight,
|
|
176
|
+
}),
|
|
177
|
+
|
|
178
|
+
codeBlockText: (_props: MarkdownDynamicProps) => ({
|
|
179
|
+
fontFamily: 'monospace',
|
|
180
|
+
color: theme.colors.text.primary,
|
|
181
|
+
}),
|
|
182
|
+
|
|
183
|
+
// Lists
|
|
184
|
+
listOrdered: (_props: MarkdownDynamicProps) => ({
|
|
185
|
+
marginVertical: 8,
|
|
186
|
+
paddingLeft: 24,
|
|
187
|
+
}),
|
|
188
|
+
|
|
189
|
+
listUnordered: (_props: MarkdownDynamicProps) => ({
|
|
190
|
+
marginVertical: 8,
|
|
191
|
+
paddingLeft: 24,
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
listItem: (_props: MarkdownDynamicProps) => ({
|
|
195
|
+
marginVertical: 4,
|
|
196
|
+
color: theme.colors.text.primary,
|
|
197
|
+
flexDirection: 'row',
|
|
198
|
+
}),
|
|
199
|
+
|
|
200
|
+
listItemBullet: (_props: MarkdownDynamicProps) => ({
|
|
201
|
+
color: theme.colors.text.secondary,
|
|
202
|
+
marginRight: 8,
|
|
203
|
+
width: 16,
|
|
204
|
+
}),
|
|
205
|
+
|
|
206
|
+
listItemContent: (_props: MarkdownDynamicProps) => ({
|
|
207
|
+
flex: 1,
|
|
208
|
+
}),
|
|
209
|
+
|
|
210
|
+
// Task list
|
|
211
|
+
taskListItem: (_props: MarkdownDynamicProps) => ({
|
|
212
|
+
flexDirection: 'row',
|
|
213
|
+
alignItems: 'flex-start',
|
|
214
|
+
gap: 8,
|
|
215
|
+
marginVertical: 4,
|
|
216
|
+
}),
|
|
217
|
+
|
|
218
|
+
taskCheckbox: (_props: MarkdownDynamicProps) => ({
|
|
219
|
+
marginTop: 4,
|
|
220
|
+
}),
|
|
221
|
+
|
|
222
|
+
// Table
|
|
223
|
+
table: (_props: MarkdownDynamicProps) => ({
|
|
224
|
+
borderWidth: 1,
|
|
225
|
+
borderColor: theme.colors.border.primary,
|
|
226
|
+
borderRadius: theme.radii.sm,
|
|
227
|
+
marginVertical: 12,
|
|
228
|
+
_web: {
|
|
229
|
+
overflow: 'hidden' as const,
|
|
230
|
+
borderCollapse: 'collapse',
|
|
231
|
+
width: '100%',
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
|
|
235
|
+
tableHead: (_props: MarkdownDynamicProps) => ({
|
|
236
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
237
|
+
}),
|
|
238
|
+
|
|
239
|
+
tableRow: (_props: MarkdownDynamicProps) => ({
|
|
240
|
+
borderBottomWidth: 1,
|
|
241
|
+
borderBottomColor: theme.colors.border.primary,
|
|
242
|
+
_web: {
|
|
243
|
+
_hover: {
|
|
244
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
|
|
249
|
+
tableCell: (_props: MarkdownDynamicProps) => ({
|
|
250
|
+
padding: 12,
|
|
251
|
+
color: theme.colors.text.primary,
|
|
252
|
+
borderRightWidth: 1,
|
|
253
|
+
borderRightColor: theme.colors.border.primary,
|
|
254
|
+
fontSize: theme.sizes.typography.body1.fontSize,
|
|
255
|
+
}),
|
|
256
|
+
|
|
257
|
+
tableHeaderCell: (_props: MarkdownDynamicProps) => ({
|
|
258
|
+
padding: 12,
|
|
259
|
+
color: theme.colors.text.primary,
|
|
260
|
+
fontWeight: '600',
|
|
261
|
+
borderRightWidth: 1,
|
|
262
|
+
borderRightColor: theme.colors.border.primary,
|
|
263
|
+
textAlign: 'left',
|
|
264
|
+
fontSize: theme.sizes.typography.body1.fontSize,
|
|
265
|
+
}),
|
|
266
|
+
|
|
267
|
+
// Image
|
|
268
|
+
image: (_props: MarkdownDynamicProps) => ({
|
|
269
|
+
maxWidth: '100%',
|
|
270
|
+
borderRadius: theme.radii.sm,
|
|
271
|
+
marginVertical: 8,
|
|
272
|
+
}),
|
|
273
|
+
|
|
274
|
+
// Horizontal rule
|
|
275
|
+
hr: (_props: MarkdownDynamicProps) => ({
|
|
276
|
+
height: 1,
|
|
277
|
+
backgroundColor: theme.colors.border.secondary,
|
|
278
|
+
marginVertical: 24,
|
|
279
|
+
borderWidth: 0,
|
|
280
|
+
}),
|
|
281
|
+
|
|
282
|
+
// Footnotes
|
|
283
|
+
footnoteContainer: (_props: MarkdownDynamicProps) => ({
|
|
284
|
+
borderTopWidth: 1,
|
|
285
|
+
borderTopColor: theme.colors.border.primary,
|
|
286
|
+
marginTop: 24,
|
|
287
|
+
paddingTop: 16,
|
|
288
|
+
}),
|
|
289
|
+
|
|
290
|
+
footnote: (_props: MarkdownDynamicProps) => ({
|
|
291
|
+
marginVertical: 4,
|
|
292
|
+
}),
|
|
293
|
+
|
|
294
|
+
footnoteRef: ({ linkIntent = 'primary' }: MarkdownDynamicProps) => ({
|
|
295
|
+
color: theme.intents[linkIntent].primary,
|
|
296
|
+
fontSize: 10,
|
|
297
|
+
_web: {
|
|
298
|
+
verticalAlign: 'super',
|
|
299
|
+
cursor: 'pointer',
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
}));
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { forwardRef, useMemo } from 'react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
5
|
+
import { markdownStyles } from './Markdown.styles';
|
|
6
|
+
import { createWebRenderers } from '../renderers/web';
|
|
7
|
+
import type { MarkdownProps } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Cross-platform Markdown renderer for web.
|
|
11
|
+
*
|
|
12
|
+
* Uses react-markdown with remark-gfm for GitHub Flavored Markdown support.
|
|
13
|
+
* Integrates with @idealyst/theme for consistent styling.
|
|
14
|
+
*/
|
|
15
|
+
const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
|
|
16
|
+
(
|
|
17
|
+
{
|
|
18
|
+
children,
|
|
19
|
+
size = 'md',
|
|
20
|
+
linkIntent = 'primary',
|
|
21
|
+
styleOverrides,
|
|
22
|
+
linkHandler,
|
|
23
|
+
imageHandler,
|
|
24
|
+
codeOptions,
|
|
25
|
+
gfm = true,
|
|
26
|
+
allowHtml = false,
|
|
27
|
+
components: customComponents,
|
|
28
|
+
style,
|
|
29
|
+
testID,
|
|
30
|
+
id,
|
|
31
|
+
accessibilityLabel,
|
|
32
|
+
},
|
|
33
|
+
ref
|
|
34
|
+
) => {
|
|
35
|
+
// Apply variants for size and linkIntent
|
|
36
|
+
markdownStyles.useVariants({
|
|
37
|
+
size,
|
|
38
|
+
linkIntent,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Create custom component renderers with theme styles
|
|
42
|
+
const renderers = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
createWebRenderers({
|
|
45
|
+
styles: markdownStyles,
|
|
46
|
+
dynamicProps: { size, linkIntent },
|
|
47
|
+
styleOverrides,
|
|
48
|
+
linkHandler,
|
|
49
|
+
imageHandler,
|
|
50
|
+
codeOptions,
|
|
51
|
+
}),
|
|
52
|
+
[size, linkIntent, styleOverrides, linkHandler, imageHandler, codeOptions]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Merge custom components with default renderers
|
|
56
|
+
const mergedComponents = useMemo(
|
|
57
|
+
() => ({
|
|
58
|
+
...renderers,
|
|
59
|
+
...customComponents,
|
|
60
|
+
}),
|
|
61
|
+
[renderers, customComponents]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Get web props for the container
|
|
65
|
+
const containerStyleArray = [
|
|
66
|
+
(markdownStyles.container as any)({ size, linkIntent }),
|
|
67
|
+
style,
|
|
68
|
+
];
|
|
69
|
+
const webProps = getWebProps(containerStyleArray);
|
|
70
|
+
|
|
71
|
+
// Configure remark plugins
|
|
72
|
+
const remarkPlugins = useMemo(() => {
|
|
73
|
+
const plugins: any[] = [];
|
|
74
|
+
if (gfm) {
|
|
75
|
+
plugins.push(remarkGfm);
|
|
76
|
+
}
|
|
77
|
+
return plugins;
|
|
78
|
+
}, [gfm]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
{...webProps}
|
|
83
|
+
ref={ref}
|
|
84
|
+
id={id}
|
|
85
|
+
data-testid={testID}
|
|
86
|
+
aria-label={accessibilityLabel}
|
|
87
|
+
>
|
|
88
|
+
<ReactMarkdown
|
|
89
|
+
remarkPlugins={remarkPlugins}
|
|
90
|
+
components={mergedComponents}
|
|
91
|
+
skipHtml={!allowHtml}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</ReactMarkdown>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
Markdown.displayName = 'Markdown';
|
|
101
|
+
|
|
102
|
+
export default Markdown;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { ReactNode, ComponentType } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
|
+
import type { Size, Intent } from '@idealyst/theme';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Markdown element types that can be individually styled
|
|
7
|
+
*/
|
|
8
|
+
export type MarkdownElementType =
|
|
9
|
+
| 'body'
|
|
10
|
+
| 'heading1'
|
|
11
|
+
| 'heading2'
|
|
12
|
+
| 'heading3'
|
|
13
|
+
| 'heading4'
|
|
14
|
+
| 'heading5'
|
|
15
|
+
| 'heading6'
|
|
16
|
+
| 'paragraph'
|
|
17
|
+
| 'strong'
|
|
18
|
+
| 'em'
|
|
19
|
+
| 'strikethrough'
|
|
20
|
+
| 'link'
|
|
21
|
+
| 'blockquote'
|
|
22
|
+
| 'codeInline'
|
|
23
|
+
| 'codeBlock'
|
|
24
|
+
| 'listOrdered'
|
|
25
|
+
| 'listUnordered'
|
|
26
|
+
| 'listItem'
|
|
27
|
+
| 'table'
|
|
28
|
+
| 'tableHead'
|
|
29
|
+
| 'tableRow'
|
|
30
|
+
| 'tableCell'
|
|
31
|
+
| 'image'
|
|
32
|
+
| 'hr'
|
|
33
|
+
| 'taskListItem'
|
|
34
|
+
| 'footnote'
|
|
35
|
+
| 'footnoteRef';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Style overrides for specific markdown elements
|
|
39
|
+
*/
|
|
40
|
+
export type MarkdownStyleOverrides = Partial<
|
|
41
|
+
Record<MarkdownElementType, StyleProp<ViewStyle | TextStyle>>
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Link handling options
|
|
46
|
+
*/
|
|
47
|
+
export interface LinkHandler {
|
|
48
|
+
/**
|
|
49
|
+
* Called when a link is pressed/clicked
|
|
50
|
+
* Return true to prevent default behavior
|
|
51
|
+
*/
|
|
52
|
+
onLinkPress?: (url: string, title?: string) => boolean | void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether to open external links in new tab (web) or browser (native)
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
openExternalLinks?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Image handling options
|
|
63
|
+
*/
|
|
64
|
+
export interface ImageHandler {
|
|
65
|
+
/**
|
|
66
|
+
* Custom image resolver for relative paths
|
|
67
|
+
*/
|
|
68
|
+
resolveImageUrl?: (src: string) => string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Default image dimensions when not specified in markdown
|
|
72
|
+
*/
|
|
73
|
+
defaultImageDimensions?: { width?: number; height?: number };
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Called when an image is pressed/clicked
|
|
77
|
+
*/
|
|
78
|
+
onImagePress?: (src: string, alt?: string) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Code block handling options
|
|
83
|
+
*/
|
|
84
|
+
export interface CodeBlockOptions {
|
|
85
|
+
/**
|
|
86
|
+
* Enable syntax highlighting
|
|
87
|
+
* @default false
|
|
88
|
+
*/
|
|
89
|
+
syntaxHighlighting?: boolean;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Syntax highlighting theme (maps to theme's light/dark)
|
|
93
|
+
* @default 'auto'
|
|
94
|
+
*/
|
|
95
|
+
syntaxTheme?: 'auto' | 'light' | 'dark';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Show line numbers in code blocks
|
|
99
|
+
* @default false
|
|
100
|
+
*/
|
|
101
|
+
showLineNumbers?: boolean;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Enable copy button for code blocks (web only)
|
|
105
|
+
* @default true
|
|
106
|
+
*/
|
|
107
|
+
copyButton?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Main Markdown component props
|
|
112
|
+
*/
|
|
113
|
+
export interface MarkdownProps {
|
|
114
|
+
/**
|
|
115
|
+
* Markdown content to render
|
|
116
|
+
*/
|
|
117
|
+
children: string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Text size variant - affects base font size and heading scales
|
|
121
|
+
* @default 'md'
|
|
122
|
+
*/
|
|
123
|
+
size?: Size;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Link color intent
|
|
127
|
+
* @default 'primary'
|
|
128
|
+
*/
|
|
129
|
+
linkIntent?: Intent;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Custom style overrides for specific elements
|
|
133
|
+
*/
|
|
134
|
+
styleOverrides?: MarkdownStyleOverrides;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Link handling configuration
|
|
138
|
+
*/
|
|
139
|
+
linkHandler?: LinkHandler;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Image handling configuration
|
|
143
|
+
*/
|
|
144
|
+
imageHandler?: ImageHandler;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Code block configuration
|
|
148
|
+
*/
|
|
149
|
+
codeOptions?: CodeBlockOptions;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Enable GFM extensions (tables, strikethrough, task lists, footnotes)
|
|
153
|
+
* @default true
|
|
154
|
+
*/
|
|
155
|
+
gfm?: boolean;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Allow raw HTML in markdown (security consideration)
|
|
159
|
+
* @default false
|
|
160
|
+
*/
|
|
161
|
+
allowHtml?: boolean;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Custom component renderers (advanced usage)
|
|
165
|
+
* Web: Maps to react-markdown components prop
|
|
166
|
+
* Native: Maps to react-native-markdown-display rules
|
|
167
|
+
*/
|
|
168
|
+
components?: Partial<Record<MarkdownElementType, ComponentType<any>>>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Container style
|
|
172
|
+
*/
|
|
173
|
+
style?: StyleProp<ViewStyle>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Test identifier
|
|
177
|
+
*/
|
|
178
|
+
testID?: string;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Unique identifier
|
|
182
|
+
*/
|
|
183
|
+
id?: string;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Accessibility label for the markdown container
|
|
187
|
+
*/
|
|
188
|
+
accessibilityLabel?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Props passed to custom markdown element renderers
|
|
193
|
+
*/
|
|
194
|
+
export interface MarkdownRendererProps<T = unknown> {
|
|
195
|
+
children?: ReactNode;
|
|
196
|
+
node?: T;
|
|
197
|
+
style?: StyleProp<ViewStyle | TextStyle>;
|
|
198
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @idealyst/markdown - Cross-platform markdown renderer (React Native)
|
|
3
|
+
*
|
|
4
|
+
* Provides a Markdown component for rendering markdown content
|
|
5
|
+
* with theme integration on React Native.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Main component
|
|
9
|
+
export { default as Markdown } from './Markdown/Markdown.native';
|
|
10
|
+
|
|
11
|
+
// Types
|
|
12
|
+
export type {
|
|
13
|
+
MarkdownProps,
|
|
14
|
+
MarkdownElementType,
|
|
15
|
+
MarkdownStyleOverrides,
|
|
16
|
+
LinkHandler,
|
|
17
|
+
ImageHandler,
|
|
18
|
+
CodeBlockOptions,
|
|
19
|
+
MarkdownRendererProps,
|
|
20
|
+
} from './Markdown/types';
|
|
21
|
+
|
|
22
|
+
// Style types
|
|
23
|
+
export type {
|
|
24
|
+
MarkdownDynamicProps,
|
|
25
|
+
MarkdownVariants,
|
|
26
|
+
} from './Markdown/Markdown.styles';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @idealyst/markdown - Cross-platform markdown renderer
|
|
3
|
+
*
|
|
4
|
+
* Provides a Markdown component for rendering markdown content
|
|
5
|
+
* with theme integration on both web and React Native platforms.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { Markdown } from '@idealyst/markdown';
|
|
10
|
+
*
|
|
11
|
+
* function App() {
|
|
12
|
+
* return (
|
|
13
|
+
* <Markdown size="md" linkIntent="primary">
|
|
14
|
+
* # Hello World
|
|
15
|
+
*
|
|
16
|
+
* This is **markdown** with _emphasis_.
|
|
17
|
+
* </Markdown>
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Main component
|
|
24
|
+
export { default as Markdown, Markdown as MarkdownComponent } from './Markdown';
|
|
25
|
+
|
|
26
|
+
// Types
|
|
27
|
+
export type {
|
|
28
|
+
MarkdownProps,
|
|
29
|
+
MarkdownElementType,
|
|
30
|
+
MarkdownStyleOverrides,
|
|
31
|
+
LinkHandler,
|
|
32
|
+
ImageHandler,
|
|
33
|
+
CodeBlockOptions,
|
|
34
|
+
MarkdownRendererProps,
|
|
35
|
+
} from './Markdown/types';
|
|
36
|
+
|
|
37
|
+
// Style types
|
|
38
|
+
export type {
|
|
39
|
+
MarkdownDynamicProps,
|
|
40
|
+
MarkdownVariants,
|
|
41
|
+
} from './Markdown/Markdown.styles';
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { TextStyle, ViewStyle } from 'react-native';
|
|
2
|
+
import type { MarkdownStyles } from 'react-native-markdown-display';
|
|
3
|
+
import type { MarkdownStyleOverrides } from '../../Markdown/types';
|
|
4
|
+
import type { MarkdownDynamicProps } from '../../Markdown/Markdown.styles';
|
|
5
|
+
|
|
6
|
+
interface CreateNativeStylesOptions {
|
|
7
|
+
styles: any;
|
|
8
|
+
dynamicProps: MarkdownDynamicProps;
|
|
9
|
+
styleOverrides?: MarkdownStyleOverrides;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper to safely extract style from a dynamic style function.
|
|
14
|
+
* Removes web-only properties like _web, _hover, etc.
|
|
15
|
+
*/
|
|
16
|
+
function extractStyle(
|
|
17
|
+
styleFn: any,
|
|
18
|
+
dynamicProps: MarkdownDynamicProps
|
|
19
|
+
): ViewStyle | TextStyle {
|
|
20
|
+
if (typeof styleFn !== 'function') {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const style = styleFn(dynamicProps);
|
|
25
|
+
|
|
26
|
+
if (!style || typeof style !== 'object') {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Remove web-only and variant properties
|
|
31
|
+
const {
|
|
32
|
+
_web,
|
|
33
|
+
_hover,
|
|
34
|
+
_active,
|
|
35
|
+
_focus,
|
|
36
|
+
variants,
|
|
37
|
+
compoundVariants,
|
|
38
|
+
...nativeStyle
|
|
39
|
+
} = style;
|
|
40
|
+
|
|
41
|
+
return nativeStyle;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Merges base style with override style.
|
|
46
|
+
*/
|
|
47
|
+
function mergeStyles(
|
|
48
|
+
base: ViewStyle | TextStyle,
|
|
49
|
+
override?: any
|
|
50
|
+
): ViewStyle | TextStyle {
|
|
51
|
+
if (!override) {
|
|
52
|
+
return base;
|
|
53
|
+
}
|
|
54
|
+
return { ...base, ...override };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a style sheet compatible with react-native-markdown-display
|
|
59
|
+
* from the theme-integrated markdown styles.
|
|
60
|
+
*/
|
|
61
|
+
export function createNativeStyles({
|
|
62
|
+
styles,
|
|
63
|
+
dynamicProps,
|
|
64
|
+
styleOverrides,
|
|
65
|
+
}: CreateNativeStylesOptions): MarkdownStyles {
|
|
66
|
+
return {
|
|
67
|
+
// Body/text
|
|
68
|
+
body: mergeStyles(
|
|
69
|
+
extractStyle(styles.body, dynamicProps),
|
|
70
|
+
styleOverrides?.body
|
|
71
|
+
) as TextStyle,
|
|
72
|
+
text: mergeStyles(
|
|
73
|
+
extractStyle(styles.body, dynamicProps),
|
|
74
|
+
styleOverrides?.body
|
|
75
|
+
) as TextStyle,
|
|
76
|
+
paragraph: mergeStyles(
|
|
77
|
+
extractStyle(styles.body, dynamicProps),
|
|
78
|
+
styleOverrides?.paragraph
|
|
79
|
+
) as ViewStyle,
|
|
80
|
+
|
|
81
|
+
// Headings
|
|
82
|
+
heading1: mergeStyles(
|
|
83
|
+
extractStyle(styles.heading1, dynamicProps),
|
|
84
|
+
styleOverrides?.heading1
|
|
85
|
+
) as TextStyle,
|
|
86
|
+
heading2: mergeStyles(
|
|
87
|
+
extractStyle(styles.heading2, dynamicProps),
|
|
88
|
+
styleOverrides?.heading2
|
|
89
|
+
) as TextStyle,
|
|
90
|
+
heading3: mergeStyles(
|
|
91
|
+
extractStyle(styles.heading3, dynamicProps),
|
|
92
|
+
styleOverrides?.heading3
|
|
93
|
+
) as TextStyle,
|
|
94
|
+
heading4: mergeStyles(
|
|
95
|
+
extractStyle(styles.heading4, dynamicProps),
|
|
96
|
+
styleOverrides?.heading4
|
|
97
|
+
) as TextStyle,
|
|
98
|
+
heading5: mergeStyles(
|
|
99
|
+
extractStyle(styles.heading5, dynamicProps),
|
|
100
|
+
styleOverrides?.heading5
|
|
101
|
+
) as TextStyle,
|
|
102
|
+
heading6: mergeStyles(
|
|
103
|
+
extractStyle(styles.heading6, dynamicProps),
|
|
104
|
+
styleOverrides?.heading6
|
|
105
|
+
) as TextStyle,
|
|
106
|
+
|
|
107
|
+
// Horizontal rule
|
|
108
|
+
hr: mergeStyles(
|
|
109
|
+
extractStyle(styles.hr, dynamicProps),
|
|
110
|
+
styleOverrides?.hr
|
|
111
|
+
) as ViewStyle,
|
|
112
|
+
|
|
113
|
+
// Emphasis
|
|
114
|
+
strong: mergeStyles(
|
|
115
|
+
extractStyle(styles.strong, dynamicProps),
|
|
116
|
+
styleOverrides?.strong
|
|
117
|
+
) as TextStyle,
|
|
118
|
+
em: mergeStyles(
|
|
119
|
+
extractStyle(styles.em, dynamicProps),
|
|
120
|
+
styleOverrides?.em
|
|
121
|
+
) as TextStyle,
|
|
122
|
+
s: mergeStyles(
|
|
123
|
+
extractStyle(styles.strikethrough, dynamicProps),
|
|
124
|
+
styleOverrides?.strikethrough
|
|
125
|
+
) as TextStyle,
|
|
126
|
+
|
|
127
|
+
// Blockquote
|
|
128
|
+
blockquote: mergeStyles(
|
|
129
|
+
extractStyle(styles.blockquote, dynamicProps),
|
|
130
|
+
styleOverrides?.blockquote
|
|
131
|
+
) as ViewStyle,
|
|
132
|
+
|
|
133
|
+
// Lists
|
|
134
|
+
bullet_list: mergeStyles(
|
|
135
|
+
extractStyle(styles.listUnordered, dynamicProps),
|
|
136
|
+
styleOverrides?.listUnordered
|
|
137
|
+
) as ViewStyle,
|
|
138
|
+
ordered_list: mergeStyles(
|
|
139
|
+
extractStyle(styles.listOrdered, dynamicProps),
|
|
140
|
+
styleOverrides?.listOrdered
|
|
141
|
+
) as ViewStyle,
|
|
142
|
+
list_item: mergeStyles(
|
|
143
|
+
extractStyle(styles.listItem, dynamicProps),
|
|
144
|
+
styleOverrides?.listItem
|
|
145
|
+
) as ViewStyle,
|
|
146
|
+
bullet_list_icon: mergeStyles(
|
|
147
|
+
extractStyle(styles.listItemBullet, dynamicProps),
|
|
148
|
+
undefined
|
|
149
|
+
) as TextStyle,
|
|
150
|
+
bullet_list_content: mergeStyles(
|
|
151
|
+
extractStyle(styles.listItemContent, dynamicProps),
|
|
152
|
+
undefined
|
|
153
|
+
) as ViewStyle,
|
|
154
|
+
ordered_list_icon: mergeStyles(
|
|
155
|
+
extractStyle(styles.listItemBullet, dynamicProps),
|
|
156
|
+
undefined
|
|
157
|
+
) as TextStyle,
|
|
158
|
+
ordered_list_content: mergeStyles(
|
|
159
|
+
extractStyle(styles.listItemContent, dynamicProps),
|
|
160
|
+
undefined
|
|
161
|
+
) as ViewStyle,
|
|
162
|
+
|
|
163
|
+
// Code
|
|
164
|
+
code_inline: mergeStyles(
|
|
165
|
+
extractStyle(styles.codeInline, dynamicProps),
|
|
166
|
+
styleOverrides?.codeInline
|
|
167
|
+
) as TextStyle,
|
|
168
|
+
code_block: mergeStyles(
|
|
169
|
+
extractStyle(styles.codeBlock, dynamicProps),
|
|
170
|
+
styleOverrides?.codeBlock
|
|
171
|
+
) as ViewStyle,
|
|
172
|
+
fence: mergeStyles(
|
|
173
|
+
extractStyle(styles.codeBlock, dynamicProps),
|
|
174
|
+
styleOverrides?.codeBlock
|
|
175
|
+
) as ViewStyle,
|
|
176
|
+
pre: mergeStyles(
|
|
177
|
+
extractStyle(styles.codeBlock, dynamicProps),
|
|
178
|
+
styleOverrides?.codeBlock
|
|
179
|
+
) as ViewStyle,
|
|
180
|
+
|
|
181
|
+
// Table
|
|
182
|
+
table: mergeStyles(
|
|
183
|
+
extractStyle(styles.table, dynamicProps),
|
|
184
|
+
styleOverrides?.table
|
|
185
|
+
) as ViewStyle,
|
|
186
|
+
thead: mergeStyles(
|
|
187
|
+
extractStyle(styles.tableHead, dynamicProps),
|
|
188
|
+
styleOverrides?.tableHead
|
|
189
|
+
) as ViewStyle,
|
|
190
|
+
tbody: {} as ViewStyle,
|
|
191
|
+
tr: mergeStyles(
|
|
192
|
+
extractStyle(styles.tableRow, dynamicProps),
|
|
193
|
+
styleOverrides?.tableRow
|
|
194
|
+
) as ViewStyle,
|
|
195
|
+
th: mergeStyles(
|
|
196
|
+
extractStyle(styles.tableHeaderCell, dynamicProps),
|
|
197
|
+
styleOverrides?.tableCell
|
|
198
|
+
) as TextStyle,
|
|
199
|
+
td: mergeStyles(
|
|
200
|
+
extractStyle(styles.tableCell, dynamicProps),
|
|
201
|
+
styleOverrides?.tableCell
|
|
202
|
+
) as TextStyle,
|
|
203
|
+
|
|
204
|
+
// Link
|
|
205
|
+
link: mergeStyles(
|
|
206
|
+
extractStyle(styles.link, dynamicProps),
|
|
207
|
+
styleOverrides?.link
|
|
208
|
+
) as TextStyle,
|
|
209
|
+
blocklink: {} as ViewStyle,
|
|
210
|
+
|
|
211
|
+
// Image
|
|
212
|
+
image: mergeStyles(
|
|
213
|
+
extractStyle(styles.image, dynamicProps),
|
|
214
|
+
styleOverrides?.image
|
|
215
|
+
) as ViewStyle,
|
|
216
|
+
|
|
217
|
+
// Other
|
|
218
|
+
textgroup: {} as ViewStyle,
|
|
219
|
+
hardbreak: {} as ViewStyle,
|
|
220
|
+
softbreak: {} as ViewStyle,
|
|
221
|
+
inline: {} as ViewStyle,
|
|
222
|
+
span: {} as ViewStyle,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
|
+
import type {
|
|
4
|
+
MarkdownStyleOverrides,
|
|
5
|
+
LinkHandler,
|
|
6
|
+
ImageHandler,
|
|
7
|
+
CodeBlockOptions,
|
|
8
|
+
} from '../../Markdown/types';
|
|
9
|
+
import type { MarkdownDynamicProps } from '../../Markdown/Markdown.styles';
|
|
10
|
+
|
|
11
|
+
interface CreateWebRenderersOptions {
|
|
12
|
+
styles: any;
|
|
13
|
+
dynamicProps: MarkdownDynamicProps;
|
|
14
|
+
styleOverrides?: MarkdownStyleOverrides;
|
|
15
|
+
linkHandler?: LinkHandler;
|
|
16
|
+
imageHandler?: ImageHandler;
|
|
17
|
+
codeOptions?: CodeBlockOptions;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RendererProps = {
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
node?: any;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates custom component renderers for react-markdown
|
|
28
|
+
* that use theme-integrated styles.
|
|
29
|
+
*/
|
|
30
|
+
export function createWebRenderers({
|
|
31
|
+
styles,
|
|
32
|
+
dynamicProps,
|
|
33
|
+
styleOverrides,
|
|
34
|
+
linkHandler,
|
|
35
|
+
imageHandler,
|
|
36
|
+
codeOptions,
|
|
37
|
+
}: CreateWebRenderersOptions): Record<string, ComponentType<RendererProps>> {
|
|
38
|
+
// Helper to get styled props
|
|
39
|
+
const getStyledProps = (elementName: string) => {
|
|
40
|
+
const styleArray = [
|
|
41
|
+
(styles[elementName] as any)?.(dynamicProps),
|
|
42
|
+
(styleOverrides as any)?.[elementName],
|
|
43
|
+
].filter(Boolean);
|
|
44
|
+
return getWebProps(styleArray);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
// Headings
|
|
49
|
+
h1: ({ children }: RendererProps) => (
|
|
50
|
+
<h1 {...getStyledProps('heading1')}>{children}</h1>
|
|
51
|
+
),
|
|
52
|
+
h2: ({ children }: RendererProps) => (
|
|
53
|
+
<h2 {...getStyledProps('heading2')}>{children}</h2>
|
|
54
|
+
),
|
|
55
|
+
h3: ({ children }: RendererProps) => (
|
|
56
|
+
<h3 {...getStyledProps('heading3')}>{children}</h3>
|
|
57
|
+
),
|
|
58
|
+
h4: ({ children }: RendererProps) => (
|
|
59
|
+
<h4 {...getStyledProps('heading4')}>{children}</h4>
|
|
60
|
+
),
|
|
61
|
+
h5: ({ children }: RendererProps) => (
|
|
62
|
+
<h5 {...getStyledProps('heading5')}>{children}</h5>
|
|
63
|
+
),
|
|
64
|
+
h6: ({ children }: RendererProps) => (
|
|
65
|
+
<h6 {...getStyledProps('heading6')}>{children}</h6>
|
|
66
|
+
),
|
|
67
|
+
|
|
68
|
+
// Paragraph
|
|
69
|
+
p: ({ children }: RendererProps) => (
|
|
70
|
+
<p {...getStyledProps('body')}>{children}</p>
|
|
71
|
+
),
|
|
72
|
+
|
|
73
|
+
// Emphasis
|
|
74
|
+
strong: ({ children }: RendererProps) => (
|
|
75
|
+
<strong {...getStyledProps('strong')}>{children}</strong>
|
|
76
|
+
),
|
|
77
|
+
em: ({ children }: RendererProps) => (
|
|
78
|
+
<em {...getStyledProps('em')}>{children}</em>
|
|
79
|
+
),
|
|
80
|
+
del: ({ children }: RendererProps) => (
|
|
81
|
+
<del {...getStyledProps('strikethrough')}>{children}</del>
|
|
82
|
+
),
|
|
83
|
+
|
|
84
|
+
// Links
|
|
85
|
+
a: ({ children, href, title }: RendererProps) => {
|
|
86
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
87
|
+
if (linkHandler?.onLinkPress) {
|
|
88
|
+
const preventDefault = linkHandler.onLinkPress(href || '', title);
|
|
89
|
+
if (preventDefault) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const isExternal =
|
|
96
|
+
href?.startsWith('http://') || href?.startsWith('https://');
|
|
97
|
+
const shouldOpenExternal =
|
|
98
|
+
isExternal && (linkHandler?.openExternalLinks ?? true);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<a
|
|
102
|
+
{...getStyledProps('link')}
|
|
103
|
+
href={href}
|
|
104
|
+
title={title}
|
|
105
|
+
onClick={handleClick}
|
|
106
|
+
target={shouldOpenExternal ? '_blank' : undefined}
|
|
107
|
+
rel={shouldOpenExternal ? 'noopener noreferrer' : undefined}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</a>
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Blockquote
|
|
115
|
+
blockquote: ({ children }: RendererProps) => (
|
|
116
|
+
<blockquote {...getStyledProps('blockquote')}>{children}</blockquote>
|
|
117
|
+
),
|
|
118
|
+
|
|
119
|
+
// Code
|
|
120
|
+
code: ({ children, className, inline }: RendererProps) => {
|
|
121
|
+
// Check if it's inline code
|
|
122
|
+
if (inline) {
|
|
123
|
+
return <code {...getStyledProps('codeInline')}>{children}</code>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Block code - extract language from className
|
|
127
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
128
|
+
const language = match ? match[1] : '';
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<code
|
|
132
|
+
{...getStyledProps('codeBlockText')}
|
|
133
|
+
data-language={language}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</code>
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
pre: ({ children }: RendererProps) => (
|
|
141
|
+
<pre {...getStyledProps('codeBlock')}>{children}</pre>
|
|
142
|
+
),
|
|
143
|
+
|
|
144
|
+
// Lists
|
|
145
|
+
ul: ({ children }: RendererProps) => (
|
|
146
|
+
<ul {...getStyledProps('listUnordered')}>{children}</ul>
|
|
147
|
+
),
|
|
148
|
+
ol: ({ children }: RendererProps) => (
|
|
149
|
+
<ol {...getStyledProps('listOrdered')}>{children}</ol>
|
|
150
|
+
),
|
|
151
|
+
li: ({ children, checked }: RendererProps) => {
|
|
152
|
+
// Task list item
|
|
153
|
+
if (typeof checked === 'boolean') {
|
|
154
|
+
return (
|
|
155
|
+
<li {...getStyledProps('taskListItem')}>
|
|
156
|
+
<input
|
|
157
|
+
type="checkbox"
|
|
158
|
+
checked={checked}
|
|
159
|
+
readOnly
|
|
160
|
+
{...getStyledProps('taskCheckbox')}
|
|
161
|
+
/>
|
|
162
|
+
<span {...getStyledProps('listItemContent')}>{children}</span>
|
|
163
|
+
</li>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return <li {...getStyledProps('listItem')}>{children}</li>;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// Table
|
|
171
|
+
table: ({ children }: RendererProps) => (
|
|
172
|
+
<table {...getStyledProps('table')}>{children}</table>
|
|
173
|
+
),
|
|
174
|
+
thead: ({ children }: RendererProps) => (
|
|
175
|
+
<thead {...getStyledProps('tableHead')}>{children}</thead>
|
|
176
|
+
),
|
|
177
|
+
tbody: ({ children }: RendererProps) => <tbody>{children}</tbody>,
|
|
178
|
+
tr: ({ children }: RendererProps) => (
|
|
179
|
+
<tr {...getStyledProps('tableRow')}>{children}</tr>
|
|
180
|
+
),
|
|
181
|
+
th: ({ children, style: alignStyle }: RendererProps) => (
|
|
182
|
+
<th
|
|
183
|
+
{...getStyledProps('tableHeaderCell')}
|
|
184
|
+
style={alignStyle}
|
|
185
|
+
>
|
|
186
|
+
{children}
|
|
187
|
+
</th>
|
|
188
|
+
),
|
|
189
|
+
td: ({ children, style: alignStyle }: RendererProps) => (
|
|
190
|
+
<td
|
|
191
|
+
{...getStyledProps('tableCell')}
|
|
192
|
+
style={alignStyle}
|
|
193
|
+
>
|
|
194
|
+
{children}
|
|
195
|
+
</td>
|
|
196
|
+
),
|
|
197
|
+
|
|
198
|
+
// Image
|
|
199
|
+
img: ({ src, alt, title }: RendererProps) => {
|
|
200
|
+
const resolvedSrc = imageHandler?.resolveImageUrl
|
|
201
|
+
? imageHandler.resolveImageUrl(src || '')
|
|
202
|
+
: src;
|
|
203
|
+
|
|
204
|
+
const handleClick = () => {
|
|
205
|
+
if (imageHandler?.onImagePress) {
|
|
206
|
+
imageHandler.onImagePress(src || '', alt);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<img
|
|
212
|
+
{...getStyledProps('image')}
|
|
213
|
+
src={resolvedSrc}
|
|
214
|
+
alt={alt}
|
|
215
|
+
title={title}
|
|
216
|
+
onClick={imageHandler?.onImagePress ? handleClick : undefined}
|
|
217
|
+
style={
|
|
218
|
+
imageHandler?.onImagePress ? { cursor: 'pointer' } : undefined
|
|
219
|
+
}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// Horizontal rule
|
|
225
|
+
hr: () => <hr {...getStyledProps('hr')} />,
|
|
226
|
+
|
|
227
|
+
// Break
|
|
228
|
+
br: () => <br />,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for react-native-markdown-display
|
|
3
|
+
*
|
|
4
|
+
* @see https://github.com/iamacup/react-native-markdown-display
|
|
5
|
+
*/
|
|
6
|
+
declare module 'react-native-markdown-display' {
|
|
7
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
8
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
9
|
+
|
|
10
|
+
export interface MarkdownStyles {
|
|
11
|
+
body?: TextStyle;
|
|
12
|
+
heading1?: TextStyle;
|
|
13
|
+
heading2?: TextStyle;
|
|
14
|
+
heading3?: TextStyle;
|
|
15
|
+
heading4?: TextStyle;
|
|
16
|
+
heading5?: TextStyle;
|
|
17
|
+
heading6?: TextStyle;
|
|
18
|
+
hr?: ViewStyle;
|
|
19
|
+
strong?: TextStyle;
|
|
20
|
+
em?: TextStyle;
|
|
21
|
+
s?: TextStyle;
|
|
22
|
+
blockquote?: ViewStyle;
|
|
23
|
+
bullet_list?: ViewStyle;
|
|
24
|
+
ordered_list?: ViewStyle;
|
|
25
|
+
list_item?: ViewStyle;
|
|
26
|
+
bullet_list_icon?: TextStyle;
|
|
27
|
+
bullet_list_content?: ViewStyle;
|
|
28
|
+
ordered_list_icon?: TextStyle;
|
|
29
|
+
ordered_list_content?: ViewStyle;
|
|
30
|
+
code_inline?: TextStyle;
|
|
31
|
+
code_block?: ViewStyle;
|
|
32
|
+
fence?: ViewStyle;
|
|
33
|
+
table?: ViewStyle;
|
|
34
|
+
thead?: ViewStyle;
|
|
35
|
+
tbody?: ViewStyle;
|
|
36
|
+
th?: TextStyle;
|
|
37
|
+
tr?: ViewStyle;
|
|
38
|
+
td?: TextStyle;
|
|
39
|
+
link?: TextStyle;
|
|
40
|
+
blocklink?: ViewStyle;
|
|
41
|
+
image?: ViewStyle;
|
|
42
|
+
text?: TextStyle;
|
|
43
|
+
textgroup?: ViewStyle;
|
|
44
|
+
paragraph?: ViewStyle;
|
|
45
|
+
hardbreak?: ViewStyle;
|
|
46
|
+
softbreak?: ViewStyle;
|
|
47
|
+
pre?: ViewStyle;
|
|
48
|
+
inline?: ViewStyle;
|
|
49
|
+
span?: ViewStyle;
|
|
50
|
+
[key: string]: ViewStyle | TextStyle | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MarkdownProps {
|
|
54
|
+
children: string;
|
|
55
|
+
style?: MarkdownStyles;
|
|
56
|
+
rules?: Record<string, ComponentType<any>>;
|
|
57
|
+
onLinkPress?: (url: string) => boolean;
|
|
58
|
+
debugPrintTree?: boolean;
|
|
59
|
+
markdownit?: any;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const Markdown: ComponentType<MarkdownProps>;
|
|
63
|
+
export default Markdown;
|
|
64
|
+
}
|