Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions website/scripts/docs-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,7 @@ function closesFence(line, marker, length) {
/**
* Whether `line` begins a new block, so a paragraph — or a blockquote's lazy
* continuation — ends before it instead of swallowing it. `next` supplies the
* single line of lookahead a table needs, a `|` row being a table only when a
* delimiter row follows it.
* single line of lookahead a table needs; `startsTable` holds the test.
*
* Six of the seven block starts are here: ATX heading, list item, blockquote,
* fence, table, and block-level raw HTML. A standalone `<img>` is deliberately
Expand All @@ -309,7 +308,7 @@ function interruptsParagraph(line, next) {
|| LIST_ITEM.test(line)
|| /^\s*>\s?/.test(line)
|| /^(\s*)(`{3,}|~{3,})/.test(line)
|| (line.includes('|') && isDelimiterRow(next ?? ''))
|| startsTable(line, next ?? '')
|| (/^\s*<\/?[a-zA-Z]/.test(line) && !/^\s*<img\b/i.test(line));
}

Expand All @@ -332,10 +331,14 @@ const SETEXT_UNDERLINE = /^\s*(=+|-+)\s*$/;
const THEMATIC_BREAK = /^\s*([-*_])(\s*\1){2,}\s*$/;

/**
* Split a table row into cells, honouring backslash-escaped pipes so a cell
* containing `` `\|` `` (which the shortcut table needs) survives intact.
* Split a table row into raw cell strings, honouring backslash-escaped pipes so
* a cell containing `` `\|` `` (which the shortcut table needs) survives intact.
*
* Kept separate from `splitRow` because `startsTable` counts cells
* speculatively on every paragraph line, where `parseInline` would raise on
* inline content the parser rejects.
*/
function splitRow(row, line) {
function splitCells(row) {
const trimmed = row.trim().replace(/^\|/, '').replace(/\|$/, '');
const cells = [];
let cur = '';
Expand All @@ -347,7 +350,12 @@ function splitRow(row, line) {
cur += ch;
}
cells.push(cur);
return cells.map((c) => parseInline(c.trim(), line));
return cells;
}

/** Split a table row into cells and parse each one's inline content. */
function splitRow(row, line) {
return splitCells(row).map((c) => parseInline(c.trim(), line));
}

/**
Expand All @@ -360,6 +368,18 @@ function isDelimiterRow(row) {
return row.includes('|') && /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/.test(row);
}

/**
* Whether `line` and the delimiter row `next` start a GFM table. The cell
* counts have to match: GFM does not recognise a table when they differ, so
* `a | b` over `| --- |` is paragraph text, and reading it as a table drops the
* delimiter row and restyles the prose with no error.
*/
function startsTable(line, next) {
return line.includes('|')
&& isDelimiterRow(next)
&& splitCells(line).length === splitCells(next).length;
}

function alignmentsFrom(row) {
return row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => {
const s = c.trim();
Expand Down Expand Up @@ -429,7 +449,7 @@ export function parseMarkdown(markdown, options = {}) {
}

// Table
if (raw.includes('|') && i + 1 < lines.length && isDelimiterRow(lines[i + 1])) {
if (i + 1 < lines.length && startsTable(raw, lines[i + 1])) {
const header = splitRow(raw, lineNo);
const align = alignmentsFrom(lines[i + 1]);
i += 2;
Expand Down
24 changes: 24 additions & 0 deletions website/scripts/docs-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,30 @@ describe('blocks', () => {
expect(() => parseMarkdown('a | b\n---\n')).toThrow(/setext heading underline/);
});

it('reads a delimiter row whose cell count differs from the header as prose', () => {
expect(parseMarkdown('Intro\na | b\n| --- |\n').blocks.map((b) => b.type)).toEqual(['paragraph']);
expect(parseMarkdown('a | b\n| --- |\n').blocks.map((b) => b.type)).toEqual(['paragraph']);
expect(parseMarkdown('a | b | c\n--- | ---\n1 | 2 | 3\n').blocks.map((b) => b.type)).toEqual(['paragraph']);
});

it('keeps a mismatched delimiter row inside the blockquote it lazily continues', () => {
const { blocks } = parseMarkdown('> quoted\na | b\n| --- |\n');
expect(blocks.map((b) => b.type)).toEqual(['blockquote']);
});

it('still parses a table whose header omits the outer pipes', () => {
const { blocks } = parseMarkdown('a|b\n-|-\n1|2\n');
expect(blocks[0].type).toBe('table');
expect(blocks[0].header.map(inlineToText)).toEqual(['a', 'b']);
expect(blocks[0].rows[0].map(inlineToText)).toEqual(['1', '2']);
});
Comment thread
dormouse-bot marked this conversation as resolved.

it('counts an escaped pipe in the header as one cell, not two', () => {
const { blocks } = parseMarkdown('a \\| b | c\n--- | ---\n1 | 2\n');
expect(blocks[0].type).toBe('table');
expect(blocks[0].header.map(inlineToText)).toEqual(['a | b', 'c']);
});

it('parses a table with an escaped pipe inside inline code', () => {
const md = '| Key | Action |\n|-----|--------|\n| `\\|` or tmux `%` | Split |\n';
const { blocks } = parseMarkdown(md);
Expand Down