@fuzdev/mdz 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +4 -0
  2. package/dist/Mdz.svelte +18 -9
  3. package/dist/Mdz.svelte.d.ts +9 -2
  4. package/dist/Mdz.svelte.d.ts.map +1 -1
  5. package/dist/MdzNodeView.svelte +129 -115
  6. package/dist/MdzNodeView.svelte.d.ts +4 -1
  7. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  8. package/dist/MdzPrecompiled.svelte +3 -3
  9. package/dist/MdzPrecompiled.svelte.d.ts.map +1 -1
  10. package/dist/MdzRoot.svelte +3 -3
  11. package/dist/MdzRoot.svelte.d.ts.map +1 -1
  12. package/dist/MdzStream.svelte +6 -6
  13. package/dist/MdzStream.svelte.d.ts.map +1 -1
  14. package/dist/MdzStreamNodeView.svelte +129 -118
  15. package/dist/MdzStreamNodeView.svelte.d.ts +4 -1
  16. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
  17. package/dist/context_helpers.js +1 -1
  18. package/dist/mdz.d.ts +6 -0
  19. package/dist/mdz.d.ts.map +1 -1
  20. package/dist/mdz.js +8 -0
  21. package/dist/mdz_contexts.d.ts.map +1 -1
  22. package/dist/mdz_debug_work.d.ts +38 -0
  23. package/dist/mdz_debug_work.d.ts.map +1 -0
  24. package/dist/mdz_debug_work.js +37 -0
  25. package/dist/mdz_helpers.d.ts +61 -0
  26. package/dist/mdz_helpers.d.ts.map +1 -1
  27. package/dist/mdz_helpers.js +74 -2
  28. package/dist/mdz_lexer.d.ts +12 -0
  29. package/dist/mdz_lexer.d.ts.map +1 -1
  30. package/dist/mdz_lexer.js +190 -40
  31. package/dist/mdz_opcodes.d.ts.map +1 -1
  32. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
  33. package/dist/mdz_opcodes_to_nodes.js +22 -17
  34. package/dist/mdz_stream_parser.d.ts.map +1 -1
  35. package/dist/mdz_stream_parser.js +8 -8
  36. package/dist/mdz_stream_parser_block.js +5 -5
  37. package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
  38. package/dist/mdz_stream_parser_inline.js +22 -20
  39. package/dist/mdz_stream_parser_link.d.ts.map +1 -1
  40. package/dist/mdz_stream_parser_link.js +88 -56
  41. package/dist/mdz_stream_parser_list.d.ts.map +1 -1
  42. package/dist/mdz_stream_parser_list.js +23 -9
  43. package/dist/mdz_stream_parser_state.d.ts +29 -0
  44. package/dist/mdz_stream_parser_state.d.ts.map +1 -1
  45. package/dist/mdz_stream_parser_state.js +59 -10
  46. package/dist/mdz_stream_parser_table.d.ts +3 -2
  47. package/dist/mdz_stream_parser_table.d.ts.map +1 -1
  48. package/dist/mdz_stream_parser_table.js +24 -13
  49. package/dist/mdz_stream_parser_text.d.ts.map +1 -1
  50. package/dist/mdz_stream_parser_text.js +36 -25
  51. package/dist/mdz_stream_parser_url.js +7 -7
  52. package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
  53. package/dist/mdz_to_svelte.d.ts +1 -1
  54. package/dist/mdz_to_svelte.d.ts.map +1 -1
  55. package/dist/mdz_to_svelte.js +28 -31
  56. package/dist/mdz_token_parser.d.ts.map +1 -1
  57. package/dist/mdz_token_parser.js +24 -25
  58. package/dist/svelte_preprocess_mdz.d.ts +2 -2
  59. package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
  60. package/dist/svelte_preprocess_mdz.js +41 -47
  61. package/package.json +28 -40
  62. package/src/lib/context_helpers.ts +2 -2
  63. package/src/lib/mdz.ts +11 -2
  64. package/src/lib/mdz_contexts.ts +5 -5
  65. package/src/lib/mdz_debug_work.ts +38 -0
  66. package/src/lib/mdz_helpers.ts +126 -23
  67. package/src/lib/mdz_lexer.ts +243 -93
  68. package/src/lib/mdz_opcodes.ts +1 -1
  69. package/src/lib/mdz_opcodes_to_nodes.ts +35 -30
  70. package/src/lib/mdz_stream_parser.ts +17 -17
  71. package/src/lib/mdz_stream_parser_block.ts +14 -14
  72. package/src/lib/mdz_stream_parser_inline.ts +38 -34
  73. package/src/lib/mdz_stream_parser_link.ts +99 -66
  74. package/src/lib/mdz_stream_parser_list.ts +41 -29
  75. package/src/lib/mdz_stream_parser_state.ts +100 -28
  76. package/src/lib/mdz_stream_parser_table.ts +47 -36
  77. package/src/lib/mdz_stream_parser_text.ts +40 -39
  78. package/src/lib/mdz_stream_parser_url.ts +11 -11
  79. package/src/lib/mdz_stream_state.svelte.ts +8 -8
  80. package/src/lib/mdz_to_svelte.ts +38 -46
  81. package/src/lib/mdz_token_parser.ts +55 -58
  82. package/src/lib/svelte_preprocess_mdz.ts +92 -89
  83. package/src/lib/tsdoc_mdz.ts +1 -1
