@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 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,3 @@
1
+ export { default } from './Markdown.native';
2
+ export * from './types';
3
+ export type { MarkdownDynamicProps, MarkdownVariants } from './Markdown.styles';
@@ -0,0 +1,6 @@
1
+ import MarkdownComponent from './Markdown.web';
2
+
3
+ export default MarkdownComponent;
4
+ export { MarkdownComponent as Markdown };
5
+ export * from './types';
6
+ export type { MarkdownDynamicProps, MarkdownVariants } from './Markdown.styles';
@@ -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
+ }