diff --git a/common/deps.ts b/common/deps.ts index 179fbbe..f8363f5 100644 --- a/common/deps.ts +++ b/common/deps.ts @@ -22,6 +22,7 @@ export { readAll } from "https://deno.land/std@0.165.0/streams/conversion.ts"; export type { BlockContext, + Element, LeafBlock, LeafBlockParser, Line, diff --git a/common/markdown_parser/parser.ts b/common/markdown_parser/parser.ts index 548781a..51baca4 100644 --- a/common/markdown_parser/parser.ts +++ b/common/markdown_parser/parser.ts @@ -9,7 +9,6 @@ import { StreamLanguage, Strikethrough, styleTags, - Table, tags as t, TaskList, yamlLanguage, @@ -178,6 +177,7 @@ const directiveStart = /^\s*\s*/; const directiveEnd = /^\s*\s*/; import { parser as directiveParser } from "./parse-query.js"; +import { Table } from "./table_parser.ts"; const highlightingDirectiveParser = directiveParser.configure({ props: [ diff --git a/common/markdown_parser/table_parser.ts b/common/markdown_parser/table_parser.ts new file mode 100644 index 0000000..1acf1c1 --- /dev/null +++ b/common/markdown_parser/table_parser.ts @@ -0,0 +1,172 @@ +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", + }], +};