@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,413 @@
1
+ /**
2
+ * Syntax highlighting module for the live preview editor.
3
+ * Converts raw markdown text into an array of semantic segments,
4
+ * preserving markdown delimiters as visible elements.
5
+ *
6
+ * Unlike the parser in markdownParser.ts (which produces an AST for rendering),
7
+ * this module produces flat {text, type, meta?} segments optimized to be
8
+ * rendered as children of a TextInput.
9
+ *
10
+ * Complexity: O(n) where n is text length.
11
+ */
12
+
13
+ import { findUnescapedToken, parseImageSourceAndTitle, isEscaped, isMarkdownFeatureEnabled, isHeadingLevelEnabled } from './markdownSyntaxUtils';
14
+ import type { HighlightSegmentType, HighlightSegment, InlineSegmentType, InlineContext, RawInlineSegment } from './markdownHighlight.types';
15
+ import type { MarkdownToolbarAction } from './markdownCore.types';
16
+
17
+ export type { HighlightSegmentType, HighlightSegment } from './markdownHighlight.types';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Regex patterns for block recognition
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const HEADING_RE = /^(#{1,6})\s+(.*)/;
24
+ const BLOCKQUOTE_RE = /^(>\s?)(.*)/;
25
+ const HORIZONTAL_RULE_RE = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/;
26
+ const UNORDERED_LIST_RE = /^(\s*[-*+]\s+)(.*)/;
27
+ const ORDERED_LIST_RE = /^(\s*\d+\.\s+)(.*)/;
28
+ const CODE_FENCE_RE = /^```/;
29
+
30
+ type AppendInlineOptions = {
31
+ baseTextType?: Extract<HighlightSegmentType, 'text' | 'heading' | 'quote'>;
32
+ meta?: Record<string, string>;
33
+ features?: MarkdownToolbarAction[];
34
+ };
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Public API
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Converts markdown text into an array of semantic segments.
42
+ *
43
+ * Parsing happens line by line:
44
+ * 1. Identifies block type (heading, list, quote, code block)
45
+ * 2. Tags block markers (e.g. #, >, -) with dedicated segment types
46
+ * 3. Analyzes inline content (bold, italic, code, link)
47
+ * 4. Produces semantic segments rendered via segment components
48
+ *
49
+ * @param markdown - Raw markdown text
50
+ * @param features - Optional list of enabled features. When provided,
51
+ * only the corresponding syntax is highlighted; disabled
52
+ * syntax is treated as plain text.
53
+ * @returns Array of semantic segments
54
+ */
55
+ export function highlightMarkdown(markdown: string, features?: MarkdownToolbarAction[]): HighlightSegment[] {
56
+ const lines = markdown.split('\n');
57
+ const segments: HighlightSegment[] = [];
58
+
59
+ let inCodeBlock = false;
60
+ let prevLineMeta: Record<string, string> | undefined;
61
+
62
+ for (let i = 0; i < lines.length; i++) {
63
+ if (i > 0) {
64
+ // Newline inherits previous heading context so Android renders
65
+ // the line break with the heading's larger lineHeight.
66
+ if (prevLineMeta?.lineContext === 'heading') {
67
+ segments.push({ text: '\n', type: 'heading', meta: prevLineMeta });
68
+ } else {
69
+ segments.push({ text: '\n', type: 'text' });
70
+ }
71
+ }
72
+ prevLineMeta = undefined;
73
+
74
+ const line = lines[i] ?? '';
75
+
76
+ // Code fence: open/close code blocks
77
+ if (isMarkdownFeatureEnabled(features, 'codeBlock') && CODE_FENCE_RE.test(line)) {
78
+ segments.push({
79
+ text: line,
80
+ type: 'delimiter',
81
+ meta: { lineContext: 'codeFence' },
82
+ });
83
+ inCodeBlock = !inCodeBlock;
84
+ continue;
85
+ }
86
+
87
+ // Inside a code block
88
+ if (inCodeBlock) {
89
+ segments.push({
90
+ text: line,
91
+ type: 'codeBlock',
92
+ });
93
+ continue;
94
+ }
95
+
96
+ // Heading: # Title
97
+ const headingMatch = line.match(HEADING_RE);
98
+ if (headingMatch && !isEscaped(line, 0) && isHeadingLevelEnabled(features, headingMatch[1]!.length)) {
99
+ const level = headingMatch[1]!.length;
100
+ const headingMeta = { lineContext: 'heading', headingLevel: String(level) };
101
+ prevLineMeta = headingMeta;
102
+ segments.push({
103
+ text: `${headingMatch[1]} `,
104
+ type: 'delimiter',
105
+ meta: headingMeta,
106
+ });
107
+ appendInlineSegments(segments, headingMatch[2] ?? '', {
108
+ baseTextType: 'heading',
109
+ meta: headingMeta,
110
+ features,
111
+ });
112
+ continue;
113
+ }
114
+
115
+ // Horizontal rule: ---, ***, ___
116
+ if (isMarkdownFeatureEnabled(features, 'divider') && HORIZONTAL_RULE_RE.test(line)) {
117
+ segments.push({
118
+ text: line,
119
+ type: 'horizontalRule',
120
+ });
121
+ continue;
122
+ }
123
+
124
+ // Blockquote: > quote
125
+ if (isMarkdownFeatureEnabled(features, 'quote')) {
126
+ const quoteMatch = line.match(BLOCKQUOTE_RE);
127
+ if (quoteMatch) {
128
+ const quoteMeta = { lineContext: 'quote' };
129
+ segments.push({
130
+ text: quoteMatch[1] ?? '',
131
+ type: 'quoteMarker',
132
+ meta: quoteMeta,
133
+ });
134
+ appendInlineSegments(segments, quoteMatch[2] ?? '', {
135
+ baseTextType: 'quote',
136
+ meta: quoteMeta,
137
+ features,
138
+ });
139
+ continue;
140
+ }
141
+ }
142
+
143
+ // Unordered list: - item
144
+ if (isMarkdownFeatureEnabled(features, 'unorderedList')) {
145
+ const ulMatch = line.match(UNORDERED_LIST_RE);
146
+ if (ulMatch) {
147
+ segments.push({
148
+ text: ulMatch[1] ?? '',
149
+ type: 'listMarker',
150
+ });
151
+ appendInlineSegments(segments, ulMatch[2] ?? '', { features });
152
+ continue;
153
+ }
154
+ }
155
+
156
+ // Ordered list: 1. item
157
+ if (isMarkdownFeatureEnabled(features, 'orderedList')) {
158
+ const olMatch = line.match(ORDERED_LIST_RE);
159
+ if (olMatch) {
160
+ segments.push({
161
+ text: olMatch[1] ?? '',
162
+ type: 'listMarker',
163
+ });
164
+ appendInlineSegments(segments, olMatch[2] ?? '', { features });
165
+ continue;
166
+ }
167
+ }
168
+
169
+ // Simple text line: inline parsing only
170
+ appendInlineSegments(segments, line, { features });
171
+ }
172
+
173
+ return segments;
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Segment composition functions
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /**
181
+ * Analyzes inline text and adds semantic segments to the array.
182
+ */
183
+ function appendInlineSegments(segments: HighlightSegment[], text: string, options: AppendInlineOptions = {}): void {
184
+ const inlineSegs = parseInlineHighlight(text, undefined, options.features);
185
+ for (const seg of inlineSegs) {
186
+ const type = resolveInlineSegmentType(seg, options.baseTextType ?? 'text');
187
+ const mergedMeta = options.meta || seg.meta ? { ...(options.meta ?? {}), ...(seg.meta ?? {}) } : undefined;
188
+ segments.push({
189
+ text: seg.text,
190
+ type,
191
+ ...(mergedMeta ? { meta: mergedMeta } : {}),
192
+ });
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Maps a raw inline segment to its public semantic HighlightSegmentType.
198
+ */
199
+ function resolveInlineSegmentType(seg: RawInlineSegment, baseTextType: Extract<HighlightSegmentType, 'text' | 'heading' | 'quote'>): HighlightSegmentType {
200
+ switch (seg.type) {
201
+ case 'delimiter':
202
+ return 'delimiter';
203
+ case 'code':
204
+ return 'code';
205
+ case 'link-label':
206
+ return 'link';
207
+ case 'link-url':
208
+ return 'linkUrl';
209
+ case 'image-alt':
210
+ return 'image';
211
+ case 'text':
212
+ default:
213
+ if (seg.bold) return 'bold';
214
+ if (seg.italic) return 'italic';
215
+ if (seg.strikethrough) return 'strikethrough';
216
+ return baseTextType;
217
+ }
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Inline parser
222
+ // ---------------------------------------------------------------------------
223
+
224
+ /**
225
+ * Inline parser that preserves markdown delimiters and tracks
226
+ * formatting context (bold, italic) recursively.
227
+ *
228
+ * Unlike parseMarkdownInline in markdownParser.ts:
229
+ * - Delimiters (**, _, `, etc.) are kept as 'delimiter' segments
230
+ * - Bold/italic context is propagated to nested levels
231
+ * - Produces flat segments (not a tree) for direct rendering
232
+ *
233
+ * @param content - Inline text to analyze
234
+ * @param context - Formatting context inherited from upper level
235
+ */
236
+ function parseInlineHighlight(content: string, context: InlineContext = { bold: false, italic: false, strikethrough: false }, features?: MarkdownToolbarAction[]): RawInlineSegment[] {
237
+ const segments: RawInlineSegment[] = [];
238
+ let cursor = 0;
239
+
240
+ /**
241
+ * Adds text to segments array, merging adjacent segments
242
+ * of same type to reduce number of Text nodes in rendering.
243
+ */
244
+ const appendText = (value: string, type: InlineSegmentType = 'text') => {
245
+ if (!value) return;
246
+ const last = segments[segments.length - 1];
247
+ // Merge only if type and formatting context match
248
+ if (last && last.type === type && last.bold === context.bold && last.italic === context.italic && last.strikethrough === context.strikethrough) {
249
+ last.text += value;
250
+ return;
251
+ }
252
+ segments.push({
253
+ text: value,
254
+ type,
255
+ bold: context.bold,
256
+ italic: context.italic,
257
+ strikethrough: context.strikethrough,
258
+ });
259
+ };
260
+
261
+ while (cursor < content.length) {
262
+ // Escape: \* → * literal
263
+ if (content[cursor] === '\\' && cursor + 1 < content.length) {
264
+ // Keep the backslash visible as a delimiter to preserve character count
265
+ // parity with the input value (required for correct cursor positioning).
266
+ segments.push({ text: '\\', type: 'delimiter', bold: false, italic: false, strikethrough: false });
267
+ appendText(content[cursor + 1] ?? '');
268
+ cursor += 2;
269
+ continue;
270
+ }
271
+
272
+ // Inline code: `code`
273
+ if (isMarkdownFeatureEnabled(features, 'code') && content[cursor] === '`') {
274
+ const closeIdx = findUnescapedToken(content, '`', cursor + 1);
275
+ if (closeIdx !== -1) {
276
+ // Backticks are delimiters, content is code
277
+ segments.push({ text: '`', type: 'delimiter', bold: false, italic: false, strikethrough: false });
278
+ segments.push({
279
+ text: content.slice(cursor + 1, closeIdx),
280
+ type: 'code',
281
+ bold: false,
282
+ italic: false,
283
+ strikethrough: false,
284
+ });
285
+ segments.push({ text: '`', type: 'delimiter', bold: false, italic: false, strikethrough: false });
286
+ cursor = closeIdx + 1;
287
+ continue;
288
+ }
289
+ }
290
+
291
+ // Bold: **text** or __text__
292
+ if (isMarkdownFeatureEnabled(features, 'bold') && (content.startsWith('**', cursor) || content.startsWith('__', cursor))) {
293
+ const marker = content.slice(cursor, cursor + 2);
294
+ const closeIdx = findUnescapedToken(content, marker, cursor + 2);
295
+ if (closeIdx !== -1) {
296
+ segments.push({ text: marker, type: 'delimiter', bold: false, italic: false, strikethrough: false });
297
+ // Recursion with bold context active: inner content
298
+ // inherits italic state from upper level
299
+ const inner = parseInlineHighlight(content.slice(cursor + 2, closeIdx), { ...context, bold: true }, features);
300
+ segments.push(...inner);
301
+ segments.push({ text: marker, type: 'delimiter', bold: false, italic: false, strikethrough: false });
302
+ cursor = closeIdx + 2;
303
+ continue;
304
+ }
305
+ }
306
+
307
+ // Strikethrough: ~~text~~
308
+ if (isMarkdownFeatureEnabled(features, 'strikethrough') && content.startsWith('~~', cursor)) {
309
+ const closeIdx = findUnescapedToken(content, '~~', cursor + 2);
310
+ if (closeIdx !== -1) {
311
+ segments.push({ text: '~~', type: 'delimiter', bold: false, italic: false, strikethrough: false });
312
+ const inner = parseInlineHighlight(content.slice(cursor + 2, closeIdx), { ...context, strikethrough: true }, features);
313
+ segments.push(...inner);
314
+ segments.push({ text: '~~', type: 'delimiter', bold: false, italic: false, strikethrough: false });
315
+ cursor = closeIdx + 2;
316
+ continue;
317
+ }
318
+ }
319
+
320
+ // Italic: *text* or _text_
321
+ if (isMarkdownFeatureEnabled(features, 'italic') && (content[cursor] === '*' || content[cursor] === '_')) {
322
+ const marker = content[cursor]!;
323
+ const closeIdx = findUnescapedToken(content, marker, cursor + 1);
324
+ if (closeIdx !== -1) {
325
+ segments.push({ text: marker, type: 'delimiter', bold: false, italic: false, strikethrough: false });
326
+ // Recursion with italic context active: inner content
327
+ // inherits bold state from upper level
328
+ const inner = parseInlineHighlight(content.slice(cursor + 1, closeIdx), { ...context, italic: true }, features);
329
+ segments.push(...inner);
330
+ segments.push({ text: marker, type: 'delimiter', bold: false, italic: false, strikethrough: false });
331
+ cursor = closeIdx + 1;
332
+ continue;
333
+ }
334
+ }
335
+
336
+ // Image: ![alt](url) or ![alt](url "title")
337
+ if (content[cursor] === '!' && content[cursor + 1] === '[') {
338
+ if (isMarkdownFeatureEnabled(features, 'image')) {
339
+ const altClose = findUnescapedToken(content, ']', cursor + 2);
340
+ if (altClose !== -1 && content[altClose + 1] === '(') {
341
+ const srcClose = findUnescapedToken(content, ')', altClose + 2);
342
+ if (srcClose !== -1) {
343
+ const fullText = content.slice(cursor, srcClose + 1);
344
+ const alt = content.slice(cursor + 2, altClose);
345
+ const rawUrl = content.slice(altClose + 2, srcClose).trim();
346
+ const { src, title } = parseImageSourceAndTitle(rawUrl, { unescapeSrc: false });
347
+ segments.push({
348
+ text: fullText,
349
+ type: 'image-alt',
350
+ bold: context.bold,
351
+ italic: context.italic,
352
+ strikethrough: context.strikethrough,
353
+ meta: { src, alt, ...(title ? { title } : {}) },
354
+ });
355
+ cursor = srcClose + 1;
356
+ continue;
357
+ }
358
+ }
359
+ } else {
360
+ // Image disabled: consume "!" as plain text and skip ahead so
361
+ // the link parser does not pick up "[alt](url)" as a link.
362
+ const altClose = findUnescapedToken(content, ']', cursor + 2);
363
+ if (altClose !== -1 && content[altClose + 1] === '(') {
364
+ const srcClose = findUnescapedToken(content, ')', altClose + 2);
365
+ if (srcClose !== -1) {
366
+ appendText(content.slice(cursor, srcClose + 1));
367
+ cursor = srcClose + 1;
368
+ continue;
369
+ }
370
+ }
371
+ appendText('!');
372
+ cursor += 1;
373
+ continue;
374
+ }
375
+ }
376
+
377
+ // Link: [label](url)
378
+ if (content[cursor] === '[') {
379
+ const labelClose = findUnescapedToken(content, ']', cursor + 1);
380
+ if (labelClose !== -1 && content[labelClose + 1] === '(') {
381
+ const hrefClose = findUnescapedToken(content, ')', labelClose + 2);
382
+ if (hrefClose !== -1) {
383
+ // Delimiters [] and () are tagged separately from label/url
384
+ segments.push({ text: '[', type: 'delimiter', bold: context.bold, italic: context.italic, strikethrough: context.strikethrough });
385
+ segments.push({
386
+ text: content.slice(cursor + 1, labelClose),
387
+ type: 'link-label',
388
+ bold: context.bold,
389
+ italic: context.italic,
390
+ strikethrough: context.strikethrough,
391
+ });
392
+ segments.push({ text: '](', type: 'delimiter', bold: context.bold, italic: context.italic, strikethrough: context.strikethrough });
393
+ segments.push({
394
+ text: content.slice(labelClose + 2, hrefClose),
395
+ type: 'link-url',
396
+ bold: context.bold,
397
+ italic: context.italic,
398
+ strikethrough: context.strikethrough,
399
+ });
400
+ segments.push({ text: ')', type: 'delimiter', bold: context.bold, italic: context.italic, strikethrough: context.strikethrough });
401
+ cursor = hrefClose + 1;
402
+ continue;
403
+ }
404
+ }
405
+ }
406
+
407
+ // Simple text character
408
+ appendText(content[cursor] ?? '');
409
+ cursor += 1;
410
+ }
411
+
412
+ return segments;
413
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Type definitions for the markdown syntax-highlighting module.
3
+ *
4
+ * Public types (`HighlightSegmentType`, `HighlightSegment`) are re-exported
5
+ * from `index.ts` for consumers who provide custom segment components.
6
+ * Internal types are used only within `markdownHighlight.ts`.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Public types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Semantic type of a highlight segment.
15
+ * Exposed to consumers so they can provide custom render components per type.
16
+ */
17
+ export type HighlightSegmentType =
18
+ | 'text'
19
+ | 'delimiter'
20
+ | 'heading'
21
+ | 'bold'
22
+ | 'italic'
23
+ | 'strikethrough'
24
+ | 'code'
25
+ | 'codeBlock'
26
+ | 'link'
27
+ | 'linkUrl'
28
+ | 'image'
29
+ | 'quote'
30
+ | 'quoteMarker'
31
+ | 'listMarker'
32
+ | 'horizontalRule';
33
+
34
+ /** Semantic highlight segment, ready for rendering via segment components. */
35
+ export type HighlightSegment = {
36
+ text: string;
37
+ type: HighlightSegmentType;
38
+ /** Optional metadata (e.g. image src, alt, title). */
39
+ meta?: Record<string, string>;
40
+ };
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Internal types (used only inside markdownHighlight.ts)
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Semantic type of an inline segment.
48
+ * Internally used to map inline tokens to public segment types.
49
+ */
50
+ export type InlineSegmentType = 'text' | 'delimiter' | 'code' | 'link-label' | 'link-url' | 'image-alt';
51
+
52
+ /**
53
+ * Inline formatting context, propagated recursively
54
+ * through nesting levels (e.g. bold inside italic).
55
+ */
56
+ export type InlineContext = {
57
+ bold: boolean;
58
+ italic: boolean;
59
+ strikethrough: boolean;
60
+ };
61
+
62
+ /**
63
+ * Intermediate segment produced by the inline parser.
64
+ * Contains both semantic type and formatting context,
65
+ * which will be combined to produce the final semantic segment.
66
+ */
67
+ export type RawInlineSegment = {
68
+ text: string;
69
+ type: InlineSegmentType;
70
+ bold: boolean;
71
+ italic: boolean;
72
+ strikethrough: boolean;
73
+ /** Optional metadata for image segments. */
74
+ meta?: Record<string, string>;
75
+ };