@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.
Files changed (77) hide show
  1. package/README.md +4 -0
  2. package/dist/Mdz.svelte +15 -7
  3. package/dist/Mdz.svelte.d.ts +9 -2
  4. package/dist/Mdz.svelte.d.ts.map +1 -1
  5. package/dist/MdzNodeView.svelte +130 -95
  6. package/dist/MdzNodeView.svelte.d.ts +6 -2
  7. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  8. package/dist/MdzStream.svelte +3 -3
  9. package/dist/MdzStream.svelte.d.ts.map +1 -1
  10. package/dist/MdzStreamNodeView.svelte +129 -95
  11. package/dist/MdzStreamNodeView.svelte.d.ts +6 -2
  12. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
  13. package/dist/context_helpers.d.ts +1 -1
  14. package/dist/mdz.d.ts +59 -2
  15. package/dist/mdz.d.ts.map +1 -1
  16. package/dist/mdz.js +37 -1
  17. package/dist/mdz_debug_work.d.ts +38 -0
  18. package/dist/mdz_debug_work.d.ts.map +1 -0
  19. package/dist/mdz_debug_work.js +37 -0
  20. package/dist/mdz_helpers.d.ts +96 -1
  21. package/dist/mdz_helpers.d.ts.map +1 -1
  22. package/dist/mdz_helpers.js +192 -0
  23. package/dist/mdz_lexer.d.ts +47 -1
  24. package/dist/mdz_lexer.d.ts.map +1 -1
  25. package/dist/mdz_lexer.js +364 -15
  26. package/dist/mdz_opcodes.d.ts +6 -1
  27. package/dist/mdz_opcodes.d.ts.map +1 -1
  28. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
  29. package/dist/mdz_opcodes_to_nodes.js +26 -0
  30. package/dist/mdz_stream_parser.d.ts.map +1 -1
  31. package/dist/mdz_stream_parser.js +20 -1
  32. package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
  33. package/dist/mdz_stream_parser_inline.js +19 -17
  34. package/dist/mdz_stream_parser_link.d.ts.map +1 -1
  35. package/dist/mdz_stream_parser_link.js +88 -56
  36. package/dist/mdz_stream_parser_list.d.ts.map +1 -1
  37. package/dist/mdz_stream_parser_list.js +32 -3
  38. package/dist/mdz_stream_parser_state.d.ts +41 -0
  39. package/dist/mdz_stream_parser_state.d.ts.map +1 -1
  40. package/dist/mdz_stream_parser_state.js +53 -3
  41. package/dist/mdz_stream_parser_table.d.ts +73 -0
  42. package/dist/mdz_stream_parser_table.d.ts.map +1 -0
  43. package/dist/mdz_stream_parser_table.js +362 -0
  44. package/dist/mdz_stream_parser_text.d.ts.map +1 -1
  45. package/dist/mdz_stream_parser_text.js +35 -24
  46. package/dist/mdz_stream_state.svelte.d.ts +5 -0
  47. package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
  48. package/dist/mdz_stream_state.svelte.js +8 -0
  49. package/dist/mdz_to_svelte.d.ts.map +1 -1
  50. package/dist/mdz_to_svelte.js +51 -20
  51. package/dist/mdz_token_parser.d.ts +16 -0
  52. package/dist/mdz_token_parser.d.ts.map +1 -1
  53. package/dist/mdz_token_parser.js +112 -14
  54. package/dist/svelte_preprocess_mdz.js +33 -39
  55. package/package.json +11 -8
  56. package/src/lib/context_helpers.ts +1 -1
  57. package/src/lib/mdz.ts +79 -3
  58. package/src/lib/mdz_contexts.ts +1 -1
  59. package/src/lib/mdz_debug_work.ts +38 -0
  60. package/src/lib/mdz_helpers.ts +222 -1
  61. package/src/lib/mdz_lexer.ts +420 -21
  62. package/src/lib/mdz_opcodes.ts +10 -1
  63. package/src/lib/mdz_opcodes_to_nodes.ts +37 -3
  64. package/src/lib/mdz_stream_parser.ts +26 -9
  65. package/src/lib/mdz_stream_parser_block.ts +2 -2
  66. package/src/lib/mdz_stream_parser_inline.ts +25 -21
  67. package/src/lib/mdz_stream_parser_link.ts +92 -59
  68. package/src/lib/mdz_stream_parser_list.ts +34 -6
  69. package/src/lib/mdz_stream_parser_state.ts +87 -5
  70. package/src/lib/mdz_stream_parser_table.ts +420 -0
  71. package/src/lib/mdz_stream_parser_text.ts +40 -39
  72. package/src/lib/mdz_stream_parser_url.ts +2 -2
  73. package/src/lib/mdz_stream_state.svelte.ts +8 -1
  74. package/src/lib/mdz_to_svelte.ts +49 -23
  75. package/src/lib/mdz_token_parser.ts +135 -21
  76. package/src/lib/svelte_preprocess_mdz.ts +37 -38
  77. package/src/lib/tsdoc_mdz.ts +1 -1
