From 07b95eb7b394f24ce96da62de6775f84bbf44d2c Mon Sep 17 00:00:00 2001 From: Haseom Shin Date: Sat, 10 Oct 2026 14:09:36 +0900 Subject: [PATCH] Compose: don't panic when a hyphenated paragraph starts with a no-break letter (#115) --- crates/compose/src/lib.rs | 5 +++++ crates/compose/src/tests.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 5a3056d8..3db5070d 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -2080,6 +2080,11 @@ fn hyphenation_points(text: &str, glyphs: &[Glyph], pp: &ParaProps, exceptions: while j < glyphs.len() && (glyphs[j].is_letter() || glyphs[j].len == 0 || glyphs[j].ch == shape::SOFT_HYPHEN) && !glyphs[j].no_break { j += 1; } + // A no-break letter can't start a hyphenatable word. + if j == i { + i += 1; + continue; + } // A discretionary hyphen in a word (or right before it) replaces automatic hyphenation. let discretionary = glyphs[i..j].iter().any(|g| g.ch == shape::SOFT_HYPHEN) || (i > 0 && glyphs[i - 1].ch == shape::SOFT_HYPHEN); let (a, b) = (glyphs[i].byte, glyphs[j - 1].byte + glyphs[j - 1].len); diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 549e7bcc..256d9a13 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -313,6 +313,18 @@ fn discretionary_hyphen_replaces_automatic_points() { assert!(lines.iter().filter(|l| l.hyphenated).count() <= 1); } +#[test] +fn hyphenation_skips_a_no_break_word_at_the_start() { + // Used to index before the first glyph and panic. + let text = "Extraordinarily long words wrap in a narrow column"; + let (mut d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 60.0, 1000.0), ParaAttrs::default()); + d.story_mut(sid).unwrap().format_chars(0..15, |f| f.over.no_break = Some(true)); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + // The no-break word stays whole on the first line. + let first = all_lines(&cs)[0]; + assert!(!first.hyphenated && first.range.end >= 15, "{:?}", &text[first.range.clone()]); +} + /// Story of `n` one-line filler paragraphs followed by `extra` paragraphs, in a 2-column frame. fn keep_doc(fillers: usize, extra: &[&str], height: f64) -> (Document, StoryId, Vec) { let mut parts: Vec = (0..fillers).map(|k| format!("Filler {k}")).collect();