@fuzdev/mdz 0.1.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/README.md +4 -0
- 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 +130 -95
- 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 +129 -95
- package/dist/MdzStreamNodeView.svelte.d.ts +6 -2
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
- package/dist/context_helpers.d.ts +1 -1
- package/dist/mdz.d.ts +59 -2
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +37 -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 +96 -1
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +192 -0
- package/dist/mdz_lexer.d.ts +47 -1
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +364 -15
- package/dist/mdz_opcodes.d.ts +6 -1
- 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 +26 -0
- package/dist/mdz_stream_parser.d.ts.map +1 -1
- package/dist/mdz_stream_parser.js +20 -1
- 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 +32 -3
- package/dist/mdz_stream_parser_state.d.ts +41 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -1
- package/dist/mdz_stream_parser_state.js +53 -3
- package/dist/mdz_stream_parser_table.d.ts +73 -0
- package/dist/mdz_stream_parser_table.d.ts.map +1 -0
- package/dist/mdz_stream_parser_table.js +362 -0
- package/dist/mdz_stream_parser_text.d.ts.map +1 -1
- package/dist/mdz_stream_parser_text.js +35 -24
- package/dist/mdz_stream_state.svelte.d.ts +5 -0
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
- package/dist/mdz_stream_state.svelte.js +8 -0
- package/dist/mdz_to_svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.js +51 -20
- package/dist/mdz_token_parser.d.ts +16 -0
- package/dist/mdz_token_parser.d.ts.map +1 -1
- package/dist/mdz_token_parser.js +112 -14
- package/dist/svelte_preprocess_mdz.js +33 -39
- package/package.json +11 -8
- package/src/lib/context_helpers.ts +1 -1
- package/src/lib/mdz.ts +79 -3
- package/src/lib/mdz_contexts.ts +1 -1
- package/src/lib/mdz_debug_work.ts +38 -0
- package/src/lib/mdz_helpers.ts +222 -1
- package/src/lib/mdz_lexer.ts +420 -21
- package/src/lib/mdz_opcodes.ts +10 -1
- package/src/lib/mdz_opcodes_to_nodes.ts +37 -3
- package/src/lib/mdz_stream_parser.ts +26 -9
- package/src/lib/mdz_stream_parser_block.ts +2 -2
- package/src/lib/mdz_stream_parser_inline.ts +25 -21
- package/src/lib/mdz_stream_parser_link.ts +92 -59
- package/src/lib/mdz_stream_parser_list.ts +34 -6
- package/src/lib/mdz_stream_parser_state.ts +87 -5
- package/src/lib/mdz_stream_parser_table.ts +420 -0
- package/src/lib/mdz_stream_parser_text.ts +40 -39
- package/src/lib/mdz_stream_parser_url.ts +2 -2
- package/src/lib/mdz_stream_state.svelte.ts +8 -1
- package/src/lib/mdz_to_svelte.ts +49 -23
- package/src/lib/mdz_token_parser.ts +135 -21
- package/src/lib/svelte_preprocess_mdz.ts +37 -38
- 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,
|
|
@@ -47,7 +48,18 @@ import {
|
|
|
47
48
|
mdz_blockquote_line_has_content,
|
|
48
49
|
mdz_blockquote_strip_width,
|
|
49
50
|
mdz_remap_segments,
|
|
50
|
-
|
|
51
|
+
mdz_split_table_row,
|
|
52
|
+
mdz_parse_table_delimiter,
|
|
53
|
+
PIPE,
|
|
54
|
+
TEXT_CLASS_BACKSLASH,
|
|
55
|
+
TEXT_CLASS_NEWLINE,
|
|
56
|
+
TEXT_CLASS_STOP,
|
|
57
|
+
TEXT_CLASS_URL,
|
|
58
|
+
TEXT_SCAN_CLASS,
|
|
59
|
+
} from './mdz_helpers.ts';
|
|
60
|
+
|
|
61
|
+
import type {MdzTableAlign} from './mdz.ts';
|
|
62
|
+
import {mdz_debug_work} from './mdz_debug_work.ts';
|
|
51
63
|
|
|
52
64
|
//
|
|
53
65
|
// Token types
|
|
@@ -84,7 +96,13 @@ export type MdzToken =
|
|
|
84
96
|
| MdzTokenListItemOpen
|
|
85
97
|
| MdzTokenListItemClose
|
|
86
98
|
| MdzTokenBlockquoteOpen
|
|
87
|
-
| MdzTokenBlockquoteClose
|
|
99
|
+
| MdzTokenBlockquoteClose
|
|
100
|
+
| MdzTokenTableOpen
|
|
101
|
+
| MdzTokenTableClose
|
|
102
|
+
| MdzTokenTableRowOpen
|
|
103
|
+
| MdzTokenTableRowClose
|
|
104
|
+
| MdzTokenTableCellOpen
|
|
105
|
+
| MdzTokenTableCellClose;
|
|
88
106
|
|
|
89
107
|
export interface MdzTokenText extends MdzTokenBase {
|
|
90
108
|
type: 'text';
|
|
@@ -207,6 +225,34 @@ export interface MdzTokenBlockquoteClose extends MdzTokenBase {
|
|
|
207
225
|
type: 'blockquote_close';
|
|
208
226
|
}
|
|
209
227
|
|
|
228
|
+
export interface MdzTokenTableOpen extends MdzTokenBase {
|
|
229
|
+
type: 'table_open';
|
|
230
|
+
/** Per-column alignment from the delimiter row; length is the column count. */
|
|
231
|
+
align: Array<MdzTableAlign>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface MdzTokenTableClose extends MdzTokenBase {
|
|
235
|
+
type: 'table_close';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface MdzTokenTableRowOpen extends MdzTokenBase {
|
|
239
|
+
type: 'table_row_open';
|
|
240
|
+
/** `true` for the header row, `false` for body rows. */
|
|
241
|
+
header: boolean;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface MdzTokenTableRowClose extends MdzTokenBase {
|
|
245
|
+
type: 'table_row_close';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface MdzTokenTableCellOpen extends MdzTokenBase {
|
|
249
|
+
type: 'table_cell_open';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface MdzTokenTableCellClose extends MdzTokenBase {
|
|
253
|
+
type: 'table_cell_close';
|
|
254
|
+
}
|
|
255
|
+
|
|
210
256
|
/**
|
|
211
257
|
* A matched list marker: `- ` (unordered) or `N. ` (ordered, 1-9 digits).
|
|
212
258
|
* `empty` markers have nothing but whitespace after the marker — valid only
|
|
@@ -258,11 +304,75 @@ export class MdzLexer {
|
|
|
258
304
|
* scanner pre-validated every line in the run as continuation).
|
|
259
305
|
*/
|
|
260
306
|
#list_run_strip: boolean = false;
|
|
307
|
+
/**
|
|
308
|
+
* Whether `#tokenize_text` is inside a table cell: `#max_search_index` is a
|
|
309
|
+
* hard stop (the cell's trimmed end, mid-line with no newline to halt on)
|
|
310
|
+
* and `\|` unescapes to a literal pipe.
|
|
311
|
+
*/
|
|
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();
|
|
261
350
|
|
|
262
351
|
constructor(text: string) {
|
|
263
352
|
this.#text = text;
|
|
264
353
|
}
|
|
265
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
|
+
|
|
266
376
|
/**
|
|
267
377
|
* Memoized `String#indexOf` over the immutable text.
|
|
268
378
|
*
|
|
@@ -279,6 +389,10 @@ export class MdzLexer {
|
|
|
279
389
|
return memo.result;
|
|
280
390
|
}
|
|
281
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
|
+
}
|
|
282
396
|
if (memo === undefined) {
|
|
283
397
|
this.#search_memo.set(needle, {from, result});
|
|
284
398
|
} else {
|
|
@@ -300,6 +414,7 @@ export class MdzLexer {
|
|
|
300
414
|
if (this.#tokenize_codeblock()) continue;
|
|
301
415
|
if (this.#tokenize_list()) continue;
|
|
302
416
|
if (this.#tokenize_blockquote()) continue;
|
|
417
|
+
if (this.#tokenize_table()) continue;
|
|
303
418
|
}
|
|
304
419
|
|
|
305
420
|
// Check for paragraph break
|
|
@@ -317,6 +432,25 @@ export class MdzLexer {
|
|
|
317
432
|
return this.#tokens;
|
|
318
433
|
}
|
|
319
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Tokenize a table cell's inline content `[cell_start, cell_end)` as a
|
|
437
|
+
* bounded inline run, with no wrapping cell tokens (the bare form of
|
|
438
|
+
* `#emit_table_cell`, sharing `#tokenize_cell_inline`). The streaming parser
|
|
439
|
+
* reuses it across a row's cells via `MdzTableCellParser`: the token buffer
|
|
440
|
+
* resets per call while the search memo — valid over the immutable bound
|
|
441
|
+
* text — carries across cells (the same reuse the sync lexer does in place).
|
|
442
|
+
*
|
|
443
|
+
* @nodocs
|
|
444
|
+
*/
|
|
445
|
+
lex_table_cell(cell_start: number, cell_end: number): Array<MdzToken> {
|
|
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;
|
|
450
|
+
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
451
|
+
return this.#tokens;
|
|
452
|
+
}
|
|
453
|
+
|
|
320
454
|
#is_at_column_0(): boolean {
|
|
321
455
|
return this.#index === 0 || this.#text.charCodeAt(this.#index - 1) === NEWLINE;
|
|
322
456
|
}
|
|
@@ -604,6 +738,163 @@ export class MdzLexer {
|
|
|
604
738
|
this.#index = last_line_end;
|
|
605
739
|
}
|
|
606
740
|
|
|
741
|
+
// -- Table tokenizers --
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Tokenize a table starting at a column-0 pipe row, emitting
|
|
745
|
+
* `table_open`/`table_row_open`/`table_cell_open` … structure tokens (the
|
|
746
|
+
* header row's cells first, then each body row's). Returns false with no
|
|
747
|
+
* side effects when the current line isn't a table: not a `| … |` row, or
|
|
748
|
+
* not followed by a delimiter row whose column count matches the header. The
|
|
749
|
+
* delimiter row's colons set `align`; body rows continue until a blank line,
|
|
750
|
+
* a non-pipe-row line, or EOF.
|
|
751
|
+
*/
|
|
752
|
+
#tokenize_table(): boolean {
|
|
753
|
+
const table_start = this.#index;
|
|
754
|
+
let header_end = this.#index_of('\n', this.#index);
|
|
755
|
+
if (header_end === -1) header_end = this.#text.length;
|
|
756
|
+
const header_cells = mdz_split_table_row(this.#text, this.#index, header_end);
|
|
757
|
+
if (header_cells === null) return false;
|
|
758
|
+
if (header_end >= this.#text.length) return false; // a header needs a delimiter row beneath it
|
|
759
|
+
const delim_start = header_end + 1;
|
|
760
|
+
let delim_end = this.#index_of('\n', delim_start);
|
|
761
|
+
if (delim_end === -1) delim_end = this.#text.length;
|
|
762
|
+
const align = mdz_parse_table_delimiter(this.#text, delim_start, delim_end);
|
|
763
|
+
if (align === null || align.length !== header_cells.length) return false;
|
|
764
|
+
|
|
765
|
+
// committed — emit the table; the delimiter row is structural (not emitted)
|
|
766
|
+
this.#tokens.push({type: 'table_open', align, start: table_start, end: header_end});
|
|
767
|
+
this.#emit_table_row(header_cells, true, table_start, header_end);
|
|
768
|
+
|
|
769
|
+
this.#index = delim_end;
|
|
770
|
+
let table_end = delim_end;
|
|
771
|
+
|
|
772
|
+
// body rows: consecutive valid pipe rows, until a blank/non-row line or EOF
|
|
773
|
+
while (this.#index < this.#text.length) {
|
|
774
|
+
const row_start = this.#index + 1; // past the newline at #index
|
|
775
|
+
let row_end = this.#index_of('\n', row_start);
|
|
776
|
+
if (row_end === -1) row_end = this.#text.length;
|
|
777
|
+
const cells = mdz_split_table_row(this.#text, row_start, row_end);
|
|
778
|
+
if (cells === null) break;
|
|
779
|
+
this.#emit_table_row(cells, false, row_start, row_end);
|
|
780
|
+
this.#index = row_end;
|
|
781
|
+
table_end = row_end;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
this.#tokens.push({type: 'table_close', start: table_end, end: table_end});
|
|
785
|
+
this.#skip_blank_lines();
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Emit one table row's open / cell / close tokens. */
|
|
790
|
+
#emit_table_row(
|
|
791
|
+
cells: Array<{start: number; end: number}>,
|
|
792
|
+
header: boolean,
|
|
793
|
+
row_start: number,
|
|
794
|
+
row_end: number,
|
|
795
|
+
): void {
|
|
796
|
+
this.#tokens.push({type: 'table_row_open', header, start: row_start, end: row_start});
|
|
797
|
+
for (const cell of cells) {
|
|
798
|
+
this.#emit_table_cell(cell.start, cell.end);
|
|
799
|
+
}
|
|
800
|
+
this.#tokens.push({type: 'table_row_close', start: row_end, end: row_end});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Emit one cell's `table_cell_open`, inline-content tokens, and
|
|
805
|
+
* `table_cell_close`. Inline tokenization is bounded to the cell's trimmed
|
|
806
|
+
* span via `#max_search_index`, with `#in_table_cell` set so `#tokenize_text`
|
|
807
|
+
* stops at that bound and unescapes `\|`.
|
|
808
|
+
*/
|
|
809
|
+
#emit_table_cell(cell_start: number, cell_end: number): void {
|
|
810
|
+
this.#tokens.push({type: 'table_cell_open', start: cell_start, end: cell_start});
|
|
811
|
+
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
812
|
+
this.#tokens.push({type: 'table_cell_close', start: cell_end, end: cell_end});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Tokenize a cell's trimmed inline span `[cell_start, cell_end)` into the
|
|
817
|
+
* current token buffer, bounded by `#max_search_index` with `#in_table_cell`
|
|
818
|
+
* set so `#tokenize_text` stops at the bound and unescapes `\|`. Shared by
|
|
819
|
+
* `#emit_table_cell` (wrapped in cell tokens) and `lex_table_cell` (bare).
|
|
820
|
+
*/
|
|
821
|
+
#tokenize_cell_inline(cell_start: number, cell_end: number): void {
|
|
822
|
+
const saved_max = this.#max_search_index;
|
|
823
|
+
this.#max_search_index = cell_end;
|
|
824
|
+
this.#in_table_cell = true;
|
|
825
|
+
this.#index = cell_start;
|
|
826
|
+
while (this.#index < cell_end) {
|
|
827
|
+
this.#tokenize_inline();
|
|
828
|
+
}
|
|
829
|
+
this.#in_table_cell = false;
|
|
830
|
+
this.#max_search_index = saved_max;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Recognize an in-item table whose header row's leading `|` is at `first`
|
|
835
|
+
* (the line's indent already skipped): a pipe row whose next line is a
|
|
836
|
+
* delimiter row, both indented past `marker_indent` (so both stay inside the
|
|
837
|
+
* item). Returns the parsed header/delimiter, or null. Pure — no emission.
|
|
838
|
+
*/
|
|
839
|
+
#match_item_table_head(
|
|
840
|
+
first: number,
|
|
841
|
+
marker_indent: number,
|
|
842
|
+
): {
|
|
843
|
+
header_cells: Array<{start: number; end: number}>;
|
|
844
|
+
header_end: number;
|
|
845
|
+
align: Array<MdzTableAlign>;
|
|
846
|
+
delim_end: number;
|
|
847
|
+
} | null {
|
|
848
|
+
const header_end = this.#index_of('\n', first);
|
|
849
|
+
if (header_end === -1) return null; // a header needs a delimiter line beneath it
|
|
850
|
+
const header_cells = mdz_split_table_row(this.#text, first, header_end);
|
|
851
|
+
if (header_cells === null) return null;
|
|
852
|
+
const delim_line_start = header_end + 1;
|
|
853
|
+
let dj = delim_line_start;
|
|
854
|
+
while (dj < this.#text.length && is_line_whitespace(this.#text.charCodeAt(dj))) dj++;
|
|
855
|
+
if (dj - delim_line_start <= marker_indent) return null; // delimiter dedented out of the item
|
|
856
|
+
let delim_end = this.#index_of('\n', dj);
|
|
857
|
+
if (delim_end === -1) delim_end = this.#text.length;
|
|
858
|
+
const align = mdz_parse_table_delimiter(this.#text, dj, delim_end);
|
|
859
|
+
if (align === null || align.length !== header_cells.length) return null;
|
|
860
|
+
return {header_cells, header_end, align, delim_end};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** Whether a table opens at `first` as a block child of an item at `marker_indent`. */
|
|
864
|
+
#is_item_table_start(first: number, marker_indent: number): boolean {
|
|
865
|
+
return this.#match_item_table_head(first, marker_indent) !== null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Tokenize an in-item table at `first` (a recognized table start). Mirrors
|
|
870
|
+
* `#tokenize_table` but per row skips the line's indent to the `|` and ends
|
|
871
|
+
* when a row dedents to `marker_indent` or shallower, leaving `#index` at the
|
|
872
|
+
* table's terminating newline so the list loop classifies what follows (no
|
|
873
|
+
* `#skip_blank_lines` — the list owns blank-line containment).
|
|
874
|
+
*/
|
|
875
|
+
#tokenize_item_table(first: number, marker_indent: number): void {
|
|
876
|
+
const head = this.#match_item_table_head(first, marker_indent)!;
|
|
877
|
+
this.#tokens.push({type: 'table_open', align: head.align, start: first, end: head.header_end});
|
|
878
|
+
this.#emit_table_row(head.header_cells, true, first, head.header_end);
|
|
879
|
+
this.#index = head.delim_end;
|
|
880
|
+
let table_end = head.delim_end;
|
|
881
|
+
while (this.#index < this.#text.length) {
|
|
882
|
+
const row_line_start = this.#index + 1; // past the newline at #index
|
|
883
|
+
let rj = row_line_start;
|
|
884
|
+
while (rj < this.#text.length && is_line_whitespace(this.#text.charCodeAt(rj))) rj++;
|
|
885
|
+
if (rj - row_line_start <= marker_indent) break; // dedent ends the table
|
|
886
|
+
if (this.#text.charCodeAt(rj) !== PIPE) break;
|
|
887
|
+
let row_end = this.#index_of('\n', rj);
|
|
888
|
+
if (row_end === -1) row_end = this.#text.length;
|
|
889
|
+
const cells = mdz_split_table_row(this.#text, rj, row_end);
|
|
890
|
+
if (cells === null) break;
|
|
891
|
+
this.#emit_table_row(cells, false, rj, row_end);
|
|
892
|
+
this.#index = row_end;
|
|
893
|
+
table_end = row_end;
|
|
894
|
+
}
|
|
895
|
+
this.#tokens.push({type: 'table_close', start: table_end, end: table_end});
|
|
896
|
+
}
|
|
897
|
+
|
|
607
898
|
// -- List tokenizers --
|
|
608
899
|
|
|
609
900
|
/**
|
|
@@ -739,6 +1030,23 @@ export class MdzLexer {
|
|
|
739
1030
|
}
|
|
740
1031
|
}
|
|
741
1032
|
|
|
1033
|
+
// a deeper pipe-row line whose next line is a delimiter becomes a
|
|
1034
|
+
// table child of the item it would otherwise continue (the fence/quote
|
|
1035
|
+
// precedent, but a two-line lookahead)
|
|
1036
|
+
if (indent > 0 && this.#text.charCodeAt(first) === PIPE) {
|
|
1037
|
+
const target_idx = this.#list_continuation_target(levels, indent);
|
|
1038
|
+
if (target_idx !== -1) {
|
|
1039
|
+
const marker_indent = levels[target_idx]!.indent;
|
|
1040
|
+
if (this.#is_item_table_start(first, marker_indent)) {
|
|
1041
|
+
while (levels.length - 1 > target_idx) {
|
|
1042
|
+
this.#list_close(levels);
|
|
1043
|
+
}
|
|
1044
|
+
this.#tokenize_item_table(first, marker_indent);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
742
1050
|
// any other line deeper than an open item's marker opens a new
|
|
743
1051
|
// `Paragraph` block child of that item (the item's inline run is never
|
|
744
1052
|
// current here — a blank, block child, or dedent intervened)
|
|
@@ -947,6 +1255,9 @@ export class MdzLexer {
|
|
|
947
1255
|
if (this.#text.charCodeAt(j) === RIGHT_ANGLE && this.#is_blockquote_opener(j)) {
|
|
948
1256
|
return nl; // a quote opener line interrupts the run
|
|
949
1257
|
}
|
|
1258
|
+
if (this.#text.charCodeAt(j) === PIPE && this.#is_item_table_start(j, deepest_indent)) {
|
|
1259
|
+
return nl; // a table opener (pipe row + delimiter) interrupts the run
|
|
1260
|
+
}
|
|
950
1261
|
i = line_start; // continuation — keep scanning
|
|
951
1262
|
}
|
|
952
1263
|
}
|
|
@@ -1301,6 +1612,23 @@ export class MdzLexer {
|
|
|
1301
1612
|
|
|
1302
1613
|
#tokenize_markdown_link(): void {
|
|
1303
1614
|
const start = this.#index;
|
|
1615
|
+
// a link open at this offset already failed, or was suppressed by the
|
|
1616
|
+
// nesting cap — emit `[` as text instead of re-descending into the same
|
|
1617
|
+
// failing children (exponential on nested `[`); the short-circuit
|
|
1618
|
+
// reproduces the revert's exact net effect (`[` text, cursor at start + 1)
|
|
1619
|
+
if (this.#failed_link_starts.has(start) || this.#depth_capped.has(start)) {
|
|
1620
|
+
this.#index = start + 1;
|
|
1621
|
+
this.#emit_text('[', start);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
// nesting-depth cap: too deep to open — render literal and record the
|
|
1625
|
+
// suppression so a shallower re-lex short-circuits here too
|
|
1626
|
+
if (this.#inline_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
1627
|
+
this.#depth_capped.add(start);
|
|
1628
|
+
this.#index = start + 1;
|
|
1629
|
+
this.#emit_text('[', start);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1304
1632
|
this.#index++; // consume [ (dispatch guarantees it)
|
|
1305
1633
|
|
|
1306
1634
|
// Emit link_text_open
|
|
@@ -1309,11 +1637,13 @@ export class MdzLexer {
|
|
|
1309
1637
|
// Tokenize children until ] — only `]` delimits link text; a bare `)`
|
|
1310
1638
|
// is ordinary content (matching the streaming parser, which has no `)`
|
|
1311
1639
|
// dispatch case — the reference scan finds its own `)` after `](`)
|
|
1640
|
+
this.#inline_depth++;
|
|
1312
1641
|
while (this.#index < this.#text.length) {
|
|
1313
1642
|
if (this.#text.charCodeAt(this.#index) === RIGHT_BRACKET) break;
|
|
1314
1643
|
if (this.#is_at_paragraph_break()) break;
|
|
1315
1644
|
this.#tokenize_inline();
|
|
1316
1645
|
}
|
|
1646
|
+
this.#inline_depth--;
|
|
1317
1647
|
|
|
1318
1648
|
// Check for ]
|
|
1319
1649
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_BRACKET) {
|
|
@@ -1382,6 +1712,13 @@ export class MdzLexer {
|
|
|
1382
1712
|
}
|
|
1383
1713
|
|
|
1384
1714
|
#revert_tokens_from_link_open(start: number): void {
|
|
1715
|
+
// record the failure so a re-lex from before this `[` short-circuits.
|
|
1716
|
+
// Keyed by `start` alone — unlike `#failed_tag`'s `start:bound` — because a
|
|
1717
|
+
// link's outcome is bound-independent: the `]`-terminator scan (the children
|
|
1718
|
+
// loop) runs to `text.length` and never consults `#max_search_index`, so a
|
|
1719
|
+
// wider-bound re-lex can't move it. The bound reaches nested tags inside the
|
|
1720
|
+
// link text, but re-attempting those wider never flips the link's net result.
|
|
1721
|
+
this.#failed_link_starts.add(start);
|
|
1385
1722
|
// Find and remove link_text_open and all tokens after it
|
|
1386
1723
|
let open_idx = -1;
|
|
1387
1724
|
for (let i = this.#tokens.length - 1; i >= 0; i--) {
|
|
@@ -1399,6 +1736,32 @@ export class MdzLexer {
|
|
|
1399
1736
|
|
|
1400
1737
|
#tokenize_tag(): void {
|
|
1401
1738
|
const start = this.#index;
|
|
1739
|
+
// short-circuit a re-attempt of a tag open that already failed at this
|
|
1740
|
+
// offset+bound — re-descending into the same failing children is
|
|
1741
|
+
// exponential on nested same-name tags. Keyed by
|
|
1742
|
+
// `#max_search_index` (captured as `bound`) because the closer search is
|
|
1743
|
+
// bound-sensitive, so a wider-bound re-lex can legitimately succeed. The
|
|
1744
|
+
// key string is built only when the memo is non-empty (adversarial
|
|
1745
|
+
// input); prose never allocates it. The short-circuit reproduces a
|
|
1746
|
+
// revert's exact net effect (`<` text, cursor at start + 1).
|
|
1747
|
+
const bound = this.#max_search_index;
|
|
1748
|
+
if (
|
|
1749
|
+
this.#depth_capped.has(start) ||
|
|
1750
|
+
(this.#failed_tag.size !== 0 && this.#failed_tag.has(`${start}:${bound}`))
|
|
1751
|
+
) {
|
|
1752
|
+
this.#index = start + 1;
|
|
1753
|
+
this.#emit_text('<', start);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
// nesting-depth cap: too deep to open a tag — render literal and record
|
|
1757
|
+
// the suppression (position-keyed) so a shallower re-lex short-circuits
|
|
1758
|
+
// here too, keeping the revert re-scan linear
|
|
1759
|
+
if (this.#inline_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
1760
|
+
this.#depth_capped.add(start);
|
|
1761
|
+
this.#index = start + 1;
|
|
1762
|
+
this.#emit_text('<', start);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1402
1765
|
this.#index++; // consume <
|
|
1403
1766
|
|
|
1404
1767
|
// Tag name must start with a letter
|
|
@@ -1445,6 +1808,7 @@ export class MdzLexer {
|
|
|
1445
1808
|
|
|
1446
1809
|
// Check for >
|
|
1447
1810
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_ANGLE) {
|
|
1811
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1448
1812
|
this.#index = start + 1;
|
|
1449
1813
|
this.#emit_text('<', start);
|
|
1450
1814
|
return;
|
|
@@ -1463,11 +1827,13 @@ export class MdzLexer {
|
|
|
1463
1827
|
const search_limit = Math.min(this.#max_search_index, this.#text.length);
|
|
1464
1828
|
const closing_tag_pos = this.#index_of(closing_tag, this.#index);
|
|
1465
1829
|
if (closing_tag_pos === -1 || closing_tag_pos > search_limit) {
|
|
1830
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1466
1831
|
this.#index = start + 1;
|
|
1467
1832
|
this.#emit_text('<', start);
|
|
1468
1833
|
return;
|
|
1469
1834
|
}
|
|
1470
1835
|
if (this.#has_paragraph_break_between(this.#index, closing_tag_pos)) {
|
|
1836
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1471
1837
|
this.#index = start + 1;
|
|
1472
1838
|
this.#emit_text('<', start);
|
|
1473
1839
|
return;
|
|
@@ -1483,8 +1849,10 @@ export class MdzLexer {
|
|
|
1483
1849
|
// the code span). Recomputed per iteration because a nested same-name
|
|
1484
1850
|
// tag legitimately consumes the nearest closer (`<b>x<b>y</b></b>`).
|
|
1485
1851
|
const saved_max = this.#max_search_index;
|
|
1852
|
+
this.#inline_depth++;
|
|
1486
1853
|
while (this.#index < this.#text.length) {
|
|
1487
1854
|
if (this.#match(closing_tag)) {
|
|
1855
|
+
this.#inline_depth--;
|
|
1488
1856
|
this.#max_search_index = saved_max;
|
|
1489
1857
|
const close_start = this.#index;
|
|
1490
1858
|
this.#index += closing_tag.length;
|
|
@@ -1507,12 +1875,14 @@ export class MdzLexer {
|
|
|
1507
1875
|
this.#max_search_index = next_close;
|
|
1508
1876
|
this.#tokenize_inline();
|
|
1509
1877
|
}
|
|
1878
|
+
this.#inline_depth--;
|
|
1510
1879
|
|
|
1511
1880
|
// No valid closer left — revert: drop the open token and all consumed
|
|
1512
1881
|
// children tokens, emit `<` as text, and re-lex from the next char
|
|
1513
1882
|
// (the `#revert_tokens_from_link_open` pattern; the old fallthrough
|
|
1514
1883
|
// left a dangling `tag_open` whose parser fallback discarded content)
|
|
1515
1884
|
this.#max_search_index = saved_max;
|
|
1885
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1516
1886
|
this.#tokens.splice(open_token_index);
|
|
1517
1887
|
this.#index = start + 1;
|
|
1518
1888
|
this.#emit_text('<', start);
|
|
@@ -1540,23 +1910,47 @@ export class MdzLexer {
|
|
|
1540
1910
|
let parts: Array<string> | null = null;
|
|
1541
1911
|
let part_start = start;
|
|
1542
1912
|
|
|
1543
|
-
|
|
1913
|
+
// in a table cell the trimmed cell end is a hard stop (no newline to
|
|
1914
|
+
// halt on mid-line) — hoisted to the loop bound
|
|
1915
|
+
const scan_end = this.#in_table_cell
|
|
1916
|
+
? Math.min(this.#max_search_index, this.#text.length)
|
|
1917
|
+
: this.#text.length;
|
|
1918
|
+
|
|
1919
|
+
while (this.#index < scan_end) {
|
|
1544
1920
|
const char_code = this.#text.charCodeAt(this.#index);
|
|
1921
|
+
// one table load classifies the char; zero (the common case — the
|
|
1922
|
+
// char can't end or interrupt the run) skips the dispatch below.
|
|
1923
|
+
// `TEXT_SCAN_CLASS` is shared with the streaming scanner
|
|
1924
|
+
// (`consume_text_run` in `mdz_stream_parser_text.ts`); the two dispatch
|
|
1925
|
+
// bodies intentionally differ around it — this one handles `\|` escapes
|
|
1926
|
+
// and list-run strip, that one URL speculation across chunk boundaries.
|
|
1927
|
+
const char_class = char_code < 128 ? TEXT_SCAN_CLASS[char_code]! : 0;
|
|
1928
|
+
if (char_class === 0) {
|
|
1929
|
+
this.#index++;
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1545
1932
|
|
|
1546
1933
|
// Stop at special characters
|
|
1547
|
-
if (
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1934
|
+
if ((char_class & TEXT_CLASS_STOP) !== 0) break;
|
|
1935
|
+
|
|
1936
|
+
if ((char_class & TEXT_CLASS_BACKSLASH) !== 0) {
|
|
1937
|
+
// `\|` is a literal pipe — the only mdz escape, scoped to table cells
|
|
1938
|
+
if (
|
|
1939
|
+
this.#in_table_cell &&
|
|
1940
|
+
this.#index + 1 < this.#max_search_index &&
|
|
1941
|
+
this.#text.charCodeAt(this.#index + 1) === PIPE
|
|
1942
|
+
) {
|
|
1943
|
+
(parts ??= []).push(this.#text.slice(part_start, this.#index));
|
|
1944
|
+
parts.push('|');
|
|
1945
|
+
this.#index += 2;
|
|
1946
|
+
part_start = this.#index;
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
this.#index++;
|
|
1950
|
+
continue;
|
|
1557
1951
|
}
|
|
1558
1952
|
|
|
1559
|
-
if (
|
|
1953
|
+
if ((char_class & TEXT_CLASS_NEWLINE) !== 0) {
|
|
1560
1954
|
if (this.#list_run_strip) {
|
|
1561
1955
|
if (this.#index >= this.#max_search_index) break; // the run's terminator
|
|
1562
1956
|
// soft break: keep the newline, strip the next line's leading
|
|
@@ -1588,26 +1982,31 @@ export class MdzLexer {
|
|
|
1588
1982
|
next_char === HYPHEN ||
|
|
1589
1983
|
next_char === BACKTICK ||
|
|
1590
1984
|
next_char === ONE ||
|
|
1591
|
-
next_char === RIGHT_ANGLE
|
|
1985
|
+
next_char === RIGHT_ANGLE ||
|
|
1986
|
+
next_char === PIPE
|
|
1592
1987
|
) {
|
|
1593
1988
|
this.#index++; // consume the newline
|
|
1594
1989
|
break;
|
|
1595
1990
|
}
|
|
1596
1991
|
}
|
|
1992
|
+
this.#index++; // single newline — ordinary text
|
|
1993
|
+
continue;
|
|
1597
1994
|
}
|
|
1598
1995
|
|
|
1599
|
-
//
|
|
1600
|
-
if (
|
|
1601
|
-
(
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
) {
|
|
1996
|
+
// URL or internal path mid-text (class guard avoids probing every char)
|
|
1997
|
+
if ((char_class & TEXT_CLASS_URL) !== 0) {
|
|
1998
|
+
if (this.#is_at_url()) break;
|
|
1999
|
+
} else if (char_code === SLASH) {
|
|
2000
|
+
if (is_at_absolute_path(this.#text, this.#index)) break;
|
|
2001
|
+
} else if (is_at_relative_path(this.#text, this.#index)) {
|
|
1605
2002
|
break;
|
|
1606
2003
|
}
|
|
1607
2004
|
|
|
1608
2005
|
this.#index++;
|
|
1609
2006
|
}
|
|
1610
2007
|
|
|
2008
|
+
if (mdz_debug_work.enabled) mdz_debug_work.total += this.#index - start;
|
|
2009
|
+
|
|
1611
2010
|
// Ensure we always consume at least one character
|
|
1612
2011
|
if (this.#index === start && this.#index < this.#text.length) {
|
|
1613
2012
|
this.#index++;
|
package/src/lib/mdz_opcodes.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type {MdzTableAlign} from './mdz.ts';
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Unique monotonic identifier for each node created by the parser.
|
|
17
19
|
* IDs are never reused within a parser instance.
|
|
@@ -39,7 +41,10 @@ export type MdzNodeTypeContainer =
|
|
|
39
41
|
| 'Element'
|
|
40
42
|
| 'Component'
|
|
41
43
|
| 'Codeblock'
|
|
42
|
-
| 'Code'
|
|
44
|
+
| 'Code'
|
|
45
|
+
| 'Table'
|
|
46
|
+
| 'TableRow'
|
|
47
|
+
| 'TableCell';
|
|
43
48
|
|
|
44
49
|
/** Node types for self-contained leaf elements. */
|
|
45
50
|
export type MdzNodeTypeVoid = 'Hr';
|
|
@@ -69,6 +74,10 @@ export interface MdzOpcodeOpen {
|
|
|
69
74
|
start_number?: number;
|
|
70
75
|
/** Authored item number. Present when `node_type` is `'ListItem'` in an ordered list. */
|
|
71
76
|
number?: number;
|
|
77
|
+
/** Per-column alignment from the delimiter row. Present when `node_type` is `'Table'`. */
|
|
78
|
+
align?: Array<MdzTableAlign>;
|
|
79
|
+
/** Whether this is the header row (`thead`/`th`). Present when `node_type` is `'TableRow'`. */
|
|
80
|
+
header?: boolean;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/**
|