@fuzdev/mdz 0.2.0 → 0.3.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/dist/Mdz.svelte +15 -7
- package/dist/Mdz.svelte.d.ts +9 -2
- package/dist/Mdz.svelte.d.ts.map +1 -1
- package/dist/MdzNodeView.svelte +118 -112
- package/dist/MdzNodeView.svelte.d.ts +6 -2
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzStream.svelte +3 -3
- package/dist/MdzStream.svelte.d.ts.map +1 -1
- package/dist/MdzStreamNodeView.svelte +117 -114
- package/dist/MdzStreamNodeView.svelte.d.ts +6 -2
- package/dist/MdzStreamNodeView.svelte.d.ts.map +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_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 +72 -0
- package/dist/mdz_lexer.d.ts +12 -0
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +164 -24
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
- package/dist/mdz_stream_parser_inline.js +19 -17
- 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 +10 -2
- 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 +52 -3
- 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 +18 -7
- package/dist/mdz_stream_parser_text.d.ts.map +1 -1
- package/dist/mdz_stream_parser_text.js +35 -24
- 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 +15 -16
- package/dist/svelte_preprocess_mdz.js +31 -37
- package/package.json +7 -4
- package/src/lib/mdz.ts +9 -0
- package/src/lib/mdz_debug_work.ts +38 -0
- package/src/lib/mdz_helpers.ts +97 -0
- package/src/lib/mdz_lexer.ts +168 -28
- package/src/lib/mdz_stream_parser_inline.ts +21 -17
- package/src/lib/mdz_stream_parser_link.ts +89 -56
- package/src/lib/mdz_stream_parser_list.ts +10 -4
- package/src/lib/mdz_stream_parser_state.ts +76 -4
- package/src/lib/mdz_stream_parser_table.ts +18 -7
- package/src/lib/mdz_stream_parser_text.ts +38 -37
- package/src/lib/mdz_to_svelte.ts +23 -31
- package/src/lib/mdz_token_parser.ts +17 -20
- package/src/lib/svelte_preprocess_mdz.ts +34 -35
package/dist/mdz_lexer.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { mdz_is_url, mdz_is_safe_reference, is_letter, is_tag_name_char, is_word_char, is_valid_path_char, mdz_trim_url_punctuation, BACKTICK, ASTERISK, UNDERSCORE, TILDE, NEWLINE, HYPHEN, HASH, SPACE, LEFT_ANGLE, RIGHT_ANGLE, SLASH, PERIOD, LEFT_BRACKET, LEFT_PAREN, RIGHT_BRACKET, A_UPPER, Z_UPPER, ZERO, ONE, NINE, HR_HYPHEN_COUNT, MIN_CODEBLOCK_BACKTICKS, MAX_HEADING_LEVEL, MAX_LIST_NUMBER_DIGITS, match_url_prefix_case_insensitive, is_at_absolute_path, is_at_relative_path, is_line_whitespace, mdz_match_blockquote_prefix, mdz_blockquote_line_has_content, mdz_blockquote_strip_width, mdz_remap_segments, mdz_split_table_row, mdz_parse_table_delimiter, PIPE,
|
|
9
|
+
import { mdz_is_url, mdz_is_safe_reference, is_letter, is_tag_name_char, is_word_char, is_valid_path_char, mdz_trim_url_punctuation, BACKTICK, ASTERISK, UNDERSCORE, TILDE, NEWLINE, HYPHEN, HASH, SPACE, LEFT_ANGLE, RIGHT_ANGLE, SLASH, PERIOD, LEFT_BRACKET, LEFT_PAREN, RIGHT_BRACKET, A_UPPER, Z_UPPER, ZERO, ONE, NINE, HR_HYPHEN_COUNT, MIN_CODEBLOCK_BACKTICKS, MAX_HEADING_LEVEL, MAX_INLINE_NESTING_DEPTH, MAX_LIST_NUMBER_DIGITS, match_url_prefix_case_insensitive, is_at_absolute_path, is_at_relative_path, is_line_whitespace, mdz_match_blockquote_prefix, mdz_blockquote_line_has_content, mdz_blockquote_strip_width, mdz_remap_segments, mdz_split_table_row, mdz_parse_table_delimiter, PIPE, TEXT_CLASS_BACKSLASH, TEXT_CLASS_NEWLINE, TEXT_CLASS_STOP, TEXT_CLASS_URL, TEXT_SCAN_CLASS, } from './mdz_helpers.js';
|
|
10
|
+
import { mdz_debug_work } from './mdz_debug_work.js';
|
|
10
11
|
//
|
|
11
12
|
// Lexer
|
|
12
13
|
//
|
|
@@ -39,9 +40,67 @@ export class MdzLexer {
|
|
|
39
40
|
* and `\|` unescapes to a literal pipe.
|
|
40
41
|
*/
|
|
41
42
|
#in_table_cell = false;
|
|
43
|
+
/**
|
|
44
|
+
* Start offsets of markdown-link opens (`[`) that definitively failed — so
|
|
45
|
+
* a re-lex from before the `[` short-circuits to literal text instead of
|
|
46
|
+
* re-attempting (and re-descending into) the same failing link. Without it,
|
|
47
|
+
* the revert-and-re-tokenize failure path is exponential on nested `[`
|
|
48
|
+
* (a ~30-byte `[`×n input hangs). A link's outcome (does a valid `](ref)`
|
|
49
|
+
* follow?) is independent of `#max_search_index`, so `start` alone keys it.
|
|
50
|
+
* The outcome is a pure function of the immutable text, so the memo is valid
|
|
51
|
+
* for the whole lex and across table-cell ranges; cleared on `rebind`.
|
|
52
|
+
*/
|
|
53
|
+
#failed_link_starts = new Set();
|
|
54
|
+
/**
|
|
55
|
+
* The tag analogue of `#failed_link_starts`, keyed `${start}:${bound}`. A
|
|
56
|
+
* tag's closer search respects `#max_search_index`, so the same `start` can
|
|
57
|
+
* fail under a narrow bound yet succeed under the wider one a re-lex uses —
|
|
58
|
+
* the bound is part of the key.
|
|
59
|
+
*/
|
|
60
|
+
#failed_tag = new Set();
|
|
61
|
+
/**
|
|
62
|
+
* Current link/tag nesting depth — the number of open
|
|
63
|
+
* `link_text_open`/`tag_open` containers being recursed into. Bumped around
|
|
64
|
+
* `#tokenize_markdown_link`/`#tokenize_tag`'s children loops (not by
|
|
65
|
+
* delimiters, which can't nest deeply — see `MAX_INLINE_NESTING_DEPTH`). At
|
|
66
|
+
* the cap a further `[`/`<` renders literal instead of opening, bounding
|
|
67
|
+
* recursion depth (no stack overflow) and, with the failure memos, the
|
|
68
|
+
* revert re-scan (linear, not quadratic) on adversarial nesting.
|
|
69
|
+
*/
|
|
70
|
+
#inline_depth = 0;
|
|
71
|
+
/**
|
|
72
|
+
* Start offsets of `[`/`<` openers suppressed by the nesting-depth cap
|
|
73
|
+
* (`#inline_depth >= MAX_INLINE_NESTING_DEPTH`) — position-keyed like
|
|
74
|
+
* `#failed_link_starts` so a re-lex from a shallower depth short-circuits to
|
|
75
|
+
* literal text rather than re-opening the construct (which would reintroduce
|
|
76
|
+
* the quadratic the cap removes). A suppression is bound-independent, so
|
|
77
|
+
* `start` alone keys it; cleared on `rebind`.
|
|
78
|
+
*/
|
|
79
|
+
#depth_capped = new Set();
|
|
42
80
|
constructor(text) {
|
|
43
81
|
this.#text = text;
|
|
44
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Rebind to a new text, so one lexer instance can be reused across inputs —
|
|
85
|
+
* the streaming table path reuses one across a table's rows via
|
|
86
|
+
* `MdzTableCellParser` instead of allocating a lexer (and its memo `Map`)
|
|
87
|
+
* per row. The search memos survive when the text is identical (they're
|
|
88
|
+
* valid over the immutable text — the common case for rows parsed from one
|
|
89
|
+
* buffered chunk) and clear otherwise. Per-parse cursor state is reset by
|
|
90
|
+
* `lex_table_cell` itself.
|
|
91
|
+
*
|
|
92
|
+
* @nodocs
|
|
93
|
+
*/
|
|
94
|
+
rebind(text) {
|
|
95
|
+
if (this.#text === text)
|
|
96
|
+
return;
|
|
97
|
+
this.#text = text;
|
|
98
|
+
this.#search_memo.clear();
|
|
99
|
+
this.#break_memo = null;
|
|
100
|
+
this.#failed_link_starts.clear();
|
|
101
|
+
this.#failed_tag.clear();
|
|
102
|
+
this.#depth_capped.clear();
|
|
103
|
+
}
|
|
45
104
|
/**
|
|
46
105
|
* Memoized `String#indexOf` over the immutable text.
|
|
47
106
|
*
|
|
@@ -58,6 +117,10 @@ export class MdzLexer {
|
|
|
58
117
|
return memo.result;
|
|
59
118
|
}
|
|
60
119
|
const result = this.#text.indexOf(needle, from);
|
|
120
|
+
if (mdz_debug_work.enabled) {
|
|
121
|
+
// scan span: `indexOf` examined [from, result] (found) or [from, EOF)
|
|
122
|
+
mdz_debug_work.total += (result === -1 ? this.#text.length : result) - from;
|
|
123
|
+
}
|
|
61
124
|
if (memo === undefined) {
|
|
62
125
|
this.#search_memo.set(needle, { from, result });
|
|
63
126
|
}
|
|
@@ -110,6 +173,9 @@ export class MdzLexer {
|
|
|
110
173
|
*/
|
|
111
174
|
lex_table_cell(cell_start, cell_end) {
|
|
112
175
|
this.#tokens = [];
|
|
176
|
+
// depth is balanced back to 0 per cell, but pin it so a future early-return
|
|
177
|
+
// between an `#inline_depth++` and its decrement can't leak into the next cell
|
|
178
|
+
this.#inline_depth = 0;
|
|
113
179
|
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
114
180
|
return this.#tokens;
|
|
115
181
|
}
|
|
@@ -1194,12 +1260,30 @@ export class MdzLexer {
|
|
|
1194
1260
|
}
|
|
1195
1261
|
#tokenize_markdown_link() {
|
|
1196
1262
|
const start = this.#index;
|
|
1263
|
+
// a link open at this offset already failed, or was suppressed by the
|
|
1264
|
+
// nesting cap — emit `[` as text instead of re-descending into the same
|
|
1265
|
+
// failing children (exponential on nested `[`); the short-circuit
|
|
1266
|
+
// reproduces the revert's exact net effect (`[` text, cursor at start + 1)
|
|
1267
|
+
if (this.#failed_link_starts.has(start) || this.#depth_capped.has(start)) {
|
|
1268
|
+
this.#index = start + 1;
|
|
1269
|
+
this.#emit_text('[', start);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
// nesting-depth cap: too deep to open — render literal and record the
|
|
1273
|
+
// suppression so a shallower re-lex short-circuits here too
|
|
1274
|
+
if (this.#inline_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
1275
|
+
this.#depth_capped.add(start);
|
|
1276
|
+
this.#index = start + 1;
|
|
1277
|
+
this.#emit_text('[', start);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1197
1280
|
this.#index++; // consume [ (dispatch guarantees it)
|
|
1198
1281
|
// Emit link_text_open
|
|
1199
1282
|
this.#tokens.push({ type: 'link_text_open', start, end: this.#index });
|
|
1200
1283
|
// Tokenize children until ] — only `]` delimits link text; a bare `)`
|
|
1201
1284
|
// is ordinary content (matching the streaming parser, which has no `)`
|
|
1202
1285
|
// dispatch case — the reference scan finds its own `)` after `](`)
|
|
1286
|
+
this.#inline_depth++;
|
|
1203
1287
|
while (this.#index < this.#text.length) {
|
|
1204
1288
|
if (this.#text.charCodeAt(this.#index) === RIGHT_BRACKET)
|
|
1205
1289
|
break;
|
|
@@ -1207,6 +1291,7 @@ export class MdzLexer {
|
|
|
1207
1291
|
break;
|
|
1208
1292
|
this.#tokenize_inline();
|
|
1209
1293
|
}
|
|
1294
|
+
this.#inline_depth--;
|
|
1210
1295
|
// Check for ]
|
|
1211
1296
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_BRACKET) {
|
|
1212
1297
|
// Revert - remove link_text_open and all children tokens added
|
|
@@ -1263,6 +1348,13 @@ export class MdzLexer {
|
|
|
1263
1348
|
});
|
|
1264
1349
|
}
|
|
1265
1350
|
#revert_tokens_from_link_open(start) {
|
|
1351
|
+
// record the failure so a re-lex from before this `[` short-circuits.
|
|
1352
|
+
// Keyed by `start` alone — unlike `#failed_tag`'s `start:bound` — because a
|
|
1353
|
+
// link's outcome is bound-independent: the `]`-terminator scan (the children
|
|
1354
|
+
// loop) runs to `text.length` and never consults `#max_search_index`, so a
|
|
1355
|
+
// wider-bound re-lex can't move it. The bound reaches nested tags inside the
|
|
1356
|
+
// link text, but re-attempting those wider never flips the link's net result.
|
|
1357
|
+
this.#failed_link_starts.add(start);
|
|
1266
1358
|
// Find and remove link_text_open and all tokens after it
|
|
1267
1359
|
let open_idx = -1;
|
|
1268
1360
|
for (let i = this.#tokens.length - 1; i >= 0; i--) {
|
|
@@ -1279,6 +1371,30 @@ export class MdzLexer {
|
|
|
1279
1371
|
}
|
|
1280
1372
|
#tokenize_tag() {
|
|
1281
1373
|
const start = this.#index;
|
|
1374
|
+
// short-circuit a re-attempt of a tag open that already failed at this
|
|
1375
|
+
// offset+bound — re-descending into the same failing children is
|
|
1376
|
+
// exponential on nested same-name tags. Keyed by
|
|
1377
|
+
// `#max_search_index` (captured as `bound`) because the closer search is
|
|
1378
|
+
// bound-sensitive, so a wider-bound re-lex can legitimately succeed. The
|
|
1379
|
+
// key string is built only when the memo is non-empty (adversarial
|
|
1380
|
+
// input); prose never allocates it. The short-circuit reproduces a
|
|
1381
|
+
// revert's exact net effect (`<` text, cursor at start + 1).
|
|
1382
|
+
const bound = this.#max_search_index;
|
|
1383
|
+
if (this.#depth_capped.has(start) ||
|
|
1384
|
+
(this.#failed_tag.size !== 0 && this.#failed_tag.has(`${start}:${bound}`))) {
|
|
1385
|
+
this.#index = start + 1;
|
|
1386
|
+
this.#emit_text('<', start);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
// nesting-depth cap: too deep to open a tag — render literal and record
|
|
1390
|
+
// the suppression (position-keyed) so a shallower re-lex short-circuits
|
|
1391
|
+
// here too, keeping the revert re-scan linear
|
|
1392
|
+
if (this.#inline_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
1393
|
+
this.#depth_capped.add(start);
|
|
1394
|
+
this.#index = start + 1;
|
|
1395
|
+
this.#emit_text('<', start);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1282
1398
|
this.#index++; // consume <
|
|
1283
1399
|
// Tag name must start with a letter
|
|
1284
1400
|
if (this.#index >= this.#text.length || !is_letter(this.#text.charCodeAt(this.#index))) {
|
|
@@ -1315,6 +1431,7 @@ export class MdzLexer {
|
|
|
1315
1431
|
}
|
|
1316
1432
|
// Check for >
|
|
1317
1433
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_ANGLE) {
|
|
1434
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1318
1435
|
this.#index = start + 1;
|
|
1319
1436
|
this.#emit_text('<', start);
|
|
1320
1437
|
return;
|
|
@@ -1332,11 +1449,13 @@ export class MdzLexer {
|
|
|
1332
1449
|
const search_limit = Math.min(this.#max_search_index, this.#text.length);
|
|
1333
1450
|
const closing_tag_pos = this.#index_of(closing_tag, this.#index);
|
|
1334
1451
|
if (closing_tag_pos === -1 || closing_tag_pos > search_limit) {
|
|
1452
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1335
1453
|
this.#index = start + 1;
|
|
1336
1454
|
this.#emit_text('<', start);
|
|
1337
1455
|
return;
|
|
1338
1456
|
}
|
|
1339
1457
|
if (this.#has_paragraph_break_between(this.#index, closing_tag_pos)) {
|
|
1458
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1340
1459
|
this.#index = start + 1;
|
|
1341
1460
|
this.#emit_text('<', start);
|
|
1342
1461
|
return;
|
|
@@ -1350,8 +1469,10 @@ export class MdzLexer {
|
|
|
1350
1469
|
// the code span). Recomputed per iteration because a nested same-name
|
|
1351
1470
|
// tag legitimately consumes the nearest closer (`<b>x<b>y</b></b>`).
|
|
1352
1471
|
const saved_max = this.#max_search_index;
|
|
1472
|
+
this.#inline_depth++;
|
|
1353
1473
|
while (this.#index < this.#text.length) {
|
|
1354
1474
|
if (this.#match(closing_tag)) {
|
|
1475
|
+
this.#inline_depth--;
|
|
1355
1476
|
this.#max_search_index = saved_max;
|
|
1356
1477
|
const close_start = this.#index;
|
|
1357
1478
|
this.#index += closing_tag.length;
|
|
@@ -1372,11 +1493,13 @@ export class MdzLexer {
|
|
|
1372
1493
|
this.#max_search_index = next_close;
|
|
1373
1494
|
this.#tokenize_inline();
|
|
1374
1495
|
}
|
|
1496
|
+
this.#inline_depth--;
|
|
1375
1497
|
// No valid closer left — revert: drop the open token and all consumed
|
|
1376
1498
|
// children tokens, emit `<` as text, and re-lex from the next char
|
|
1377
1499
|
// (the `#revert_tokens_from_link_open` pattern; the old fallthrough
|
|
1378
1500
|
// left a dangling `tag_open` whose parser fallback discarded content)
|
|
1379
1501
|
this.#max_search_index = saved_max;
|
|
1502
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1380
1503
|
this.#tokens.splice(open_token_index);
|
|
1381
1504
|
this.#index = start + 1;
|
|
1382
1505
|
this.#emit_text('<', start);
|
|
@@ -1400,15 +1523,30 @@ export class MdzLexer {
|
|
|
1400
1523
|
// content: parts accumulate around each stripped region
|
|
1401
1524
|
let parts = null;
|
|
1402
1525
|
let part_start = start;
|
|
1403
|
-
|
|
1526
|
+
// in a table cell the trimmed cell end is a hard stop (no newline to
|
|
1527
|
+
// halt on mid-line) — hoisted to the loop bound
|
|
1528
|
+
const scan_end = this.#in_table_cell
|
|
1529
|
+
? Math.min(this.#max_search_index, this.#text.length)
|
|
1530
|
+
: this.#text.length;
|
|
1531
|
+
while (this.#index < scan_end) {
|
|
1404
1532
|
const char_code = this.#text.charCodeAt(this.#index);
|
|
1405
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1533
|
+
// one table load classifies the char; zero (the common case — the
|
|
1534
|
+
// char can't end or interrupt the run) skips the dispatch below.
|
|
1535
|
+
// `TEXT_SCAN_CLASS` is shared with the streaming scanner
|
|
1536
|
+
// (`consume_text_run` in `mdz_stream_parser_text.ts`); the two dispatch
|
|
1537
|
+
// bodies intentionally differ around it — this one handles `\|` escapes
|
|
1538
|
+
// and list-run strip, that one URL speculation across chunk boundaries.
|
|
1539
|
+
const char_class = char_code < 128 ? TEXT_SCAN_CLASS[char_code] : 0;
|
|
1540
|
+
if (char_class === 0) {
|
|
1541
|
+
this.#index++;
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
// Stop at special characters
|
|
1545
|
+
if ((char_class & TEXT_CLASS_STOP) !== 0)
|
|
1546
|
+
break;
|
|
1547
|
+
if ((char_class & TEXT_CLASS_BACKSLASH) !== 0) {
|
|
1548
|
+
// `\|` is a literal pipe — the only mdz escape, scoped to table cells
|
|
1549
|
+
if (this.#in_table_cell &&
|
|
1412
1550
|
this.#index + 1 < this.#max_search_index &&
|
|
1413
1551
|
this.#text.charCodeAt(this.#index + 1) === PIPE) {
|
|
1414
1552
|
(parts ??= []).push(this.#text.slice(part_start, this.#index));
|
|
@@ -1417,18 +1555,10 @@ export class MdzLexer {
|
|
|
1417
1555
|
part_start = this.#index;
|
|
1418
1556
|
continue;
|
|
1419
1557
|
}
|
|
1558
|
+
this.#index++;
|
|
1559
|
+
continue;
|
|
1420
1560
|
}
|
|
1421
|
-
|
|
1422
|
-
if (char_code === BACKTICK ||
|
|
1423
|
-
char_code === ASTERISK ||
|
|
1424
|
-
char_code === UNDERSCORE ||
|
|
1425
|
-
char_code === TILDE ||
|
|
1426
|
-
char_code === LEFT_BRACKET ||
|
|
1427
|
-
char_code === RIGHT_BRACKET ||
|
|
1428
|
-
char_code === LEFT_ANGLE) {
|
|
1429
|
-
break;
|
|
1430
|
-
}
|
|
1431
|
-
if (char_code === NEWLINE) {
|
|
1561
|
+
if ((char_class & TEXT_CLASS_NEWLINE) !== 0) {
|
|
1432
1562
|
if (this.#list_run_strip) {
|
|
1433
1563
|
if (this.#index >= this.#max_search_index)
|
|
1434
1564
|
break; // the run's terminator
|
|
@@ -1465,15 +1595,25 @@ export class MdzLexer {
|
|
|
1465
1595
|
break;
|
|
1466
1596
|
}
|
|
1467
1597
|
}
|
|
1598
|
+
this.#index++; // single newline — ordinary text
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
// URL or internal path mid-text (class guard avoids probing every char)
|
|
1602
|
+
if ((char_class & TEXT_CLASS_URL) !== 0) {
|
|
1603
|
+
if (this.#is_at_url())
|
|
1604
|
+
break;
|
|
1605
|
+
}
|
|
1606
|
+
else if (char_code === SLASH) {
|
|
1607
|
+
if (is_at_absolute_path(this.#text, this.#index))
|
|
1608
|
+
break;
|
|
1468
1609
|
}
|
|
1469
|
-
|
|
1470
|
-
if (((char_code === 104 /* h */ || char_code === 72) /* H */ && this.#is_at_url()) ||
|
|
1471
|
-
(char_code === SLASH && is_at_absolute_path(this.#text, this.#index)) ||
|
|
1472
|
-
(char_code === PERIOD && is_at_relative_path(this.#text, this.#index))) {
|
|
1610
|
+
else if (is_at_relative_path(this.#text, this.#index)) {
|
|
1473
1611
|
break;
|
|
1474
1612
|
}
|
|
1475
1613
|
this.#index++;
|
|
1476
1614
|
}
|
|
1615
|
+
if (mdz_debug_work.enabled)
|
|
1616
|
+
mdz_debug_work.total += this.#index - start;
|
|
1477
1617
|
// Ensure we always consume at least one character
|
|
1478
1618
|
if (this.#index === start && this.#index < this.#text.length) {
|
|
1479
1619
|
this.#index++;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mdz_stream_parser_inline.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_inline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,OAAO,EACN,KAAK,oBAAoB,EAYzB,MAAM,8BAA8B,CAAC;AA6GtC;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,OAAO,oBAAoB,EAC3B,WAAW,IAAI,GAAG,IAAI,EACtB,WAAW,MAAM,GAAG,eAAe,EACnC,gBAAc,KACZ,OA2BF,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,oBAAoB,EAAE,gBAAc,KAAG,OAyCxE,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,oBAAoB,KAAG,OAOvE,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,oBAAoB,EAAE,WAAW,MAAM,KAAG,OAuBhF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,oBAAoB,GAChC,OAAO,oBAAoB,EAC3B,gBAAc,KACZ,WAAW,GAAG,UAAU,GAAG,SAmB7B,CAAC;AAIF,cAAc;AACd,eAAO,MAAM,QAAQ,GAAI,OAAO,oBAAoB,EAAE,gBAAc,KAAG,
|
|
1
|
+
{"version":3,"file":"mdz_stream_parser_inline.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_inline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,OAAO,EACN,KAAK,oBAAoB,EAYzB,MAAM,8BAA8B,CAAC;AA6GtC;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,OAAO,oBAAoB,EAC3B,WAAW,IAAI,GAAG,IAAI,EACtB,WAAW,MAAM,GAAG,eAAe,EACnC,gBAAc,KACZ,OA2BF,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,oBAAoB,EAAE,gBAAc,KAAG,OAyCxE,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,oBAAoB,KAAG,OAOvE,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,oBAAoB,EAAE,WAAW,MAAM,KAAG,OAuBhF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,oBAAoB,GAChC,OAAO,oBAAoB,EAC3B,gBAAc,KACZ,WAAW,GAAG,UAAU,GAAG,SAmB7B,CAAC;AAIF,cAAc;AACd,eAAO,MAAM,QAAQ,GAAI,OAAO,oBAAoB,EAAE,gBAAc,KAAG,OAwFtE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,oBAAoB,EAAE,MAAM,MAAM,KAAG,MAyB7E,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,GAAI,OAAO,oBAAoB,KAAG,OAO5E,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,oBAAoB,KAAG,IA2ClE,CAAC"}
|
|
@@ -297,15 +297,20 @@ export const scan_closer_boundary = (state, forced = false) => {
|
|
|
297
297
|
/** @nodocs */
|
|
298
298
|
export const try_code = (state, forced = false) => {
|
|
299
299
|
const start = state.pos;
|
|
300
|
-
|
|
300
|
+
const search_start = start + 1; // past opening backtick
|
|
301
301
|
// compute search boundary: don't scan past open formatting delimiters
|
|
302
|
-
const search_limit = code_search_limit(state,
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
302
|
+
const search_limit = code_search_limit(state, search_start);
|
|
303
|
+
// find the first closing backtick or newline (memoized — a candidate that
|
|
304
|
+
// holds under open formatting re-enters here on every feed, and a raw scan
|
|
305
|
+
// would re-cover the same growing region each time, going quadratic on a
|
|
306
|
+
// single long line)
|
|
307
|
+
const close_pos = buffer_index_of(state, '`', search_start);
|
|
308
|
+
const newline_pos = buffer_index_of(state, '\n', search_start);
|
|
309
|
+
const terminator = close_pos !== -1 && (newline_pos === -1 || close_pos < newline_pos) ? close_pos : newline_pos;
|
|
310
|
+
if (terminator !== -1 && terminator < search_limit) {
|
|
311
|
+
if (terminator === close_pos) {
|
|
307
312
|
// found close
|
|
308
|
-
const content = state.buffer.slice(
|
|
313
|
+
const content = state.buffer.slice(search_start, terminator);
|
|
309
314
|
if (content.length === 0) {
|
|
310
315
|
// empty code — treat as text
|
|
311
316
|
consume_delimiter_as_text(state, '``');
|
|
@@ -320,20 +325,17 @@ export const try_code = (state, forced = false) => {
|
|
|
320
325
|
content,
|
|
321
326
|
text_type: 'Code',
|
|
322
327
|
start: offset(state, start),
|
|
323
|
-
end: offset(state,
|
|
328
|
+
end: offset(state, terminator + 1),
|
|
324
329
|
});
|
|
325
330
|
state.active_text_id = null;
|
|
326
|
-
state.pos =
|
|
327
|
-
state.column +=
|
|
331
|
+
state.pos = terminator + 1;
|
|
332
|
+
state.column += terminator + 1 - start;
|
|
328
333
|
state.prev_char = BACKTICK;
|
|
329
334
|
return true;
|
|
330
335
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
i++;
|
|
336
|
+
// newline before any close — inline code can't span lines
|
|
337
|
+
consume_delimiter_as_text(state, '`');
|
|
338
|
+
return true;
|
|
337
339
|
}
|
|
338
340
|
// hit search limit (open formatting delimiter boundary) — treat backtick as
|
|
339
341
|
// text. This is the documented backtick-adjacent divergence point: when the
|
|
@@ -341,7 +343,7 @@ export const try_code = (state, forced = false) => {
|
|
|
341
343
|
// processed), the one-shot parse rejects that italic and scans unbounded —
|
|
342
344
|
// see the `MdzStreamParser` TSDoc and the parity suite's expected-divergence
|
|
343
345
|
// battery
|
|
344
|
-
if (
|
|
346
|
+
if (search_limit < state.buffer.length) {
|
|
345
347
|
consume_delimiter_as_text(state, '`');
|
|
346
348
|
return true;
|
|
347
349
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mdz_stream_parser_link.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_link.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"mdz_stream_parser_link.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_link.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAYd,MAAM,8BAA8B,CAAC;AAMtC,cAAc;AACd,eAAO,MAAM,aAAa,GAAI,OAAO,oBAAoB,KAAG,OAoB3D,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAC9B,OAAO,oBAAoB,EAC3B,gBAAgB,MAAM,EACtB,aAAa,MAAM,KACjB,IAUF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,oBAAoB,EAAE,gBAAgB,MAAM,KAAG,OA4EvF,CAAC;AAIF,cAAc;AACd,eAAO,MAAM,YAAY,GAAI,OAAO,oBAAoB,KAAG,SAqF1D,CAAC;AAEF,cAAc;AACd,eAAO,MAAM,aAAa,GAAI,OAAO,oBAAoB,KAAG,SAiD3D,CAAC"}
|
|
@@ -4,11 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
|
-
import { A_UPPER, LEFT_BRACKET, LEFT_PAREN,
|
|
8
|
-
import { accumulate_text, alloc_id, emit, ensure_paragraph, flush_text, is_run_boundary, offset, pop_stack_entry, push_stack_entry, revert_above, } from './mdz_stream_parser_state.js';
|
|
7
|
+
import { A_UPPER, LEFT_BRACKET, LEFT_PAREN, MAX_INLINE_NESTING_DEPTH, RIGHT_ANGLE, RIGHT_BRACKET, RIGHT_PAREN, SLASH, SPACE, Z_UPPER, is_letter, is_tag_name_char, is_valid_path_char, mdz_is_safe_reference, mdz_is_url, } from './mdz_helpers.js';
|
|
8
|
+
import { accumulate_text, alloc_id, buffer_index_of, emit, ensure_paragraph, flush_text, is_run_boundary, offset, pop_stack_entry, push_stack_entry, revert_above, } from './mdz_stream_parser_state.js';
|
|
9
|
+
import { consume_delimiter_as_text } from './mdz_stream_parser_text.js';
|
|
10
|
+
import { mdz_debug_work } from './mdz_debug_work.js';
|
|
9
11
|
// -- Links --
|
|
10
12
|
/** @nodocs */
|
|
11
13
|
export const try_link_open = (state) => {
|
|
14
|
+
// nesting-depth cap: too deep to open — render `[` literal, matching the
|
|
15
|
+
// sync lexer's `#tokenize_markdown_link` cap (keeps the two parsers agreeing
|
|
16
|
+
// on where deep `[` stops nesting)
|
|
17
|
+
if (state.open_link_tag_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
18
|
+
consume_delimiter_as_text(state, '[');
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
12
21
|
flush_text(state);
|
|
13
22
|
ensure_paragraph(state);
|
|
14
23
|
const id = alloc_id(state);
|
|
@@ -55,63 +64,67 @@ export const try_complete_link = (state, link_stack_idx) => {
|
|
|
55
64
|
abort_link_to_text(state, link_stack_idx, bracket_pos);
|
|
56
65
|
return true;
|
|
57
66
|
}
|
|
58
|
-
// found ]( —
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
end: offset(state, i + 1),
|
|
95
|
-
reference,
|
|
96
|
-
link_type,
|
|
97
|
-
});
|
|
98
|
-
state.active_text_id = null;
|
|
99
|
-
state.pos = i + 1;
|
|
100
|
-
state.column += i + 1 - bracket_pos;
|
|
101
|
-
state.prev_char = RIGHT_PAREN;
|
|
67
|
+
// found ]( — find the reference's terminator: `)` completes, a space or
|
|
68
|
+
// newline aborts. Memoized searches (see `buffer_index_of`) — a link whose
|
|
69
|
+
// URL arrives incrementally re-enters here on every feed with `pos` still
|
|
70
|
+
// at the `]`, and a raw scan would re-cover the growing reference each
|
|
71
|
+
// time, going quadratic on long streamed URLs.
|
|
72
|
+
const ref_start = bracket_pos + 2;
|
|
73
|
+
const close_pos = buffer_index_of(state, ')', ref_start);
|
|
74
|
+
const space_pos = buffer_index_of(state, ' ', ref_start);
|
|
75
|
+
const newline_pos = buffer_index_of(state, '\n', ref_start);
|
|
76
|
+
let invalid_pos = space_pos;
|
|
77
|
+
if (newline_pos !== -1 && (invalid_pos === -1 || newline_pos < invalid_pos)) {
|
|
78
|
+
invalid_pos = newline_pos;
|
|
79
|
+
}
|
|
80
|
+
if (close_pos === -1) {
|
|
81
|
+
// didn't find ) — need more input or invalid
|
|
82
|
+
if (invalid_pos === -1)
|
|
83
|
+
return false; // need more input
|
|
84
|
+
// found invalid char before any ) — revert
|
|
85
|
+
abort_link_to_text(state, link_stack_idx, bracket_pos);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (invalid_pos !== -1 && invalid_pos < close_pos) {
|
|
89
|
+
// invalid character in URL (simplified check)
|
|
90
|
+
abort_link_to_text(state, link_stack_idx, bracket_pos);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const i = close_pos;
|
|
94
|
+
const reference = state.buffer.slice(ref_start, i);
|
|
95
|
+
// validate reference: non-empty and only valid path chars
|
|
96
|
+
if (!reference.trim()) {
|
|
97
|
+
abort_link_to_text(state, link_stack_idx, bracket_pos);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
for (let j = 0; j < reference.length; j++) {
|
|
101
|
+
if (!is_valid_path_char(reference.charCodeAt(j))) {
|
|
102
|
+
abort_link_to_text(state, link_stack_idx, bracket_pos);
|
|
102
103
|
return true;
|
|
103
104
|
}
|
|
104
|
-
if (c === NEWLINE || c === SPACE) {
|
|
105
|
-
// invalid character in URL (simplified check)
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
i++;
|
|
109
105
|
}
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
// Reject unsafe protocols (javascript:, data:, etc.) at parse time.
|
|
107
|
+
// `is_valid_path_char` permits `:` so we have to filter explicitly.
|
|
108
|
+
if (!mdz_is_safe_reference(reference)) {
|
|
109
|
+
abort_link_to_text(state, link_stack_idx, bracket_pos);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// success — close the link with reference
|
|
113
|
+
flush_text(state);
|
|
114
|
+
revert_above(state, link_stack_idx);
|
|
115
|
+
const entry = pop_stack_entry(state);
|
|
116
|
+
const link_type = mdz_is_url(reference) ? 'external' : 'internal';
|
|
117
|
+
emit(state, {
|
|
118
|
+
type: 'close',
|
|
119
|
+
id: entry.id,
|
|
120
|
+
end: offset(state, i + 1),
|
|
121
|
+
reference,
|
|
122
|
+
link_type,
|
|
123
|
+
});
|
|
124
|
+
state.active_text_id = null;
|
|
125
|
+
state.pos = i + 1;
|
|
126
|
+
state.column += i + 1 - bracket_pos;
|
|
127
|
+
state.prev_char = RIGHT_PAREN;
|
|
115
128
|
return true;
|
|
116
129
|
};
|
|
117
130
|
// -- Tags --
|
|
@@ -129,7 +142,18 @@ export const try_tag_open = (state) => {
|
|
|
129
142
|
if (!is_letter(state.buffer.charCodeAt(i))) {
|
|
130
143
|
return 'not_match';
|
|
131
144
|
}
|
|
145
|
+
// nesting-depth cap: `<letter…` is a tag open, but we're too deep to nest —
|
|
146
|
+
// render `<` literal, matching the sync lexer's `#tokenize_tag` cap. Checked
|
|
147
|
+
// after the closer/hold cases above (a `</…>` closer still closes; a `<` at
|
|
148
|
+
// the buffer end still holds), so only genuine opens are suppressed.
|
|
149
|
+
if (state.open_link_tag_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
150
|
+
consume_delimiter_as_text(state, '<');
|
|
151
|
+
return 'consumed';
|
|
152
|
+
}
|
|
132
153
|
// collect tag name
|
|
154
|
+
// TODO the name/whitespace scans rescan from `start` on every held retry
|
|
155
|
+
// of an unclosed tag's open tail — needs a hold watermark; a length cap would
|
|
156
|
+
// diverge from the sync parser on pathological multi-KB "tag names"
|
|
133
157
|
const name_start = i;
|
|
134
158
|
while (i < state.buffer.length && is_tag_name_char(state.buffer.charCodeAt(i))) {
|
|
135
159
|
i++;
|
|
@@ -201,9 +225,17 @@ export const try_close_tag = (state) => {
|
|
|
201
225
|
return 'not_match';
|
|
202
226
|
const name = state.buffer.slice(name_start, i);
|
|
203
227
|
i++; // past >
|
|
228
|
+
// O(1) bail when no tag of this name is open — without it, every `</name>`
|
|
229
|
+
// candidate walks the whole inline stack, which grows one frame per leaked
|
|
230
|
+
// unclosed `<Tag>` and goes quadratic on tag-dense input (the same shape
|
|
231
|
+
// `open_counts` guards for the delimiter inlines)
|
|
232
|
+
if (!state.open_tag_counts?.get(name))
|
|
233
|
+
return 'not_match';
|
|
204
234
|
// find matching open tag on stack
|
|
205
235
|
let found_idx = -1;
|
|
206
236
|
for (let j = state.stack.length - 1; j >= 0; j--) {
|
|
237
|
+
if (mdz_debug_work.enabled)
|
|
238
|
+
mdz_debug_work.total++; // frames walked (mismatched-tags guard)
|
|
207
239
|
const entry = state.stack[j];
|
|
208
240
|
if ((entry.node_type === 'Element' || entry.node_type === 'Component') &&
|
|
209
241
|
entry.tag_name === name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mdz_stream_parser_list.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAmBH,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAad,MAAM,8BAA8B,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;CACf;AAoBD;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAC7B,OAAO,oBAAoB,EAC3B,GAAG,MAAM,EACT,QAAQ,OAAO,KACb,mBAAmB,GAAG,SAAS,GAAG,IAsDpC,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,OAAO,oBAAoB,EAC3B,GAAG,MAAM,EACT,QAAQ,OAAO,KACb,oBAAoB,GAAG,SAAS,GAAG,IAuBrC,CAAC;AAgGF;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,oBAAoB,EAAE,QAAQ,OAAO,KAAG,SAY7E,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,iBAAiB,GAC7B,OAAO,oBAAoB,EAC3B,QAAQ,OAAO,KACb,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"mdz_stream_parser_list.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAmBH,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAad,MAAM,8BAA8B,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;CACf;AAoBD;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAC7B,OAAO,oBAAoB,EAC3B,GAAG,MAAM,EACT,QAAQ,OAAO,KACb,mBAAmB,GAAG,SAAS,GAAG,IAsDpC,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,OAAO,oBAAoB,EAC3B,GAAG,MAAM,EACT,QAAQ,OAAO,KACb,oBAAoB,GAAG,SAAS,GAAG,IAuBrC,CAAC;AAgGF;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,oBAAoB,EAAE,QAAQ,OAAO,KAAG,SAY7E,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,iBAAiB,GAC7B,OAAO,oBAAoB,EAC3B,QAAQ,OAAO,KACb,OAAO,GAAG,YAqMZ,CAAC"}
|
|
@@ -334,8 +334,16 @@ export const process_list_line = (state, forced) => {
|
|
|
334
334
|
}
|
|
335
335
|
if (marker !== null && marker.empty) {
|
|
336
336
|
// empty item: only at an indent exactly matching an open level of the
|
|
337
|
-
// same marker type — anything else degrades below
|
|
338
|
-
|
|
337
|
+
// same marker type — anything else degrades below (manual loop like
|
|
338
|
+
// `list_continuation_target` — no per-line closure allocation)
|
|
339
|
+
let level_idx = -1;
|
|
340
|
+
for (let li = 0; li < state.list_levels.length; li++) {
|
|
341
|
+
const l = state.list_levels[li];
|
|
342
|
+
if (l.indent === indent && l.ordered === marker.ordered) {
|
|
343
|
+
level_idx = li;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
339
347
|
if (level_idx !== -1) {
|
|
340
348
|
close_list_run(state);
|
|
341
349
|
while (state.list_levels.length - 1 > level_idx) {
|