Update MD060/table-column-style to account for emoji and CJK characters rendering at 2x Latin width (fixes #1869).

This commit is contained in:
David Anson 2025-11-25 14:15:50 -08:00
parent ec380a87c0
commit 9b116ae889
13 changed files with 254 additions and 61 deletions

View file

@ -21,6 +21,7 @@ changelog
Changelog Changelog
changelogs changelogs
Changelogs Changelogs
CJK
CLI CLI
coc-markdownlint coc-markdownlint
CodeQL CodeQL
@ -67,6 +68,7 @@ mdl
MDN MDN
minified minified
MkDocs MkDocs
monospaced
MSBuild MSBuild
namespace namespace
Neovim Neovim

View file

@ -1,9 +1,9 @@
This rule is triggered when the column separators of a This rule is triggered when the column separators of a
[GitHub Flavored Markdown table][gfm-table-060] are used inconsistently. [GitHub Flavored Markdown table][gfm-table-060] are used inconsistently.
This rule recognizes three table column styles based on popular use: This rule recognizes three table column styles based on popular use.
Style `aligned` looks the most like a table: Style `aligned` ensures table delimiters are vertically aligned:
```markdown ```markdown
| Character | Meaning | | Character | Meaning |
@ -12,7 +12,16 @@ Style `aligned` looks the most like a table:
| N | No | | N | No |
``` ```
Style `compact` uses a single space to pad cell content: The `aligned` style ignores cell content, so the following is also valid:
```markdown
| Character | Meaning |
|-----------|---------|
| Y | Yes |
| N | No |
```
Style `compact` avoids extra padding with a single space around cell content:
```markdown ```markdown
| Character | Meaning | | Character | Meaning |
@ -21,7 +30,7 @@ Style `compact` uses a single space to pad cell content:
| N | No | | N | No |
``` ```
Style `tight` uses no padding for cell content: Style `tight` uses no padding at all for cell content:
```markdown ```markdown
|Character|Meaning| |Character|Meaning|
@ -31,16 +40,18 @@ Style `tight` uses no padding for cell content:
``` ```
When this rule's `style` parameter is set to `aligned`, `compact`, or `tight`, When this rule's `style` parameter is set to `aligned`, `compact`, or `tight`,
every table must match the corresponding pattern and errors will be reported for every table must match the corresponding pattern and any violations will be
any violations. By default, or when the `any` style is used, each table is reported. By default, or when the `any` style is used, each table is analyzed to
analyzed to see if it satisfies any supported style. If so, no errors are see if it satisfies any supported style. If so, no violations are reported. If
reported. If not, errors are be reported for whichever style would produce the not, violations are be reported for whichever style would produce the *fewest*
*fewest* errors (i.e., whichever style is the closest match). issues (i.e., whichever style is the closest match).
Note: Pipe alignment for the `aligned` style is based on character count, so Note: Delimiter placement for the `aligned` style is based on visual appearance
wide characters and multi-character encodings can produce unexpected results. and not character count. Because editors typically render [emoji][emoji] and
The following table is correctly aligned based on character count, though some [CJK characters][cjk-characters] at *twice* the width of
editors render the emoji wider: [Latin characters][latin-script], this rule takes that into account for tables
using the `aligned` style. The following table is correctly formatted and will
appear aligned in most editors and monospaced fonts:
<!-- markdownlint-capture --> <!-- markdownlint-capture -->
<!-- markdownlint-disable extended-ascii --> <!-- markdownlint-disable extended-ascii -->
@ -48,12 +59,15 @@ editors render the emoji wider:
```markdown ```markdown
| Response | Emoji | | Response | Emoji |
| -------- | ----- | | -------- | ----- |
| Yes | ✅ | | Yes | ✅ |
| No | ❎ | | No | ❎ |
``` ```
<!-- markdownlint-restore --> <!-- markdownlint-restore -->
Rationale: Consistent formatting makes it easier to understand a document. Rationale: Consistent formatting makes it easier to understand a document.
[cjk-characters]: https://en.wikipedia.org/wiki/CJK_characters
[emoji]: https://en.wikipedia.org/wiki/Emoji
[gfm-table-060]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables [gfm-table-060]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables
[latin-script]: https://en.wikipedia.org/wiki/Latin_script

View file

@ -2700,9 +2700,9 @@ Parameters:
This rule is triggered when the column separators of a This rule is triggered when the column separators of a
[GitHub Flavored Markdown table][gfm-table-060] are used inconsistently. [GitHub Flavored Markdown table][gfm-table-060] are used inconsistently.
This rule recognizes three table column styles based on popular use: This rule recognizes three table column styles based on popular use.
Style `aligned` looks the most like a table: Style `aligned` ensures table delimiters are vertically aligned:
```markdown ```markdown
| Character | Meaning | | Character | Meaning |
@ -2711,7 +2711,16 @@ Style `aligned` looks the most like a table:
| N | No | | N | No |
``` ```
Style `compact` uses a single space to pad cell content: The `aligned` style ignores cell content, so the following is also valid:
```markdown
| Character | Meaning |
|-----------|---------|
| Y | Yes |
| N | No |
```
Style `compact` avoids extra padding with a single space around cell content:
```markdown ```markdown
| Character | Meaning | | Character | Meaning |
@ -2720,7 +2729,7 @@ Style `compact` uses a single space to pad cell content:
| N | No | | N | No |
``` ```
Style `tight` uses no padding for cell content: Style `tight` uses no padding at all for cell content:
```markdown ```markdown
|Character|Meaning| |Character|Meaning|
@ -2730,16 +2739,18 @@ Style `tight` uses no padding for cell content:
``` ```
When this rule's `style` parameter is set to `aligned`, `compact`, or `tight`, When this rule's `style` parameter is set to `aligned`, `compact`, or `tight`,
every table must match the corresponding pattern and errors will be reported for every table must match the corresponding pattern and any violations will be
any violations. By default, or when the `any` style is used, each table is reported. By default, or when the `any` style is used, each table is analyzed to
analyzed to see if it satisfies any supported style. If so, no errors are see if it satisfies any supported style. If so, no violations are reported. If
reported. If not, errors are be reported for whichever style would produce the not, violations are be reported for whichever style would produce the *fewest*
*fewest* errors (i.e., whichever style is the closest match). issues (i.e., whichever style is the closest match).
Note: Pipe alignment for the `aligned` style is based on character count, so Note: Delimiter placement for the `aligned` style is based on visual appearance
wide characters and multi-character encodings can produce unexpected results. and not character count. Because editors typically render [emoji][emoji] and
The following table is correctly aligned based on character count, though some [CJK characters][cjk-characters] at *twice* the width of
editors render the emoji wider: [Latin characters][latin-script], this rule takes that into account for tables
using the `aligned` style. The following table is correctly formatted and will
appear aligned in most editors and monospaced fonts:
<!-- markdownlint-capture --> <!-- markdownlint-capture -->
<!-- markdownlint-disable extended-ascii --> <!-- markdownlint-disable extended-ascii -->
@ -2747,15 +2758,18 @@ editors render the emoji wider:
```markdown ```markdown
| Response | Emoji | | Response | Emoji |
| -------- | ----- | | -------- | ----- |
| Yes | ✅ | | Yes | ✅ |
| No | ❎ | | No | ❎ |
``` ```
<!-- markdownlint-restore --> <!-- markdownlint-restore -->
Rationale: Consistent formatting makes it easier to understand a document. Rationale: Consistent formatting makes it easier to understand a document.
[cjk-characters]: https://en.wikipedia.org/wiki/CJK_characters
[emoji]: https://en.wikipedia.org/wiki/Emoji
[gfm-table-060]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables [gfm-table-060]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables
[latin-script]: https://en.wikipedia.org/wiki/Latin_script
<!-- markdownlint-configure-file { <!-- markdownlint-configure-file {
"no-inline-html": { "no-inline-html": {

View file

@ -12,9 +12,9 @@ Parameters:
This rule is triggered when the column separators of a This rule is triggered when the column separators of a
[GitHub Flavored Markdown table][gfm-table-060] are used inconsistently. [GitHub Flavored Markdown table][gfm-table-060] are used inconsistently.
This rule recognizes three table column styles based on popular use: This rule recognizes three table column styles based on popular use.
Style `aligned` looks the most like a table: Style `aligned` ensures table delimiters are vertically aligned:
```markdown ```markdown
| Character | Meaning | | Character | Meaning |
@ -23,7 +23,16 @@ Style `aligned` looks the most like a table:
| N | No | | N | No |
``` ```
Style `compact` uses a single space to pad cell content: The `aligned` style ignores cell content, so the following is also valid:
```markdown
| Character | Meaning |
|-----------|---------|
| Y | Yes |
| N | No |
```
Style `compact` avoids extra padding with a single space around cell content:
```markdown ```markdown
| Character | Meaning | | Character | Meaning |
@ -32,7 +41,7 @@ Style `compact` uses a single space to pad cell content:
| N | No | | N | No |
``` ```
Style `tight` uses no padding for cell content: Style `tight` uses no padding at all for cell content:
```markdown ```markdown
|Character|Meaning| |Character|Meaning|
@ -42,16 +51,18 @@ Style `tight` uses no padding for cell content:
``` ```
When this rule's `style` parameter is set to `aligned`, `compact`, or `tight`, When this rule's `style` parameter is set to `aligned`, `compact`, or `tight`,
every table must match the corresponding pattern and errors will be reported for every table must match the corresponding pattern and any violations will be
any violations. By default, or when the `any` style is used, each table is reported. By default, or when the `any` style is used, each table is analyzed to
analyzed to see if it satisfies any supported style. If so, no errors are see if it satisfies any supported style. If so, no violations are reported. If
reported. If not, errors are be reported for whichever style would produce the not, violations are be reported for whichever style would produce the *fewest*
*fewest* errors (i.e., whichever style is the closest match). issues (i.e., whichever style is the closest match).
Note: Pipe alignment for the `aligned` style is based on character count, so Note: Delimiter placement for the `aligned` style is based on visual appearance
wide characters and multi-character encodings can produce unexpected results. and not character count. Because editors typically render [emoji][emoji] and
The following table is correctly aligned based on character count, though some [CJK characters][cjk-characters] at *twice* the width of
editors render the emoji wider: [Latin characters][latin-script], this rule takes that into account for tables
using the `aligned` style. The following table is correctly formatted and will
appear aligned in most editors and monospaced fonts:
<!-- markdownlint-capture --> <!-- markdownlint-capture -->
<!-- markdownlint-disable extended-ascii --> <!-- markdownlint-disable extended-ascii -->
@ -59,12 +70,15 @@ editors render the emoji wider:
```markdown ```markdown
| Response | Emoji | | Response | Emoji |
| -------- | ----- | | -------- | ----- |
| Yes | ✅ | | Yes | ✅ |
| No | ❎ | | No | ❎ |
``` ```
<!-- markdownlint-restore --> <!-- markdownlint-restore -->
Rationale: Consistent formatting makes it easier to understand a document. Rationale: Consistent formatting makes it easier to understand a document.
[cjk-characters]: https://en.wikipedia.org/wiki/CJK_characters
[emoji]: https://en.wikipedia.org/wiki/Emoji
[gfm-table-060]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables [gfm-table-060]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables
[latin-script]: https://en.wikipedia.org/wiki/Latin_script

View file

@ -2,10 +2,37 @@
import { filterByTypes } from "../helpers/micromark-helpers.cjs"; import { filterByTypes } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs"; import { filterByTypesCached } from "./cache.mjs";
import stringWidth from "string-width";
/** @typedef {import("micromark-extension-gfm-table")} */ /** @typedef {import("micromark-extension-gfm-table")} */
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("markdownlint").RuleOnErrorInfo} RuleOnErrorInfo */ /** @typedef {import("markdownlint").RuleOnErrorInfo} RuleOnErrorInfo */
/**
* @typedef Column
* @property {number} actual Actual column (1-based).
* @property {number} effective Effective column (1-based).
*/
/**
* Gets a list of table cell divider columns.
*
* @param {readonly string[]} lines File/string lines.
* @param {MicromarkToken} row Micromark row token.
* @returns {Column[]} Divider columns.
*/
function getTableDividerColumns(lines, row) {
return filterByTypes(
row.children,
[ "tableCellDivider" ]
).map(
(divider) => ({
"actual": divider.startColumn,
"effective": stringWidth(lines[row.startLine - 1].slice(0, divider.startColumn - 1))
})
);
}
/** /**
* Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects. * Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects.
* *
@ -44,13 +71,13 @@ export default {
/** @type {RuleOnErrorInfo[]} */ /** @type {RuleOnErrorInfo[]} */
const errorsIfAligned = []; const errorsIfAligned = [];
if (styleAlignedAllowed) { if (styleAlignedAllowed) {
const headingDividerColumns = filterByTypes(headingRow.children, [ "tableCellDivider" ]).map((divider) => divider.startColumn); const headingDividerColumns = getTableDividerColumns(params.lines, headingRow);
for (const row of rows.slice(1)) { for (const row of rows.slice(1)) {
const remainingHeadingDividerColumns = new Set(headingDividerColumns); const remainingHeadingDividerColumns = new Set(headingDividerColumns.map((column) => column.effective));
const rowDividerColumns = filterByTypes(row.children, [ "tableCellDivider" ]).map((divider) => divider.startColumn); const rowDividerColumns = getTableDividerColumns(params.lines, row);
for (const dividerColumn of rowDividerColumns) { for (const dividerColumn of rowDividerColumns) {
if ((remainingHeadingDividerColumns.size > 0) && !remainingHeadingDividerColumns.delete(dividerColumn)) { if ((remainingHeadingDividerColumns.size > 0) && !remainingHeadingDividerColumns.delete(dividerColumn.effective)) {
addError(errorsIfAligned, row.startLine, dividerColumn, "Table pipe does not align with heading for style \"aligned\""); addError(errorsIfAligned, row.startLine, dividerColumn.actual, "Table pipe does not align with heading for style \"aligned\"");
} }
} }
} }

View file

@ -80,7 +80,8 @@
"micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0",
"micromark-extension-gfm-table": "2.1.1", "micromark-extension-gfm-table": "2.1.1",
"micromark-extension-math": "3.1.0", "micromark-extension-math": "3.1.0",
"micromark-util-types": "2.0.2" "micromark-util-types": "2.0.2",
"string-width": "8.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.39.1", "@eslint/js": "9.39.1",

View file

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

View file

@ -556,8 +556,6 @@ Generated by [AVA](https://avajs.dev).
test-repos/v8-v8-dev/src/features/promise-combinators.md:23 error MD058/blanks-around-tables Tables should be surrounded by blank lines [Context: "| name ..."]␊ test-repos/v8-v8-dev/src/features/promise-combinators.md:23 error MD058/blanks-around-tables Tables should be surrounded by blank lines [Context: "| name ..."]␊
test-repos/v8-v8-dev/src/features/promise-combinators.md:28 error MD058/blanks-around-tables Tables should be surrounded by blank lines [Context: "| [\`Promise.any\`](#promise.any..."]␊ test-repos/v8-v8-dev/src/features/promise-combinators.md:28 error MD058/blanks-around-tables Tables should be surrounded by blank lines [Context: "| [\`Promise.any\`](#promise.any..."]␊
test-repos/v8-v8-dev/src/features/promise-combinators.md:25:172 error MD060/table-column-style Table column style [Table pipe does not align with heading for style "aligned"]␊ test-repos/v8-v8-dev/src/features/promise-combinators.md:25:172 error MD060/table-column-style Table column style [Table pipe does not align with heading for style "aligned"]␊
test-repos/v8-v8-dev/src/features/promise-combinators.md:26:162 error MD060/table-column-style Table column style [Table pipe does not align with heading for style "aligned"]␊
test-repos/v8-v8-dev/src/features/promise-combinators.md:27:162 error MD060/table-column-style Table column style [Table pipe does not align with heading for style "aligned"]␊
test-repos/v8-v8-dev/src/features/promise-combinators.md:28:172 error MD060/table-column-style Table column style [Table pipe does not align with heading for style "aligned"]␊ test-repos/v8-v8-dev/src/features/promise-combinators.md:28:172 error MD060/table-column-style Table column style [Table pipe does not align with heading for style "aligned"]␊
test-repos/v8-v8-dev/src/features/promise-finally.md:82:1 error MD033/no-inline-html Inline HTML [Element: feature-support]␊ test-repos/v8-v8-dev/src/features/promise-finally.md:82:1 error MD033/no-inline-html Inline HTML [Element: feature-support]␊
test-repos/v8-v8-dev/src/features/regexp-match-indices.md:134:1 error MD033/no-inline-html Inline HTML [Element: feature-support]␊ test-repos/v8-v8-dev/src/features/regexp-match-indices.md:134:1 error MD033/no-inline-html Inline HTML [Element: feature-support]␊

View file

@ -67803,7 +67803,7 @@ Generated by [AVA](https://avajs.dev).
errorContext: null, errorContext: null,
errorDetail: 'Table pipe does not align with heading for style "aligned"', errorDetail: 'Table pipe does not align with heading for style "aligned"',
errorRange: [ errorRange: [
19, 20,
1, 1,
], ],
fixInfo: null, fixInfo: null,
@ -67857,9 +67857,9 @@ Generated by [AVA](https://avajs.dev).
| Response | Emoji |␊ | Response | Emoji |␊
| -------- | ----- |␊ | -------- | ----- |␊
| Yes | ✅ |␊ | Yes | ✅ |␊
| No | ❎ |␊ | No | ❎ |␊
| Oops | ❌ |␊ | Oops | ❌ |␊
{MD060:-2}␊ {MD060:-2}␊
@ -72533,6 +72533,72 @@ Generated by [AVA](https://avajs.dev).
`, `,
} }
## table-column-style-wide-characters.md
> Snapshot 1
{
errors: [],
fixed: `# Table Column Style - Wide Characters␊
## Emoji␊
| AB | CD |␊
| -- | -- |␊
| EF | GH |␊
| ✅ | KL |␊
| MN | ✅ |␊
| ✅ | ✅ |␊
| ✅ | CD |␊
| -- | -- |␊
| EF | GH |␊
| ✅ | KL |␊
| MN | ✅ |␊
| ✅ | ✅ |␊
| AB | ✅ |␊
| -- | -- |␊
| EF | GH |␊
| ✅ | KL |␊
| MN | ✅ |␊
| ✅ | ✅ |␊
| ✅ | ✅ |␊
| -- | -- |␊
| EF | GH |␊
| ✅ | KL |␊
| MN | ✅ |␊
| ✅ | ✅ |␊
## Hello World␊
| Language | Translation |␊
|---------------------|----------------|␊
| Emoji | 👋🌎 |␊
| Portuguese (Brazil) | Olá mundo |␊
| Turkish | Merhaba dünya |␊
| Chinese (Mandarin) | 你好,世界 |␊
| Japanese | こんにちは世界 |␊
| Korean | 안녕 세상 |␊
## ANSI Escape Codes␊
| Style | Escape codes |␊
| --------- | ------------------------------------------------------- |␊
| Bold | \\u001B[1m大胆な\\u001B[22m |␊
| Underline | \\u001B[4mUnderline\\u001B[0m |␊
| Link | \\u001B]8;;https://example.com\\u0007Link\\u001B]8;;\\u0007 |␊
<!-- markdownlint-configure-file {␊
"no-bare-urls": false,␊
"table-column-style": {␊
"style": "aligned"␊
}␊
} -->␊
`,
}
## table-content-with-issues.md ## table-content-with-issues.md
> Snapshot 1 > Snapshot 1

View file

@ -4,9 +4,9 @@
| Response | Emoji | | Response | Emoji |
| -------- | ----- | | -------- | ----- |
| Yes | ✅ | | Yes | ✅ |
| No | ❎ | | No | ❎ |
| Oops | ❌ | | Oops | ❌ |
{MD060:-2} {MD060:-2}

View file

@ -0,0 +1,57 @@
# Table Column Style - Wide Characters
## Emoji
| AB | CD |
| -- | -- |
| EF | GH |
| ✅ | KL |
| MN | ✅ |
| ✅ | ✅ |
| ✅ | CD |
| -- | -- |
| EF | GH |
| ✅ | KL |
| MN | ✅ |
| ✅ | ✅ |
| AB | ✅ |
| -- | -- |
| EF | GH |
| ✅ | KL |
| MN | ✅ |
| ✅ | ✅ |
| ✅ | ✅ |
| -- | -- |
| EF | GH |
| ✅ | KL |
| MN | ✅ |
| ✅ | ✅ |
## Hello World
| Language | Translation |
|---------------------|----------------|
| Emoji | 👋🌎 |
| Portuguese (Brazil) | Olá mundo |
| Turkish | Merhaba dünya |
| Chinese (Mandarin) | 你好,世界 |
| Japanese | こんにちは世界 |
| Korean | 안녕 세상 |
## ANSI Escape Codes
| Style | Escape codes |
| --------- | ------------------------------------------------------- |
| Bold | \u001B[1m大胆な\u001B[22m |
| Underline | \u001B[4mUnderline\u001B[0m |
| Link | \u001B]8;;https://example.com\u0007Link\u001B]8;;\u0007 |
<!-- markdownlint-configure-file {
"no-bare-urls": false,
"table-column-style": {
"style": "aligned"
}
} -->