From 257e79de27cab241cc7ee4ee3312712c52c4f250 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 05:24:08 -0400 Subject: [PATCH] =?UTF-8?q?Text:=20Japanese=20line=20breaking=20keeps=20sm?= =?UTF-8?q?all=20kana=20and=20"=E3=83=BC"=20off=20line=20starts;=20only=20?= =?UTF-8?q?commas=20and=20stops=20hang?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Observed on PowerPoint's export of a wrapped Japanese paragraph (24 pt, 11 characters a line): it breaks "けません(禁則処" / "理)。ー長音もで" / "す。っゃゅょ小書き文字", i.e. small kana (っゃゅょ…), the prolonged sound mark and iteration marks never start a line (strict kinsoku), where UAX #14's default allows it. And "し、句読点「、」や" / "「。」は…": with hanging punctuation on, a closing bracket that overflows does not hang; only the ideographic comma and full stop (and their full-width and half-width forms) do. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/text/src/lib.rs | 65 ++++++++++++++++++++++++++-------------- crates/text/src/tests.rs | 15 ++++++++++ 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 3df5a74..dae8a6d 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -1057,31 +1057,50 @@ fn is_hangul(c: char) -> bool { matches!(c, '\u{1100}'..='\u{11FF}' | '\u{3130}'..='\u{318F}' | '\u{A960}'..='\u{A97F}' | '\u{AC00}'..='\u{D7FF}') } -/// Closing East Asian punctuation that may hang past the end of a line (`hangingPunct`). +/// East Asian commas and full stops, the punctuation PowerPoint lets hang past the end of a line +/// (`hangingPunct`). Closing brackets don't hang: "「。」" overflowing by its "」" moves to the next +/// line whole. fn is_hanging_punct(c: char) -> bool { + matches!(c, '、' | '。' | ',' | '.' | '、' | '。') +} + +/// Small kana, the prolonged sound mark and iteration marks: PowerPoint keeps them off the start +/// of a line (strict Japanese line breaking), where UAX #14's default allows a break before them. +fn is_cj_nonstarter(c: char) -> bool { matches!( c, - '、' | '。' - | ',' - | '.' - | ':' - | ';' - | '!' - | '?' - | ')' - | '」' - | '』' - | '】' - | '〕' - | '〉' - | '》' - | '〙' - | '〛' - | '}' - | ']' - | '」' - | '、' - | '。' + 'ぁ' | 'ぃ' + | 'ぅ' + | 'ぇ' + | 'ぉ' + | 'っ' + | 'ゃ' + | 'ゅ' + | 'ょ' + | 'ゎ' + | 'ゕ' + | 'ゖ' + | 'ァ' + | 'ィ' + | 'ゥ' + | 'ェ' + | 'ォ' + | 'ッ' + | 'ャ' + | 'ュ' + | 'ョ' + | 'ヮ' + | 'ヵ' + | 'ヶ' + | 'ー' + | '々' + | '〻' + | 'ゝ' + | 'ゞ' + | 'ヽ' + | 'ヾ' + | '\u{31F0}'..='\u{31FF}' + | 'ァ'..='ー' ) } @@ -1113,7 +1132,7 @@ fn break_opportunities(text: &str, latin_break: bool) -> (Vec, Vec) for (i, w) in chars.windows(2).enumerate() { let (prev, next) = (w[0], w[1]); // PowerPoint never breaks after a zero-width space, nor before an em dash (only after it). - let keep = if (is_hangul(prev) && is_hangul(next)) || prev == '\u{200B}' || next == '\u{2014}' { + let keep = if (is_hangul(prev) && is_hangul(next)) || prev == '\u{200B}' || next == '\u{2014}' || is_cj_nonstarter(next) { false } else { latin_break || !matches!(prev, '/' | '\\') || next.is_whitespace() diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 8613e02..112e100 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1013,3 +1013,18 @@ fn a_line_broken_at_a_soft_hyphen_shows_a_hyphen() { assert_eq!(wide.lines.len(), 1); assert!(wide.runs.iter().all(|r| r.cells.iter().all(|c| c.1 != '\u{AD}' || r.glyphs.iter().all(|g| g.0 != r.face.glyph_for('-'))))); } + +#[test] +fn small_kana_and_closing_brackets_follow_powerpoint_kinsoku() { + // "っ" and "ー" never start a line: the character before them moves down too. + let w = one_line("あいうえお") + 0.5; + for (text, first) in [("あいうえおっかき", "あいうえ"), ("あいうえおーかき", "あいうえ"), ("あいうえおかき", "あいうえお")] + { + let lines = wrapped(text, w, |p| p.hanging_punct = Some(true)); + assert_eq!(lines[0], first, "{text}: {lines:?}"); + } + // A closing bracket doesn't hang like "。" does: "「。」" overflowing by "」" moves down whole. + let w = one_line("あいうえ「。") + 0.5; + let lines = wrapped("あいうえ「。」かき", w, |p| p.hanging_punct = Some(true)); + assert_eq!(lines[0], "あいうえ", "{lines:?}"); +}