1
0
silverbullet/common/markdown_parser/table_parser.ts
2023-01-13 16:59:28 +01:00

173 lines
4.5 KiB
TypeScript

import {
BlockContext,
Element,
LeafBlock,
LeafBlockParser,
Line,
MarkdownConfig,
tags as t,
} from "../deps.ts";
function parseRow(
cx: BlockContext,
line: string,
startI = 0,
elts?: Element[],
offset = 0,
) {
let count = 0, first = true, cellStart = -1, cellEnd = -1, esc = false;
let parseCell = () => {
elts!.push(
cx.elt(
"TableCell",
offset + cellStart,
offset + cellEnd,
cx.parser.parseInline(
line.slice(cellStart, cellEnd),
offset + cellStart,
),
),
);
};
let inWikilink = false;
for (let i = startI; i < line.length; i++) {
let next = line.charCodeAt(i);
if (next === 91 /* '[' */ && line.charAt(i + 1) === "[") {
inWikilink = true;
} else if (
next === 93 /* ']' */ && line.charAt(i - 1) === "]" && inWikilink
) {
inWikilink = false;
}
if (next == 124 /* '|' */ && !esc && !inWikilink) {
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++) {
let 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))
) {
let 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
let 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-ignore: internal
let next = cx.scanLine(cx.absoluteLineEnd + 1).text;
return delimiterLine.test(next) &&
parseRow(cx, line.text, line.basePos) ==
parseRow(cx, next, line.basePos);
},
before: "SetextHeading",
}],
};