diff --git a/doc-build/md060.md b/doc-build/md060.md index 4adc754e..0cff46bc 100644 --- a/doc-build/md060.md +++ b/doc-build/md060.md @@ -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 *fewest* errors (i.e., whichever style is the closest match). -Note: Pipe alignment for the `aligned` style is based on character count, so -wide characters and multi-character encodings can produce unexpected results. +Note: Pipe alignment for the `aligned` style is based on ... The following table is correctly aligned based on character count, though some editors render the emoji wider: @@ -48,8 +47,8 @@ editors render the emoji wider: ```markdown | Response | Emoji | | -------- | ----- | -| Yes | ✅ | -| No | ❎ | +| Yes | ✅ | +| No | ❎ | ``` diff --git a/doc/Rules.md b/doc/Rules.md index e938844a..5cbbe072 100644 --- a/doc/Rules.md +++ b/doc/Rules.md @@ -2696,6 +2696,8 @@ Parameters: - `style`: Table column style (`string`, default `any`, values `aligned` / `any` / `compact` / `tight`) +- `wide_character`: RegExp for matching wide character(s) (`string`, default + `undefined`) This rule is triggered when the column separators of a [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 *fewest* errors (i.e., whichever style is the closest match). -Note: Pipe alignment for the `aligned` style is based on character count, so -wide characters and multi-character encodings can produce unexpected results. +Note: Pipe alignment for the `aligned` style is based on ... The following table is correctly aligned based on character count, though some editors render the emoji wider: @@ -2747,8 +2748,8 @@ editors render the emoji wider: ```markdown | Response | Emoji | | -------- | ----- | -| Yes | ✅ | -| No | ❎ | +| Yes | ✅ | +| No | ❎ | ``` diff --git a/doc/md060.md b/doc/md060.md index 3bae44b8..e2adbf3b 100644 --- a/doc/md060.md +++ b/doc/md060.md @@ -8,6 +8,8 @@ Parameters: - `style`: Table column style (`string`, default `any`, values `aligned` / `any` / `compact` / `tight`) +- `wide_character`: RegExp for matching wide character(s) (`string`, default + `undefined`) This rule is triggered when the column separators of a [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 *fewest* errors (i.e., whichever style is the closest match). -Note: Pipe alignment for the `aligned` style is based on character count, so -wide characters and multi-character encodings can produce unexpected results. +Note: Pipe alignment for the `aligned` style is based on ... The following table is correctly aligned based on character count, though some editors render the emoji wider: @@ -59,8 +60,8 @@ editors render the emoji wider: ```markdown | Response | Emoji | | -------- | ----- | -| Yes | ✅ | -| No | ❎ | +| Yes | ✅ | +| No | ❎ | ``` diff --git a/lib/configuration-strict.d.ts b/lib/configuration-strict.d.ts index 563e22ed..bb1fce10 100644 --- a/lib/configuration-strict.d.ts +++ b/lib/configuration-strict.d.ts @@ -2281,6 +2281,10 @@ export interface ConfigurationStrict { * Table column style */ 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 @@ -2301,6 +2305,10 @@ export interface ConfigurationStrict { * Table column style */ 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 diff --git a/lib/md060.mjs b/lib/md060.mjs index f6b95785..cac49744 100644 --- a/lib/md060.mjs +++ b/lib/md060.mjs @@ -4,8 +4,46 @@ import { filterByTypes } from "../helpers/micromark-helpers.cjs"; import { filterByTypesCached } from "./cache.mjs"; /** @typedef {import("micromark-extension-gfm-table")} */ +/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */ /** @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. * @@ -33,6 +71,9 @@ export default { const styleAlignedAllowed = (style === "any") || (style === "aligned"); const styleCompactAllowed = (style === "any") || (style === "compact"); 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 const tables = filterByTypesCached([ "table" ]); @@ -44,13 +85,13 @@ export default { /** @type {RuleOnErrorInfo[]} */ const errorsIfAligned = []; 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)) { - const remainingHeadingDividerColumns = new Set(headingDividerColumns); - const rowDividerColumns = filterByTypes(row.children, [ "tableCellDivider" ]).map((divider) => divider.startColumn); + const remainingHeadingDividerColumns = new Set(headingDividerColumns.map((column) => column.effective)); + const rowDividerColumns = getTableDividerColumns(params.lines, row, wideCharacterRe); for (const dividerColumn of rowDividerColumns) { - if ((remainingHeadingDividerColumns.size > 0) && !remainingHeadingDividerColumns.delete(dividerColumn)) { - addError(errorsIfAligned, row.startLine, dividerColumn, "Table pipe does not align with heading for style \"aligned\""); + if ((remainingHeadingDividerColumns.size > 0) && !remainingHeadingDividerColumns.delete(dividerColumn.effective)) { + addError(errorsIfAligned, row.startLine, dividerColumn.actual, "Table pipe does not align with heading for style \"aligned\""); } } } diff --git a/package.json b/package.json index 13dde923..bdb665ac 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "build-config": "npm run build-config-schema && npm run build-config-example", "build-config-example": "node schema/build-config-example.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-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", diff --git a/schema/.markdownlint.jsonc b/schema/.markdownlint.jsonc index 831440ad..2998d2cd 100644 --- a/schema/.markdownlint.jsonc +++ b/schema/.markdownlint.jsonc @@ -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 - "style": "any" + "style": "any", + // RegExp for matching wide character(s) } } \ No newline at end of file diff --git a/schema/.markdownlint.yaml b/schema/.markdownlint.yaml index e769041e..b6fa4d70 100644 --- a/schema/.markdownlint.yaml +++ b/schema/.markdownlint.yaml @@ -304,3 +304,4 @@ MD059: MD060: # Table column style style: "any" + # RegExp for matching wide character(s) diff --git a/schema/build-config-schema.mjs b/schema/build-config-schema.mjs index 3b5a7c2b..94713d2a 100644 --- a/schema/build-config-schema.mjs +++ b/schema/build-config-schema.mjs @@ -645,6 +645,12 @@ for (const rule of rules) { ], "default": "any" }; + // @ts-ignore + subscheme.properties.wide_character = { + "description": "RegExp for matching wide character(s)", + "type": "string", + "default": undefined + }; break; default: break; diff --git a/schema/markdownlint-config-schema-strict.json b/schema/markdownlint-config-schema-strict.json index f9c0a250..3be0aaff 100644 --- a/schema/markdownlint-config-schema-strict.json +++ b/schema/markdownlint-config-schema-strict.json @@ -4701,6 +4701,10 @@ "tight" ], "default": "any" + }, + "wide_character": { + "description": "RegExp for matching wide character(s)", + "type": "string" } } } @@ -4747,6 +4751,10 @@ "tight" ], "default": "any" + }, + "wide_character": { + "description": "RegExp for matching wide character(s)", + "type": "string" } } } diff --git a/schema/markdownlint-config-schema.json b/schema/markdownlint-config-schema.json index 91b2bc2f..2213b075 100644 --- a/schema/markdownlint-config-schema.json +++ b/schema/markdownlint-config-schema.json @@ -4701,6 +4701,10 @@ "tight" ], "default": "any" + }, + "wide_character": { + "description": "RegExp for matching wide character(s)", + "type": "string" } } } @@ -4747,6 +4751,10 @@ "tight" ], "default": "any" + }, + "wide_character": { + "description": "RegExp for matching wide character(s)", + "type": "string" } } } diff --git a/test/markdownlint-test.mjs b/test/markdownlint-test.mjs index f53bf9a0..af0275e3 100644 --- a/test/markdownlint-test.mjs +++ b/test/markdownlint-test.mjs @@ -1101,7 +1101,7 @@ test("readme", async(t) => { }); test("validateJsonUsingConfigSchemaStrict", async(t) => { - t.plan(219); + t.plan(221); // @ts-ignore const ajv = new Ajv(ajvOptions); const validateSchemaStrict = ajv.compile(configSchemaStrict); diff --git a/test/snapshots/markdownlint-test-scenarios.mjs.md b/test/snapshots/markdownlint-test-scenarios.mjs.md index da599a58..dbc453c7 100644 --- a/test/snapshots/markdownlint-test-scenarios.mjs.md +++ b/test/snapshots/markdownlint-test-scenarios.mjs.md @@ -67803,7 +67803,7 @@ Generated by [AVA](https://avajs.dev). errorContext: null, errorDetail: 'Table pipe does not align with heading for style "aligned"', errorRange: [ - 19, + 20, 1, ], fixInfo: null, @@ -67857,9 +67857,9 @@ Generated by [AVA](https://avajs.dev). ␊ | Response | Emoji |␊ | -------- | ----- |␊ - | Yes | ✅ |␊ - | No | ❎ |␊ - | Oops | ❌ |␊ + | Yes | ✅ |␊ + | No | ❎ |␊ + | Oops | ❌ |␊ ␊ {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}␊ + ␊ + ␊ + `, + } + +## 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...␊ + ␊ + ␊ + `, + } + ## table-content-with-issues.md > Snapshot 1 diff --git a/test/snapshots/markdownlint-test-scenarios.mjs.snap b/test/snapshots/markdownlint-test-scenarios.mjs.snap index 38d5fd73..db4654b9 100644 Binary files a/test/snapshots/markdownlint-test-scenarios.mjs.snap and b/test/snapshots/markdownlint-test-scenarios.mjs.snap differ diff --git a/test/table-column-style-emoji.md b/test/table-column-style-emoji.md index 58120159..7fc0ffc5 100644 --- a/test/table-column-style-emoji.md +++ b/test/table-column-style-emoji.md @@ -4,9 +4,9 @@ | Response | Emoji | | -------- | ----- | -| Yes | ✅ | -| No | ❎ | -| Oops | ❌ | +| Yes | ✅ | +| No | ❎ | +| Oops | ❌ | {MD060:-2} diff --git a/test/table-column-style-wide-characters-custom.md b/test/table-column-style-wide-characters-custom.md new file mode 100644 index 00000000..d0823836 --- /dev/null +++ b/test/table-column-style-wide-characters-custom.md @@ -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} + + diff --git a/test/table-column-style-wide-characters.md b/test/table-column-style-wide-characters.md new file mode 100644 index 00000000..ee61596a --- /dev/null +++ b/test/table-column-style-wide-characters.md @@ -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... + +