From 99cd3d7426dd78ce7e6a94753c797d3f35e6675f Mon Sep 17 00:00:00 2001 From: Seiya <20365512+seiyab@users.noreply.github.com> Date: Thu, 18 Sep 2025 21:12:26 +0900 Subject: [PATCH] Improve emoji width measurement (#17813) Co-authored-by: fisker --- changelog_unreleased/markdown/17813.md | 28 +++++ src/utils/get-string-width.js | 11 +- src/utils/narrow-emojis.evaluate.js | 117 ++++++++++++++++++ .../table/__snapshots__/format.test.js.snap | 22 ++++ tests/format/markdown/table/issue-15572.md | 5 + tests/integration/__tests__/util-shared.js | 3 + 6 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 changelog_unreleased/markdown/17813.md create mode 100644 src/utils/narrow-emojis.evaluate.js create mode 100644 tests/format/markdown/table/issue-15572.md diff --git a/changelog_unreleased/markdown/17813.md b/changelog_unreleased/markdown/17813.md new file mode 100644 index 000000000..199a61cb1 --- /dev/null +++ b/changelog_unreleased/markdown/17813.md @@ -0,0 +1,28 @@ +#### Improve emoji size measurement (#17813 by @seiyab) + +This change improves table alignment in all languages. + + +```md + +| | | +| :-: | :-: | +| ✔ | ✘ | +| ✘ | ✔ | +| ✔ | ✘ | + + +| | | +| :-: | :-: | +| ✔ | ✘ | +| ✘ | ✔ | +| ✔ | ✘ | + + + +| | | +| :-: | :-: | +| ✔ | ✘ | +| ✘ | ✔ | +| ✔ | ✘ | +``` diff --git a/src/utils/get-string-width.js b/src/utils/get-string-width.js index 70fe8ebeb..b1f24a7ce 100644 --- a/src/utils/get-string-width.js +++ b/src/utils/get-string-width.js @@ -5,8 +5,10 @@ import { // @ts-expect-error -- Private _isWide as isWide, } from "get-east-asian-width"; +import narrowEmojis from "./narrow-emojis.evaluate.js"; const notAsciiRegex = /[^\x20-\x7F]/u; +const narrowEmojisSet = new Set(narrowEmojis); // Similar to https://github.com/sindresorhus/string-width // We don't strip ansi, always treat ambiguous width characters as having narrow width. @@ -24,7 +26,9 @@ function getStringWidth(text) { return text.length; } - text = text.replace(emojiRegex(), " "); + text = text.replace(emojiRegex(), (match) => + narrowEmojisSet.has(match) ? " " : " ", + ); let width = 0; // Use `Intl.Segmenter` when we drop support for Node.js v14 @@ -43,6 +47,11 @@ function getStringWidth(text) { continue; } + // Ignore Variation Selectors + if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) { + continue; + } + width += isFullWidth(codePoint) || isWide(codePoint) ? 2 : 1; } diff --git a/src/utils/narrow-emojis.evaluate.js b/src/utils/narrow-emojis.evaluate.js new file mode 100644 index 000000000..27b678337 --- /dev/null +++ b/src/utils/narrow-emojis.evaluate.js @@ -0,0 +1,117 @@ +import assert from "node:assert"; + +/** + * How the narrow emoji list was generated: + * 1. Start with Unicode's official emoji list https://unicode.org/Public/emoji/latest/emoji-test.txt + * 2. Select emojis that: + * - consist of a single code point, and + * - also have a variant with U+FE0F. + * 3. Manually remove any false positives. + */ +const narrowEmojis = [ + "\u{263A}", // ☺ + "\u{2639}", // ☹ + "\u{2620}", // ☠ + "\u{2763}", // ❣ + "\u{2764}", // ❤ + "\u{270C}", // ✌ + "\u{261D}", // ☝ + "\u{270D}", // ✍ + "\u{26F7}", // ⛷ + "\u{26F9}", // ⛹ + "\u{2618}", // ☘ + "\u{26E9}", // ⛩ + "\u{2668}", // ♨ + "\u{2708}", // ✈ + "\u{23F1}", // ⏱ + "\u{23F2}", // ⏲ + "\u{2600}", // ☀ + "\u{2601}", // ☁ + "\u{26C8}", // ⛈ + "\u{2602}", // ☂ + "\u{26F1}", // ⛱ + "\u{2744}", // ❄ + "\u{2603}", // ☃ + "\u{2604}", // ☄ + "\u{26F8}", // ⛸ + "\u{2660}", // ♠ + "\u{2665}", // ♥ + "\u{2666}", // ♦ + "\u{2663}", // ♣ + "\u{265F}", // ♟ + "\u{26D1}", // ⛑ + "\u{260E}", // ☎ + "\u{2328}", // ⌨ + "\u{2709}", // ✉ + "\u{270F}", // ✏ + "\u{2712}", // ✒ + "\u{2702}", // ✂ + "\u{26CF}", // ⛏ + "\u{2692}", // ⚒ + "\u{2694}", // ⚔ + "\u{2699}", // ⚙ + "\u{2696}", // ⚖ + "\u{26D3}", // ⛓ + "\u{2697}", // ⚗ + "\u{26B0}", // ⚰ + "\u{26B1}", // ⚱ + "\u{26A0}", // ⚠ + "\u{2622}", // ☢ + "\u{2623}", // ☣ + "\u{2B06}", // ⬆ + "\u{2197}", // ↗ + "\u{27A1}", // ➡ + "\u{2198}", // ↘ + "\u{2B07}", // ⬇ + "\u{2199}", // ↙ + "\u{2B05}", // ⬅ + "\u{2196}", // ↖ + "\u{2195}", // ↕ + "\u{2194}", // ↔ + "\u{21A9}", // ↩ + "\u{21AA}", // ↪ + "\u{2934}", // ⤴ + "\u{2935}", // ⤵ + "\u{269B}", // ⚛ + "\u{2721}", // ✡ + "\u{2638}", // ☸ + "\u{262F}", // ☯ + "\u{271D}", // ✝ + "\u{2626}", // ☦ + "\u{262A}", // ☪ + "\u{262E}", // ☮ + "\u{25B6}", // ▶ + "\u{25C0}", // ◀ + "\u{23F8}", // ⏸ + "\u{23F9}", // ⏹ + "\u{23FA}", // ⏺ + "\u{23CF}", // ⏏ + "\u{2640}", // ♀ + "\u{2642}", // ♂ + "\u{26A7}", // ⚧ + "\u{2716}", // ✖ + "\u{267E}", // ♾ + "\u{203C}", // ‼ + "\u{2049}", // ⁉ + "\u{2695}", // ⚕ + "\u{267B}", // ♻ + "\u{269C}", // ⚜ + "\u{2611}", // ☑ + "\u{2714}", // ✔ + "\u{2733}", // ✳ + "\u{2734}", // ✴ + "\u{2747}", // ❇ + "\u{00A9}", // © + "\u{00AE}", // ® + "\u{2122}", // ™ + "\u{2139}", // ℹ + "\u{25FC}", // ◼ + "\u{25FB}", // ◻ + "\u{25AA}", // ▪ + "\u{25AB}", // ▫ +]; + +assert.equal(new Set(narrowEmojis).size, narrowEmojis.length); +assert.equal(narrowEmojis.join("").length, narrowEmojis.length); + +export default narrowEmojis.sort().join(""); diff --git a/tests/format/markdown/table/__snapshots__/format.test.js.snap b/tests/format/markdown/table/__snapshots__/format.test.js.snap index 6760e6b21..492a83221 100644 --- a/tests/format/markdown/table/__snapshots__/format.test.js.snap +++ b/tests/format/markdown/table/__snapshots__/format.test.js.snap @@ -116,6 +116,28 @@ Default | CLI Override | API Override ================================================================================ `; +exports[`issue-15572.md - {"proseWrap":"always"} format 1`] = ` +====================================options===================================== +parsers: ["markdown"] +printWidth: 80 +proseWrap: "always" + | printWidth +=====================================input====================================== +| | | +| :-: | :-: | +| ✔ | ✘ | +| ✘ | ✔ | +| ✔ | ✘ | +=====================================output===================================== +| | | +| :-: | :-: | +| ✔ | ✘ | +| ✘ | ✔ | +| ✔ | ✘ | + +================================================================================ +`; + exports[`simple.md - {"proseWrap":"always"} format 1`] = ` ====================================options===================================== parsers: ["markdown"] diff --git a/tests/format/markdown/table/issue-15572.md b/tests/format/markdown/table/issue-15572.md new file mode 100644 index 000000000..984349fad --- /dev/null +++ b/tests/format/markdown/table/issue-15572.md @@ -0,0 +1,5 @@ +| | | +| :-: | :-: | +| ✔ | ✘ | +| ✘ | ✔ | +| ✔ | ✘ | \ No newline at end of file diff --git a/tests/integration/__tests__/util-shared.js b/tests/integration/__tests__/util-shared.js index 4fdf3ab37..5cf9f7aaa 100644 --- a/tests/integration/__tests__/util-shared.js +++ b/tests/integration/__tests__/util-shared.js @@ -87,6 +87,9 @@ test("sharedUtil.getStringWidth", () => { expect(getStringWidth("👶🏽")).toBe(2); expect(getStringWidth("👩‍👩‍👦‍👦")).toBe(2); expect(getStringWidth("👨‍❤️‍💋‍👨")).toBe(2); + + expect(getStringWidth("©︎")).toBe(1); + expect(getStringWidth("©️")).toBe(2); }); test("sharedUtil.getAlignmentSize", () => {