From 5927216227411bc2cdaf30d50559e0a475ab4832 Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Sun, 4 Oct 2026 12:20:28 +0900 Subject: [PATCH] fix(markdown): preserve newline around Chinese/Japanese characters not around Korean letters in all proseWrap settings (#20143) Co-authored-by: fisker Cheung --- changelog_unreleased/markdown/20143.md | 29 ++++++ src/language-markdown/print/whitespace.js | 35 ++++++- .../__snapshots__/format.test.js.snap | 8 +- .../__snapshots__/format.test.js.snap | 93 +++++++++++++++++-- .../format/markdown/splitCjkText/cjkMixed.md | 11 +++ .../markdown/splitCjkText/format.test.js | 13 ++- .../splitCjkText/non-bmp/format.test.js | 5 +- 7 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 changelog_unreleased/markdown/20143.md create mode 100644 tests/format/markdown/splitCjkText/cjkMixed.md diff --git a/changelog_unreleased/markdown/20143.md b/changelog_unreleased/markdown/20143.md new file mode 100644 index 000000000..16ba0127f --- /dev/null +++ b/changelog_unreleased/markdown/20143.md @@ -0,0 +1,29 @@ +#### Preserve newlines around Chinese/Japanese characters not around Korean letters in all proseWrap settings (#20143 by @tats-u) + + +```md + +English +English +日本語 +日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語 +日本語 +한국어 +한국어 +中文 +中文! +한국어 +(中文) + + +English English日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語 한국어 한국어 中文中文!한국어(中文) + + +English English +日本語 +日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語 +日本語 한국어 한국어 中文 +中文! +한국어 +(中文) +``` diff --git a/src/language-markdown/print/whitespace.js b/src/language-markdown/print/whitespace.js index 9cef69da2..e622a71e7 100644 --- a/src/language-markdown/print/whitespace.js +++ b/src/language-markdown/print/whitespace.js @@ -251,7 +251,10 @@ function isBreakable(path, value, proseWrap, isLink, options) { * normalized form of link labels. https://spec.commonmark.org/0.30/#matches */ function printWhitespace(path, value, proseWrap, isLink, options) { - if (proseWrap === "preserve" && value === "\n") { + if ( + (proseWrap === "preserve" || isLineBreakAmbiguous(path)) && + value === "\n" + ) { return hardline; } @@ -276,6 +279,36 @@ function printWhitespaceNode(path, options) { ); } +/** + * Determines if a line break around a CJ character can be handled differently by browsers. + * + * If `true`, since Prettier is just a formatter, which should not change the semantics of the code, it should leave such an ambiguous line break as-is + * and let the browser decide how to interpret it regardless of `proseWrap`. + * + * Once all browsers behave consistently in the future, this function will no longer be needed. + * + * @param {AstPath} path + * @returns {boolean} `true` if a line break around a CJ character are removed by some browsers but converted to a space by others. + */ +function isLineBreakAmbiguous(path) { + const { previous, next } = path; + // All browsers treat a line break between a CJ letter and a non-CJ character as + // equivalent to a space. However, Prettier v3 intentionally decided that, in + // some cases, such a line break is equivalent to removing it without + // converting it to a space, so in v3 we leave it as-is instead of converting it + // to a space or removing it. + // See also: https://github.com/prettier/prettier/pull/16805 + return ( + (Boolean(previous?.isCJ) || Boolean(next?.isCJ)) && + // A line break between a CJ letter and a Korean letter has already been interchangeable with a space since Prettier v3.0.0. + // A line break between a CJK punctuation character and a Korean letter is handled differently depending on the browser and the content language. + !( + (previous.kind === KIND_K_LETTER && next.kind === KIND_CJ_LETTER) || + (next.kind === KIND_K_LETTER && previous.kind === KIND_CJ_LETTER) + ) + ); +} + function shouldPreventBreak(path, options) { const { proseWrap } = options; if (proseWrap === "never") { diff --git a/tests/format/markdown/paragraph/__snapshots__/format.test.js.snap b/tests/format/markdown/paragraph/__snapshots__/format.test.js.snap index 7b0d2fad6..01a249ff3 100644 --- a/tests/format/markdown/paragraph/__snapshots__/format.test.js.snap +++ b/tests/format/markdown/paragraph/__snapshots__/format.test.js.snap @@ -43,7 +43,9 @@ This ia an english paragraph with a CJK quote “中文“. 扩展运算符(spread)是三个点(\`...\`)。 -::: warning 注意该网站在国外无法访问,故以下演示无效 ::: +::: warning 注意 +该网站在国外无法访问,故以下演示无效 +::: IVS 麻󠄁羽󠄀‼️ @@ -97,7 +99,9 @@ This ia an english paragraph with a CJK quote “中文“. 扩展运算符(spread)是三个点(\`...\`)。 -::: warning 注意该网站在国外无法访问,故以下演示无效 ::: +::: warning 注意 +该网站在国外无法访问,故以下演示无效 +::: IVS 麻󠄁羽󠄀‼️ diff --git a/tests/format/markdown/splitCjkText/__snapshots__/format.test.js.snap b/tests/format/markdown/splitCjkText/__snapshots__/format.test.js.snap index 8ca5034b7..e95bc52b8 100644 --- a/tests/format/markdown/splitCjkText/__snapshots__/format.test.js.snap +++ b/tests/format/markdown/splitCjkText/__snapshots__/format.test.js.snap @@ -38,6 +38,36 @@ nasalカ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク ================================================================================ `; +exports[`cjkMixed.md - {"proseWrap":"always"} format 1`] = ` +====================================options===================================== +parsers: ["markdown"] +proseWrap: "always" + printWidth: 80 (default) | +=====================================input====================================== +English +English +日本語 +日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語 +日本語 +한국어 +한국어 +中文 +中文! +한국어 +(中文) + +=====================================output===================================== +English English +日本語 +日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語 +日本語 한국어 한국어 中文 +中文! +한국어 +(中文) + +================================================================================ +`; + exports[`han-kana-alnum.md - {"proseWrap":"always"} format 1`] = ` ====================================options===================================== parsers: ["markdown"] @@ -290,20 +320,71 @@ Go Rust =====================================output===================================== -日本語、にほんご。汉语, 中文. 日本語,にほんご.English -words!? 漢字!汉字?「セリフ」(括弧) 文字(括弧)文字【括弧】日本語English +日本語 +、 +にほんご +。 +汉语, +中文. +日 +本 +語 +, +に +ほ +ん +ご +. +English words!? +漢字 +! +汉字 +? +「セリフ」 +(括弧) +文字 +(括弧) +文字 +【括弧】 +日本語 +English -「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)」「禁則(きんそく)処理(しょり)」「禁則(きんそく)処理(しょり)!!!!」「禁則(きんそく)処理(しょり)!!!」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)!」「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)」 +「禁則(きんそく)処理(しょり)」 +「禁則(きんそく)処理(しょり)!!!!」 +「禁則(きんそく)処理(しょり)!!!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 +「禁則(きんそく)処理(しょり)!」 -中点・中点 +中点 +・ +中点 -禁則処理にわざと違反した文章のテストを今から行います。準備はいいでしょうか?レディ、ゴー! +禁則処理にわざと違反した文章のテストを今から行います。準備はいいでしょうか?レデ +ィ、ゴー! [ウ ィキペディア] [ウ ィキペディア]: https://ja.wikipedia.org/ -C言語・C++・Go・Rust +C言 +語 +・ +C++ +・ +Go +・ +Rust ================================================================================ `; diff --git a/tests/format/markdown/splitCjkText/cjkMixed.md b/tests/format/markdown/splitCjkText/cjkMixed.md new file mode 100644 index 000000000..930d57039 --- /dev/null +++ b/tests/format/markdown/splitCjkText/cjkMixed.md @@ -0,0 +1,11 @@ +English +English +日本語 +日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語日本語 +日本語 +한국어 +한국어 +中文 +中文! +한국어 +(中文) diff --git a/tests/format/markdown/splitCjkText/format.test.js b/tests/format/markdown/splitCjkText/format.test.js index a80abe149..fd1299ee7 100644 --- a/tests/format/markdown/splitCjkText/format.test.js +++ b/tests/format/markdown/splitCjkText/format.test.js @@ -18,7 +18,10 @@ const snippets = [ // (39 + 40 + 41) * 2 === 40 * 3 * 2 === 40 * 6 // lineWidth is 80 and "文" is double-width // This is being saved for after Chrome and Safari bug is fixed. - output: /* `${"文".repeat(40)}\n`.repeat(6) */ `${"文".repeat(40 * 6)}\n`, + get output() { + /* `${"文".repeat(40)}\n`.repeat(6) */ + return this.code.replace(/ (?=\n)/g, ""); + }, }, ]; @@ -36,7 +39,13 @@ const snippets = [ name: "Should remove newline around CJ(K) punctuation(-like)", code, // Note: replaceAll is not supported in Node 14. - output: `${code.replace(/\n/g, "")}\n`, + // In CSS, a line break between a CJK and a non-CJK character is always interchangeable + // with a space, but Prettier v3 made a design mistake where, in some cases, + // it was treated as equivalent to being deleted + // (see https://github.com/prettier/prettier/issues/14936). + // Therefore, .replace is disabled until it gets fixed in + // https://github.com/prettier/prettier/pull/16805. + output: code, // `${code.replace(/\n/g, "")}\n`, }); } diff --git a/tests/format/markdown/splitCjkText/non-bmp/format.test.js b/tests/format/markdown/splitCjkText/non-bmp/format.test.js index eb4d00ae1..e9b6f24cd 100644 --- a/tests/format/markdown/splitCjkText/non-bmp/format.test.js +++ b/tests/format/markdown/splitCjkText/non-bmp/format.test.js @@ -1,7 +1,8 @@ // This preamble is unnecessary after #16805 const preamble = "a 字 a 字 a 字"; const code = `${preamble}\n${"葛\u{e0100}\n".repeat(3)}${"𠮷\n".repeat(2)}終\n`; -const output = `${preamble}${"葛\u{e0100}".repeat(3)}${"𠮷".repeat(2)}終\n`; +// Once browser behavior is standardized across browsers, comment out output and switch to the output below. +// const output = `${preamble}${"葛\u{e0100}".repeat(3)}${"𠮷".repeat(2)}終\n`; runFormatTest( { @@ -10,7 +11,7 @@ runFormatTest( { code, name: "Make sure non-BMP CJK characters are treated as CJK", - output, + output: code, }, ], },