@fuzdev/mdz 0.2.0 → 0.4.0
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/README.md +4 -0
- package/dist/Mdz.svelte +18 -9
- package/dist/Mdz.svelte.d.ts +9 -2
- package/dist/Mdz.svelte.d.ts.map +1 -1
- package/dist/MdzNodeView.svelte +129 -115
- package/dist/MdzNodeView.svelte.d.ts +4 -1
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzPrecompiled.svelte +3 -3
- package/dist/MdzPrecompiled.svelte.d.ts.map +1 -1
- package/dist/MdzRoot.svelte +3 -3
- package/dist/MdzRoot.svelte.d.ts.map +1 -1
- package/dist/MdzStream.svelte +6 -6
- package/dist/MdzStream.svelte.d.ts.map +1 -1
- package/dist/MdzStreamNodeView.svelte +129 -118
- package/dist/MdzStreamNodeView.svelte.d.ts +4 -1
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
- package/dist/context_helpers.js +1 -1
- package/dist/mdz.d.ts +6 -0
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +8 -0
- package/dist/mdz_contexts.d.ts.map +1 -1
- package/dist/mdz_debug_work.d.ts +38 -0
- package/dist/mdz_debug_work.d.ts.map +1 -0
- package/dist/mdz_debug_work.js +37 -0
- package/dist/mdz_helpers.d.ts +61 -0
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +74 -2
- package/dist/mdz_lexer.d.ts +12 -0
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +190 -40
- package/dist/mdz_opcodes.d.ts.map +1 -1
- package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
- package/dist/mdz_opcodes_to_nodes.js +22 -17
- package/dist/mdz_stream_parser.d.ts.map +1 -1
- package/dist/mdz_stream_parser.js +8 -8
- package/dist/mdz_stream_parser_block.js +5 -5
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
- package/dist/mdz_stream_parser_inline.js +22 -20
- package/dist/mdz_stream_parser_link.d.ts.map +1 -1
- package/dist/mdz_stream_parser_link.js +88 -56
- package/dist/mdz_stream_parser_list.d.ts.map +1 -1
- package/dist/mdz_stream_parser_list.js +23 -9
- package/dist/mdz_stream_parser_state.d.ts +29 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -1
- package/dist/mdz_stream_parser_state.js +59 -10
- package/dist/mdz_stream_parser_table.d.ts +3 -2
- package/dist/mdz_stream_parser_table.d.ts.map +1 -1
- package/dist/mdz_stream_parser_table.js +24 -13
- package/dist/mdz_stream_parser_text.d.ts.map +1 -1
- package/dist/mdz_stream_parser_text.js +36 -25
- package/dist/mdz_stream_parser_url.js +7 -7
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.d.ts +1 -1
- package/dist/mdz_to_svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.js +28 -31
- package/dist/mdz_token_parser.d.ts.map +1 -1
- package/dist/mdz_token_parser.js +24 -25
- package/dist/svelte_preprocess_mdz.d.ts +2 -2
- package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
- package/dist/svelte_preprocess_mdz.js +41 -47
- package/package.json +28 -40
- package/src/lib/context_helpers.ts +2 -2
- package/src/lib/mdz.ts +11 -2
- package/src/lib/mdz_contexts.ts +5 -5
- package/src/lib/mdz_debug_work.ts +38 -0
- package/src/lib/mdz_helpers.ts +126 -23
- package/src/lib/mdz_lexer.ts +243 -93
- package/src/lib/mdz_opcodes.ts +1 -1
- package/src/lib/mdz_opcodes_to_nodes.ts +35 -30
- package/src/lib/mdz_stream_parser.ts +17 -17
- package/src/lib/mdz_stream_parser_block.ts +14 -14
- package/src/lib/mdz_stream_parser_inline.ts +38 -34
- package/src/lib/mdz_stream_parser_link.ts +99 -66
- package/src/lib/mdz_stream_parser_list.ts +41 -29
- package/src/lib/mdz_stream_parser_state.ts +100 -28
- package/src/lib/mdz_stream_parser_table.ts +47 -36
- package/src/lib/mdz_stream_parser_text.ts +40 -39
- package/src/lib/mdz_stream_parser_url.ts +11 -11
- package/src/lib/mdz_stream_state.svelte.ts +8 -8
- package/src/lib/mdz_to_svelte.ts +38 -46
- package/src/lib/mdz_token_parser.ts +55 -58
- package/src/lib/svelte_preprocess_mdz.ts +92 -89
- package/src/lib/tsdoc_mdz.ts +1 -1
package/src/lib/mdz_lexer.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
HR_HYPHEN_COUNT,
|
|
39
39
|
MIN_CODEBLOCK_BACKTICKS,
|
|
40
40
|
MAX_HEADING_LEVEL,
|
|
41
|
+
MAX_INLINE_NESTING_DEPTH,
|
|
41
42
|
MAX_LIST_NUMBER_DIGITS,
|
|
42
43
|
match_url_prefix_case_insensitive,
|
|
43
44
|
is_at_absolute_path,
|
|
@@ -50,10 +51,15 @@ import {
|
|
|
50
51
|
mdz_split_table_row,
|
|
51
52
|
mdz_parse_table_delimiter,
|
|
52
53
|
PIPE,
|
|
53
|
-
|
|
54
|
+
TEXT_CLASS_BACKSLASH,
|
|
55
|
+
TEXT_CLASS_NEWLINE,
|
|
56
|
+
TEXT_CLASS_STOP,
|
|
57
|
+
TEXT_CLASS_URL,
|
|
58
|
+
TEXT_SCAN_CLASS
|
|
54
59
|
} from './mdz_helpers.ts';
|
|
55
60
|
|
|
56
|
-
import type {MdzTableAlign} from './mdz.ts';
|
|
61
|
+
import type { MdzTableAlign } from './mdz.ts';
|
|
62
|
+
import { mdz_debug_work } from './mdz_debug_work.ts';
|
|
57
63
|
|
|
58
64
|
//
|
|
59
65
|
// Token types
|
|
@@ -289,8 +295,8 @@ export class MdzLexer {
|
|
|
289
295
|
#index: number = 0;
|
|
290
296
|
#tokens: Array<MdzToken> = [];
|
|
291
297
|
#max_search_index: number = Number.MAX_SAFE_INTEGER;
|
|
292
|
-
#search_memo: Map<string, {from: number; result: number}> = new Map();
|
|
293
|
-
#break_memo: {from: number; result: number} | null = null;
|
|
298
|
+
#search_memo: Map<string, { from: number; result: number }> = new Map();
|
|
299
|
+
#break_memo: { from: number; result: number } | null = null;
|
|
294
300
|
/**
|
|
295
301
|
* Whether `#tokenize_text` is inside a list-item inline run: single
|
|
296
302
|
* newlines are soft breaks whose following structural indent strips from
|
|
@@ -304,11 +310,69 @@ export class MdzLexer {
|
|
|
304
310
|
* and `\|` unescapes to a literal pipe.
|
|
305
311
|
*/
|
|
306
312
|
#in_table_cell: boolean = false;
|
|
313
|
+
/**
|
|
314
|
+
* Start offsets of markdown-link opens (`[`) that definitively failed — so
|
|
315
|
+
* a re-lex from before the `[` short-circuits to literal text instead of
|
|
316
|
+
* re-attempting (and re-descending into) the same failing link. Without it,
|
|
317
|
+
* the revert-and-re-tokenize failure path is exponential on nested `[`
|
|
318
|
+
* (a ~30-byte `[`×n input hangs). A link's outcome (does a valid `](ref)`
|
|
319
|
+
* follow?) is independent of `#max_search_index`, so `start` alone keys it.
|
|
320
|
+
* The outcome is a pure function of the immutable text, so the memo is valid
|
|
321
|
+
* for the whole lex and across table-cell ranges; cleared on `rebind`.
|
|
322
|
+
*/
|
|
323
|
+
#failed_link_starts: Set<number> = new Set();
|
|
324
|
+
/**
|
|
325
|
+
* The tag analogue of `#failed_link_starts`, keyed `${start}:${bound}`. A
|
|
326
|
+
* tag's closer search respects `#max_search_index`, so the same `start` can
|
|
327
|
+
* fail under a narrow bound yet succeed under the wider one a re-lex uses —
|
|
328
|
+
* the bound is part of the key.
|
|
329
|
+
*/
|
|
330
|
+
#failed_tag: Set<string> = new Set();
|
|
331
|
+
/**
|
|
332
|
+
* Current link/tag nesting depth — the number of open
|
|
333
|
+
* `link_text_open`/`tag_open` containers being recursed into. Bumped around
|
|
334
|
+
* `#tokenize_markdown_link`/`#tokenize_tag`'s children loops (not by
|
|
335
|
+
* delimiters, which can't nest deeply — see `MAX_INLINE_NESTING_DEPTH`). At
|
|
336
|
+
* the cap a further `[`/`<` renders literal instead of opening, bounding
|
|
337
|
+
* recursion depth (no stack overflow) and, with the failure memos, the
|
|
338
|
+
* revert re-scan (linear, not quadratic) on adversarial nesting.
|
|
339
|
+
*/
|
|
340
|
+
#inline_depth: number = 0;
|
|
341
|
+
/**
|
|
342
|
+
* Start offsets of `[`/`<` openers suppressed by the nesting-depth cap
|
|
343
|
+
* (`#inline_depth >= MAX_INLINE_NESTING_DEPTH`) — position-keyed like
|
|
344
|
+
* `#failed_link_starts` so a re-lex from a shallower depth short-circuits to
|
|
345
|
+
* literal text rather than re-opening the construct (which would reintroduce
|
|
346
|
+
* the quadratic the cap removes). A suppression is bound-independent, so
|
|
347
|
+
* `start` alone keys it; cleared on `rebind`.
|
|
348
|
+
*/
|
|
349
|
+
#depth_capped: Set<number> = new Set();
|
|
307
350
|
|
|
308
351
|
constructor(text: string) {
|
|
309
352
|
this.#text = text;
|
|
310
353
|
}
|
|
311
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Rebind to a new text, so one lexer instance can be reused across inputs —
|
|
357
|
+
* the streaming table path reuses one across a table's rows via
|
|
358
|
+
* `MdzTableCellParser` instead of allocating a lexer (and its memo `Map`)
|
|
359
|
+
* per row. The search memos survive when the text is identical (they're
|
|
360
|
+
* valid over the immutable text — the common case for rows parsed from one
|
|
361
|
+
* buffered chunk) and clear otherwise. Per-parse cursor state is reset by
|
|
362
|
+
* `lex_table_cell` itself.
|
|
363
|
+
*
|
|
364
|
+
* @nodocs
|
|
365
|
+
*/
|
|
366
|
+
rebind(text: string): void {
|
|
367
|
+
if (this.#text === text) return;
|
|
368
|
+
this.#text = text;
|
|
369
|
+
this.#search_memo.clear();
|
|
370
|
+
this.#break_memo = null;
|
|
371
|
+
this.#failed_link_starts.clear();
|
|
372
|
+
this.#failed_tag.clear();
|
|
373
|
+
this.#depth_capped.clear();
|
|
374
|
+
}
|
|
375
|
+
|
|
312
376
|
/**
|
|
313
377
|
* Memoized `String#indexOf` over the immutable text.
|
|
314
378
|
*
|
|
@@ -325,8 +389,12 @@ export class MdzLexer {
|
|
|
325
389
|
return memo.result;
|
|
326
390
|
}
|
|
327
391
|
const result = this.#text.indexOf(needle, from);
|
|
392
|
+
if (mdz_debug_work.enabled) {
|
|
393
|
+
// scan span: `indexOf` examined [from, result] (found) or [from, EOF)
|
|
394
|
+
mdz_debug_work.total += (result === -1 ? this.#text.length : result) - from;
|
|
395
|
+
}
|
|
328
396
|
if (memo === undefined) {
|
|
329
|
-
this.#search_memo.set(needle, {from, result});
|
|
397
|
+
this.#search_memo.set(needle, { from, result });
|
|
330
398
|
} else {
|
|
331
399
|
memo.from = from;
|
|
332
400
|
memo.result = result;
|
|
@@ -353,7 +421,7 @@ export class MdzLexer {
|
|
|
353
421
|
if (this.#is_at_paragraph_break()) {
|
|
354
422
|
const start = this.#index;
|
|
355
423
|
this.#skip_blank_lines();
|
|
356
|
-
this.#tokens.push({type: 'paragraph_break', start, end: this.#index});
|
|
424
|
+
this.#tokens.push({ type: 'paragraph_break', start, end: this.#index });
|
|
357
425
|
continue;
|
|
358
426
|
}
|
|
359
427
|
|
|
@@ -376,6 +444,9 @@ export class MdzLexer {
|
|
|
376
444
|
*/
|
|
377
445
|
lex_table_cell(cell_start: number, cell_end: number): Array<MdzToken> {
|
|
378
446
|
this.#tokens = [];
|
|
447
|
+
// depth is balanced back to 0 per cell, but pin it so a future early-return
|
|
448
|
+
// between an `#inline_depth++` and its decrement can't leak into the next cell
|
|
449
|
+
this.#inline_depth = 0;
|
|
379
450
|
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
380
451
|
return this.#tokens;
|
|
381
452
|
}
|
|
@@ -469,7 +540,7 @@ export class MdzLexer {
|
|
|
469
540
|
type: 'heading_start',
|
|
470
541
|
level: hash_count as 1 | 2 | 3 | 4 | 5 | 6,
|
|
471
542
|
start,
|
|
472
|
-
end: this.#index
|
|
543
|
+
end: this.#index // end of "## " prefix
|
|
473
544
|
});
|
|
474
545
|
|
|
475
546
|
// Find end-of-line to bound nested tokenizers (prevents tag scanner from scanning past heading)
|
|
@@ -509,7 +580,7 @@ export class MdzLexer {
|
|
|
509
580
|
this.#max_search_index = saved_max;
|
|
510
581
|
|
|
511
582
|
// Emit heading_end marker so the token parser knows where heading content stops
|
|
512
|
-
this.#tokens.push({type: 'heading_end', start: this.#index, end: this.#index});
|
|
583
|
+
this.#tokens.push({ type: 'heading_end', start: this.#index, end: this.#index });
|
|
513
584
|
|
|
514
585
|
// Skip newlines after heading
|
|
515
586
|
this.#skip_blank_lines();
|
|
@@ -542,7 +613,7 @@ export class MdzLexer {
|
|
|
542
613
|
const start = this.#index;
|
|
543
614
|
this.#index = i; // consume up to but not including newline
|
|
544
615
|
|
|
545
|
-
this.#tokens.push({type: 'hr', start, end: this.#index});
|
|
616
|
+
this.#tokens.push({ type: 'hr', start, end: this.#index });
|
|
546
617
|
|
|
547
618
|
// Skip newlines after HR
|
|
548
619
|
this.#skip_blank_lines();
|
|
@@ -570,7 +641,7 @@ export class MdzLexer {
|
|
|
570
641
|
* Match a quote prefix in the complete sync text — never `'pending'`.
|
|
571
642
|
* See `mdz_match_blockquote_prefix`.
|
|
572
643
|
*/
|
|
573
|
-
#match_blockquote_prefix(i: number): {depth: number; content_start: number} | null {
|
|
644
|
+
#match_blockquote_prefix(i: number): { depth: number; content_start: number } | null {
|
|
574
645
|
const prefix = mdz_match_blockquote_prefix(this.#text, i, true);
|
|
575
646
|
return prefix === 'pending' ? null : prefix;
|
|
576
647
|
}
|
|
@@ -623,7 +694,7 @@ export class MdzLexer {
|
|
|
623
694
|
// quote's content parses exactly like a standalone document (a complete
|
|
624
695
|
// fence opener line on the last quote line opens an empty fence, not text)
|
|
625
696
|
const parts: Array<string> = [];
|
|
626
|
-
const segments: Array<{inner_start: number; source_start: number}> = [];
|
|
697
|
+
const segments: Array<{ inner_start: number; source_start: number }> = [];
|
|
627
698
|
let inner_len = 0;
|
|
628
699
|
let line_first = first;
|
|
629
700
|
let last_line_end = first;
|
|
@@ -631,7 +702,7 @@ export class MdzLexer {
|
|
|
631
702
|
const strip = line_first + mdz_blockquote_strip_width(this.#text, line_first);
|
|
632
703
|
let nl = this.#text.indexOf('\n', line_first);
|
|
633
704
|
if (nl === -1) nl = this.#text.length;
|
|
634
|
-
segments.push({inner_start: inner_len, source_start: strip});
|
|
705
|
+
segments.push({ inner_start: inner_len, source_start: strip });
|
|
635
706
|
const content = this.#text.slice(Math.min(strip, nl), nl < this.#text.length ? nl + 1 : nl);
|
|
636
707
|
parts.push(content);
|
|
637
708
|
inner_len += content.length;
|
|
@@ -656,14 +727,14 @@ export class MdzLexer {
|
|
|
656
727
|
line_first = next_first;
|
|
657
728
|
}
|
|
658
729
|
|
|
659
|
-
this.#tokens.push({type: 'blockquote_open', start: first, end: first});
|
|
730
|
+
this.#tokens.push({ type: 'blockquote_open', start: first, end: first });
|
|
660
731
|
const inner_tokens = new MdzLexer(parts.join('')).tokenize();
|
|
661
732
|
for (const token of inner_tokens) {
|
|
662
733
|
token.start = mdz_remap_segments(segments, token.start);
|
|
663
734
|
token.end = mdz_remap_segments(segments, token.end);
|
|
664
735
|
this.#tokens.push(token);
|
|
665
736
|
}
|
|
666
|
-
this.#tokens.push({type: 'blockquote_close', start: last_line_end, end: last_line_end});
|
|
737
|
+
this.#tokens.push({ type: 'blockquote_close', start: last_line_end, end: last_line_end });
|
|
667
738
|
this.#index = last_line_end;
|
|
668
739
|
}
|
|
669
740
|
|
|
@@ -692,7 +763,7 @@ export class MdzLexer {
|
|
|
692
763
|
if (align === null || align.length !== header_cells.length) return false;
|
|
693
764
|
|
|
694
765
|
// committed — emit the table; the delimiter row is structural (not emitted)
|
|
695
|
-
this.#tokens.push({type: 'table_open', align, start: table_start, end: header_end});
|
|
766
|
+
this.#tokens.push({ type: 'table_open', align, start: table_start, end: header_end });
|
|
696
767
|
this.#emit_table_row(header_cells, true, table_start, header_end);
|
|
697
768
|
|
|
698
769
|
this.#index = delim_end;
|
|
@@ -710,23 +781,23 @@ export class MdzLexer {
|
|
|
710
781
|
table_end = row_end;
|
|
711
782
|
}
|
|
712
783
|
|
|
713
|
-
this.#tokens.push({type: 'table_close', start: table_end, end: table_end});
|
|
784
|
+
this.#tokens.push({ type: 'table_close', start: table_end, end: table_end });
|
|
714
785
|
this.#skip_blank_lines();
|
|
715
786
|
return true;
|
|
716
787
|
}
|
|
717
788
|
|
|
718
789
|
/** Emit one table row's open / cell / close tokens. */
|
|
719
790
|
#emit_table_row(
|
|
720
|
-
cells: Array<{start: number; end: number}>,
|
|
791
|
+
cells: Array<{ start: number; end: number }>,
|
|
721
792
|
header: boolean,
|
|
722
793
|
row_start: number,
|
|
723
|
-
row_end: number
|
|
794
|
+
row_end: number
|
|
724
795
|
): void {
|
|
725
|
-
this.#tokens.push({type: 'table_row_open', header, start: row_start, end: row_start});
|
|
796
|
+
this.#tokens.push({ type: 'table_row_open', header, start: row_start, end: row_start });
|
|
726
797
|
for (const cell of cells) {
|
|
727
798
|
this.#emit_table_cell(cell.start, cell.end);
|
|
728
799
|
}
|
|
729
|
-
this.#tokens.push({type: 'table_row_close', start: row_end, end: row_end});
|
|
800
|
+
this.#tokens.push({ type: 'table_row_close', start: row_end, end: row_end });
|
|
730
801
|
}
|
|
731
802
|
|
|
732
803
|
/**
|
|
@@ -736,9 +807,9 @@ export class MdzLexer {
|
|
|
736
807
|
* stops at that bound and unescapes `\|`.
|
|
737
808
|
*/
|
|
738
809
|
#emit_table_cell(cell_start: number, cell_end: number): void {
|
|
739
|
-
this.#tokens.push({type: 'table_cell_open', start: cell_start, end: cell_start});
|
|
810
|
+
this.#tokens.push({ type: 'table_cell_open', start: cell_start, end: cell_start });
|
|
740
811
|
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
741
|
-
this.#tokens.push({type: 'table_cell_close', start: cell_end, end: cell_end});
|
|
812
|
+
this.#tokens.push({ type: 'table_cell_close', start: cell_end, end: cell_end });
|
|
742
813
|
}
|
|
743
814
|
|
|
744
815
|
/**
|
|
@@ -767,9 +838,9 @@ export class MdzLexer {
|
|
|
767
838
|
*/
|
|
768
839
|
#match_item_table_head(
|
|
769
840
|
first: number,
|
|
770
|
-
marker_indent: number
|
|
841
|
+
marker_indent: number
|
|
771
842
|
): {
|
|
772
|
-
header_cells: Array<{start: number; end: number}>;
|
|
843
|
+
header_cells: Array<{ start: number; end: number }>;
|
|
773
844
|
header_end: number;
|
|
774
845
|
align: Array<MdzTableAlign>;
|
|
775
846
|
delim_end: number;
|
|
@@ -786,7 +857,7 @@ export class MdzLexer {
|
|
|
786
857
|
if (delim_end === -1) delim_end = this.#text.length;
|
|
787
858
|
const align = mdz_parse_table_delimiter(this.#text, dj, delim_end);
|
|
788
859
|
if (align === null || align.length !== header_cells.length) return null;
|
|
789
|
-
return {header_cells, header_end, align, delim_end};
|
|
860
|
+
return { header_cells, header_end, align, delim_end };
|
|
790
861
|
}
|
|
791
862
|
|
|
792
863
|
/** Whether a table opens at `first` as a block child of an item at `marker_indent`. */
|
|
@@ -803,7 +874,12 @@ export class MdzLexer {
|
|
|
803
874
|
*/
|
|
804
875
|
#tokenize_item_table(first: number, marker_indent: number): void {
|
|
805
876
|
const head = this.#match_item_table_head(first, marker_indent)!;
|
|
806
|
-
this.#tokens.push({
|
|
877
|
+
this.#tokens.push({
|
|
878
|
+
type: 'table_open',
|
|
879
|
+
align: head.align,
|
|
880
|
+
start: first,
|
|
881
|
+
end: head.header_end
|
|
882
|
+
});
|
|
807
883
|
this.#emit_table_row(head.header_cells, true, first, head.header_end);
|
|
808
884
|
this.#index = head.delim_end;
|
|
809
885
|
let table_end = head.delim_end;
|
|
@@ -821,7 +897,7 @@ export class MdzLexer {
|
|
|
821
897
|
this.#index = row_end;
|
|
822
898
|
table_end = row_end;
|
|
823
899
|
}
|
|
824
|
-
this.#tokens.push({type: 'table_close', start: table_end, end: table_end});
|
|
900
|
+
this.#tokens.push({ type: 'table_close', start: table_end, end: table_end });
|
|
825
901
|
}
|
|
826
902
|
|
|
827
903
|
// -- List tokenizers --
|
|
@@ -893,7 +969,7 @@ export class MdzLexer {
|
|
|
893
969
|
}
|
|
894
970
|
if (levels[levels.length - 1]!.ordered === marker.ordered) {
|
|
895
971
|
// sibling item
|
|
896
|
-
this.#tokens.push({type: 'list_item_close', start: first, end: first});
|
|
972
|
+
this.#tokens.push({ type: 'list_item_close', start: first, end: first });
|
|
897
973
|
this.#list_item_open(marker);
|
|
898
974
|
} else {
|
|
899
975
|
// marker-type switch: close the list, open a new one at this indent
|
|
@@ -909,13 +985,13 @@ export class MdzLexer {
|
|
|
909
985
|
// empty item: only at an indent exactly matching an open level of
|
|
910
986
|
// the same marker type — anything else degrades below
|
|
911
987
|
const level_idx = levels.findIndex(
|
|
912
|
-
(l) => l.indent === indent && l.ordered === marker.ordered
|
|
988
|
+
(l) => l.indent === indent && l.ordered === marker.ordered
|
|
913
989
|
);
|
|
914
990
|
if (level_idx !== -1) {
|
|
915
991
|
while (levels.length - 1 > level_idx) {
|
|
916
992
|
this.#list_close(levels);
|
|
917
993
|
}
|
|
918
|
-
this.#tokens.push({type: 'list_item_close', start: first, end: first});
|
|
994
|
+
this.#tokens.push({ type: 'list_item_close', start: first, end: first });
|
|
919
995
|
this.#list_item_open(marker);
|
|
920
996
|
// consume the marker and any trailing whitespace, leaving #index
|
|
921
997
|
// at the line's newline (or EOF)
|
|
@@ -985,7 +1061,7 @@ export class MdzLexer {
|
|
|
985
1061
|
while (levels.length - 1 > target_idx) {
|
|
986
1062
|
this.#list_close(levels);
|
|
987
1063
|
}
|
|
988
|
-
this.#tokens.push({type: 'paragraph_break', start: line_start, end: line_start});
|
|
1064
|
+
this.#tokens.push({ type: 'paragraph_break', start: line_start, end: line_start });
|
|
989
1065
|
this.#list_run(levels, first);
|
|
990
1066
|
continue;
|
|
991
1067
|
}
|
|
@@ -1020,13 +1096,13 @@ export class MdzLexer {
|
|
|
1020
1096
|
start: i,
|
|
1021
1097
|
marker_end: after,
|
|
1022
1098
|
content_start: after,
|
|
1023
|
-
empty: true
|
|
1099
|
+
empty: true
|
|
1024
1100
|
};
|
|
1025
1101
|
}
|
|
1026
1102
|
if (this.#text.charCodeAt(after) !== SPACE) return null;
|
|
1027
1103
|
const content_start = after + 1;
|
|
1028
1104
|
const empty = this.#rest_is_blank(content_start);
|
|
1029
|
-
return {ordered: false, number: 0, start: i, marker_end: after, content_start, empty};
|
|
1105
|
+
return { ordered: false, number: 0, start: i, marker_end: after, content_start, empty };
|
|
1030
1106
|
}
|
|
1031
1107
|
if (c >= ZERO && c <= NINE) {
|
|
1032
1108
|
let j = i + 1;
|
|
@@ -1048,13 +1124,13 @@ export class MdzLexer {
|
|
|
1048
1124
|
start: i,
|
|
1049
1125
|
marker_end: after,
|
|
1050
1126
|
content_start: after,
|
|
1051
|
-
empty: true
|
|
1127
|
+
empty: true
|
|
1052
1128
|
};
|
|
1053
1129
|
}
|
|
1054
1130
|
if (this.#text.charCodeAt(after) !== SPACE) return null;
|
|
1055
1131
|
const content_start = after + 1;
|
|
1056
1132
|
const empty = this.#rest_is_blank(content_start);
|
|
1057
|
-
return {ordered: true, number, start: i, marker_end: after, content_start, empty};
|
|
1133
|
+
return { ordered: true, number, start: i, marker_end: after, content_start, empty };
|
|
1058
1134
|
}
|
|
1059
1135
|
return null;
|
|
1060
1136
|
}
|
|
@@ -1091,26 +1167,31 @@ export class MdzLexer {
|
|
|
1091
1167
|
type: 'list_open',
|
|
1092
1168
|
ordered: marker.ordered,
|
|
1093
1169
|
start: marker.start,
|
|
1094
|
-
end: marker.start
|
|
1170
|
+
end: marker.start
|
|
1095
1171
|
});
|
|
1096
1172
|
this.#list_item_open(marker);
|
|
1097
|
-
levels.push({indent, ordered: marker.ordered});
|
|
1173
|
+
levels.push({ indent, ordered: marker.ordered });
|
|
1098
1174
|
}
|
|
1099
1175
|
|
|
1100
1176
|
/** Emit a `list_item_open` token for `marker`. */
|
|
1101
1177
|
#list_item_open(marker: MdzListMarker): void {
|
|
1102
1178
|
const end = marker.empty ? marker.marker_end : marker.content_start;
|
|
1103
1179
|
if (marker.ordered) {
|
|
1104
|
-
this.#tokens.push({
|
|
1180
|
+
this.#tokens.push({
|
|
1181
|
+
type: 'list_item_open',
|
|
1182
|
+
number: marker.number,
|
|
1183
|
+
start: marker.start,
|
|
1184
|
+
end
|
|
1185
|
+
});
|
|
1105
1186
|
} else {
|
|
1106
|
-
this.#tokens.push({type: 'list_item_open', start: marker.start, end});
|
|
1187
|
+
this.#tokens.push({ type: 'list_item_open', start: marker.start, end });
|
|
1107
1188
|
}
|
|
1108
1189
|
}
|
|
1109
1190
|
|
|
1110
1191
|
/** Close the deepest open level: its current item, then its list. */
|
|
1111
1192
|
#list_close(levels: Array<MdzListLevel>): void {
|
|
1112
|
-
this.#tokens.push({type: 'list_item_close', start: this.#index, end: this.#index});
|
|
1113
|
-
this.#tokens.push({type: 'list_close', start: this.#index, end: this.#index});
|
|
1193
|
+
this.#tokens.push({ type: 'list_item_close', start: this.#index, end: this.#index });
|
|
1194
|
+
this.#tokens.push({ type: 'list_close', start: this.#index, end: this.#index });
|
|
1114
1195
|
levels.pop();
|
|
1115
1196
|
}
|
|
1116
1197
|
|
|
@@ -1207,8 +1288,8 @@ export class MdzLexer {
|
|
|
1207
1288
|
*/
|
|
1208
1289
|
#match_fence(
|
|
1209
1290
|
first: number,
|
|
1210
|
-
fence_indent: number
|
|
1211
|
-
): {token: MdzTokenCodeblock; end: number} | null {
|
|
1291
|
+
fence_indent: number
|
|
1292
|
+
): { token: MdzTokenCodeblock; end: number } | null {
|
|
1212
1293
|
let i = first;
|
|
1213
1294
|
let backtick_count = 0;
|
|
1214
1295
|
while (i < this.#text.length && this.#text.charCodeAt(i) === BACKTICK) {
|
|
@@ -1265,8 +1346,8 @@ export class MdzLexer {
|
|
|
1265
1346
|
? this.#text.slice(content_start, Math.max(content_start, line_start - 1))
|
|
1266
1347
|
: content_lines.join('\n');
|
|
1267
1348
|
return {
|
|
1268
|
-
token: {type: 'codeblock', lang, content, start: first, end: m},
|
|
1269
|
-
end: m
|
|
1349
|
+
token: { type: 'codeblock', lang, content, start: first, end: m },
|
|
1350
|
+
end: m
|
|
1270
1351
|
};
|
|
1271
1352
|
}
|
|
1272
1353
|
}
|
|
@@ -1276,7 +1357,7 @@ export class MdzLexer {
|
|
|
1276
1357
|
if (fence_indent > 0) {
|
|
1277
1358
|
const line_end = nl === -1 ? this.#text.length : nl;
|
|
1278
1359
|
content_lines.push(
|
|
1279
|
-
this.#text.slice(line_start + Math.min(fence_indent, line_indent), line_end)
|
|
1360
|
+
this.#text.slice(line_start + Math.min(fence_indent, line_indent), line_end)
|
|
1280
1361
|
);
|
|
1281
1362
|
}
|
|
1282
1363
|
if (nl === -1) break;
|
|
@@ -1287,8 +1368,8 @@ export class MdzLexer {
|
|
|
1287
1368
|
let content = fence_indent === 0 ? this.#text.slice(content_start) : content_lines.join('\n');
|
|
1288
1369
|
if (content.endsWith('\n')) content = content.slice(0, -1);
|
|
1289
1370
|
return {
|
|
1290
|
-
token: {type: 'codeblock', lang, content, start: first, end: this.#text.length},
|
|
1291
|
-
end: this.#text.length
|
|
1371
|
+
token: { type: 'codeblock', lang, content, start: first, end: this.#text.length },
|
|
1372
|
+
end: this.#text.length
|
|
1292
1373
|
};
|
|
1293
1374
|
}
|
|
1294
1375
|
|
|
@@ -1382,7 +1463,7 @@ export class MdzLexer {
|
|
|
1382
1463
|
}
|
|
1383
1464
|
|
|
1384
1465
|
this.#index = content_end + 1;
|
|
1385
|
-
this.#tokens.push({type: 'code', content, start, end: this.#index});
|
|
1466
|
+
this.#tokens.push({ type: 'code', content, start, end: this.#index });
|
|
1386
1467
|
}
|
|
1387
1468
|
|
|
1388
1469
|
#tokenize_double_delimiter(delimiter: '**' | '~~', kind: 'bold' | 'strikethrough'): void {
|
|
@@ -1421,7 +1502,7 @@ export class MdzLexer {
|
|
|
1421
1502
|
const open_type = kind === 'bold' ? 'bold_open' : 'strikethrough_open';
|
|
1422
1503
|
const close_type = kind === 'bold' ? 'bold_close' : 'strikethrough_close';
|
|
1423
1504
|
const open_token_index = this.#tokens.length;
|
|
1424
|
-
this.#tokens.push({type: open_type, start, end: this.#index});
|
|
1505
|
+
this.#tokens.push({ type: open_type, start, end: this.#index });
|
|
1425
1506
|
|
|
1426
1507
|
// Set search boundary for nested parsers
|
|
1427
1508
|
const saved_max = this.#max_search_index;
|
|
@@ -1448,11 +1529,11 @@ export class MdzLexer {
|
|
|
1448
1529
|
return;
|
|
1449
1530
|
}
|
|
1450
1531
|
|
|
1451
|
-
this.#tokens.push({type: close_type, start: close_index, end: close_index + 2});
|
|
1532
|
+
this.#tokens.push({ type: close_type, start: close_index, end: close_index + 2 });
|
|
1452
1533
|
this.#index = close_index + 2;
|
|
1453
1534
|
} else {
|
|
1454
1535
|
// Didn't reach closing - convert opening to text
|
|
1455
|
-
this.#tokens[open_token_index] = {type: 'text', content: delimiter, start, end: start + 2};
|
|
1536
|
+
this.#tokens[open_token_index] = { type: 'text', content: delimiter, start, end: start + 2 };
|
|
1456
1537
|
}
|
|
1457
1538
|
}
|
|
1458
1539
|
|
|
@@ -1495,7 +1576,7 @@ export class MdzLexer {
|
|
|
1495
1576
|
// Emit open token
|
|
1496
1577
|
this.#index++;
|
|
1497
1578
|
const open_token_index = this.#tokens.length;
|
|
1498
|
-
this.#tokens.push({type: 'italic_open', start, end: this.#index});
|
|
1579
|
+
this.#tokens.push({ type: 'italic_open', start, end: this.#index });
|
|
1499
1580
|
|
|
1500
1581
|
// Set search boundary for nested parsers
|
|
1501
1582
|
const saved_max = this.#max_search_index;
|
|
@@ -1525,7 +1606,7 @@ export class MdzLexer {
|
|
|
1525
1606
|
this.#tokens.push({
|
|
1526
1607
|
type: 'italic_close',
|
|
1527
1608
|
start: close_index,
|
|
1528
|
-
end: close_index + 1
|
|
1609
|
+
end: close_index + 1
|
|
1529
1610
|
});
|
|
1530
1611
|
this.#index = close_index + 1;
|
|
1531
1612
|
} else {
|
|
@@ -1534,26 +1615,45 @@ export class MdzLexer {
|
|
|
1534
1615
|
type: 'text',
|
|
1535
1616
|
content: '_',
|
|
1536
1617
|
start,
|
|
1537
|
-
end: start + 1
|
|
1618
|
+
end: start + 1
|
|
1538
1619
|
};
|
|
1539
1620
|
}
|
|
1540
1621
|
}
|
|
1541
1622
|
|
|
1542
1623
|
#tokenize_markdown_link(): void {
|
|
1543
1624
|
const start = this.#index;
|
|
1625
|
+
// a link open at this offset already failed, or was suppressed by the
|
|
1626
|
+
// nesting cap — emit `[` as text instead of re-descending into the same
|
|
1627
|
+
// failing children (exponential on nested `[`); the short-circuit
|
|
1628
|
+
// reproduces the revert's exact net effect (`[` text, cursor at start + 1)
|
|
1629
|
+
if (this.#failed_link_starts.has(start) || this.#depth_capped.has(start)) {
|
|
1630
|
+
this.#index = start + 1;
|
|
1631
|
+
this.#emit_text('[', start);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
// nesting-depth cap: too deep to open — render literal and record the
|
|
1635
|
+
// suppression so a shallower re-lex short-circuits here too
|
|
1636
|
+
if (this.#inline_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
1637
|
+
this.#depth_capped.add(start);
|
|
1638
|
+
this.#index = start + 1;
|
|
1639
|
+
this.#emit_text('[', start);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1544
1642
|
this.#index++; // consume [ (dispatch guarantees it)
|
|
1545
1643
|
|
|
1546
1644
|
// Emit link_text_open
|
|
1547
|
-
this.#tokens.push({type: 'link_text_open', start, end: this.#index});
|
|
1645
|
+
this.#tokens.push({ type: 'link_text_open', start, end: this.#index });
|
|
1548
1646
|
|
|
1549
1647
|
// Tokenize children until ] — only `]` delimits link text; a bare `)`
|
|
1550
1648
|
// is ordinary content (matching the streaming parser, which has no `)`
|
|
1551
1649
|
// dispatch case — the reference scan finds its own `)` after `](`)
|
|
1650
|
+
this.#inline_depth++;
|
|
1552
1651
|
while (this.#index < this.#text.length) {
|
|
1553
1652
|
if (this.#text.charCodeAt(this.#index) === RIGHT_BRACKET) break;
|
|
1554
1653
|
if (this.#is_at_paragraph_break()) break;
|
|
1555
1654
|
this.#tokenize_inline();
|
|
1556
1655
|
}
|
|
1656
|
+
this.#inline_depth--;
|
|
1557
1657
|
|
|
1558
1658
|
// Check for ]
|
|
1559
1659
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_BRACKET) {
|
|
@@ -1567,7 +1667,7 @@ export class MdzLexer {
|
|
|
1567
1667
|
this.#tokens.push({
|
|
1568
1668
|
type: 'link_text_close',
|
|
1569
1669
|
start: bracket_close_start,
|
|
1570
|
-
end: this.#index
|
|
1670
|
+
end: this.#index
|
|
1571
1671
|
});
|
|
1572
1672
|
|
|
1573
1673
|
// Check for (
|
|
@@ -1617,11 +1717,18 @@ export class MdzLexer {
|
|
|
1617
1717
|
reference,
|
|
1618
1718
|
link_type,
|
|
1619
1719
|
start: bracket_close_start + 1, // after ]
|
|
1620
|
-
end: this.#index
|
|
1720
|
+
end: this.#index
|
|
1621
1721
|
});
|
|
1622
1722
|
}
|
|
1623
1723
|
|
|
1624
1724
|
#revert_tokens_from_link_open(start: number): void {
|
|
1725
|
+
// record the failure so a re-lex from before this `[` short-circuits.
|
|
1726
|
+
// Keyed by `start` alone — unlike `#failed_tag`'s `start:bound` — because a
|
|
1727
|
+
// link's outcome is bound-independent: the `]`-terminator scan (the children
|
|
1728
|
+
// loop) runs to `text.length` and never consults `#max_search_index`, so a
|
|
1729
|
+
// wider-bound re-lex can't move it. The bound reaches nested tags inside the
|
|
1730
|
+
// link text, but re-attempting those wider never flips the link's net result.
|
|
1731
|
+
this.#failed_link_starts.add(start);
|
|
1625
1732
|
// Find and remove link_text_open and all tokens after it
|
|
1626
1733
|
let open_idx = -1;
|
|
1627
1734
|
for (let i = this.#tokens.length - 1; i >= 0; i--) {
|
|
@@ -1639,6 +1746,32 @@ export class MdzLexer {
|
|
|
1639
1746
|
|
|
1640
1747
|
#tokenize_tag(): void {
|
|
1641
1748
|
const start = this.#index;
|
|
1749
|
+
// short-circuit a re-attempt of a tag open that already failed at this
|
|
1750
|
+
// offset+bound — re-descending into the same failing children is
|
|
1751
|
+
// exponential on nested same-name tags. Keyed by
|
|
1752
|
+
// `#max_search_index` (captured as `bound`) because the closer search is
|
|
1753
|
+
// bound-sensitive, so a wider-bound re-lex can legitimately succeed. The
|
|
1754
|
+
// key string is built only when the memo is non-empty (adversarial
|
|
1755
|
+
// input); prose never allocates it. The short-circuit reproduces a
|
|
1756
|
+
// revert's exact net effect (`<` text, cursor at start + 1).
|
|
1757
|
+
const bound = this.#max_search_index;
|
|
1758
|
+
if (
|
|
1759
|
+
this.#depth_capped.has(start) ||
|
|
1760
|
+
(this.#failed_tag.size !== 0 && this.#failed_tag.has(`${start}:${bound}`))
|
|
1761
|
+
) {
|
|
1762
|
+
this.#index = start + 1;
|
|
1763
|
+
this.#emit_text('<', start);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
// nesting-depth cap: too deep to open a tag — render literal and record
|
|
1767
|
+
// the suppression (position-keyed) so a shallower re-lex short-circuits
|
|
1768
|
+
// here too, keeping the revert re-scan linear
|
|
1769
|
+
if (this.#inline_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
1770
|
+
this.#depth_capped.add(start);
|
|
1771
|
+
this.#index = start + 1;
|
|
1772
|
+
this.#emit_text('<', start);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1642
1775
|
this.#index++; // consume <
|
|
1643
1776
|
|
|
1644
1777
|
// Tag name must start with a letter
|
|
@@ -1678,13 +1811,14 @@ export class MdzLexer {
|
|
|
1678
1811
|
name: tag_name,
|
|
1679
1812
|
is_component,
|
|
1680
1813
|
start,
|
|
1681
|
-
end: this.#index
|
|
1814
|
+
end: this.#index
|
|
1682
1815
|
});
|
|
1683
1816
|
return;
|
|
1684
1817
|
}
|
|
1685
1818
|
|
|
1686
1819
|
// Check for >
|
|
1687
1820
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_ANGLE) {
|
|
1821
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1688
1822
|
this.#index = start + 1;
|
|
1689
1823
|
this.#emit_text('<', start);
|
|
1690
1824
|
return;
|
|
@@ -1703,11 +1837,13 @@ export class MdzLexer {
|
|
|
1703
1837
|
const search_limit = Math.min(this.#max_search_index, this.#text.length);
|
|
1704
1838
|
const closing_tag_pos = this.#index_of(closing_tag, this.#index);
|
|
1705
1839
|
if (closing_tag_pos === -1 || closing_tag_pos > search_limit) {
|
|
1840
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1706
1841
|
this.#index = start + 1;
|
|
1707
1842
|
this.#emit_text('<', start);
|
|
1708
1843
|
return;
|
|
1709
1844
|
}
|
|
1710
1845
|
if (this.#has_paragraph_break_between(this.#index, closing_tag_pos)) {
|
|
1846
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1711
1847
|
this.#index = start + 1;
|
|
1712
1848
|
this.#emit_text('<', start);
|
|
1713
1849
|
return;
|
|
@@ -1715,7 +1851,7 @@ export class MdzLexer {
|
|
|
1715
1851
|
|
|
1716
1852
|
// Emit tag_open
|
|
1717
1853
|
const open_token_index = this.#tokens.length;
|
|
1718
|
-
this.#tokens.push({type: 'tag_open', name: tag_name, is_component, start, end: this.#index});
|
|
1854
|
+
this.#tokens.push({ type: 'tag_open', name: tag_name, is_component, start, end: this.#index });
|
|
1719
1855
|
|
|
1720
1856
|
// Tokenize children until the closing tag. Each iteration narrows
|
|
1721
1857
|
// `#max_search_index` to the next closer so a child's delimiter scan
|
|
@@ -1723,8 +1859,10 @@ export class MdzLexer {
|
|
|
1723
1859
|
// the code span). Recomputed per iteration because a nested same-name
|
|
1724
1860
|
// tag legitimately consumes the nearest closer (`<b>x<b>y</b></b>`).
|
|
1725
1861
|
const saved_max = this.#max_search_index;
|
|
1862
|
+
this.#inline_depth++;
|
|
1726
1863
|
while (this.#index < this.#text.length) {
|
|
1727
1864
|
if (this.#match(closing_tag)) {
|
|
1865
|
+
this.#inline_depth--;
|
|
1728
1866
|
this.#max_search_index = saved_max;
|
|
1729
1867
|
const close_start = this.#index;
|
|
1730
1868
|
this.#index += closing_tag.length;
|
|
@@ -1732,7 +1870,7 @@ export class MdzLexer {
|
|
|
1732
1870
|
type: 'tag_close',
|
|
1733
1871
|
name: tag_name,
|
|
1734
1872
|
start: close_start,
|
|
1735
|
-
end: this.#index
|
|
1873
|
+
end: this.#index
|
|
1736
1874
|
});
|
|
1737
1875
|
return;
|
|
1738
1876
|
}
|
|
@@ -1747,12 +1885,14 @@ export class MdzLexer {
|
|
|
1747
1885
|
this.#max_search_index = next_close;
|
|
1748
1886
|
this.#tokenize_inline();
|
|
1749
1887
|
}
|
|
1888
|
+
this.#inline_depth--;
|
|
1750
1889
|
|
|
1751
1890
|
// No valid closer left — revert: drop the open token and all consumed
|
|
1752
1891
|
// children tokens, emit `<` as text, and re-lex from the next char
|
|
1753
1892
|
// (the `#revert_tokens_from_link_open` pattern; the old fallthrough
|
|
1754
1893
|
// left a dangling `tag_open` whose parser fallback discarded content)
|
|
1755
1894
|
this.#max_search_index = saved_max;
|
|
1895
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1756
1896
|
this.#tokens.splice(open_token_index);
|
|
1757
1897
|
this.#index = start + 1;
|
|
1758
1898
|
this.#emit_text('<', start);
|
|
@@ -1780,16 +1920,33 @@ export class MdzLexer {
|
|
|
1780
1920
|
let parts: Array<string> | null = null;
|
|
1781
1921
|
let part_start = start;
|
|
1782
1922
|
|
|
1783
|
-
|
|
1923
|
+
// in a table cell the trimmed cell end is a hard stop (no newline to
|
|
1924
|
+
// halt on mid-line) — hoisted to the loop bound
|
|
1925
|
+
const scan_end = this.#in_table_cell
|
|
1926
|
+
? Math.min(this.#max_search_index, this.#text.length)
|
|
1927
|
+
: this.#text.length;
|
|
1928
|
+
|
|
1929
|
+
while (this.#index < scan_end) {
|
|
1784
1930
|
const char_code = this.#text.charCodeAt(this.#index);
|
|
1931
|
+
// one table load classifies the char; zero (the common case — the
|
|
1932
|
+
// char can't end or interrupt the run) skips the dispatch below.
|
|
1933
|
+
// `TEXT_SCAN_CLASS` is shared with the streaming scanner
|
|
1934
|
+
// (`consume_text_run` in `mdz_stream_parser_text.ts`); the two dispatch
|
|
1935
|
+
// bodies intentionally differ around it — this one handles `\|` escapes
|
|
1936
|
+
// and list-run strip, that one URL speculation across chunk boundaries.
|
|
1937
|
+
const char_class = char_code < 128 ? TEXT_SCAN_CLASS[char_code]! : 0;
|
|
1938
|
+
if (char_class === 0) {
|
|
1939
|
+
this.#index++;
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Stop at special characters
|
|
1944
|
+
if ((char_class & TEXT_CLASS_STOP) !== 0) break;
|
|
1785
1945
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
// scoped here)
|
|
1789
|
-
if (this.#in_table_cell) {
|
|
1790
|
-
if (this.#index >= this.#max_search_index) break;
|
|
1946
|
+
if ((char_class & TEXT_CLASS_BACKSLASH) !== 0) {
|
|
1947
|
+
// `\|` is a literal pipe — the only mdz escape, scoped to table cells
|
|
1791
1948
|
if (
|
|
1792
|
-
|
|
1949
|
+
this.#in_table_cell &&
|
|
1793
1950
|
this.#index + 1 < this.#max_search_index &&
|
|
1794
1951
|
this.#text.charCodeAt(this.#index + 1) === PIPE
|
|
1795
1952
|
) {
|
|
@@ -1799,22 +1956,11 @@ export class MdzLexer {
|
|
|
1799
1956
|
part_start = this.#index;
|
|
1800
1957
|
continue;
|
|
1801
1958
|
}
|
|
1959
|
+
this.#index++;
|
|
1960
|
+
continue;
|
|
1802
1961
|
}
|
|
1803
1962
|
|
|
1804
|
-
|
|
1805
|
-
if (
|
|
1806
|
-
char_code === BACKTICK ||
|
|
1807
|
-
char_code === ASTERISK ||
|
|
1808
|
-
char_code === UNDERSCORE ||
|
|
1809
|
-
char_code === TILDE ||
|
|
1810
|
-
char_code === LEFT_BRACKET ||
|
|
1811
|
-
char_code === RIGHT_BRACKET ||
|
|
1812
|
-
char_code === LEFT_ANGLE
|
|
1813
|
-
) {
|
|
1814
|
-
break;
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
if (char_code === NEWLINE) {
|
|
1963
|
+
if ((char_class & TEXT_CLASS_NEWLINE) !== 0) {
|
|
1818
1964
|
if (this.#list_run_strip) {
|
|
1819
1965
|
if (this.#index >= this.#max_search_index) break; // the run's terminator
|
|
1820
1966
|
// soft break: keep the newline, strip the next line's leading
|
|
@@ -1853,20 +1999,24 @@ export class MdzLexer {
|
|
|
1853
1999
|
break;
|
|
1854
2000
|
}
|
|
1855
2001
|
}
|
|
2002
|
+
this.#index++; // single newline — ordinary text
|
|
2003
|
+
continue;
|
|
1856
2004
|
}
|
|
1857
2005
|
|
|
1858
|
-
//
|
|
1859
|
-
if (
|
|
1860
|
-
(
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
) {
|
|
2006
|
+
// URL or internal path mid-text (class guard avoids probing every char)
|
|
2007
|
+
if ((char_class & TEXT_CLASS_URL) !== 0) {
|
|
2008
|
+
if (this.#is_at_url()) break;
|
|
2009
|
+
} else if (char_code === SLASH) {
|
|
2010
|
+
if (is_at_absolute_path(this.#text, this.#index)) break;
|
|
2011
|
+
} else if (is_at_relative_path(this.#text, this.#index)) {
|
|
1864
2012
|
break;
|
|
1865
2013
|
}
|
|
1866
2014
|
|
|
1867
2015
|
this.#index++;
|
|
1868
2016
|
}
|
|
1869
2017
|
|
|
2018
|
+
if (mdz_debug_work.enabled) mdz_debug_work.total += this.#index - start;
|
|
2019
|
+
|
|
1870
2020
|
// Ensure we always consume at least one character
|
|
1871
2021
|
if (this.#index === start && this.#index < this.#text.length) {
|
|
1872
2022
|
this.#index++;
|
|
@@ -1876,7 +2026,7 @@ export class MdzLexer {
|
|
|
1876
2026
|
parts === null
|
|
1877
2027
|
? this.#text.slice(start, this.#index)
|
|
1878
2028
|
: parts.join('') + this.#text.slice(part_start, this.#index);
|
|
1879
|
-
this.#tokens.push({type: 'text', content, start, end: this.#index});
|
|
2029
|
+
this.#tokens.push({ type: 'text', content, start, end: this.#index });
|
|
1880
2030
|
}
|
|
1881
2031
|
|
|
1882
2032
|
// -- Auto-link tokenizers --
|
|
@@ -1905,7 +2055,7 @@ export class MdzLexer {
|
|
|
1905
2055
|
reference,
|
|
1906
2056
|
link_type: 'external',
|
|
1907
2057
|
start,
|
|
1908
|
-
end: this.#index
|
|
2058
|
+
end: this.#index
|
|
1909
2059
|
});
|
|
1910
2060
|
}
|
|
1911
2061
|
|
|
@@ -1930,14 +2080,14 @@ export class MdzLexer {
|
|
|
1930
2080
|
reference,
|
|
1931
2081
|
link_type: 'internal',
|
|
1932
2082
|
start,
|
|
1933
|
-
end: this.#index
|
|
2083
|
+
end: this.#index
|
|
1934
2084
|
});
|
|
1935
2085
|
}
|
|
1936
2086
|
|
|
1937
2087
|
// -- Helper methods --
|
|
1938
2088
|
|
|
1939
2089
|
#emit_text(content: string, start: number): void {
|
|
1940
|
-
this.#tokens.push({type: 'text', content, start, end: start + content.length});
|
|
2090
|
+
this.#tokens.push({ type: 'text', content, start, end: start + content.length });
|
|
1941
2091
|
}
|
|
1942
2092
|
|
|
1943
2093
|
#has_paragraph_break_between(from: number, to: number): boolean {
|
|
@@ -1976,7 +2126,7 @@ export class MdzLexer {
|
|
|
1976
2126
|
i = this.#text.indexOf('\n', j);
|
|
1977
2127
|
}
|
|
1978
2128
|
if (memo === null) {
|
|
1979
|
-
this.#break_memo = {from, result};
|
|
2129
|
+
this.#break_memo = { from, result };
|
|
1980
2130
|
} else {
|
|
1981
2131
|
memo.from = from;
|
|
1982
2132
|
memo.result = result;
|