@domenico-esposito/react-native-markdown-editor 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +265 -0
  3. package/build/MarkdownRenderer.d.ts +12 -0
  4. package/build/MarkdownRenderer.d.ts.map +1 -0
  5. package/build/MarkdownRenderer.js +165 -0
  6. package/build/MarkdownRenderer.js.map +1 -0
  7. package/build/MarkdownTextInput.d.ts +10 -0
  8. package/build/MarkdownTextInput.d.ts.map +1 -0
  9. package/build/MarkdownTextInput.js +233 -0
  10. package/build/MarkdownTextInput.js.map +1 -0
  11. package/build/MarkdownToolbar.d.ts +11 -0
  12. package/build/MarkdownToolbar.d.ts.map +1 -0
  13. package/build/MarkdownToolbar.js +98 -0
  14. package/build/MarkdownToolbar.js.map +1 -0
  15. package/build/index.d.ts +14 -0
  16. package/build/index.d.ts.map +1 -0
  17. package/build/index.js +11 -0
  18. package/build/index.js.map +1 -0
  19. package/build/markdownCore.types.d.ts +321 -0
  20. package/build/markdownCore.types.d.ts.map +1 -0
  21. package/build/markdownCore.types.js +2 -0
  22. package/build/markdownCore.types.js.map +1 -0
  23. package/build/markdownHighlight.d.ts +31 -0
  24. package/build/markdownHighlight.d.ts.map +1 -0
  25. package/build/markdownHighlight.js +378 -0
  26. package/build/markdownHighlight.js.map +1 -0
  27. package/build/markdownHighlight.types.d.ts +48 -0
  28. package/build/markdownHighlight.types.d.ts.map +1 -0
  29. package/build/markdownHighlight.types.js +9 -0
  30. package/build/markdownHighlight.types.js.map +1 -0
  31. package/build/markdownParser.d.ts +16 -0
  32. package/build/markdownParser.d.ts.map +1 -0
  33. package/build/markdownParser.js +309 -0
  34. package/build/markdownParser.js.map +1 -0
  35. package/build/markdownRendererDefaults.d.ts +113 -0
  36. package/build/markdownRendererDefaults.d.ts.map +1 -0
  37. package/build/markdownRendererDefaults.js +174 -0
  38. package/build/markdownRendererDefaults.js.map +1 -0
  39. package/build/markdownSegment.types.d.ts +22 -0
  40. package/build/markdownSegment.types.d.ts.map +1 -0
  41. package/build/markdownSegment.types.js +2 -0
  42. package/build/markdownSegment.types.js.map +1 -0
  43. package/build/markdownSegmentDefaults.d.ts +43 -0
  44. package/build/markdownSegmentDefaults.d.ts.map +1 -0
  45. package/build/markdownSegmentDefaults.js +176 -0
  46. package/build/markdownSegmentDefaults.js.map +1 -0
  47. package/build/markdownSyntaxUtils.d.ts +58 -0
  48. package/build/markdownSyntaxUtils.d.ts.map +1 -0
  49. package/build/markdownSyntaxUtils.js +98 -0
  50. package/build/markdownSyntaxUtils.js.map +1 -0
  51. package/build/markdownToolbarActions.d.ts +12 -0
  52. package/build/markdownToolbarActions.d.ts.map +1 -0
  53. package/build/markdownToolbarActions.js +212 -0
  54. package/build/markdownToolbarActions.js.map +1 -0
  55. package/build/useMarkdownEditor.d.ts +10 -0
  56. package/build/useMarkdownEditor.d.ts.map +1 -0
  57. package/build/useMarkdownEditor.js +219 -0
  58. package/build/useMarkdownEditor.js.map +1 -0
  59. package/jest.config.js +10 -0
  60. package/package.json +45 -0
  61. package/src/MarkdownRenderer.tsx +240 -0
  62. package/src/MarkdownTextInput.tsx +263 -0
  63. package/src/MarkdownToolbar.tsx +126 -0
  64. package/src/index.ts +31 -0
  65. package/src/markdownCore.types.ts +405 -0
  66. package/src/markdownHighlight.ts +413 -0
  67. package/src/markdownHighlight.types.ts +75 -0
  68. package/src/markdownParser.ts +345 -0
  69. package/src/markdownRendererDefaults.tsx +207 -0
  70. package/src/markdownSegment.types.ts +24 -0
  71. package/src/markdownSegmentDefaults.tsx +208 -0
  72. package/src/markdownSyntaxUtils.ts +139 -0
  73. package/src/markdownToolbarActions.ts +296 -0
  74. package/src/useMarkdownEditor.ts +265 -0
  75. package/tsconfig.json +9 -0
  76. package/tsconfig.test.json +8 -0