@@ -6,11 +6,12 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ import type { MdzTableAlign } from './mdz.js';
9
10
  export interface MdzTokenBase {
10
11
  start: number;
11
12
  end: number;
12
13
  }
13
- export type MdzToken = MdzTokenText | MdzTokenCode | MdzTokenCodeblock | MdzTokenBoldOpen | MdzTokenBoldClose | MdzTokenItalicOpen | MdzTokenItalicClose | MdzTokenStrikethroughOpen | MdzTokenStrikethroughClose | MdzTokenLinkTextOpen | MdzTokenLinkTextClose | MdzTokenLinkRef | MdzTokenAutolink | MdzTokenHeadingStart | MdzTokenHr | MdzTokenTagOpen | MdzTokenTagSelfClose | MdzTokenTagClose | MdzTokenHeadingEnd | MdzTokenParagraphBreak | MdzTokenListOpen | MdzTokenListClose | MdzTokenListItemOpen | MdzTokenListItemClose | MdzTokenBlockquoteOpen | MdzTokenBlockquoteClose;
14
+ export type MdzToken = MdzTokenText | MdzTokenCode | MdzTokenCodeblock | MdzTokenBoldOpen | MdzTokenBoldClose | MdzTokenItalicOpen | MdzTokenItalicClose | MdzTokenStrikethroughOpen | MdzTokenStrikethroughClose | MdzTokenLinkTextOpen | MdzTokenLinkTextClose | MdzTokenLinkRef | MdzTokenAutolink | MdzTokenHeadingStart | MdzTokenHr | MdzTokenTagOpen | MdzTokenTagSelfClose | MdzTokenTagClose | MdzTokenHeadingEnd | MdzTokenParagraphBreak | MdzTokenListOpen | MdzTokenListClose | MdzTokenListItemOpen | MdzTokenListItemClose | MdzTokenBlockquoteOpen | MdzTokenBlockquoteClose | MdzTokenTableOpen | MdzTokenTableClose | MdzTokenTableRowOpen | MdzTokenTableRowClose | MdzTokenTableCellOpen | MdzTokenTableCellClose;
14
15
  export interface MdzTokenText extends MdzTokenBase {
15
16
  type: 'text';
16
17
  content: string;
@@ -106,6 +107,28 @@ export interface MdzTokenBlockquoteOpen extends MdzTokenBase {
106
107
  export interface MdzTokenBlockquoteClose extends MdzTokenBase {
107
108
  type: 'blockquote_close';
108
109
  }
110
+ export interface MdzTokenTableOpen extends MdzTokenBase {
111
+ type: 'table_open';
112
+ /** Per-column alignment from the delimiter row; length is the column count. */
113
+ align: Array<MdzTableAlign>;
114
+ }
115
+ export interface MdzTokenTableClose extends MdzTokenBase {
116
+ type: 'table_close';
117
+ }
118
+ export interface MdzTokenTableRowOpen extends MdzTokenBase {
119
+ type: 'table_row_open';
120
+ /** `true` for the header row, `false` for body rows. */
121
+ header: boolean;
122
+ }
123
+ export interface MdzTokenTableRowClose extends MdzTokenBase {
124
+ type: 'table_row_close';
125
+ }
126
+ export interface MdzTokenTableCellOpen extends MdzTokenBase {
127
+ type: 'table_cell_open';
128
+ }
129
+ export interface MdzTokenTableCellClose extends MdzTokenBase {
130
+ type: 'table_cell_close';
131
+ }
109
132
  /**
110
133
  * Tokenizes mdz input into a flat `MdzToken` stream for `MdzTokenParser`.
111
134
  *
@@ -118,6 +141,29 @@ export interface MdzTokenBlockquoteClose extends MdzTokenBase {
118
141
  export declare class MdzLexer {
119
142
  #private;
120
143
  constructor(text: string);
144
+ /**
145
+ * Rebind to a new text, so one lexer instance can be reused across inputs —
146
+ * the streaming table path reuses one across a table's rows via
147
+ * `MdzTableCellParser` instead of allocating a lexer (and its memo `Map`)
148
+ * per row. The search memos survive when the text is identical (they're
149
+ * valid over the immutable text — the common case for rows parsed from one
150
+ * buffered chunk) and clear otherwise. Per-parse cursor state is reset by
151
+ * `lex_table_cell` itself.
152
+ *
153
+ * @nodocs
154
+ */
155
+ rebind(text: string): void;
121
156
  tokenize(): Array<MdzToken>;
157
+ /**
158
+ * Tokenize a table cell's inline content `[cell_start, cell_end)` as a
159
+ * bounded inline run, with no wrapping cell tokens (the bare form of
160
+ * `#emit_table_cell`, sharing `#tokenize_cell_inline`). The streaming parser
161
+ * reuses it across a row's cells via `MdzTableCellParser`: the token buffer
162
+ * resets per call while the search memo — valid over the immutable bound
163
+ * text — carries across cells (the same reuse the sync lexer does in place).
164
+ *
165
+ * @nodocs
166
+ */
167
+ lex_table_cell(cell_start: number, cell_end: number): Array<MdzToken>;
122
168
  }
123
169
  //# sourceMappingURL=mdz_lexer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mdz_lexer.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_lexer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mdz_lexer.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_lexer.ts"],"names":[],"mappings":"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"}
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, } from './mdz_helpers.js';
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
  //
