├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── index.d.ts ├── index.js ├── lib └── index.js ├── license ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .DS_Store 4 | *.d.ts 5 | *.log 6 | yarn.lock 7 | !/index.d.ts 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type {Options} from './lib/index.js' 2 | 3 | export {gfmTableFromMarkdown, gfmTableToMarkdown} from './lib/index.js' 4 | 5 | // Add custom data tracked to turn markdown into a tree. 6 | declare module 'mdast-util-from-markdown' { 7 | interface CompileData { 8 | /** 9 | * Whether we’re currently in a table. 10 | */ 11 | inTable?: boolean | undefined 12 | } 13 | } 14 | 15 | // Add custom data tracked to turn a syntax tree into markdown. 16 | declare module 'mdast-util-to-markdown' { 17 | interface ConstructNameMap { 18 | /** 19 | * Whole table. 20 | * 21 | * ```markdown 22 | * > | | a | 23 | * ^^^^^ 24 | * > | | - | 25 | * ^^^^^ 26 | * ``` 27 | */ 28 | table: 'table' 29 | 30 | /** 31 | * Table cell. 32 | * 33 | * ```markdown 34 | * > | | a | 35 | * ^^^^^ 36 | * | | - | 37 | * ``` 38 | */ 39 | tableCell: 'tableCell' 40 | 41 | /** 42 | * Table row. 43 | * 44 | * ```markdown 45 | * > | | a | 46 | * ^^^^^ 47 | * | | - | 48 | * ``` 49 | */ 50 | tableRow: 'tableRow' 51 | } 52 | } 53 | 54 | // Note: `Table` is exposed from `@types/mdast`. 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {gfmTableFromMarkdown, gfmTableToMarkdown} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('mdast').InlineCode} InlineCode 3 | * @typedef {import('mdast').Table} Table 4 | * @typedef {import('mdast').TableCell} TableCell 5 | * @typedef {import('mdast').TableRow} TableRow 6 | * 7 | * @typedef {import('markdown-table').Options} MarkdownTableOptions 8 | * 9 | * @typedef {import('mdast-util-from-markdown').CompileContext} CompileContext 10 | * @typedef {import('mdast-util-from-markdown').Extension} FromMarkdownExtension 11 | * @typedef {import('mdast-util-from-markdown').Handle} FromMarkdownHandle 12 | * 13 | * @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension 14 | * @typedef {import('mdast-util-to-markdown').Handle} ToMarkdownHandle 15 | * @typedef {import('mdast-util-to-markdown').State} State 16 | * @typedef {import('mdast-util-to-markdown').Info} Info 17 | */ 18 | 19 | /** 20 | * @typedef Options 21 | * Configuration. 22 | * @property {boolean | null | undefined} [tableCellPadding=true] 23 | * Whether to add a space of padding between delimiters and cells (default: 24 | * `true`). 25 | * @property {boolean | null | undefined} [tablePipeAlign=true] 26 | * Whether to align the delimiters (default: `true`). 27 | * @property {MarkdownTableOptions['stringLength'] | null | undefined} [stringLength] 28 | * Function to detect the length of table cell content, used when aligning 29 | * the delimiters between cells (optional). 30 | */ 31 | 32 | import {ok as assert} from 'devlop' 33 | import {markdownTable} from 'markdown-table' 34 | import {defaultHandlers} from 'mdast-util-to-markdown' 35 | 36 | /** 37 | * Create an extension for `mdast-util-from-markdown` to enable GFM tables in 38 | * markdown. 39 | * 40 | * @returns {FromMarkdownExtension} 41 | * Extension for `mdast-util-from-markdown` to enable GFM tables. 42 | */ 43 | export function gfmTableFromMarkdown() { 44 | return { 45 | enter: { 46 | table: enterTable, 47 | tableData: enterCell, 48 | tableHeader: enterCell, 49 | tableRow: enterRow 50 | }, 51 | exit: { 52 | codeText: exitCodeText, 53 | table: exitTable, 54 | tableData: exit, 55 | tableHeader: exit, 56 | tableRow: exit 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * @this {CompileContext} 63 | * @type {FromMarkdownHandle} 64 | */ 65 | function enterTable(token) { 66 | const align = token._align 67 | assert(align, 'expected `_align` on table') 68 | this.enter( 69 | { 70 | type: 'table', 71 | align: align.map(function (d) { 72 | return d === 'none' ? null : d 73 | }), 74 | children: [] 75 | }, 76 | token 77 | ) 78 | this.data.inTable = true 79 | } 80 | 81 | /** 82 | * @this {CompileContext} 83 | * @type {FromMarkdownHandle} 84 | */ 85 | function exitTable(token) { 86 | this.exit(token) 87 | this.data.inTable = undefined 88 | } 89 | 90 | /** 91 | * @this {CompileContext} 92 | * @type {FromMarkdownHandle} 93 | */ 94 | function enterRow(token) { 95 | this.enter({type: 'tableRow', children: []}, token) 96 | } 97 | 98 | /** 99 | * @this {CompileContext} 100 | * @type {FromMarkdownHandle} 101 | */ 102 | function exit(token) { 103 | this.exit(token) 104 | } 105 | 106 | /** 107 | * @this {CompileContext} 108 | * @type {FromMarkdownHandle} 109 | */ 110 | function enterCell(token) { 111 | this.enter({type: 'tableCell', children: []}, token) 112 | } 113 | 114 | // Overwrite the default code text data handler to unescape escaped pipes when 115 | // they are in tables. 116 | /** 117 | * @this {CompileContext} 118 | * @type {FromMarkdownHandle} 119 | */ 120 | function exitCodeText(token) { 121 | let value = this.resume() 122 | 123 | if (this.data.inTable) { 124 | value = value.replace(/\\([\\|])/g, replace) 125 | } 126 | 127 | const node = this.stack[this.stack.length - 1] 128 | assert(node.type === 'inlineCode') 129 | node.value = value 130 | this.exit(token) 131 | } 132 | 133 | /** 134 | * @param {string} $0 135 | * @param {string} $1 136 | * @returns {string} 137 | */ 138 | function replace($0, $1) { 139 | // Pipes work, backslashes don’t (but can’t escape pipes). 140 | return $1 === '|' ? $1 : $0 141 | } 142 | 143 | /** 144 | * Create an extension for `mdast-util-to-markdown` to enable GFM tables in 145 | * markdown. 146 | * 147 | * @param {Options | null | undefined} [options] 148 | * Configuration. 149 | * @returns {ToMarkdownExtension} 150 | * Extension for `mdast-util-to-markdown` to enable GFM tables. 151 | */ 152 | export function gfmTableToMarkdown(options) { 153 | const settings = options || {} 154 | const padding = settings.tableCellPadding 155 | const alignDelimiters = settings.tablePipeAlign 156 | const stringLength = settings.stringLength 157 | const around = padding ? ' ' : '|' 158 | 159 | return { 160 | unsafe: [ 161 | {character: '\r', inConstruct: 'tableCell'}, 162 | {character: '\n', inConstruct: 'tableCell'}, 163 | // A pipe, when followed by a tab or space (padding), or a dash or colon 164 | // (unpadded delimiter row), could result in a table. 165 | {atBreak: true, character: '|', after: '[\t :-]'}, 166 | // A pipe in a cell must be encoded. 167 | {character: '|', inConstruct: 'tableCell'}, 168 | // A colon must be followed by a dash, in which case it could start a 169 | // delimiter row. 170 | {atBreak: true, character: ':', after: '-'}, 171 | // A delimiter row can also start with a dash, when followed by more 172 | // dashes, a colon, or a pipe. 173 | // This is a stricter version than the built in check for lists, thematic 174 | // breaks, and setex heading underlines though: 175 | // 176 | {atBreak: true, character: '-', after: '[:|-]'} 177 | ], 178 | handlers: { 179 | inlineCode: inlineCodeWithTable, 180 | table: handleTable, 181 | tableCell: handleTableCell, 182 | tableRow: handleTableRow 183 | } 184 | } 185 | 186 | /** 187 | * @type {ToMarkdownHandle} 188 | * @param {Table} node 189 | */ 190 | function handleTable(node, _, state, info) { 191 | return serializeData(handleTableAsData(node, state, info), node.align) 192 | } 193 | 194 | /** 195 | * This function isn’t really used normally, because we handle rows at the 196 | * table level. 197 | * But, if someone passes in a table row, this ensures we make somewhat sense. 198 | * 199 | * @type {ToMarkdownHandle} 200 | * @param {TableRow} node 201 | */ 202 | function handleTableRow(node, _, state, info) { 203 | const row = handleTableRowAsData(node, state, info) 204 | const value = serializeData([row]) 205 | // `markdown-table` will always add an align row 206 | return value.slice(0, value.indexOf('\n')) 207 | } 208 | 209 | /** 210 | * @type {ToMarkdownHandle} 211 | * @param {TableCell} node 212 | */ 213 | function handleTableCell(node, _, state, info) { 214 | const exit = state.enter('tableCell') 215 | const subexit = state.enter('phrasing') 216 | const value = state.containerPhrasing(node, { 217 | ...info, 218 | before: around, 219 | after: around 220 | }) 221 | subexit() 222 | exit() 223 | return value 224 | } 225 | 226 | /** 227 | * @param {Array>} matrix 228 | * @param {Array | null | undefined} [align] 229 | */ 230 | function serializeData(matrix, align) { 231 | return markdownTable(matrix, { 232 | align, 233 | // @ts-expect-error: `markdown-table` types should support `null`. 234 | alignDelimiters, 235 | // @ts-expect-error: `markdown-table` types should support `null`. 236 | padding, 237 | // @ts-expect-error: `markdown-table` types should support `null`. 238 | stringLength 239 | }) 240 | } 241 | 242 | /** 243 | * @param {Table} node 244 | * @param {State} state 245 | * @param {Info} info 246 | */ 247 | function handleTableAsData(node, state, info) { 248 | const children = node.children 249 | let index = -1 250 | /** @type {Array>} */ 251 | const result = [] 252 | const subexit = state.enter('table') 253 | 254 | while (++index < children.length) { 255 | result[index] = handleTableRowAsData(children[index], state, info) 256 | } 257 | 258 | subexit() 259 | 260 | return result 261 | } 262 | 263 | /** 264 | * @param {TableRow} node 265 | * @param {State} state 266 | * @param {Info} info 267 | */ 268 | function handleTableRowAsData(node, state, info) { 269 | const children = node.children 270 | let index = -1 271 | /** @type {Array} */ 272 | const result = [] 273 | const subexit = state.enter('tableRow') 274 | 275 | while (++index < children.length) { 276 | // Note: the positional info as used here is incorrect. 277 | // Making it correct would be impossible due to aligning cells? 278 | // And it would need copy/pasting `markdown-table` into this project. 279 | result[index] = handleTableCell(children[index], node, state, info) 280 | } 281 | 282 | subexit() 283 | 284 | return result 285 | } 286 | 287 | /** 288 | * @type {ToMarkdownHandle} 289 | * @param {InlineCode} node 290 | */ 291 | function inlineCodeWithTable(node, parent, state) { 292 | let value = defaultHandlers.inlineCode(node, parent, state) 293 | 294 | if (state.stack.includes('tableCell')) { 295 | value = value.replace(/\|/g, '\\$&') 296 | } 297 | 298 | return value 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2020 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdast-util-gfm-table", 3 | "version": "2.0.0", 4 | "description": "mdast extension to parse and serialize GFM tables", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unist", 8 | "mdast", 9 | "mdast-util", 10 | "util", 11 | "utility", 12 | "markdown", 13 | "markup", 14 | "table", 15 | "row", 16 | "column", 17 | "cell", 18 | "tabular", 19 | "gfm" 20 | ], 21 | "repository": "syntax-tree/mdast-util-gfm-table", 22 | "bugs": "https://github.com/syntax-tree/mdast-util-gfm-table/issues", 23 | "funding": { 24 | "type": "opencollective", 25 | "url": "https://opencollective.com/unified" 26 | }, 27 | "author": "Titus Wormer (https://wooorm.com)", 28 | "contributors": [ 29 | "Titus Wormer (https://wooorm.com)" 30 | ], 31 | "sideEffects": false, 32 | "type": "module", 33 | "exports": "./index.js", 34 | "files": [ 35 | "lib/", 36 | "index.d.ts", 37 | "index.js" 38 | ], 39 | "dependencies": { 40 | "@types/mdast": "^4.0.0", 41 | "devlop": "^1.0.0", 42 | "markdown-table": "^3.0.0", 43 | "mdast-util-from-markdown": "^2.0.0", 44 | "mdast-util-to-markdown": "^2.0.0" 45 | }, 46 | "devDependencies": { 47 | "@types/node": "^20.0.0", 48 | "c8": "^8.0.0", 49 | "micromark-extension-gfm-table": "^2.0.0", 50 | "prettier": "^2.0.0", 51 | "remark-cli": "^11.0.0", 52 | "remark-preset-wooorm": "^9.0.0", 53 | "string-width": "^6.0.0", 54 | "type-coverage": "^2.0.0", 55 | "typescript": "^5.0.0", 56 | "unist-util-remove-position": "^5.0.0", 57 | "xo": "^0.54.0" 58 | }, 59 | "scripts": { 60 | "prepack": "npm run build && npm run format", 61 | "build": "tsc --build --clean && tsc --build && type-coverage", 62 | "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", 63 | "test-api-prod": "node --conditions production test.js", 64 | "test-api-dev": "node --conditions development test.js", 65 | "test-api": "npm run test-api-dev && npm run test-api-prod", 66 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 67 | "test": "npm run build && npm run format && npm run test-coverage" 68 | }, 69 | "prettier": { 70 | "bracketSpacing": false, 71 | "semi": false, 72 | "singleQuote": true, 73 | "tabWidth": 2, 74 | "trailingComma": "none", 75 | "useTabs": false 76 | }, 77 | "remarkConfig": { 78 | "plugins": [ 79 | "remark-preset-wooorm" 80 | ] 81 | }, 82 | "typeCoverage": { 83 | "atLeast": 100, 84 | "detail": true, 85 | "ignoreCatch": true, 86 | "strict": true 87 | }, 88 | "xo": { 89 | "overrides": [ 90 | { 91 | "files": [ 92 | "**/*.ts" 93 | ], 94 | "rules": { 95 | "@typescript-eslint/consistent-type-definitions": "off" 96 | } 97 | } 98 | ], 99 | "prettier": true 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mdast-util-gfm-table 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | [![Sponsors][sponsors-badge]][collective] 8 | [![Backers][backers-badge]][collective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | [mdast][] extensions to parse and serialize [GFM][] tables. 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When to use this](#when-to-use-this) 17 | * [Install](#install) 18 | * [Use](#use) 19 | * [API](#api) 20 | * [`gfmTableFromMarkdown`](#gfmtablefrommarkdown) 21 | * [`gfmTableToMarkdown(options?)`](#gfmtabletomarkdownoptions) 22 | * [`Options`](#options) 23 | * [Examples](#examples) 24 | * [Example: `stringLength`](#example-stringlength) 25 | * [HTML](#html) 26 | * [Syntax](#syntax) 27 | * [Syntax tree](#syntax-tree) 28 | * [Nodes](#nodes) 29 | * [Enumeration](#enumeration) 30 | * [Content model](#content-model) 31 | * [Types](#types) 32 | * [Compatibility](#compatibility) 33 | * [Related](#related) 34 | * [Contribute](#contribute) 35 | * [License](#license) 36 | 37 | ## What is this? 38 | 39 | This package contains two extensions that add support for GFM table syntax in 40 | markdown to [mdast][]. 41 | These extensions plug into 42 | [`mdast-util-from-markdown`][mdast-util-from-markdown] (to support parsing 43 | tables in markdown into a syntax tree) and 44 | [`mdast-util-to-markdown`][mdast-util-to-markdown] (to support serializing 45 | tables in syntax trees to markdown). 46 | 47 | ## When to use this 48 | 49 | You can use these extensions when you are working with 50 | `mdast-util-from-markdown` and `mdast-util-to-markdown` already. 51 | 52 | When working with `mdast-util-from-markdown`, you must combine this package 53 | with [`micromark-extension-gfm-table`][extension]. 54 | 55 | When you don’t need a syntax tree, you can use [`micromark`][micromark] 56 | directly with `micromark-extension-gfm-table`. 57 | 58 | When you are working with syntax trees and want all of GFM, use 59 | [`mdast-util-gfm`][mdast-util-gfm] instead. 60 | 61 | All these packages are used [`remark-gfm`][remark-gfm], which 62 | focusses on making it easier to transform content by abstracting these 63 | internals away. 64 | 65 | This utility does not handle how markdown is turned to HTML. 66 | That’s done by [`mdast-util-to-hast`][mdast-util-to-hast]. 67 | 68 | ## Install 69 | 70 | This package is [ESM only][esm]. 71 | In Node.js (version 16+), install with [npm][]: 72 | 73 | ```sh 74 | npm install mdast-util-gfm-table 75 | ``` 76 | 77 | In Deno with [`esm.sh`][esmsh]: 78 | 79 | ```js 80 | import {gfmTableFromMarkdown, gfmTableToMarkdown} from 'https://esm.sh/mdast-util-gfm-table@2' 81 | ``` 82 | 83 | In browsers with [`esm.sh`][esmsh]: 84 | 85 | ```html 86 | 89 | ``` 90 | 91 | ## Use 92 | 93 | Say our document `example.md` contains: 94 | 95 | ```markdown 96 | | a | b | c | d | 97 | | - | :- | -: | :-: | 98 | | e | f | 99 | | g | h | i | j | k | 100 | ``` 101 | 102 | …and our module `example.js` looks as follows: 103 | 104 | ```js 105 | import fs from 'node:fs/promises' 106 | import {gfmTable} from 'micromark-extension-gfm-table' 107 | import {fromMarkdown} from 'mdast-util-from-markdown' 108 | import {gfmTableFromMarkdown, gfmTableToMarkdown} from 'mdast-util-gfm-table' 109 | import {toMarkdown} from 'mdast-util-to-markdown' 110 | 111 | const doc = await fs.readFile('example.md') 112 | 113 | const tree = fromMarkdown(doc, { 114 | extensions: [gfmTable()], 115 | mdastExtensions: [gfmTableFromMarkdown()] 116 | }) 117 | 118 | console.log(tree) 119 | 120 | const out = toMarkdown(tree, {extensions: [gfmTableToMarkdown()]}) 121 | 122 | console.log(out) 123 | ``` 124 | 125 | …now running `node example.js` yields (positional info removed for brevity): 126 | 127 | ```js 128 | { 129 | type: 'root', 130 | children: [ 131 | { 132 | type: 'table', 133 | align: [null, 'left', 'right', 'center'], 134 | children: [ 135 | { 136 | type: 'tableRow', 137 | children: [ 138 | {type: 'tableCell', children: [{type: 'text', value: 'a'}]}, 139 | {type: 'tableCell', children: [{type: 'text', value: 'b'}]}, 140 | {type: 'tableCell', children: [{type: 'text', value: 'c'}]}, 141 | {type: 'tableCell', children: [{type: 'text', value: 'd'}]} 142 | ] 143 | }, 144 | { 145 | type: 'tableRow', 146 | children: [ 147 | {type: 'tableCell', children: [{type: 'text', value: 'e'}]}, 148 | {type: 'tableCell', children: [{type: 'text', value: 'f'}]} 149 | ] 150 | }, 151 | { 152 | type: 'tableRow', 153 | children: [ 154 | {type: 'tableCell', children: [{type: 'text', value: 'g'}]}, 155 | {type: 'tableCell', children: [{type: 'text', value: 'h'}]}, 156 | {type: 'tableCell', children: [{type: 'text', value: 'i'}]}, 157 | {type: 'tableCell', children: [{type: 'text', value: 'j'}]}, 158 | {type: 'tableCell', children: [{type: 'text', value: 'k'}]} 159 | ] 160 | } 161 | ] 162 | } 163 | ] 164 | } 165 | ``` 166 | 167 | ```markdown 168 | | a | b | c | d | | 169 | | - | :- | -: | :-: | - | 170 | | e | f | | | | 171 | | g | h | i | j | k | 172 | ``` 173 | 174 | ## API 175 | 176 | This package exports the identifiers 177 | [`gfmTableFromMarkdown`][api-gfm-table-from-markdown] and 178 | [`gfmTableToMarkdown`][api-gfm-table-to-markdown]. 179 | There is no default export. 180 | 181 | ### `gfmTableFromMarkdown` 182 | 183 | Create an extension for [`mdast-util-from-markdown`][mdast-util-from-markdown] 184 | to enable GFM tables in markdown. 185 | 186 | ###### Returns 187 | 188 | Extension for `mdast-util-from-markdown` to enable GFM tables 189 | ([`FromMarkdownExtension`][from-markdown-extension]). 190 | 191 | ### `gfmTableToMarkdown(options?)` 192 | 193 | Create an extension for [`mdast-util-to-markdown`][mdast-util-to-markdown] to 194 | enable GFM tables in markdown. 195 | 196 | ###### Parameters 197 | 198 | * `options` ([`Options`][api-options], optional) 199 | — configuration 200 | 201 | ###### Returns 202 | 203 | Extension for `mdast-util-to-markdown` to enable GFM tables 204 | ([`ToMarkdownExtension`][to-markdown-extension]). 205 | 206 | ### `Options` 207 | 208 | Configuration (TypeScript type). 209 | 210 | ###### Fields 211 | 212 | * `tableCellPadding` (`boolean`, default: `true`) 213 | — whether to add a space of padding between delimiters and cells 214 | * `tablePipeAlign` (`boolean`, default: `true`) 215 | — whether to align the delimiters 216 | * `stringLength` (`((value: string) => number)`, default: `s => s.length`) 217 | — function to detect the length of table cell content, used when aligning 218 | the delimiters between cells 219 | 220 | ## Examples 221 | 222 | ### Example: `stringLength` 223 | 224 | It’s possible to align tables based on the visual width of cells. 225 | First, let’s show the problem: 226 | 227 | ```js 228 | import {gfmTable} from 'micromark-extension-gfm-table' 229 | import {fromMarkdown} from 'mdast-util-from-markdown' 230 | import {gfmTableFromMarkdown, gfmTableToMarkdown} from 'mdast-util-gfm-table' 231 | import {toMarkdown} from 'mdast-util-to-markdown' 232 | 233 | const doc = `| Alpha | Bravo | 234 | | - | - | 235 | | 中文 | Charlie | 236 | | 👩‍❤️‍👩 | Delta |` 237 | 238 | const tree = fromMarkdown(doc, { 239 | extensions: [gfmTable], 240 | mdastExtensions: [gfmTableFromMarkdown] 241 | }) 242 | 243 | console.log(toMarkdown(tree, {extensions: [gfmTableToMarkdown()]})) 244 | ``` 245 | 246 | The above code shows how these utilities can be used to format markdown. 247 | The output is as follows: 248 | 249 | ```markdown 250 | | Alpha | Bravo | 251 | | -------- | ------- | 252 | | 中文 | Charlie | 253 | | 👩‍❤️‍👩 | Delta | 254 | ``` 255 | 256 | To improve the alignment of these full-width characters and emoji, pass a 257 | `stringLength` function that calculates the visual width of cells. 258 | One such algorithm is [`string-width`][string-width]. 259 | It can be used like so: 260 | 261 | ```diff 262 | @@ -2,6 +2,7 @@ import {gfmTable} from 'micromark-extension-gfm-table' 263 | import {fromMarkdown} from 'mdast-util-from-markdown' 264 | import {gfmTableFromMarkdown, gfmTableToMarkdown} from 'mdast-util-gfm-table' 265 | import {toMarkdown} from 'mdast-util-to-markdown' 266 | +import stringWidth from 'string-width' 267 | 268 | const doc = `| Alpha | Bravo | 269 | | - | - | 270 | @@ -13,4 +14,8 @@ const tree = fromMarkdown(doc, { 271 | mdastExtensions: [gfmTableFromMarkdown()] 272 | }) 273 | 274 | -console.log(toMarkdown(tree, {extensions: [gfmTableToMarkdown()]})) 275 | +console.log( 276 | + toMarkdown(tree, { 277 | + extensions: [gfmTableToMarkdown({stringLength: stringWidth})] 278 | + }) 279 | +) 280 | ``` 281 | 282 | The output of our code with these changes is as follows: 283 | 284 | ```markdown 285 | | Alpha | Bravo | 286 | | ----- | ------- | 287 | | 中文 | Charlie | 288 | | 👩‍❤️‍👩 | Delta | 289 | ``` 290 | 291 | ## HTML 292 | 293 | This utility does not handle how markdown is turned to HTML. 294 | That’s done by [`mdast-util-to-hast`][mdast-util-to-hast]. 295 | 296 | ## Syntax 297 | 298 | See [Syntax in `micromark-extension-gfm-table`][syntax]. 299 | 300 | ## Syntax tree 301 | 302 | The following interfaces are added to **[mdast][]** by this utility. 303 | 304 | ### Nodes 305 | 306 | #### `Table` 307 | 308 | ```idl 309 | interface Table <: Parent { 310 | type: 'table' 311 | align: [alignType]? 312 | children: [TableContent] 313 | } 314 | ``` 315 | 316 | **Table** (**[Parent][dfn-parent]**) represents two-dimensional data. 317 | 318 | **Table** can be used where **[flow][dfn-flow-content]** content is expected. 319 | Its content model is **[table][dfn-table-content]** content. 320 | 321 | The *[head][term-head]* of the node represents the labels of the columns. 322 | 323 | An `align` field can be present. 324 | If present, it must be a list of **[alignTypes][dfn-enum-align-type]**. 325 | It represents how cells in columns are aligned. 326 | 327 | For example, the following markdown: 328 | 329 | ```markdown 330 | | foo | bar | 331 | | :-- | :-: | 332 | | baz | qux | 333 | ``` 334 | 335 | Yields: 336 | 337 | ```js 338 | { 339 | type: 'table', 340 | align: ['left', 'center'], 341 | children: [ 342 | { 343 | type: 'tableRow', 344 | children: [ 345 | { 346 | type: 'tableCell', 347 | children: [{type: 'text', value: 'foo'}] 348 | }, 349 | { 350 | type: 'tableCell', 351 | children: [{type: 'text', value: 'bar'}] 352 | } 353 | ] 354 | }, 355 | { 356 | type: 'tableRow', 357 | children: [ 358 | { 359 | type: 'tableCell', 360 | children: [{type: 'text', value: 'baz'}] 361 | }, 362 | { 363 | type: 'tableCell', 364 | children: [{type: 'text', value: 'qux'}] 365 | } 366 | ] 367 | } 368 | ] 369 | } 370 | ``` 371 | 372 | #### `TableRow` 373 | 374 | ```idl 375 | interface TableRow <: Parent { 376 | type: "tableRow" 377 | children: [RowContent] 378 | } 379 | ``` 380 | 381 | **TableRow** (**[Parent][dfn-parent]**) represents a row of cells in a table. 382 | 383 | **TableRow** can be used where **[table][dfn-table-content]** content is 384 | expected. 385 | Its content model is **[row][dfn-row-content]** content. 386 | 387 | If the node is a *[head][term-head]*, it represents the labels of the columns 388 | for its parent **[Table][dfn-table]**. 389 | 390 | For an example, see **[Table][dfn-table]**. 391 | 392 | #### `TableCell` 393 | 394 | ```idl 395 | interface TableCell <: Parent { 396 | type: "tableCell" 397 | children: [PhrasingContent] 398 | } 399 | ``` 400 | 401 | **TableCell** (**[Parent][dfn-parent]**) represents a header cell in a 402 | **[Table][dfn-table]**, if its parent is a *[head][term-head]*, or a data 403 | cell otherwise. 404 | 405 | **TableCell** can be used where **[row][dfn-row-content]** content is expected. 406 | Its content model is **[phrasing][dfn-phrasing-content]** content excluding 407 | **[Break][dfn-break]** nodes. 408 | 409 | For an example, see **[Table][dfn-table]**. 410 | 411 | ### Enumeration 412 | 413 | #### `alignType` 414 | 415 | ```idl 416 | enum alignType { 417 | 'center' | 'left' | 'right' | null 418 | } 419 | ``` 420 | 421 | **alignType** represents how phrasing content is aligned 422 | ([\[CSSTEXT\]][css-text]). 423 | 424 | * **`'left'`**: See the [`left`][css-left] value of the `text-align` CSS 425 | property 426 | * **`'right'`**: See the [`right`][css-right] value of the `text-align` 427 | CSS property 428 | * **`'center'`**: See the [`center`][css-center] value of the `text-align` 429 | CSS property 430 | * **`null`**: phrasing content is aligned as defined by the host environment 431 | 432 | ### Content model 433 | 434 | #### `FlowContent` (GFM table) 435 | 436 | ```idl 437 | type FlowContentGfm = Table | FlowContent 438 | ``` 439 | 440 | #### `TableContent` 441 | 442 | ```idl 443 | type TableContent = TableRow 444 | ``` 445 | 446 | **Table** content represent the rows in a table. 447 | 448 | #### `RowContent` 449 | 450 | ```idl 451 | type RowContent = TableCell 452 | ``` 453 | 454 | **Row** content represent the cells in a row. 455 | 456 | ## Types 457 | 458 | This package is fully typed with [TypeScript][]. 459 | It exports the additional type [`Options`][api-options]. 460 | 461 | The `Table`, `TableRow`, and `TableCell` types of the mdast nodes are exposed 462 | from `@types/mdast`. 463 | 464 | ## Compatibility 465 | 466 | Projects maintained by the unified collective are compatible with maintained 467 | versions of Node.js. 468 | 469 | When we cut a new major release, we drop support for unmaintained versions of 470 | Node. 471 | This means we try to keep the current release line, `mdast-util-gfm-table@^2`, 472 | compatible with Node.js 16. 473 | 474 | This utility works with `mdast-util-from-markdown` version 2+ and 475 | `mdast-util-to-markdown` version 2+. 476 | 477 | ## Related 478 | 479 | * [`remarkjs/remark-gfm`][remark-gfm] 480 | — remark plugin to support GFM 481 | * [`syntax-tree/mdast-util-gfm`][mdast-util-gfm] 482 | — same but all of GFM (autolink literals, footnotes, strikethrough, tables, 483 | tasklists) 484 | * [`micromark/micromark-extension-gfm-table`][extension] 485 | — micromark extension to parse GFM tables 486 | 487 | ## Contribute 488 | 489 | See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for 490 | ways to get started. 491 | See [`support.md`][support] for ways to get help. 492 | 493 | This project has a [code of conduct][coc]. 494 | By interacting with this repository, organization, or community you agree to 495 | abide by its terms. 496 | 497 | ## License 498 | 499 | [MIT][license] © [Titus Wormer][author] 500 | 501 | 502 | 503 | [build-badge]: https://github.com/syntax-tree/mdast-util-gfm-table/workflows/main/badge.svg 504 | 505 | [build]: https://github.com/syntax-tree/mdast-util-gfm-table/actions 506 | 507 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/mdast-util-gfm-table.svg 508 | 509 | [coverage]: https://codecov.io/github/syntax-tree/mdast-util-gfm-table 510 | 511 | [downloads-badge]: https://img.shields.io/npm/dm/mdast-util-gfm-table.svg 512 | 513 | [downloads]: https://www.npmjs.com/package/mdast-util-gfm-table 514 | 515 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=mdast-util-gfm-table 516 | 517 | [size]: https://bundlejs.com/?q=mdast-util-gfm-table 518 | 519 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 520 | 521 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 522 | 523 | [collective]: https://opencollective.com/unified 524 | 525 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 526 | 527 | [chat]: https://github.com/syntax-tree/unist/discussions 528 | 529 | [npm]: https://docs.npmjs.com/cli/install 530 | 531 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 532 | 533 | [esmsh]: https://esm.sh 534 | 535 | [typescript]: https://www.typescriptlang.org 536 | 537 | [license]: license 538 | 539 | [author]: https://wooorm.com 540 | 541 | [health]: https://github.com/syntax-tree/.github 542 | 543 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 544 | 545 | [support]: https://github.com/syntax-tree/.github/blob/main/support.md 546 | 547 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 548 | 549 | [remark-gfm]: https://github.com/remarkjs/remark-gfm 550 | 551 | [mdast]: https://github.com/syntax-tree/mdast 552 | 553 | [mdast-util-gfm]: https://github.com/syntax-tree/mdast-util-gfm 554 | 555 | [mdast-util-from-markdown]: https://github.com/syntax-tree/mdast-util-from-markdown 556 | 557 | [mdast-util-to-markdown]: https://github.com/syntax-tree/mdast-util-to-markdown 558 | 559 | [mdast-util-to-hast]: https://github.com/syntax-tree/mdast-util-to-hast 560 | 561 | [micromark]: https://github.com/micromark/micromark 562 | 563 | [extension]: https://github.com/micromark/micromark-extension-gfm-table 564 | 565 | [syntax]: https://github.com/micromark/micromark-extension-gfm-table#syntax 566 | 567 | [gfm]: https://github.github.com/gfm/ 568 | 569 | [string-width]: https://github.com/sindresorhus/string-width 570 | 571 | [css-text]: https://drafts.csswg.org/css-text/ 572 | 573 | [css-left]: https://drafts.csswg.org/css-text/#valdef-text-align-left 574 | 575 | [css-right]: https://drafts.csswg.org/css-text/#valdef-text-align-right 576 | 577 | [css-center]: https://drafts.csswg.org/css-text/#valdef-text-align-center 578 | 579 | [term-head]: https://github.com/syntax-tree/unist#head 580 | 581 | [dfn-parent]: https://github.com/syntax-tree/mdast#parent 582 | 583 | [dfn-phrasing-content]: https://github.com/syntax-tree/mdast#phrasingcontent 584 | 585 | [dfn-break]: https://github.com/syntax-tree/mdast#break 586 | 587 | [from-markdown-extension]: https://github.com/syntax-tree/mdast-util-from-markdown#extension 588 | 589 | [to-markdown-extension]: https://github.com/syntax-tree/mdast-util-to-markdown#options 590 | 591 | [api-gfm-table-from-markdown]: #gfmtablefrommarkdown 592 | 593 | [api-gfm-table-to-markdown]: #gfmtabletomarkdownoptions 594 | 595 | [api-options]: #options 596 | 597 | [dfn-flow-content]: #flowcontent-gfm-table 598 | 599 | [dfn-table-content]: #tablecontent 600 | 601 | [dfn-enum-align-type]: #aligntype 602 | 603 | [dfn-row-content]: #rowcontent 604 | 605 | [dfn-table]: #table 606 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('mdast').Table} Table 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import test from 'node:test' 7 | import stringWidth from 'string-width' 8 | import {gfmTable} from 'micromark-extension-gfm-table' 9 | import {fromMarkdown} from 'mdast-util-from-markdown' 10 | import {gfmTableFromMarkdown, gfmTableToMarkdown} from 'mdast-util-gfm-table' 11 | import {toMarkdown} from 'mdast-util-to-markdown' 12 | import {removePosition} from 'unist-util-remove-position' 13 | 14 | test('core', async function (t) { 15 | await t.test('should expose the public api', async function () { 16 | assert.deepEqual(Object.keys(await import('mdast-util-gfm-table')).sort(), [ 17 | 'gfmTableFromMarkdown', 18 | 'gfmTableToMarkdown' 19 | ]) 20 | }) 21 | }) 22 | 23 | test('gfmTableFromMarkdown()', async function (t) { 24 | await t.test('should support tables', async function () { 25 | assert.deepEqual( 26 | fromMarkdown('| a\n| -', { 27 | extensions: [gfmTable()], 28 | mdastExtensions: [gfmTableFromMarkdown()] 29 | }), 30 | { 31 | type: 'root', 32 | children: [ 33 | { 34 | type: 'table', 35 | align: [null], 36 | children: [ 37 | { 38 | type: 'tableRow', 39 | children: [ 40 | { 41 | type: 'tableCell', 42 | children: [ 43 | { 44 | type: 'text', 45 | value: 'a', 46 | position: { 47 | start: {line: 1, column: 3, offset: 2}, 48 | end: {line: 1, column: 4, offset: 3} 49 | } 50 | } 51 | ], 52 | position: { 53 | start: {line: 1, column: 1, offset: 0}, 54 | end: {line: 1, column: 4, offset: 3} 55 | } 56 | } 57 | ], 58 | position: { 59 | start: {line: 1, column: 1, offset: 0}, 60 | end: {line: 1, column: 4, offset: 3} 61 | } 62 | } 63 | ], 64 | position: { 65 | start: {line: 1, column: 1, offset: 0}, 66 | end: {line: 2, column: 4, offset: 7} 67 | } 68 | } 69 | ], 70 | position: { 71 | start: {line: 1, column: 1, offset: 0}, 72 | end: {line: 2, column: 4, offset: 7} 73 | } 74 | } 75 | ) 76 | }) 77 | 78 | await t.test('should support alignment', async function () { 79 | assert.deepEqual( 80 | fromMarkdown('| a | b | c | d |\n| - | :- | -: | :-: |', { 81 | extensions: [gfmTable()], 82 | mdastExtensions: [gfmTableFromMarkdown()] 83 | }), 84 | { 85 | type: 'root', 86 | children: [ 87 | { 88 | type: 'table', 89 | align: [null, 'left', 'right', 'center'], 90 | children: [ 91 | { 92 | type: 'tableRow', 93 | children: [ 94 | { 95 | type: 'tableCell', 96 | children: [ 97 | { 98 | type: 'text', 99 | value: 'a', 100 | position: { 101 | start: {line: 1, column: 3, offset: 2}, 102 | end: {line: 1, column: 4, offset: 3} 103 | } 104 | } 105 | ], 106 | position: { 107 | start: {line: 1, column: 1, offset: 0}, 108 | end: {line: 1, column: 5, offset: 4} 109 | } 110 | }, 111 | { 112 | type: 'tableCell', 113 | children: [ 114 | { 115 | type: 'text', 116 | value: 'b', 117 | position: { 118 | start: {line: 1, column: 7, offset: 6}, 119 | end: {line: 1, column: 8, offset: 7} 120 | } 121 | } 122 | ], 123 | position: { 124 | start: {line: 1, column: 5, offset: 4}, 125 | end: {line: 1, column: 9, offset: 8} 126 | } 127 | }, 128 | { 129 | type: 'tableCell', 130 | children: [ 131 | { 132 | type: 'text', 133 | value: 'c', 134 | position: { 135 | start: {line: 1, column: 11, offset: 10}, 136 | end: {line: 1, column: 12, offset: 11} 137 | } 138 | } 139 | ], 140 | position: { 141 | start: {line: 1, column: 9, offset: 8}, 142 | end: {line: 1, column: 13, offset: 12} 143 | } 144 | }, 145 | { 146 | type: 'tableCell', 147 | children: [ 148 | { 149 | type: 'text', 150 | value: 'd', 151 | position: { 152 | start: {line: 1, column: 15, offset: 14}, 153 | end: {line: 1, column: 16, offset: 15} 154 | } 155 | } 156 | ], 157 | position: { 158 | start: {line: 1, column: 13, offset: 12}, 159 | end: {line: 1, column: 18, offset: 17} 160 | } 161 | } 162 | ], 163 | position: { 164 | start: {line: 1, column: 1, offset: 0}, 165 | end: {line: 1, column: 18, offset: 17} 166 | } 167 | } 168 | ], 169 | position: { 170 | start: {line: 1, column: 1, offset: 0}, 171 | end: {line: 2, column: 22, offset: 39} 172 | } 173 | } 174 | ], 175 | position: { 176 | start: {line: 1, column: 1, offset: 0}, 177 | end: {line: 2, column: 22, offset: 39} 178 | } 179 | } 180 | ) 181 | }) 182 | 183 | await t.test( 184 | 'should support an escaped pipe in code in a table cell', 185 | async function () { 186 | const tree = fromMarkdown('| `\\|` |\n | --- |', { 187 | extensions: [gfmTable()], 188 | mdastExtensions: [gfmTableFromMarkdown()] 189 | }) 190 | 191 | removePosition(tree, {force: true}) 192 | 193 | assert.deepEqual(tree, { 194 | type: 'root', 195 | children: [ 196 | { 197 | type: 'table', 198 | align: [null], 199 | children: [ 200 | { 201 | type: 'tableRow', 202 | children: [ 203 | { 204 | type: 'tableCell', 205 | children: [{type: 'inlineCode', value: '|'}] 206 | } 207 | ] 208 | } 209 | ] 210 | } 211 | ] 212 | }) 213 | } 214 | ) 215 | 216 | await t.test( 217 | 'should not support an escaped pipe in code *not* in a table cell', 218 | async function () { 219 | const tree = fromMarkdown('`\\|`', { 220 | extensions: [gfmTable()], 221 | mdastExtensions: [gfmTableFromMarkdown()] 222 | }) 223 | 224 | removePosition(tree, {force: true}) 225 | 226 | assert.deepEqual(tree, { 227 | type: 'root', 228 | children: [ 229 | {type: 'paragraph', children: [{type: 'inlineCode', value: '\\|'}]} 230 | ] 231 | }) 232 | } 233 | ) 234 | 235 | await t.test( 236 | 'should not support an escaped escape in code in a table cell', 237 | async function () { 238 | const tree = fromMarkdown('| `\\\\|`\\\\` b |\n | --- | --- |', { 239 | extensions: [gfmTable()], 240 | mdastExtensions: [gfmTableFromMarkdown()] 241 | }) 242 | 243 | removePosition(tree, {force: true}) 244 | 245 | assert.deepEqual(tree, { 246 | type: 'root', 247 | children: [ 248 | { 249 | type: 'table', 250 | align: [null, null], 251 | children: [ 252 | { 253 | type: 'tableRow', 254 | children: [ 255 | {type: 'tableCell', children: [{type: 'text', value: '`\\'}]}, 256 | { 257 | type: 'tableCell', 258 | children: [ 259 | {type: 'inlineCode', value: '\\\\'}, 260 | {type: 'text', value: ' b'} 261 | ] 262 | } 263 | ] 264 | } 265 | ] 266 | } 267 | ] 268 | }) 269 | } 270 | ) 271 | }) 272 | 273 | test('gfmTableToMarkdown', async function (t) { 274 | /** @type {Table} */ 275 | const minitable = { 276 | type: 'table', 277 | align: [null, 'left', 'center', 'right'], 278 | children: [ 279 | { 280 | type: 'tableRow', 281 | children: [ 282 | {type: 'tableCell', children: [{type: 'text', value: 'a'}]}, 283 | {type: 'tableCell', children: [{type: 'text', value: 'b'}]}, 284 | {type: 'tableCell', children: [{type: 'text', value: 'c'}]} 285 | ] 286 | } 287 | ] 288 | } 289 | 290 | const minitableDefault = toMarkdown(minitable, { 291 | extensions: [gfmTableToMarkdown()] 292 | }) 293 | 294 | await t.test('should serialize a table cell', async function () { 295 | assert.deepEqual( 296 | toMarkdown( 297 | { 298 | type: 'tableCell', 299 | children: [ 300 | {type: 'text', value: 'a '}, 301 | {type: 'emphasis', children: [{type: 'text', value: 'b'}]}, 302 | {type: 'text', value: ' c.'} 303 | ] 304 | }, 305 | {extensions: [gfmTableToMarkdown()]} 306 | ), 307 | 'a *b* c.\n' 308 | ) 309 | }) 310 | 311 | await t.test('should serialize a table row', async function () { 312 | assert.deepEqual( 313 | toMarkdown( 314 | { 315 | type: 'tableRow', 316 | children: [ 317 | {type: 'tableCell', children: [{type: 'text', value: 'a'}]}, 318 | { 319 | type: 'tableCell', 320 | children: [ 321 | {type: 'text', value: 'b '}, 322 | {type: 'emphasis', children: [{type: 'text', value: 'c'}]}, 323 | {type: 'text', value: ' d.'} 324 | ] 325 | } 326 | ] 327 | }, 328 | {extensions: [gfmTableToMarkdown()]} 329 | ), 330 | '| a | b *c* d. |\n' 331 | ) 332 | }) 333 | 334 | await t.test('should serialize a table', async function () { 335 | assert.deepEqual( 336 | toMarkdown( 337 | { 338 | type: 'table', 339 | children: [ 340 | { 341 | type: 'tableRow', 342 | children: [ 343 | {type: 'tableCell', children: [{type: 'text', value: 'a'}]}, 344 | { 345 | type: 'tableCell', 346 | children: [ 347 | {type: 'text', value: 'b '}, 348 | {type: 'emphasis', children: [{type: 'text', value: 'c'}]}, 349 | {type: 'text', value: ' d.'} 350 | ] 351 | } 352 | ] 353 | }, 354 | { 355 | type: 'tableRow', 356 | children: [ 357 | {type: 'tableCell', children: [{type: 'text', value: 'e'}]}, 358 | { 359 | type: 'tableCell', 360 | children: [{type: 'inlineCode', value: 'f'}] 361 | } 362 | ] 363 | } 364 | ] 365 | }, 366 | {extensions: [gfmTableToMarkdown()]} 367 | ), 368 | '| a | b *c* d. |\n| - | -------- |\n| e | `f` |\n' 369 | ) 370 | }) 371 | 372 | await t.test('should align cells', async function () { 373 | assert.deepEqual( 374 | toMarkdown( 375 | { 376 | type: 'table', 377 | align: [null, 'left', 'center', 'right'], 378 | children: [ 379 | { 380 | type: 'tableRow', 381 | children: [ 382 | {type: 'tableCell', children: [{type: 'text', value: 'a'}]}, 383 | {type: 'tableCell', children: [{type: 'text', value: 'b'}]}, 384 | {type: 'tableCell', children: [{type: 'text', value: 'c'}]}, 385 | {type: 'tableCell', children: [{type: 'text', value: 'd'}]} 386 | ] 387 | }, 388 | { 389 | type: 'tableRow', 390 | children: [ 391 | {type: 'tableCell', children: [{type: 'text', value: 'aaa'}]}, 392 | {type: 'tableCell', children: [{type: 'text', value: 'bbb'}]}, 393 | {type: 'tableCell', children: [{type: 'text', value: 'ccc'}]}, 394 | {type: 'tableCell', children: [{type: 'text', value: 'ddd'}]} 395 | ] 396 | } 397 | ] 398 | }, 399 | {extensions: [gfmTableToMarkdown()]} 400 | ), 401 | '| a | b | c | d |\n| --- | :-- | :-: | --: |\n| aaa | bbb | ccc | ddd |\n' 402 | ) 403 | }) 404 | 405 | await t.test('should support `tableCellPadding: false`', async function () { 406 | assert.deepEqual( 407 | toMarkdown(minitable, { 408 | extensions: [gfmTableToMarkdown({tableCellPadding: false})] 409 | }), 410 | '|a|b | c |\n|-|:-|:-:|\n' 411 | ) 412 | }) 413 | 414 | await t.test( 415 | 'should support `tableCellPadding: true` (default)', 416 | async function () { 417 | assert.deepEqual( 418 | toMarkdown(minitable, { 419 | extensions: [gfmTableToMarkdown({tableCellPadding: true})] 420 | }), 421 | minitableDefault 422 | ) 423 | } 424 | ) 425 | 426 | await t.test('should support `tablePipeAlign: false`', async function () { 427 | assert.deepEqual( 428 | toMarkdown(minitable, { 429 | extensions: [gfmTableToMarkdown({tablePipeAlign: false})] 430 | }), 431 | '| a | b | c |\n| - | :- | :-: |\n' 432 | ) 433 | }) 434 | 435 | await t.test( 436 | 'should support `tablePipeAlign: true` (default)', 437 | async function () { 438 | assert.deepEqual( 439 | toMarkdown(minitable, { 440 | extensions: [gfmTableToMarkdown({tablePipeAlign: true})] 441 | }), 442 | minitableDefault 443 | ) 444 | } 445 | ) 446 | 447 | await t.test('should support `stringLength`', async function () { 448 | assert.deepEqual( 449 | toMarkdown( 450 | { 451 | type: 'table', 452 | align: [], 453 | children: [ 454 | { 455 | type: 'tableRow', 456 | children: [ 457 | {type: 'tableCell', children: [{type: 'text', value: 'a'}]}, 458 | {type: 'tableCell', children: [{type: 'text', value: '古'}]}, 459 | {type: 'tableCell', children: [{type: 'text', value: '🤔'}]} 460 | ] 461 | } 462 | ] 463 | }, 464 | {extensions: [gfmTableToMarkdown({stringLength: stringWidth})]} 465 | ), 466 | '| a | 古 | 🤔 |\n| - | -- | -- |\n' 467 | ) 468 | }) 469 | 470 | await t.test( 471 | 'should escape the leading pipe in what would start or continue a table', 472 | async function () { 473 | assert.deepEqual( 474 | toMarkdown( 475 | { 476 | type: 'paragraph', 477 | children: [{type: 'text', value: '| a |\n| - |'}] 478 | }, 479 | {extensions: [gfmTableToMarkdown()]} 480 | ), 481 | '\\| a |\n\\| - |\n' 482 | ) 483 | } 484 | ) 485 | 486 | await t.test( 487 | 'should escape the leading dash in what could start a delimiter row (done by list dash)', 488 | async function () { 489 | assert.deepEqual( 490 | toMarkdown( 491 | {type: 'paragraph', children: [{type: 'text', value: 'a|\n-|'}]}, 492 | {extensions: [gfmTableToMarkdown()]} 493 | ), 494 | 'a|\n\\-|\n' 495 | ) 496 | } 497 | ) 498 | 499 | await t.test( 500 | 'should escape the leading colon in what could start a delimiter row', 501 | async function () { 502 | assert.deepEqual( 503 | toMarkdown( 504 | {type: 'paragraph', children: [{type: 'text', value: 'a\n:-'}]}, 505 | {extensions: [gfmTableToMarkdown()]} 506 | ), 507 | 'a\n\\:-\n' 508 | ) 509 | } 510 | ) 511 | 512 | await t.test( 513 | 'should not escape a backslash in code in a table cell', 514 | async function () { 515 | assert.deepEqual( 516 | toMarkdown( 517 | {type: 'tableCell', children: [{type: 'inlineCode', value: 'a\\b'}]}, 518 | {extensions: [gfmTableToMarkdown()]} 519 | ), 520 | '`a\\b`\n' 521 | ) 522 | } 523 | ) 524 | 525 | await t.test( 526 | 'should not escape an “escaped” backslash in code in a table cell', 527 | async function () { 528 | assert.deepEqual( 529 | toMarkdown( 530 | { 531 | type: 'tableCell', 532 | children: [{type: 'inlineCode', value: 'a\\\\b'}] 533 | }, 534 | {extensions: [gfmTableToMarkdown()]} 535 | ), 536 | '`a\\\\b`\n' 537 | ) 538 | } 539 | ) 540 | 541 | await t.test( 542 | 'should not escape an “escaped” other punctuation character in code in a table cell', 543 | async function () { 544 | assert.deepEqual( 545 | toMarkdown( 546 | {type: 'tableCell', children: [{type: 'inlineCode', value: 'a\\+b'}]}, 547 | {extensions: [gfmTableToMarkdown()]} 548 | ), 549 | '`a\\+b`\n' 550 | ) 551 | } 552 | ) 553 | 554 | await t.test( 555 | 'should not escape a pipe character in code *not* in a table cell', 556 | async function () { 557 | assert.deepEqual( 558 | toMarkdown( 559 | {type: 'inlineCode', value: 'a|b'}, 560 | {extensions: [gfmTableToMarkdown()]} 561 | ), 562 | '`a|b`\n' 563 | ) 564 | } 565 | ) 566 | 567 | await t.test( 568 | 'should escape a pipe character in code in a table cell', 569 | async function () { 570 | assert.deepEqual( 571 | toMarkdown( 572 | {type: 'tableCell', children: [{type: 'inlineCode', value: 'a|b'}]}, 573 | {extensions: [gfmTableToMarkdown()]} 574 | ), 575 | '`a\\|b`\n' 576 | ) 577 | } 578 | ) 579 | 580 | await t.test('should escape eols in a table cell', async function () { 581 | assert.deepEqual( 582 | toMarkdown( 583 | {type: 'tableCell', children: [{type: 'text', value: 'a\nb'}]}, 584 | {extensions: [gfmTableToMarkdown()]} 585 | ), 586 | 'a b\n' 587 | ) 588 | }) 589 | 590 | await t.test( 591 | 'should escape phrasing characters in table cells', 592 | async function () { 593 | assert.deepEqual( 594 | toMarkdown( 595 | { 596 | type: 'tableRow', 597 | children: [ 598 | {type: 'tableCell', children: [{type: 'text', value: ''}]}, 599 | {type: 'tableCell', children: [{type: 'text', value: '*a'}]}, 600 | {type: 'tableCell', children: [{type: 'text', value: '![]()'}]} 601 | ] 602 | }, 603 | {extensions: [gfmTableToMarkdown()]} 604 | ), 605 | '| \\ | \\*a | !\\[]\\() |\n' 606 | ) 607 | } 608 | ) 609 | 610 | await t.test('should escape pipes in a table cell', async function () { 611 | assert.deepEqual( 612 | toMarkdown( 613 | {type: 'tableCell', children: [{type: 'text', value: 'a|b'}]}, 614 | {extensions: [gfmTableToMarkdown()]} 615 | ), 616 | 'a\\|b\n' 617 | ) 618 | }) 619 | 620 | await t.test( 621 | 'should escape multiple pipes in inline code in a table cell', 622 | async function () { 623 | assert.deepEqual( 624 | toMarkdown( 625 | {type: 'tableCell', children: [{type: 'inlineCode', value: 'a|b|c'}]}, 626 | {extensions: [gfmTableToMarkdown()]} 627 | ), 628 | '`a\\|b\\|c`\n' 629 | ) 630 | } 631 | ) 632 | 633 | await t.test( 634 | 'should escape multiple pipes in a table cell', 635 | async function () { 636 | assert.deepEqual( 637 | toMarkdown( 638 | {type: 'tableCell', children: [{type: 'text', value: 'a|b|c'}]}, 639 | {extensions: [gfmTableToMarkdown()]} 640 | ), 641 | 'a\\|b\\|c\n' 642 | ) 643 | } 644 | ) 645 | 646 | await t.test( 647 | 'should escape adjacent pipes in a table cell', 648 | async function () { 649 | assert.deepEqual( 650 | toMarkdown( 651 | {type: 'tableCell', children: [{type: 'inlineCode', value: 'a||b'}]}, 652 | {extensions: [gfmTableToMarkdown()]} 653 | ), 654 | '`a\\|\\|b`\n' 655 | ) 656 | assert.deepEqual( 657 | toMarkdown( 658 | {type: 'tableCell', children: [{type: 'text', value: 'a||b'}]}, 659 | {extensions: [gfmTableToMarkdown()]} 660 | ), 661 | 'a\\|\\|b\n' 662 | ) 663 | } 664 | ) 665 | }) 666 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "exactOptionalPropertyTypes": true, 8 | "lib": ["es2022"], 9 | "module": "node16", 10 | "strict": true, 11 | "target": "es2022" 12 | }, 13 | "exclude": ["coverage/", "node_modules/"], 14 | "include": ["**/*.js", "index.d.ts"] 15 | } 16 | --------------------------------------------------------------------------------