Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions lib/md060.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,37 @@

import { filterByTypes } from "../helpers/micromark-helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";
import stringWidth from "string-width";

// Emoji and other characters with double visual width (width 2)
// Extended_Pictographic covers graphical emoji; the remaining ranges cover
// the Wide/Fullwidth values of UAX #11 (East Asian Width, Unicode 17.0):
// https://www.unicode.org/reports/tr11/
// Note that arrows (U+2190-U+21FF) are "Ambiguous" in UAX #11 but are
// rendered double-width in CJK contexts, so they are included here.
// The U+1F200-U+1F265 range is kept explicit because only part of it is
// Extended_Pictographic (the lint rule's Unicode data disagrees with V8).
// eslint-disable-next-line regexp/no-dupe-characters-character-class
const doubleWidthRe = /[\p{Extended_Pictographic}\u{1100}-\u{115F}\u{2190}-\u{21FF}\u{2329}\u{232A}\u{2E80}-\u{2FFF}\u{3000}-\u{9FFF}\u{A000}-\u{A4CF}\u{A960}-\u{A97F}\u{AC00}-\u{D7FF}\u{F900}-\u{FAFF}\u{FE10}-\u{FE19}\u{FE30}-\u{FE6B}\u{FF01}-\u{FF60}\u{FFE0}-\u{FFE6}\u{16FE0}-\u{16FF6}\u{17000}-\u{18D1E}\u{1AFF0}-\u{1AFFE}\u{1B000}-\u{1B2FB}\u{1D300}-\u{1D376}\u{1F200}-\u{1F265}\u{20000}-\u{3FFFD}]/gu;

const segmenter = new Intl.Segmenter();

/**
* Gets the visual width of a string (number of columns to display it).
*
* Computed as the number of grapheme clusters plus the number of
* double-width characters (emoji and UAX #11 Wide/Fullwidth characters).
*
* @param {string} text Input text.
* @returns {number} Visual width.
*/
function visualWidth(text) {
let graphemes = 0;
// eslint-disable-next-line no-unused-vars
for (const unused of segmenter.segment(text)) {
graphemes++;
}
return graphemes + (text.match(doubleWidthRe)?.length ?? 0);
}

