diff --git a/changelog_unreleased/misc/19538.md b/changelog_unreleased/misc/19538.md new file mode 100644 index 000000000..37875a14f --- /dev/null +++ b/changelog_unreleased/misc/19538.md @@ -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. + + +```js +// Input +moment.updateLocale("bo", { monthsShort: "ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢".split("_") }); + +// Prettier stable +moment.updateLocale("bo", { + monthsShort: + "ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢".split("_"), +}); + +// Prettier main +moment.updateLocale("bo", { + monthsShort: "ཟླ་༡_ཟླ་༢_ཟླ་༣_ཟླ་༤_ཟླ་༥_ཟླ་༦_ཟླ་༧_ཟླ་༨_ཟླ་༩_ཟླ་༡༠_ཟླ་༡༡_ཟླ་༡༢".split("_"), +}); +``` diff --git a/src/utilities/get-string-width.js b/src/utilities/get-string-width.js index d5618e769..951846653 100644 --- a/src/utilities/get-string-width.js +++ b/src/utilities/get-string-width.js @@ -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; } diff --git a/tests/format/js/string-width/__snapshots__/format.test.js.snap b/tests/format/js/string-width/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..bccd85914 --- /dev/null +++ b/tests/format/js/string-width/__snapshots__/format.test.js.snap @@ -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 अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित टेक्स्ट ब्लॉक है। ", +); + +================================================================================ +`; diff --git a/tests/format/js/string-width/format.test.js b/tests/format/js/string-width/format.test.js new file mode 100644 index 000000000..67c251cce --- /dev/null +++ b/tests/format/js/string-width/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["babel", "flow", "typescript"]); diff --git a/tests/format/js/string-width/string-width.js b/tests/format/js/string-width/string-width.js new file mode 100644 index 000000000..1af3e7c9b --- /dev/null +++ b/tests/format/js/string-width/string-width.js @@ -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 अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित टेक्स्ट ब्लॉक है। "); diff --git a/tests/integration/__tests__/public-utilities.js b/tests/integration/__tests__/public-utilities.js index 8a5c06fe1..9155d7150 100644 --- a/tests/integration/__tests__/public-utilities.js +++ b/tests/integration/__tests__/public-utilities.js @@ -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);