@fuzdev/mdz 0.1.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/Mdz.svelte +39 -0
  4. package/dist/Mdz.svelte.d.ts +17 -0
  5. package/dist/Mdz.svelte.d.ts.map +1 -0
  6. package/dist/MdzNodeView.svelte +140 -0
  7. package/dist/MdzNodeView.svelte.d.ts +9 -0
  8. package/dist/MdzNodeView.svelte.d.ts.map +1 -0
  9. package/dist/MdzPrecompiled.svelte +33 -0
  10. package/dist/MdzPrecompiled.svelte.d.ts +17 -0
  11. package/dist/MdzPrecompiled.svelte.d.ts.map +1 -0
  12. package/dist/MdzRoot.svelte +40 -0
  13. package/dist/MdzRoot.svelte.d.ts +14 -0
  14. package/dist/MdzRoot.svelte.d.ts.map +1 -0
  15. package/dist/MdzStream.svelte +37 -0
  16. package/dist/MdzStream.svelte.d.ts +18 -0
  17. package/dist/MdzStream.svelte.d.ts.map +1 -0
  18. package/dist/MdzStreamNodeView.svelte +150 -0
  19. package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
  20. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
  21. package/dist/context_helpers.d.ts +23 -0
  22. package/dist/context_helpers.d.ts.map +1 -0
  23. package/dist/context_helpers.js +19 -0
  24. package/dist/mdz.d.ts +152 -0
  25. package/dist/mdz.d.ts.map +1 -0
  26. package/dist/mdz.js +61 -0
  27. package/dist/mdz_contexts.d.ts +106 -0
  28. package/dist/mdz_contexts.d.ts.map +1 -0
  29. package/dist/mdz_contexts.js +42 -0
  30. package/dist/mdz_helpers.d.ts +321 -0
  31. package/dist/mdz_helpers.d.ts.map +1 -0
  32. package/dist/mdz_helpers.js +668 -0
  33. package/dist/mdz_lexer.d.ts +123 -0
  34. package/dist/mdz_lexer.d.ts.map +1 -0
  35. package/dist/mdz_lexer.js +1405 -0
  36. package/dist/mdz_opcodes.d.ts +193 -0
  37. package/dist/mdz_opcodes.d.ts.map +1 -0
  38. package/dist/mdz_opcodes.js +14 -0
  39. package/dist/mdz_opcodes_to_nodes.d.ts +26 -0
  40. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
  41. package/dist/mdz_opcodes_to_nodes.js +410 -0
  42. package/dist/mdz_stream_parser.d.ts +109 -0
  43. package/dist/mdz_stream_parser.d.ts.map +1 -0
  44. package/dist/mdz_stream_parser.js +699 -0
  45. package/dist/mdz_stream_parser_block.d.ts +99 -0
  46. package/dist/mdz_stream_parser_block.d.ts.map +1 -0
  47. package/dist/mdz_stream_parser_block.js +437 -0
  48. package/dist/mdz_stream_parser_inline.d.ts +98 -0
  49. package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
  50. package/dist/mdz_stream_parser_inline.js +481 -0
  51. package/dist/mdz_stream_parser_link.d.ts +29 -0
  52. package/dist/mdz_stream_parser_link.d.ts.map +1 -0
  53. package/dist/mdz_stream_parser_link.js +227 -0
  54. package/dist/mdz_stream_parser_list.d.ts +106 -0
  55. package/dist/mdz_stream_parser_list.d.ts.map +1 -0
  56. package/dist/mdz_stream_parser_list.js +438 -0
  57. package/dist/mdz_stream_parser_state.d.ts +476 -0
  58. package/dist/mdz_stream_parser_state.d.ts.map +1 -0
  59. package/dist/mdz_stream_parser_state.js +651 -0
  60. package/dist/mdz_stream_parser_text.d.ts +25 -0
  61. package/dist/mdz_stream_parser_text.d.ts.map +1 -0
  62. package/dist/mdz_stream_parser_text.js +67 -0
  63. package/dist/mdz_stream_parser_url.d.ts +76 -0
  64. package/dist/mdz_stream_parser_url.d.ts.map +1 -0
  65. package/dist/mdz_stream_parser_url.js +328 -0
  66. package/dist/mdz_stream_state.svelte.d.ts +53 -0
  67. package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
  68. package/dist/mdz_stream_state.svelte.js +360 -0
  69. package/dist/mdz_to_svelte.d.ts +71 -0
  70. package/dist/mdz_to_svelte.d.ts.map +1 -0
  71. package/dist/mdz_to_svelte.js +127 -0
  72. package/dist/mdz_token_parser.d.ts +20 -0
  73. package/dist/mdz_token_parser.d.ts.map +1 -0
  74. package/dist/mdz_token_parser.js +449 -0
  75. package/dist/svelte_preprocess_mdz.d.ts +80 -0
  76. package/dist/svelte_preprocess_mdz.d.ts.map +1 -0
  77. package/dist/svelte_preprocess_mdz.js +543 -0
  78. package/dist/tsdoc_mdz.d.ts +45 -0
  79. package/dist/tsdoc_mdz.d.ts.map +1 -0
  80. package/dist/tsdoc_mdz.js +88 -0
  81. package/package.json +131 -0
  82. package/src/lib/context_helpers.ts +52 -0
  83. package/src/lib/mdz.ts +190 -0
  84. package/src/lib/mdz_contexts.ts +99 -0
  85. package/src/lib/mdz_helpers.ts +727 -0
  86. package/src/lib/mdz_lexer.ts +1761 -0
  87. package/src/lib/mdz_opcodes.ts +227 -0
  88. package/src/lib/mdz_opcodes_to_nodes.ts +470 -0
  89. package/src/lib/mdz_stream_parser.ts +766 -0
  90. package/src/lib/mdz_stream_parser_block.ts +490 -0
  91. package/src/lib/mdz_stream_parser_inline.ts +527 -0
  92. package/src/lib/mdz_stream_parser_link.ts +283 -0
  93. package/src/lib/mdz_stream_parser_list.ts +525 -0
  94. package/src/lib/mdz_stream_parser_state.ts +957 -0
  95. package/src/lib/mdz_stream_parser_text.ts +95 -0
  96. package/src/lib/mdz_stream_parser_url.ts +386 -0
  97. package/src/lib/mdz_stream_state.svelte.ts +383 -0
  98. package/src/lib/mdz_to_svelte.ts +213 -0
  99. package/src/lib/mdz_token_parser.ts +544 -0
  100. package/src/lib/svelte_preprocess_mdz.ts +777 -0
  101. package/src/lib/tsdoc_mdz.ts +97 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * List handlers for the streaming mdz parser — column-0 list starts and the
