301 lines
8 KiB
JavaScript
301 lines
8 KiB
JavaScript
|
/**
|
|||
|
* @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:
|
|||
|
// <https://github.com/syntax-tree/mdast-util-to-markdown/blob/51a2038/lib/unsafe.js#L57>
|
|||
|
{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<Array<string>>} matrix
|
|||
|
* @param {Array<string | null | undefined> | 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<Array<string>>} */
|
|||
|
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<string>} */
|
|||
|
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
|
|||
|
}
|
|||
|
}
|