@@ -33,9 +34,73 @@ export class MdzLexer {
33
34
  * scanner pre-validated every line in the run as continuation).
34
35
  */
35
36
  #list_run_strip = false;
37
+ /**
38
+ * Whether `#tokenize_text` is inside a table cell: `#max_search_index` is a
39
+ * hard stop (the cell's trimmed end, mid-line with no newline to halt on)
40
+ * and `\|` unescapes to a literal pipe.
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();
36
80
  constructor(text) {
37
81
  this.#text = text;
38
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
+ }
39
104
  /**
40
105
  * Memoized `String#indexOf` over the immutable text.
41
106
  *
@@ -52,6 +117,10 @@ export class MdzLexer {
52
117
  return memo.result;
53
118
  }
54
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
+ }
55
124
  if (memo === undefined) {
56
125
  this.#search_memo.set(needle, { from, result });
57
126
  }
@@ -77,6 +146,8 @@ export class MdzLexer {
77
146
  continue;
78
147
  if (this.#tokenize_blockquote())
79
148
  continue;
149
+ if (this.#tokenize_table())
150
+ continue;
80
151
  }
81
152
  // Check for paragraph break
82
153
  if (this.#is_at_paragraph_break()) {
@@ -90,6 +161,24 @@ export class MdzLexer {
90
161
  }
91
162
  return this.#tokens;
92
163
  }
164
+ /**
165
+ * Tokenize a table cell's inline content `[cell_start, cell_end)` as a
166
+ * bounded inline run, with no wrapping cell tokens (the bare form of
167
+ * `#emit_table_cell`, sharing `#tokenize_cell_inline`). The streaming parser
168
+ * reuses it across a row's cells via `MdzTableCellParser`: the token buffer
169
+ * resets per call while the search memo — valid over the immutable bound
170
+ * text — carries across cells (the same reuse the sync lexer does in place).
171
+ *
172
+ * @nodocs
173
+ */
174
+ lex_table_cell(cell_start, cell_end) {
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;
179
+ this.#tokenize_cell_inline(cell_start, cell_end);
180
+ return this.#tokens;
181
+ }
93
182
  #is_at_column_0() {
