This commit is contained in:
David Anson 2025-11-22 19:40:26 -08:00
parent 7668b0a263
commit f44a15e430
17 changed files with 333 additions and 27 deletions

View file

@ -37,8 +37,7 @@ analyzed to see if it satisfies any supported style. If so, no errors are
reported. If not, errors are be reported for whichever style would produce the reported. If not, errors are be reported for whichever style would produce the
*fewest* errors (i.e., whichever style is the closest match). *fewest* errors (i.e., whichever style is the closest match).
Note: Pipe alignment for the `aligned` style is based on character count, so Note: Pipe alignment for the `aligned` style is based on ...
wide characters and multi-character encodings can produce unexpected results.
The following table is correctly aligned based on character count, though some The following table is correctly aligned based on character count, though some
editors render the emoji wider: editors render the emoji wider:
@ -48,8 +47,8 @@ editors render the emoji wider:
```markdown ```markdown
| Response | Emoji | | Response | Emoji |
| -------- | ----- | | -------- | ----- |
| Yes | ✅ | | Yes | ✅ |
| No | ❎ | | No | ❎ |
``` ```
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

View file

@ -2696,6 +2696,8 @@ Parameters:
- `style`: Table column style (`string`, default `any`, values `aligned` / - `style`: Table column style (`string`, default `any`, values `aligned` /
`any` / `compact` / `tight`) `any` / `compact` / `tight`)
- `wide_character`: RegExp for matching wide character(s) (`string`, default
`undefined`)
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.
@ -2736,8 +2738,7 @@ analyzed to see if it satisfies any supported style. If so, no errors are
reported. If not, errors are be reported for whichever style would produce the reported. If not, errors are be reported for whichever style would produce the
*fewest* errors (i.e., whichever style is the closest match). *fewest* errors (i.e., whichever style is the closest match).
Note: Pipe alignment for the `aligned` style is based on character count, so Note: Pipe alignment for the `aligned` style is based on ...
wide characters and multi-character encodings can produce unexpected results.
The following table is correctly aligned based on character count, though some The following table is correctly aligned based on character count, though some
editors render the emoji wider: editors render the emoji wider:
@ -2747,8 +2748,8 @@ editors render the emoji wider:
```markdown ```markdown
| Response | Emoji | | Response | Emoji |
| -------- | ----- | | -------- | ----- |
| Yes | ✅ | | Yes | ✅ |
| No | ❎ | | No | ❎ |
``` ```
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

View file

@ -8,6 +8,8 @@ Parameters:
- `style`: Table column style (`string`, default `any`, values `aligned` / - `style`: Table column style (`string`, default `any`, values `aligned` /
`any` / `compact` / `tight`) `any` / `compact` / `tight`)
- `wide_character`: RegExp for matching wide character(s) (`string`, default
`undefined`)
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.
@ -48,8 +50,7 @@ analyzed to see if it satisfies any supported style. If so, no errors are
reported. If not, errors are be reported for whichever style would produce the reported. If not, errors are be reported for whichever style would produce the
*fewest* errors (i.e., whichever style is the closest match). *fewest* errors (i.e., whichever style is the closest match).
Note: Pipe alignment for the `aligned` style is based on character count, so Note: Pipe alignment for the `aligned` style is based on ...
wide characters and multi-character encodings can produce unexpected results.
The following table is correctly aligned based on character count, though some The following table is correctly aligned based on character count, though some
editors render the emoji wider: editors render the emoji wider:
@ -59,8 +60,8 @@ editors render the emoji wider:
```markdown ```markdown
| Response | Emoji | | Response | Emoji |
| -------- | ----- | | -------- | ----- |
| Yes | ✅ | | Yes | ✅ |
| No | ❎ | | No | ❎ |
``` ```
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

View file

