Update MD060/table-column-style to add aligned_delimiter parameter (fixes #1854).

This commit is contained in:
David Anson 2025-11-30 16:39:56 -08:00
parent 8e974f95d5
commit f6c5369ef4
14 changed files with 648 additions and 32 deletions

View file

@ -1,9 +1,9 @@
This rule is triggered when the column separators of a
This rule is triggered when the column separator pipe characters (`|`) of a
[GitHub Flavored Markdown table][gfm-table-060] are used inconsistently.
This rule recognizes three table column styles based on popular use.
Style `aligned` ensures table delimiters are vertically aligned:
Style `aligned` ensures pipe characters are vertically aligned:
```markdown
| Character | Meaning |
@ -46,7 +46,40 @@ see if it satisfies any supported style. If so, no violations are reported. If
not, violations are be reported for whichever style would produce the *fewest*
issues (i.e., whichever style is the closest match).
Note: Delimiter placement for the `aligned` style is based on visual appearance
Setting the `aligned_delimiter` parameter to `true` requires pipe characters in
the delimiter row to align with those in the header row. This can be used with
`compact` and `tight` tables to make the header text more obvious. (It's already
required for tables with style `aligned`.)
Style `compact` with `aligned_delimiter`:
```markdown
| Character | Meaning |
| --------- | ------- |
| Y | Yes |
| N | No |
```
Style `tight` with `aligned_delimiter`:
```markdown
|Character|Meaning|
|---------|-------|
|Y|Yes|
|N|No|
```
**Note**: This rule does not require leading/trailing pipe characters, so this
is also a valid table for style `compact`:
```markdown
Character | Meaning
--- | ---
Y | Yes
N | No
```
**Note**: Pipe alignment for the `aligned` style is based on visual appearance
and not character count. Because editors typically render [emoji][emoji] and
[CJK characters][cjk-characters] at *twice* the width of
[Latin characters][latin-script], this rule takes that into account for tables

View file

@ -2694,15 +2694,16 @@ Aliases: `table-column-style`
Parameters:
- `aligned_delimiter`: Aligned delimiter columns (`boolean`, default `false`)
- `style`: Table column style (`string`, default `any`, values `aligned` /
`any` / `compact` / `tight`)
This rule is triggered when the column separators of a
This rule is triggered when the column separator pipe characters (`|`) of a
[GitHub Flavored Markdown table][gfm-table-060] are used inconsistently.
This rule recognizes three table column styles based on popular use.
Style `aligned` ensures table delimiters are vertically aligned:
Style `aligned` ensures pipe characters are vertically aligned:
```markdown
| Character | Meaning |
@ -2745,7 +2746,40 @@ see if it satisfies any supported style. If so, no violations are reported. If
not, violations are be reported for whichever style would produce the *fewest*
issues (i.e., whichever style is the closest match).
Note: Delimiter placement for the `aligned` style is based on visual appearance
Setting the `aligned_delimiter` parameter to `true` requires pipe characters in
the delimiter row to align with those in the header row. This can be used with
`compact` and `tight` tables to make the header text more obvious. (It's already
required for tables with style `aligned`.)
Style `compact` with `aligned_delimiter`:
```markdown
| Character | Meaning |
| --------- | ------- |
| Y | Yes |
| N | No |
```
Style `tight` with `aligned_delimiter`:
```markdown
|Character|Meaning|
|---------|-------|
|Y|Yes|
|N|No|
```
**Note**: This rule does not require leading/trailing pipe characters, so this
is also a valid table for style `compact`:
```markdown
Character | Meaning
--- | ---
Y | Yes
N | No
```
**Note**: Pipe alignment for the `aligned` style is based on visual appearance
and not character count. Because editors typically render [emoji][emoji] and
[CJK characters][cjk-characters] at *twice* the width of
[Latin characters][latin-script], this rule takes that into account for tables

View file

@ -6,15 +6,16 @@ Aliases: `table-column-style`
Parameters:
- `aligned_delimiter`: Aligned delimiter columns (`boolean`, default `false`)
- `style`: Table column style (`string`, default `any`, values `aligned` /
`any` / `compact` / `tight`)
This rule is triggered when the column separators of a
This rule is triggered when the column separator pipe characters (`|`) of a
[GitHub Flavored Markdown table][gfm-table-060] are used inconsistently.
This rule recognizes three table column styles based on popular use.
Style `aligned` ensures table delimiters are vertically aligned:
Style `aligned` ensures pipe characters are vertically aligned:
```markdown
| Character | Meaning |
@ -57,7 +58,40 @@ see if it satisfies any supported style. If so, no violations are reported. If
not, violations are be reported for whichever style would produce the *fewest*
issues (i.e., whichever style is the closest match).
Note: Delimiter placement for the `aligned` style is based on visual appearance
Setting the `aligned_delimiter` parameter to `true` requires pipe characters in
the delimiter row to align with those in the header row. This can be used with
`compact` and `tight` tables to make the header text more obvious. (It's already
required for tables with style `aligned`.)
Style `compact` with `aligned_delimiter`:
```markdown
| Character | Meaning |
| --------- | ------- |
| Y | Yes |
| N | No |
```
Style `tight` with `aligned_delimiter`:
```markdown
|Character|Meaning|
|---------|-------|
|Y|Yes|
|N|No|
```
**Note**: This rule does not require leading/trailing pipe characters, so this
is also a valid table for style `compact`:
```markdown
Character | Meaning
--- | ---
Y | Yes
N | No
```
**Note**: Pipe alignment for the `aligned` style is based on visual appearance
and not character count. Because editors typically render [emoji][emoji] and
[CJK characters][cjk-characters] at *twice* the width of
[Latin characters][latin-script], this rule takes that into account for tables

View file

@ -2281,6 +2281,10 @@ export interface ConfigurationStrict {
* Table column style
*/
style?: "any" | "aligned" | "compact" | "tight";
/**
* Aligned delimiter columns
*/
aligned_delimiter?: boolean;
};
/**
* MD060/table-column-style : Table column style : https://github.com/DavidAnson/markdownlint/blob/v0.39.0/doc/md060.md
@ -2301,6 +2305,10 @@ export interface ConfigurationStrict {
* Table column style
*/
style?: "any" | "aligned" | "compact" | "tight";
/**
* Aligned delimiter columns
*/
aligned_delimiter?: boolean;
};
/**
* headings : MD001, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043

View file

@ -8,6 +8,22 @@ import stringWidth from "string-width";
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("markdownlint").RuleOnErrorInfo} RuleOnErrorInfo */
/**
* Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects.
*
* @param {RuleOnErrorInfo[]} errors List of errors.
* @param {number} lineNumber Line number.
* @param {number} column Column number.
* @param {string} detail Detail message.
*/
function addError(errors, lineNumber, column, detail) {
errors.push({
lineNumber,
detail,
"range": [ column, 1 ]
});
}
/**
* @typedef Column
* @property {number} actual Actual column (1-based).
@ -34,19 +50,28 @@ function getTableDividerColumns(lines, row) {
}
/**
* Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects.
* Checks the specified table rows for consistency with the "aligned" style.
*
* @param {RuleOnErrorInfo[]} errors List of errors.
* @param {number} lineNumber Line number.
* @param {number} column Column number.
* @param {readonly string[]} lines File/string lines.
* @param {MicromarkToken[]} rows Micromark row tokens.
* @param {string} detail Detail message.
* @returns {RuleOnErrorInfo[]} List of errors.
*/
function addError(errors, lineNumber, column, detail) {
errors.push({
lineNumber,
detail,
"range": [ column, 1 ]
});
function checkStyleAligned(lines, rows, detail) {
/** @type {RuleOnErrorInfo[]} */
const errorInfos = [];
const headingRow = rows[0];
const headingDividerColumns = getTableDividerColumns(lines, headingRow);
for (const row of rows.slice(1)) {
const remainingHeadingDividerColumns = new Set(headingDividerColumns.map((column) => column.effective));
const rowDividerColumns = getTableDividerColumns(lines, row);
for (const dividerColumn of rowDividerColumns) {
if ((remainingHeadingDividerColumns.size > 0) && !remainingHeadingDividerColumns.delete(dividerColumn.effective)) {
addError(errorInfos, row.startLine, dividerColumn.actual, detail);
}
}
}
return errorInfos;
}
/** @type {import("markdownlint").Rule} */
@ -60,27 +85,19 @@ export default {
const styleAlignedAllowed = (style === "any") || (style === "aligned");
const styleCompactAllowed = (style === "any") || (style === "compact");
const styleTightAllowed = (style === "any") || (style === "tight");
const alignedDelimiter = !!params.config.aligned_delimiter;
const lines = params.lines;
// Scan all tables/rows
const tables = filterByTypesCached([ "table" ]);
for (const table of tables) {
const rows = filterByTypes(table.children, [ "tableDelimiterRow", "tableRow" ]);
const headingRow = rows[0];
// Determine errors for style "aligned"
/** @type {RuleOnErrorInfo[]} */
const errorsIfAligned = [];
if (styleAlignedAllowed) {
const headingDividerColumns = getTableDividerColumns(params.lines, headingRow);
for (const row of rows.slice(1)) {
const remainingHeadingDividerColumns = new Set(headingDividerColumns.map((column) => column.effective));
const rowDividerColumns = getTableDividerColumns(params.lines, row);
for (const dividerColumn of rowDividerColumns) {
if ((remainingHeadingDividerColumns.size > 0) && !remainingHeadingDividerColumns.delete(dividerColumn.effective)) {
addError(errorsIfAligned, row.startLine, dividerColumn.actual, "Table pipe does not align with heading for style \"aligned\"");
}
}
}
errorsIfAligned.push(...checkStyleAligned(lines, rows, "Table pipe does not align with heading for style \"aligned\""));
}
// Determine errors for styles "compact" and "tight"
@ -92,6 +109,11 @@ export default {
(styleCompactAllowed || styleTightAllowed) &&
!(styleAlignedAllowed && (errorsIfAligned.length === 0))
) {
if (alignedDelimiter) {
const errorInfos = checkStyleAligned(lines, rows.slice(0, 2), "Table pipe does not align with heading for option \"aligned_delimiter\"");
errorsIfCompact.push(...errorInfos);
errorsIfTight.push(...errorInfos);
}
for (const row of rows) {
const tokensOfInterest = filterByTypes(row.children, [ "tableCellDivider", "tableContent", "whitespace" ]);
for (let i = 0; i < tokensOfInterest.length; i++) {

View file

@ -340,6 +340,8 @@
// MD060/table-column-style : Table column style : https://github.com/DavidAnson/markdownlint/blob/v0.39.0/doc/md060.md
"MD060": {
// Table column style
"style": "any"
"style": "any",
// Aligned delimiter columns
"aligned_delimiter": false
}
}

View file

@ -304,3 +304,5 @@ MD059:
MD060:
# Table column style
style: "any"
# Aligned delimiter columns
aligned_delimiter: false

View file

@ -645,6 +645,12 @@ for (const rule of rules) {
],
"default": "any"
};
// @ts-ignore
subscheme.properties.aligned_delimiter = {
"description": "Aligned delimiter columns",
"type": "boolean",
"default": false
};
break;
default:
break;

View file

@ -4701,6 +4701,11 @@
"tight"
],
"default": "any"
},
"aligned_delimiter": {
"description": "Aligned delimiter columns",
"type": "boolean",
"default": false
}
}
}
@ -4747,6 +4752,11 @@
"tight"
],
"default": "any"
},
"aligned_delimiter": {
"description": "Aligned delimiter columns",
"type": "boolean",
"default": false
}
}
}

View file

@ -4701,6 +4701,11 @@
"tight"
],
"default": "any"
},
"aligned_delimiter": {
"description": "Aligned delimiter columns",
"type": "boolean",
"default": false
}
}
}
@ -4747,6 +4752,11 @@
"tight"
],
"default": "any"
},
"aligned_delimiter": {
"description": "Aligned delimiter columns",
"type": "boolean",
"default": false
}
}
}

View file

@ -1101,7 +1101,7 @@ test("readme", async(t) => {
});
test("validateJsonUsingConfigSchemaStrict", async(t) => {
t.plan(221);
t.plan(222);
// @ts-ignore
const ajv = new Ajv(ajvOptions);
const validateSchemaStrict = ajv.compile(configSchemaStrict);

View file

@ -62068,6 +62068,349 @@ Generated by [AVA](https://avajs.dev).
`,
}
## table-column-style-aligned-delimiter.md
> Snapshot 1
{
errors: [
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
5,
1,
],
fixInfo: null,
lineNumber: 28,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
9,
1,
],
fixInfo: null,
lineNumber: 28,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
13,
1,
],
fixInfo: null,
lineNumber: 28,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
22,
1,
],
fixInfo: null,
lineNumber: 36,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
4,
1,
],
fixInfo: null,
lineNumber: 50,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
20,
1,
],
fixInfo: null,
lineNumber: 58,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
3,
1,
],
fixInfo: null,
lineNumber: 72,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
5,
1,
],
fixInfo: null,
lineNumber: 72,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
7,
1,
],
fixInfo: null,
lineNumber: 72,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
18,
1,
],
fixInfo: null,
lineNumber: 80,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
2,
1,
],
fixInfo: null,
lineNumber: 94,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
4,
1,
],
fixInfo: null,
lineNumber: 94,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for option "aligned_delimiter"',
errorRange: [
17,
1,
],
fixInfo: null,
lineNumber: 102,
ruleDescription: 'Table column style',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md060.md',
ruleNames: [
'MD060',
'table-column-style',
],
severity: 'error',
},
],
fixed: `# Table Column Style - Aligned Delimiter␊
## Aligned / Edge Pipes␊
| Heading | Heading | Heading |␊
| ------- | --------- | ------- |␊
| Text | Text text | Text |␊
| Text | Text text | Text |␊
| Text | Text text | Text |␊
## Aligned / No Edge Pipes␊
Heading | Heading | Heading␊
------- | --------- | -------␊
Text | Text text | Text␊
Text | Text text | Text␊
Text | Text text | Text␊
## Compact / Edge Pipes␊
| Heading | Heading | Heading |␊
| ------- | ------- | ------- |␊
| Text | Text text | Text |␊
| Text text | Text text text | Text |␊
| Text | Text | Text |␊
| Heading | Heading | Heading |␊
| - | - | - |␊
| Text | Text text | Text |␊
| Text text | Text text text | Text |␊
| Text | Text | Text |␊
{MD060:-5}␊
| Heading | Heading | Heading |␊
| ------- | -------- | ------ |␊
| Text text | Text text text | Text |␊
{MD060:-3}␊
## Compact / No Edge Pipes␊
Heading | Heading | Heading␊
------- | ------- | -------␊
Text | Text text | Text␊
Text text | Text text text | Text␊
Text | Text | Text␊
Heading | Heading | Heading␊
-- | -- | --␊
Text | Text text | Text␊
Text text | Text text text | Text␊
Text | Text | Text␊
{MD060:-5}␊
Heading | Heading | Heading␊
------- | -------- | ------␊
Text text | Text text text | Text␊
{MD060:-3}␊
## Tight / Edge Pipes␊
|Heading|Heading|Heading|␊
|-------|-------|-------|␊
|Text|Text text|Text|␊
|Text text|Text text text|Text|␊
|Text|Text|Text|␊
|Heading|Heading|Heading|␊
|-|-|-|␊
|Text|Text text|Text|␊
|Text text|Text text text|Text|␊
|Text|Text|Text|␊
{MD060:-5}␊
|Heading|Heading|Heading|␊
|-------|--------|------|␊
|Text text|Text text text|Text|␊
{MD060:-3}␊
## Tight / No Edge Pipes␊
Heading|Heading|Heading␊
-------|-------|-------␊
Text|Text text|Text␊
Text text|Text text text|Text␊
Text|Text|Text␊
Heading|Heading|Heading␊
-|-|-␊
Text|Text text|Text␊
Text text|Text text text|Text␊
Text|Text|Text␊
{MD060:-5}␊
Heading|Heading|Heading␊
-------|--------|------␊
Text text|Text text text|Text␊
{MD060:-3}␊
<!-- markdownlint-configure-file {␊
"table-column-style": {␊
"aligned_delimiter": true␊
},␊
"table-pipe-style": false␊
} -->␊
`,
}
## table-column-style-aligned.md
> Snapshot 1

