@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,544 @@
1
+ /**
2
+ * mdz token parser — consumes a `MdzToken[]` stream to build the `MdzNode[]` AST.
3
+ *
4
+ * Phase 2 of the synchronous parsing pipeline behind `mdz_parse`.
5
+ * Phase 1 (lexer) is in `mdz_lexer.ts`.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import type {
11
+ MdzNode,
12
+ MdzNodeText,
13
+ MdzNodeBold,
14
+ MdzNodeItalic,
15
+ MdzNodeStrikethrough,
16
+ MdzNodeLink,
17
+ MdzNodeHeading,
18
+ MdzNodeList,
19
+ MdzNodeListItem,
20
+ MdzNodeBlockquote,
21
+ MdzNodeElement,
22
+ MdzNodeComponent,
23
+ } from './mdz.js';
24
+ import {
25
+ has_non_whitespace,
26
+ mdz_extract_single_tag,
27
+ mdz_heading_id,
28
+ mdz_merge_adjacent_text,
29
+ mdz_push_merging_text,
30
+ } from './mdz_helpers.js';
31
+ import {
32
+ type MdzToken,
33
+ type MdzTokenHeadingStart,
34
+ type MdzTokenLinkTextOpen,
35
+ type MdzTokenLinkRef,
36
+ type MdzTokenAutolink,
37
+ type MdzTokenTagOpen,
38
+ type MdzTokenTagSelfClose,
39
+ type MdzTokenListOpen,
40
+ type MdzTokenListItemOpen,
41
+ } from './mdz_lexer.js';
42
+
43
+ /**
44
+ * Builds an `MdzNode[]` tree from a lexed `MdzToken[]` stream.
45
+ * Used by `mdz_parse`, which should be preferred for simple usage.
46
+ */
47
+ export class MdzTokenParser {
48
+ #tokens: Array<MdzToken>;
49
+ #index: number = 0;
50
+
51
+ constructor(tokens: Array<MdzToken>) {
52
+ this.#tokens = tokens;
53
+ }
54
+
55
+ parse(): Array<MdzNode> {
56
+ return this.#parse_block_stream(false);
57
+ }
58
+
59
+ /**
60
+ * Parse a run of block-level tokens into nodes — the document root, or a
61
+ * blockquote's contents (a quote's content is a mini-document, so both
62
+ * share one loop). When `in_blockquote`, a `blockquote_close` token ends
63
+ * the run; at the root the token stream's end does.
64
+ */
65
+ #parse_block_stream(in_blockquote: boolean): Array<MdzNode> {
66
+ const nodes: Array<MdzNode> = [];
67
+ const paragraph_children: Array<MdzNode> = [];
68
+
69
+ while (this.#index < this.#tokens.length) {
70
+ const token = this.#tokens[this.#index]!;
71
+
72
+ if (in_blockquote && token.type === 'blockquote_close') {
73
+ this.#index++;
74
+ break;
75
+ }
76
+
77
+ // Block-level tokens
78
+ if (token.type === 'heading_start') {
79
+ const flushed = this.#flush_paragraph(paragraph_children, true);
80
+ if (flushed) nodes.push(flushed);
81
+ nodes.push(this.#parse_heading());
82
+ continue;
83
+ }
84
+
85
+ if (token.type === 'hr') {
86
+ const flushed = this.#flush_paragraph(paragraph_children, true);
87
+ if (flushed) nodes.push(flushed);
88
+ nodes.push({type: 'Hr', start: token.start, end: token.end});
89
+ this.#index++;
90
+ continue;
91
+ }
92
+
93
+ if (token.type === 'codeblock') {
94
+ const flushed = this.#flush_paragraph(paragraph_children, true);
95
+ if (flushed) nodes.push(flushed);
96
+ nodes.push({
97
+ type: 'Codeblock',
98
+ lang: token.lang,
99
+ content: token.content,
100
+ start: token.start,
101
+ end: token.end,
102
+ });
103
+ this.#index++;
104
+ continue;
105
+ }
106
+
107
+ if (token.type === 'list_open') {
108
+ const flushed = this.#flush_paragraph(paragraph_children, true);
109
+ if (flushed) nodes.push(flushed);
110
+ nodes.push(this.#parse_list());
111
+ continue;
112
+ }
113
+
114
+ if (token.type === 'blockquote_open') {
115
+ const flushed = this.#flush_paragraph(paragraph_children, true);
116
+ if (flushed) nodes.push(flushed);
117
+ nodes.push(this.#parse_blockquote());
118
+ continue;
119
+ }
120
+
121
+ if (token.type === 'paragraph_break') {
122
+ const flushed = this.#flush_paragraph(paragraph_children, true);
123
+ if (flushed) nodes.push(flushed);
124
+ this.#index++;
125
+ continue;
126
+ }
127
+
128
+ // Inline tokens → paragraph children
129
+ const node = this.#parse_inline();
130
+ if (node) paragraph_children.push(node);
131
+ }
132
+
133
+ // Flush remaining content
134
+ const final_paragraph = this.#flush_paragraph(paragraph_children, true);
135
+ if (final_paragraph) nodes.push(final_paragraph);
136
+
137
+ return nodes;
138
+ }
139
+
140
+ #parse_blockquote(): MdzNodeBlockquote {
141
+ const open = this.#tokens[this.#index]!;
142
+ this.#index++;
143
+ const children = this.#parse_block_stream(true);
144
+ // children-derived end, like List — no-empty-start means children exist,
145
+ // with the open's position as a defensive fallback
146
+ const end = children.length > 0 ? children[children.length - 1]!.end : open.end;
147
+ return {type: 'Blockquote', children, start: open.start, end};
148
+ }
149
+
150
+ #parse_heading(): MdzNodeHeading {
151
+ const token = this.#tokens[this.#index]! as MdzTokenHeadingStart;
152
+ const start = token.start;
153
+ const level = token.level;
154
+ this.#index++;
155
+
156
+ const children: Array<MdzNode> = [];
157
+
158
+ // Collect inline nodes until heading_end marker
159
+ while (this.#index < this.#tokens.length) {
160
+ const t = this.#tokens[this.#index]!;
161
+ if (t.type === 'heading_end') {
162
+ this.#index++; // consume the marker
163
+ break;
164
+ }
165
+ const node = this.#parse_inline();
166
+ if (node) children.push(node);
167
+ }
168
+
169
+ const merged = mdz_merge_adjacent_text(children);
170
+ const end = merged.length > 0 ? merged[merged.length - 1]!.end : start + level + 1;
171
+
172
+ return {type: 'Heading', level, id: mdz_heading_id(merged), children: merged, start, end};
173
+ }
174
+
175
+ #parse_list(): MdzNodeList {
176
+ const open = this.#tokens[this.#index]! as MdzTokenListOpen;
177
+ this.#index++;
178
+
179
+ const children: Array<MdzNodeListItem> = [];
180
+
181
+ while (this.#index < this.#tokens.length) {
182
+ const t = this.#tokens[this.#index]!;
183
+ if (t.type === 'list_item_open') {
184
+ children.push(this.#parse_list_item());
185
+ continue;
186
+ }
187
+ if (t.type === 'list_close') {
188
+ this.#index++;
189
+ break;
190
+ }
191
+ this.#index++; // skip anything unexpected (defensive)
192
+ }
193
+
194
+ const end = children.length > 0 ? children[children.length - 1]!.end : open.end;
195
+ if (open.ordered) {
196
+ return {
197
+ type: 'List',
198
+ ordered: true,
199
+ start_number: children[0]?.number ?? 1,
200
+ children,
201
+ start: open.start,
202
+ end,
203
+ };
204
+ }
205
+ return {type: 'List', ordered: false, children, start: open.start, end};
206
+ }
207
+
208
+ #parse_list_item(): MdzNodeListItem {
209
+ const open = this.#tokens[this.#index]! as MdzTokenListItemOpen;
210
+ this.#index++;
211
+
212
+ // the item's first inline run is a direct child run; after a
213
+ // `paragraph_break`, subsequent runs become `Paragraph` block children
214
+ const children: Array<MdzNode> = [];
215
+ const paragraph: Array<MdzNode> = [];
216
+ let in_paragraph = false;
217
+
218
+ const flush = () => {
219
+ const flushed = this.#flush_paragraph(paragraph, true);
220
+ if (flushed) children.push(flushed);
221
+ };
222
+
223
+ while (this.#index < this.#tokens.length) {
224
+ const t = this.#tokens[this.#index]!;
225
+ if (t.type === 'list_item_close') {
226
+ this.#index++;
227
+ break;
228
+ }
229
+ if (t.type === 'list_open') {
230
+ flush();
231
+ in_paragraph = false;
232
+ children.push(this.#parse_list());
233
+ continue;
234
+ }
235
+ if (t.type === 'blockquote_open') {
236
+ flush();
237
+ in_paragraph = false;
238
+ children.push(this.#parse_blockquote());
239
+ continue;
240
+ }
241
+ if (t.type === 'codeblock') {
242
+ flush();
243
+ in_paragraph = false;
244
+ children.push({
245
+ type: 'Codeblock',
246
+ lang: t.lang,
247
+ content: t.content,
248
+ start: t.start,
249
+ end: t.end,
250
+ });
251
+ this.#index++;
252
+ continue;
253
+ }
254
+ if (t.type === 'paragraph_break') {
255
+ flush();
256
+ in_paragraph = true;
257
+ this.#index++;
258
+ continue;
259
+ }
260
+ const node = this.#parse_inline();
261
+ if (node) {
262
+ if (in_paragraph) {
263
+ paragraph.push(node);
264
+ } else {
265
+ mdz_push_merging_text(children, node);
266
+ }
267
+ }
268
+ }
269
+ flush();
270
+
271
+ const end = children.length > 0 ? children[children.length - 1]!.end : open.end;
272
+ if (open.number !== undefined) {
273
+ return {type: 'ListItem', number: open.number, children, start: open.start, end};
274
+ }
275
+ return {type: 'ListItem', children, start: open.start, end};
276
+ }
277
+
278
+ #parse_inline(): MdzNode | null {
279
+ if (this.#index >= this.#tokens.length) return null;
280
+
281
+ const token = this.#tokens[this.#index]!;
282
+
283
+ switch (token.type) {
284
+ case 'text':
285
+ this.#index++;
286
+ return {type: 'Text', content: token.content, start: token.start, end: token.end};
287
+
288
+ case 'code':
289
+ this.#index++;
290
+ return {type: 'Code', content: token.content, start: token.start, end: token.end};
291
+
292
+ case 'bold_open':
293
+ return this.#parse_delimiter_pair('bold_close', 'Bold', '**');
294
+
295
+ case 'italic_open':
296
+ return this.#parse_delimiter_pair('italic_close', 'Italic', '_');
297
+
298
+ case 'strikethrough_open':
299
+ return this.#parse_delimiter_pair('strikethrough_close', 'Strikethrough', '~~');
300
+
301
+ case 'link_text_open':
302
+ return this.#parse_link();
303
+
304
+ case 'autolink':
305
+ return this.#parse_autolink();
306
+
307
+ case 'tag_open':
308
+ return this.#parse_tag_node();
309
+
310
+ case 'tag_self_close':
311
+ return this.#parse_self_close_tag();
312
+
313
+ // Orphaned close tokens - treat as text
314
+ case 'bold_close':
315
+ this.#index++;
316
+ return {type: 'Text', content: '**', start: token.start, end: token.end};
317
+
318
+ case 'italic_close':
319
+ this.#index++;
320
+ return {type: 'Text', content: '_', start: token.start, end: token.end};
321
+
322
+ case 'strikethrough_close':
323
+ this.#index++;
324
+ return {type: 'Text', content: '~~', start: token.start, end: token.end};
325
+
326
+ case 'link_text_close':
327
+ this.#index++;
328
+ return {type: 'Text', content: ']', start: token.start, end: token.end};
329
+
330
+ case 'link_ref':
331
+ this.#index++;
332
+ return {
333
+ type: 'Text',
334
+ content: `(${token.reference})`,
335
+ start: token.start,
336
+ end: token.end,
337
+ };
338
+
339
+ case 'tag_close':
340
+ this.#index++;
341
+ return {
342
+ type: 'Text',
343
+ content: `</${token.name}>`,
344
+ start: token.start,
345
+ end: token.end,
346
+ };
347
+
348
+ default:
349
+ this.#index++;
350
+ return null;
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Parse a delimiter-paired inline (the cursor is on a
356
+ * `bold_open`/`italic_open`/`strikethrough_open` token) into its container
357
+ * node, consuming through the matching close token. An opener whose close
358
+ * token never arrives (block boundary or end of tokens first) degrades to
359
+ * the literal delimiter text — the parse bodies are identical across the
360
+ * three pairs, only the token/node types and delimiter text differ.
361
+ */
362
+ #parse_delimiter_pair(
363
+ close_type: 'bold_close' | 'italic_close' | 'strikethrough_close',
364
+ node_type: 'Bold' | 'Italic' | 'Strikethrough',
365
+ delimiter: '**' | '_' | '~~',
366
+ ): MdzNodeBold | MdzNodeItalic | MdzNodeStrikethrough | MdzNodeText {
367
+ const start = this.#tokens[this.#index]!.start;
368
+ this.#index++;
369
+
370
+ const children: Array<MdzNode> = [];
371
+
372
+ while (this.#index < this.#tokens.length) {
373
+ const t = this.#tokens[this.#index]!;
374
+ if (t.type === close_type) {
375
+ this.#index++;
376
+ return {type: node_type, children, start, end: t.end};
377
+ }
378
+ if (
379
+ t.type === 'paragraph_break' ||
380
+ t.type === 'heading_start' ||
381
+ t.type === 'hr' ||
382
+ t.type === 'codeblock' ||
383
+ t.type === 'list_open' ||
384
+ t.type === 'list_item_close' ||
385
+ t.type === 'blockquote_open'
386
+ ) {
387
+ break;
388
+ }
389
+ const node = this.#parse_inline();
390
+ if (node) mdz_push_merging_text(children, node);
391
+ }
392
+
393
+ // Unclosed - treat as text
394
+ return {type: 'Text', content: delimiter, start, end: start + delimiter.length};
395
+ }
396
+
397
+ #parse_link(): MdzNodeLink | MdzNodeText {
398
+ const open_token = this.#tokens[this.#index]! as MdzTokenLinkTextOpen;
399
+ const start = open_token.start;
400
+ this.#index++;
401
+
402
+ const children: Array<MdzNode> = [];
403
+
404
+ while (this.#index < this.#tokens.length) {
405
+ const t = this.#tokens[this.#index]!;
406
+ if (t.type === 'link_text_close') {
407
+ this.#index++;
408
+
409
+ // Expect link_ref next
410
+ if (this.#index < this.#tokens.length && this.#tokens[this.#index]!.type === 'link_ref') {
411
+ const ref_token = this.#tokens[this.#index]! as MdzTokenLinkRef;
412
+ this.#index++;
413
+ return {
414
+ type: 'Link',
415
+ reference: ref_token.reference,
416
+ children,
417
+ link_type: ref_token.link_type,
418
+ start,
419
+ end: ref_token.end,
420
+ };
421
+ }
422
+
423
+ // No link_ref - treat as text
424
+ return {type: 'Text', content: '[', start, end: start + 1};
425
+ }
426
+ if (
427
+ t.type === 'paragraph_break' ||
428
+ t.type === 'heading_start' ||
429
+ t.type === 'hr' ||
430
+ t.type === 'codeblock' ||
431
+ t.type === 'list_open' ||
432
+ t.type === 'list_item_close' ||
433
+ t.type === 'blockquote_open'
434
+ ) {
435
+ break;
436
+ }
437
+ const node = this.#parse_inline();
438
+ if (node) mdz_push_merging_text(children, node);
439
+ }
440
+
441
+ return {type: 'Text', content: '[', start, end: start + 1};
442
+ }
443
+
444
+ #parse_autolink(): MdzNodeLink {
445
+ const token = this.#tokens[this.#index]! as MdzTokenAutolink;
446
+ this.#index++;
447
+ return {
448
+ type: 'Link',
449
+ reference: token.reference,
450
+ children: [{type: 'Text', content: token.reference, start: token.start, end: token.end}],
451
+ link_type: token.link_type,
452
+ start: token.start,
453
+ end: token.end,
454
+ };
455
+ }
456
+
457
+ #parse_tag_node(): MdzNodeElement | MdzNodeComponent | MdzNodeText {
458
+ const open_token = this.#tokens[this.#index]! as MdzTokenTagOpen;
459
+ const start = open_token.start;
460
+ const tag_name = open_token.name;
461
+ const node_type: 'Component' | 'Element' = open_token.is_component ? 'Component' : 'Element';
462
+ this.#index++;
463
+
464
+ const children: Array<MdzNode> = [];
465
+
466
+ while (this.#index < this.#tokens.length) {
467
+ const t = this.#tokens[this.#index]!;
468
+ if (t.type === 'tag_close' && t.name === tag_name) {
469
+ this.#index++;
470
+ return {type: node_type, name: tag_name, children, start, end: t.end};
471
+ }
472
+ const node = this.#parse_inline();
473
+ if (node) mdz_push_merging_text(children, node);
474
+ }
475
+
476
+ // Unclosed tag
477
+ return {type: 'Text', content: '<', start, end: start + 1};
478
+ }
479
+
480
+ #parse_self_close_tag(): MdzNodeElement | MdzNodeComponent {
481
+ const token = this.#tokens[this.#index]! as MdzTokenTagSelfClose;
482
+ const node_type: 'Component' | 'Element' = token.is_component ? 'Component' : 'Element';
483
+ this.#index++;
484
+ return {type: node_type, name: token.name, children: [], start: token.start, end: token.end};
485
+ }
486
+
487
+ // -- Paragraph flushing --
488
+
489
+ #flush_paragraph(paragraph_children: Array<MdzNode>, trim_trailing = false): MdzNode | null {
490
+ if (paragraph_children.length === 0) return null;
491
+
492
+ if (trim_trailing) {
493
+ // Trim trailing newlines from last text node. The end adjusts by the
494
+ // trimmed amount only — recomputing from the content length would
495
+ // clobber source-accurate spans where structural stripping (list
496
+ // continuation indent, quote prefixes) made content shorter than its
497
+ // source span.
498
+ const last = paragraph_children[paragraph_children.length - 1]!;
499
+ if (last.type === 'Text') {
500
+ const trimmed = last.content.replace(/\n+$/, '');
501
+ if (trimmed) {
502
+ if (trimmed !== last.content) {
503
+ last.end -= last.content.length - trimmed.length;
504
+ last.content = trimmed;
505
+ }
506
+ } else {
507
+ paragraph_children.pop();
508
+ }
509
+ }
510
+
511
+ // Skip whitespace-only paragraphs — ASCII whitespace only, matching
512
+ // the grammar's blank-line definition and the streaming parser's
513
+ // `has_non_whitespace_content` (Unicode whitespace like NBSP is
514
+ // content; `.trim()` here would silently drop it)
515
+ const has_content = paragraph_children.some(
516
+ (n) => n.type !== 'Text' || has_non_whitespace(n.content),
517
+ );
518
+ if (!has_content) {
519
+ paragraph_children.length = 0;
520
+ return null;
521
+ }
522
+ }
523
+
524
+ // Single tag extraction (MDX convention)
525
+ const single_tag = mdz_extract_single_tag(paragraph_children);
526
+ if (single_tag) {
527
+ paragraph_children.length = 0;
528
+ return single_tag;
529
+ }
530
+
531
+ // Merge adjacent text nodes
532
+ const merged = mdz_merge_adjacent_text(paragraph_children.slice());
533
+ paragraph_children.length = 0;
534
+
535
+ if (merged.length === 0) return null;
536
+
537
+ return {
538
+ type: 'Paragraph',
539
+ children: merged,
540
+ start: merged[0]!.start,
541
+ end: merged[merged.length - 1]!.end,
542
+ };
543
+ }
544
+ }