// To do: next major: remove. /** * @typedef {Options} MarkdownTableOptions * Configuration. */ /** * @typedef Options * Configuration. * @property {boolean | null | undefined} [alignDelimiters=true] * Whether to align the delimiters (default: `true`); * they are aligned by default: * * ```markdown * | Alpha | B | * | ----- | ----- | * | C | Delta | * ``` * * Pass `false` to make them staggered: * * ```markdown * | Alpha | B | * | - | - | * | C | Delta | * ``` * @property {ReadonlyArray | string | null | undefined} [align] * How to align columns (default: `''`); * one style for all columns or styles for their respective columns; * each style is either `'l'` (left), `'r'` (right), or `'c'` (center); * other values are treated as `''`, which doesn’t place the colon in the * alignment row but does align left; * *only the lowercased first character is used, so `Right` is fine.* * @property {boolean | null | undefined} [delimiterEnd=true] * Whether to end each row with the delimiter (default: `true`). * * > 👉 **Note**: please don’t use this: it could create fragile structures * > that aren’t understandable to some markdown parsers. * * When `true`, there are ending delimiters: * * ```markdown * | Alpha | B | * | ----- | ----- | * | C | Delta | * ``` * * When `false`, there are no ending delimiters: * * ```markdown * | Alpha | B * | ----- | ----- * | C | Delta * ``` * @property {boolean | null | undefined} [delimiterStart=true] * Whether to begin each row with the delimiter (default: `true`). * * > 👉 **Note**: please don’t use this: it could create fragile structures * > that aren’t understandable to some markdown parsers. * * When `true`, there are starting delimiters: * * ```markdown * | Alpha | B | * | ----- | ----- | * | C | Delta | * ``` * * When `false`, there are no starting delimiters: * * ```markdown * Alpha | B | * ----- | ----- | * C | Delta | * ``` * @property {boolean | null | undefined} [padding=true] * Whether to add a space of padding between delimiters and cells * (default: `true`). * * When `true`, there is padding: * * ```markdown * | Alpha | B | * | ----- | ----- | * | C | Delta | * ``` * * When `false`, there is no padding: * * ```markdown * |Alpha|B | * |-----|-----| * |C |Delta| * ``` * @property {((value: string) => number) | null | undefined} [stringLength] * Function to detect the length of table cell content (optional); * this is used when aligning the delimiters (`|`) between table cells; * full-width characters and emoji mess up delimiter alignment when viewing * the markdown source; * to fix this, you can pass this function, * which receives the cell content and returns its “visible” size; * note that what is and isn’t visible depends on where the text is displayed. * * Without such a function, the following: * * ```js * markdownTable([ * ['Alpha', 'Bravo'], * ['中文', 'Charlie'], * ['👩‍❤️‍👩', 'Delta'] * ]) * ``` * * Yields: * * ```markdown * | Alpha | Bravo | * | - | - | * | 中文 | Charlie | * | 👩‍❤️‍👩 | Delta | * ``` * * With [`string-width`](https://github.com/sindresorhus/string-width): * * ```js * import stringWidth from 'string-width' * * markdownTable( * [ * ['Alpha', 'Bravo'], * ['中文', 'Charlie'], * ['👩‍❤️‍👩', 'Delta'] * ], * {stringLength: stringWidth} * ) * ``` * * Yields: * * ```markdown * | Alpha | Bravo | * | ----- | ------- | * | 中文 | Charlie | * | 👩‍❤️‍👩 | Delta | * ``` */ /** * @param {string} value * Cell value. * @returns {number} * Cell size. */ function defaultStringLength(value) { return value.length } /** * Generate a markdown * ([GFM](https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables)) * table. * * @param {ReadonlyArray>} table * Table data (matrix of strings). * @param {Readonly | null | undefined} [options] * Configuration (optional). * @returns {string} * Result. */ export function markdownTable(table, options) { const settings = options || {} // To do: next major: change to spread. const align = (settings.align || []).concat() const stringLength = settings.stringLength || defaultStringLength /** @type {Array} Character codes as symbols for alignment per column. */ const alignments = [] /** @type {Array>} Cells per row. */ const cellMatrix = [] /** @type {Array>} Sizes of each cell per row. */ const sizeMatrix = [] /** @type {Array} */ const longestCellByColumn = [] let mostCellsPerRow = 0 let rowIndex = -1 // This is a superfluous loop if we don’t align delimiters, but otherwise we’d // do superfluous work when aligning, so optimize for aligning. while (++rowIndex < table.length) { /** @type {Array} */ const row = [] /** @type {Array} */ const sizes = [] let columnIndex = -1 if (table[rowIndex].length > mostCellsPerRow) { mostCellsPerRow = table[rowIndex].length } while (++columnIndex < table[rowIndex].length) { const cell = serialize(table[rowIndex][columnIndex]) if (settings.alignDelimiters !== false) { const size = stringLength(cell) sizes[columnIndex] = size if ( longestCellByColumn[columnIndex] === undefined || size > longestCellByColumn[columnIndex] ) { longestCellByColumn[columnIndex] = size } } row.push(cell) } cellMatrix[rowIndex] = row sizeMatrix[rowIndex] = sizes } // Figure out which alignments to use. let columnIndex = -1 if (typeof align === 'object' && 'length' in align) { while (++columnIndex < mostCellsPerRow) { alignments[columnIndex] = toAlignment(align[columnIndex]) } } else { const code = toAlignment(align) while (++columnIndex < mostCellsPerRow) { alignments[columnIndex] = code } } // Inject the alignment row. columnIndex = -1 /** @type {Array} */ const row = [] /** @type {Array} */ const sizes = [] while (++columnIndex < mostCellsPerRow) { const code = alignments[columnIndex] let before = '' let after = '' if (code === 99 /* `c` */) { before = ':' after = ':' } else if (code === 108 /* `l` */) { before = ':' } else if (code === 114 /* `r` */) { after = ':' } // There *must* be at least one hyphen-minus in each alignment cell. let size = settings.alignDelimiters === false ? 1 : Math.max( 1, longestCellByColumn[columnIndex] - before.length - after.length ) const cell = before + '-'.repeat(size) + after if (settings.alignDelimiters !== false) { size = before.length + size + after.length if (size > longestCellByColumn[columnIndex]) { longestCellByColumn[columnIndex] = size } sizes[columnIndex] = size } row[columnIndex] = cell } // Inject the alignment row. cellMatrix.splice(1, 0, row) sizeMatrix.splice(1, 0, sizes) rowIndex = -1 /** @type {Array} */ const lines = [] while (++rowIndex < cellMatrix.length) { const row = cellMatrix[rowIndex] const sizes = sizeMatrix[rowIndex] columnIndex = -1 /** @type {Array} */ const line = [] while (++columnIndex < mostCellsPerRow) { const cell = row[columnIndex] || '' let before = '' let after = '' if (settings.alignDelimiters !== false) { const size = longestCellByColumn[columnIndex] - (sizes[columnIndex] || 0) const code = alignments[columnIndex] if (code === 114 /* `r` */) { before = ' '.repeat(size) } else if (code === 99 /* `c` */) { if (size % 2) { before = ' '.repeat(size / 2 + 0.5) after = ' '.repeat(size / 2 - 0.5) } else { before = ' '.repeat(size / 2) after = before } } else { after = ' '.repeat(size) } } if (settings.delimiterStart !== false && !columnIndex) { line.push('|') } if ( settings.padding !== false && // Don’t add the opening space if we’re not aligning and the cell is // empty: there will be a closing space. !(settings.alignDelimiters === false && cell === '') && (settings.delimiterStart !== false || columnIndex) ) { line.push(' ') } if (settings.alignDelimiters !== false) { line.push(before) } line.push(cell) if (settings.alignDelimiters !== false) { line.push(after) } if (settings.padding !== false) { line.push(' ') } if ( settings.delimiterEnd !== false || columnIndex !== mostCellsPerRow - 1 ) { line.push('|') } } lines.push( settings.delimiterEnd === false ? line.join('').replace(/ +$/, '') : line.join('') ) } return lines.join('\n') } /** * @param {string | null | undefined} [value] * Value to serialize. * @returns {string} * Result. */ function serialize(value) { return value === null || value === undefined ? '' : String(value) } /** * @param {string | null | undefined} value * Value. * @returns {number} * Alignment. */ function toAlignment(value) { const code = typeof value === 'string' ? value.codePointAt(0) : 0 return code === 67 /* `C` */ || code === 99 /* `c` */ ? 99 /* `c` */ : code === 76 /* `L` */ || code === 108 /* `l` */ ? 108 /* `l` */ : code === 82 /* `R` */ || code === 114 /* `r` */ ? 114 /* `r` */ : 0 }