@@ -0,0 +1,345 @@
1
+ import type { MarkdownBlockNode, MarkdownInlineNode, MarkdownToolbarAction } from './markdownCore.types';
2
+ import { findUnescapedToken, parseImageSourceAndTitle, unescapeMarkdown, isEscaped, isMarkdownFeatureEnabled, isHeadingLevelEnabled } from './markdownSyntaxUtils';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Regex patterns for block recognition
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const FENCE_PATTERN = /^```([\w-]+)?\s*$/;
9
+ const FENCE_CLOSE_PATTERN = /^```\s*$/;
10
+ const HEADING_PATTERN = /^(#{1,6})\s+(.+)$/;
11
+ const BLOCKQUOTE_PATTERN = /^>\s?(.*)$/;
12
+ const HORIZONTAL_RULE_PATTERN = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/;
13
+ const UNORDERED_LIST_PATTERN = /^\s*[-*+]\s+(.*)$/;
14
+ const ORDERED_LIST_PATTERN = /^\s*\d+\.\s+(.*)$/;
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Public API
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Parses markdown text into block nodes.
22
+ *
23
+ * Parsing is line-oriented: block-level constructs are identified first,
24
+ * then text fragments are delegated to the inline parser.
25
+ */
26
+ export function parseMarkdown(markdown: string, features?: MarkdownToolbarAction[]): MarkdownBlockNode[] {
27
+ const lines = markdown.replace(/\r\n?/g, '\n').split('\n');
28
+ const blocks: MarkdownBlockNode[] = [];
29
+ let cursor = 0;
30
+
31
+ while (cursor < lines.length) {
32
+ const line = lines[cursor] ?? '';
33
+
34
+ if (line.trim().length === 0) {
35
+ blocks.push({ type: 'spacer' });
36
+ cursor += 1;
37
+ continue;
38
+ }
39
+
40
+ // Fenced code block: ```lang ... ```
41
+ if (isMarkdownFeatureEnabled(features, 'codeBlock')) {
42
+ const fenceMatch = line.match(FENCE_PATTERN);
43
+ if (fenceMatch) {
44
+ const closingFenceIndex = findClosingFence(lines, cursor + 1);
45
+ if (closingFenceIndex !== -1) {
46
+ blocks.push({
47
+ type: 'codeBlock',
48
+ language: fenceMatch[1],
49
+ content: lines.slice(cursor + 1, closingFenceIndex).join('\n'),
50
+ });
51
+ cursor = closingFenceIndex + 1;
52
+ continue;
53
+ }
54
+ }
55
+ }
56
+
57
+ // Heading: # ... ######
58
+ const headingMatch = line.match(HEADING_PATTERN);
59
+ if (headingMatch && !isEscaped(line, 0) && isHeadingLevelEnabled(features, headingMatch[1].length)) {
60
+ const level = headingMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6;
61
+ blocks.push({
62
+ type: 'heading',
63
+ level,
64
+ children: parseMarkdownInline(headingMatch[2], features),
65
+ });
66
+ cursor += 1;
67
+ continue;
68
+ }
69
+
70
+ // Horizontal rule: ---, ***, ___
71
+ if (isMarkdownFeatureEnabled(features, 'divider') && HORIZONTAL_RULE_PATTERN.test(line)) {
72
+ blocks.push({ type: 'horizontalRule' });
73
+ cursor += 1;
74
+ continue;
75
+ }
76
+
77
+ // Blockquote: > quote
78
+ if (isMarkdownFeatureEnabled(features, 'quote') && BLOCKQUOTE_PATTERN.test(line)) {
79
+ const quotedLines: string[] = [];
80
+ while (cursor < lines.length) {
81
+ const quoteMatch = lines[cursor]?.match(BLOCKQUOTE_PATTERN);
82
+ if (!quoteMatch) {
83
+ break;
84
+ }
85
+ quotedLines.push(quoteMatch[1]);
86
+ cursor += 1;
87
+ }
88
+ blocks.push({
89
+ type: 'blockquote',
90
+ children: parseMarkdownInline(quotedLines.join('\n'), features),
91
+ });
92
+ continue;
93
+ }
94
+
95
+ // List: bullet (-, *, +) or ordered (1.)
96
+ if (
97
+ (isMarkdownFeatureEnabled(features, 'unorderedList') && UNORDERED_LIST_PATTERN.test(line)) ||
98
+ (isMarkdownFeatureEnabled(features, 'orderedList') && ORDERED_LIST_PATTERN.test(line))
99
+ ) {
100
+ const ordered = ORDERED_LIST_PATTERN.test(line);
101
+ const itemPattern = ordered ? ORDERED_LIST_PATTERN : UNORDERED_LIST_PATTERN;
102
+ const items: MarkdownInlineNode[][] = [];
103
+
104
+ while (cursor < lines.length) {
105
+ const itemMatch = lines[cursor]?.match(itemPattern);
106
+ if (!itemMatch) {
107
+ break;
108
+ }
109
+ items.push(parseMarkdownInline(itemMatch[1], features));
110
+ cursor += 1;
111
+ }
112
+
113
+ blocks.push({
114
+ type: 'list',
115
+ ordered,
116
+ items,
117
+ });
118
+ continue;
119
+ }
120
+
121
+ // Paragraph: consecutive non-empty lines until next block start
122
+ const paragraphLines: string[] = [];
123
+ while (cursor < lines.length) {
124
+ const currentLine = lines[cursor] ?? '';
125
+ if (currentLine.trim().length === 0 || isBlockStart(lines, cursor, features)) {
126
+ break;
127
+ }
128
+ paragraphLines.push(currentLine);
129
+ cursor += 1;
130
+ }
131
+
132
+ if (paragraphLines.length > 0) {
133
+ blocks.push({
134
+ type: 'paragraph',
135
+ children: parseMarkdownInline(paragraphLines.join('\n'), features),
136
+ });
137
+ continue;
138
+ }
139
+
140
+ cursor += 1;
141
+ }
142
+
143
+ return blocks;
144
+ }
145
+
146
+ /**
147
+ * Parses inline markdown tokens inside a block.
148
+ *
149
+ * The parser scans left-to-right and uses recursion for nested emphasis
150
+ * and link labels.
151
+ */
152
+ export function parseMarkdownInline(content: string, features?: MarkdownToolbarAction[]): MarkdownInlineNode[] {
153
+ const nodes: MarkdownInlineNode[] = [];
154
+ let cursor = 0;
155
+
156
+ const appendText = (value: string) => {
157
+ if (!value) {
158
+ return;
159
+ }
160
+ const lastNode = nodes[nodes.length - 1];
161
+ if (lastNode?.type === 'text') {
162
+ lastNode.content += value;
163
+ return;
164
+ }
165
+ // Keep adjacent plain text in a single node to reduce fragmentation.
166
+ nodes.push({ type: 'text', content: value });
167
+ };
168
+
169
+ while (cursor < content.length) {
170
+ // Escape: \* -> literal *
171
+ if (content[cursor] === '\\' && cursor + 1 < content.length) {
172
+ appendText(content[cursor + 1] ?? '');
173
+ cursor += 2;
174
+ continue;
175
+ }
176
+
177
+ // Inline code: `code`
178
+ if (isMarkdownFeatureEnabled(features, 'code') && content[cursor] === '`') {
179
+ const closingIndex = findUnescapedToken(content, '`', cursor + 1);
180
+ if (closingIndex !== -1) {
181
+ nodes.push({
182
+ type: 'code',
183
+ content: unescapeMarkdown(content.slice(cursor + 1, closingIndex)),
184
+ });
185
+ cursor = closingIndex + 1;
186
+ continue;
187
+ }
188
+ }
189
+
190
+ // Bold: **text** or __text__
191
+ if (isMarkdownFeatureEnabled(features, 'bold') && (content.startsWith('**', cursor) || content.startsWith('__', cursor))) {
192
+ const marker = content.slice(cursor, cursor + 2);
193
+ const closingIndex = findUnescapedToken(content, marker, cursor + marker.length);
194
+ if (closingIndex !== -1) {
195
+ nodes.push({
196
+ type: 'bold',
197
+ children: parseMarkdownInline(content.slice(cursor + marker.length, closingIndex), features),
198
+ });
199
+ cursor = closingIndex + marker.length;
200
+ continue;
201
+ }
202
+ }
203
+
204
+ // Strikethrough: ~~text~~
205
+ if (isMarkdownFeatureEnabled(features, 'strikethrough') && content.startsWith('~~', cursor)) {
206
+ const closingIndex = findUnescapedToken(content, '~~', cursor + 2);
207
+ if (closingIndex !== -1) {
208
+ nodes.push({
209
+ type: 'strikethrough',
210
+ children: parseMarkdownInline(content.slice(cursor + 2, closingIndex), features),
211
+ });
212
+ cursor = closingIndex + 2;
213
+ continue;
214
+ }
215
+ }
216
+
217
+ // Italic: *text* or _text_
218
+ if (isMarkdownFeatureEnabled(features, 'italic') && (content[cursor] === '*' || content[cursor] === '_')) {
219
+ const marker = content[cursor];
220
+ const closingIndex = findUnescapedToken(content, marker, cursor + 1);
221
+ if (closingIndex !== -1) {
222
+ nodes.push({
223
+ type: 'italic',
224
+ children: parseMarkdownInline(content.slice(cursor + 1, closingIndex), features),
225
+ });
226
+ cursor = closingIndex + 1;
227
+ continue;
228
+ }
229
+ }
230
+
231
+ // Image: ![alt](url) or ![alt](url "title")
232
+ if (content[cursor] === '!' && content[cursor + 1] === '[') {
233
+ if (isMarkdownFeatureEnabled(features, 'image')) {
234
+ const altClosingIndex = findUnescapedToken(content, ']', cursor + 2);
235
+ const srcOpenIndex = altClosingIndex + 1;
236
+
237
+ if (altClosingIndex !== -1 && content[srcOpenIndex] === '(') {
238
+ const srcClosingIndex = findUnescapedToken(content, ')', srcOpenIndex + 1);
239
+ if (srcClosingIndex !== -1) {
240
+ const alt = unescapeMarkdown(content.slice(cursor + 2, altClosingIndex));
241
+ const raw = content.slice(srcOpenIndex + 1, srcClosingIndex);
242
+ const { src, title } = parseImageSourceAndTitle(raw);
243
+ nodes.push({ type: 'image', src, alt, title });
244
+ cursor = srcClosingIndex + 1;
245
+ continue;
246
+ }
247
+ }
248
+ } else {
249
+ // Image disabled: consume the whole "![alt](url)" as plain text
250
+ // so the link parser does not pick up "[alt](url)" as a link.
251
+ const altClosingIndex = findUnescapedToken(content, ']', cursor + 2);
252
+ if (altClosingIndex !== -1 && content[altClosingIndex + 1] === '(') {
253
+ const srcClosingIndex = findUnescapedToken(content, ')', altClosingIndex + 2);
254
+ if (srcClosingIndex !== -1) {
255
+ appendText(content.slice(cursor, srcClosingIndex + 1));
256
+ cursor = srcClosingIndex + 1;
257
+ continue;
258
+ }
259
+ }
260
+ appendText('!');
261
+ cursor += 1;
262
+ continue;
263
+ }
264
+ }
265
+
266
+ // Link: [label](url)
267
+ if (content[cursor] === '[') {
268
+ const labelClosingIndex = findUnescapedToken(content, ']', cursor + 1);
269
+ const linkOpenIndex = labelClosingIndex + 1;
270
+
271
+ if (labelClosingIndex !== -1 && content[linkOpenIndex] === '(') {
272
+ const hrefClosingIndex = findUnescapedToken(content, ')', linkOpenIndex + 1);
273
+ if (hrefClosingIndex !== -1) {
274
+ nodes.push({
275
+ type: 'link',
276
+ href: unescapeMarkdown(content.slice(linkOpenIndex + 1, hrefClosingIndex)),
277
+ children: parseMarkdownInline(content.slice(cursor + 1, labelClosingIndex), features),
278
+ });
279
+ cursor = hrefClosingIndex + 1;
280
+ continue;
281
+ }
282
+ }
283
+ }
284
+
285
+ // Simple text character.
286
+ appendText(content[cursor] ?? '');
287
+ cursor += 1;
288
+ }
289
+
290
+ return nodes;
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Internal helpers
295
+ // ---------------------------------------------------------------------------
296
+
297
+ /**
298
+ * Determines whether the line at `index` begins a new block-level construct
299
+ * (heading, rule, quote, list, or fenced code block).
300
+ *
301
+ * Used by the paragraph collector to know when to stop consuming lines.
302
+ */
303
+ function isBlockStart(lines: string[], index: number, features?: MarkdownToolbarAction[]): boolean {
304
+ const line = lines[index] ?? '';
305
+ if (line.trim().length === 0) {
306
+ return false;
307
+ }
308
+ if (isMarkdownFeatureEnabled(features, 'heading') && HEADING_PATTERN.test(line)) {
309
+ const m = line.match(HEADING_PATTERN);
310
+ if (m && isHeadingLevelEnabled(features, m[1].length)) {
311
+ return true;
312
+ }
313
+ }
314
+ if (isMarkdownFeatureEnabled(features, 'divider') && HORIZONTAL_RULE_PATTERN.test(line)) {
315
+ return true;
316
+ }
317
+ if (isMarkdownFeatureEnabled(features, 'quote') && BLOCKQUOTE_PATTERN.test(line)) {
318
+ return true;
319
+ }
320
+ if (
321
+ (isMarkdownFeatureEnabled(features, 'unorderedList') && UNORDERED_LIST_PATTERN.test(line)) ||
322
+ (isMarkdownFeatureEnabled(features, 'orderedList') && ORDERED_LIST_PATTERN.test(line))
323
+ ) {
324
+ return true;
325
+ }
326
+ if (isMarkdownFeatureEnabled(features, 'codeBlock') && FENCE_PATTERN.test(line)) {
327
+ // Avoid treating an unmatched opening fence as a block boundary.
328
+ return findClosingFence(lines, index + 1) !== -1;
329
+ }
330
+ return false;
331
+ }
332
+
333
+ /**
334
+ * Scans forward from `startIndex` looking for a closing code fence (` ``` `).
335
+ *
336
+ * @returns The line index of the closing fence, or `-1` if none is found.
337
+ */
338
+ function findClosingFence(lines: string[], startIndex: number): number {
339
+ for (let index = startIndex; index < lines.length; index += 1) {
340
+ if (FENCE_CLOSE_PATTERN.test(lines[index] ?? '')) {
341
+ return index;
342
+ }
343
+ }
344
+ return -1;
345
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Default component implementations and styles for {@link MarkdownRenderer}.
3
+ *
4
+ * Each markdown tag has a corresponding default component that provides
5
+ * basic visual styling. Consumers can override any subset via the
6
+ * `components` prop on `MarkdownRenderer`.
7
+ */
8
+
9
+ import * as React from 'react';
10
+ import { Image, StyleSheet, Text, View } from 'react-native';
11
+
12
+ import type { MarkdownComponentMap } from './markdownCore.types';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Default components
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const DefaultRoot: MarkdownComponentMap['root'] = ({ children, style }) => <View style={[styles.root, style]}>{children}</View>;
19
+
20
+ const DefaultParagraph: MarkdownComponentMap['paragraph'] = ({ children }) => <Text style={styles.paragraph}>{children}</Text>;
21
+
22
+ const DefaultText: MarkdownComponentMap['text'] = ({ children }) => <Text>{children}</Text>;
23
+
24
+ const DefaultHeading1: MarkdownComponentMap['heading1'] = ({ children }) => <Text style={styles.heading1}>{children}</Text>;
25
+
26
+ const DefaultHeading2: MarkdownComponentMap['heading2'] = ({ children }) => <Text style={styles.heading2}>{children}</Text>;
27
+
28
+ const DefaultHeading3: MarkdownComponentMap['heading3'] = ({ children }) => <Text style={styles.heading3}>{children}</Text>;
29
+
30
+ const DefaultHeading4: MarkdownComponentMap['heading4'] = ({ children }) => <Text style={styles.heading4}>{children}</Text>;
31
+
32
+ const DefaultHeading5: MarkdownComponentMap['heading5'] = ({ children }) => <Text style={styles.heading5}>{children}</Text>;
33
+
34
+ const DefaultHeading6: MarkdownComponentMap['heading6'] = ({ children }) => <Text style={styles.heading6}>{children}</Text>;
35
+
36
+ const DefaultBold: MarkdownComponentMap['bold'] = ({ children }) => <Text style={styles.bold}>{children}</Text>;
37
+
38
+ const DefaultItalic: MarkdownComponentMap['italic'] = ({ children }) => <Text style={styles.italic}>{children}</Text>;
39
+
40
+ const DefaultStrikethrough: MarkdownComponentMap['strikethrough'] = ({ children }) => <Text style={styles.strikethrough}>{children}</Text>;
41
+
42
+ const DefaultInlineCode: MarkdownComponentMap['inlineCode'] = ({ children }) => <Text style={styles.inlineCode}>{children}</Text>;
43
+
44
+ const DefaultCodeBlock: MarkdownComponentMap['codeBlock'] = ({ text, language }) => (
45
+ <View style={styles.codeBlock}>
46
+ <Text style={styles.codeBlockLanguage}>{language?.trim() || 'Code'}</Text>
47
+ <Text style={styles.codeBlockText}>{text}</Text>
48
+ </View>
49
+ );
50
+
51
+ const DefaultBlockquote: MarkdownComponentMap['blockquote'] = ({ children }) => <View style={styles.blockquote}>{children}</View>;
52
+
53
+ const DefaultHorizontalRule: MarkdownComponentMap['horizontalRule'] = () => <View style={styles.horizontalRule} />;
54
+
55
+ const DefaultSpacer: MarkdownComponentMap['spacer'] = () => <View style={styles.spacer} />;
56
+
57
+ const DefaultUnorderedList: MarkdownComponentMap['unorderedList'] = ({ children }) => <View style={styles.list}>{children}</View>;
58
+
59
+ const DefaultOrderedList: MarkdownComponentMap['orderedList'] = ({ children }) => <View style={styles.list}>{children}</View>;
60
+
61
+ const DefaultListItem: MarkdownComponentMap['listItem'] = ({ children }) => <View style={styles.listItem}>{children}</View>;
62
+
63
+ const DefaultLink: MarkdownComponentMap['link'] = ({ children }) => <Text style={styles.link}>{children}</Text>;
64
+
65
+ const DefaultImage: MarkdownComponentMap['image'] = ({ src, alt, title }) => (
66
+ <View style={styles.imageContainer}>
67
+ <Image source={{ uri: src }} style={styles.image} resizeMode="contain" accessibilityLabel={alt ?? title} />
68
+ {alt ? <Text style={styles.imageAlt}>{alt}</Text> : null}
69
+ </View>
70
+ );
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Component map
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /** Complete map of default markdown rendering components. */
77
+ export const DEFAULT_COMPONENTS: MarkdownComponentMap = {
78
+ root: DefaultRoot,
79
+ paragraph: DefaultParagraph,
80
+ text: DefaultText,
81
+ heading1: DefaultHeading1,
82
+ heading2: DefaultHeading2,
83
+ heading3: DefaultHeading3,
84
+ heading4: DefaultHeading4,
85
+ heading5: DefaultHeading5,
86
+ heading6: DefaultHeading6,
87
+ bold: DefaultBold,
88
+ italic: DefaultItalic,
89
+ strikethrough: DefaultStrikethrough,
90
+ inlineCode: DefaultInlineCode,
91
+ codeBlock: DefaultCodeBlock,
92
+ blockquote: DefaultBlockquote,
93
+ horizontalRule: DefaultHorizontalRule,
94
+ unorderedList: DefaultUnorderedList,
95
+ orderedList: DefaultOrderedList,
96
+ listItem: DefaultListItem,
97
+ link: DefaultLink,
98
+ image: DefaultImage,
99
+ spacer: DefaultSpacer,
100
+ };
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Styles
104
+ // ---------------------------------------------------------------------------
105
+
106
+ export const styles = StyleSheet.create({
107
+ root: {
108
+ gap: 8,
109
+ },
110
+ paragraph: {
111
+ color: '#1f1f1f',
112
+ lineHeight: 22,
113
+ },
114
+ heading1: {
115
+ fontSize: 30,
116
+ fontWeight: '700',
117
+ },
118
+ heading2: {
119
+ fontSize: 26,
120
+ fontWeight: '700',
121
+ },
122
+ heading3: {
123
+ fontSize: 22,
124
+ fontWeight: '700',
125
+ },
126
+ heading4: {
127
+ fontSize: 20,
128
+ fontWeight: '700',
129
+ },
130
+ heading5: {
131
+ fontSize: 18,
132
+ fontWeight: '700',
133
+ },
134
+ heading6: {
135
+ fontSize: 16,
136
+ fontWeight: '700',
137
+ },
138
+ bold: {
139
+ fontWeight: '700',
140
+ },
141
+ italic: {
142
+ fontStyle: 'italic',
143
+ },
144
+ strikethrough: {
145
+ textDecorationLine: 'line-through' as const,
146
+ },
147
+ inlineCode: {
148
+ fontFamily: 'Courier',
149
+ backgroundColor: '#f1f1f1',
150
+ paddingHorizontal: 4,
151
+ borderRadius: 4,
152
+ },
153
+ codeBlock: {
154
+ backgroundColor: '#f1f1f1',
155
+ borderRadius: 8,
156
+ padding: 12,
157
+ },
158
+ codeBlockLanguage: {
159
+ alignSelf: 'flex-start',
160
+ fontSize: 11,
161
+ fontWeight: '700',
162
+ backgroundColor: '#e2e2e2',
163
+ borderRadius: 4,
164
+ paddingHorizontal: 6,
165
+ paddingVertical: 2,
166
+ marginBottom: 8,
167
+ },
168
+ codeBlockText: {
169
+ fontFamily: 'Courier',
170
+ },
171
+ blockquote: {
172
+ borderLeftWidth: 3,
173
+ borderLeftColor: '#acacac',
174
+ paddingLeft: 10,
175
+ },
176
+ horizontalRule: {
177
+ borderBottomWidth: 1,
178
+ borderBottomColor: '#d0d0d0',
179
+ marginVertical: 8,
180
+ },
181
+ list: {
182
+ gap: 4,
183
+ },
184
+ listItem: {
185
+ flexDirection: 'row',
186
+ },
187
+ link: {
188
+ color: '#2d5eff',
189
+ textDecorationLine: 'underline',
190
+ },
191
+ image: {
192
+ width: '100%',
193
+ height: 200,
194
+ },
195
+ imageContainer: {
196
+ alignItems: 'center',
197
+ },
198
+ imageAlt: {
199
+ fontSize: 12,
200
+ color: '#6b6b6b',
201
+ marginTop: 4,
202
+ textAlign: 'center',
203
+ },
204
+ spacer: {
205
+ height: 8,
206
+ },
207
+ });
@@ -0,0 +1,24 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+
3
+ import type { HighlightSegmentType } from './markdownHighlight.types';
4
+
5
+ /**
6
+ * Props received by every custom segment component.
7
+ * The component MUST render a Text (or a component that inherits from Text),
8
+ * since segments are children of a TextInput.
9
+ */
10
+ export type SegmentComponentProps = {
11
+ /** Semantic type of the segment (e.g. 'bold', 'heading', 'code'). */
12
+ type: HighlightSegmentType;
13
+ /** Optional metadata emitted by the highlighter (e.g. heading level, image info). */
14
+ meta?: Record<string, string>;
15
+ /** The segment text content. */
16
+ children: ReactNode;
17
+ };
18
+
19
+ /**
20
+ * Map of custom components keyed by segment type.
21
+ * Only the types you want to override need to be specified;
22
+ * unspecified types fall back to defaults.
23
+ */
24
+ export type SegmentComponentMap = Partial<Record<HighlightSegmentType, ComponentType<SegmentComponentProps>>>;