@tabnas/markdown 0.4.2 → 0.5.1

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.
package/dist/block.js ADDED
@@ -0,0 +1,1411 @@
1
+ "use strict";
2
+ /* Copyright (c) 2021-2026 Richard Rodger, MIT License */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.parseBlocks = parseBlocks;
5
+ // Phase 1 of the CommonMark parse: block structure.
6
+ // Spec 0.31.2, Appendix A "A parsing strategy".
7
+ //
8
+ // The document is a tree of blocks whose right spine — the document, then last
9
+ // child, then last child of that, and so on — is *open*: each open block
10
+ // imposes a continuation condition on the next line. For every line we
11
+ //
12
+ // 1. walk that spine, consuming each block's continuation marker (`>` for a
13
+ // block quote, the content indent for a list item, ...) and remember the
14
+ // last container that matched;
15
+ // 2. look for new block starts at the offset that walk reached, closing the
16
+ // unmatched tail of the spine as soon as a start actually matches;
17
+ // 3. add whatever is left of the line to the deepest open block, opening a
18
+ // paragraph if there is nowhere else for the text to go.
19
+ //
20
+ // Unmatched blocks are closed in step 2/3 rather than in step 1 because a
21
+ // paragraph line may be a *lazy continuation*: its containers' markers are
22
+ // missing, but the paragraph carries on regardless (§5.1 rule 2).
23
+ //
24
+ // Positions on a line are tracked twice: `offset`, a UTF-16 index used to
25
+ // slice the text we keep, and `column`, the tab-expanded display column used
26
+ // for every indent decision (§2.2). The two must move together, including the
27
+ // case where a marker consumes only part of a tab — see `advanceOffset` and
28
+ // `addLine`.
29
+ //
30
+ // This file owns everything the block phase sets on a node: type, level,
31
+ // literal (code_block/html_block), info/isFenced, listData (including the
32
+ // final `tight`), sourcepos, and `stringContent` — the raw, unparsed text of
33
+ // paragraphs and headings that the inline phase consumes.
34
+ const node_ts_1 = require("./node.js");
35
+ const common_ts_1 = require("./common.js");
36
+ const inline_ts_1 = require("./inline.js");
37
+ /** §4.4: four columns of indentation open an indented code block. */
38
+ const CODE_INDENT = 4;
39
+ const C_TAB = 9;
40
+ const C_SPACE = 32;
41
+ const C_GREATERTHAN = 62;
42
+ const C_LESSTHAN = 60;
43
+ const C_OPEN_BRACKET = 91;
44
+ const C_BACKSLASH = 92;
45
+ const C_PIPE = 124;
46
+ /** §2.1: \r\n, \n and a lone \r are all line endings. */
47
+ const RE_LINE_ENDING = /\r\n|\n|\r/;
48
+ /** §4.1: 3+ matching `*`, `_` or `-`, each optionally followed by spaces/tabs. */
49
+ const RE_THEMATIC_BREAK = /^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/;
50
+ /** Fast reject: every block start begins with one of these characters. */
51
+ const RE_MAYBE_SPECIAL = /^[#`~*+_=<>0-9-]/;
52
+ /**
53
+ * The same fast reject with `|` and `:` added, used when `gfm` is on: a GFM
54
+ * table's delimiter row may begin with either (`| --- |`, `:-: | ---:`), and
55
+ * neither is a CommonMark block start.
56
+ *
57
+ * Kept as a separate constant rather than folded into the one above so that a
58
+ * `gfm:false` parse takes exactly the path it always did — same characters
59
+ * rejected, same lines never offered to the block starts at all.
60
+ */
61
+ const RE_MAYBE_SPECIAL_GFM = /^[#`~*+_=<>0-9:|-]/;
62
+ const RE_NON_SPACE = /[^ \t\f\v\r\n]/;
63
+ /** §5.2 list markers. */
64
+ const RE_BULLET_LIST_MARKER = /^[*+-]/;
65
+ const RE_ORDERED_LIST_MARKER = /^(\d{1,9})([.)])/;
66
+ /** §4.2: 1–6 `#` followed by a space, a tab, or the end of the line. */
67
+ const RE_ATX_HEADING_MARKER = /^#{1,6}(?:[ \t]+|$)/;
68
+ /** §4.5: an info string after a backtick fence may not contain a backtick. */
69
+ const RE_CODE_FENCE = /^`{3,}(?!.*`)|^~{3,}/;
70
+ const RE_CLOSING_CODE_FENCE = /^(?:`{3,}|~{3,})(?=[ \t]*$)/;
71
+ /** §4.3: a run of `=` or `-` with only spaces/tabs after it. */
72
+ const RE_SETEXT_HEADING_LINE = /^(?:=+|-+)[ \t]*$/;
73
+ // --- HTML blocks (§4.6) -----------------------------------------------------
74
+ //
75
+ // Seven kinds, each with its own start and end condition. Types 1–5 end on a
76
+ // line *containing* their closing string (checked in `incorporateLine` after
77
+ // the line is added); types 6 and 7 end at a blank line (checked in the
78
+ // html_block continuation condition). Type 7 alone may not interrupt a
79
+ // paragraph.
80
+ /** Tag-shape fragments shared by HTML block type 7 (§6.6 raw HTML). */
81
+ const TAGNAME = '[A-Za-z][A-Za-z0-9-]*';
82
+ const ATTRIBUTENAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*';
83
+ const UNQUOTEDVALUE = "[^\"'=<>`\\x00-\\x20]+";
84
+ const SINGLEQUOTEDVALUE = "'[^']*'";
85
+ const DOUBLEQUOTEDVALUE = '"[^"]*"';
86
+ const ATTRIBUTEVALUE = '(?:' + UNQUOTEDVALUE + '|' + SINGLEQUOTEDVALUE + '|' + DOUBLEQUOTEDVALUE + ')';
87
+ const ATTRIBUTEVALUESPEC = '(?:[ \\t]*=[ \\t]*' + ATTRIBUTEVALUE + ')';
88
+ const ATTRIBUTE = '(?:[ \\t]+' + ATTRIBUTENAME + ATTRIBUTEVALUESPEC + '?)';
89
+ const OPENTAG = '<' + TAGNAME + ATTRIBUTE + '*[ \\t]*/?>';
90
+ const CLOSETAG = '</' + TAGNAME + '[ \\t]*>';
91
+ /** The type 6 tag list, verbatim from §4.6 (0.31.2 has `search`, not `source`). */
92
+ const BLOCK_TAGS = [
93
+ 'address',
94
+ 'article',
95
+ 'aside',
96
+ 'base',
97
+ 'basefont',
98
+ 'blockquote',
99
+ 'body',
100
+ 'caption',
101
+ 'center',
102
+ 'col',
103
+ 'colgroup',
104
+ 'dd',
105
+ 'details',
106
+ 'dialog',
107
+ 'dir',
108
+ 'div',
109
+ 'dl',
110
+ 'dt',
111
+ 'fieldset',
112
+ 'figcaption',
113
+ 'figure',
114
+ 'footer',
115
+ 'form',
116
+ 'frame',
117
+ 'frameset',
118
+ 'h[1-6]',
119
+ 'head',
120
+ 'header',
121
+ 'hr',
122
+ 'html',
123
+ 'iframe',
124
+ 'legend',
125
+ 'li',
126
+ 'link',
127
+ 'main',
128
+ 'menu',
129
+ 'menuitem',
130
+ 'nav',
131
+ 'noframes',
132
+ 'ol',
133
+ 'optgroup',
134
+ 'option',
135
+ 'p',
136
+ 'param',
137
+ 'search',
138
+ 'section',
139
+ 'summary',
140
+ 'table',
141
+ 'tbody',
142
+ 'td',
143
+ 'tfoot',
144
+ 'th',
145
+ 'thead',
146
+ 'title',
147
+ 'tr',
148
+ 'track',
149
+ 'ul',
150
+ ].join('|');
151
+ /** Indexed by HTML block type 1–7; slot 0 is unused. */
152
+ const RE_HTML_BLOCK_OPEN = [
153
+ /(?:)/,
154
+ /^<(?:script|pre|textarea|style)(?:[ \t]|>|$)/i,
155
+ /^<!--/,
156
+ /^<[?]/,
157
+ /^<![A-Za-z]/,
158
+ /^<!\[CDATA\[/,
159
+ new RegExp('^</?(?:' + BLOCK_TAGS + ')(?:[ \\t]|/?>|$)', 'i'),
160
+ new RegExp('^(?:' + OPENTAG + '|' + CLOSETAG + ')[ \\t]*$', 'i'),
161
+ ];
162
+ /** End conditions for types 1–5; types 6 and 7 end at a blank line instead. */
163
+ const RE_HTML_BLOCK_CLOSE = [
164
+ /(?:)/,
165
+ /<\/(?:script|pre|textarea|style)>/i,
166
+ /-->/,
167
+ /\?>/,
168
+ />/,
169
+ /\]\]>/,
170
+ ];
171
+ // --- small lexical helpers --------------------------------------------------
172
+ /** Character code at `pos`, or -1 past the end of the line. */
173
+ function peek(ln, pos) {
174
+ return 0 <= pos && pos < ln.length ? ln.charCodeAt(pos) : -1;
175
+ }
176
+ /**
177
+ * Number of characters (code points) in `s`. `s.length` counts UTF-16 units,
178
+ * which double-counts astral characters; sourcepos columns count characters.
179
+ */
180
+ function charCount(s) {
181
+ let n = 0;
182
+ for (let i = 0; i < s.length; i++) {
183
+ const c = s.charCodeAt(i);
184
+ // Skip the low half of a surrogate pair; a lone surrogate still counts once.
185
+ if (0xd800 <= c && c <= 0xdbff && i + 1 < s.length) {
186
+ const d = s.charCodeAt(i + 1);
187
+ if (0xdc00 <= d && d <= 0xdfff)
188
+ i++;
189
+ }
190
+ n++;
191
+ }
192
+ return n;
193
+ }
194
+ function isSpaceOrTab(c) {
195
+ return C_SPACE === c || C_TAB === c;
196
+ }
197
+ function isBlank(s) {
198
+ return !RE_NON_SPACE.test(s);
199
+ }
200
+ // --- GFM tables (extension) -------------------------------------------------
201
+ //
202
+ // Everything the extension needs from a line is here: how a row splits into
203
+ // cells, and whether a line is a delimiter row. Both are pure functions of one
204
+ // line of text, which is what lets the block start below stay a few lines
205
+ // long.
206
+ /** ASCII whitespace, the set `cmark_isspace` trims from a table cell. */
207
+ function isAsciiSpace(c) {
208
+ return 32 === c || (9 <= c && c <= 13);
209
+ }
210
+ function trimAsciiSpace(s) {
211
+ let start = 0;
212
+ let end = s.length;
213
+ while (start < end && isAsciiSpace(s.charCodeAt(start)))
214
+ start++;
215
+ while (end > start && isAsciiSpace(s.charCodeAt(end - 1)))
216
+ end--;
217
+ return 0 === start && end === s.length ? s : s.slice(start, end);
218
+ }
219
+ /**
220
+ * Split one row of a table into its cell texts.
221
+ *
222
+ * Cells are separated by *unescaped* pipes; a leading and a trailing pipe are
223
+ * both optional and may differ from row to row, so they are stripped before
224
+ * the split rather than producing empty cells at the ends. A trailing pipe is
225
+ * only a delimiter if it is itself unescaped, which is why the backslash run
226
+ * before it is counted.
227
+ *
228
+ * `\|` is resolved to a literal `|` *here*, as the extension requires ("include
229
+ * a pipe in a cell's content by escaping it, including inside other inline
230
+ * spans"). Doing it at split time is the whole point: the inline phase never
231
+ * sees the backslash, so a code span in the cell yields a real pipe —
232
+ * `` b `\|` az `` is `b <code>|</code> az`, which no amount of unescaping
233
+ * *after* inline parsing could produce. Every other backslash escape is left
234
+ * exactly as written for the inline phase to handle in the usual way, so no
235
+ * cell text is unescaped twice.
236
+ *
237
+ * Scanning skips the character after any backslash, so `\\` cannot shield the
238
+ * pipe that follows it: `a\\|b` is two cells, the first holding `a\\`.
239
+ *
240
+ * A row of nothing but whitespace has no cells at all, which is how a header
241
+ * candidate that is really empty fails the cell-count test instead of matching
242
+ * a one-column delimiter row.
243
+ */
244
+ function splitTableRow(line) {
245
+ let start = 0;
246
+ let end = line.length;
247
+ while (start < end && isAsciiSpace(line.charCodeAt(start)))
248
+ start++;
249
+ while (end > start && isAsciiSpace(line.charCodeAt(end - 1)))
250
+ end--;
251
+ if (start === end)
252
+ return [];
253
+ // Optional leading pipe.
254
+ if (C_PIPE === line.charCodeAt(start))
255
+ start++;
256
+ // Optional trailing pipe — but an escaped one is content, not a delimiter.
257
+ if (start < end && C_PIPE === line.charCodeAt(end - 1)) {
258
+ let bs = end - 2;
259
+ while (start <= bs && C_BACKSLASH === line.charCodeAt(bs))
260
+ bs--;
261
+ // An even-length backslash run leaves the pipe unescaped.
262
+ if (0 === (end - 2 - bs) % 2)
263
+ end--;
264
+ }
265
+ const cells = [];
266
+ // `cur` holds the parts of the current cell that have already been rewritten;
267
+ // `seg` is the start of the run since then, so the common case (no escapes)
268
+ // costs one slice per cell rather than one per character.
269
+ let cur = '';
270
+ let seg = start;
271
+ let i = start;
272
+ while (i < end) {
273
+ const c = line.charCodeAt(i);
274
+ if (C_BACKSLASH === c && i + 1 < end) {
275
+ if (C_PIPE === line.charCodeAt(i + 1)) {
276
+ cur += line.slice(seg, i) + '|';
277
+ i += 2;
278
+ seg = i;
279
+ }
280
+ else {
281
+ i += 2;
282
+ }
283
+ continue;
284
+ }
285
+ if (C_PIPE === c) {
286
+ cells.push(trimAsciiSpace(cur + line.slice(seg, i)));
287
+ cur = '';
288
+ i++;
289
+ seg = i;
290
+ continue;
291
+ }
292
+ i++;
293
+ }
294
+ cells.push(trimAsciiSpace(cur + line.slice(seg, end)));
295
+ return cells;
296
+ }
297
+ /**
298
+ * A delimiter cell is hyphens with an optional leading and/or trailing colon,
299
+ * and nothing else. One `+`, one space in the middle, one stray character and
300
+ * the line is not a delimiter row.
301
+ */
302
+ const RE_TABLE_DELIMITER_CELL = /^(:?)-+(:?)$/;
303
+ /**
304
+ * Read a line as a table's delimiter row, returning one alignment per column,
305
+ * or null if it is not one.
306
+ */
307
+ function parseDelimiterRow(line) {
308
+ const cells = splitTableRow(line);
309
+ if (0 === cells.length)
310
+ return null;
311
+ const align = [];
312
+ for (const cell of cells) {
313
+ const m = RE_TABLE_DELIMITER_CELL.exec(cell);
314
+ if (null === m)
315
+ return null;
316
+ const left = ':' === m[1];
317
+ const right = ':' === m[2];
318
+ align.push(left && right ? 'center' : left ? 'left' : right ? 'right' : null);
319
+ }
320
+ return align;
321
+ }
322
+ /**
323
+ * Cap on the empty cells inserted to pad short body rows, across a whole
324
+ * document — cmark-gfm's `MAX_AUTOCOMPLETED_CELLS`, and for the same reason.
325
+ *
326
+ * Padding is the one place where a table's node count is not bounded by the
327
+ * size of its source: a 10000-column header followed by 10000 one-cell rows is
328
+ * 60 KB of input asking for 10^8 cells. Every other part of the extension is
329
+ * linear in the input, so this single budget is what keeps the whole of it so.
330
+ * Reaching it needs input that is already pathological; ordinary tables are
331
+ * nowhere near.
332
+ */
333
+ const MAX_AUTOCOMPLETED_CELLS = 0x80000;
334
+ /** §5.3: two markers make the same list only if type, bullet and delimiter agree. */
335
+ function listsMatch(listData, itemData) {
336
+ return (listData.type === itemData.type &&
337
+ listData.delimiter === itemData.delimiter &&
338
+ listData.bulletChar === itemData.bulletChar);
339
+ }
340
+ /**
341
+ * §5.3 looseness input: does this block end with a blank line? For lists and
342
+ * items the question recurses into the last child, since the blank line is
343
+ * recorded on the deepest block that saw it. The `lastLineChecked` flag makes
344
+ * the recursion linear over the whole document rather than quadratic.
345
+ */
346
+ function endsWithBlankLine(block) {
347
+ let cur = block;
348
+ while (cur) {
349
+ if (cur.lastLineBlank)
350
+ return true;
351
+ const t = cur.type;
352
+ if (!cur.lastLineChecked && ('list' === t || 'item' === t)) {
353
+ cur.lastLineChecked = true;
354
+ cur = cur.lastChild;
355
+ }
356
+ else {
357
+ cur.lastLineChecked = true;
358
+ return false;
359
+ }
360
+ }
361
+ return false;
362
+ }
363
+ const notItem = (t) => 'item' !== t;
364
+ const noChildren = () => false;
365
+ const BLOCK_HANDLERS = {
366
+ document: {
367
+ continue: () => 0,
368
+ finalize: () => undefined,
369
+ canContain: notItem,
370
+ acceptsLines: false,
371
+ verbatim: false,
372
+ },
373
+ list: {
374
+ continue: () => 0,
375
+ finalize: (_parser, block) => {
376
+ // §5.3: the list is loose if any item is followed by a blank line, or if
377
+ // any item directly contains two blocks separated by one. A blank line
378
+ // at the very end of the last item does not count — hence the
379
+ // `item.next` / `subitem.next` guards.
380
+ let item = block.firstChild;
381
+ while (item) {
382
+ if (endsWithBlankLine(item) && item.next) {
383
+ if (block.listData)
384
+ block.listData.tight = false;
385
+ break;
386
+ }
387
+ let subitem = item.firstChild;
388
+ while (subitem) {
389
+ if (endsWithBlankLine(subitem) && (item.next || subitem.next)) {
390
+ if (block.listData)
391
+ block.listData.tight = false;
392
+ break;
393
+ }
394
+ subitem = subitem.next;
395
+ }
396
+ item = item.next;
397
+ }
398
+ if (block.lastChild)
399
+ block.sourcepos[1] = block.lastChild.sourcepos[1];
400
+ },
401
+ canContain: (t) => 'item' === t,
402
+ acceptsLines: false,
403
+ verbatim: false,
404
+ },
405
+ block_quote: {
406
+ continue: (parser) => {
407
+ const ln = parser.currentLine;
408
+ if (!parser.indented && C_GREATERTHAN === peek(ln, parser.nextNonspace)) {
409
+ parser.advanceNextNonspace();
410
+ parser.advanceOffset(1, false);
411
+ // §5.1: one optional space of indentation after `>`, tab-aware.
412
+ if (isSpaceOrTab(peek(ln, parser.offset)))
413
+ parser.advanceOffset(1, true);
414
+ return 0;
415
+ }
416
+ return 1;
417
+ },
418
+ finalize: () => undefined,
419
+ canContain: notItem,
420
+ acceptsLines: false,
421
+ verbatim: false,
422
+ },
423
+ item: {
424
+ continue: (parser, container) => {
425
+ const data = container.listData;
426
+ if (null === data)
427
+ return 1;
428
+ if (parser.blank) {
429
+ // An item whose first line is blank cannot be continued by a second
430
+ // blank line (§5.2 rule 3: at most one leading blank line).
431
+ if (null === container.firstChild)
432
+ return 1;
433
+ parser.advanceNextNonspace();
434
+ return 0;
435
+ }
436
+ // §5.2: subsequent lines belong to the item if indented to the content
437
+ // column derived from the marker.
438
+ if (parser.indent >= data.markerOffset + data.padding) {
439
+ parser.advanceOffset(data.markerOffset + data.padding, true);
440
+ return 0;
441
+ }
442
+ return 1;
443
+ },
444
+ finalize: (_parser, block) => {
445
+ if (block.lastChild) {
446
+ block.sourcepos[1] = block.lastChild.sourcepos[1];
447
+ }
448
+ else if (block.listData) {
449
+ block.sourcepos[1] = [
450
+ block.sourcepos[0][0],
451
+ block.listData.markerOffset + block.listData.padding,
452
+ ];
453
+ }
454
+ },
455
+ canContain: notItem,
456
+ acceptsLines: false,
457
+ verbatim: false,
458
+ },
459
+ heading: {
460
+ // A heading is always exactly one line (setext headings are converted from
461
+ // an already-complete paragraph).
462
+ continue: () => 1,
463
+ finalize: () => undefined,
464
+ canContain: noChildren,
465
+ acceptsLines: false,
466
+ verbatim: false,
467
+ },
468
+ thematic_break: {
469
+ continue: () => 1,
470
+ finalize: () => undefined,
471
+ canContain: noChildren,
472
+ acceptsLines: false,
473
+ verbatim: false,
474
+ },
475
+ code_block: {
476
+ continue: (parser, container) => {
477
+ const ln = parser.currentLine;
478
+ const indent = parser.indent;
479
+ if (container.isFenced) {
480
+ // §4.5: the closing fence is the same character, at least as long, at
481
+ // most 3 columns indented, and followed only by spaces/tabs.
482
+ const rest = ln.slice(parser.nextNonspace);
483
+ const match = indent <= 3 && ln.charAt(parser.nextNonspace) === container.fenceChar
484
+ ? rest.match(RE_CLOSING_CODE_FENCE)
485
+ : null;
486
+ if (match && match[0].length >= container.fenceLength) {
487
+ parser.lastLineLength = charCount(ln.slice(0, parser.offset + indent + match[0].length));
488
+ parser.finalize(container, parser.lineNumber);
489
+ return 2;
490
+ }
491
+ // Content is de-indented by up to the opening fence's own indent.
492
+ let i = container.fenceOffset;
493
+ while (0 < i && isSpaceOrTab(peek(ln, parser.offset))) {
494
+ parser.advanceOffset(1, true);
495
+ i--;
496
+ }
497
+ return 0;
498
+ }
499
+ // §4.4 indented code: four columns, or a blank line (which is kept).
500
+ if (indent >= CODE_INDENT) {
501
+ parser.advanceOffset(CODE_INDENT, true);
502
+ return 0;
503
+ }
504
+ if (parser.blank) {
505
+ parser.advanceNextNonspace();
506
+ return 0;
507
+ }
508
+ return 1;
509
+ },
510
+ finalize: (_parser, block) => {
511
+ if (block.isFenced) {
512
+ // The first accumulated line is the info string, not content.
513
+ const content = block.stringContent;
514
+ const nl = content.indexOf('\n');
515
+ const firstLine = -1 === nl ? content : content.slice(0, nl);
516
+ block.info = (0, common_ts_1.unescapeString)(firstLine.trim());
517
+ block.literal = -1 === nl ? '' : content.slice(nl + 1);
518
+ }
519
+ else {
520
+ // §4.4: trailing blank lines are not part of an indented code block.
521
+ block.literal = block.stringContent.replace(/(\n *)+$/, '\n');
522
+ }
523
+ block.stringContent = '';
524
+ },
525
+ canContain: noChildren,
526
+ acceptsLines: true,
527
+ verbatim: true,
528
+ },
529
+ html_block: {
530
+ continue: (parser, container) => {
531
+ // Types 6 and 7 end at a blank line; 1–5 end on their closing string,
532
+ // which `incorporateLine` checks after adding the line.
533
+ const type = parser.htmlBlockType(container);
534
+ return parser.blank && (6 === type || 7 === type) ? 1 : 0;
535
+ },
536
+ finalize: (_parser, block) => {
537
+ block.literal = block.stringContent.replace(/(\n *)+$/, '');
538
+ block.stringContent = '';
539
+ },
540
+ canContain: noChildren,
541
+ acceptsLines: true,
542
+ verbatim: true,
543
+ },
544
+ paragraph: {
545
+ continue: (parser) => (parser.blank ? 1 : 0),
546
+ finalize: (parser, block) => {
547
+ // §4.7: a paragraph may begin with link reference definitions. Consume
548
+ // them from the front; if nothing is left, the paragraph disappears.
549
+ let hasReferenceDefs = false;
550
+ let pos = 0;
551
+ while (C_OPEN_BRACKET === block.stringContent.charCodeAt(0) &&
552
+ 0 !== (pos = (0, inline_ts_1.parseReference)(block.stringContent, parser.refmap))) {
553
+ block.stringContent = block.stringContent.slice(pos);
554
+ hasReferenceDefs = true;
555
+ }
556
+ if (hasReferenceDefs && isBlank(block.stringContent))
557
+ block.unlink();
558
+ },
559
+ canContain: noChildren,
560
+ acceptsLines: true,
561
+ verbatim: false,
562
+ },
563
+ // GFM tables. The header row is built when the table opens (see
564
+ // `tryOpenTable`); every later line is accumulated raw and split into a body
565
+ // row at finalize, so the table behaves like a paragraph that happens to
566
+ // print as a grid — it ends at a blank line, and any block start interrupts
567
+ // it, because `verbatim` is false and `canContain` refuses everything the
568
+ // block starts can open.
569
+ table: {
570
+ continue: (parser) => (parser.blank ? 1 : 0),
571
+ finalize: (parser, block) => finalizeTable(parser, block),
572
+ canContain: (t) => 'table_row' === t,
573
+ acceptsLines: true,
574
+ verbatim: false,
575
+ },
576
+ // Rows and cells are created closed, and are never the parser's tip, so
577
+ // nothing here is reached in a normal parse. They exist so that `handlerFor`
578
+ // — which throws for an unknown type — cannot be the way a malformed tree
579
+ // surfaces.
580
+ table_row: {
581
+ continue: () => 1,
582
+ finalize: () => undefined,
583
+ canContain: (t) => 'table_cell' === t,
584
+ acceptsLines: false,
585
+ verbatim: false,
586
+ },
587
+ table_cell: {
588
+ continue: () => 1,
589
+ finalize: () => undefined,
590
+ canContain: noChildren,
591
+ acceptsLines: false,
592
+ verbatim: false,
593
+ },
594
+ };
595
+ /**
596
+ * Append one row of `cells` to `table`, padded with empty cells or truncated
597
+ * so that it is exactly as wide as the delimiter row said.
598
+ *
599
+ * Rows and cells are built closed: they are not part of the open spine, and
600
+ * the line walk in `incorporateLine` must stop at the table itself.
601
+ */
602
+ function addTableRow(parser, table, cells, isHeader, lineNumber, endColumn) {
603
+ const width = table.tableAlign?.length ?? 0;
604
+ // See MAX_AUTOCOMPLETED_CELLS: padding is the only unbounded part of a table.
605
+ let limit = width;
606
+ if (cells.length < width) {
607
+ const budget = MAX_AUTOCOMPLETED_CELLS - parser.autocompletedCells;
608
+ const padding = width - cells.length;
609
+ if (padding > budget)
610
+ limit = cells.length + (0 < budget ? budget : 0);
611
+ parser.autocompletedCells += limit - cells.length;
612
+ }
613
+ // A row and its cells occupy one source line. Columns within that line are
614
+ // not tracked: nothing reads them (the AST drops sourcepos and the renderer
615
+ // ignores it), and threading offsets through the cell split to invent them
616
+ // would be cost with no reader. Each node needs its own array — sourcepos is
617
+ // mutable — so the span is rebuilt rather than shared.
618
+ const span = () => [
619
+ [lineNumber, 1],
620
+ [lineNumber, endColumn],
621
+ ];
622
+ const row = new node_ts_1.MdNode('table_row', span());
623
+ row.isHeaderRow = isHeader;
624
+ row.open = false;
625
+ table.appendChild(row);
626
+ for (let i = 0; i < limit; i++) {
627
+ const cell = new node_ts_1.MdNode('table_cell', span());
628
+ cell.open = false;
629
+ // Consumed by the inline phase, exactly as a paragraph's content is.
630
+ cell.stringContent = i < cells.length ? cells[i] : '';
631
+ row.appendChild(cell);
632
+ }
633
+ }
634
+ /**
635
+ * Close a table: turn the raw lines it accumulated into body rows.
636
+ *
637
+ * The first accumulated line is the delimiter row that opened the table. It
638
+ * carries no data — its alignments are already on the node — so it is skipped
639
+ * here, which is also what keeps the source line numbering of the rows below
640
+ * it straightforward: the table accepts exactly one line per source line, and
641
+ * a blank line closes it, so there are no gaps.
642
+ */
643
+ function finalizeTable(parser, table) {
644
+ const lines = table.stringContent.split('\n');
645
+ table.stringContent = '';
646
+ // Line 0 is the delimiter row; the last element is the empty string after
647
+ // the final newline.
648
+ const headerLine = table.sourcepos[0][0];
649
+ for (let i = 1; i < lines.length; i++) {
650
+ const line = lines[i];
651
+ if ('' === line)
652
+ continue;
653
+ addTableRow(parser, table, splitTableRow(line), false, headerLine + i + 1, charCount(line));
654
+ }
655
+ }
656
+ function handlerFor(type) {
657
+ const handler = BLOCK_HANDLERS[type];
658
+ if (undefined === handler)
659
+ throw new Error('markdown: no block handler for ' + type);
660
+ return handler;
661
+ }
662
+ // --- list markers (§5.2) ----------------------------------------------------
663
+ /**
664
+ * Parse a list marker at `nextNonspace` and derive the item's content indent.
665
+ *
666
+ * The content indent is marker width + the spaces that follow it, with two
667
+ * exceptions that both fall back to marker width + 1:
668
+ *
669
+ * - rule 2, item starting with indented code: 5+ spaces after the marker
670
+ * means only the first space belongs to the marker, the rest is code;
671
+ * - rule 3, item starting with a blank line: nothing follows the marker at
672
+ * all, so there are no spaces to measure.
673
+ *
674
+ * Returns null when this is not a list item start. Advances the parser to the
675
+ * item's content column when it is.
676
+ */
677
+ function parseListMarker(parser, container) {
678
+ // Four columns of indentation is indented code, never a list marker.
679
+ if (parser.indent >= CODE_INDENT)
680
+ return null;
681
+ const rest = parser.currentLine.slice(parser.nextNonspace);
682
+ const data = {
683
+ type: 'bullet',
684
+ tight: true, // lists are tight until finalize proves otherwise
685
+ start: 1,
686
+ delimiter: '',
687
+ bulletChar: '',
688
+ padding: 0,
689
+ markerOffset: parser.indent,
690
+ };
691
+ let match = rest.match(RE_BULLET_LIST_MARKER);
692
+ if (match) {
693
+ data.type = 'bullet';
694
+ data.bulletChar = match[0][0];
695
+ }
696
+ else {
697
+ match = rest.match(RE_ORDERED_LIST_MARKER);
698
+ // §5.2 exception 1(b): an ordered item interrupting a paragraph must start at 1.
699
+ if (null === match || ('paragraph' === container.type && 1 !== parseInt(match[1], 10))) {
700
+ return null;
701
+ }
702
+ data.type = 'ordered';
703
+ data.start = parseInt(match[1], 10);
704
+ data.delimiter = match[2];
705
+ }
706
+ // At least one space or tab (or the end of the line) must follow the marker.
707
+ let nextc = peek(parser.currentLine, parser.nextNonspace + match[0].length);
708
+ if (!(-1 === nextc || C_TAB === nextc || C_SPACE === nextc))
709
+ return null;
710
+ // §5.2 exception 1(a): an item interrupting a paragraph may not start blank.
711
+ if ('paragraph' === container.type &&
712
+ !RE_NON_SPACE.test(parser.currentLine.slice(parser.nextNonspace + match[0].length))) {
713
+ return null;
714
+ }
715
+ parser.advanceNextNonspace(); // to the marker
716
+ parser.advanceOffset(match[0].length, true); // past the marker
717
+ const spacesStartCol = parser.column;
718
+ const spacesStartOffset = parser.offset;
719
+ do {
720
+ parser.advanceOffset(1, true);
721
+ nextc = peek(parser.currentLine, parser.offset);
722
+ } while (parser.column - spacesStartCol < 5 && isSpaceOrTab(nextc));
723
+ const blankItem = -1 === peek(parser.currentLine, parser.offset);
724
+ const spacesAfterMarker = parser.column - spacesStartCol;
725
+ if (spacesAfterMarker >= 5 || spacesAfterMarker < 1 || blankItem) {
726
+ data.padding = match[0].length + 1;
727
+ parser.column = spacesStartCol;
728
+ parser.offset = spacesStartOffset;
729
+ if (isSpaceOrTab(peek(parser.currentLine, parser.offset)))
730
+ parser.advanceOffset(1, true);
731
+ }
732
+ else {
733
+ data.padding = match[0].length + spacesAfterMarker;
734
+ }
735
+ return data;
736
+ }
737
+ /**
738
+ * Tried in order, and the order is the spec's: a setext underline beats a
739
+ * thematic break (so `Foo\n---` is a heading), a thematic break beats a list
740
+ * item (so `- - -` is a break), and indented code comes last because any of
741
+ * the others may be indented up to three columns.
742
+ */
743
+ const BLOCK_STARTS = [
744
+ // Block quote (§5.1).
745
+ (parser) => {
746
+ if (!parser.indented && C_GREATERTHAN === peek(parser.currentLine, parser.nextNonspace)) {
747
+ parser.advanceNextNonspace();
748
+ parser.advanceOffset(1, false);
749
+ if (isSpaceOrTab(peek(parser.currentLine, parser.offset)))
750
+ parser.advanceOffset(1, true);
751
+ parser.closeUnmatchedBlocks();
752
+ parser.addChild('block_quote', parser.nextNonspace);
753
+ return 1;
754
+ }
755
+ return 0;
756
+ },
757
+ // ATX heading (§4.2).
758
+ (parser) => {
759
+ if (parser.indented)
760
+ return 0;
761
+ const match = parser.currentLine.slice(parser.nextNonspace).match(RE_ATX_HEADING_MARKER);
762
+ if (null === match)
763
+ return 0;
764
+ parser.advanceNextNonspace();
765
+ parser.advanceOffset(match[0].length, false);
766
+ parser.closeUnmatchedBlocks();
767
+ const container = parser.addChild('heading', parser.nextNonspace);
768
+ container.level = match[0].replace(/[ \t]+$/, '').length;
769
+ // An optional closing sequence of `#`s, preceded by spaces or tabs, is
770
+ // dropped; a line of nothing but `#`s has empty content.
771
+ container.stringContent = parser.currentLine
772
+ .slice(parser.offset)
773
+ .replace(/^[ \t]*#+[ \t]*$/, '')
774
+ .replace(/[ \t]+#+[ \t]*$/, '');
775
+ parser.advanceOffset(parser.currentLine.length - parser.offset, false);
776
+ return 2;
777
+ },
778
+ // Fenced code block (§4.5).
779
+ (parser) => {
780
+ if (parser.indented)
781
+ return 0;
782
+ const match = parser.currentLine.slice(parser.nextNonspace).match(RE_CODE_FENCE);
783
+ if (null === match)
784
+ return 0;
785
+ const fenceLength = match[0].length;
786
+ parser.closeUnmatchedBlocks();
787
+ const container = parser.addChild('code_block', parser.nextNonspace);
788
+ container.isFenced = true;
789
+ container.fenceLength = fenceLength;
790
+ container.fenceChar = match[0][0];
791
+ container.fenceOffset = parser.indent;
792
+ parser.advanceNextNonspace();
793
+ parser.advanceOffset(fenceLength, false);
794
+ // The rest of the line is added as the block's first line: the info string.
795
+ return 2;
796
+ },
797
+ // HTML block (§4.6), types 1-7.
798
+ (parser, container) => {
799
+ if (parser.indented || C_LESSTHAN !== peek(parser.currentLine, parser.nextNonspace))
800
+ return 0;
801
+ const s = parser.currentLine.slice(parser.nextNonspace);
802
+ for (let blockType = 1; blockType <= 7; blockType++) {
803
+ if (!RE_HTML_BLOCK_OPEN[blockType].test(s))
804
+ continue;
805
+ // Type 7 may not interrupt a paragraph — including a paragraph we are
806
+ // only about to continue lazily.
807
+ if (7 === blockType &&
808
+ ('paragraph' === container.type ||
809
+ (!parser.allClosed && !parser.blank && 'paragraph' === parser.openTip.type))) {
810
+ continue;
811
+ }
812
+ parser.closeUnmatchedBlocks();
813
+ // The offset is deliberately not advanced: leading spaces are part of
814
+ // the raw HTML.
815
+ const block = parser.addChild('html_block', parser.offset);
816
+ parser.setHtmlBlockType(block, blockType);
817
+ return 2;
818
+ }
819
+ return 0;
820
+ },
821
+ // Setext heading underline (§4.3) — converts the whole open paragraph.
822
+ (parser, container) => {
823
+ if (parser.indented || 'paragraph' !== container.type)
824
+ return 0;
825
+ const match = parser.currentLine.slice(parser.nextNonspace).match(RE_SETEXT_HEADING_LINE);
826
+ if (null === match)
827
+ return 0;
828
+ parser.closeUnmatchedBlocks();
829
+ // §4.7: the paragraph may still have led with reference definitions; only
830
+ // what remains becomes the heading. If nothing remains this is not a
831
+ // setext underline at all (it falls through to thematic break / paragraph).
832
+ let pos = 0;
833
+ while (C_OPEN_BRACKET === container.stringContent.charCodeAt(0) &&
834
+ 0 !== (pos = (0, inline_ts_1.parseReference)(container.stringContent, parser.refmap))) {
835
+ container.stringContent = container.stringContent.slice(pos);
836
+ }
837
+ if (0 === container.stringContent.length)
838
+ return 0;
839
+ const start = container.sourcepos[0];
840
+ const heading = new node_ts_1.MdNode('heading', [[start[0], start[1]], [0, 0]]);
841
+ heading.level = '=' === match[0][0] ? 1 : 2;
842
+ heading.stringContent = container.stringContent;
843
+ container.insertAfter(heading);
844
+ container.unlink();
845
+ parser.tip = heading;
846
+ parser.advanceOffset(parser.currentLine.length - parser.offset, false);
847
+ return 2;
848
+ },
849
+ // Thematic break (§4.1).
850
+ (parser) => {
851
+ if (parser.indented)
852
+ return 0;
853
+ if (!RE_THEMATIC_BREAK.test(parser.currentLine.slice(parser.nextNonspace)))
854
+ return 0;
855
+ parser.closeUnmatchedBlocks();
856
+ parser.addChild('thematic_break', parser.nextNonspace);
857
+ parser.advanceOffset(parser.currentLine.length - parser.offset, false);
858
+ return 2;
859
+ },
860
+ // List item (§5.2). `parseListMarker` rejects a marker indented four or more
861
+ // columns on its own, so there is no `indented` test here.
862
+ (parser, container) => {
863
+ const data = parseListMarker(parser, container);
864
+ if (null === data)
865
+ return 0;
866
+ parser.closeUnmatchedBlocks();
867
+ // §5.3: a change of bullet character or ordered delimiter starts a new list.
868
+ const tip = parser.openTip;
869
+ if ('list' !== tip.type || null === tip.listData || !listsMatch(tip.listData, data)) {
870
+ parser.addChild('list', parser.nextNonspace).listData = data;
871
+ }
872
+ parser.addChild('item', parser.nextNonspace).listData = data;
873
+ return 1;
874
+ },
875
+ // Indented code block (§4.4) — may not interrupt a paragraph.
876
+ (parser) => {
877
+ if (!parser.indented || 'paragraph' === parser.openTip.type || parser.blank)
878
+ return 0;
879
+ parser.advanceOffset(CODE_INDENT, true);
880
+ parser.closeUnmatchedBlocks();
881
+ parser.addChild('code_block', parser.offset);
882
+ return 2;
883
+ },
884
+ // GFM table (extension) — a delimiter row directly under an open paragraph.
885
+ //
886
+ // Last, and the position is load-bearing in both directions. It must come
887
+ // after the setext underline so that `foo` over `---` stays an `<h2>` rather
888
+ // than becoming a one-column table, and after the list marker so that `- | -`
889
+ // stays a list item; cmark-gfm reaches its extensions from the same place,
890
+ // once every built-in start has refused the line.
891
+ (parser, container) => (parser.tryOpenTable(container) ? 2 : 0),
892
+ ];
893
+ // --- the parser -------------------------------------------------------------
894
+ class BlockParser {
895
+ constructor(options) {
896
+ this.refmap = {};
897
+ /** False while some open block failed its continuation condition. */
898
+ this.allClosed = true;
899
+ this.currentLine = '';
900
+ this.lineNumber = 0;
901
+ this.lastLineLength = 0;
902
+ /**
903
+ * The length of the line before the one `lastLineLength` measures.
904
+ *
905
+ * `finalize` dates a block's end from `lastLineLength`, which is right for
906
+ * every block that ends on the line before the current one. A GFM table
907
+ * splits its paragraph *two* lines back — the header row is the previous
908
+ * line and stays with the table, so the paragraph left behind ends on the
909
+ * line before that — and needs this one instead.
910
+ */
911
+ this.prevLineLength = 0;
912
+ // Memoize the last offset in `currentLine` whose character count is known,
913
+ // so sourcepos columns cost amortized O(1). Both reset per line.
914
+ // See `sourceColumn`.
915
+ this.colAnchorOffset = 0;
916
+ this.colAnchorChars = 0;
917
+ /** UTF-16 index into `currentLine`. */
918
+ this.offset = 0;
919
+ /** Tab-expanded display column matching `offset` (§2.2). */
920
+ this.column = 0;
921
+ /** True when `offset` sits inside a tab whose columns were partly consumed. */
922
+ this.partiallyConsumedTab = false;
923
+ this.nextNonspace = 0;
924
+ this.nextNonspaceColumn = 0;
925
+ /** Columns between `column` and the next non-space character. */
926
+ this.indent = 0;
927
+ this.indented = false;
928
+ this.blank = false;
929
+ /** Empty cells inserted so far to pad short table rows. See MAX_AUTOCOMPLETED_CELLS. */
930
+ this.autocompletedCells = 0;
931
+ /**
932
+ * The text `addLine` last appended, the column it started in, and the block
933
+ * it went to.
934
+ *
935
+ * `tryOpenTable` needs the *last line* of an open paragraph, and reading it
936
+ * back out of `stringContent` is not affordable: that content is built by
937
+ * repeated `+=`, so every index into it flattens a fresh rope — O(n) per
938
+ * line, O(n²) over a paragraph whose every other line looks like a delimiter
939
+ * row, which untrusted input can write. Keeping the line itself here costs
940
+ * two field writes and never touches the accumulated string.
941
+ *
942
+ * The node is recorded alongside it because "the previous line went to this
943
+ * very paragraph" is the extension's own rule, not a cache-validity trick:
944
+ * a header row is by definition the line immediately above the delimiter
945
+ * row. If some other block took that line, there is no header row.
946
+ *
947
+ * The column is what a table's sourcepos starts at: a table opens on its
948
+ * header row, which is a line already consumed by the time the delimiter row
949
+ * identifies it, so its column has to have been recorded when it went past.
950
+ */
951
+ this.lastAddedLine = '';
952
+ this.lastAddedColumn = 1;
953
+ this.lastAddedTo = null;
954
+ // MdNode has no field for the HTML block type, and the tree contract is not
955
+ // ours to change, so open HTML blocks carry it here.
956
+ this.htmlBlockTypes = new Map();
957
+ this.options = options;
958
+ this.maybeSpecial = options.gfm ? RE_MAYBE_SPECIAL_GFM : RE_MAYBE_SPECIAL;
959
+ this.doc = new node_ts_1.MdNode('document', [
960
+ [1, 1],
961
+ [0, 0],
962
+ ]);
963
+ this.tip = this.doc;
964
+ this.oldtip = this.doc;
965
+ this.lastMatchedContainer = this.doc;
966
+ }
967
+ /** The deepest open block. Only valid while a line is being processed. */
968
+ get openTip() {
969
+ if (null === this.tip)
970
+ throw new Error('markdown: no open block');
971
+ return this.tip;
972
+ }
973
+ htmlBlockType(block) {
974
+ return this.htmlBlockTypes.get(block) ?? 0;
975
+ }
976
+ setHtmlBlockType(block, type) {
977
+ this.htmlBlockTypes.set(block, type);
978
+ }
979
+ /**
980
+ * Advance `count` characters (`columns` false) or `count` columns (`columns`
981
+ * true), expanding tabs to the next 4-column tab stop (§2.2). A tab whose
982
+ * columns are only partly consumed leaves `offset` on the tab itself and
983
+ * sets `partiallyConsumedTab`, so `addLine` can re-emit the remainder as
984
+ * spaces.
985
+ */
986
+ advanceOffset(count, columns) {
987
+ const ln = this.currentLine;
988
+ while (0 < count && this.offset < ln.length) {
989
+ const c = ln.charAt(this.offset);
990
+ if ('\t' === c) {
991
+ const charsToTab = 4 - (this.column % 4);
992
+ if (columns) {
993
+ this.partiallyConsumedTab = charsToTab > count;
994
+ const charsToAdvance = charsToTab > count ? count : charsToTab;
995
+ this.column += charsToAdvance;
996
+ this.offset += this.partiallyConsumedTab ? 0 : 1;
997
+ count -= charsToAdvance;
998
+ }
999
+ else {
1000
+ this.partiallyConsumedTab = false;
1001
+ this.column += charsToTab;
1002
+ this.offset += 1;
1003
+ count -= 1;
1004
+ }
1005
+ }
1006
+ else {
1007
+ this.partiallyConsumedTab = false;
1008
+ this.offset += 1;
1009
+ this.column += 1; // block markers are ASCII, so one char is one column
1010
+ count -= 1;
1011
+ }
1012
+ }
1013
+ }
1014
+ advanceNextNonspace() {
1015
+ this.offset = this.nextNonspace;
1016
+ this.column = this.nextNonspaceColumn;
1017
+ this.partiallyConsumedTab = false;
1018
+ }
1019
+ /** Locate the next non-space character and the indent leading up to it. */
1020
+ findNextNonspace() {
1021
+ const ln = this.currentLine;
1022
+ let i = this.offset;
1023
+ let cols = this.column;
1024
+ let c = '';
1025
+ for (;;) {
1026
+ c = ln.charAt(i);
1027
+ if (' ' === c) {
1028
+ i++;
1029
+ cols++;
1030
+ }
1031
+ else if ('\t' === c) {
1032
+ i++;
1033
+ cols += 4 - (cols % 4);
1034
+ }
1035
+ else {
1036
+ break;
1037
+ }
1038
+ }
1039
+ this.blank = '' === c || '\n' === c || '\r' === c;
1040
+ this.nextNonspace = i;
1041
+ this.nextNonspaceColumn = cols;
1042
+ this.indent = this.nextNonspaceColumn - this.column;
1043
+ this.indented = this.indent >= CODE_INDENT;
1044
+ }
1045
+ /** Add the rest of the current line to the tip's raw content. */
1046
+ addLine() {
1047
+ const tip = this.openTip;
1048
+ // Taken before the tab fixup below moves `offset`, so a line's recorded
1049
+ // column is the one `addChild` would report for that same position.
1050
+ const column = this.sourceColumn(this.offset) + 1; // offset 0 is column 1
1051
+ let text = '';
1052
+ if (this.partiallyConsumedTab) {
1053
+ this.offset += 1; // step over the tab
1054
+ const charsToTab = 4 - (this.column % 4);
1055
+ text = ' '.repeat(charsToTab);
1056
+ }
1057
+ text += this.currentLine.slice(this.offset);
1058
+ tip.stringContent += text + '\n';
1059
+ this.lastAddedLine = text;
1060
+ this.lastAddedColumn = column;
1061
+ this.lastAddedTo = tip;
1062
+ }
1063
+ /**
1064
+ * Open a block of `tag` as a child of the tip, closing blocks that cannot
1065
+ * contain it first.
1066
+ */
1067
+ /**
1068
+ * Characters of `currentLine` before UTF-16 offset `offset` — what a
1069
+ * sourcepos column counts.
1070
+ *
1071
+ * Not simply `offset`: a JavaScript string index counts UTF-16 units, so an
1072
+ * astral character (emoji, rare CJK) would advance the column by two. A
1073
+ * column means a character, so `\u{1F600} *x*` must report the same columns
1074
+ * in both runtimes.
1075
+ *
1076
+ * Counted incrementally from the last offset already measured. `addChild`
1077
+ * runs once per container opened, so counting from the start of the line
1078
+ * every time would make a line of n nested containers — `> - > - …` — cost
1079
+ * O(n²); untrusted input picks n. Offsets advance monotonically as a line is
1080
+ * consumed, making this amortized O(1); a smaller offset than the anchor is
1081
+ * still correct, just recounted from the start.
1082
+ */
1083
+ sourceColumn(offset) {
1084
+ if (offset < this.colAnchorOffset) {
1085
+ this.colAnchorOffset = 0;
1086
+ this.colAnchorChars = 0;
1087
+ }
1088
+ this.colAnchorChars += charCount(this.currentLine.slice(this.colAnchorOffset, offset));
1089
+ this.colAnchorOffset = offset;
1090
+ return this.colAnchorChars;
1091
+ }
1092
+ addChild(tag, offset) {
1093
+ while (!handlerFor(this.openTip.type).canContain(tag)) {
1094
+ this.finalize(this.openTip, this.lineNumber - 1);
1095
+ }
1096
+ const columnNumber = this.sourceColumn(offset) + 1; // offset 0 is column 1
1097
+ const node = new node_ts_1.MdNode(tag, [
1098
+ [this.lineNumber, columnNumber],
1099
+ [0, 0],
1100
+ ]);
1101
+ this.openTip.appendChild(node);
1102
+ this.tip = node;
1103
+ return node;
1104
+ }
1105
+ /**
1106
+ * GFM tables: try to read the current line as the delimiter row of a table
1107
+ * whose header row is the last line of the open paragraph `container`.
1108
+ *
1109
+ * The two rows must agree on the number of cells; if they do not, there is
1110
+ * no table and the line is nothing special (spec example 6 keeps the whole
1111
+ * thing a paragraph). The header row is the paragraph's **last** line only,
1112
+ * so a paragraph that had earlier lines is split: those lines stay a
1113
+ * paragraph — finalized here, which is what still lets them contribute link
1114
+ * reference definitions — and the table is opened after it.
1115
+ *
1116
+ * On success the table is the tip and the caller returns "leaf started", so
1117
+ * step 3 adds the delimiter row itself to the table's raw content. It is
1118
+ * skipped again by `finalizeTable`; keeping it costs nothing and makes the
1119
+ * table's accumulated lines line up one-for-one with the source lines.
1120
+ */
1121
+ tryOpenTable(container) {
1122
+ if (!this.options.gfm || this.indented || 'paragraph' !== container.type)
1123
+ return false;
1124
+ const align = parseDelimiterRow(this.currentLine.slice(this.nextNonspace));
1125
+ if (null === align)
1126
+ return false;
1127
+ // The header row is the line directly above, which is the line `addLine`
1128
+ // last wrote — to this paragraph, or there is no header row at all.
1129
+ if (this.lastAddedTo !== container)
1130
+ return false;
1131
+ const headerCells = splitTableRow(this.lastAddedLine);
1132
+ if (headerCells.length !== align.length)
1133
+ return false;
1134
+ this.closeUnmatchedBlocks();
1135
+ const headerLine = this.lineNumber - 1;
1136
+ const headerColumn = this.lastAddedColumn;
1137
+ // `stringContent` ends with that same line plus the newline `addLine` put
1138
+ // after it, so the header row starts this far in — no scan needed, and the
1139
+ // content is only ever touched on the split branch, at most once per table.
1140
+ const content = container.stringContent;
1141
+ const headerStart = content.length - this.lastAddedLine.length - 1;
1142
+ if (0 < headerStart) {
1143
+ // Split: everything above the header row stays a paragraph. It ends two
1144
+ // lines back, not one, so `finalize`'s default end column — the previous
1145
+ // line's, which here is the header row's — would be somebody else's.
1146
+ container.stringContent = content.slice(0, headerStart);
1147
+ this.finalize(container, headerLine - 1, this.prevLineLength);
1148
+ }
1149
+ else {
1150
+ const parent = container.parent;
1151
+ container.unlink();
1152
+ this.tip = parent;
1153
+ }
1154
+ const table = this.addChild('table', this.nextNonspace);
1155
+ // `addChild` both dated and placed the table from the delimiter row, and
1156
+ // neither belongs to it: the table begins on the header row, one line
1157
+ // above, in the column where *that* row's content starts. The two rows are
1158
+ // indented independently, so the delimiter row's column is simply somebody
1159
+ // else's — ` a | b` over `| - | -` starts in column 3, and `a | b` over
1160
+ // ` | - | -` starts in column 1.
1161
+ table.sourcepos[0][0] = headerLine;
1162
+ table.sourcepos[0][1] = headerColumn;
1163
+ table.tableAlign = align;
1164
+ // The row is measured over its own text, exactly as `finalizeTable`
1165
+ // measures the body rows: `lastLineLength` counts the whole source line,
1166
+ // container markers included, so inside a block quote it would end the
1167
+ // header row two columns past where every body row of the same table ends.
1168
+ addTableRow(this, table, headerCells, true, headerLine, charCount(this.lastAddedLine));
1169
+ return true;
1170
+ }
1171
+ /** Close every block that failed its continuation condition on this line. */
1172
+ closeUnmatchedBlocks() {
1173
+ if (this.allClosed)
1174
+ return;
1175
+ while (this.oldtip !== this.lastMatchedContainer) {
1176
+ const parent = this.oldtip.parent;
1177
+ this.finalize(this.oldtip, this.lineNumber - 1);
1178
+ if (null === parent)
1179
+ break;
1180
+ this.oldtip = parent;
1181
+ }
1182
+ this.allClosed = true;
1183
+ }
1184
+ /**
1185
+ * Close a block, record its end position, and run its finalizer.
1186
+ *
1187
+ * `endColumn` defaults to the previous line's length, which is where every
1188
+ * block that ends on `lineNumber` ends. Only a table's paragraph split
1189
+ * passes it: that one closes a block two lines back, where the default is
1190
+ * one line too recent.
1191
+ */
1192
+ finalize(block, lineNumber, endColumn = this.lastLineLength) {
1193
+ const above = block.parent;
1194
+ block.open = false;
1195
+ block.sourcepos[1] = [lineNumber, endColumn];
1196
+ handlerFor(block.type).finalize(this, block);
1197
+ this.htmlBlockTypes.delete(block);
1198
+ this.tip = above;
1199
+ }
1200
+ /**
1201
+ * Incorporate one line into the tree — the three steps of Appendix A.
1202
+ */
1203
+ incorporateLine(ln) {
1204
+ let allMatched = true;
1205
+ let container = this.doc;
1206
+ // `lastLineLength` still measures the previous line here. Captured before
1207
+ // anything can overwrite it — an HTML block closing mid-line does — and
1208
+ // installed as `prevLineLength` at whichever exit updates `lastLineLength`,
1209
+ // so the two always describe consecutive lines.
1210
+ const lengthBeforeThisLine = this.lastLineLength;
1211
+ this.oldtip = this.openTip;
1212
+ this.offset = 0;
1213
+ this.column = 0;
1214
+ this.blank = false;
1215
+ this.partiallyConsumedTab = false;
1216
+ this.lineNumber += 1;
1217
+ this.currentLine = ln;
1218
+ // The column anchor memoizes offsets into `currentLine`; a new line
1219
+ // invalidates it. See `sourceColumn`.
1220
+ this.colAnchorOffset = 0;
1221
+ this.colAnchorChars = 0;
1222
+ // Step 1: walk the open blocks, consuming continuation markers. Stop at
1223
+ // the first block that does not match; `container` is then the last
1224
+ // matched one.
1225
+ let lastChild = container.lastChild;
1226
+ while (lastChild && lastChild.open) {
1227
+ container = lastChild;
1228
+ this.findNextNonspace();
1229
+ const result = handlerFor(container.type).continue(this, container);
1230
+ if (1 === result) {
1231
+ allMatched = false;
1232
+ }
1233
+ else if (2 === result) {
1234
+ // A fenced code block was closed by this line; nothing else to do.
1235
+ return;
1236
+ }
1237
+ if (!allMatched) {
1238
+ const parent = container.parent;
1239
+ if (parent)
1240
+ container = parent;
1241
+ break;
1242
+ }
1243
+ lastChild = container.lastChild;
1244
+ }
1245
+ this.allClosed = container === this.oldtip;
1246
+ this.lastMatchedContainer = container;
1247
+ // Step 2: look for new block starts under the last matched container —
1248
+ // unless its lines are literal text, in which case there is nothing to
1249
+ // look for.
1250
+ let matchedLeaf = handlerFor(container.type).verbatim;
1251
+ while (!matchedLeaf) {
1252
+ this.findNextNonspace();
1253
+ if (!this.indented && !this.maybeSpecial.test(ln.slice(this.nextNonspace))) {
1254
+ this.advanceNextNonspace();
1255
+ break;
1256
+ }
1257
+ let i = 0;
1258
+ while (i < BLOCK_STARTS.length) {
1259
+ const result = BLOCK_STARTS[i](this, container);
1260
+ if (1 === result) {
1261
+ container = this.openTip;
1262
+ break;
1263
+ }
1264
+ if (2 === result) {
1265
+ container = this.openTip;
1266
+ matchedLeaf = true;
1267
+ break;
1268
+ }
1269
+ i++;
1270
+ }
1271
+ if (i === BLOCK_STARTS.length) {
1272
+ // Nothing matched: the rest of the line is plain text.
1273
+ this.advanceNextNonspace();
1274
+ break;
1275
+ }
1276
+ }
1277
+ // Step 3: whatever is left of the line is text for the deepest open block.
1278
+ if (!this.allClosed && !this.blank && 'paragraph' === this.openTip.type) {
1279
+ // Lazy continuation (§5.1 rule 2): the markers are missing but an open
1280
+ // paragraph absorbs the line anyway, and its containers stay open.
1281
+ this.addLine();
1282
+ this.prevLineLength = lengthBeforeThisLine;
1283
+ this.lastLineLength = charCount(ln);
1284
+ return;
1285
+ }
1286
+ this.closeUnmatchedBlocks();
1287
+ if (this.blank && container.lastChild)
1288
+ container.lastChild.lastLineBlank = true;
1289
+ const t = container.type;
1290
+ // A block quote line is never blank (it has a `>`), blank lines inside
1291
+ // fenced code do not affect list looseness, and an item's own first blank
1292
+ // line is part of the item, not a separator.
1293
+ const lastLineBlank = this.blank &&
1294
+ !('block_quote' === t ||
1295
+ ('code_block' === t && container.isFenced) ||
1296
+ ('item' === t &&
1297
+ null === container.firstChild &&
1298
+ container.sourcepos[0][0] === this.lineNumber));
1299
+ // Propagate up: a container whose last line was blank ends with a blank line.
1300
+ let cont = container;
1301
+ while (cont) {
1302
+ cont.lastLineBlank = lastLineBlank;
1303
+ cont = cont.parent;
1304
+ }
1305
+ if (handlerFor(t).acceptsLines) {
1306
+ this.addLine();
1307
+ // §4.6: HTML block types 1-5 end on the line that contains their closing
1308
+ // string, which may be the line that opened them.
1309
+ if ('html_block' === t) {
1310
+ const type = this.htmlBlockType(container);
1311
+ if (1 <= type &&
1312
+ type <= 5 &&
1313
+ RE_HTML_BLOCK_CLOSE[type].test(this.currentLine.slice(this.offset))) {
1314
+ this.lastLineLength = charCount(ln);
1315
+ this.finalize(container, this.lineNumber);
1316
+ }
1317
+ }
1318
+ }
1319
+ else if (this.offset < ln.length && !this.blank) {
1320
+ // Nowhere else for the text to go: open a paragraph for it.
1321
+ this.addChild('paragraph', this.offset);
1322
+ this.advanceNextNonspace();
1323
+ this.addLine();
1324
+ }
1325
+ this.prevLineLength = lengthBeforeThisLine;
1326
+ this.lastLineLength = charCount(ln);
1327
+ }
1328
+ parse(input) {
1329
+ // §2.3: insecure characters are replaced before anything else looks at them.
1330
+ const text = -1 === input.indexOf('\u0000') ? input : input.replace(/\0/g, '\uFFFD');
1331
+ const lines = text.split(RE_LINE_ENDING);
1332
+ let len = lines.length;
1333
+ // A final line ending does not introduce a trailing blank line.
1334
+ if (0 < text.length && '' === lines[len - 1])
1335
+ len -= 1;
1336
+ for (let i = 0; i < len; i++) {
1337
+ this.incorporateLine(lines[i]);
1338
+ }
1339
+ while (this.tip) {
1340
+ this.finalize(this.tip, len);
1341
+ }
1342
+ this.doc.gfm = this.options.gfm;
1343
+ if (this.options.gfm)
1344
+ markTaskListItems(this.doc);
1345
+ return { doc: this.doc, refmap: this.refmap };
1346
+ }
1347
+ }
1348
+ // --- GFM task list items ----------------------------------------------------
1349
+ /**
1350
+ * The task list item marker: optional spaces, `[`, either a whitespace
1351
+ * character or `x`/`X`, `]`, and then at least one whitespace character before
1352
+ * any other content.
1353
+ *
1354
+ * The trailing whitespace is required by the extension ("…and at least one
1355
+ * whitespace character before any other content"), so `- [x]` with nothing
1356
+ * after it is an ordinary item whose text is `[x]`. It is matched as a space
1357
+ * or a tab rather than as any whitespace: a line ending there would mean the
1358
+ * content starts on the *next* line, and consuming it would swallow the
1359
+ * paragraph's first soft break.
1360
+ */
1361
+ const RE_TASK_LIST_MARKER = /^ *\[([ \tXx])\][ \t]+/;
1362
+ const TASK_LIST_CONTAINERS = {
1363
+ document: true,
1364
+ block_quote: true,
1365
+ list: true,
1366
+ item: true,
1367
+ };
1368
+ /**
1369
+ * GFM task list items: mark every item whose first block is a paragraph
1370
+ * beginning with a task list item marker, and consume the marker so it does
1371
+ * not survive into the text.
1372
+ *
1373
+ * Run at the end of the block phase, over `stringContent` — before the inline
1374
+ * phase has seen it. That is what makes the marker win over anything the
1375
+ * inline scanner would otherwise make of the brackets: with a `[x]: /url`
1376
+ * definition in the document, `- [x] foo` is still a task item rather than a
1377
+ * link. It also means the item's `checked` state is settled before `ast.ts` or
1378
+ * `html.ts` looks at the tree.
1379
+ *
1380
+ * Iterative rather than recursive: container nesting depth is chosen by the
1381
+ * input, and `'> '.repeat(20000)` is a document this parser otherwise handles.
1382
+ */
1383
+ function markTaskListItems(doc) {
1384
+ const stack = [doc];
1385
+ while (0 < stack.length) {
1386
+ const node = stack.pop();
1387
+ if ('item' === node.type) {
1388
+ const first = node.firstChild;
1389
+ if (null !== first && 'paragraph' === first.type) {
1390
+ const m = RE_TASK_LIST_MARKER.exec(first.stringContent);
1391
+ if (null !== m) {
1392
+ // Whitespace between the brackets is unchecked; `x`/`X` is checked.
1393
+ node.checked = ' ' !== m[1] && '\t' !== m[1];
1394
+ first.stringContent = first.stringContent.slice(m[0].length);
1395
+ }
1396
+ }
1397
+ }
1398
+ if (true === TASK_LIST_CONTAINERS[node.type]) {
1399
+ for (let c = node.lastChild; c; c = c.prev)
1400
+ stack.push(c);
1401
+ }
1402
+ }
1403
+ }
1404
+ /**
1405
+ * Phase 1: build the block tree and collect link reference definitions.
1406
+ * Paragraph and heading text is left raw in `stringContent` for phase 2.
1407
+ */
1408
+ function parseBlocks(input, options) {
1409
+ return new BlockParser(options).parse(input);
1410
+ }
1411
+ //# sourceMappingURL=block.js.map