@tabnas/markdown 0.5.0 → 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/README.md +65 -10
- package/dist/ast.d.ts +31 -1
- package/dist/ast.js +26 -1
- package/dist/ast.js.map +1 -1
- package/dist/block.js +448 -8
- package/dist/block.js.map +1 -1
- package/dist/html.d.ts +9 -3
- package/dist/html.js +137 -14
- package/dist/html.js.map +1 -1
- package/dist/inline.d.ts +7 -1
- package/dist/inline.js +431 -2
- package/dist/inline.js.map +1 -1
- package/dist/markdown.d.ts +5 -2
- package/dist/markdown.js.map +1 -1
- package/dist/node.d.ts +39 -1
- package/dist/node.js +36 -0
- package/dist/node.js.map +1 -1
- package/dist/options.d.ts +9 -4
- package/dist/options.js.map +1 -1
- package/package.json +2 -1
- package/src/ast.ts +56 -2
- package/src/block.ts +495 -10
- package/src/html.ts +140 -15
- package/src/inline.ts +463 -2
- package/src/markdown.ts +7 -1
- package/src/node.ts +53 -0
- package/src/options.ts +9 -4
package/dist/block.js
CHANGED
|
@@ -41,12 +41,24 @@ const C_SPACE = 32;
|
|
|
41
41
|
const C_GREATERTHAN = 62;
|
|
42
42
|
const C_LESSTHAN = 60;
|
|
43
43
|
const C_OPEN_BRACKET = 91;
|
|
44
|
+
const C_BACKSLASH = 92;
|
|
45
|
+
const C_PIPE = 124;
|
|
44
46
|
/** §2.1: \r\n, \n and a lone \r are all line endings. */
|
|
45
47
|
const RE_LINE_ENDING = /\r\n|\n|\r/;
|
|
46
48
|
/** §4.1: 3+ matching `*`, `_` or `-`, each optionally followed by spaces/tabs. */
|
|
47
49
|
const RE_THEMATIC_BREAK = /^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/;
|
|
48
50
|
/** Fast reject: every block start begins with one of these characters. */
|
|
49
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:|-]/;
|
|
50
62
|
const RE_NON_SPACE = /[^ \t\f\v\r\n]/;
|
|
51
63
|
/** §5.2 list markers. */
|
|
52
64
|
const RE_BULLET_LIST_MARKER = /^[*+-]/;
|
|
@@ -185,6 +197,140 @@ function isSpaceOrTab(c) {
|
|
|
185
197
|
function isBlank(s) {
|
|
186
198
|
return !RE_NON_SPACE.test(s);
|
|
187
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;
|
|
188
334
|
/** §5.3: two markers make the same list only if type, bullet and delimiter agree. */
|
|
189
335
|
function listsMatch(listData, itemData) {
|
|
190
336
|
return (listData.type === itemData.type &&
|
|
@@ -222,6 +368,7 @@ const BLOCK_HANDLERS = {
|
|
|
222
368
|
finalize: () => undefined,
|
|
223
369
|
canContain: notItem,
|
|
224
370
|
acceptsLines: false,
|
|
371
|
+
verbatim: false,
|
|
225
372
|
},
|
|
226
373
|
list: {
|
|
227
374
|
continue: () => 0,
|
|
@@ -253,6 +400,7 @@ const BLOCK_HANDLERS = {
|
|
|
253
400
|
},
|
|
254
401
|
canContain: (t) => 'item' === t,
|
|
255
402
|
acceptsLines: false,
|
|
403
|
+
verbatim: false,
|
|
256
404
|
},
|
|
257
405
|
block_quote: {
|
|
258
406
|
continue: (parser) => {
|
|
@@ -270,6 +418,7 @@ const BLOCK_HANDLERS = {
|
|
|
270
418
|
finalize: () => undefined,
|
|
271
419
|
canContain: notItem,
|
|
272
420
|
acceptsLines: false,
|
|
421
|
+
verbatim: false,
|
|
273
422
|
},
|
|
274
423
|
item: {
|
|
275
424
|
continue: (parser, container) => {
|
|
@@ -305,6 +454,7 @@ const BLOCK_HANDLERS = {
|
|
|
305
454
|
},
|
|
306
455
|
canContain: notItem,
|
|
307
456
|
acceptsLines: false,
|
|
457
|
+
verbatim: false,
|
|
308
458
|
},
|
|
309
459
|
heading: {
|
|
310
460
|
// A heading is always exactly one line (setext headings are converted from
|
|
@@ -313,12 +463,14 @@ const BLOCK_HANDLERS = {
|
|
|
313
463
|
finalize: () => undefined,
|
|
314
464
|
canContain: noChildren,
|
|
315
465
|
acceptsLines: false,
|
|
466
|
+
verbatim: false,
|
|
316
467
|
},
|
|
317
468
|
thematic_break: {
|
|
318
469
|
continue: () => 1,
|
|
319
470
|
finalize: () => undefined,
|
|
320
471
|
canContain: noChildren,
|
|
321
472
|
acceptsLines: false,
|
|
473
|
+
verbatim: false,
|
|
322
474
|
},
|
|
323
475
|
code_block: {
|
|
324
476
|
continue: (parser, container) => {
|
|
@@ -372,6 +524,7 @@ const BLOCK_HANDLERS = {
|
|
|
372
524
|
},
|
|
373
525
|
canContain: noChildren,
|
|
374
526
|
acceptsLines: true,
|
|
527
|
+
verbatim: true,
|
|
375
528
|
},
|
|
376
529
|
html_block: {
|
|
377
530
|
continue: (parser, container) => {
|
|
@@ -386,6 +539,7 @@ const BLOCK_HANDLERS = {
|
|
|
386
539
|
},
|
|
387
540
|
canContain: noChildren,
|
|
388
541
|
acceptsLines: true,
|
|
542
|
+
verbatim: true,
|
|
389
543
|
},
|
|
390
544
|
paragraph: {
|
|
391
545
|
continue: (parser) => (parser.blank ? 1 : 0),
|
|
@@ -404,8 +558,101 @@ const BLOCK_HANDLERS = {
|
|
|
404
558
|
},
|
|
405
559
|
canContain: noChildren,
|
|
406
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,
|
|
407
593
|
},
|
|
408
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
|
+
}
|
|
409
656
|
function handlerFor(type) {
|
|
410
657
|
const handler = BLOCK_HANDLERS[type];
|
|
411
658
|
if (undefined === handler)
|
|
@@ -634,6 +881,14 @@ const BLOCK_STARTS = [
|
|
|
634
881
|
parser.addChild('code_block', parser.offset);
|
|
635
882
|
return 2;
|
|
636
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),
|
|
637
892
|
];
|
|
638
893
|
// --- the parser -------------------------------------------------------------
|
|
639
894
|
class BlockParser {
|
|
@@ -644,6 +899,16 @@ class BlockParser {
|
|
|
644
899
|
this.currentLine = '';
|
|
645
900
|
this.lineNumber = 0;
|
|
646
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;
|
|
647
912
|
// Memoize the last offset in `currentLine` whose character count is known,
|
|
648
913
|
// so sourcepos columns cost amortized O(1). Both reset per line.
|
|
649
914
|
// See `sourceColumn`.
|
|
@@ -661,10 +926,36 @@ class BlockParser {
|
|
|
661
926
|
this.indent = 0;
|
|
662
927
|
this.indented = false;
|
|
663
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;
|
|
664
954
|
// MdNode has no field for the HTML block type, and the tree contract is not
|
|
665
955
|
// ours to change, so open HTML blocks carry it here.
|
|
666
956
|
this.htmlBlockTypes = new Map();
|
|
667
957
|
this.options = options;
|
|
958
|
+
this.maybeSpecial = options.gfm ? RE_MAYBE_SPECIAL_GFM : RE_MAYBE_SPECIAL;
|
|
668
959
|
this.doc = new node_ts_1.MdNode('document', [
|
|
669
960
|
[1, 1],
|
|
670
961
|
[0, 0],
|
|
@@ -754,12 +1045,20 @@ class BlockParser {
|
|
|
754
1045
|
/** Add the rest of the current line to the tip's raw content. */
|
|
755
1046
|
addLine() {
|
|
756
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 = '';
|
|
757
1052
|
if (this.partiallyConsumedTab) {
|
|
758
1053
|
this.offset += 1; // step over the tab
|
|
759
1054
|
const charsToTab = 4 - (this.column % 4);
|
|
760
|
-
|
|
1055
|
+
text = ' '.repeat(charsToTab);
|
|
761
1056
|
}
|
|
762
|
-
|
|
1057
|
+
text += this.currentLine.slice(this.offset);
|
|
1058
|
+
tip.stringContent += text + '\n';
|
|
1059
|
+
this.lastAddedLine = text;
|
|
1060
|
+
this.lastAddedColumn = column;
|
|
1061
|
+
this.lastAddedTo = tip;
|
|
763
1062
|
}
|
|
764
1063
|
/**
|
|
765
1064
|
* Open a block of `tag` as a child of the tip, closing blocks that cannot
|
|
@@ -803,6 +1102,72 @@ class BlockParser {
|
|
|
803
1102
|
this.tip = node;
|
|
804
1103
|
return node;
|
|
805
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
|
+
}
|
|
806
1171
|
/** Close every block that failed its continuation condition on this line. */
|
|
807
1172
|
closeUnmatchedBlocks() {
|
|
808
1173
|
if (this.allClosed)
|
|
@@ -816,11 +1181,18 @@ class BlockParser {
|
|
|
816
1181
|
}
|
|
817
1182
|
this.allClosed = true;
|
|
818
1183
|
}
|
|
819
|
-
/**
|
|
820
|
-
|
|
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) {
|
|
821
1193
|
const above = block.parent;
|
|
822
1194
|
block.open = false;
|
|
823
|
-
block.sourcepos[1] = [lineNumber,
|
|
1195
|
+
block.sourcepos[1] = [lineNumber, endColumn];
|
|
824
1196
|
handlerFor(block.type).finalize(this, block);
|
|
825
1197
|
this.htmlBlockTypes.delete(block);
|
|
826
1198
|
this.tip = above;
|
|
@@ -831,6 +1203,11 @@ class BlockParser {
|
|
|
831
1203
|
incorporateLine(ln) {
|
|
832
1204
|
let allMatched = true;
|
|
833
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;
|
|
834
1211
|
this.oldtip = this.openTip;
|
|
835
1212
|
this.offset = 0;
|
|
836
1213
|
this.column = 0;
|
|
@@ -867,11 +1244,13 @@ class BlockParser {
|
|
|
867
1244
|
}
|
|
868
1245
|
this.allClosed = container === this.oldtip;
|
|
869
1246
|
this.lastMatchedContainer = container;
|
|
870
|
-
// Step 2: look for new block starts under the last matched container
|
|
871
|
-
|
|
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;
|
|
872
1251
|
while (!matchedLeaf) {
|
|
873
1252
|
this.findNextNonspace();
|
|
874
|
-
if (!this.indented && !
|
|
1253
|
+
if (!this.indented && !this.maybeSpecial.test(ln.slice(this.nextNonspace))) {
|
|
875
1254
|
this.advanceNextNonspace();
|
|
876
1255
|
break;
|
|
877
1256
|
}
|
|
@@ -900,6 +1279,7 @@ class BlockParser {
|
|
|
900
1279
|
// Lazy continuation (§5.1 rule 2): the markers are missing but an open
|
|
901
1280
|
// paragraph absorbs the line anyway, and its containers stay open.
|
|
902
1281
|
this.addLine();
|
|
1282
|
+
this.prevLineLength = lengthBeforeThisLine;
|
|
903
1283
|
this.lastLineLength = charCount(ln);
|
|
904
1284
|
return;
|
|
905
1285
|
}
|
|
@@ -942,6 +1322,7 @@ class BlockParser {
|
|
|
942
1322
|
this.advanceNextNonspace();
|
|
943
1323
|
this.addLine();
|
|
944
1324
|
}
|
|
1325
|
+
this.prevLineLength = lengthBeforeThisLine;
|
|
945
1326
|
this.lastLineLength = charCount(ln);
|
|
946
1327
|
}
|
|
947
1328
|
parse(input) {
|
|
@@ -958,9 +1339,68 @@ class BlockParser {
|
|
|
958
1339
|
while (this.tip) {
|
|
959
1340
|
this.finalize(this.tip, len);
|
|
960
1341
|
}
|
|
1342
|
+
this.doc.gfm = this.options.gfm;
|
|
1343
|
+
if (this.options.gfm)
|
|
1344
|
+
markTaskListItems(this.doc);
|
|
961
1345
|
return { doc: this.doc, refmap: this.refmap };
|
|
962
1346
|
}
|
|
963
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
|
+
}
|
|
964
1404
|
/**
|
|
965
1405
|
* Phase 1: build the block tree and collect link reference definitions.
|
|
966
1406
|
* Paragraph and heading text is left raw in `stringContent` for phase 2.
|