View file

@ -0,0 +1,112 @@
# Table Column Style - Aligned Delimiter
## Aligned / Edge Pipes
| Heading | Heading | Heading |
| ------- | --------- | ------- |
| Text | Text text | Text |
| Text | Text text | Text |
| Text | Text text | Text |
## Aligned / No Edge Pipes
Heading | Heading | Heading
------- | --------- | -------
Text | Text text | Text
Text | Text text | Text
Text | Text text | Text
## Compact / Edge Pipes
| Heading | Heading | Heading |
| ------- | ------- | ------- |
| Text | Text text | Text |
| Text text | Text text text | Text |
| Text | Text | Text |
| Heading | Heading | Heading |
| - | - | - |
| Text | Text text | Text |
| Text text | Text text text | Text |
| Text | Text | Text |
{MD060:-5}
| Heading | Heading | Heading |
| ------- | -------- | ------ |
| Text text | Text text text | Text |
{MD060:-3}
## Compact / No Edge Pipes
Heading | Heading | Heading
------- | ------- | -------
Text | Text text | Text
Text text | Text text text | Text
Text | Text | Text
Heading | Heading | Heading
-- | -- | --
Text | Text text | Text
Text text | Text text text | Text
Text | Text | Text
{MD060:-5}
Heading | Heading | Heading
------- | -------- | ------
Text text | Text text text | Text
{MD060:-3}
## Tight / Edge Pipes
|Heading|Heading|Heading|
|-------|-------|-------|
|Text|Text text|Text|
|Text text|Text text text|Text|
|Text|Text|Text|
|Heading|Heading|Heading|
|-|-|-|
|Text|Text text|Text|
|Text text|Text text text|Text|
|Text|Text|Text|
{MD060:-5}
|Heading|Heading|Heading|
|-------|--------|------|
|Text text|Text text text|Text|
{MD060:-3}
## Tight / No Edge Pipes
Heading|Heading|Heading
-------|-------|-------
Text|Text text|Text
Text text|Text text text|Text
Text|Text|Text
Heading|Heading|Heading
-|-|-
Text|Text text|Text
Text text|Text text text|Text
Text|Text|Text
{MD060:-5}
Heading|Heading|Heading
-------|--------|------
Text text|Text text text|Text
{MD060:-3}
<!-- markdownlint-configure-file {
"table-column-style": {
"aligned_delimiter": true
},
"table-pipe-style": false
} -->