Text: Japanese line breaking keeps small kana and "ー" off line starts; only commas and stops hang

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) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-13 05:24:08 -04:00
parent d30e4edb45
commit 257e79de27
2 changed files with 57 additions and 23 deletions

View File

@ -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<bool>, Vec<bool>)
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()

View File

@ -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:?}");
}