@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
@@ -26,9 +26,12 @@ import type {
26
26
  MdzNodeHeading,
27
27
  MdzNodeCode,
28
28
  MdzNodeListItem,
29
- } from './mdz.js';
30
- import type {MdzOpcode, MdzNodeId} from './mdz_opcodes.js';
31
- import {mdz_extract_single_tag, mdz_heading_id, mdz_merge_adjacent_text} from './mdz_helpers.js';
29
+ MdzNodeTableRow,
30
+ MdzNodeTableCell,
31
+ MdzTableAlign,
32
+ } from './mdz.ts';
33
+ import type {MdzOpcode, MdzNodeId} from './mdz_opcodes.ts';
34
+ import {mdz_extract_single_tag, mdz_heading_id, mdz_merge_adjacent_text} from './mdz_helpers.ts';
32
35
 
33
36
  interface StackFrame {
34
37
  id: MdzNodeId;
@@ -43,6 +46,8 @@ interface StackFrame {
43
46
  ordered?: boolean;
44
47
  start_number?: number;
45
48
  number?: number;
49
+ align?: Array<MdzTableAlign>;
50
+ header?: boolean;
46
51
  // metadata from close opcode (deferred)
47
52
  end?: number;
48
53
  reference?: string;
@@ -76,6 +81,8 @@ export const mdz_opcodes_to_nodes = (opcodes: Array<MdzOpcode>): Array<MdzNode>
76
81
  ordered: op.ordered,
77
82
  start_number: op.start_number,
78
83
  number: op.number,
84
+ align: op.align,
85
+ header: op.header,
79
86
  });
80
87
  break;
81
88
  }
@@ -417,6 +424,33 @@ const build_node = (frame: StackFrame, frame_children: Array<MdzNode>): MdzNode
417
424
  return {type: 'Blockquote', children: frame_children, start: frame.start, end};
418
425
  }
419
426
 