@ -2281,6 +2281,10 @@ export interface ConfigurationStrict {
* Table column style * Table column style
*/ */
style?: "any" | "aligned" | "compact" | "tight"; style?: "any" | "aligned" | "compact" | "tight";
/**
* RegExp for matching wide character(s)
*/
wide_character?: string;
}; };
/** /**
* MD060/table-column-style : Table column style : https://github.com/DavidAnson/markdownlint/blob/v0.39.0/doc/md060.md * 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 * Table column style
*/ */
style?: "any" | "aligned" | "compact" | "tight"; style?: "any" | "aligned" | "compact" | "tight";
/**
* RegExp for matching wide character(s)
*/
wide_character?: string;
}; };
/** /**
* headings : MD001, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043 * headings : MD001, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043

View file

@ -4,8 +4,46 @@ import { filterByTypes } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs"; import { filterByTypesCached } from "./cache.mjs";
/** @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 */
// See https://unicode.org/reports/tr51/
const defaultWideCharacterReString = "\\p{RGI_Emoji}";
/**
* @typedef Column
* @property {number} actual Actual column (1-based)
* @property {number} effective Effective column (1-based)
*/
/**
* Gets the effective (adjusted for wide characters) column for an actual column.
*
* @param {string} line Line of text.
* @param {number} column Actual column (1-based).
* @param {RegExp} wideRe Wide character RegExp.
* @returns {number} Effective column (1-based).
*/
function effectiveColumn(line, column, wideRe) {
const wideCharacterCount = (line.slice(0, column - 1).match(wideRe) || []).length;
return column + wideCharacterCount;
}
/**
* Gets a list of table cell divider columns.
*
* @param {readonly string[]} lines File/string lines.
* @param {MicromarkToken} row Micromark row token.
* @param {RegExp} wideRe Wide character RegExp.
* @returns {Column[]} Divider columns.
*/
function getTableDividerColumns(lines, row, wideRe) {
return filterByTypes(
row.children,
[ "tableCellDivider" ]).map((divider) => ({ "actual": divider.startColumn, "effective": effectiveColumn(lines[row.startLine - 1], divider.startColumn, wideRe) })
);
}
/** /**
* Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects. * Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects.
* *
@ -33,6 +71,9 @@ export default {
const styleAlignedAllowed = (style === "any") || (style === "aligned"); const styleAlignedAllowed = (style === "any") || (style === "aligned");
const styleCompactAllowed = (style === "any") || (style === "compact"); const styleCompactAllowed = (style === "any") || (style === "compact");
const styleTightAllowed = (style === "any") || (style === "tight"); const styleTightAllowed = (style === "any") || (style === "tight");
const wideCharacter = params.config.wide_character;
const wideCharacterReString = (wideCharacter === undefined) ? defaultWideCharacterReString : wideCharacter;
const wideCharacterRe = new RegExp(wideCharacterReString, "gv");
// Scan all tables/rows // Scan all tables/rows
const tables = filterByTypesCached([ "table" ]); const tables = filterByTypesCached([ "table" ]);
@ -44,13 +85,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, wideCharacterRe);
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, wideCharacterRe);
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

@ -36,7 +36,7 @@
"build-config": "npm run build-config-schema && npm run build-config-example", "build-config": "npm run build-config-schema && npm run build-config-example",
"build-config-example": "node schema/build-config-example.mjs", "build-config-example": "node schema/build-config-example.mjs",
"build-config-schema": "node schema/build-config-schema.mjs", "build-config-schema": "node schema/build-config-schema.mjs",
"build-declaration": "tsc --allowJs --checkJs --declaration --emitDeclarationOnly --module nodenext --outDir dts --rootDir . --target es2015 lib/exports.mjs lib/exports-async.mjs lib/exports-promise.mjs lib/exports-sync.mjs lib/markdownlint.mjs lib/resolve-module.cjs && node scripts/index.mjs copy dts/lib/exports.d.mts lib/exports.d.mts && node scripts/index.mjs copy dts/lib/exports-async.d.mts lib/exports-async.d.mts && node scripts/index.mjs copy dts/lib/exports-promise.d.mts lib/exports-promise.d.mts && node scripts/index.mjs copy dts/lib/exports-sync.d.mts lib/exports-sync.d.mts && node scripts/index.mjs copy dts/lib/markdownlint.d.mts lib/markdownlint.d.mts && node scripts/index.mjs copy dts/lib/resolve-module.d.cts lib/resolve-module.d.cts && node scripts/index.mjs remove dts", "build-declaration": "tsc --allowJs --checkJs --declaration --emitDeclarationOnly --module nodenext --outDir dts --rootDir . --target es2024 lib/exports.mjs lib/exports-async.mjs lib/exports-promise.mjs lib/exports-sync.mjs lib/markdownlint.mjs lib/resolve-module.cjs && node scripts/index.mjs copy dts/lib/exports.d.mts lib/exports.d.mts && node scripts/index.mjs copy dts/lib/exports-async.d.mts lib/exports-async.d.mts && node scripts/index.mjs copy dts/lib/exports-promise.d.mts lib/exports-promise.d.mts && node scripts/index.mjs copy dts/lib/exports-sync.d.mts lib/exports-sync.d.mts && node scripts/index.mjs copy dts/lib/markdownlint.d.mts lib/markdownlint.d.mts && node scripts/index.mjs copy dts/lib/resolve-module.d.cts lib/resolve-module.d.cts && node scripts/index.mjs remove dts",
"build-demo": "node scripts/index.mjs copy node_modules/markdown-it/dist/markdown-it.min.js demo/markdown-it.min.js && cd demo && webpack --no-stats", "build-demo": "node scripts/index.mjs copy node_modules/markdown-it/dist/markdown-it.min.js demo/markdown-it.min.js && cd demo && webpack --no-stats",
"build-docs": "node doc-build/build-rules.mjs", "build-docs": "node doc-build/build-rules.mjs",
"ci": "npm-run-all --continue-on-error --parallel build-demo lint serial-config-docs serial-declaration test-cover && git diff --exit-code", "ci": "npm-run-all --continue-on-error --parallel build-demo lint serial-config-docs serial-declaration test-cover && git diff --exit-code",

View file

@ -340,6 +340,7 @@
// MD060/table-column-style : Table column style : https://github.com/DavidAnson/markdownlint/blob/v0.39.0/doc/md060.md // MD060/table-column-style : Table column style : https://github.com/DavidAnson/markdownlint/blob/v0.39.0/doc/md060.md
"MD060": { "MD060": {
// Table column style // Table column style
"style": "any" "style": "any",
// RegExp for matching wide character(s)
} }
} }

View file

@ -304,3 +304,4 @@ MD059:
MD060: MD060:
# Table column style # Table column style
style: "any" style: "any"
# RegExp for matching wide character(s)

View file

@ -645,6 +645,12 @@ for (const rule of rules) {
], ],
"default": "any" "default": "any"
}; };
// @ts-ignore
subscheme.properties.wide_character = {
"description": "RegExp for matching wide character(s)",
"type": "string",
"default": undefined
};
break; break;
default: default:
break; break;

View file

@ -4701,6 +4701,10 @@
"tight" "tight"
], ],
"default": "any" "default": "any"
},
"wide_character": {
"description": "RegExp for matching wide character(s)",
"type": "string"
} }
} }
} }
@ -4747,6 +4751,10 @@
"tight" "tight"
], ],
"default": "any" "default": "any"
},
"wide_character": {
"description": "RegExp for matching wide character(s)",
"type": "string"
} }
} }
} }

View file

@ -4701,6 +4701,10 @@
"tight" "tight"
], ],
"default": "any" "default": "any"
},
"wide_character": {
"description": "RegExp for matching wide character(s)",
"type": "string"
} }
} }
} }
@ -4747,6 +4751,10 @@
"tight" "tight"
], ],
"default": "any" "default": "any"
},
"wide_character": {
"description": "RegExp for matching wide character(s)",
"type": "string"
} }
} }
} }

View file

@ -1101,7 +1101,7 @@ test("readme", async(t) => {
}); });
test("validateJsonUsingConfigSchemaStrict", async(t) => { test("validateJsonUsingConfigSchemaStrict", async(t) => {
t.plan(219); t.plan(221);
// @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

@ -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,174 @@ Generated by [AVA](https://avajs.dev).
`, `,
} }
## table-column-style-wide-characters-custom.md
> Snapshot 1
{
errors: [
{
errorContext: null,
errorDetail: 'Table pipe does not align with heading for style "aligned"',
errorRange: [
6,
1,
],
fixInfo: null,
lineNumber: 12,
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 style "aligned"',
errorRange: [
11,
1,
],
fixInfo: null,
lineNumber: 12,
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 style "aligned"',
errorRange: [
11,
1,
],
fixInfo: null,
lineNumber: 13,
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 style "aligned"',
errorRange: [
6,
1,
],
fixInfo: null,
lineNumber: 14,
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 style "aligned"',
errorRange: [
11,
1,
],
fixInfo: null,
lineNumber: 14,
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 - Wide Characters (Custom)␊
| NN | W |␊
| -- | -- |␊
| NN | NN |␊
| W | NN |␊
| NN | W |␊
| W | W |␊
| ✅N | NN |␊
| NN | ✅N |␊
| ✅N | ✅N |␊
| WW | NN |␊
| NN | WW |␊
| WW | WW |␊
{MD060:-4} {MD060:-3} {MD060:-2}␊
<!-- markdownlint-configure-file {␊
"table-column-style": {␊
"style": "aligned",␊
"wide_character": "[W]"␊
}␊
} -->␊
`,
}
## 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 | ✅ |␊
| ✅ | ✅ |␊
## CJK␊
TODO...␊
<!-- markdownlint-configure-file {␊
"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,23 @@
# Table Column Style - Wide Characters (Custom)
| NN | W |
| -- | -- |
| NN | NN |
| W | NN |
| NN | W |
| W | W |
| ✅N | NN |
| NN | ✅N |
| ✅N | ✅N |
| WW | NN |
| NN | WW |
| WW | WW |
{MD060:-4} {MD060:-3} {MD060:-2}
<!-- markdownlint-configure-file {
"table-column-style": {
"style": "aligned",
"wide_character": "[W]"
}
} -->

View file

@ -0,0 +1,41 @@
# 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 | ✅ |
| ✅ | ✅ |
## CJK
TODO...
<!-- markdownlint-configure-file {
"table-column-style": {
"style": "aligned"
}
} -->