@copilotkit/markdown 0.0.1 → 2.0.0-rc.12

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/dist/index.cjs ADDED
@@ -0,0 +1,2023 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/parser/helpers.ts
4
+ /**
5
+ * Default parser options for streaming markdown.
6
+ */
7
+ const DEFAULT_OPTIONS = {
8
+ segmenter: true,
9
+ enableTables: true,
10
+ enableAutolinks: true
11
+ };
12
+ /**
13
+ * Merges user-provided parser options with defaults.
14
+ *
15
+ * @param options - Optional partial parser options.
16
+ * @returns Fully populated parser options.
17
+ */
18
+ function normalizeOptions(options) {
19
+ return {
20
+ segmenter: options?.segmenter ?? DEFAULT_OPTIONS.segmenter,
21
+ enableTables: options?.enableTables ?? DEFAULT_OPTIONS.enableTables,
22
+ enableAutolinks: options?.enableAutolinks ?? DEFAULT_OPTIONS.enableAutolinks
23
+ };
24
+ }
25
+ /**
26
+ * Normalizes streaming input chunks by handling trailing carriage-return boundaries.
27
+ *
28
+ * @param chunk - Incoming raw chunk.
29
+ * @param hadPendingCR - Whether the previous chunk ended with `\\r`.
30
+ * @returns Normalized text and whether this chunk now ends with a pending `\\r`.
31
+ */
32
+ function normalizeChunk(chunk, hadPendingCR) {
33
+ const input = (hadPendingCR ? "\r" : "") + chunk;
34
+ const pendingCarriageReturn = input.endsWith("\r");
35
+ return {
36
+ text: (pendingCarriageReturn ? input.slice(0, -1) : input).replace(/\r\n?/g, "\n"),
37
+ pendingCarriageReturn
38
+ };
39
+ }
40
+ /**
41
+ * Splits normalized source text into lines with source offsets.
42
+ *
43
+ * @param source - Normalized source text.
44
+ * @returns Line descriptors preserving absolute ranges.
45
+ */
46
+ function splitLines(source) {
47
+ const lines = [];
48
+ let start = 0;
49
+ for (let i = 0; i < source.length; i += 1) {
50
+ if (source[i] !== "\n") continue;
51
+ lines.push({
52
+ text: source.slice(start, i),
53
+ start,
54
+ end: i + 1,
55
+ hasNewline: true
56
+ });
57
+ start = i + 1;
58
+ }
59
+ if (start < source.length || source.length === 0) lines.push({
60
+ text: source.slice(start),
61
+ start,
62
+ end: source.length,
63
+ hasNewline: false
64
+ });
65
+ return lines;
66
+ }
67
+ /**
68
+ * Returns the trailing unterminated line text.
69
+ *
70
+ * @param source - Full source text.
71
+ * @returns Last line content after the final newline.
72
+ */
73
+ function getLineBuffer(source) {
74
+ const newline = source.lastIndexOf("\n");
75
+ return newline < 0 ? source : source.slice(newline + 1);
76
+ }
77
+ /**
78
+ * Converts an absolute source index to 1-based line/column.
79
+ *
80
+ * @param source - Source text.
81
+ * @param index - Absolute index.
82
+ * @returns 1-based line and column coordinates.
83
+ */
84
+ function toLineColumn(source, index) {
85
+ let line = 1;
86
+ let column = 1;
87
+ for (let i = 0; i < index; i += 1) if (source[i] === "\n") {
88
+ line += 1;
89
+ column = 1;
90
+ } else column += 1;
91
+ return {
92
+ line,
93
+ column
94
+ };
95
+ }
96
+ /**
97
+ * Returns true for blank lines.
98
+ */
99
+ function isBlank(value) {
100
+ return /^\s*$/.test(value);
101
+ }
102
+ /**
103
+ * Returns true when a line is a citation definition.
104
+ */
105
+ function isCitationDefinitionLine(value) {
106
+ return /^\s{0,3}\[\^([^\]\s]+)\]:\s*(.*)$/.test(value);
107
+ }
108
+ /**
109
+ * Returns true when a line is an unfinished citation-definition prefix candidate.
110
+ *
111
+ * This enables optimistic streaming behavior for definition lines that begin with
112
+ * `[^` but are not yet complete enough to classify as either:
113
+ * 1) a valid citation definition, or
114
+ * 2) plain paragraph text.
115
+ *
116
+ * @param value - Raw line text.
117
+ * @param hasNewline - Whether the source line is newline-terminated.
118
+ * @param isComplete - Whether the parser has been finalized.
119
+ */
120
+ function isCitationDefinitionPrefixCandidate(value, hasNewline, isComplete) {
121
+ if (hasNewline || isComplete || isCitationDefinitionLine(value)) return false;
122
+ return /^\s{0,3}\[\^[^\]\s]*$/.test(value) || /^\s{0,3}\[\^[^\]\s]+\]$/.test(value);
123
+ }
124
+ /**
125
+ * Matches an opening fenced code block marker.
126
+ */
127
+ function matchFenceOpen(value) {
128
+ const trimmed = value.trimStart();
129
+ const backtick = /^(`{3,})(.*)$/.exec(trimmed);
130
+ if (backtick) return {
131
+ marker: "```",
132
+ length: backtick[1].length,
133
+ info: backtick[2].trim()
134
+ };
135
+ const tilde = /^(~{3,})(.*)$/.exec(trimmed);
136
+ if (tilde) return {
137
+ marker: "~~~",
138
+ length: tilde[1].length,
139
+ info: tilde[2].trim()
140
+ };
141
+ return null;
142
+ }
143
+ /**
144
+ * Matches a valid fenced code block closing line.
145
+ */
146
+ function matchFenceClose(value, marker, length) {
147
+ if (!(marker === "```" ? /^\s*`{3,}\s*$/ : /^\s*~{3,}\s*$/).test(value)) return false;
148
+ return value.trim().length >= length;
149
+ }
150
+ /**
151
+ * Matches ATX heading syntax and extracts heading metadata.
152
+ */
153
+ function matchAtxHeading(value) {
154
+ const match = /^\s{0,3}(#{1,6})[ \t]+(.+?)\s*#*\s*$/.exec(value);
155
+ if (!match) return null;
156
+ return {
157
+ level: match[1].length,
158
+ text: match[2],
159
+ contentOffset: value.indexOf(match[2])
160
+ };
161
+ }
162
+ /**
163
+ * Returns true for setext heading underline lines.
164
+ */
165
+ function isSetextUnderline(value) {
166
+ return /^\s{0,3}(=+|-+)\s*$/.test(value);
167
+ }
168
+ /**
169
+ * Returns true for thematic break syntax.
170
+ */
171
+ function isThematicBreak(value) {
172
+ return /^\s{0,3}((\*\s*){3,}|(-\s*){3,}|(_\s*){3,})$/.test(value);
173
+ }
174
+ /**
175
+ * Matches ordered and unordered list-item markers.
176
+ */
177
+ function matchListItem(value) {
178
+ const unordered = /^(\s{0,3})([-+*])(\s+)(.*)$/.exec(value);
179
+ if (unordered) {
180
+ const indent = unordered[1].length;
181
+ const contentIndent = indent + unordered[2].length + unordered[3].length;
182
+ return {
183
+ ordered: false,
184
+ start: 1,
185
+ content: unordered[4],
186
+ indent,
187
+ contentIndent
188
+ };
189
+ }
190
+ const ordered = /^(\s{0,3})(\d{1,9})([.)])(\s+)(.*)$/.exec(value);
191
+ if (ordered) {
192
+ const indent = ordered[1].length;
193
+ const contentIndent = indent + (ordered[2].length + ordered[3].length) + ordered[4].length;
194
+ return {
195
+ ordered: true,
196
+ start: Number(ordered[2]),
197
+ content: ordered[5],
198
+ indent,
199
+ contentIndent
200
+ };
201
+ }
202
+ return null;
203
+ }
204
+ /**
205
+ * Returns true for blockquote marker lines.
206
+ */
207
+ function isBlockquoteLine(value) {
208
+ return /^\s{0,3}>\s?/.test(value);
209
+ }
210
+ /**
211
+ * Returns true when two lines form a GFM table header + divider pair.
212
+ */
213
+ function isPipeTableHeader(first, second) {
214
+ return looksLikeTableRow(first.text) && isTableDividerRow(second.text);
215
+ }
216
+ /**
217
+ * Returns true when a line appears to be a table row.
218
+ */
219
+ function looksLikeTableRow(value) {
220
+ return value.includes("|") && /\S/.test(value.replace(/\|/g, ""));
221
+ }
222
+ /**
223
+ * Returns true for GFM table divider rows.
224
+ */
225
+ function isTableDividerRow(value) {
226
+ const cells = splitTableCells(value);
227
+ if (cells.length === 0) return false;
228
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
229
+ }
230
+ /**
231
+ * Splits a pipe-table row into trimmed cell values.
232
+ */
233
+ function splitTableCells(value) {
234
+ const trimmed = value.trim();
235
+ const body = trimmed.startsWith("|") ? trimmed.slice(1) : trimmed;
236
+ return (body.endsWith("|") ? body.slice(0, -1) : body).split("|").map((cell) => cell.trim());
237
+ }
238
+ /**
239
+ * Converts table divider cells into alignment metadata.
240
+ */
241
+ function parseTableAlignment(cells) {
242
+ return cells.map((cell) => {
243
+ const trimmed = cell.trim();
244
+ const left = trimmed.startsWith(":");
245
+ const right = trimmed.endsWith(":");
246
+ if (left && right) return "center";
247
+ if (left) return "left";
248
+ if (right) return "right";
249
+ return "none";
250
+ });
251
+ }
252
+ /**
253
+ * Determines whether a line starts a new block construct.
254
+ */
255
+ function startsNewBlock(lines, index, enableTables) {
256
+ const line = lines[index].text;
257
+ return !!matchFenceOpen(line) || !!matchAtxHeading(line) || isThematicBreak(line) || !!matchListItem(line) || isBlockquoteLine(line) || enableTables && index + 1 < lines.length && isPipeTableHeader(lines[index], lines[index + 1]);
258
+ }
259
+ /**
260
+ * Returns true when a character is escapable in CommonMark inline syntax.
261
+ */
262
+ function isEscapable(ch) {
263
+ return /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(ch);
264
+ }
265
+ /**
266
+ * Finds the matching closing delimiter for a nested delimiter pair.
267
+ */
268
+ function findClosing(input, openAt, openChar, closeChar) {
269
+ let depth = 0;
270
+ for (let i = openAt; i < input.length; i += 1) {
271
+ const ch = input[i];
272
+ if (ch === "\\") {
273
+ i += 1;
274
+ continue;
275
+ }
276
+ if (ch === openChar) {
277
+ depth += 1;
278
+ continue;
279
+ }
280
+ if (ch === closeChar) {
281
+ depth -= 1;
282
+ if (depth === 0) return i;
283
+ }
284
+ }
285
+ return -1;
286
+ }
287
+ /**
288
+ * Parses a markdown link destination and optional title.
289
+ */
290
+ function parseLinkDestination(input) {
291
+ const trimmed = input.trim();
292
+ if (trimmed.startsWith("<")) {
293
+ const close = trimmed.indexOf(">");
294
+ if (close > 0) {
295
+ const url = trimmed.slice(1, close);
296
+ const title = parseLinkTitle(trimmed.slice(close + 1).trim());
297
+ return title ? {
298
+ url,
299
+ title
300
+ } : { url };
301
+ }
302
+ }
303
+ const firstSpace = trimmed.search(/\s/);
304
+ if (firstSpace < 0) return { url: trimmed };
305
+ const url = trimmed.slice(0, firstSpace);
306
+ const title = parseLinkTitle(trimmed.slice(firstSpace).trim());
307
+ return title ? {
308
+ url,
309
+ title
310
+ } : { url };
311
+ }
312
+ /**
313
+ * Parses CommonMark-style link titles (`\"...\"`, `'...'`, `( ... )`).
314
+ */
315
+ function parseLinkTitle(input) {
316
+ if (!input) return;
317
+ if (input.startsWith("\"") && input.endsWith("\"") || input.startsWith("'") && input.endsWith("'")) return input.slice(1, -1);
318
+ if (input.startsWith("(") && input.endsWith(")")) return input.slice(1, -1);
319
+ }
320
+ /**
321
+ * Returns true for URL-shaped text supported by autolink detection.
322
+ */
323
+ function isUrl(value) {
324
+ return /^(https?:\/\/|www\.)\S+$/.test(value);
325
+ }
326
+ /**
327
+ * Returns true for email-shaped text supported by autolink detection.
328
+ */
329
+ function isEmail(value) {
330
+ return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(value);
331
+ }
332
+ /**
333
+ * Trims punctuation suffixes from autolink candidates.
334
+ */
335
+ function trimAutolinkTrailingPunctuation(value) {
336
+ let end = value.length;
337
+ while (end > 0 && /[.,;:!?]/.test(value[end - 1])) end -= 1;
338
+ let candidate = value.slice(0, end);
339
+ if (candidate.endsWith(")")) {
340
+ const open = (candidate.match(/\(/g) ?? []).length;
341
+ if ((candidate.match(/\)/g) ?? []).length > open) candidate = candidate.slice(0, -1);
342
+ }
343
+ return candidate;
344
+ }
345
+
346
+ //#endregion
347
+ //#region src/parser/citations.ts
348
+ /**
349
+ * Extracts citation definitions from source text.
350
+ *
351
+ * @param source - Full normalized markdown source.
352
+ * @param warnings - Mutable warning collection for parse diagnostics.
353
+ * @returns Citation state with collected definitions and empty reference order.
354
+ */
355
+ function parseCitationDefinitions(source) {
356
+ const definitions = {};
357
+ const warnings = [];
358
+ const lines = source.split("\n");
359
+ let offset = 0;
360
+ for (const line of lines) {
361
+ const match = /^\s{0,3}\[\^([^\]\s]+)\]:\s*(.*)$/.exec(line);
362
+ if (!match) {
363
+ offset += line.length + 1;
364
+ continue;
365
+ }
366
+ const id = match[1];
367
+ if (definitions[id]) {
368
+ warnings.push({
369
+ code: "invalid_citation_definition",
370
+ at: offset
371
+ });
372
+ offset += line.length + 1;
373
+ continue;
374
+ }
375
+ const extracted = extractTrailingUrl(match[2].trim());
376
+ definitions[id] = {
377
+ id,
378
+ text: extracted.text,
379
+ ...extracted.url ? { url: extracted.url } : {}
380
+ };
381
+ offset += line.length + 1;
382
+ }
383
+ return {
384
+ citations: {
385
+ order: [],
386
+ numbers: {},
387
+ definitions
388
+ },
389
+ warnings
390
+ };
391
+ }
392
+ /**
393
+ * Assigns or returns a stable citation number by first-seen inline reference order.
394
+ *
395
+ * @param citations - Citation state that tracks numbering.
396
+ * @param idRef - Citation identifier (without `[^` and `]` markers).
397
+ * @returns Stable numeric citation number.
398
+ */
399
+ function assignCitationNumber(citations, idRef) {
400
+ const existing = citations.numbers[idRef];
401
+ if (typeof existing === "number") return {
402
+ citations,
403
+ number: existing
404
+ };
405
+ const next = citations.order.length + 1;
406
+ return {
407
+ citations: {
408
+ ...citations,
409
+ order: [...citations.order, idRef],
410
+ numbers: {
411
+ ...citations.numbers,
412
+ [idRef]: next
413
+ }
414
+ },
415
+ number: next
416
+ };
417
+ }
418
+ function extractTrailingUrl(input) {
419
+ if (!input) return { text: "" };
420
+ const match = /(?:\s|^)(https?:\/\/\S+|www\.\S+)$/.exec(input);
421
+ if (!match) return { text: input };
422
+ return {
423
+ text: input.slice(0, match.index).trimEnd(),
424
+ url: match[1]
425
+ };
426
+ }
427
+
428
+ //#endregion
429
+ //#region src/parser/segments.ts
430
+ /**
431
+ * Computes parse-time text segments for animation and incremental rendering.
432
+ *
433
+ * @param text - Raw text content for a single text node.
434
+ * @param absoluteStart - Absolute source index where `text` begins.
435
+ * @param context - Parse context containing segmenter options and warning state.
436
+ * @returns Segment list or empty list when segmentation is disabled/unavailable.
437
+ */
438
+ function createSegments(text, absoluteStart, options) {
439
+ if (options.segmenter === false) return {
440
+ segments: [],
441
+ hasWarnedSegmenterUnavailable: options.hasWarnedSegmenterUnavailable
442
+ };
443
+ const segmenterCtor = typeof Intl === "undefined" ? void 0 : Intl.Segmenter;
444
+ if (!segmenterCtor) {
445
+ if (!options.hasWarnedSegmenterUnavailable) return {
446
+ segments: [],
447
+ warning: {
448
+ code: "segmenter_unavailable",
449
+ at: absoluteStart
450
+ },
451
+ hasWarnedSegmenterUnavailable: true
452
+ };
453
+ return {
454
+ segments: [],
455
+ hasWarnedSegmenterUnavailable: true
456
+ };
457
+ }
458
+ const granularity = options.segmenter === true ? "word" : options.segmenter.granularity ?? "word";
459
+ const segmenter = new segmenterCtor(options.segmenter === true ? void 0 : options.segmenter.locale, { granularity });
460
+ const segments = [];
461
+ for (const segment of segmenter.segment(text)) {
462
+ const segmentText = segment.segment;
463
+ const start = absoluteStart + segment.index;
464
+ segments.push({
465
+ text: segmentText,
466
+ start,
467
+ end: start + segmentText.length,
468
+ kind: granularity,
469
+ isWhitespace: /^\s+$/.test(segmentText)
470
+ });
471
+ }
472
+ return {
473
+ segments: clusterPunctuationSegments(segments),
474
+ hasWarnedSegmenterUnavailable: options.hasWarnedSegmenterUnavailable
475
+ };
476
+ }
477
+ function clusterPunctuationSegments(segments) {
478
+ const withClosingAttached = segments.reduce((acc, segment) => {
479
+ const previous = acc[acc.length - 1];
480
+ if (previous && !previous.isWhitespace && isClosingPunctuationOnly(segment.text)) {
481
+ acc[acc.length - 1] = {
482
+ ...previous,
483
+ text: `${previous.text}${segment.text}`,
484
+ end: segment.end
485
+ };
486
+ return acc;
487
+ }
488
+ return [...acc, segment];
489
+ }, []);
490
+ const clustered = [];
491
+ let i = 0;
492
+ while (i < withClosingAttached.length) {
493
+ const current = withClosingAttached[i];
494
+ const next = i + 1 < withClosingAttached.length ? withClosingAttached[i + 1] : null;
495
+ if (next && !next.isWhitespace && isOpeningPunctuationOnly(current.text)) {
496
+ clustered.push({
497
+ ...next,
498
+ text: `${current.text}${next.text}`,
499
+ start: current.start
500
+ });
501
+ i += 2;
502
+ continue;
503
+ }
504
+ clustered.push(current);
505
+ i += 1;
506
+ }
507
+ return clustered;
508
+ }
509
+ function isClosingPunctuationOnly(value) {
510
+ return isOnlyFromSet(value, CLOSING_PUNCTUATION$1);
511
+ }
512
+ function isOpeningPunctuationOnly(value) {
513
+ return isOnlyFromSet(value, OPENING_PUNCTUATION);
514
+ }
515
+ function isOnlyFromSet(value, charset) {
516
+ return value.length > 0 && [...value].every((character) => charset.has(character));
517
+ }
518
+ const CLOSING_PUNCTUATION$1 = new Set([
519
+ "]",
520
+ ",",
521
+ ".",
522
+ "!",
523
+ "?",
524
+ ";",
525
+ ":",
526
+ "%",
527
+ ")",
528
+ "}",
529
+ ">",
530
+ "\"",
531
+ "'",
532
+ "、",
533
+ "。",
534
+ ",",
535
+ ".",
536
+ "!",
537
+ "?",
538
+ ":",
539
+ ";",
540
+ "%",
541
+ ")",
542
+ "]",
543
+ "}",
544
+ "〉",
545
+ "》",
546
+ "」",
547
+ "』",
548
+ "】",
549
+ "〕",
550
+ "〗"
551
+ ]);
552
+ const OPENING_PUNCTUATION = new Set([
553
+ "(",
554
+ "{",
555
+ "[",
556
+ "\"",
557
+ "'",
558
+ "〈",
559
+ "《",
560
+ "「",
561
+ "『",
562
+ "【",
563
+ "〔",
564
+ "〖",
565
+ "(",
566
+ "[",
567
+ "{"
568
+ ]);
569
+
570
+ //#endregion
571
+ //#region src/parser/inline-parser.ts
572
+ /**
573
+ * Parses inline markdown content into inline draft nodes.
574
+ *
575
+ * @param text - Inline source text.
576
+ * @param absoluteStart - Absolute source offset for the first character in `text`.
577
+ * @param path - Stable path prefix for emitted nodes.
578
+ * @param context - Immutable parse context shared across block/inline parsing.
579
+ * @returns Inline draft nodes and updated immutable parser context.
580
+ */
581
+ function parseInline(text, absoluteStart, path, context) {
582
+ const nodes = [];
583
+ let i = 0;
584
+ let textBuffer = "";
585
+ let textBufferStart = -1;
586
+ let citations = context.citations;
587
+ let warnings = context.warnings;
588
+ let hasWarnedSegmenterUnavailable = context.hasWarnedSegmenterUnavailable;
589
+ const flushTextBuffer = () => {
590
+ if (textBuffer.length === 0 || textBufferStart < 0) return;
591
+ const appendResult = appendText(nodes, textBuffer, textBufferStart, path, context.options.segmenter, hasWarnedSegmenterUnavailable);
592
+ warnings = appendResult.warnings.length > 0 ? [...warnings, ...appendResult.warnings] : warnings;
593
+ hasWarnedSegmenterUnavailable = appendResult.hasWarnedSegmenterUnavailable;
594
+ textBuffer = "";
595
+ textBufferStart = -1;
596
+ };
597
+ const appendBufferedText = (value, start) => {
598
+ if (value.length === 0) return;
599
+ if (textBuffer.length === 0) textBufferStart = start;
600
+ textBuffer += value;
601
+ };
602
+ while (i < text.length) {
603
+ const current = text[i];
604
+ if (current === "\n") {
605
+ const hardByBackslash = countTrailing(text, i - 1, "\\") % 2 === 1;
606
+ const hardBySpaces = i > 1 && text.slice(i - 2, i) === " ";
607
+ const hard = hardByBackslash || hardBySpaces;
608
+ if (hardByBackslash && textBuffer.endsWith("\\")) textBuffer = textBuffer.slice(0, -1);
609
+ flushTextBuffer();
610
+ nodes.push({
611
+ path: `${path}.${nodes.length}`,
612
+ type: hard ? "hard-break" : "soft-break",
613
+ range: {
614
+ start: absoluteStart + i,
615
+ end: absoluteStart + i + 1
616
+ },
617
+ closed: true,
618
+ props: {},
619
+ children: []
620
+ });
621
+ i += 1;
622
+ continue;
623
+ }
624
+ if (current === "\\" && i + 1 < text.length && isEscapable(text[i + 1])) {
625
+ appendBufferedText(text[i + 1], absoluteStart + i);
626
+ i += 2;
627
+ continue;
628
+ }
629
+ const citation = parseCitationInline(text, i, absoluteStart, path, citations, nodes.length, context.isComplete);
630
+ if (citation) {
631
+ flushTextBuffer();
632
+ nodes.push(citation.node);
633
+ i = citation.next;
634
+ citations = citation.citations;
635
+ continue;
636
+ }
637
+ const image = parseImageInline(text, i, absoluteStart, path, nodes.length);
638
+ if (image) {
639
+ flushTextBuffer();
640
+ nodes.push(image.node);
641
+ i = image.next;
642
+ continue;
643
+ }
644
+ const link = parseLinkInline(text, i, absoluteStart, path, nodes.length, context, citations, warnings, hasWarnedSegmenterUnavailable);
645
+ if (link) {
646
+ flushTextBuffer();
647
+ nodes.push(link.node);
648
+ i = link.next;
649
+ citations = link.citations;
650
+ warnings = link.warnings;
651
+ hasWarnedSegmenterUnavailable = link.hasWarnedSegmenterUnavailable;
652
+ continue;
653
+ }
654
+ const auto = parseAutolinkInline(text, i, absoluteStart, path, context.options.enableAutolinks, nodes.length);
655
+ if (auto) {
656
+ flushTextBuffer();
657
+ nodes.push(auto.node);
658
+ i = auto.next;
659
+ continue;
660
+ }
661
+ const code = parseInlineCode(text, i, absoluteStart, path, nodes.length);
662
+ if (code) {
663
+ flushTextBuffer();
664
+ nodes.push(code.node);
665
+ i = code.next;
666
+ continue;
667
+ }
668
+ const strong = parseDelimitedInline(text, i, "**", "strong", absoluteStart, path, nodes.length, context, citations, warnings, hasWarnedSegmenterUnavailable) ?? parseDelimitedInline(text, i, "__", "strong", absoluteStart, path, nodes.length, context, citations, warnings, hasWarnedSegmenterUnavailable);
669
+ if (strong) {
670
+ flushTextBuffer();
671
+ nodes.push(strong.node);
672
+ i = strong.next;
673
+ citations = strong.citations;
674
+ warnings = strong.warnings;
675
+ hasWarnedSegmenterUnavailable = strong.hasWarnedSegmenterUnavailable;
676
+ continue;
677
+ }
678
+ const strike = parseDelimitedInline(text, i, "~~", "strikethrough", absoluteStart, path, nodes.length, context, citations, warnings, hasWarnedSegmenterUnavailable);
679
+ if (strike) {
680
+ flushTextBuffer();
681
+ nodes.push(strike.node);
682
+ i = strike.next;
683
+ citations = strike.citations;
684
+ warnings = strike.warnings;
685
+ hasWarnedSegmenterUnavailable = strike.hasWarnedSegmenterUnavailable;
686
+ continue;
687
+ }
688
+ const em = parseDelimitedInline(text, i, "*", "em", absoluteStart, path, nodes.length, context, citations, warnings, hasWarnedSegmenterUnavailable) ?? parseDelimitedInline(text, i, "_", "em", absoluteStart, path, nodes.length, context, citations, warnings, hasWarnedSegmenterUnavailable);
689
+ if (em) {
690
+ flushTextBuffer();
691
+ nodes.push(em.node);
692
+ i = em.next;
693
+ citations = em.citations;
694
+ warnings = em.warnings;
695
+ hasWarnedSegmenterUnavailable = em.hasWarnedSegmenterUnavailable;
696
+ continue;
697
+ }
698
+ appendBufferedText(text[i], absoluteStart + i);
699
+ i += 1;
700
+ }
701
+ flushTextBuffer();
702
+ return {
703
+ value: annotateNoBreakBeforeSegments(nodes),
704
+ citations,
705
+ warnings,
706
+ hasWarnedSegmenterUnavailable
707
+ };
708
+ }
709
+ function countTrailing(text, index, value) {
710
+ let run = 0;
711
+ let i = index;
712
+ while (i >= 0 && text[i] === value) {
713
+ run += 1;
714
+ i -= 1;
715
+ }
716
+ return run;
717
+ }
718
+ function annotateNoBreakBeforeSegments(nodes) {
719
+ let changed = false;
720
+ const next = nodes.map((node, index) => {
721
+ if (node.type !== "text") return node;
722
+ const segments = node.props["segments"];
723
+ if (!Array.isArray(segments) || segments.length === 0) return node;
724
+ const typedSegments = segments;
725
+ const firstSegment = typedSegments[0];
726
+ if (!firstSegment) return node;
727
+ const previous = index > 0 ? nodes[index - 1] : null;
728
+ const shouldMark = startsWithClosingPunctuation(firstSegment.text) && shouldPreventBreakBeforeNode(previous);
729
+ if (shouldMark === (firstSegment.noBreakBefore === true)) return node;
730
+ const nextSegments = [shouldMark ? {
731
+ ...firstSegment,
732
+ noBreakBefore: true
733
+ } : {
734
+ ...firstSegment,
735
+ noBreakBefore: void 0
736
+ }, ...typedSegments.slice(1)];
737
+ changed = true;
738
+ return {
739
+ ...node,
740
+ props: {
741
+ ...node.props,
742
+ segments: nextSegments
743
+ }
744
+ };
745
+ });
746
+ return changed ? next : nodes;
747
+ }
748
+ function shouldPreventBreakBeforeNode(node) {
749
+ if (!node) return false;
750
+ if (node.type === "soft-break" || node.type === "hard-break") return false;
751
+ if (node.type === "text") {
752
+ const text = String(node.props["text"] ?? "");
753
+ if (!text || /\s$/u.test(text)) return false;
754
+ }
755
+ return true;
756
+ }
757
+ function startsWithClosingPunctuation(value) {
758
+ if (!value) return false;
759
+ const [first] = [...value];
760
+ return first != null && CLOSING_PUNCTUATION.has(first);
761
+ }
762
+ const CLOSING_PUNCTUATION = new Set([
763
+ "]",
764
+ ",",
765
+ ".",
766
+ "!",
767
+ "?",
768
+ ";",
769
+ ":",
770
+ "%",
771
+ ")",
772
+ "}",
773
+ ">",
774
+ "\"",
775
+ "'",
776
+ "、",
777
+ "。",
778
+ ",",
779
+ ".",
780
+ "!",
781
+ "?",
782
+ ":",
783
+ ";",
784
+ "%",
785
+ ")",
786
+ "]",
787
+ "}",
788
+ "〉",
789
+ "》",
790
+ "」",
791
+ "』",
792
+ "】",
793
+ "〕",
794
+ "〗"
795
+ ]);
796
+ function appendText(nodes, text, start, path, segmenter, hasWarnedSegmenterUnavailable) {
797
+ if (!text) return {
798
+ warnings: [],
799
+ hasWarnedSegmenterUnavailable
800
+ };
801
+ const segmentation = createSegments(text, start, {
802
+ segmenter,
803
+ hasWarnedSegmenterUnavailable
804
+ });
805
+ nodes.push({
806
+ path: `${path}.${nodes.length}`,
807
+ type: "text",
808
+ range: {
809
+ start,
810
+ end: start + text.length
811
+ },
812
+ closed: true,
813
+ props: {
814
+ text,
815
+ segments: segmentation.segments
816
+ },
817
+ children: []
818
+ });
819
+ return {
820
+ warnings: segmentation.warning ? [segmentation.warning] : [],
821
+ hasWarnedSegmenterUnavailable: segmentation.hasWarnedSegmenterUnavailable
822
+ };
823
+ }
824
+ function parseCitationInline(text, at, start, path, citations, index, isComplete) {
825
+ if (!text.startsWith("[^", at)) return null;
826
+ const close = text.indexOf("]", at + 2);
827
+ if (close >= 0) {
828
+ const idRef = text.slice(at + 2, close).trim();
829
+ if (!idRef) return null;
830
+ const numbered = assignCitationNumber(citations, idRef);
831
+ return {
832
+ node: {
833
+ path: `${path}.${index}`,
834
+ type: "citation",
835
+ range: {
836
+ start: start + at,
837
+ end: start + close + 1
838
+ },
839
+ closed: true,
840
+ props: {
841
+ idRef,
842
+ number: numbered.number
843
+ },
844
+ children: []
845
+ },
846
+ next: close + 1,
847
+ citations: numbered.citations
848
+ };
849
+ }
850
+ if (isComplete) return null;
851
+ const partial = text.slice(at);
852
+ if (!/^\[\^[^\]\s]+$/u.test(partial)) return null;
853
+ const partialIdRef = partial.slice(2);
854
+ return {
855
+ node: {
856
+ path: `${path}.${index}`,
857
+ type: "citation",
858
+ range: {
859
+ start: start + at,
860
+ end: start + text.length
861
+ },
862
+ closed: false,
863
+ props: { idRef: partialIdRef },
864
+ children: []
865
+ },
866
+ next: text.length,
867
+ citations
868
+ };
869
+ }
870
+ function parseImageInline(text, at, start, path, index) {
871
+ if (!text.startsWith("![", at)) return null;
872
+ const labelEnd = findClosing(text, at + 1, "[", "]");
873
+ if (labelEnd < 0 || text[labelEnd + 1] !== "(") return null;
874
+ const destEnd = findClosing(text, labelEnd + 1, "(", ")");
875
+ if (destEnd < 0) return null;
876
+ const alt = text.slice(at + 2, labelEnd);
877
+ const parsed = parseLinkDestination(text.slice(labelEnd + 2, destEnd));
878
+ return {
879
+ node: {
880
+ path: `${path}.${index}`,
881
+ type: "image",
882
+ range: {
883
+ start: start + at,
884
+ end: start + destEnd + 1
885
+ },
886
+ closed: true,
887
+ props: {
888
+ url: parsed.url,
889
+ alt,
890
+ ...parsed.title ? { title: parsed.title } : {}
891
+ },
892
+ children: []
893
+ },
894
+ next: destEnd + 1
895
+ };
896
+ }
897
+ function parseLinkInline(text, at, start, path, index, context, citations, warnings, hasWarnedSegmenterUnavailable) {
898
+ if (text[at] !== "[" || text.startsWith("[^", at)) return null;
899
+ const labelEnd = findClosing(text, at, "[", "]");
900
+ if (labelEnd < 0 || text[labelEnd + 1] !== "(") return null;
901
+ const destEnd = findClosing(text, labelEnd + 1, "(", ")");
902
+ if (destEnd < 0) return null;
903
+ const label = text.slice(at + 1, labelEnd);
904
+ const parsed = parseLinkDestination(text.slice(labelEnd + 2, destEnd));
905
+ const childResult = parseInline(label, start + at + 1, `${path}.${index}`, {
906
+ ...context,
907
+ citations,
908
+ warnings,
909
+ hasWarnedSegmenterUnavailable
910
+ });
911
+ return {
912
+ node: {
913
+ path: `${path}.${index}`,
914
+ type: "link",
915
+ range: {
916
+ start: start + at,
917
+ end: start + destEnd + 1
918
+ },
919
+ closed: true,
920
+ props: {
921
+ url: parsed.url,
922
+ ...parsed.title ? { title: parsed.title } : {}
923
+ },
924
+ children: childResult.value
925
+ },
926
+ next: destEnd + 1,
927
+ citations: childResult.citations,
928
+ warnings: childResult.warnings,
929
+ hasWarnedSegmenterUnavailable: childResult.hasWarnedSegmenterUnavailable
930
+ };
931
+ }
932
+ function parseAutolinkInline(text, at, start, path, enabled, index) {
933
+ if (!enabled) return null;
934
+ if (text[at] === "<") {
935
+ const close = text.indexOf(">", at + 1);
936
+ if (close > at + 1) {
937
+ const value = text.slice(at + 1, close).trim();
938
+ if (isUrl(value) || isEmail(value)) {
939
+ const url = isEmail(value) ? `mailto:${value}` : value;
940
+ return {
941
+ node: {
942
+ path: `${path}.${index}`,
943
+ type: "autolink",
944
+ range: {
945
+ start: start + at,
946
+ end: start + close + 1
947
+ },
948
+ closed: true,
949
+ props: {
950
+ url,
951
+ text: value
952
+ },
953
+ children: []
954
+ },
955
+ next: close + 1
956
+ };
957
+ }
958
+ }
959
+ }
960
+ if (!(at === 0 || /\s|[([{"'`]/.test(text[at - 1]))) return null;
961
+ const match = /^(https?:\/\/\S+|www\.\S+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/.exec(text.slice(at));
962
+ if (!match) return null;
963
+ const raw = trimAutolinkTrailingPunctuation(match[1]);
964
+ if (!raw) return null;
965
+ const url = isEmail(raw) ? `mailto:${raw}` : raw.startsWith("www.") ? `https://${raw}` : raw;
966
+ return {
967
+ node: {
968
+ path: `${path}.${index}`,
969
+ type: "autolink",
970
+ range: {
971
+ start: start + at,
972
+ end: start + at + raw.length
973
+ },
974
+ closed: true,
975
+ props: {
976
+ url,
977
+ text: raw
978
+ },
979
+ children: []
980
+ },
981
+ next: at + raw.length
982
+ };
983
+ }
984
+ function parseInlineCode(text, at, start, path, index) {
985
+ if (text[at] !== "`") return null;
986
+ let len = 1;
987
+ while (text[at + len] === "`") len += 1;
988
+ const delimiter = "`".repeat(len);
989
+ const close = text.indexOf(delimiter, at + len);
990
+ if (close < 0) return {
991
+ node: {
992
+ path: `${path}.${index}`,
993
+ type: "inline-code",
994
+ range: {
995
+ start: start + at,
996
+ end: start + text.length
997
+ },
998
+ closed: false,
999
+ props: { text: text.slice(at + len) },
1000
+ children: []
1001
+ },
1002
+ next: text.length
1003
+ };
1004
+ return {
1005
+ node: {
1006
+ path: `${path}.${index}`,
1007
+ type: "inline-code",
1008
+ range: {
1009
+ start: start + at,
1010
+ end: start + close + len
1011
+ },
1012
+ closed: true,
1013
+ props: { text: text.slice(at + len, close) },
1014
+ children: []
1015
+ },
1016
+ next: close + len
1017
+ };
1018
+ }
1019
+ function parseDelimitedInline(text, at, delimiter, type, start, path, index, context, citations, warnings, hasWarnedSegmenterUnavailable) {
1020
+ if (!text.startsWith(delimiter, at)) return null;
1021
+ const close = text.indexOf(delimiter, at + delimiter.length);
1022
+ if (close < 0) {
1023
+ const children = parseInline(text.slice(at + delimiter.length), start + at + delimiter.length, `${path}.${index}`, {
1024
+ ...context,
1025
+ citations,
1026
+ warnings,
1027
+ hasWarnedSegmenterUnavailable
1028
+ });
1029
+ return {
1030
+ node: {
1031
+ path: `${path}.${index}`,
1032
+ type,
1033
+ range: {
1034
+ start: start + at,
1035
+ end: start + text.length
1036
+ },
1037
+ closed: false,
1038
+ props: {},
1039
+ children: children.value
1040
+ },
1041
+ next: text.length,
1042
+ citations: children.citations,
1043
+ warnings: children.warnings,
1044
+ hasWarnedSegmenterUnavailable: children.hasWarnedSegmenterUnavailable
1045
+ };
1046
+ }
1047
+ const children = parseInline(text.slice(at + delimiter.length, close), start + at + delimiter.length, `${path}.${index}`, {
1048
+ ...context,
1049
+ citations,
1050
+ warnings,
1051
+ hasWarnedSegmenterUnavailable
1052
+ });
1053
+ return {
1054
+ node: {
1055
+ path: `${path}.${index}`,
1056
+ type,
1057
+ range: {
1058
+ start: start + at,
1059
+ end: start + close + delimiter.length
1060
+ },
1061
+ closed: true,
1062
+ props: {},
1063
+ children: children.value
1064
+ },
1065
+ next: close + delimiter.length,
1066
+ citations: children.citations,
1067
+ warnings: children.warnings,
1068
+ hasWarnedSegmenterUnavailable: children.hasWarnedSegmenterUnavailable
1069
+ };
1070
+ }
1071
+
1072
+ //#endregion
1073
+ //#region src/parser/block-parser.ts
1074
+ /**
1075
+ * Parses a contiguous range of source lines into block-level draft nodes.
1076
+ *
1077
+ * @param lines - Line-split source with offsets.
1078
+ * @param from - Inclusive start line index.
1079
+ * @param to - Exclusive end line index.
1080
+ * @param path - Stable path prefix for emitted nodes.
1081
+ * @param context - Shared parse context carrying options, warnings, and citations.
1082
+ * @param isRoot - Whether this parse call is for the document root.
1083
+ * @returns Root draft node plus updated immutable parse context.
1084
+ */
1085
+ function parseBlocks(lines, from, to, path, context, isRoot) {
1086
+ let root = {
1087
+ path,
1088
+ type: "document",
1089
+ range: {
1090
+ start: from < to ? lines[from].start : 0,
1091
+ end: from < to ? lines[to - 1].end : 0
1092
+ },
1093
+ closed: false,
1094
+ props: {},
1095
+ children: []
1096
+ };
1097
+ let i = from;
1098
+ let citations = context.citations;
1099
+ let warnings = context.warnings;
1100
+ let hasWarnedSegmenterUnavailable = context.hasWarnedSegmenterUnavailable;
1101
+ while (i < to) {
1102
+ const line = lines[i];
1103
+ const isCitationDefinition = isCitationDefinitionLine(line.text);
1104
+ const isCitationDefinitionPrefix = isCitationDefinitionPrefixCandidate(line.text, line.hasNewline, context.isComplete);
1105
+ if (isBlank(line.text) || isCitationDefinition || isCitationDefinitionPrefix) {
1106
+ i += 1;
1107
+ continue;
1108
+ }
1109
+ const fenced = matchFenceOpen(line.text);
1110
+ if (fenced) {
1111
+ const nodePath = `${path}.${root.children.length}`;
1112
+ const parsed = parseCodeFence(lines, i, to, nodePath, context, fenced);
1113
+ root.children = [...root.children, parsed.node];
1114
+ i = parsed.next;
1115
+ continue;
1116
+ }
1117
+ if (isBlockquoteLine(line.text)) {
1118
+ const nodePath = `${path}.${root.children.length}`;
1119
+ const parsed = parseBlockquote(lines, i, to, nodePath, {
1120
+ ...context,
1121
+ citations,
1122
+ warnings,
1123
+ hasWarnedSegmenterUnavailable
1124
+ });
1125
+ root.children = [...root.children, parsed.value];
1126
+ i = parsed.next;
1127
+ citations = parsed.citations;
1128
+ warnings = parsed.warnings;
1129
+ hasWarnedSegmenterUnavailable = parsed.hasWarnedSegmenterUnavailable;
1130
+ continue;
1131
+ }
1132
+ const listMarker = matchListItem(line.text);
1133
+ if (listMarker) {
1134
+ const nodePath = `${path}.${root.children.length}`;
1135
+ const parsed = parseList(lines, i, to, nodePath, {
1136
+ ...context,
1137
+ citations,
1138
+ warnings,
1139
+ hasWarnedSegmenterUnavailable
1140
+ }, listMarker.ordered);
1141
+ root.children = [...root.children, parsed.value];
1142
+ i = parsed.next;
1143
+ citations = parsed.citations;
1144
+ warnings = parsed.warnings;
1145
+ hasWarnedSegmenterUnavailable = parsed.hasWarnedSegmenterUnavailable;
1146
+ continue;
1147
+ }
1148
+ const heading = matchAtxHeading(line.text);
1149
+ if (heading) {
1150
+ const nodePath = `${path}.${root.children.length}`;
1151
+ const children = parseInline(heading.text, line.start + heading.contentOffset, nodePath, {
1152
+ ...context,
1153
+ citations,
1154
+ warnings,
1155
+ hasWarnedSegmenterUnavailable
1156
+ });
1157
+ root.children = [...root.children, {
1158
+ path: nodePath,
1159
+ type: "heading",
1160
+ range: {
1161
+ start: line.start,
1162
+ end: line.end
1163
+ },
1164
+ closed: context.isComplete || line.hasNewline,
1165
+ props: { level: heading.level },
1166
+ children: children.value
1167
+ }];
1168
+ citations = children.citations;
1169
+ warnings = children.warnings;
1170
+ hasWarnedSegmenterUnavailable = children.hasWarnedSegmenterUnavailable;
1171
+ i += 1;
1172
+ continue;
1173
+ }
1174
+ if (i + 1 < to && isSetextUnderline(lines[i + 1].text) && !isBlank(line.text)) {
1175
+ const nodePath = `${path}.${root.children.length}`;
1176
+ const level = lines[i + 1].text.trim().startsWith("=") ? 1 : 2;
1177
+ const children = parseInline(line.text, line.start, nodePath, {
1178
+ ...context,
1179
+ citations,
1180
+ warnings,
1181
+ hasWarnedSegmenterUnavailable
1182
+ });
1183
+ root.children = [...root.children, {
1184
+ path: nodePath,
1185
+ type: "heading",
1186
+ range: {
1187
+ start: line.start,
1188
+ end: lines[i + 1].end
1189
+ },
1190
+ closed: context.isComplete || lines[i + 1].hasNewline,
1191
+ props: { level },
1192
+ children: children.value
1193
+ }];
1194
+ citations = children.citations;
1195
+ warnings = children.warnings;
1196
+ hasWarnedSegmenterUnavailable = children.hasWarnedSegmenterUnavailable;
1197
+ i += 2;
1198
+ continue;
1199
+ }
1200
+ if (isThematicBreak(line.text)) {
1201
+ root.children = [...root.children, {
1202
+ path: `${path}.${root.children.length}`,
1203
+ type: "thematic-break",
1204
+ range: {
1205
+ start: line.start,
1206
+ end: line.end
1207
+ },
1208
+ closed: true,
1209
+ props: {},
1210
+ children: []
1211
+ }];
1212
+ i += 1;
1213
+ continue;
1214
+ }
1215
+ if (context.options.enableTables && i + 1 < to && isPipeTableHeader(lines[i], lines[i + 1])) {
1216
+ const nodePath = `${path}.${root.children.length}`;
1217
+ const parsed = parsePipeTable(lines, i, to, nodePath, {
1218
+ ...context,
1219
+ citations,
1220
+ warnings,
1221
+ hasWarnedSegmenterUnavailable
1222
+ });
1223
+ root.children = [...root.children, parsed.value];
1224
+ i = parsed.next;
1225
+ citations = parsed.citations;
1226
+ warnings = parsed.warnings;
1227
+ hasWarnedSegmenterUnavailable = parsed.hasWarnedSegmenterUnavailable;
1228
+ continue;
1229
+ }
1230
+ const nodePath = `${path}.${root.children.length}`;
1231
+ const parsed = parseParagraph(lines, i, to, nodePath, {
1232
+ ...context,
1233
+ citations,
1234
+ warnings,
1235
+ hasWarnedSegmenterUnavailable
1236
+ });
1237
+ root.children = [...root.children, parsed.value];
1238
+ i = parsed.next;
1239
+ citations = parsed.citations;
1240
+ warnings = parsed.warnings;
1241
+ hasWarnedSegmenterUnavailable = parsed.hasWarnedSegmenterUnavailable;
1242
+ }
1243
+ root = {
1244
+ ...root,
1245
+ closed: !isRoot || context.isComplete || to > from && lines[to - 1].hasNewline && root.children.every((child) => child.closed)
1246
+ };
1247
+ return {
1248
+ value: root,
1249
+ citations,
1250
+ warnings,
1251
+ hasWarnedSegmenterUnavailable
1252
+ };
1253
+ }
1254
+ function parseCodeFence(lines, from, to, path, context, fenceOpen) {
1255
+ let i = from + 1;
1256
+ let closeLine = -1;
1257
+ while (i < to) {
1258
+ if (matchFenceClose(lines[i].text, fenceOpen.marker, fenceOpen.length)) {
1259
+ closeLine = i;
1260
+ break;
1261
+ }
1262
+ i += 1;
1263
+ }
1264
+ const endLine = closeLine >= 0 ? closeLine : to - 1;
1265
+ const text = lines.slice(from + 1, closeLine >= 0 ? closeLine : to).map((line) => line.text).join("\n");
1266
+ const closed = closeLine >= 0 || context.isComplete;
1267
+ return {
1268
+ node: {
1269
+ path,
1270
+ type: "code-block",
1271
+ range: {
1272
+ start: lines[from].start,
1273
+ end: lines[endLine].end
1274
+ },
1275
+ closed,
1276
+ props: {
1277
+ fence: fenceOpen.marker,
1278
+ ...fenceOpen.info ? { info: fenceOpen.info.split(/\s+/)[0] } : {},
1279
+ ...fenceOpen.info.includes(" ") ? { meta: fenceOpen.info.slice(fenceOpen.info.indexOf(" ") + 1) } : {},
1280
+ text
1281
+ },
1282
+ children: []
1283
+ },
1284
+ next: closeLine >= 0 ? closeLine + 1 : to
1285
+ };
1286
+ }
1287
+ function parseBlockquote(lines, from, to, path, context) {
1288
+ const picked = [];
1289
+ let i = from;
1290
+ while (i < to) {
1291
+ const line = lines[i];
1292
+ if (isBlank(line.text) && picked.length > 0) {
1293
+ picked.push(line);
1294
+ i += 1;
1295
+ continue;
1296
+ }
1297
+ if (isBlockquoteLine(line.text) || !isBlank(line.text) && picked.length > 0 && !startsNewBlock(lines, i, context.options.enableTables)) {
1298
+ picked.push(line);
1299
+ i += 1;
1300
+ continue;
1301
+ }
1302
+ break;
1303
+ }
1304
+ const stripped = picked.map((line) => {
1305
+ const marker = /^(\s{0,3}>\s?)/.exec(line.text);
1306
+ const markerWidth = marker?.[1].length ?? 0;
1307
+ const text = marker ? line.text.slice(markerWidth) : line.text;
1308
+ const start = line.start + markerWidth;
1309
+ return {
1310
+ text,
1311
+ start,
1312
+ end: start + text.length,
1313
+ hasNewline: line.hasNewline
1314
+ };
1315
+ });
1316
+ const children = parseBlocks(stripped, 0, stripped.length, path, context, false);
1317
+ return {
1318
+ value: {
1319
+ path,
1320
+ type: "blockquote",
1321
+ range: {
1322
+ start: picked[0].start,
1323
+ end: picked[picked.length - 1].end
1324
+ },
1325
+ closed: context.isComplete || picked[picked.length - 1].hasNewline,
1326
+ props: {},
1327
+ children: children.value.children
1328
+ },
1329
+ next: i,
1330
+ citations: children.citations,
1331
+ warnings: children.warnings,
1332
+ hasWarnedSegmenterUnavailable: children.hasWarnedSegmenterUnavailable
1333
+ };
1334
+ }
1335
+ function parseList(lines, from, to, path, context, ordered) {
1336
+ const items = [];
1337
+ let i = from;
1338
+ let sawBlankBetweenItems = false;
1339
+ let listStartNumber = null;
1340
+ let citations = context.citations;
1341
+ let warnings = context.warnings;
1342
+ let hasWarnedSegmenterUnavailable = context.hasWarnedSegmenterUnavailable;
1343
+ while (i < to) {
1344
+ const marker = matchListItem(lines[i].text);
1345
+ if (!marker || marker.ordered !== ordered) break;
1346
+ if (ordered && listStartNumber === null) listStartNumber = marker.start;
1347
+ const contentLines = [{
1348
+ ...lines[i],
1349
+ text: marker.content
1350
+ }];
1351
+ const contentIndent = marker.contentIndent;
1352
+ i += 1;
1353
+ let hadBlank = false;
1354
+ while (i < to) {
1355
+ const next = lines[i];
1356
+ if (matchListItem(next.text)) break;
1357
+ if (isBlank(next.text)) {
1358
+ hadBlank = true;
1359
+ contentLines.push(next);
1360
+ i += 1;
1361
+ if (i < to) {
1362
+ if (matchListItem(lines[i].text)) {
1363
+ sawBlankBetweenItems = true;
1364
+ break;
1365
+ }
1366
+ }
1367
+ continue;
1368
+ }
1369
+ if ((/^\s*/.exec(next.text)?.[0].length ?? 0) < contentIndent) break;
1370
+ const continuation = next.text.slice(contentIndent);
1371
+ contentLines.push({
1372
+ ...next,
1373
+ text: continuation
1374
+ });
1375
+ i += 1;
1376
+ }
1377
+ const paragraphText = contentLines.map((line) => line.text).join("\n").trimEnd();
1378
+ const itemPath = `${path}.${items.length}`;
1379
+ const parsedInline = parseInline(paragraphText, contentLines[0].start, `${itemPath}.0`, {
1380
+ ...context,
1381
+ citations,
1382
+ warnings,
1383
+ hasWarnedSegmenterUnavailable
1384
+ });
1385
+ const paragraph = {
1386
+ path: `${itemPath}.0`,
1387
+ type: "paragraph",
1388
+ range: {
1389
+ start: contentLines[0].start,
1390
+ end: contentLines[contentLines.length - 1].end
1391
+ },
1392
+ closed: context.isComplete || contentLines[contentLines.length - 1].hasNewline,
1393
+ props: {},
1394
+ children: parsedInline.value
1395
+ };
1396
+ items.push({
1397
+ path: itemPath,
1398
+ type: "list-item",
1399
+ range: {
1400
+ start: contentLines[0].start,
1401
+ end: contentLines[contentLines.length - 1].end
1402
+ },
1403
+ closed: context.isComplete || contentLines[contentLines.length - 1].hasNewline,
1404
+ props: {},
1405
+ children: [paragraph]
1406
+ });
1407
+ citations = parsedInline.citations;
1408
+ warnings = parsedInline.warnings;
1409
+ hasWarnedSegmenterUnavailable = parsedInline.hasWarnedSegmenterUnavailable;
1410
+ if (hadBlank) sawBlankBetweenItems = true;
1411
+ }
1412
+ return {
1413
+ value: {
1414
+ path,
1415
+ type: "list",
1416
+ range: {
1417
+ start: lines[from].start,
1418
+ end: lines[Math.max(from, i - 1)].end
1419
+ },
1420
+ closed: context.isComplete || lines[Math.max(from, i - 1)].hasNewline,
1421
+ props: {
1422
+ ordered,
1423
+ start: ordered ? listStartNumber ?? 1 : null,
1424
+ tight: !sawBlankBetweenItems
1425
+ },
1426
+ children: items
1427
+ },
1428
+ next: i,
1429
+ citations,
1430
+ warnings,
1431
+ hasWarnedSegmenterUnavailable
1432
+ };
1433
+ }
1434
+ function parsePipeTable(lines, from, to, path, context) {
1435
+ const header = splitTableCells(lines[from].text);
1436
+ const align = parseTableAlignment(splitTableCells(lines[from + 1].text));
1437
+ const rows = [];
1438
+ let i = from + 2;
1439
+ while (i < to && looksLikeTableRow(lines[i].text)) {
1440
+ rows.push(splitTableCells(lines[i].text));
1441
+ i += 1;
1442
+ }
1443
+ let citations = context.citations;
1444
+ let warnings = context.warnings;
1445
+ let hasWarnedSegmenterUnavailable = context.hasWarnedSegmenterUnavailable;
1446
+ const headerChildren = [];
1447
+ for (let cellIndex = 0; cellIndex < header.length; cellIndex += 1) {
1448
+ const parsed = parseInline(header[cellIndex], lines[from].start, `${path}.0.${cellIndex}`, {
1449
+ ...context,
1450
+ citations,
1451
+ warnings,
1452
+ hasWarnedSegmenterUnavailable
1453
+ });
1454
+ headerChildren.push({
1455
+ path: `${path}.0.${cellIndex}`,
1456
+ type: "table-cell",
1457
+ range: {
1458
+ start: lines[from].start,
1459
+ end: lines[from].end
1460
+ },
1461
+ closed: true,
1462
+ props: {},
1463
+ children: parsed.value
1464
+ });
1465
+ citations = parsed.citations;
1466
+ warnings = parsed.warnings;
1467
+ hasWarnedSegmenterUnavailable = parsed.hasWarnedSegmenterUnavailable;
1468
+ }
1469
+ const headerRow = {
1470
+ path: `${path}.0`,
1471
+ type: "table-row",
1472
+ range: {
1473
+ start: lines[from].start,
1474
+ end: lines[from].end
1475
+ },
1476
+ closed: true,
1477
+ props: { isHeader: true },
1478
+ children: headerChildren
1479
+ };
1480
+ const bodyRows = [];
1481
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
1482
+ const row = rows[rowIndex];
1483
+ const cellNodes = [];
1484
+ for (let cellIndex = 0; cellIndex < row.length; cellIndex += 1) {
1485
+ const parsed = parseInline(row[cellIndex], lines[from + 2 + rowIndex].start, `${path}.${rowIndex + 1}.${cellIndex}`, {
1486
+ ...context,
1487
+ citations,
1488
+ warnings,
1489
+ hasWarnedSegmenterUnavailable
1490
+ });
1491
+ cellNodes.push({
1492
+ path: `${path}.${rowIndex + 1}.${cellIndex}`,
1493
+ type: "table-cell",
1494
+ range: {
1495
+ start: lines[from + 2 + rowIndex].start,
1496
+ end: lines[from + 2 + rowIndex].end
1497
+ },
1498
+ closed: true,
1499
+ props: {},
1500
+ children: parsed.value
1501
+ });
1502
+ citations = parsed.citations;
1503
+ warnings = parsed.warnings;
1504
+ hasWarnedSegmenterUnavailable = parsed.hasWarnedSegmenterUnavailable;
1505
+ }
1506
+ bodyRows.push({
1507
+ path: `${path}.${rowIndex + 1}`,
1508
+ type: "table-row",
1509
+ range: {
1510
+ start: lines[from + 2 + rowIndex].start,
1511
+ end: lines[from + 2 + rowIndex].end
1512
+ },
1513
+ closed: true,
1514
+ props: { isHeader: false },
1515
+ children: cellNodes
1516
+ });
1517
+ }
1518
+ return {
1519
+ value: {
1520
+ path,
1521
+ type: "table",
1522
+ range: {
1523
+ start: lines[from].start,
1524
+ end: lines[i - 1].end
1525
+ },
1526
+ closed: context.isComplete || lines[i - 1].hasNewline,
1527
+ props: { align },
1528
+ children: [headerRow, ...bodyRows]
1529
+ },
1530
+ next: i,
1531
+ citations,
1532
+ warnings,
1533
+ hasWarnedSegmenterUnavailable
1534
+ };
1535
+ }
1536
+ function parseParagraph(lines, from, to, path, context) {
1537
+ const picked = [];
1538
+ let i = from;
1539
+ while (i < to) {
1540
+ if (isBlank(lines[i].text)) break;
1541
+ if (picked.length > 0 && startsNewBlock(lines, i, context.options.enableTables)) break;
1542
+ if (isCitationDefinitionLine(lines[i].text) || isCitationDefinitionPrefixCandidate(lines[i].text, lines[i].hasNewline, context.isComplete)) break;
1543
+ picked.push(lines[i]);
1544
+ i += 1;
1545
+ }
1546
+ const children = parseInline(picked.map((line) => line.text).join("\n"), picked[0].start, path, context);
1547
+ return {
1548
+ value: {
1549
+ path,
1550
+ type: "paragraph",
1551
+ range: {
1552
+ start: picked[0].start,
1553
+ end: picked[picked.length - 1].end
1554
+ },
1555
+ closed: context.isComplete || picked[picked.length - 1].hasNewline,
1556
+ props: {},
1557
+ children: children.value
1558
+ },
1559
+ next: i,
1560
+ citations: children.citations,
1561
+ warnings: children.warnings,
1562
+ hasWarnedSegmenterUnavailable: children.hasWarnedSegmenterUnavailable
1563
+ };
1564
+ }
1565
+
1566
+ //#endregion
1567
+ //#region src/parser/ops.ts
1568
+ /**
1569
+ * Flattens a draft tree into reducer operations.
1570
+ *
1571
+ * @param root - Draft AST root.
1572
+ * @returns Ordered operations for reducer application.
1573
+ */
1574
+ function createAstOpsFromDraft(root) {
1575
+ const ops = [];
1576
+ function visit(node, parentPath) {
1577
+ const childPaths = node.children.map((child, index) => `${node.path}.${index}`);
1578
+ ops.push({
1579
+ kind: "upsert-node",
1580
+ path: node.path,
1581
+ parentPath,
1582
+ type: node.type,
1583
+ range: node.range,
1584
+ closed: node.closed,
1585
+ props: node.props,
1586
+ childPaths
1587
+ });
1588
+ for (let index = 0; index < node.children.length; index += 1) {
1589
+ const child = node.children[index];
1590
+ visit({
1591
+ ...child,
1592
+ path: `${node.path}.${index}`
1593
+ }, node.path);
1594
+ }
1595
+ }
1596
+ visit(root, null);
1597
+ return ops;
1598
+ }
1599
+ /**
1600
+ * Applies AST ops with identity reuse for unchanged nodes.
1601
+ *
1602
+ * @param input - Previous parser state fragments and ops to apply.
1603
+ * @returns Next materialized state fragments.
1604
+ */
1605
+ function applyAstOps(input) {
1606
+ const previousNodeById = new Map(input.previousNodes.map((node) => [node.id, node]));
1607
+ const nodeOps = input.ops.filter((op) => op.kind === "upsert-node");
1608
+ const pathToId = {};
1609
+ let nextId = input.nextId;
1610
+ for (const op of nodeOps) {
1611
+ const id = input.previousPathToId[op.path] ?? nextId++;
1612
+ pathToId[op.path] = id;
1613
+ }
1614
+ const nodesById = /* @__PURE__ */ new Map();
1615
+ for (const op of nodeOps) {
1616
+ const id = pathToId[op.path];
1617
+ const materialized = opToAstNode(op, id, op.parentPath ? pathToId[op.parentPath] : null, op.childPaths.map((childPath) => pathToId[childPath]));
1618
+ const previous = previousNodeById.get(id);
1619
+ const reconciled = previous?.type === "text" && materialized.type === "text" ? reconcileTextNode(previous, materialized) : materialized;
1620
+ nodesById.set(id, previous && astNodeEquals(previous, reconciled) ? previous : reconciled);
1621
+ }
1622
+ const rootPath = nodeOps[0]?.path;
1623
+ if (!rootPath) throw new Error("StreamingMarkdown op stream did not include a root node");
1624
+ const warnings = input.ops.find((op) => op.kind === "set-warnings")?.warnings ?? [];
1625
+ const citations = input.ops.find((op) => op.kind === "set-citations")?.citations ?? {
1626
+ order: [],
1627
+ numbers: {},
1628
+ definitions: {}
1629
+ };
1630
+ const hasWarnedSegmenterUnavailable = input.ops.find((op) => op.kind === "set-segmenter-warning-state")?.hasWarnedSegmenterUnavailable ?? false;
1631
+ return {
1632
+ nodes: Array.from(nodesById.values()).sort((a, b) => a.id - b.id),
1633
+ rootId: pathToId[rootPath],
1634
+ nextId,
1635
+ pathToId,
1636
+ warnings,
1637
+ citations,
1638
+ hasWarnedSegmenterUnavailable
1639
+ };
1640
+ }
1641
+ function opToAstNode(op, id, parentId, children) {
1642
+ const base = {
1643
+ id,
1644
+ type: op.type,
1645
+ parentId,
1646
+ closed: op.closed,
1647
+ range: op.range
1648
+ };
1649
+ switch (op.type) {
1650
+ case "document": return {
1651
+ ...base,
1652
+ type: "document",
1653
+ children
1654
+ };
1655
+ case "paragraph": return {
1656
+ ...base,
1657
+ type: "paragraph",
1658
+ children
1659
+ };
1660
+ case "heading": return {
1661
+ ...base,
1662
+ type: "heading",
1663
+ level: op.props["level"],
1664
+ children
1665
+ };
1666
+ case "blockquote": return {
1667
+ ...base,
1668
+ type: "blockquote",
1669
+ children
1670
+ };
1671
+ case "list": return {
1672
+ ...base,
1673
+ type: "list",
1674
+ ordered: op.props["ordered"],
1675
+ start: op.props["start"],
1676
+ tight: op.props["tight"],
1677
+ children
1678
+ };
1679
+ case "list-item": return {
1680
+ ...base,
1681
+ type: "list-item",
1682
+ children
1683
+ };
1684
+ case "code-block": return {
1685
+ ...base,
1686
+ type: "code-block",
1687
+ fence: op.props["fence"],
1688
+ info: op.props["info"],
1689
+ meta: op.props["meta"],
1690
+ text: op.props["text"]
1691
+ };
1692
+ case "table": return {
1693
+ ...base,
1694
+ type: "table",
1695
+ align: op.props["align"],
1696
+ children
1697
+ };
1698
+ case "table-row": return {
1699
+ ...base,
1700
+ type: "table-row",
1701
+ isHeader: op.props["isHeader"],
1702
+ children
1703
+ };
1704
+ case "table-cell": return {
1705
+ ...base,
1706
+ type: "table-cell",
1707
+ children
1708
+ };
1709
+ case "thematic-break": return {
1710
+ ...base,
1711
+ type: "thematic-break"
1712
+ };
1713
+ case "text": return {
1714
+ ...base,
1715
+ type: "text",
1716
+ text: op.props["text"],
1717
+ segments: op.props["segments"]
1718
+ };
1719
+ case "em": return {
1720
+ ...base,
1721
+ type: "em",
1722
+ children
1723
+ };
1724
+ case "strong": return {
1725
+ ...base,
1726
+ type: "strong",
1727
+ children
1728
+ };
1729
+ case "strikethrough": return {
1730
+ ...base,
1731
+ type: "strikethrough",
1732
+ children
1733
+ };
1734
+ case "inline-code": return {
1735
+ ...base,
1736
+ type: "inline-code",
1737
+ text: op.props["text"]
1738
+ };
1739
+ case "soft-break": return {
1740
+ ...base,
1741
+ type: "soft-break"
1742
+ };
1743
+ case "hard-break": return {
1744
+ ...base,
1745
+ type: "hard-break"
1746
+ };
1747
+ case "link": return {
1748
+ ...base,
1749
+ type: "link",
1750
+ url: op.props["url"],
1751
+ title: op.props["title"],
1752
+ children
1753
+ };
1754
+ case "image": return {
1755
+ ...base,
1756
+ type: "image",
1757
+ url: op.props["url"],
1758
+ title: op.props["title"],
1759
+ alt: op.props["alt"]
1760
+ };
1761
+ case "autolink": return {
1762
+ ...base,
1763
+ type: "autolink",
1764
+ url: op.props["url"],
1765
+ text: op.props["text"]
1766
+ };
1767
+ default: return {
1768
+ ...base,
1769
+ type: "citation",
1770
+ idRef: op.props["idRef"],
1771
+ number: op.props["number"]
1772
+ };
1773
+ }
1774
+ }
1775
+ function astNodeEquals(a, b) {
1776
+ return JSON.stringify(a) === JSON.stringify(b);
1777
+ }
1778
+ function reconcileTextNode(previous, next) {
1779
+ if (next.segments.length === 0 || previous.segments.length === 0) return next;
1780
+ const previousBySignature = groupSegmentsBySignature(previous.segments);
1781
+ let reusedAnySegment = false;
1782
+ const segments = next.segments.map((segment) => {
1783
+ const signature = getSegmentSignature(segment);
1784
+ const bucket = previousBySignature.get(signature);
1785
+ if (!bucket?.length) return segment;
1786
+ const [reused, ...rest] = bucket;
1787
+ if (rest.length) previousBySignature.set(signature, rest);
1788
+ else previousBySignature.delete(signature);
1789
+ reusedAnySegment = true;
1790
+ return reused;
1791
+ });
1792
+ if (areSegmentArraysIdentical(previous.segments, segments)) return {
1793
+ ...next,
1794
+ segments: previous.segments
1795
+ };
1796
+ if (!reusedAnySegment) return next;
1797
+ return {
1798
+ ...next,
1799
+ segments
1800
+ };
1801
+ }
1802
+ /**
1803
+ * Returns true when both arrays contain the same segment object references in order.
1804
+ */
1805
+ function areSegmentArraysIdentical(a, b) {
1806
+ if (a.length !== b.length) return false;
1807
+ return a.every((segment, index) => segment === b[index]);
1808
+ }
1809
+ /**
1810
+ * Produces a stable signature for matching segments across immutable parse iterations.
1811
+ */
1812
+ function getSegmentSignature(segment) {
1813
+ return `${segment.start}:${segment.end}:${segment.text}:${segment.kind}:${String(segment.isWhitespace)}:${String(segment.noBreakBefore === true)}`;
1814
+ }
1815
+ /**
1816
+ * Groups previous segments by signature to support one-to-one identity reuse.
1817
+ */
1818
+ function groupSegmentsBySignature(segments) {
1819
+ const bySignature = /* @__PURE__ */ new Map();
1820
+ for (const segment of segments) {
1821
+ const signature = getSegmentSignature(segment);
1822
+ const existing = bySignature.get(signature) ?? [];
1823
+ bySignature.set(signature, [...existing, segment]);
1824
+ }
1825
+ return bySignature;
1826
+ }
1827
+
1828
+ //#endregion
1829
+ //#region src/parser/state.ts
1830
+ /**
1831
+ * Creates a new streaming markdown parser state.
1832
+ *
1833
+ * @param options - Optional parser option overrides.
1834
+ * @returns New immutable parser state.
1835
+ * @public
1836
+ */
1837
+ function createStreamingMarkdownParserState(options) {
1838
+ return {
1839
+ nextId: 2,
1840
+ nodes: [{
1841
+ id: 1,
1842
+ type: "document",
1843
+ parentId: null,
1844
+ closed: false,
1845
+ range: {
1846
+ start: 0,
1847
+ end: 0
1848
+ },
1849
+ children: []
1850
+ }],
1851
+ rootId: 1,
1852
+ stack: [1],
1853
+ mode: "block",
1854
+ warnings: [],
1855
+ citations: {
1856
+ order: [],
1857
+ numbers: {},
1858
+ definitions: {}
1859
+ },
1860
+ lineBuffer: "",
1861
+ isComplete: false,
1862
+ index: 0,
1863
+ line: 1,
1864
+ column: 1,
1865
+ options: normalizeOptions(options),
1866
+ source: "",
1867
+ pathToId: { "0": 1 },
1868
+ pendingCarriageReturn: false,
1869
+ hasWarnedSegmenterUnavailable: false
1870
+ };
1871
+ }
1872
+ /**
1873
+ * Parses a streaming chunk and returns the next immutable parser state.
1874
+ *
1875
+ * @param state - Current parser state.
1876
+ * @param chunk - Incoming text chunk.
1877
+ * @returns Updated parser state.
1878
+ * @public
1879
+ */
1880
+ function parseStreamingMarkdownChunk(state, chunk) {
1881
+ const normalized = normalizeChunk(chunk, state.pendingCarriageReturn);
1882
+ return rebuildState(state, {
1883
+ source: state.source + normalized.text,
1884
+ isComplete: false,
1885
+ pendingCarriageReturn: normalized.pendingCarriageReturn
1886
+ });
1887
+ }
1888
+ /**
1889
+ * Finalizes parsing by closing any provisional nodes and flushing trailing CR.
1890
+ *
1891
+ * @param state - Current parser state.
1892
+ * @returns Final parser state.
1893
+ * @public
1894
+ */
1895
+ function finalizeStreamingMarkdown(state) {
1896
+ if (state.isComplete) return state;
1897
+ return rebuildState(state, {
1898
+ source: state.source + (state.pendingCarriageReturn ? "\n" : ""),
1899
+ isComplete: true,
1900
+ pendingCarriageReturn: false
1901
+ });
1902
+ }
1903
+ function rebuildState(prev, input) {
1904
+ const definitions = parseCitationDefinitions(input.source);
1905
+ const parseContext = {
1906
+ options: prev.options,
1907
+ warnings: definitions.warnings,
1908
+ citations: definitions.citations,
1909
+ isComplete: input.isComplete,
1910
+ hasWarnedSegmenterUnavailable: prev.hasWarnedSegmenterUnavailable
1911
+ };
1912
+ const lines = splitLines(input.source);
1913
+ const parsedBlocks = parseBlocks(lines, 0, lines.length, "0", parseContext, true);
1914
+ const ops = [
1915
+ {
1916
+ kind: "set-warnings",
1917
+ warnings: parsedBlocks.warnings
1918
+ },
1919
+ {
1920
+ kind: "set-citations",
1921
+ citations: parsedBlocks.citations
1922
+ },
1923
+ {
1924
+ kind: "set-segmenter-warning-state",
1925
+ hasWarnedSegmenterUnavailable: parsedBlocks.hasWarnedSegmenterUnavailable
1926
+ },
1927
+ ...createAstOpsFromDraft(parsedBlocks.value)
1928
+ ];
1929
+ const reduced = applyAstOps({
1930
+ previousNodes: prev.nodes,
1931
+ previousPathToId: prev.pathToId,
1932
+ nextId: prev.nextId,
1933
+ ops
1934
+ });
1935
+ const lineBuffer = getLineBuffer(input.source);
1936
+ const { line, column } = toLineColumn(input.source, input.source.length);
1937
+ return {
1938
+ ...prev,
1939
+ nextId: reduced.nextId,
1940
+ nodes: reduced.nodes,
1941
+ rootId: reduced.rootId,
1942
+ stack: computeOpenStack(reduced.nodes, reduced.rootId),
1943
+ mode: inferMode(reduced.nodes, reduced.rootId),
1944
+ warnings: reduced.warnings,
1945
+ citations: reduced.citations,
1946
+ lineBuffer,
1947
+ isComplete: input.isComplete,
1948
+ index: input.source.length,
1949
+ line,
1950
+ column,
1951
+ source: input.source,
1952
+ pathToId: reduced.pathToId,
1953
+ pendingCarriageReturn: input.pendingCarriageReturn,
1954
+ hasWarnedSegmenterUnavailable: reduced.hasWarnedSegmenterUnavailable
1955
+ };
1956
+ }
1957
+ function computeOpenStack(nodes, rootId) {
1958
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
1959
+ const stack = [];
1960
+ let cursor = rootId;
1961
+ while (cursor != null) {
1962
+ const node = nodeById.get(cursor);
1963
+ if (!node || node.closed) break;
1964
+ stack.push(cursor);
1965
+ if (!("children" in node) || node.children.length === 0) break;
1966
+ const lastChild = node.children[node.children.length - 1];
1967
+ cursor = typeof lastChild === "number" ? lastChild : null;
1968
+ }
1969
+ return stack;
1970
+ }
1971
+ function inferMode(nodes, rootId) {
1972
+ if (rootId == null) return "block";
1973
+ const root = nodes.find((node) => node.id === rootId);
1974
+ if (!root || !("children" in root) || root.children.length === 0) return "block";
1975
+ const lastChild = nodes.find((node) => node.id === root.children[root.children.length - 1]);
1976
+ if (!lastChild || lastChild.closed) return "block";
1977
+ if (lastChild.type === "paragraph") return "paragraph";
1978
+ if (lastChild.type === "heading") return "heading";
1979
+ if (lastChild.type === "blockquote") return "blockquote";
1980
+ if (lastChild.type === "list-item") return "list-item";
1981
+ if (lastChild.type === "code-block") return "code-fence";
1982
+ if (lastChild.type === "table") return "table";
1983
+ return "block";
1984
+ }
1985
+
1986
+ //#endregion
1987
+ //#region src/dev-warning.ts
1988
+ let warned = false;
1989
+ const MIGRATION_GUIDE_URL = "https://docs.copilotkit.ai/migrate/markdown-renderer#migrate-with-your-ai-assistant";
1990
+ const HIGHLIGHTABLE_CODE = /(^|\n)[ \t]*(```|~~~)[ \t]*[A-Za-z][\w+#-]*/;
1991
+ const BLOCK_MATH = /\$\$[\s\S]+?\$\$/;
1992
+ const BRACKET_MATH = /\\\[[\s\S]+?\\\]/;
1993
+ function isProduction() {
1994
+ return typeof process !== "undefined" && process.env != null && process.env.NODE_ENV === "production";
1995
+ }
1996
+ /**
1997
+ * Dev-only, once-per-session warning emitted when CopilotKit's built-in default
1998
+ * markdown renderer encounters markdown it won't render richly — math
1999
+ * (`$$…$$` / `\[…\]`) or a language-tagged fenced code block (rendered as plain,
2000
+ * un-highlighted code). Helps people upgrading from the bundled Streamdown
2001
+ * default notice the behavioral change and points them at the migration guide.
2002
+ *
2003
+ * No-op in production and after the first warning. Only the built-in default
2004
+ * renderers call this — a custom `markdownRenderer` never triggers it.
2005
+ *
2006
+ * @internal
2007
+ */
2008
+ function warnUnsupportedRichSyntaxOnce(content) {
2009
+ if (warned || !content || isProduction()) return;
2010
+ const hasMath = BLOCK_MATH.test(content) || BRACKET_MATH.test(content);
2011
+ const hasHighlightableCode = HIGHLIGHTABLE_CODE.test(content);
2012
+ if (!hasMath && !hasHighlightableCode) return;
2013
+ warned = true;
2014
+ const [feature, missing] = hasMath ? ["math", "math typesetting"] : ["a syntax-highlighted code block", "syntax highlighting"];
2015
+ console.warn(`[CopilotKit] The built-in markdown renderer rendered ${feature} without ${missing}. It supports GFM, tables, autolinks, and citations, but not math, syntax highlighting, or diagrams. To restore those, plug in a custom markdownRenderer (e.g. react-markdown or streamdown) — or paste the migration prompt into your AI assistant: ${MIGRATION_GUIDE_URL}`);
2016
+ }
2017
+
2018
+ //#endregion
2019
+ exports.createStreamingMarkdownParserState = createStreamingMarkdownParserState;
2020
+ exports.finalizeStreamingMarkdown = finalizeStreamingMarkdown;
2021
+ exports.parseStreamingMarkdownChunk = parseStreamingMarkdownChunk;
2022
+ exports.warnUnsupportedRichSyntaxOnce = warnUnsupportedRichSyntaxOnce;
2023
+ //# sourceMappingURL=index.cjs.map