/** @typedef {typeof import("micromark-extension-gfm-table")} _ambient_ */
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
Expand Down Expand Up @@ -47,7 +77,7 @@ function getTableDividerColumns(lines, row) {
).map(
(divider) => ({
"actual": divider.startColumn,
"effective": stringWidth(lines[row.startLine - 1].slice(0, divider.startColumn - 1))
"effective": visualWidth(lines[row.startLine - 1].slice(0, divider.startColumn - 1))
})
);
}
Expand Down
2 changes: 1 addition & 1 deletion test/markdownlint-test-scenarios.mjs.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -60518,7 +60518,7 @@ exports[`markdownlint-test-scenarios.mjs > table-column-style-trailing-spaces.md
exports[`markdownlint-test-scenarios.mjs > table-column-style-wide-characters.md 1`] = `
{
"errors": [],
"fixed": "# Table Column Style - Wide Characters\\n\\n## Emoji\\n\\n| AB | CD |\\n| -- | -- |\\n| EF | GH |\\n| ✅ | KL |\\n| MN | ✅ |\\n| ✅ | ✅ |\\n\\n| ✅ | CD |\\n| -- | -- |\\n| EF | GH |\\n| ✅ | KL |\\n| MN | ✅ |\\n| ✅ | ✅ |\\n\\n| AB | ✅ |\\n| -- | -- |\\n| EF | GH |\\n| ✅ | KL |\\n| MN | ✅ |\\n| ✅ | ✅ |\\n\\n| ✅ | ✅ |\\n| -- | -- |\\n| EF | GH |\\n| ✅ | KL |\\n| MN | ✅ |\\n| ✅ | ✅ |\\n\\n## Hello World\\n\\n| Language | Translation |\\n|---------------------|----------------|\\n| Emoji | 👋🌎 |\\n| Portuguese (Brazil) | Olá mundo |\\n| Turkish | Merhaba dünya |\\n| Chinese (Mandarin) | 你好,世界 |\\n| Japanese | こんにちは世界 |\\n| Korean | 안녕 세상 |\\n\\n## ANSI Escape Codes\\n\\n| Style | Escape codes |\\n| --------- | ------------------------------------------------------- |\\n| Bold | \\\\u001B[1m大胆な\\\\u001B[22m |\\n| Underline | \\\\u001B[4mUnderline\\\\u001B[0m |\\n| Link | \\\\u001B]8;;https://example.com\\\\u0007Link\\\\u001B]8;;\\\\u0007 |\\n\\n<!-- markdownlint-configure-file {\\n \\"no-bare-urls\\": false,\\n \\"table-column-style\\": {\\n \\"style\\": \\"aligned\\"\\n }\\n} -->\\n"
"fixed": "# Table Column Style - Wide Characters\\n\\n## Emoji\\n\\n| AB | CD |\\n| -- | -- |\\n| EF | GH |\\n| ✅ | KL |\\n| MN | ✅ |\\n| ✅ | ✅ |\\n\\n| ✅ | CD |\\n| -- | -- |\\n| EF | GH |\\n| ✅ | KL |\\n| MN | ✅ |\\n| ✅ | ✅ |\\n\\n| AB | ✅ |\\n| -- | -- |\\n| EF | GH |\\n| ✅ | KL |\\n| MN | ✅ |\\n| ✅ | ✅ |\\n\\n| ✅ | ✅ |\\n| -- | -- |\\n| EF | GH |\\n| ✅ | KL |\\n| MN | ✅ |\\n| ✅ | ✅ |\\n\\n## Hello World\\n\\n| Language | Translation |\\n|---------------------|----------------|\\n| Emoji | 👋🌎 |\\n| Portuguese (Brazil) | Olá mundo |\\n| Turkish | Merhaba dünya |\\n| Chinese (Mandarin) | 你好,世界 |\\n| Japanese | こんにちは世界 |\\n| Korean | 안녕 세상 |\\n\\n## ANSI Escape Codes\\n\\n| Style | Escape codes |\\n| --------- | ------------------------------------------------------- |\\n| Bold | \\\\u001B[1m大胆な\\\\u001B[22m |\\n| Underline | \\\\u001B[4mUnderline\\\\u001B[0m |\\n| Link | \\\\u001B]8;;https://example.com\\\\u0007Link\\\\u001B]8;;\\\\u0007 |\\n\\n## Arrows\\n\\n| Dir | Icon |\\n| --- | ---- |\\n| Up | ↑ |\\n| Dn | ↓ |\\n| LfR | ←→ |\\n\\n## Rare CJK\\n\\n| Rare | Note |\\n| ---- | ---- |\\n| 𠀀 | ExtB |\\n| 〇 | Zero |\\n\\n<!-- markdownlint-configure-file {\\n \\"no-bare-urls\\": false,\\n \\"table-column-style\\": {\\n \\"style\\": \\"aligned\\"\\n }\\n} -->\\n"
}
`;

Expand Down
15 changes: 15 additions & 0 deletions test/table-column-style-wide-characters.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@
| Underline | \u001B[4mUnderline\u001B[0m |
| Link | \u001B]8;;https://example.com\u0007Link\u001B]8;;\u0007 |

## Arrows

| Dir | Icon |
| --- | ---- |
| Up | ↑ |
| Dn | ↓ |
| LfR | ←→ |

## Rare CJK

| Rare | Note |
| ---- | ---- |
| 𠀀 | ExtB |
| 〇 | Zero |

<!-- markdownlint-configure-file {
"no-bare-urls": false,
"table-column-style": {
Expand Down