427
+ case 'Table': {
428
+ // rows are the only children — `end` derives from the last, matching
429
+ // the sync token parser
430
+ const children = frame_children as Array<MdzNodeTableRow>;
431
+ const end = children.length > 0 ? children[children.length - 1]!.end : frame.end!;
432
+ return {type: 'Table', align: frame.align ?? [], children, start: frame.start, end};
433
+ }
434
+
435
+ case 'TableRow': {
436
+ const children = frame_children as Array<MdzNodeTableCell>;
437
+ return {
438
+ type: 'TableRow',
439
+ header: frame.header ?? false,
440
+ children,
441
+ start: frame.start,
442
+ end: frame.end!,
443
+ };
444
+ }
445
+
446
+ case 'TableCell':
447
+ return {
448
+ type: 'TableCell',
449
+ children: mdz_merge_adjacent_text(frame_children),
450
+ start: frame.start,
451
+ end: frame.end!,
452
+ };
453
+
420
454
  case 'Code': {
421
455
  // optimistic inline code container — concatenate children text into leaf MdzNodeCode
422
456
  let content = '';
@@ -35,7 +35,7 @@
35
35
  * @module
36
36
  */
37
37
 
38
- import type {MdzOpcode} from './mdz_opcodes.js';
38
+ import type {MdzOpcode} from './mdz_opcodes.ts';
39
39
  import {
40
40
  BACKTICK,
41
41
  ASTERISK,
@@ -44,6 +44,7 @@ import {
44
44
  NEWLINE,
45
45
  HYPHEN,
46
46
  HASH,
47
+ PIPE,
47
48
  LEFT_ANGLE,
48
49
  RIGHT_ANGLE,
49
50
  SLASH,
@@ -61,7 +62,7 @@ import {
61
62
  mdz_blockquote_line_has_content,
62
63
  mdz_blockquote_strip_width,
63
64
  mdz_remap_segments,
64
- } from './mdz_helpers.js';
65
+ } from './mdz_helpers.ts';
65
66
  import {
66
67
  type MdzStreamParserState,
67
68
  type TryResult,
@@ -80,7 +81,7 @@ import {
80
81
  pop_stack_entry,
81
82
  push_stack_entry,
82
83
  revert_failed_close,
83
- } from './mdz_stream_parser_state.js';
84
+ } from './mdz_stream_parser_state.ts';
84
85
  import {
85
86
  process_codeblock,
86
87
  process_codeblock_forced,
@@ -88,8 +89,9 @@ import {
88
89
  try_codeblock_open,
89
90
  try_heading,
90
91
  try_hr,
91
- } from './mdz_stream_parser_block.js';
92
- import {process_list_line, try_list_start} from './mdz_stream_parser_list.js';
92
+ } from './mdz_stream_parser_block.ts';
93
+ import {process_list_line, try_list_start} from './mdz_stream_parser_list.ts';
94
+ import {close_table, process_table_line, try_table_start} from './mdz_stream_parser_table.ts';
93
95
  import {
94
96
  check_close_word_boundary,
95
97
  close_delimiter,
@@ -97,13 +99,13 @@ import {
97
99
  try_code,
98
100
  try_double_delimiter,
99
101
  try_italic,
100
- } from './mdz_stream_parser_inline.js';
102
+ } from './mdz_stream_parser_inline.ts';
101
103
  import {
102
104
  try_close_tag,
103
105
  try_complete_link,
104
106
  try_link_open,
105
107
  try_tag_open,
106
- } from './mdz_stream_parser_link.js';
108
+ } from './mdz_stream_parser_link.ts';
107
109
  import {
108
110
  complete_pending_url,
109
111
  process_url_content,
@@ -111,8 +113,8 @@ import {
111
113
  try_auto_path_absolute,
112
114
  try_auto_path_relative,
113
115
  try_auto_url_forced,
114
- } from './mdz_stream_parser_url.js';
115
- import {consume_delimiter_as_text, consume_text_run} from './mdz_stream_parser_text.js';
116
+ } from './mdz_stream_parser_url.ts';
117
+ import {consume_delimiter_as_text, consume_text_run} from './mdz_stream_parser_text.ts';
116
118
 
117
119
  /**
118
120
  * Streaming opcode parser for mdz content.
@@ -224,6 +226,11 @@ const finish_state = (state: MdzStreamParserState): void => {
224
226
  if (state.buffer.length > 0) {
225
227
  process_loop(state, true);
226
228
  }
229
+ // close an open top-level table — forced processing ends it at the last
230
+ // row, but an already-drained buffer (everything consumed) needs this net
231
+ if (state.table) {
232
+ close_table(state);
233
+ }
227
234
  // close an open blockquote — finishes its nested inner document (an
228
235
  // unclosed fence inside ends at the quote's EOF, which is this) and
229
236
  // cascades recursively through deeper quotes
@@ -558,6 +565,12 @@ const process_loop = (state: MdzStreamParserState, forced: boolean): void => {
558
565
  continue;
559
566
  }
560
567
 
568
+ // open table — each line is a body row or ends the table
569
+ if (state.table) {
570
+ if (!process_table_line(state, forced)) return; // need more input
571
+ continue;
572
+ }
573
+
561
574
  const char_code = state.buffer.charCodeAt(state.pos);
562
575
 
563
576
  // newline handling
@@ -632,6 +645,10 @@ const process_loop = (state: MdzStreamParserState, forced: boolean): void => {
632
645
  const r = try_blockquote_start(state, forced);
633
646
  if (r === 'consumed') continue;
634
647
  if (!forced && r === 'need_more') return;
648
+ } else if (char_code === PIPE) {
649
+ const r = try_table_start(state, forced);
650
+ if (r === 'consumed') continue;
651
+ if (!forced && r === 'need_more') return;
635
652
  }
636
653
  }
637
654
 
@@ -17,7 +17,7 @@ import {
17
17
  NEWLINE,
18
18
  SPACE,
19
19
  is_line_whitespace,
20
- } from './mdz_helpers.js';
20
+ } from './mdz_helpers.ts';
21
21
  import {
22
22
  type CodeblockState,
23
23
  type MdzStreamParserState,
@@ -28,7 +28,7 @@ import {
28
28
  flush_text,
29
29
  offset,
30
30
  push_stack_entry,
31
- } from './mdz_stream_parser_state.js';
31
+ } from './mdz_stream_parser_state.ts';
32
32
 
33
33
  /**
34
34
  * Try to parse a heading at column 0.
@@ -14,7 +14,7 @@ import {
14
14
  is_word_char,
15
15
  mdz_blockquote_line_has_content,
16
16
  mdz_match_blockquote_prefix,
17
- } from './mdz_helpers.js';
17
+ } from './mdz_helpers.ts';
18
18
  import {
19
19
  type MdzStreamParserState,
20
20
  accumulate_text,
@@ -28,9 +28,9 @@ import {
28
28
  pop_stack_entry,
29
29
  push_stack_entry,
30
30
  revert_above,
31
- } from './mdz_stream_parser_state.js';
32
- import {match_fence_opener, match_list_marker} from './mdz_stream_parser_list.js';
33
- import {consume_delimiter_as_text} from './mdz_stream_parser_text.js';
31
+ } from './mdz_stream_parser_state.ts';
32
+ import {match_fence_opener, match_list_marker} from './mdz_stream_parser_list.ts';
33
+ import {consume_delimiter_as_text} from './mdz_stream_parser_text.ts';
34
34
 
35
35
  /**
36
36
  * Whether `buffer` contains a paragraph break — a newline followed (across
@@ -339,17 +339,24 @@ export const scan_closer_boundary = (
339
339
  /** @nodocs */
340
340
  export const try_code = (state: MdzStreamParserState, forced = false): boolean => {
341
341
  const start = state.pos;
342
- let i = start + 1; // past opening backtick
342
+ const search_start = start + 1; // past opening backtick
343
343
 
344
344
  // compute search boundary: don't scan past open formatting delimiters
345
- const search_limit = code_search_limit(state, start + 1);
345
+ const search_limit = code_search_limit(state, search_start);
346
346
 
347
- // scan for closing backtick (must be before newline and search boundary)
348
- while (i < state.buffer.length && i < search_limit) {
349
- const c = state.buffer.charCodeAt(i);
350
- if (c === BACKTICK) {
347
+ // find the first closing backtick or newline (memoized — a candidate that
348
+ // holds under open formatting re-enters here on every feed, and a raw scan
349
+ // would re-cover the same growing region each time, going quadratic on a
350
+ // single long line)
351
+ const close_pos = buffer_index_of(state, '`', search_start);
352
+ const newline_pos = buffer_index_of(state, '\n', search_start);
353
+ const terminator =
354
+ close_pos !== -1 && (newline_pos === -1 || close_pos < newline_pos) ? close_pos : newline_pos;
355
+
356
+ if (terminator !== -1 && terminator < search_limit) {
357
+ if (terminator === close_pos) {
351
358
  // found close
352
- const content = state.buffer.slice(start + 1, i);
359
+ const content = state.buffer.slice(search_start, terminator);
353
360
  if (content.length === 0) {
354
361
  // empty code — treat as text
355
362
  consume_delimiter_as_text(state, '``');
@@ -364,20 +371,17 @@ export const try_code = (state: MdzStreamParserState, forced = false): boolean =
364
371
  content,
365
372
  text_type: 'Code',
366
373
  start: offset(state, start),
367
- end: offset(state, i + 1),
374
+ end: offset(state, terminator + 1),
368
375
  });
369
376
  state.active_text_id = null;
370
- state.pos = i + 1;
371
- state.column += i + 1 - start;
377
+ state.pos = terminator + 1;
378
+ state.column += terminator + 1 - start;
372
379
  state.prev_char = BACKTICK;
373
380
  return true;
374
381
  }
375
- if (c === NEWLINE) {
376
- // inline code can't span lines — treat opening backtick as text
377
- consume_delimiter_as_text(state, '`');
378
- return true;
379
- }
380
- i++;
382
+ // newline before any close — inline code can't span lines
383
+ consume_delimiter_as_text(state, '`');
384
+ return true;
381
385
  }
382
386
 
383
387
  // hit search limit (open formatting delimiter boundary) — treat backtick as
@@ -386,7 +390,7 @@ export const try_code = (state: MdzStreamParserState, forced = false): boolean =
386
390
  // processed), the one-shot parse rejects that italic and scans unbounded —
387
391
  // see the `MdzStreamParser` TSDoc and the parity suite's expected-divergence
388
392
  // battery
389
- if (i < state.buffer.length && i >= search_limit) {
393
+ if (search_limit < state.buffer.length) {
390
394
  consume_delimiter_as_text(state, '`');
391
395
  return true;
392
396
  }
@@ -5,12 +5,12 @@
5
5
  * @module
6
6
  */
7
7
 
8
- import type {MdzNodeTypeContainer} from './mdz_opcodes.js';
8
+ import type {MdzNodeTypeContainer} from './mdz_opcodes.ts';
9
9
  import {
10
10
  A_UPPER,
11
11
  LEFT_BRACKET,
12
12
  LEFT_PAREN,
13
- NEWLINE,
13
+ MAX_INLINE_NESTING_DEPTH,
14
14
  RIGHT_ANGLE,
15
15
  RIGHT_BRACKET,
16
16
  RIGHT_PAREN,
@@ -22,12 +22,13 @@ import {
22
22
  is_valid_path_char,
23
23
  mdz_is_safe_reference,
24
24
  mdz_is_url,
25
- } from './mdz_helpers.js';
25
+ } from './mdz_helpers.ts';
26
26
  import {
27
27
  type MdzStreamParserState,
28
28
  type TryResult,
29
29
  accumulate_text,
30
30
  alloc_id,
31
+ buffer_index_of,
31
32
  emit,
32
33
  ensure_paragraph,
33
34
  flush_text,
@@ -36,12 +37,21 @@ import {
36
37
  pop_stack_entry,
37
38
  push_stack_entry,
38
39
  revert_above,
39
- } from './mdz_stream_parser_state.js';
40
+ } from './mdz_stream_parser_state.ts';
41
+ import {consume_delimiter_as_text} from './mdz_stream_parser_text.ts';
42
+ import {mdz_debug_work} from './mdz_debug_work.ts';
40
43
 
41
44
  // -- Links --
42
45
 
43
46
  /** @nodocs */
44
47
  export const try_link_open = (state: MdzStreamParserState): boolean => {
48
+ // nesting-depth cap: too deep to open — render `[` literal, matching the
49
+ // sync lexer's `#tokenize_markdown_link` cap (keeps the two parsers agreeing
50
+ // on where deep `[` stops nesting)
51
+ if (state.open_link_tag_depth >= MAX_INLINE_NESTING_DEPTH) {
52
+ consume_delimiter_as_text(state, '[');
53
+ return true;
54
+ }
45
55
  flush_text(state);
46
56
  ensure_paragraph(state);
47
57
 
@@ -97,65 +107,69 @@ export const try_complete_link = (state: MdzStreamParserState, link_stack_idx: n
97
107
  return true;
98
108
  }
99
109
 
100
- // found ]( — scan for )
101
- let i = bracket_pos + 2; // past ](
102
- while (i < state.buffer.length) {
103
- const c = state.buffer.charCodeAt(i);
104
- if (c === RIGHT_PAREN) {
105
- const reference = state.buffer.slice(bracket_pos + 2, i);
106
- // validate reference: non-empty and only valid path chars
107
- if (!reference.trim()) {
108
- abort_link_to_text(state, link_stack_idx, bracket_pos);
109
- return true;
110
- }
111
- let valid = true;
112
- for (let j = 0; j < reference.length; j++) {
113
- if (!is_valid_path_char(reference.charCodeAt(j))) {
114
- valid = false;
115
- break;
116
- }
117
- }
118
- if (!valid) {
119
- abort_link_to_text(state, link_stack_idx, bracket_pos);
120
- return true;
121
- }
122
- // Reject unsafe protocols (javascript:, data:, etc.) at parse time.
123
- // `is_valid_path_char` permits `:` so we have to filter explicitly.
124
- if (!mdz_is_safe_reference(reference)) {
125
- abort_link_to_text(state, link_stack_idx, bracket_pos);
126
- return true;
127
- }
128
-
129
- // success — close the link with reference
130
- flush_text(state);
131
- revert_above(state, link_stack_idx);
132
- const entry = pop_stack_entry(state);
133
- const link_type = mdz_is_url(reference) ? 'external' : 'internal';
134
- emit(state, {
135
- type: 'close',
136
- id: entry.id,
137
- end: offset(state, i + 1),
138
- reference,
139
- link_type,
140
- });
141
- state.active_text_id = null;
142
- state.pos = i + 1;
143
- state.column += i + 1 - bracket_pos;
144
- state.prev_char = RIGHT_PAREN;
110
+ // found ]( — find the reference's terminator: `)` completes, a space or
111
+ // newline aborts. Memoized searches (see `buffer_index_of`) — a link whose
112
+ // URL arrives incrementally re-enters here on every feed with `pos` still
113
+ // at the `]`, and a raw scan would re-cover the growing reference each
114
+ // time, going quadratic on long streamed URLs.
115
+ const ref_start = bracket_pos + 2;
116
+ const close_pos = buffer_index_of(state, ')', ref_start);
117
+ const space_pos = buffer_index_of(state, ' ', ref_start);
118
+ const newline_pos = buffer_index_of(state, '\n', ref_start);
119
+ let invalid_pos = space_pos;
120
+ if (newline_pos !== -1 && (invalid_pos === -1 || newline_pos < invalid_pos)) {
121
+ invalid_pos = newline_pos;
122
+ }
123
+
124
+ if (close_pos === -1) {
125
+ // didn't find ) — need more input or invalid
126
+ if (invalid_pos === -1) return false; // need more input
127
+ // found invalid char before any ) — revert
128
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
129
+ return true;
130
+ }
131
+ if (invalid_pos !== -1 && invalid_pos < close_pos) {
132
+ // invalid character in URL (simplified check)
133
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
134
+ return true;
135
+ }
136
+
137
+ const i = close_pos;
138
+ const reference = state.buffer.slice(ref_start, i);
139
+ // validate reference: non-empty and only valid path chars
140
+ if (!reference.trim()) {
141
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
142
+ return true;
143
+ }
144
+ for (let j = 0; j < reference.length; j++) {
145
+ if (!is_valid_path_char(reference.charCodeAt(j))) {
146
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
145
147
  return true;
146
148
  }
147
- if (c === NEWLINE || c === SPACE) {
148
- // invalid character in URL (simplified check)
149
- break;
150
- }
151
- i++;
149
+ }
150
+ // Reject unsafe protocols (javascript:, data:, etc.) at parse time.
151
+ // `is_valid_path_char` permits `:` so we have to filter explicitly.
152
+ if (!mdz_is_safe_reference(reference)) {
153
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
154
+ return true;
152
155
  }
153
156
 
154
- // didn't find ) — need more input or invalid
155
- if (i >= state.buffer.length) return false; // need more input
156
-
157
- // found invalid char before ) — revert
158
- abort_link_to_text(state, link_stack_idx, bracket_pos);
157
+ // success — close the link with reference
158
+ flush_text(state);
159
+ revert_above(state, link_stack_idx);
160
+ const entry = pop_stack_entry(state);
161
+ const link_type = mdz_is_url(reference) ? 'external' : 'internal';
162
+ emit(state, {
163
+ type: 'close',
164
+ id: entry.id,
165
+ end: offset(state, i + 1),
166
+ reference,
167
+ link_type,
168
+ });
169
+ state.active_text_id = null;
170
+ state.pos = i + 1;
171
+ state.column += i + 1 - bracket_pos;
172
+ state.prev_char = RIGHT_PAREN;
159
173
  return true;
160
174
  };
161
175
 
@@ -176,7 +190,19 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
176
190
  return 'not_match';
177
191
  }
178
192
 
193
+ // nesting-depth cap: `<letter…` is a tag open, but we're too deep to nest —
194
+ // render `<` literal, matching the sync lexer's `#tokenize_tag` cap. Checked
195
+ // after the closer/hold cases above (a `</…>` closer still closes; a `<` at
196
+ // the buffer end still holds), so only genuine opens are suppressed.
197
+ if (state.open_link_tag_depth >= MAX_INLINE_NESTING_DEPTH) {
198
+ consume_delimiter_as_text(state, '<');
199
+ return 'consumed';
200
+ }
201
+
179
202
  // collect tag name
203
+ // TODO the name/whitespace scans rescan from `start` on every held retry
204
+ // of an unclosed tag's open tail — needs a hold watermark; a length cap would
205
+ // diverge from the sync parser on pathological multi-KB "tag names"
180
206
  const name_start = i;
181
207
  while (i < state.buffer.length && is_tag_name_char(state.buffer.charCodeAt(i))) {
182
208
  i++;
@@ -255,9 +281,16 @@ export const try_close_tag = (state: MdzStreamParserState): TryResult => {
255
281
  const name = state.buffer.slice(name_start, i);
256
282
  i++; // past >
257
283
 
284
+ // O(1) bail when no tag of this name is open — without it, every `</name>`
285
+ // candidate walks the whole inline stack, which grows one frame per leaked
286
+ // unclosed `<Tag>` and goes quadratic on tag-dense input (the same shape
287
+ // `open_counts` guards for the delimiter inlines)
288
+ if (!state.open_tag_counts?.get(name)) return 'not_match';
289
+
258
290
  // find matching open tag on stack
259
291
  let found_idx = -1;
260
292
  for (let j = state.stack.length - 1; j >= 0; j--) {
293
+ if (mdz_debug_work.enabled) mdz_debug_work.total++; // frames walked (mismatched-tags guard)
261
294
  const entry = state.stack[j]!;
262
295
  if (
263
296
  (entry.node_type === 'Element' || entry.node_type === 'Component') &&
@@ -21,13 +21,15 @@ import {
21
21
  NEWLINE,
22
22
  NINE,
23
23
  PERIOD,
24
+ PIPE,
24
25
  RIGHT_ANGLE,
25
26
  SPACE,
26
27
  ZERO,
27
28
  is_line_whitespace,
28
29
  mdz_match_blockquote_prefix,
29
30
  mdz_blockquote_line_has_content,
30
- } from './mdz_helpers.js';
31
+ } from './mdz_helpers.ts';
32
+ import {match_table_in_item_head, open_table_in_item} from './mdz_stream_parser_table.ts';
31
33
  import {
32
34
  type MdzStreamParserState,
33
35
  type TryResult,
@@ -43,7 +45,7 @@ import {
43
45
  flush_text,
44
46
  offset,
45
47
  push_stack_entry,
46
- } from './mdz_stream_parser_state.js';
48
+ } from './mdz_stream_parser_state.ts';
47
49
 
48
50
  /**
49
51
  * A matched list marker: `- ` (unordered) or `N. ` (ordered, 1-9 digits).
@@ -416,10 +418,16 @@ export const process_list_line = (
416
418
 
417
419
  if (marker !== null && marker.empty) {
418
420
  // empty item: only at an indent exactly matching an open level of the
419
- // same marker type — anything else degrades below
420
- const level_idx = state.list_levels.findIndex(
421
- (l) => l.indent === indent && l.ordered === marker.ordered,
422
- );
421
+ // same marker type — anything else degrades below (manual loop like
422
+ // `list_continuation_target` — no per-line closure allocation)
423
+ let level_idx = -1;
424
+ for (let li = 0; li < state.list_levels.length; li++) {
425
+ const l = state.list_levels[li]!;
426
+ if (l.indent === indent && l.ordered === marker.ordered) {
427
+ level_idx = li;
428
+ break;
429
+ }
430
+ }
423
431
  if (level_idx !== -1) {
424
432
  close_list_run(state);
425
433
  while (state.list_levels.length - 1 > level_idx) {
@@ -495,6 +503,26 @@ export const process_list_line = (
495
503
  }
496
504
  }
497
505
 
506
+ // a deeper pipe-row line whose next line is a delimiter becomes a table
507
+ // child of the item it would otherwise continue (the fence/quote precedent,
508
+ // a two-line lookahead held until the delimiter line arrives)
509
+ if (indent > 0 && buffer.charCodeAt(j) === PIPE) {
510
+ const target_idx = list_continuation_target(state, indent);
511
+ if (target_idx !== -1) {
512
+ const marker_indent = state.list_levels[target_idx]!.indent;
513
+ const head = match_table_in_item_head(state, j, marker_indent, forced);
514
+ if (head === 'pending') return false;
515
+ if (head !== null) {
516
+ close_list_run(state);
517
+ while (state.list_levels.length - 1 > target_idx) {
518
+ close_list_level(state);
519
+ }
520
+ open_table_in_item(state, j, marker_indent, head);
521
+ return true;
522
+ }
523
+ }
524
+ }
525
+
498
526
  if (run_current && indent > deepest.indent) {
499
527
  // continuation line: soft break, structural indent stripped
500
528
  return continue_run(state, nl_pos, j);