remark-lint-table-cell-padding
Version:
remark-lint rule to warn when table cells are incorrectly padded
892 lines (833 loc) • 32.8 kB
JavaScript
/**
* remark-lint rule to warn when GFM table cells are padded inconsistently.
*
* ## What is this?
*
* This package checks table cell padding.
* 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(remarkLintTableCellPadding[, options])`
*
* Warn when GFM table cells are padded inconsistently.
*
* ###### Parameters
*
* * `options`
* ([`Options`][api-options], [`Style`][api-style], or `'consistent'`,
* default: `'consistent'`)
* — configuration
*
* ###### Returns
*
* Transform ([`Transformer` from `unified`][github-unified-transformer]).
*
* ### `Options`
*
* Configuration (TypeScript type).
*
* ###### Properties
*
* * `stringLength` (`(value: string) => number`, optional)
* — function to detect cell size
* * `style` ([`Style`][api-style] or `'consistent'`, optional)
* — preferred style or whether to detect the first style
*
* ### `Style`
*
* Style (TypeScript type).
*
* * `'compact'`
* — prefer zero spaces between pipes and content
* * `'padded'`
* — prefer at least one space between pipes and content
*
* ###### Type
*
* ```ts
* type Style = 'compact' | 'padded'
* ```
*
* ## Recommendation
*
* It’s recommended to use at least one space between pipes and content for
* legibility of the markup (`'padded'`).
*
* ## Fix
*
* [`remark-stringify`][github-remark-stringify] with
* [`remark-gfm`][github-remark-gfm] formats all table cells as padded by
* default.
* Pass `tableCellPadding: false` to use a more compact style.
*
* Aligning perfectly in all cases is not possible because whether characters
* look aligned or not depends on where the markup is shown.
* Some characters (such as emoji or Chinese characters) show smaller or bigger
* in different places.
* You can pass a `stringLength` function to `remark-gfm`,
* to align better for your use case,
* in which case this rule must be configured with the same `stringLength`.
*
* [api-options]: #options
* [api-remark-lint-table-cell-padding]: #unifieduseremarklinttablecellpadding-options
* [api-style]: #style
* [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-cell-padding
* @author Titus Wormer
* @copyright Titus Wormer
* @license MIT
*
* @example
* {"config": "padded", "gfm": true, "name": "ok.md"}
*
* | Planet | Symbol | Satellites | Mean anomaly (°) |
* | ------- | :----- | :--------: | ---------------: |
* | Mercury | ☿ | None | 174 796 |
*
* | Planet | Symbol | Satellites | Mean anomaly (°) |
* | - | :- | :-: | -: |
* | Venus | ♀ | None | 50 115 |
*
* @example
* {"config": "padded", "gfm": true, "label": "input", "name": "not-ok.md"}
*
* | Planet |
* | -------|
* | Mercury|
*
* |Planet |
* |------ |
* |Venus |
*
* | Planet |
* | ------ |
* | Venus |
* @example
* {"config": "padded", "gfm": true, "label": "output", "name": "not-ok.md"}
*
* 2:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 3:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 5:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 6:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 7:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 9:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space
* 9:12: Unexpected `2` spaces between cell content and edge, expected `1` space, remove `1` space
* 10:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space
* 10:12: Unexpected `2` spaces between cell content and edge, expected `1` space, remove `1` space
* 11:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space
* 11:12: Unexpected `3` spaces between cell content and edge, expected between `1` (unaligned) and `2` (aligned) spaces, remove between `1` and `2` spaces
*
* @example
* {"config": "compact", "gfm": true, "name": "ok.md"}
*
* |Planet |Symbol|Satellites|Mean anomaly (°)|
* |-------|:-----|:--------:|---------------:|
* |Mercury|☿ | None | 174 796|
*
* |Planet|Symbol|Satellites|Mean anomaly (°)|
* |-|:-|:-:|-:|
* |Venus|♀|None|50 115|
*
* @example
* {"config": "compact", "gfm": true, "label": "input", "name": "not-ok.md"}
*
* | Planet |
* | -------|
* | Mercury|
*
* |Planet |
* |------ |
* |Venus |
*
* | Planet |
* | ------ |
* | Venus |
* @example
* {"config": "compact", "gfm": true, "label": "output", "name": "not-ok.md"}
*
* 1:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 2:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 3:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 5:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 6:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 7:9: Unexpected `2` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `1` and `2` spaces
* 9:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces
* 9:12: Unexpected `2` spaces between cell content and edge, expected `0` spaces, remove `2` spaces
* 10:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces
* 10:12: Unexpected `2` spaces between cell content and edge, expected `0` spaces, remove `2` spaces
* 11:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces
* 11:12: Unexpected `3` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces
*
* @example
* {"gfm": true, "name": "consistent-padded-ok.md"}
*
* | Planet |
* | - |
*
* @example
* {"gfm": true, "label": "input", "name": "consistent-padded-nok.md"}
*
* | Planet|
* | - |
* @example
* {"gfm": true, "label": "output", "name": "consistent-padded-nok.md"}
*
* 1:9: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
*
* @example
* {"gfm": true, "name": "consistent-compact-ok.md"}
*
* |Planet|
* |-|
*
* @example
* {"gfm": true, "label": "input", "name": "consistent-compact-nok.md"}
*
* |Planet |
* |-|
* @example
* {"gfm": true, "label": "output", "name": "consistent-compact-nok.md"}
*
* 1:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
*
* @example
* {"gfm": true, "name": "empty.md"}
*
* | | Satellites |
* | - | - |
* | Mercury | |
*
* @example
* {"config": "compact", "gfm": true, "name": "string-length-default.md"}
*
* |Alpha|Bravo |
* |-----|-------|
* |冥王星 |Charlie|
* |🪐 |Delta |
*
* @example
* {"config": {"style": "compact", "stringLength": "__STRING_WIDTH__"}, "gfm": true, "name": "string-length-custom.md"}
*
* |Alpha|Bravo |
* |-----|-------|
* |冥王星|Charlie|
* |🪐 |Delta |
*
* @example
* {"gfm": true, "name": "missing-cells.md"}
*
* | Planet | Symbol | Satellites |
* | - | - | - |
* | Mercury |
* | Venus | ♀ |
* | Earth | 🜨 and ♁ | 1 |
* | Mars | ♂ | 2 | 19 412 |
*
* @example
* {"config": "padded", "gfm": true, "label": "input", "name": "missing-fences.md"}
*
* ␠Planet|Symbol|Satellites
* ------:|:-----|----------
* Mercury|☿ |0
*
* Planet|Symbol
* -----:|------
* ␠Venus|♀
* @example
* {"config": "padded", "gfm": true, "label": "output", "name": "missing-fences.md"}
*
* 1:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 1:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 1:15: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 1:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 2:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 2:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 2:15: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 2:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 3:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 3:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 3:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 5:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 5:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 6:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 6:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 7:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 7:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
*
* @example
* {"config": "compact", "gfm": true, "label": "input", "name": "missing-fences.md"}
*
* Planet | Symbol | Satellites
* -: | - | -
* Mercury | ☿ | 0
*
* Planet | Symbol
* -----: | ------
* ␠Venus | ♀
* @example
* {"config": "compact", "gfm": true, "label": "output", "name": "missing-fences.md"}
*
* 1:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 1:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 1:17: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 1:19: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 2:4: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 2:6: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 2:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 3:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 3:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 3:15: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 5:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 5:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 6:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 6:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 7:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 7:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
*
* @example
* {"config": "compact", "gfm": true, "label": "input", "name": "trailing-spaces.md"}
*
* Planet | Symbol␠
* -: | -␠
* Mercury | ☿␠␠
*
* | Planet | Symbol |␠
* | ------ | ------ |␠
* | Venus | ♀ |␠␠
* @example
* {"config": "compact", "gfm": true, "label": "output", "name": "trailing-spaces.md"}
*
* 1:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 1:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 2:4: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 2:6: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 3:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 3:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 5:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 5:10: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 5:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 5:19: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 6:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 6:10: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 6:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 6:19: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 7:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 7:10: Unexpected `2` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `1` and `2` spaces
* 7:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 7:19: Unexpected `6` spaces between cell content and edge, expected between `0` (unaligned) and `5` (aligned) spaces, remove between `1` and `6` spaces
*
* @example
* {"config": "compact", "gfm": true, "label": "input", "name": "nothing.md"}
*
* | | | |
* | - | - | - |
* | | | |
* @example
* {"config": "compact", "gfm": true, "label": "output", "name": "nothing.md"}
*
* 1:5: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces
* 1:9: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces
* 1:13: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces
* 2:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 2:5: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 2:7: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 2:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 2:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space
* 2:13: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space
* 3:5: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces
* 3:9: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces
* 3:13: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces
*
* @example
* {"config": "padded", "gfm": true, "label": "input", "name": "nothing.md"}
*
* ||||
* |-|-|-|
* ||||
* @example
* {"config": "padded", "gfm": true, "label": "output", "name": "nothing.md"}
*
* 1:2: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space
* 1:3: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space
* 1:4: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space
* 2:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 2:3: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 2:4: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 2:5: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 2:6: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 2:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 3:2: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space
* 3:3: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space
* 3:4: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space
*
* @example
* {"config": "padded", "gfm": true, "label": "input", "name": "more-weirdness.md"}
*
* Mercury
* |-
*
* Venus
* -|
* @example
* {"config": "padded", "gfm": true, "label": "output", "name": "more-weirdness.md"}
*
* 2:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space
* 5:2: Unexpected `0` spaces between cell content and edge, expected between `1` (unaligned) and `5` (aligned) spaces, add between `5` and `1` space
*
* @example
* {"config": "padded", "gfm": true, "label": "input", "name": "containers.md"}
*
* > | Mercury|
* > | - |
*
* * | Venus|
* | - |
*
* > * > | Earth|
* > > | - |
* @example
* {"config": "padded", "gfm": true, "label": "output", "name": "containers.md"}
*
* 1:12: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 4:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
* 7:14: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
*
* @example
* {"config": "padded", "gfm": true, "label": "input", "name": "windows.md"}
*
* | Mercury|␍␊| --- |␍␊| None |
* @example
* {"config": "padded", "gfm": true, "label": "output", "name": "windows.md"}
*
* 1:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space
*
* @example
* {"config": "🌍", "gfm": true, "label": "output", "name": "not-ok.md", "positionless": true}
*
* 1:1: Unexpected value `🌍` for `style`, expected `'compact'`, `'padded'`, or `'consistent'`
*/
/**
* @import {AlignType, Nodes, Root} from 'mdast'
* @import {Point} from 'unist'
*/
/**
* @typedef Options
* Configuration.
* @property {((value: string) => number) | null | undefined} [stringLength]
* Function to detect cell size (optional).
* @property {Style | 'consistent' | null | undefined} [style='consistent']
* Preferred style or whether to detect the first style (default: `'consistent'`).
*
* @typedef {'compact' | 'padded'} Style
* Styles.
*/
import {ok as assert} from 'devlop'
import {phrasing} from 'mdast-util-phrasing'
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position'
import {SKIP, visitParents} from 'unist-util-visit-parents'
import {VFileMessage} from 'vfile-message'
const remarkLintTableCellPadding = lintRule(
{
origin: 'remark-lint:table-cell-padding',
url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-table-cell-padding#readme'
},
/**
* @param {Root} tree
* Tree.
* @param {Options | Style | 'consistent' | null | undefined} [options='consistent']
* Configuration (default: `'consistent'`).
* @returns {undefined}
* Nothing.
*/
function (tree, file, options) {
/**
* @typedef Entry
* @property {AlignType} align
* @property {Array<Nodes>} ancestors
* @property {number} column
* @property {Size | undefined} size
*
* @typedef Size
* @property {number | undefined} left
* @property {Point} leftPoint
* @property {number} middle
* @property {number | undefined} right
* @property {Point} rightPoint
*/
const value = String(file)
/** @type {Style | undefined} */
let expected
/** @type {VFileMessage | undefined} */
let cause
/** @type {Options['stringLength']} */
let stringLength
/** @type {Options['style']} */
let style
if (options && typeof options === 'object') {
stringLength = options.stringLength
style = options.style
} else {
style = options
}
if (style === null || style === undefined || style === 'consistent') {
// Empty.
} else if (style === 'compact' || style === 'padded') {
expected = style
} else {
file.fail(
'Unexpected value `' +
style +
"` for `style`, expected `'compact'`, `'padded'`, or `'consistent'`"
)
}
visitParents(tree, function (table, parents) {
// Do not walk into phrasing.
if (phrasing(table)) {
return SKIP
}
if (table.type !== 'table') return
const entries = inferTable([...parents, table])
// Find max column sizes.
/** @type {Array<number>} */
const sizes = []
for (const entry of entries) {
if (
entry.size &&
(sizes[entry.column] === undefined ||
entry.size.middle > sizes[entry.column])
) {
sizes[entry.column] = entry.size.middle
}
}
// Find the first cell that is the biggest in its column.
if (!expected) {
for (const info of entries) {
if (
info.size &&
info.size.middle &&
info.size.middle === sizes[info.column]
) {
const node = info.ancestors.at(-1)
assert(node) // Always defined.
expected = info.size.left ? 'padded' : 'compact'
cause = new VFileMessage(
"Cell padding style `'" +
expected +
"'` first defined for `'consistent'` here",
{
ancestors: info.ancestors,
place: node.position,
ruleId: 'table-cell-padding',
source: 'remark-lint'
}
)
}
}
}
/* c8 ignore next -- always a cell. */
if (!expected) return
for (const info of entries) {
checkSide('left', info, sizes)
checkSide('right', info, sizes)
}
// No tables in tables.
return SKIP
})
/**
* @param {'left' | 'right'} side
* Side to check.
* @param {Entry} info
* Info.
* @param {Array<number>} sizes
* Max column sizes.
* @returns {undefined}
* Nothing.
*/
function checkSide(side, info, sizes) {
if (!info.size) {
return
}
const actual = info.size[side]
if (actual === undefined) {
return
}
const alignSpaces = sizes[info.column] - info.size.middle
const min = expected === 'compact' ? 0 : 1
/** @type {number} */
let max = min
if (info.align === 'center') {
max += Math.ceil(alignSpaces / 2)
} else if (info.align === 'right' ? side === 'left' : side === 'right') {
max += alignSpaces
}
// For empty cells,
// the `left` field is used for all the whitespace in them.
if (info.size.middle === 0) {
if (side === 'right') return
max = Math.max(max, sizes[info.column] + 2 * min)
}
if (actual < min || actual > max) {
const differenceMin = min - actual
const differenceMinAbsolute = Math.abs(differenceMin)
const differenceMax = max - actual
const differenceMaxAbsolute = Math.abs(differenceMax)
file.message(
'Unexpected `' +
actual +
'` ' +
pluralize('space', actual) +
' between cell ' +
(side === 'left' ? 'edge' : 'content') +
' and ' +
(side === 'left' ? 'content' : 'edge') +
', expected ' +
(min === max ? '' : 'between `' + min + '` (unaligned) and ') +
'`' +
max +
'` ' +
(min === max ? '' : '(aligned) ') +
pluralize('space', max) +
', ' +
(differenceMin < 0 ? 'remove' : 'add') +
(differenceMin === differenceMax
? ''
: ' between `' + differenceMaxAbsolute + '` and') +
' `' +
differenceMinAbsolute +
'` ' +
pluralize('space', differenceMinAbsolute),
{
ancestors: info.ancestors,
cause,
place: side === 'left' ? info.size.leftPoint : info.size.rightPoint
}
)
}
}
// Note: this code is also in `remark-lint-table-pipe-alignment`.
/**
* Get info about cells in a table.
*
* @param {Array<Nodes>} ancestors
* Ancestors.
* @returns {Array<Entry>}
* Entries.
*/
function inferTable(ancestors) {
const node = ancestors.at(-1)
assert(node) // Always defined.
assert(node.type === 'table') // Always table.
/* c8 ignore next -- `align` is optional in AST. */
const align = node.align || []
/** @type {Array<Entry>} */
const result = []
let rowIndex = -1
// Regular rows.
while (++rowIndex < node.children.length) {
const row = node.children[rowIndex]
let column = -1
while (++column < row.children.length) {
const node = row.children[column]
result.push({
align: align[column],
ancestors: [...ancestors, row, node],
column,
size: inferSize(
pointStart(node),
pointEnd(node),
column === row.children.length - 1
)
})
}
if (rowIndex === 0) {
const alignRow = inferAlignRow(ancestors, align)
if (alignRow) result.push(...alignRow)
}
}
return result
}
/**
* @param {Array<Nodes>} ancestors
* @param {Array<AlignType>} align
* @returns {Array<Entry> | undefined}
*/
function inferAlignRow(ancestors, align) {
const node = ancestors.at(-1)
assert(node) // Always defined.
assert(node.type === 'table') // Always table.
const headEnd = pointEnd(node.children[0])
if (!headEnd || typeof headEnd.offset !== 'number') return
let index = headEnd.offset
if (value.charCodeAt(index) === 13 /* `\r` */) index++
/* c8 ignore next -- should never happen, alignment is needed. */
if (value.charCodeAt(index) !== 10 /* `\n` */) return
index++
/** @type {Array<Entry>} */
const result = []
const line = headEnd.line + 1
// 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)
}
/* c8 ignore next 7 -- should always be found. */
if (
code !== 45 /* `-` */ &&
code !== 58 /* `:` */ &&
code !== 124 /* `|` */
) {
return
}
let lineEndOffset = value.indexOf('\n', index)
if (lineEndOffset === -1) lineEndOffset = value.length
if (value.charCodeAt(lineEndOffset - 1) === 13 /* `\r` */) lineEndOffset--
let column = 0
let cellStart = index
let cellEnd = value.indexOf('|', index + (code === 124 ? 1 : 0))
if (cellEnd === -1 || cellEnd > lineEndOffset) {
cellEnd = lineEndOffset
}
while (cellStart !== cellEnd) {
let nextCellEnd = value.indexOf('|', cellEnd + 1)
if (nextCellEnd === -1 || nextCellEnd > lineEndOffset) {
nextCellEnd = lineEndOffset
}
// Check if the trail is empty,
// which means it’s a closing pipe with trailing whitespace.
if (nextCellEnd === lineEndOffset) {
let maybeEnd = lineEndOffset
let code = value.charCodeAt(maybeEnd - 1)
while (code === 9 /* `\t` */ || code === 32 /* ` ` */) {
maybeEnd--
code = value.charCodeAt(maybeEnd - 1)
}
if (cellEnd + 1 === maybeEnd) {
cellEnd = lineEndOffset
}
}
result.push({
align: align[column],
ancestors,
column,
size: inferSize(
{
line,
column: cellStart - index + 1,
offset: cellStart
},
{line, column: cellEnd - index + 1, offset: cellEnd},
cellEnd === lineEndOffset
)
})
cellStart = cellEnd
cellEnd = nextCellEnd
column++
}
return result
}
/**
* @param {Point | undefined} start
* Start point.
* @param {Point | undefined} end
* End point.
* @param {boolean} tailCell
* Whether this is the last cell in a row.
* @returns {Size | undefined}
* Size info.
*/
function inferSize(start, end, tailCell) {
if (
end &&
start &&
typeof end.offset === 'number' &&
typeof start.offset === 'number'
) {
let leftIndex = start.offset
/** @type {number | undefined} */
let left
/** @type {number | undefined} */
let right
if (value.charCodeAt(leftIndex) === 124 /* `|` */) {
left = 0
leftIndex++
while (value.charCodeAt(leftIndex) === 32) {
left++
leftIndex++
}
}
// Else, A leading pipe can only be omitted in the first cell.
// Where we never want leading whitespace, as it’s seen as
// indentation, and could turn into an indented block.
let rightIndex = end.offset
// The final pipe, if it exists, is part of the last cell in a row
// according to positional info.
if (tailCell) {
while (value.charCodeAt(rightIndex - 1) === 32) {
rightIndex--
}
// Found a pipe: we expect more whitespace.
if (
rightIndex > leftIndex &&
value.charCodeAt(rightIndex - 1) === 124 /* `|` */
) {
rightIndex--
}
// No pipe at the last cell: the trailing whitespace is part of
// the cell.
else {
rightIndex = end.offset
}
}
/** @type {number} */
const rightEdgeIndex = rightIndex
if (value.charCodeAt(rightIndex) === 124 /* `|` */) {
right = 0
while (
rightIndex - 1 > leftIndex &&
value.charCodeAt(rightIndex - 1) === 32
) {
right++
rightIndex--
}
}
// Else, a trailing pipe can only be omitted in the last cell.
// Where we never want trailing whitespace.
const middle = stringLength
? stringLength(value.slice(leftIndex, rightIndex))
: rightIndex - leftIndex
return {
left,
leftPoint: {
line: start.line,
column: start.column + (leftIndex - start.offset),
offset: leftIndex
},
middle,
right,
rightPoint: {
line: end.line,
column: end.column - (end.offset - rightEdgeIndex),
offset: rightEdgeIndex
}
}
}
}
}
)
export default remarkLintTableCellPadding