3
+ * line classifier that drives structure inside open lists.
4
+ *
5
+ * The streaming analogue of the sync lexer's `#tokenize_list`: the level
6
+ * stack lives in `state.list_levels`, and List/ListItem frames sit on the
7
+ * main stack (never reverted, like Paragraph). Every line inside an open
8
+ * list classifies from bounded input — leading whitespace plus a few marker
9
+ * chars for most lines, the whole line for empty markers and fence openers —
10
+ * and blank-run containment consumes one blank line at a time, holding only
11
+ * the structural close-vs-contain decision, never renderable content.
12
+ *
13
+ * @module
14
+ */
15
+ import { BACKTICK, HYPHEN, MAX_LIST_NUMBER_DIGITS, MIN_CODEBLOCK_BACKTICKS, NEWLINE, NINE, PERIOD, RIGHT_ANGLE, SPACE, ZERO, is_line_whitespace, mdz_match_blockquote_prefix, mdz_blockquote_line_has_content, } from './mdz_helpers.js';
16
+ import { accumulate_text, alloc_id, close_all_list_levels, close_list_item, close_list_level, close_list_run, close_paragraph, emit, extend_accumulated_end, flush_text, offset, push_stack_entry, } from './mdz_stream_parser_state.js';
17
+ /**
18
+ * Whether only line whitespace remains between `from` and the line's end —
19
+ * `'pending'` when the buffer ends inside the run while streaming (the line
20
+ * hasn't resolved); in forced mode EOF is a line end.
21
+ */
22
+ const rest_is_blank = (state, from, forced) => {
23
+ let i = from;
24
+ while (i < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(i))) {
25
+ i++;
26
+ }
27
+ if (i >= state.buffer.length)
28
+ return forced ? true : 'pending';
29
+ return state.buffer.charCodeAt(i) === NEWLINE;
30
+ };
31
+ /**
32
+ * Match a list marker at `i` — the streaming analogue of the sync lexer's
33
+ * `#match_list_marker`, with `'pending'` for a marker that can't resolve
34
+ * until more input arrives (digits cut off at buffer end, a possible empty
35
+ * marker whose line hasn't ended). Returns null for anything that's
36
+ * definitely not a marker (`-x`, `--`, a tab after the marker, 10+ digits).
37
+ *
38
+ * @nodocs
39
+ */
40
+ export const match_list_marker = (state, i, forced) => {
41
+ const { buffer } = state;
42
+ const c = buffer.charCodeAt(i);
43
+ if (c === HYPHEN) {
44
+ const after = i + 1;
45
+ const blank = rest_is_blank(state, after, forced);
46
+ if (blank === 'pending')
47
+ return 'pending';
48
+ if (blank) {
49
+ return {
50
+ ordered: false,
51
+ number: 0,
52
+ start: i,
53
+ marker_end: after,
54
+ content_start: after,
55
+ empty: true,
56
+ };
57
+ }
58
+ // in bounds: `blank === false` means the scan found a char at or past `after`
59
+ if (buffer.charCodeAt(after) !== SPACE)
60
+ return null;
61
+ const content_start = after + 1;
62
+ const empty = rest_is_blank(state, content_start, forced);
63
+ if (empty === 'pending')
64
+ return 'pending';
65
+ return { ordered: false, number: 0, start: i, marker_end: after, content_start, empty };
66
+ }
67
+ if (c >= ZERO && c <= NINE) {
68
+ let j = i + 1;
69
+ while (j < buffer.length && buffer.charCodeAt(j) >= ZERO && buffer.charCodeAt(j) <= NINE) {
70
+ j++;
71
+ }
72
+ if (j - i > MAX_LIST_NUMBER_DIGITS)
73
+ return null;
74
+ if (j >= buffer.length)
75
+ return forced ? null : 'pending'; // digits could grow, `.` could follow
76
+ if (buffer.charCodeAt(j) !== PERIOD)
77
+ return null;
78
+ const number = Number(buffer.slice(i, j));
79
+ const after = j + 1;
80
+ const blank = rest_is_blank(state, after, forced);
81
+ if (blank === 'pending')
82
+ return 'pending';
83
+ if (blank) {
84
+ return {
85
+ ordered: true,
86
+ number,
87
+ start: i,
88
+ marker_end: after,
89
+ content_start: after,
90
+ empty: true,
91
+ };
92
+ }
93
+ // in bounds: `blank === false` means the scan found a char at or past `after`
94
+ if (buffer.charCodeAt(after) !== SPACE)
95
+ return null;
96
+ const content_start = after + 1;
97
+ const empty = rest_is_blank(state, content_start, forced);
98
+ if (empty === 'pending')
99
+ return 'pending';
100
+ return { ordered: true, number, start: i, marker_end: after, content_start, empty };
101
+ }
102
+ return null;
103
+ };
104
+ /**
105
+ * Match a complete fence opener line at `i`: >=3 backticks, an optional
106
+ * language word, only whitespace to a terminating newline. `'pending'` while
107
+ * the line could still become one; null when it definitely isn't — including
108
+ * an opener cut off at EOF without its newline (forced), which stays text,
109
+ * matching the sync parser's opener rule.
110
+ *
111
+ * @nodocs
112
+ */
113
+ export const match_fence_opener = (state, i, forced) => {
114
+ const { buffer } = state;
115
+ let j = i;
116
+ let count = 0;
117
+ while (j < buffer.length && buffer.charCodeAt(j) === BACKTICK) {
118
+ count++;
119
+ j++;
120
+ }
121
+ if (j >= buffer.length && !forced)
122
+ return 'pending'; // run could still grow
123
+ if (count < MIN_CODEBLOCK_BACKTICKS)
124
+ return null;
125
+ const lang_start = j;
126
+ while (j < buffer.length) {
127
+ const c = buffer.charCodeAt(j);
128
+ if (c === NEWLINE || is_line_whitespace(c))
129
+ break;
130
+ j++;
131
+ }
132
+ const lang = j > lang_start ? buffer.slice(lang_start, j) : null;
133
+ while (j < buffer.length && is_line_whitespace(buffer.charCodeAt(j))) {
134
+ j++;
135
+ }
136
+ if (j >= buffer.length)
137
+ return forced ? null : 'pending'; // the opener needs its newline
138
+ if (buffer.charCodeAt(j) !== NEWLINE)
139
+ return null;
140
+ return { backtick_count: count, lang, content_start: j + 1 };
141
+ };
142
+ /** Emit a `ListItem` open for `marker` and push its frame. */
143
+ const open_list_item = (state, marker) => {
144
+ const id = alloc_id(state);
145
+ const start = offset(state, marker.start);
146
+ if (marker.ordered) {
147
+ emit(state, { type: 'open', id, node_type: 'ListItem', start, number: marker.number });
148
+ }
149
+ else {
150
+ emit(state, { type: 'open', id, node_type: 'ListItem', start });
151
+ }
152
+ push_stack_entry(state, id, 'ListItem', start);
153
+ if (marker.empty) {
154
+ // a childless close uses this as the item's end (sync's `open.end`)
155
+ state.last_empty_item_end = offset(state, marker.marker_end);
156
+ }
157
+ state.active_text_id = null;
158
+ };
159
+ /** Open a new list level — its `List` frame and first item — at `indent`. */
160
+ const open_list_level = (state, indent, marker) => {
161
+ const id = alloc_id(state);
162
+ const start = offset(state, marker.start);
163
+ if (marker.ordered) {
164
+ emit(state, {
165
+ type: 'open',
166
+ id,
167
+ node_type: 'List',
168
+ start,
169
+ ordered: true,
170
+ start_number: marker.number,
171
+ });
172
+ }
173
+ else {
174
+ emit(state, { type: 'open', id, node_type: 'List', start, ordered: false });
175
+ }
176
+ push_stack_entry(state, id, 'List', start);
177
+ open_list_item(state, marker);
178
+ state.list_levels.push({ indent, ordered: marker.ordered });
179
+ };
180
+ /**
181
+ * Begin a non-empty item's inline run at the marker line's content. Extra
182
+ * whitespace after the marker space is content-leading slop, trimmed like
183
+ * the sync lexer's `#list_run` — the marker guaranteed a non-whitespace char
184
+ * before the line end, so the scan can't hit the buffer end.
185
+ */
186
+ const begin_item_run = (state, marker, line_start) => {
187
+ let i = marker.content_start;
188
+ while (i < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(i))) {
189
+ i++;
190
+ }
191
+ state.in_list_item_run = true;
192
+ state.pos = i;
193
+ state.column = i - line_start;
194
+ state.prev_char = SPACE;
195
+ };
196
+ /**
197
+ * The level index of the deepest open item a line at `indent` can attach to —
198
+ * the deepest level with marker indent strictly less than `indent` — or -1.
199
+ * Levels above it get popped by the caller.
200
+ */
201
+ const list_continuation_target = (state, indent) => {
202
+ for (let i = state.list_levels.length - 1; i >= 0; i--) {
203
+ if (state.list_levels[i].indent < indent)
204
+ return i;
205
+ }
206
+ return -1;
207
+ };
208
+ /**
209
+ * Continue the current run across a soft break: the newline joins the run's
210
+ * text, the continuation line's structural indent strips (its span folds
211
+ * into the preceding text's source end, matching the sync lexer's
212
+ * `#list_run_strip`), and inline processing resumes at the line's content.
213
+ */
214
+ const continue_run = (state, nl_pos, content_pos) => {
215
+ accumulate_text(state, '\n', offset(state, nl_pos));
216
+ extend_accumulated_end(state, offset(state, content_pos));
217
+ state.pos = content_pos;
218
+ state.column = content_pos - (nl_pos + 1);
219
+ state.prev_char = NEWLINE;
220
+ return true;
221
+ };
222
+ /**
223
+ * Try to start a list at a column-0 marker line — `try_hr` has already
224
+ * rejected `---` when the char is `-`. Mirrors the sync lexer's
225
+ * `#tokenize_list` entry conditions: empty markers never start a list, the
226
+ * marker line must show content before the open confirms (a line-bounded
227
+ * hold, the heading has-content pattern), and a non-`1.` ordered marker
228
+ * never interrupts a current paragraph (the hard-wrap `2024.` guard).
229
+ *
230
+ * @nodocs
231
+ */
232
+ export const try_list_start = (state, forced) => {
233
+ const line_start = state.pos;
234
+ const marker = match_list_marker(state, line_start, forced);
235
+ if (marker === 'pending')
236
+ return 'need_more';
237
+ if (marker === null || marker.empty)
238
+ return 'not_match';
239
+ if (marker.ordered && marker.number !== 1 && state.in_paragraph)
240
+ return 'not_match';
241
+ flush_text(state);
242
+ close_paragraph(state);
243
+ open_list_level(state, 0, marker);
244
+ begin_item_run(state, marker, line_start);
245
+ return 'consumed';
246
+ };
247
+ /**
248
+ * Classify the line after a newline inside an open list and apply its
249
+ * structural effect — the streaming analogue of the sync lexer's
250
+ * `#tokenize_list` line loop plus `#list_run_end`'s continuation rules.
251
+ * Called with `pos` at the newline; returns `false` to hold until the line
252
+ * classifies (never in forced mode). Dispatch, in order:
253
+ *
254
+ * - blank line: the current run closes; the close-vs-contain decision waits
255
+ * on the next non-blank line (one blank consumes per call, so long blank
256
+ * runs make incremental progress across chunks)
257
+ * - marker lines: nest when deeper (subject to the ordered interrupt guard
258
+ * against a current run), else snap to the nearest surviving level as a
259
+ * sibling or a marker-type switch; empty markers at an exactly-matching
260
+ * level form empty items
261
+ * - deeper fence opener lines: a code block child of the item they'd
262
+ * otherwise continue (`process_codeblock_in_item` streams it)
263
+ * - deeper quote opener lines (a prefix with content after it): the levels
264
+ * pop to the attach target and `'blockquote'` returns with `pos` at the prefix —
265
+ * the main loop opens the blockquote child (the quote machinery lives
266
+ * there, beside the nested-parser driving)
267
+ * - deeper non-marker lines: continuation of a current run (soft break,
268
+ * indent stripped), else a new `Paragraph` block child
269
+ * - anything else: the whole list closes and the line re-dispatches at the
270
+ * top level
271
+ *
272
+ * @nodocs
273
+ */
274
+ export const process_list_line = (state, forced) => {
275
+ const { buffer } = state;
276
+ const nl_pos = state.pos;
277
+ const line_start = nl_pos + 1;
278
+ let j = line_start;
279
+ while (j < buffer.length && is_line_whitespace(buffer.charCodeAt(j))) {
280
+ j++;
281
+ }
282
+ if (j >= buffer.length && !forced)
283
+ return false; // unresolved line — hold
284
+ if (j >= buffer.length || buffer.charCodeAt(j) === NEWLINE) {
285
+ // blank line (or blank to EOF): the current run is over either way
286
+ close_list_run(state);
287
+ if (j >= buffer.length) {
288
+ // forced: blank to EOF — the list closes
289
+ close_all_list_levels(state);
290
+ }
291
+ state.pos = j; // at the next newline (re-enter here) or EOF
292
+ state.column = 0;
293
+ state.prev_char = NEWLINE;
294
+ return true;
295
+ }
296
+ const indent = j - line_start;
297
+ const run_current = state.in_list_item_run || state.in_paragraph;
298
+ const deepest = state.list_levels[state.list_levels.length - 1];
299
+ const marker = match_list_marker(state, j, forced);
300
+ if (marker === 'pending')
301
+ return false;
302
+ if (marker !== null && !marker.empty) {
303
+ if (indent > deepest.indent) {
304
+ // deeper marker nests — except a non-`1.` ordered marker against a
305
+ // current run, which is hard-wrapped continuation text (the `2024.`
306
+ // guard, applied uniformly to item runs)
307
+ if (run_current && marker.ordered && marker.number !== 1) {
308
+ return continue_run(state, nl_pos, j);
309
+ }
310
+ close_list_run(state);
311
+ open_list_level(state, indent, marker);
312
+ }
313
+ else {
314
+ // dedent snaps to the nearest surviving level
315
+ close_list_run(state);
316
+ while (state.list_levels.length > 1 &&
317
+ state.list_levels[state.list_levels.length - 1].indent > indent) {
318
+ close_list_level(state);
319
+ }
320
+ if (state.list_levels[state.list_levels.length - 1].ordered === marker.ordered) {
321
+ // sibling item
322
+ close_list_item(state);
323
+ open_list_item(state, marker);
324
+ }
325
+ else {
326
+ // marker-type switch: close the list, open a new one at this indent
327
+ close_list_level(state);
328
+ open_list_level(state, indent, marker);
329
+ }
330
+ }
331
+ begin_item_run(state, marker, line_start);
332
+ return true;
333
+ }
334
+ if (marker !== null && marker.empty) {
335
+ // empty item: only at an indent exactly matching an open level of the
336
+ // same marker type — anything else degrades below
337
+ const level_idx = state.list_levels.findIndex((l) => l.indent === indent && l.ordered === marker.ordered);
338
+ if (level_idx !== -1) {
339
+ close_list_run(state);
340
+ while (state.list_levels.length - 1 > level_idx) {
341
+ close_list_level(state);
342
+ }
343
+ close_list_item(state);
344
+ open_list_item(state, marker);
345
+ // consume the marker and trailing whitespace, leaving pos at the
346
+ // line's newline (or EOF)
347
+ let k = marker.marker_end;
348
+ while (k < buffer.length && is_line_whitespace(buffer.charCodeAt(k))) {
349
+ k++;
350
+ }
351
+ state.pos = k;
352
+ state.column = k - line_start;
353
+ state.prev_char = NEWLINE;
354
+ return true;
355
+ }
356
+ }
357
+ // a deeper fence opener line becomes a code block child of the item it
358
+ // would otherwise continue
359
+ if (indent > 0 && buffer.charCodeAt(j) === BACKTICK) {
360
+ const target_idx = list_continuation_target(state, indent);
361
+ if (target_idx !== -1) {
362
+ const fence = match_fence_opener(state, j, forced);
363
+ if (fence === 'pending')
364
+ return false;
365
+ if (fence !== null) {
366
+ close_list_run(state);
367
+ while (state.list_levels.length - 1 > target_idx) {
368
+ close_list_level(state);
369
+ }
370
+ const id = alloc_id(state);
371
+ const cb_start = offset(state, j);
372
+ emit(state, { type: 'open', id, node_type: 'Codeblock', start: cb_start, lang: fence.lang });
373
+ state.codeblock = {
374
+ id,
375
+ backtick_count: fence.backtick_count,
376
+ text_id: null,
377
+ fence_indent: indent,
378
+ start: cb_start,
379
+ };
380
+ state.active_text_id = null;
381
+ state.pos = fence.content_start;
382
+ state.column = 0;
383
+ state.prev_char = NEWLINE;
384
+ return true;
385
+ }
386
+ }
387
+ }
388
+ // a deeper quote opener line (prefix with content) becomes a blockquote
389
+ // child of the item it would otherwise continue — the fence precedent
390
+ if (indent > 0 && buffer.charCodeAt(j) === RIGHT_ANGLE) {
391
+ const target_idx = list_continuation_target(state, indent);
392
+ if (target_idx !== -1) {
393
+ const prefix = mdz_match_blockquote_prefix(buffer, j, forced);
394
+ if (prefix === 'pending')
395
+ return false;
396
+ if (prefix !== null) {
397
+ const has_content = mdz_blockquote_line_has_content(buffer, prefix.content_start, forced);
398
+ if (has_content === 'pending')
399
+ return false;
400
+ if (has_content) {
401
+ close_list_run(state);
402
+ while (state.list_levels.length - 1 > target_idx) {
403
+ close_list_level(state);
404
+ }
405
+ state.pos = j;
406
+ state.column = indent;
407
+ state.prev_char = NEWLINE;
408
+ return 'blockquote';
409
+ }
410
+ }
411
+ }
412
+ }
413
+ if (run_current && indent > deepest.indent) {
414
+ // continuation line: soft break, structural indent stripped
415
+ return continue_run(state, nl_pos, j);
416
+ }
417
+ // the run (if any) is over: a deeper line opens a new Paragraph block
418
+ // child of the item it attaches to; anything else ends the list
419
+ close_list_run(state);
420
+ const target_idx = indent > 0 ? list_continuation_target(state, indent) : -1;
421
+ if (target_idx !== -1) {
422
+ while (state.list_levels.length - 1 > target_idx) {
423
+ close_list_level(state);
424
+ }
425
+ // the Paragraph opens lazily at the first emit (`ensure_paragraph`)
426
+ state.pos = j;
427
+ state.column = indent;
428
+ state.prev_char = NEWLINE;
429
+ return true;
430
+ }
431
+ // nothing list-shaped — the list ends; the main loop re-dispatches this
432
+ // line (paragraph, heading, HR, fence, or a fresh list)
433
+ close_all_list_levels(state);
434
+ state.pos = line_start;
435
+ state.column = 0;
436
+ state.prev_char = NEWLINE;
437
+ return true;
438
+ };