remark-lint-table-pipes
Version:
remark-lint rule to warn when table rows are not fenced with pipes
277 lines (251 loc) • 8.17 kB
JavaScript
/**
* remark-lint rule to warn when GFM table rows have no initial or
* final cell delimiter.
*
* ## What is this?
*
* This package checks that table rows have initial and final delimiters.
* Tables are a GFM feature enabled with [`remark-gfm`][github-remark-gfm].
*
* ## When should I use this?
*
* You can use this package to check that tables are consistent.
*
* ## API
*
* ### `unified().use(remarkLintTablePipes)`
*
* Warn when GFM table rows have no initial or final cell delimiter.
*
* ###### Parameters
*
* There are no options.
*
* ###### Returns
*
* Transform ([`Transformer` from `unified`][github-unified-transformer]).
*
* ## Recommendation
*
* While tables don’t require initial or final delimiters (the pipes before the
* first and after the last cells in a row),
* it arguably does look weird without.
*
* ## Fix
*
* [`remark-stringify`][github-remark-stringify] with
* [`remark-gfm`][github-remark-gfm] formats all tables with initial and final
* delimiters.
*
* [api-remark-lint-table-pipes]: #unifieduseremarklinttablepipes
* [github-remark-gfm]: https://github.com/remarkjs/remark-gfm
* [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify
* [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer
*
* @module table-pipes
* @author Titus Wormer
* @copyright Titus Wormer
* @license MIT
*
* @example
* {"name": "ok.md", "gfm": true}
*
* Small table:
*
* | Planet | Mean anomaly (°) |
* | :- | -: |
* | Mercury | 174 796 |
*
* @example
* {"name": "not-ok.md", "label": "input", "gfm": true}
*
* Planet | Mean anomaly (°)
* :- | -:
* Mercury | 174 796
* @example
* {"name": "not-ok.md", "label": "output", "gfm": true}
*
* 1:1: Unexpected missing closing pipe in row, expected `|`
* 1:26: Unexpected missing opening pipe in row, expected `|`
* 2:1: Unexpected missing closing pipe in row, expected `|`
* 2:8: Unexpected missing opening pipe in row, expected `|`
* 3:1: Unexpected missing closing pipe in row, expected `|`
* 3:18: Unexpected missing opening pipe in row, expected `|`
*
* @example
* {"gfm": true, "label": "input", "name": "missing-cells.md"}
*
* Planet | Symbol | Satellites
* :- | - | -
* Mercury
* Venus | ♀
* Earth | ♁ | 1
* Mars | ♂ | 2 | 19 412
* @example
* {"gfm": true, "label": "output", "name": "missing-cells.md"}
*
* 1:1: Unexpected missing closing pipe in row, expected `|`
* 1:29: Unexpected missing opening pipe in row, expected `|`
* 2:1: Unexpected missing closing pipe in row, expected `|`
* 2:11: Unexpected missing opening pipe in row, expected `|`
* 3:1: Unexpected missing closing pipe in row, expected `|`
* 3:8: Unexpected missing opening pipe in row, expected `|`
* 4:1: Unexpected missing closing pipe in row, expected `|`
* 4:10: Unexpected missing opening pipe in row, expected `|`
* 5:1: Unexpected missing closing pipe in row, expected `|`
* 5:14: Unexpected missing opening pipe in row, expected `|`
* 6:1: Unexpected missing closing pipe in row, expected `|`
* 6:22: Unexpected missing opening pipe in row, expected `|`
*
* @example
* {"gfm": true, "label": "input", "name": "trailing-spaces.md"}
*
* ␠␠Planet␠␠
* ␠-:␠
*
* ␠␠| Planet |␠␠
* ␠| -: |␠
* @example
* {"gfm": true, "label": "output", "name": "trailing-spaces.md"}
*
* 1:3: Unexpected missing closing pipe in row, expected `|`
* 1:11: Unexpected missing opening pipe in row, expected `|`
* 2:2: Unexpected missing closing pipe in row, expected `|`
* 2:5: Unexpected missing opening pipe in row, expected `|`
*
* @example
* {"gfm": true, "label": "input", "name": "windows.md"}
*
* Mercury␍␊:-␍␊None
* @example
* {"gfm": true, "label": "output", "name": "windows.md"}
*
* 1:1: Unexpected missing closing pipe in row, expected `|`
* 1:8: Unexpected missing opening pipe in row, expected `|`
* 2:1: Unexpected missing closing pipe in row, expected `|`
* 2:3: Unexpected missing opening pipe in row, expected `|`
* 3:1: Unexpected missing closing pipe in row, expected `|`
* 3:5: Unexpected missing opening pipe in row, expected `|`
*/
/**
* @import {Nodes, Root} from 'mdast'
* @import {Point} from 'unist'
*/
import {phrasing} from 'mdast-util-phrasing'
import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position'
import {SKIP, visitParents} from 'unist-util-visit-parents'
const remarkLintTablePipes = lintRule(
{
origin: 'remark-lint:table-pipes',
url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-table-pipes#readme'
},
/**
* @param {Root} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
function (tree, file) {
const value = String(file)
visitParents(tree, function (node, parents) {
// Do not walk into phrasing.
if (phrasing(node)) {
return SKIP
}
if (node.type !== 'table') return
let index = -1
while (++index < node.children.length) {
const row = node.children[index]
const start = pointStart(row)
const end = pointEnd(row)
if (start && typeof start.offset === 'number') {
checkStart(start.offset, start, [...parents, node, row])
}
if (end && typeof end.offset === 'number') {
checkEnd(end.offset, end, [...parents, node, row])
// Align row.
if (index === 0) {
let index = end.offset
if (value.charCodeAt(index) === 13 /* `\r` */) index++
/* c8 ignore next -- should never happen, alignment is needed. */
if (value.charCodeAt(index) !== 10 /* `\n` */) continue
index++
const lineStart = index
// Alignment row can only be on the second line,
// so containers can only indent with `>` or spaces.
let code = value.charCodeAt(index)
while (
code === 9 /* `\t` */ ||
code === 32 /* ` ` */ ||
code === 62 /* `>` */
) {
index++
code = value.charCodeAt(index)
}
checkStart(
index,
{
line: end.line + 1,
column: index - lineStart + 1,
offset: index
},
[...parents, node]
)
index = value.indexOf('\n', index)
if (index === -1) index = value.length
if (value.charCodeAt(index - 1) === 13 /* `\r` */) index--
checkEnd(
index,
{
line: end.line + 1,
column: index - lineStart + 1,
offset: index
},
[...parents, node]
)
}
}
}
// No tables in tables.
return SKIP
})
/**
* @param {number} index
* @param {Point} place
* @param {Array<Nodes>} ancestors
*/
function checkStart(index, place, ancestors) {
let code = value.charCodeAt(index)
/* c8 ignore next 3 -- parser currently places indent outside. */
while (code === 9 /* `\t` */ || code === 32 /* ` ` */) {
code = value.charCodeAt(++index)
}
if (code !== 124 /* `|` */) {
file.message('Unexpected missing closing pipe in row, expected `|`', {
ancestors,
place
})
}
}
/**
* @param {number} index
* @param {Point} place
* @param {Array<Nodes>} ancestors
*/
function checkEnd(index, place, ancestors) {
let code = value.charCodeAt(index - 1)
while (code === 9 /* `\t` */ || code === 32 /* ` ` */) {
index--
code = value.charCodeAt(index - 1)
}
if (code !== 124 /* `|` */) {
file.message('Unexpected missing opening pipe in row, expected `|`', {
ancestors,
place
})
}
}
}
)
export default remarkLintTablePipes