/** * @typedef {import('mdast').InlineCode} InlineCode * @typedef {import('mdast').Table} Table * @typedef {import('mdast').TableCell} TableCell * @typedef {import('mdast').TableRow} TableRow * * @typedef {import('markdown-table').Options} MarkdownTableOptions * * @typedef {import('mdast-util-from-markdown').CompileContext} CompileContext * @typedef {import('mdast-util-from-markdown').Extension} FromMarkdownExtension * @typedef {import('mdast-util-from-markdown').Handle} FromMarkdownHandle * * @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension * @typedef {import('mdast-util-to-markdown').Handle} ToMarkdownHandle * @typedef {import('mdast-util-to-markdown').State} State * @typedef {import('mdast-util-to-markdown').Info} Info */ /** * @typedef Options * Configuration. * @property {boolean | null | undefined} [tableCellPadding=true] * Whether to add a space of padding between delimiters and cells (default: * `true`). * @property {boolean | null | undefined} [tablePipeAlign=true] * Whether to align the delimiters (default: `true`). * @property {MarkdownTableOptions['stringLength'] | null | undefined} [stringLength] * Function to detect the length of table cell content, used when aligning * the delimiters between cells (optional). */ import {ok as assert} from 'devlop' import {markdownTable} from 'markdown-table' import {defaultHandlers} from 'mdast-util-to-markdown' /** * Create an extension for `mdast-util-from-markdown` to enable GFM tables in * markdown. * * @returns {FromMarkdownExtension} * Extension for `mdast-util-from-markdown` to enable GFM tables. */ export function gfmTableFromMarkdown() { return { enter: { table: enterTable, tableData: enterCell, tableHeader: enterCell, tableRow: enterRow }, exit: { codeText: exitCodeText, table: exitTable, tableData: exit, tableHeader: exit, tableRow: exit } } } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function enterTable(token) { const align = token._align assert(align, 'expected `_align` on table') this.enter( { type: 'table', align: align.map(function (d) { return d === 'none' ? null : d }), children: [] }, token ) this.data.inTable = true } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function exitTable(token) { this.exit(token) this.data.inTable = undefined } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function enterRow(token) { this.enter({type: 'tableRow', children: []}, token) } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function exit(token) { this.exit(token) } /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function enterCell(token) { this.enter({type: 'tableCell', children: []}, token) } // Overwrite the default code text data handler to unescape escaped pipes when // they are in tables. /** * @this {CompileContext} * @type {FromMarkdownHandle} */ function exitCodeText(token) { let value = this.resume() if (this.data.inTable) { value = value.replace(/\\([\\|])/g, replace) } const node = this.stack[this.stack.length - 1] assert(node.type === 'inlineCode') node.value = value this.exit(token) } /** * @param {string} $0 * @param {string} $1 * @returns {string} */ function replace($0, $1) { // Pipes work, backslashes don’t (but can’t escape pipes). return $1 === '|' ? $1 : $0 } /** * Create an extension for `mdast-util-to-markdown` to enable GFM tables in * markdown. * * @param {Options | null | undefined} [options] * Configuration. * @returns {ToMarkdownExtension} * Extension for `mdast-util-to-markdown` to enable GFM tables. */ export function gfmTableToMarkdown(options) { const settings = options || {} const padding = settings.tableCellPadding const alignDelimiters = settings.tablePipeAlign const stringLength = settings.stringLength const around = padding ? ' ' : '|' return { unsafe: [ {character: '\r', inConstruct: 'tableCell'}, {character: '\n', inConstruct: 'tableCell'}, // A pipe, when followed by a tab or space (padding), or a dash or colon // (unpadded delimiter row), could result in a table. {atBreak: true, character: '|', after: '[\t :-]'}, // A pipe in a cell must be encoded. {character: '|', inConstruct: 'tableCell'}, // A colon must be followed by a dash, in which case it could start a // delimiter row. {atBreak: true, character: ':', after: '-'}, // A delimiter row can also start with a dash, when followed by more // dashes, a colon, or a pipe. // This is a stricter version than the built in check for lists, thematic // breaks, and setex heading underlines though: // {atBreak: true, character: '-', after: '[:|-]'} ], handlers: { inlineCode: inlineCodeWithTable, table: handleTable, tableCell: handleTableCell, tableRow: handleTableRow } } /** * @type {ToMarkdownHandle} * @param {Table} node */ function handleTable(node, _, state, info) { return serializeData(handleTableAsData(node, state, info), node.align) } /** * This function isn’t really used normally, because we handle rows at the * table level. * But, if someone passes in a table row, this ensures we make somewhat sense. * * @type {ToMarkdownHandle} * @param {TableRow} node */ function handleTableRow(node, _, state, info) { const row = handleTableRowAsData(node, state, info) const value = serializeData([row]) // `markdown-table` will always add an align row return value.slice(0, value.indexOf('\n')) } /** * @type {ToMarkdownHandle} * @param {TableCell} node */ function handleTableCell(node, _, state, info) { const exit = state.enter('tableCell') const subexit = state.enter('phrasing') const value = state.containerPhrasing(node, { ...info, before: around, after: around }) subexit() exit() return value } /** * @param {Array>} matrix * @param {Array | null | undefined} [align] */ function serializeData(matrix, align) { return markdownTable(matrix, { align, // @ts-expect-error: `markdown-table` types should support `null`. alignDelimiters, // @ts-expect-error: `markdown-table` types should support `null`. padding, // @ts-expect-error: `markdown-table` types should support `null`. stringLength }) } /** * @param {Table} node * @param {State} state * @param {Info} info */ function handleTableAsData(node, state, info) { const children = node.children let index = -1 /** @type {Array>} */ const result = [] const subexit = state.enter('table') while (++index < children.length) { result[index] = handleTableRowAsData(children[index], state, info) } subexit() return result } /** * @param {TableRow} node * @param {State} state * @param {Info} info */ function handleTableRowAsData(node, state, info) { const children = node.children let index = -1 /** @type {Array} */ const result = [] const subexit = state.enter('tableRow') while (++index < children.length) { // Note: the positional info as used here is incorrect. // Making it correct would be impossible due to aligning cells? // And it would need copy/pasting `markdown-table` into this project. result[index] = handleTableCell(children[index], node, state, info) } subexit() return result } /** * @type {ToMarkdownHandle} * @param {InlineCode} node */ function inlineCodeWithTable(node, parent, state) { let value = defaultHandlers.inlineCode(node, parent, state) if (state.stack.includes('tableCell')) { value = value.replace(/\|/g, '\\$&') } return value } }