173 lines
4.5 KiB
TypeScript
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",
|
|
}],
|
|
};
|