94
183
  return this.#index === 0 || this.#text.charCodeAt(this.#index - 1) === NEWLINE;
95
184
  }
@@ -350,6 +439,156 @@ export class MdzLexer {
350
439
  this.#tokens.push({ type: 'blockquote_close', start: last_line_end, end: last_line_end });
351
440
  this.#index = last_line_end;
352
441
  }
442
+ // -- Table tokenizers --
443
+ /**
444
+ * Tokenize a table starting at a column-0 pipe row, emitting
445
+ * `table_open`/`table_row_open`/`table_cell_open` … structure tokens (the
446
+ * header row's cells first, then each body row's). Returns false with no
447
+ * side effects when the current line isn't a table: not a `| … |` row, or
448
+ * not followed by a delimiter row whose column count matches the header. The
449
+ * delimiter row's colons set `align`; body rows continue until a blank line,
450
+ * a non-pipe-row line, or EOF.
451
+ */
452
+ #tokenize_table() {
453
+ const table_start = this.#index;
454
+ let header_end = this.#index_of('\n', this.#index);
455
+ if (header_end === -1)
456
+ header_end = this.#text.length;
457
+ const header_cells = mdz_split_table_row(this.#text, this.#index, header_end);
458
+ if (header_cells === null)
459
+ return false;
460
+ if (header_end >= this.#text.length)
461
+ return false; // a header needs a delimiter row beneath it
462
+ const delim_start = header_end + 1;
463
+ let delim_end = this.#index_of('\n', delim_start);
464
+ if (delim_end === -1)
465
+ delim_end = this.#text.length;
466
+ const align = mdz_parse_table_delimiter(this.#text, delim_start, delim_end);
467
+ if (align === null || align.length !== header_cells.length)
468
+ return false;
469
+ // committed — emit the table; the delimiter row is structural (not emitted)
470
+ this.#tokens.push({ type: 'table_open', align, start: table_start, end: header_end });
471
+ this.#emit_table_row(header_cells, true, table_start, header_end);
472
+ this.#index = delim_end;
473
+ let table_end = delim_end;
474
+ // body rows: consecutive valid pipe rows, until a blank/non-row line or EOF
475
+ while (this.#index < this.#text.length) {
476
+ const row_start = this.#index + 1; // past the newline at #index
477
+ let row_end = this.#index_of('\n', row_start);
478
+ if (row_end === -1)
479
+ row_end = this.#text.length;
480
+ const cells = mdz_split_table_row(this.#text, row_start, row_end);
481
+ if (cells === null)
482
+ break;
483
+ this.#emit_table_row(cells, false, row_start, row_end);
484
+ this.#index = row_end;
485
+ table_end = row_end;
486
+ }
487
+ this.#tokens.push({ type: 'table_close', start: table_end, end: table_end });
488
+ this.#skip_blank_lines();
489
+ return true;
490
+ }
491
+ /** Emit one table row's open / cell / close tokens. */
492
+ #emit_table_row(cells, header, row_start, row_end) {
493
+ this.#tokens.push({ type: 'table_row_open', header, start: row_start, end: row_start });
494
+ for (const cell of cells) {
495
+ this.#emit_table_cell(cell.start, cell.end);
496
+ }
497
+ this.#tokens.push({ type: 'table_row_close', start: row_end, end: row_end });
498
+ }
499
+ /**
500
+ * Emit one cell's `table_cell_open`, inline-content tokens, and
501
+ * `table_cell_close`. Inline tokenization is bounded to the cell's trimmed
502
+ * span via `#max_search_index`, with `#in_table_cell` set so `#tokenize_text`
503
+ * stops at that bound and unescapes `\|`.
504
+ */
505
+ #emit_table_cell(cell_start, cell_end) {
506
+ this.#tokens.push({ type: 'table_cell_open', start: cell_start, end: cell_start });
507
+ this.#tokenize_cell_inline(cell_start, cell_end);
508
+ this.#tokens.push({ type: 'table_cell_close', start: cell_end, end: cell_end });
509
+ }
510
+ /**
511
+ * Tokenize a cell's trimmed inline span `[cell_start, cell_end)` into the
512
+ * current token buffer, bounded by `#max_search_index` with `#in_table_cell`
513
+ * set so `#tokenize_text` stops at the bound and unescapes `\|`. Shared by
514
+ * `#emit_table_cell` (wrapped in cell tokens) and `lex_table_cell` (bare).
515
+ */
516
+ #tokenize_cell_inline(cell_start, cell_end) {
517
+ const saved_max = this.#max_search_index;
518
+ this.#max_search_index = cell_end;
519
+ this.#in_table_cell = true;
520
+ this.#index = cell_start;
521
+ while (this.#index < cell_end) {
522
+ this.#tokenize_inline();
523
+ }
524
+ this.#in_table_cell = false;
525
+ this.#max_search_index = saved_max;
526
+ }
527
+ /**
528
+ * Recognize an in-item table whose header row's leading `|` is at `first`
529
+ * (the line's indent already skipped): a pipe row whose next line is a
530
+ * delimiter row, both indented past `marker_indent` (so both stay inside the
531
+ * item). Returns the parsed header/delimiter, or null. Pure — no emission.
532
+ */
533
+ #match_item_table_head(first, marker_indent) {
534
+ const header_end = this.#index_of('\n', first);
535
+ if (header_end === -1)
536
+ return null; // a header needs a delimiter line beneath it
537
+ const header_cells = mdz_split_table_row(this.#text, first, header_end);
538
+ if (header_cells === null)
539
+ return null;
540
+ const delim_line_start = header_end + 1;
541
+ let dj = delim_line_start;
542
+ while (dj < this.#text.length && is_line_whitespace(this.#text.charCodeAt(dj)))
543
+ dj++;
544
+ if (dj - delim_line_start <= marker_indent)
545
+ return null; // delimiter dedented out of the item
546
+ let delim_end = this.#index_of('\n', dj);
547
+ if (delim_end === -1)
548
+ delim_end = this.#text.length;
549
+ const align = mdz_parse_table_delimiter(this.#text, dj, delim_end);
550
+ if (align === null || align.length !== header_cells.length)
551
+ return null;
552
+ return { header_cells, header_end, align, delim_end };
553
+ }
554
+ /** Whether a table opens at `first` as a block child of an item at `marker_indent`. */
555
+ #is_item_table_start(first, marker_indent) {
556
+ return this.#match_item_table_head(first, marker_indent) !== null;
557
+ }
558
+ /**
559
+ * Tokenize an in-item table at `first` (a recognized table start). Mirrors
560
+ * `#tokenize_table` but per row skips the line's indent to the `|` and ends
561
+ * when a row dedents to `marker_indent` or shallower, leaving `#index` at the
562
+ * table's terminating newline so the list loop classifies what follows (no
563
+ * `#skip_blank_lines` — the list owns blank-line containment).
564
+ */
565
+ #tokenize_item_table(first, marker_indent) {
566
+ const head = this.#match_item_table_head(first, marker_indent);
567
+ this.#tokens.push({ type: 'table_open', align: head.align, start: first, end: head.header_end });
568
+ this.#emit_table_row(head.header_cells, true, first, head.header_end);
569
+ this.#index = head.delim_end;
570
+ let table_end = head.delim_end;
571
+ while (this.#index < this.#text.length) {
572
+ const row_line_start = this.#index + 1; // past the newline at #index
573
+ let rj = row_line_start;
574
+ while (rj < this.#text.length && is_line_whitespace(this.#text.charCodeAt(rj)))
575
+ rj++;
576
+ if (rj - row_line_start <= marker_indent)
577
+ break; // dedent ends the table
578
+ if (this.#text.charCodeAt(rj) !== PIPE)
579
+ break;
580
+ let row_end = this.#index_of('\n', rj);
581
+ if (row_end === -1)
582
+ row_end = this.#text.length;
583
+ const cells = mdz_split_table_row(this.#text, rj, row_end);
584
+ if (cells === null)
585
+ break;
586
+ this.#emit_table_row(cells, false, rj, row_end);
587
+ this.#index = row_end;
588
+ table_end = row_end;
589
+ }
590
+ this.#tokens.push({ type: 'table_close', start: table_end, end: table_end });
591
+ }
353
592
  // -- List tokenizers --
354
593
  /**
355
594
  * Tokenize an entire list starting at a column-0 marker line, emitting
@@ -478,6 +717,22 @@ export class MdzLexer {
478
717
  continue;
479
718
  }
480
719
  }
720
+ // a deeper pipe-row line whose next line is a delimiter becomes a
721
+ // table child of the item it would otherwise continue (the fence/quote
722
+ // precedent, but a two-line lookahead)
723
+ if (indent > 0 && this.#text.charCodeAt(first) === PIPE) {
724
+ const target_idx = this.#list_continuation_target(levels, indent);
725
+ if (target_idx !== -1) {
726
+ const marker_indent = levels[target_idx].indent;
727
+ if (this.#is_item_table_start(first, marker_indent)) {
728
+ while (levels.length - 1 > target_idx) {
729
+ this.#list_close(levels);
730
+ }
731
+ this.#tokenize_item_table(first, marker_indent);
732
+ continue;
733
+ }
734
+ }
735
+ }
481
736
  // any other line deeper than an open item's marker opens a new
482
737
  // `Paragraph` block child of that item (the item's inline run is never
483
738
  // current here — a blank, block child, or dedent intervened)
@@ -681,6 +936,9 @@ export class MdzLexer {
681
936
  if (this.#text.charCodeAt(j) === RIGHT_ANGLE && this.#is_blockquote_opener(j)) {
682
937
  return nl; // a quote opener line interrupts the run
683
938
  }
939
+ if (this.#text.charCodeAt(j) === PIPE && this.#is_item_table_start(j, deepest_indent)) {
940
+ return nl; // a table opener (pipe row + delimiter) interrupts the run
941
+ }
684
942
  i = line_start; // continuation — keep scanning
685
943
  }
686
944
  }
@@ -1002,12 +1260,30 @@ export class MdzLexer {
1002
1260
  }
1003
1261
  #tokenize_markdown_link() {
1004
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
+ }
1005
1280
  this.#index++; // consume [ (dispatch guarantees it)
1006
1281
  // Emit link_text_open
1007
1282
  this.#tokens.push({ type: 'link_text_open', start, end: this.#index });
1008
1283
  // Tokenize children until ] — only `]` delimits link text; a bare `)`
1009
1284
  // is ordinary content (matching the streaming parser, which has no `)`
1010
1285
  // dispatch case — the reference scan finds its own `)` after `](`)
1286
+ this.#inline_depth++;
1011
1287
  while (this.#index < this.#text.length) {
1012
1288
  if (this.#text.charCodeAt(this.#index) === RIGHT_BRACKET)
1013
1289
  break;
@@ -1015,6 +1291,7 @@ export class MdzLexer {
1015
1291
  break;
1016
1292
  this.#tokenize_inline();
1017
1293
  }
1294
+ this.#inline_depth--;
1018
1295
  // Check for ]
1019
1296
  if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_BRACKET) {
1020
1297
  // Revert - remove link_text_open and all children tokens added
@@ -1071,6 +1348,13 @@ export class MdzLexer {
1071
1348
  });
1072
1349
  }
1073
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);
1074
1358
  // Find and remove link_text_open and all tokens after it
1075
1359
  let open_idx = -1;
1076
1360
  for (let i = this.#tokens.length - 1; i >= 0; i--) {
@@ -1087,6 +1371,30 @@ export class MdzLexer {
1087
1371
  }
1088
1372
  #tokenize_tag() {
1089
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
+ }
1090
1398
  this.#index++; // consume <
1091
1399
  // Tag name must start with a letter
1092
1400
  if (this.#index >= this.#text.length || !is_letter(this.#text.charCodeAt(this.#index))) {
@@ -1123,6 +1431,7 @@ export class MdzLexer {
1123
1431
  }
1124
1432
  // Check for >
1125
1433
  if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_ANGLE) {
1434
+ this.#failed_tag.add(`${start}:${bound}`);
1126
1435
  this.#index = start + 1;
1127
1436
  this.#emit_text('<', start);
1128
1437
  return;
@@ -1140,11 +1449,13 @@ export class MdzLexer {
1140
1449
  const search_limit = Math.min(this.#max_search_index, this.#text.length);
1141
1450
  const closing_tag_pos = this.#index_of(closing_tag, this.#index);
1142
1451
  if (closing_tag_pos === -1 || closing_tag_pos > search_limit) {
1452
+ this.#failed_tag.add(`${start}:${bound}`);
1143
1453
  this.#index = start + 1;
1144
1454
  this.#emit_text('<', start);
1145
1455
  return;
1146
1456
  }
1147
1457
  if (this.#has_paragraph_break_between(this.#index, closing_tag_pos)) {
1458
+ this.#failed_tag.add(`${start}:${bound}`);
1148
1459
  this.#index = start + 1;
1149
1460
  this.#emit_text('<', start);
1150
1461
  return;
@@ -1158,8 +1469,10 @@ export class MdzLexer {
1158
1469
  // the code span). Recomputed per iteration because a nested same-name
1159
1470
  // tag legitimately consumes the nearest closer (`<b>x<b>y</b></b>`).
1160
1471
  const saved_max = this.#max_search_index;
1472
+ this.#inline_depth++;
1161
1473
  while (this.#index < this.#text.length) {
1162
1474
  if (this.#match(closing_tag)) {
1475
+ this.#inline_depth--;
1163
1476
  this.#max_search_index = saved_max;
1164
1477
  const close_start = this.#index;
1165
1478
  this.#index += closing_tag.length;
@@ -1180,11 +1493,13 @@ export class MdzLexer {
1180
1493
  this.#max_search_index = next_close;
1181
1494
  this.#tokenize_inline();
1182
1495
  }
1496
+ this.#inline_depth--;
1183
1497
  // No valid closer left — revert: drop the open token and all consumed
1184
1498
  // children tokens, emit `<` as text, and re-lex from the next char
1185
1499
  // (the `#revert_tokens_from_link_open` pattern; the old fallthrough
1186
1500
  // left a dangling `tag_open` whose parser fallback discarded content)
1187
1501
  this.#max_search_index = saved_max;
1502
+ this.#failed_tag.add(`${start}:${bound}`);
1188
1503
  this.#tokens.splice(open_token_index);
1189
1504
  this.#index = start + 1;
1190
1505
  this.#emit_text('<', start);
@@ -1208,19 +1523,42 @@ export class MdzLexer {
1208
1523
  // content: parts accumulate around each stripped region
1209
1524
  let parts = null;
1210
1525
  let part_start = start;
1211
- while (this.#index < this.#text.length) {
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) {
1212
1532
  const char_code = this.#text.charCodeAt(this.#index);
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
+ }
1213
1544
  // Stop at special characters
1214
- if (char_code === BACKTICK ||
1215
- char_code === ASTERISK ||
1216
- char_code === UNDERSCORE ||
1217
- char_code === TILDE ||
1218
- char_code === LEFT_BRACKET ||
1219
- char_code === RIGHT_BRACKET ||
1220
- char_code === LEFT_ANGLE) {
1545
+ if ((char_class & TEXT_CLASS_STOP) !== 0)
1221
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 &&
1550
+ this.#index + 1 < this.#max_search_index &&
1551
+ this.#text.charCodeAt(this.#index + 1) === PIPE) {
1552
+ (parts ??= []).push(this.#text.slice(part_start, this.#index));
1553
+ parts.push('|');
1554
+ this.#index += 2;
1555
+ part_start = this.#index;
1556
+ continue;
1557
+ }
1558
+ this.#index++;
1559
+ continue;
1222
1560
  }
1223
- if (char_code === NEWLINE) {
1561
+ if ((char_class & TEXT_CLASS_NEWLINE) !== 0) {
1224
1562
  if (this.#list_run_strip) {
1225
1563
  if (this.#index >= this.#max_search_index)
1226
1564
  break; // the run's terminator
@@ -1251,20 +1589,31 @@ export class MdzLexer {
1251
1589
  next_char === HYPHEN ||
1252
1590
  next_char === BACKTICK ||
1253
1591
  next_char === ONE ||
1254
- next_char === RIGHT_ANGLE) {
1592
+ next_char === RIGHT_ANGLE ||
1593
+ next_char === PIPE) {
1255
1594
  this.#index++; // consume the newline
1256
1595
  break;
1257
1596
  }
1258
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;
1259
1609
  }
1260
- // Check for URL or internal path mid-text (char code guard avoids startsWith on every char)
1261
- if (((char_code === 104 /* h */ || char_code === 72) /* H */ && this.#is_at_url()) ||
1262
- (char_code === SLASH && is_at_absolute_path(this.#text, this.#index)) ||
1263
- (char_code === PERIOD && is_at_relative_path(this.#text, this.#index))) {
1610
+ else if (is_at_relative_path(this.#text, this.#index)) {
1264
1611
  break;
1265
1612
  }
1266
1613
  this.#index++;
1267
1614
  }
1615
+ if (mdz_debug_work.enabled)
1616
+ mdz_debug_work.total += this.#index - start;
1268
1617
  // Ensure we always consume at least one character
1269
1618
  if (this.#index === start && this.#index < this.#text.length) {
1270
1619
  this.#index++;
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * @module
13
13
  */
14
+ import type { MdzTableAlign } from './mdz.js';
14
15
  /**
15
16
  * Unique monotonic identifier for each node created by the parser.
16
17
  * IDs are never reused within a parser instance.
@@ -24,7 +25,7 @@ export type MdzNodeId = number;
24
25
  * visible yet opens an optimistic `Code` container whose content streams as
25
26
  * text children (collapsed back to `content` by consumers at `close`).
26
27
  */
27
- export type MdzNodeTypeContainer = 'Paragraph' | 'Bold' | 'Italic' | 'Strikethrough' | 'Link' | 'Heading' | 'List' | 'ListItem' | 'Blockquote' | 'Element' | 'Component' | 'Codeblock' | 'Code';
28
+ export type MdzNodeTypeContainer = 'Paragraph' | 'Bold' | 'Italic' | 'Strikethrough' | 'Link' | 'Heading' | 'List' | 'ListItem' | 'Blockquote' | 'Element' | 'Component' | 'Codeblock' | 'Code' | 'Table' | 'TableRow' | 'TableCell';
28
29
  /** Node types for self-contained leaf elements. */
29
30
  export type MdzNodeTypeVoid = 'Hr';
30
31
  /** Discriminant for leaf text nodes (see `MdzNodeTypeContainer` for `Code`'s dual form). */
@@ -51,6 +52,10 @@ export interface MdzOpcodeOpen {
51
52
  start_number?: number;
52
53
  /** Authored item number. Present when `node_type` is `'ListItem'` in an ordered list. */
53
54
  number?: number;
55
+ /** Per-column alignment from the delimiter row. Present when `node_type` is `'Table'`. */
56
+ align?: Array<MdzTableAlign>;
57
+ /** Whether this is the header row (`thead`/`th`). Present when `node_type` is `'TableRow'`. */
58
+ header?: boolean;
54
59
  }
55
60
  /**
56
61
  * Close a previously opened container node.