Files
Matouš Jan FialkaandGitHub b83c1e03d2 Fix Markdown table rendering and remove deprecated command button remnants (#1879)
* Fix table row parser

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

* Remove all `{[command button]}` remnants

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

* Add tests

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

* Fix extended task rendering in case of following EoL

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

---------

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
2026-03-12 13:46:48 +01:00

188 lines
4.7 KiB
TypeScript

import type {
BlockContext,
Element,
LeafBlock,
LeafBlockParser,
Line,
MarkdownConfig,
} from "@lezer/markdown";
import { tags as t } from "@lezer/highlight";
function parseRow(
cx: BlockContext,
line: string,
startI = 0,
elts?: Element[],
offset = 0,
) {
let count = 0,
first = true,
cellStart = -1,
cellEnd = -1,
esc = false;
const parseCell = () => {
elts!.push(
cx.elt(
"TableCell",
offset + cellStart,
offset + cellEnd,
cx.parser.parseInline(
line.slice(cellStart, cellEnd),
offset + cellStart,
),
),
);
};
// Track bracket nesting depth to protect `|` inside `[[wikilink]]`s,
// `[attribute: value]`s (and any future bracket-delimited syntax).
let bracketDepth = 0;
for (let i = startI; i < line.length; i++) {
const next = line.charCodeAt(i);
if (next === 91 /* '[' */) {
bracketDepth++;
} else if (next === 93 /* ']' */) {
if (bracketDepth > 0) bracketDepth--;
}
if (next === 124 /* '|' */ && !esc && bracketDepth === 0) {
if (!first || cellStart > -1) count++;
first = false;
if (elts) {
if (cellStart > -1) parseCell();
elts.push(cx.elt("TableDelimiter", i + offset, i + offset + 1));
}
cellStart = cellEnd = -1;
} else if (esc || (next !== 32 && next !== 9)) {
if (cellStart < 0) cellStart = i;
cellEnd = i + 1;
}
esc = !esc && next === 92;
}
if (cellStart > -1) {
count++;
if (elts) parseCell();
}
return count;
}
function hasPipe(str: string, start: number) {
for (let i = start; i < str.length; i++) {
const next = str.charCodeAt(i);
if (next === 124 /* '|' */) return true;
if (next === 92 /* '\\' */) i++;
}
return false;
}
const delimiterLine = /^\|?(\s*:?-+:?\s*\|)+(\s*:?-+:?\s*)?$/;
class TableParser implements LeafBlockParser {
// Null means we haven't seen the second line yet, false means this
// isn't a table, and an array means this is a table and we've
// parsed the given rows so far.
rows: false | null | Element[] = null;
nextLine(cx: BlockContext, line: Line, leaf: LeafBlock) {
if (this.rows == null) {
// Second line
this.rows = false;
let lineText;
if (
(line.next === 45 ||
line.next === 58 ||
line.next === 124) /* '-:|' */ &&
delimiterLine.test((lineText = line.text.slice(line.pos)))
) {
const firstRow: Element[] = [],
firstCount = parseRow(cx, leaf.content, 0, firstRow, leaf.start);
if (firstCount === parseRow(cx, lineText, line.pos)) {
this.rows = [
cx.elt(
"TableHeader",
leaf.start,
leaf.start + leaf.content.length,
firstRow,
),
cx.elt(
"TableDelimiter",
cx.lineStart + line.pos,
cx.lineStart + line.text.length,
),
];
}
}
} else if (this.rows) {
// Line after the second
const content: Element[] = [];
parseRow(cx, line.text, line.pos, content, cx.lineStart);
this.rows.push(
cx.elt(
"TableRow",
cx.lineStart + line.pos,
cx.lineStart + line.text.length,
content,
),
);
}
return false;
}
finish(cx: BlockContext, leaf: LeafBlock) {
if (!this.rows) return false;
cx.addLeafElement(
leaf,
cx.elt(
"Table",
leaf.start,
leaf.start + leaf.content.length,
this.rows as readonly Element[],
),
);
return true;
}
}
/// This extension provides
/// [GFM-style](https://github.github.com/gfm/#tables-extension-)
/// tables, using syntax like this:
///
/// ```
/// | head 1 | head 2 |
/// | --- | --- |
/// | cell 1 | cell 2 |
/// ```
export const Table: MarkdownConfig = {
defineNodes: [
{ name: "Table", block: true },
{ name: "TableHeader", style: { "TableHeader/...": t.heading } },
"TableRow",
{ name: "TableCell", style: t.content },
{ name: "TableDelimiter", style: t.processingInstruction },
],
parseBlock: [
{
name: "Table",
leaf(_, leaf) {
return hasPipe(leaf.content, 0) ? new TableParser() : null;
},
endLeaf(cx, line, leaf) {
if (
leaf.parsers.some((p) => p instanceof TableParser) ||
!hasPipe(line.text, line.basePos)
)
return false;
// @ts-expect-error: internal
const next = cx.scanLine(cx.absoluteLineEnd + 1).text;
return (
delimiterLine.test(next) &&
parseRow(cx, line.text, line.basePos) ===
parseRow(cx, next, line.basePos)
);
},
before: "SetextHeading",
},
],
};