This commit is contained in:
David Anson 2025-11-24 13:58:48 -08:00
parent 4447540366
commit 3ea07a7614
15 changed files with 15 additions and 363 deletions

View file

@ -2,58 +2,34 @@
import { filterByTypes } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
import stringWidth from "string-width";
/** @typedef {import("micromark-extension-gfm-table")} */
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
/** @typedef {import("markdownlint").RuleOnErrorInfo} RuleOnErrorInfo */
const regExpFlags = "gv";
const anyCharacterRe = new RegExp("[\\s\\S]", regExpFlags);
// See:
// https://www.unicode.org/reports/tr11/
// https://unicode.org/reports/tr24/
// https://unicode.org/reports/tr51/
// https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEast_Asian_Width%3DFullwidth%3A%5D&abb=on&esc=on&g=&i=
// Notes:
// The East_Asian_Width property is not supported (seemingly at all) by JavaScript, so East_Asian_Width=Fullwidth ranges are matched directly:
// https://github.com/tc39/proposal-regexp-unicode-property-escapes/issues/28
// As an alternative to matching by Script names, consider matching East_Asian_Width=Wide (Wide is a superset of Fullwidth) directly as well:
// https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEast_Asian_Width%3DWide%3A%5D&abb=on&esc=on&g=&i=
const defaultWideCharacterReString = "[\\p{RGI_Emoji}\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}\\u3000\\uFF01-\\uFF60\\uFFE0-\\uFFE6]";
/**
* @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} wideCharacterRe Wide character RegExp.
* @returns {number} Effective column (1-based).
*/
function effectiveColumn(line, column, wideCharacterRe) {
const span = line.slice(0, column - 1);
const totalCharacterCount = (span.match(anyCharacterRe) || []).length;
const wideCharacterCount = (span.match(wideCharacterRe) || []).length;
return totalCharacterCount + wideCharacterCount;
}
/**
* Gets a list of table cell divider columns.
*
* @param {readonly string[]} lines File/string lines.
* @param {MicromarkToken} row Micromark row token.
* @param {RegExp} wideCharacterRe Wide character RegExp.
* @returns {Column[]} Divider columns.
*/
function getTableDividerColumns(lines, row, wideCharacterRe) {
function getTableDividerColumns(lines, row) {
return filterByTypes(
row.children,
[ "tableCellDivider" ]).map((divider) => ({ "actual": divider.startColumn, "effective": effectiveColumn(lines[row.startLine - 1], divider.startColumn, wideCharacterRe) })
[ "tableCellDivider" ]
).map(
(divider) => ({
"actual": divider.startColumn,
"effective": stringWidth(lines[row.startLine - 1].slice(0, divider.startColumn - 1))
})
);
}
@ -84,7 +60,6 @@ export default {
const styleAlignedAllowed = (style === "any") || (style === "aligned");
const styleCompactAllowed = (style === "any") || (style === "compact");
const styleTightAllowed = (style === "any") || (style === "tight");
const wideCharacterRe = new RegExp(params.config.wide_character || defaultWideCharacterReString, regExpFlags);
// Scan all tables/rows
const tables = filterByTypesCached([ "table" ]);
@ -96,10 +71,10 @@ export default {
/** @type {RuleOnErrorInfo[]} */
const errorsIfAligned = [];
if (styleAlignedAllowed) {
const headingDividerColumns = getTableDividerColumns(params.lines, headingRow, wideCharacterRe);
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, wideCharacterRe);
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\"");