@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.
- package/.eslintrc.js +5 -0
- package/README.md +265 -0
- package/build/MarkdownRenderer.d.ts +12 -0
- package/build/MarkdownRenderer.d.ts.map +1 -0
- package/build/MarkdownRenderer.js +165 -0
- package/build/MarkdownRenderer.js.map +1 -0
- package/build/MarkdownTextInput.d.ts +10 -0
- package/build/MarkdownTextInput.d.ts.map +1 -0
- package/build/MarkdownTextInput.js +233 -0
- package/build/MarkdownTextInput.js.map +1 -0
- package/build/MarkdownToolbar.d.ts +11 -0
- package/build/MarkdownToolbar.d.ts.map +1 -0
- package/build/MarkdownToolbar.js +98 -0
- package/build/MarkdownToolbar.js.map +1 -0
- package/build/index.d.ts +14 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +11 -0
- package/build/index.js.map +1 -0
- package/build/markdownCore.types.d.ts +321 -0
- package/build/markdownCore.types.d.ts.map +1 -0
- package/build/markdownCore.types.js +2 -0
- package/build/markdownCore.types.js.map +1 -0
- package/build/markdownHighlight.d.ts +31 -0
- package/build/markdownHighlight.d.ts.map +1 -0
- package/build/markdownHighlight.js +378 -0
- package/build/markdownHighlight.js.map +1 -0
- package/build/markdownHighlight.types.d.ts +48 -0
- package/build/markdownHighlight.types.d.ts.map +1 -0
- package/build/markdownHighlight.types.js +9 -0
- package/build/markdownHighlight.types.js.map +1 -0
- package/build/markdownParser.d.ts +16 -0
- package/build/markdownParser.d.ts.map +1 -0
- package/build/markdownParser.js +309 -0
- package/build/markdownParser.js.map +1 -0
- package/build/markdownRendererDefaults.d.ts +113 -0
- package/build/markdownRendererDefaults.d.ts.map +1 -0
- package/build/markdownRendererDefaults.js +174 -0
- package/build/markdownRendererDefaults.js.map +1 -0
- package/build/markdownSegment.types.d.ts +22 -0
- package/build/markdownSegment.types.d.ts.map +1 -0
- package/build/markdownSegment.types.js +2 -0
- package/build/markdownSegment.types.js.map +1 -0
- package/build/markdownSegmentDefaults.d.ts +43 -0
- package/build/markdownSegmentDefaults.d.ts.map +1 -0
- package/build/markdownSegmentDefaults.js +176 -0
- package/build/markdownSegmentDefaults.js.map +1 -0
- package/build/markdownSyntaxUtils.d.ts +58 -0
- package/build/markdownSyntaxUtils.d.ts.map +1 -0
- package/build/markdownSyntaxUtils.js +98 -0
- package/build/markdownSyntaxUtils.js.map +1 -0
- package/build/markdownToolbarActions.d.ts +12 -0
- package/build/markdownToolbarActions.d.ts.map +1 -0
- package/build/markdownToolbarActions.js +212 -0
- package/build/markdownToolbarActions.js.map +1 -0
- package/build/useMarkdownEditor.d.ts +10 -0
- package/build/useMarkdownEditor.d.ts.map +1 -0
- package/build/useMarkdownEditor.js +219 -0
- package/build/useMarkdownEditor.js.map +1 -0
- package/jest.config.js +10 -0
- package/package.json +45 -0
- package/src/MarkdownRenderer.tsx +240 -0
- package/src/MarkdownTextInput.tsx +263 -0
- package/src/MarkdownToolbar.tsx +126 -0
- package/src/index.ts +31 -0
- package/src/markdownCore.types.ts +405 -0
- package/src/markdownHighlight.ts +413 -0
- package/src/markdownHighlight.types.ts +75 -0
- package/src/markdownParser.ts +345 -0
- package/src/markdownRendererDefaults.tsx +207 -0
- package/src/markdownSegment.types.ts +24 -0
- package/src/markdownSegmentDefaults.tsx +208 -0
- package/src/markdownSyntaxUtils.ts +139 -0
- package/src/markdownToolbarActions.ts +296 -0
- package/src/useMarkdownEditor.ts +265 -0
- package/tsconfig.json +9 -0
- 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:  or 
|
|
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
|
+
};
|