Improve emoji width measurement (#17813)

Co-authored-by: fisker <lionkay@gmail.com>
This commit is contained in:
Seiyaandfisker authored and GitHub committed 2025-09-18 20:12:26 +08:00
1 parent 5067313d68
commit 99cd3d7426
6 files changed
+185 -1

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
#### Improve emoji size measurement (#17813 by @seiyab)
This change improves table alignment in all languages.
<!-- prettier-ignore -->
```md
<!-- Input -->
| | |
| :-: | :-: |
| ✔ | ✘ |
| ✘ | ✔ |
| ✔ | ✘ |
<!-- Input -->
| | |
| :-: | :-: |
| ✔ | ✘ |
| ✘ | ✔ |
| ✔ | ✘ |
<!-- Prettier main -->
| | |
| :-: | :-: |
| ✔ | ✘ |
| ✘ | ✔ |
| ✔ | ✘ |
```
+10 -1
View File
@@ -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;
}
+117
View File
@@ -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("");
@@ -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"]
@@ -0,0 +1,5 @@
| | |
| :-: | :-: |
| ✔ | ✘ |
| ✘ | ✔ |
| ✔ | ✘ |
@@ -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", () => {