@@ -5,12 +5,12 @@
5
5
  * @module
6
6
  */
7
7
 
8
- import type {MdzNodeTypeContainer} from './mdz_opcodes.ts';
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,
@@ -21,13 +21,14 @@ import {
21
21
  is_tag_name_char,
22
22
  is_valid_path_char,
23
23
  mdz_is_safe_reference,
24
- mdz_is_url,
24
+ mdz_is_url
25
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,
@@ -35,19 +36,28 @@ import {
35
36
  offset,
36
37
  pop_stack_entry,
37
38
  push_stack_entry,
38
- revert_above,
39
+ revert_above
39
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
 
48
58
  const id = alloc_id(state);
49
59
  const link_start = offset(state);
50
- emit(state, {type: 'open', id, node_type: 'Link', start: link_start});
60
+ emit(state, { type: 'open', id, node_type: 'Link', start: link_start });
51
61
  push_stack_entry(state, id, 'Link', link_start, '[');
52
62
  state.active_text_id = null;
53
63
  state.pos++;
@@ -66,12 +76,12 @@ export const try_link_open = (state: MdzStreamParserState): boolean => {
66
76
  export const abort_link_to_text = (
67
77
  state: MdzStreamParserState,
68
78
  link_stack_idx: number,
69
- bracket_pos: number,
79
+ bracket_pos: number
70
80
  ): void => {
71
81
  flush_text(state);
72
82
  revert_above(state, link_stack_idx);
73
83
  const entry = pop_stack_entry(state);
74
- emit(state, {type: 'revert', id: entry.id, replacement_text: '[', start: entry.start});
84
+ emit(state, { type: 'revert', id: entry.id, replacement_text: '[', start: entry.start });
75
85
  state.active_text_id = null;
76
86
  accumulate_text(state, ']', offset(state, bracket_pos));
77
87
  state.pos = bracket_pos + 1;
@@ -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++;
@@ -204,8 +230,8 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
204
230
  ensure_paragraph(state);
205
231
  const id = alloc_id(state);
206
232
  const tag_start = offset(state, start);
207
- emit(state, {type: 'open', id, node_type, start: tag_start, name});
208
- emit(state, {type: 'close', id, end: offset(state, i + 2)});
233
+ emit(state, { type: 'open', id, node_type, start: tag_start, name });
234
+ emit(state, { type: 'close', id, end: offset(state, i + 2) });
209
235
  state.active_text_id = null;
210
236
  state.pos = i + 2;
211
237
  state.column += i + 2 - start;
@@ -228,7 +254,7 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
228
254
  const id = alloc_id(state);
229
255
  const tag_start = offset(state, start);
230
256
  const delimiter = state.buffer.slice(start, i); // e.g., "<Alert>"
231
- emit(state, {type: 'open', id, node_type, start: tag_start, name});
257
+ emit(state, { type: 'open', id, node_type, start: tag_start, name });
232
258
  push_stack_entry(state, id, node_type, tag_start, delimiter, name);
233
259
  state.active_text_id = null;
234
260
  state.pos = 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') &&
@@ -274,7 +307,7 @@ export const try_close_tag = (state: MdzStreamParserState): TryResult => {
274
307
  flush_text(state);
275
308
  revert_above(state, found_idx);
276
309
  const entry = pop_stack_entry(state);
277
- emit(state, {type: 'close', id: entry.id, end: offset(state, i)});
310
+ emit(state, { type: 'close', id: entry.id, end: offset(state, i) });
278
311
  state.active_text_id = null;
279
312
  state.column += i - state.pos;
280
313
  state.pos = i;
@@ -27,9 +27,9 @@ import {
27
27
  ZERO,
28
28
  is_line_whitespace,
29
29
  mdz_match_blockquote_prefix,
30
- mdz_blockquote_line_has_content,
30
+ mdz_blockquote_line_has_content
31
31
  } from './mdz_helpers.ts';
32
- import {match_table_in_item_head, open_table_in_item} from './mdz_stream_parser_table.ts';
32
+ import { match_table_in_item_head, open_table_in_item } from './mdz_stream_parser_table.ts';
33
33
  import {
34
34
  type MdzStreamParserState,
35
35
  type TryResult,
@@ -44,7 +44,7 @@ import {
44
44
  extend_accumulated_end,
45
45
  flush_text,
46
46
  offset,
47
- push_stack_entry,
47
+ push_stack_entry
48
48
  } from './mdz_stream_parser_state.ts';
49
49
 
50
50
  /**
@@ -76,7 +76,7 @@ export interface MdzStreamListMarker {
76
76
  const rest_is_blank = (
77
77
  state: MdzStreamParserState,
78
78
  from: number,
79
- forced: boolean,
79
+ forced: boolean
80
80
  ): boolean | 'pending' => {
81
81
  let i = from;
82
82
  while (i < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(i))) {
@@ -98,9 +98,9 @@ const rest_is_blank = (
98
98
  export const match_list_marker = (
99
99
  state: MdzStreamParserState,
100
100
  i: number,
101
- forced: boolean,
101
+ forced: boolean
102
102
  ): MdzStreamListMarker | 'pending' | null => {
103
- const {buffer} = state;
103
+ const { buffer } = state;
104
104
  const c = buffer.charCodeAt(i);
105
105
  if (c === HYPHEN) {
106
106
  const after = i + 1;
@@ -113,7 +113,7 @@ export const match_list_marker = (
113
113
  start: i,
114
114
  marker_end: after,
115
115
  content_start: after,
116
- empty: true,
116
+ empty: true
117
117
  };
118
118
  }
119
119
  // in bounds: `blank === false` means the scan found a char at or past `after`
@@ -121,7 +121,7 @@ export const match_list_marker = (
121
121
  const content_start = after + 1;
122
122
  const empty = rest_is_blank(state, content_start, forced);
123
123
  if (empty === 'pending') return 'pending';
124
- return {ordered: false, number: 0, start: i, marker_end: after, content_start, empty};
124
+ return { ordered: false, number: 0, start: i, marker_end: after, content_start, empty };
125
125
  }
126
126
  if (c >= ZERO && c <= NINE) {
127
127
  let j = i + 1;
@@ -142,7 +142,7 @@ export const match_list_marker = (
142
142
  start: i,
143
143
  marker_end: after,
144
144
  content_start: after,
145
- empty: true,
145
+ empty: true
146
146
  };
147
147
  }
148
148
  // in bounds: `blank === false` means the scan found a char at or past `after`
@@ -150,7 +150,7 @@ export const match_list_marker = (
150
150
  const content_start = after + 1;
151
151
  const empty = rest_is_blank(state, content_start, forced);
152
152
  if (empty === 'pending') return 'pending';
153
- return {ordered: true, number, start: i, marker_end: after, content_start, empty};
153
+ return { ordered: true, number, start: i, marker_end: after, content_start, empty };
154
154
  }
155
155
  return null;
156
156
  };
@@ -179,9 +179,9 @@ export interface MdzStreamFenceOpener {
179
179
  export const match_fence_opener = (
180
180
  state: MdzStreamParserState,
181
181
  i: number,
182
- forced: boolean,
182
+ forced: boolean
183
183
  ): MdzStreamFenceOpener | 'pending' | null => {
184
- const {buffer} = state;
184
+ const { buffer } = state;
185
185
  let j = i;
186
186
  let count = 0;
187
187
  while (j < buffer.length && buffer.charCodeAt(j) === BACKTICK) {
@@ -202,7 +202,7 @@ export const match_fence_opener = (
202
202
  }
203
203
  if (j >= buffer.length) return forced ? null : 'pending'; // the opener needs its newline
204
204
  if (buffer.charCodeAt(j) !== NEWLINE) return null;
205
- return {backtick_count: count, lang, content_start: j + 1};
205
+ return { backtick_count: count, lang, content_start: j + 1 };
206
206
  };
207
207
 
208
208
  /** Emit a `ListItem` open for `marker` and push its frame. */
@@ -210,9 +210,9 @@ const open_list_item = (state: MdzStreamParserState, marker: MdzStreamListMarker
210
210
  const id = alloc_id(state);
211
211
  const start = offset(state, marker.start);
212
212
  if (marker.ordered) {
213
- emit(state, {type: 'open', id, node_type: 'ListItem', start, number: marker.number});
213
+ emit(state, { type: 'open', id, node_type: 'ListItem', start, number: marker.number });
214
214
  } else {
215
- emit(state, {type: 'open', id, node_type: 'ListItem', start});
215
+ emit(state, { type: 'open', id, node_type: 'ListItem', start });
216
216
  }
217
217
  push_stack_entry(state, id, 'ListItem', start);
218
218
  if (marker.empty) {
@@ -226,7 +226,7 @@ const open_list_item = (state: MdzStreamParserState, marker: MdzStreamListMarker
226
226
  const open_list_level = (
227
227
  state: MdzStreamParserState,
228
228
  indent: number,
229
- marker: MdzStreamListMarker,
229
+ marker: MdzStreamListMarker
230
230
  ): void => {
231
231
  const id = alloc_id(state);
232
232
  const start = offset(state, marker.start);
@@ -237,14 +237,14 @@ const open_list_level = (
237
237
  node_type: 'List',
238
238
  start,
239
239
  ordered: true,
240
- start_number: marker.number,
240
+ start_number: marker.number
241
241
  });
242
242
  } else {
243
- emit(state, {type: 'open', id, node_type: 'List', start, ordered: false});
243
+ emit(state, { type: 'open', id, node_type: 'List', start, ordered: false });
244
244
  }
245
245
  push_stack_entry(state, id, 'List', start);
246
246
  open_list_item(state, marker);
247
- state.list_levels.push({indent, ordered: marker.ordered});
247
+ state.list_levels.push({ indent, ordered: marker.ordered });
248
248
  };
249
249
 
250
250
  /**
@@ -256,7 +256,7 @@ const open_list_level = (
256
256
  const begin_item_run = (
257
257
  state: MdzStreamParserState,
258
258
  marker: MdzStreamListMarker,
259
- line_start: number,
259
+ line_start: number
260
260
  ): void => {
261
261
  let i = marker.content_start;
262
262
  while (i < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(i))) {
@@ -289,7 +289,7 @@ const list_continuation_target = (state: MdzStreamParserState, indent: number):
289
289
  const continue_run = (
290
290
  state: MdzStreamParserState,
291
291
  nl_pos: number,
292
- content_pos: number,
292
+ content_pos: number
293
293
  ): boolean => {
294
294
  accumulate_text(state, '\n', offset(state, nl_pos));
295
295
  extend_accumulated_end(state, offset(state, content_pos));
@@ -352,9 +352,9 @@ export const try_list_start = (state: MdzStreamParserState, forced: boolean): Tr
352
352
  */
353
353
  export const process_list_line = (
354
354
  state: MdzStreamParserState,
355
- forced: boolean,
355
+ forced: boolean
356
356
  ): boolean | 'blockquote' => {
357
- const {buffer} = state;
357
+ const { buffer } = state;
358
358
  const nl_pos = state.pos;
359
359
  const line_start = nl_pos + 1;
360
360
  let j = line_start;
@@ -418,10 +418,16 @@ export const process_list_line = (
418
418
 
419
419
  if (marker !== null && marker.empty) {
420
420
  // empty item: only at an indent exactly matching an open level of the
421
- // same marker type — anything else degrades below
422
- const level_idx = state.list_levels.findIndex(
423
- (l) => l.indent === indent && l.ordered === marker.ordered,
424
- );
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
+ }
425
431
  if (level_idx !== -1) {
426
432
  close_list_run(state);
427
433
  while (state.list_levels.length - 1 > level_idx) {
@@ -456,13 +462,19 @@ export const process_list_line = (
456
462
  }
457
463
  const id = alloc_id(state);
458
464
  const cb_start = offset(state, j);
459
- emit(state, {type: 'open', id, node_type: 'Codeblock', start: cb_start, lang: fence.lang});
465
+ emit(state, {
466
+ type: 'open',
467
+ id,
468
+ node_type: 'Codeblock',
469
+ start: cb_start,
470
+ lang: fence.lang
471
+ });
460
472
  state.codeblock = {
461
473
  id,
462
474
  backtick_count: fence.backtick_count,
463
475
  text_id: null,
464
476
  fence_indent: indent,
465
- start: cb_start,
477
+ start: cb_start
466
478
  };
467
479
  state.active_text_id = null;
468
480
  state.pos = fence.content_start;