diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index a9c11793b..c4263d5d2 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -46,6 +46,11 @@ pub enum Item<'a> { } impl<'a> Item<'a> { + /// Whether this is a tag item. + pub fn is_tag(&self) -> bool { + matches!(self, Self::Tag(_)) + } + /// If this a text item, return it. pub fn text(&self) -> Option<&ShapedText<'a>> { match self { diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 97ec0a9d1..f85694677 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -80,8 +80,7 @@ impl Line<'_> { // CJK character at line end should not be adjusted. if self .items - .last() - .and_then(Item::text) + .trailing_text() .map(|s| s.cjk_justifiable_at_last()) .unwrap_or(false) { @@ -176,7 +175,7 @@ pub fn line<'a>( // Add a hyphen at the line start, if a previous dash should be repeated. if let Some(pred) = pred && pred.dash == Some(Dash::Hard) - && let Some(base) = pred.items.last_text() + && let Some(base) = pred.items.trailing_text() && should_repeat_hyphen(base.lang, full) && let Some(hyphen) = ShapedText::hyphen(engine, p.config.fallback, base, trim, false) @@ -188,7 +187,7 @@ pub fn line<'a>( // Add a hyphen at the line end, if we ended on a soft hyphen. if dash == Some(Dash::Soft) - && let Some(base) = items.last_text() + && let Some(base) = items.trailing_text() && let Some(hyphen) = ShapedText::hyphen(engine, p.config.fallback, base, trim, true) { @@ -253,7 +252,7 @@ fn trim_weak_spacing(items: &mut Items) { } // Trim weak spacing at the end of the line. - while matches!(items.last(), Some(Item::Absolute(_, true))) { + while matches!(items.iter().next_back(), Some(Item::Absolute(_, true))) { items.pop(); } } @@ -355,7 +354,7 @@ fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) /// Add spacing around punctuation marks for CJ glyphs at the line start. fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { - let Some(shaped) = items.first_text_mut() else { return }; + let Some(shaped) = items.leading_text_mut() else { return }; let Some(glyph) = shaped.glyphs.first() else { return }; if glyph.is_cjk_right_aligned_punctuation() { @@ -380,7 +379,7 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { /// Add spacing around punctuation marks for CJ glyphs at the line end. fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) { - let Some(shaped) = items.last_text_mut() else { return }; + let Some(shaped) = items.trailing_text_mut() else { return }; let Some(glyph) = shaped.glyphs.last() else { return }; // Deal with CJK punctuation at line ends. @@ -481,7 +480,7 @@ pub fn commit( } // Handle hanging punctuation to the left. - if let Some(Item::Text(text)) = line.items.first() + if let Some(text) = line.items.leading_text() && let Some(glyph) = text.glyphs.first() && !text.dir.is_positive() && text.styles.get(TextElem::overhang) @@ -493,7 +492,7 @@ pub fn commit( } // Handle hanging punctuation to the right. - if let Some(Item::Text(text)) = line.items.last() + if let Some(text) = line.items.trailing_text() && let Some(glyph) = text.glyphs.last() && text.dir.is_positive() && text.styles.get(TextElem::overhang) @@ -685,7 +684,7 @@ impl<'a> Items<'a> { } /// Iterate over the items. - pub fn iter(&self) -> impl Iterator> { + pub fn iter(&self) -> impl DoubleEndedIterator> { self.0.iter().map(|(_, item)| &**item) } @@ -694,33 +693,30 @@ impl<'a> Items<'a> { /// /// Note that this is different from `.iter().enumerate()` which would /// provide the indices in visual order! - pub fn indexed_iter(&self) -> impl Iterator)> { + pub fn indexed_iter( + &self, + ) -> impl DoubleEndedIterator)> { self.0.iter() } - /// Access the first item. - pub fn first(&self) -> Option<&Item<'a>> { - self.0.first().map(|(_, item)| &**item) + /// Access the first item (skipping tags), if it is text. + pub fn leading_text(&self) -> Option<&ShapedText<'a>> { + self.0.iter().find(|(_, item)| !item.is_tag())?.1.text() } - /// Access the last item. - pub fn last(&self) -> Option<&Item<'a>> { - self.0.last().map(|(_, item)| &**item) + /// Access the first item (skipping tags) mutably, if it is text. + pub fn leading_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { + self.0.iter_mut().find(|(_, item)| !item.is_tag())?.1.text_mut() } - /// Access the last item, if it is text. - pub fn last_text(&self) -> Option<&ShapedText<'a>> { - self.0.last()?.1.text() + /// Access the last item (skipping tags), if it is text. + pub fn trailing_text(&self) -> Option<&ShapedText<'a>> { + self.0.iter().rev().find(|(_, item)| !item.is_tag())?.1.text() } - /// Access the first item mutably, if it is text. - pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { - self.0.first_mut()?.1.text_mut() - } - - /// Access the last item mutably, if it is text. - pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { - self.0.last_mut()?.1.text_mut() + /// Access the last item (skipping tags) mutably, if it is text. + pub fn trailing_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { + self.0.iter_mut().rev().find(|(_, item)| !item.is_tag())?.1.text_mut() } /// Reorder the items starting at the given index to RTL. diff --git a/tests/ref/issue-hyphenate-after-tag.png b/tests/ref/issue-hyphenate-after-tag.png new file mode 100644 index 000000000..2628a9c30 Binary files /dev/null and b/tests/ref/issue-hyphenate-after-tag.png differ diff --git a/tests/suite/layout/inline/hyphenate.typ b/tests/suite/layout/inline/hyphenate.typ index 892a0d328..8245803ac 100644 --- a/tests/suite/layout/inline/hyphenate.typ +++ b/tests/suite/layout/inline/hyphenate.typ @@ -171,3 +171,12 @@ Hello-#text(red)[world] #set text(costs: (hyphenation: 1%, runt: 2%)) #set text(costs: (widow: 3%)) #context test(text.costs, (hyphenation: 1%, runt: 2%, widow: 3%, orphan: 100%)) + +--- issue-hyphenate-after-tag --- +// Ensure that an invisible tag does not prevent hyphenation. +#set page(width: 50pt) +#set text(hyphenate: true) +#show "Tree": emph +#show emph: set text(red) +#show emph: it => it + metadata(none) +Treebeard