Handle zero-width marks in string width (#19538)

Co-authored-by: fisker Cheung <lionkay@gmail.com>
This commit is contained in:
Alexander Lichterandfisker Cheung authored and GitHub committed 2026-07-10 19:20:54 +08:00
1 parent e46109d602
commit fdd6965b2e
6 files changed
+64 -10

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
#### Handle Unicode zero-width marks in string width (#19538 by @TheAlexLichter)
Prettier now treats Unicode nonspacing and enclosing marks as zero-width when measuring lines.
<!-- prettier-ignore -->
```js
// Input
moment.updateLocale("bo", { monthsShort: "ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢".split("_") });
// Prettier stable
moment.updateLocale("bo", {
monthsShort:
"ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢".split("_"),
});
// Prettier main
moment.updateLocale("bo", {
monthsShort: "ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢".split("_"),
});
```
+4 -7
View File
@@ -8,6 +8,8 @@ import {
import { isNarrowEmojiCharacter } from "narrow-emojis";
const notAsciiRegex = /[^\x20-\x7F]/;
// Exclude [`Spacing Mark`](https://www.compart.com/en/unicode/category/Mc) because spacing marks contribute horizontal width.
const zeroWidthMarkRegex = /[\p{Nonspacing_Mark}\p{Enclosing_Mark}]/u;
// Similar to https://github.com/sindresorhus/string-width
// We don't strip ansi, always treat ambiguous width characters as having narrow width.
@@ -42,13 +44,8 @@ function getStringWidth(text) {
continue;
}
// Ignore combining characters
if (codePoint >= 0x300 && codePoint <= 0x36f) {
continue;
}
// Ignore Variation Selectors
if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) {
// Ignore zero-width marks, including combining marks and variation selectors
if (zeroWidthMarkRegex.test(character)) {
continue;
}
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`string-width.js format 1`] = `
====================================options=====================================
parsers: ["babel", "flow", "typescript"]
printWidth: 80 (default) |
=====================================input======================================
should___fit_____________("ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢");
should_break_____________("ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢ ");
// https://github.com/react/react/blob/06b2a50f321502514443f675eaa44be8ae826218/fixtures/fizz/src/BigComponent.js#L154
should___fit_____("यह Google अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित टेक्स्ट ब्लॉक है।");
should_break_____("यह Google अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित टेक्स्ट ब्लॉक है। ");
=====================================output=====================================
should___fit_____________("ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢");
should_break_____________(
"ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢ ",
);
// https://github.com/react/react/blob/06b2a50f321502514443f675eaa44be8ae826218/fixtures/fizz/src/BigComponent.js#L154
should___fit_____("यह Google अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित टेक्स्ट ब्लॉक है।");
should_break_____(
"यह Google अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित टेक्स्ट ब्लॉक है। ",
);
================================================================================
`;
@@ -0,0 +1 @@
runFormatTest(import.meta, ["babel", "flow", "typescript"]);
@@ -0,0 +1,6 @@
should___fit_____________("ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢");
should_break_____________("ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢ ");
// https://github.com/react/react/blob/06b2a50f321502514443f675eaa44be8ae826218/fixtures/fizz/src/BigComponent.js#L154
should___fit_____("यह Google अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित टेक्स्ट ब्लॉक है।");
should_break_____("यह Google अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित टेक्स्ट ब्लॉक है। ");
@@ -71,9 +71,7 @@ test("sharedUtil.getStringWidth", () => {
expect(getStringWidth("\u{2194}\u{FE0F}")).toBe(2);
expect(getStringWidth("\u{1F469}")).toBe(2);
expect(getStringWidth("\u{1F469}\u{1F3FF}")).toBe(2);
// Ideally this should be `2`, switch to use `Intl.Segmenter` will fix it
// https://github.com/prettier/prettier/pull/14793#discussion_r1185840038
expect(getStringWidth("\u{845B}\u{E0100}")).toBe(3);
expect(getStringWidth("\u{845B}\u{E0100}")).toBe(2);
expect(getStringWidth(String.fromCharCode(0))).toBe(0);
expect(getStringWidth(String.fromCharCode(31))).toBe(0);
@@ -82,6 +80,10 @@ test("sharedUtil.getStringWidth", () => {
expect(getStringWidth(String.fromCharCode(159))).toBe(0);
expect(getStringWidth("\u001B")).toBe(0);
expect(getStringWidth("x\u0300")).toBe(1);
expect(getStringWidth("ཟླ")).toBe(1);
expect(getStringWidth("a\u20DD")).toBe(1);
expect(getStringWidth("\u093E")).toBe(1);
expect(getStringWidth("\u0915\u093E")).toBe(2);
expect(getStringWidth("👶")).toBe(2);
expect(getStringWidth("👶🏽")).toBe(2);