diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 659d33f4a..cf788b15b 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -26,6 +26,7 @@ const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified b /// first and last one since they may be broken apart by the start or end of the /// line, respectively. But even those can partially reuse previous results when /// the break index is safe-to-break per rustybuzz. +#[derive(Debug)] pub struct Line<'a> { /// The items the line is made of. pub items: Items<'a>, @@ -219,7 +220,7 @@ fn collect_items<'a>( // Add fallback text to expand the line height, if necessary. if !items.iter().any(|item| matches!(item, Item::Text(_))) { if let Some(fallback) = fallback { - items.push(fallback); + items.push(fallback, usize::MAX); } } @@ -270,12 +271,13 @@ fn collect_range<'a>( items: &mut Items<'a>, fallback: &mut Option>, ) { - for (subrange, item) in p.slice(range.clone()) { + for (idx, run) in p.slice(range.clone()).enumerate() { // All non-text items are just kept, they can't be split. - let Item::Text(shaped) = item else { - items.push(item); + let Item::Text(shaped) = &run.item else { + items.push(&run.item, idx); continue; }; + let subrange = &run.range; // The intersection range of the item, the subrange, and the line's // trimming. @@ -293,10 +295,10 @@ fn collect_range<'a>( } else if split { // When the item is split in half, reshape it. let reshaped = shaped.reshape(engine, sliced); - items.push(Item::Text(reshaped)); + items.push(Item::Text(reshaped), idx); } else { // When the item is fully contained, just keep it. - items.push(item); + items.push(&run.item, idx); } } } @@ -499,16 +501,16 @@ pub fn commit( // Build the frames and determine the height and baseline. let mut frames = vec![]; - for item in line.items.iter() { - let mut push = |offset: &mut Abs, frame: Frame| { + for item in line.items.indexed_iter() { + let mut push = |offset: &mut Abs, frame: Frame, idx: usize| { let width = frame.width(); top.set_max(frame.baseline()); bottom.set_max(frame.size().y - frame.baseline()); - frames.push((*offset, frame)); + frames.push((*offset, frame, idx)); *offset += width; }; - match item { + match &*item.item { Item::Absolute(v, _) => { offset += *v; } @@ -520,7 +522,7 @@ pub fn commit( layout_box(elem, engine, loc.relayout(), styles, region) })?; apply_baseline_shift(&mut frame, *styles); - push(&mut offset, frame); + push(&mut offset, frame, item.idx); } else { offset += amount; } @@ -532,15 +534,15 @@ pub fn commit( justification_ratio, extra_justification, ); - push(&mut offset, frame); + push(&mut offset, frame, item.idx); } Item::Frame(frame) => { - push(&mut offset, frame.clone()); + push(&mut offset, frame.clone(), item.idx); } Item::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); frame.push(Point::zero(), FrameItem::Tag((*tag).clone())); - frames.push((offset, frame)); + frames.push((offset, frame, item.idx)); } Item::Skip(_) => {} } @@ -559,8 +561,9 @@ pub fn commit( add_par_line_marker(&mut output, marker, engine, locator, top); } + frames.sort_by_key(|(_, _, idx)| *idx); // Construct the line's frame. - for (offset, frame) in frames { + for (offset, frame, _) in frames { let x = offset + p.config.align.position(remaining); let y = top - frame.baseline(); output.push_frame(Point::new(x, y), frame); @@ -627,7 +630,7 @@ fn overhang(c: char) -> f64 { } /// A collection of owned or borrowed inline items. -pub struct Items<'a>(Vec>); +pub struct Items<'a>(Vec>); impl<'a> Items<'a> { /// Create empty items. @@ -636,33 +639,38 @@ impl<'a> Items<'a> { } /// Push a new item. - pub fn push(&mut self, entry: impl Into>) { - self.0.push(entry.into()); + pub fn push(&mut self, entry: impl Into>, idx: usize) { + self.0.push(IndexedItemEntry { item: entry.into(), idx }); } /// Iterate over the items pub fn iter(&self) -> impl Iterator> { - self.0.iter().map(|item| &**item) + self.0.iter().map(|item| &*item.item) + } + + /// Iterate over the items with indices + pub fn indexed_iter(&self) -> impl Iterator> { + self.0.iter() } /// Access the first item. pub fn first(&self) -> Option<&Item<'a>> { - self.0.first().map(|item| &**item) + self.0.first().map(|item| &*item.item) } /// Access the last item. pub fn last(&self) -> Option<&Item<'a>> { - self.0.last().map(|item| &**item) + self.0.last().map(|item| &*item.item) } /// Access the first item mutably, if it is text. pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { - self.0.first_mut()?.text_mut() + self.0.first_mut()?.item.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()?.text_mut() + self.0.last_mut()?.item.text_mut() } /// Reorder the items starting at the given index to RTL. @@ -673,12 +681,17 @@ impl<'a> Items<'a> { impl<'a> FromIterator> for Items<'a> { fn from_iter>>(iter: I) -> Self { - Self(iter.into_iter().collect()) + Self( + iter.into_iter() + .enumerate() + .map(|(idx, item)| IndexedItemEntry { item, idx }) + .collect(), + ) } } impl<'a> Deref for Items<'a> { - type Target = Vec>; + type Target = Vec>; fn deref(&self) -> &Self::Target { &self.0 @@ -697,7 +710,18 @@ impl Debug for Items<'_> { } } +/// An item accompanied by its position within a line. +#[derive(Debug)] +pub struct IndexedItemEntry<'a> { + pub item: ItemEntry<'a>, + pub idx: usize, +} + /// A reference to or a boxed item. +/// +/// This is conceptually similar to a [`Cow<'a, Item<'a>>`][std::borrow::Cow], +/// but we box owned items since an [`Item`] is much bigger than +/// a box. pub enum ItemEntry<'a> { Ref(&'a Item<'a>), Box(Box>), diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 31512604f..5e900da0d 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -823,8 +823,8 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { /// Whether hyphenation is enabled at the given offset. fn hyphenate_at(p: &Preparation, offset: usize) -> bool { p.config.hyphenate.unwrap_or_else(|| { - let (_, item) = p.get(offset); - match item.text() { + let run = p.get(offset); + match run.item.text() { Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify), None => false, } @@ -834,8 +834,8 @@ fn hyphenate_at(p: &Preparation, offset: usize) -> bool { /// The text language at the given offset. fn lang_at(p: &Preparation, offset: usize) -> Option { let lang = p.config.lang.or_else(|| { - let (_, item) = p.get(offset); - let styles = item.text()?.styles; + let run = p.get(offset); + let styles = run.item.text()?.styles; Some(TextElem::lang_in(styles)) })?; @@ -900,8 +900,8 @@ impl Estimates { let mut shrinkability = CumulativeVec::with_capacity(cap); let mut justifiables = CumulativeVec::with_capacity(cap); - for (range, item) in p.items.iter() { - if let Item::Text(shaped) = item { + for run in p.items.iter() { + if let Item::Text(shaped) = &run.item { for g in shaped.glyphs.iter() { let byte_len = g.range.len(); let stretch = g.stretchability().0 + g.stretchability().1; @@ -912,13 +912,13 @@ impl Estimates { justifiables.push(byte_len, g.is_justifiable() as usize); } } else { - widths.push(range.len(), item.natural_width()); + widths.push(run.range.len(), run.item.natural_width()); } - widths.adjust(range.end); - stretchability.adjust(range.end); - shrinkability.adjust(range.end); - justifiables.adjust(range.end); + widths.adjust(run.range.end); + stretchability.adjust(run.range.end); + shrinkability.adjust(run.range.end); + justifiables.adjust(run.range.end); } Self { diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 5d7fcd7cb..ac834e3cb 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -3,6 +3,12 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; +#[derive(Debug)] +pub struct Run<'a> { + pub item: Item<'a>, + pub range: Range, +} + /// A representation in which children are already layouted and text is already /// preshaped. /// @@ -20,22 +26,22 @@ pub struct Preparation<'a> { /// direction). pub bidi: Option>, /// Text runs, spacing and layouted elements. - pub items: Vec<(Range, Item<'a>)>, + pub items: Vec>, /// Maps from byte indices to item indices. pub indices: Vec, /// The span mapper. pub spans: SpanMapper, } -impl<'a> Preparation<'a> { +impl Preparation<'_> { /// Get the item that contains the given `text_offset`. - pub fn get(&self, offset: usize) -> &(Range, Item<'a>) { + pub fn get(&self, offset: usize) -> &Run { let idx = self.indices.get(offset).copied().unwrap_or(0); &self.items[idx] } /// Iterate over the items that intersect the given `sliced` range. - pub fn slice(&self, sliced: Range) -> impl Iterator)> { + pub fn slice(&self, sliced: Range) -> impl Iterator { // Usually, we don't want empty-range items at the start of the line // (because they will be part of the previous line), but for the first // line, we need to keep them. @@ -43,8 +49,8 @@ impl<'a> Preparation<'a> { 0 => 0, n => self.indices.get(n).copied().unwrap_or(0), }; - self.items[start..].iter().take_while(move |(range, _)| { - range.start < sliced.end || range.end <= sliced.end + self.items[start..].iter().take_while(move |run| { + run.range.start < sliced.end || run.range.end <= sliced.end }) } } @@ -84,7 +90,9 @@ pub fn prepare<'a>( Segment::Text(_, styles) => { shape_range(&mut items, engine, text, &bidi, range, styles); } - Segment::Item(item) => items.push((range, item)), + Segment::Item(item) => { + items.push(Run { range, item }); + } } cursor = end; @@ -92,8 +100,8 @@ pub fn prepare<'a>( // Build the mapping from byte to item indices. let mut indices = Vec::with_capacity(text.len()); - for (i, (range, _)) in items.iter().enumerate() { - indices.extend(range.clone().map(|_| i)); + for (i, run) in items.iter().enumerate() { + indices.extend(run.range.clone().map(|_| i)); } if config.cjk_latin_spacing { @@ -113,15 +121,15 @@ pub fn prepare<'a>( /// Add some spacing between Han characters and western characters. See /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// in Horizontal Written Mode -fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) { +fn add_cjk_latin_spacing(items: &mut [Run]) { let mut items = items .iter_mut() - .filter(|(_, x)| !matches!(x, Item::Tag(_))) + .filter(|run| !matches!(run.item, Item::Tag(_))) .peekable(); let mut prev: Option<&ShapedGlyph> = None; - while let Some((_, item)) = items.next() { - let Some(text) = item.text_mut() else { + while let Some(run) = items.next() { + let Some(text) = run.item.text_mut() else { prev = None; continue; }; @@ -135,7 +143,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) { let next = glyphs.peek().map(|n| n as _).or_else(|| { items .peek() - .and_then(|(_, i)| i.text()) + .and_then(|run| run.item.text()) .and_then(|shaped| shaped.glyphs.first()) }); diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 8236d1e36..b8c7b853c 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -19,6 +19,7 @@ use typst_utils::SliceExt; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; +use super::prepare::Run; use super::{decorate, Item, Range, SpanMapper}; use crate::modifiers::{FrameModifiers, FrameModify}; @@ -592,7 +593,7 @@ impl Debug for ShapedText<'_> { /// Group a range of text by BiDi level and script, shape the runs and generate /// items for them. pub fn shape_range<'a>( - items: &mut Vec<(Range, Item<'a>)>, + items: &mut Vec>, engine: &Engine, text: &'a str, bidi: &BidiInfo<'a>, @@ -606,7 +607,7 @@ pub fn shape_range<'a>( let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; let shaped = shape(engine, range.start, &text[range.clone()], styles, dir, lang, region); - items.push((range, Item::Text(shaped))); + items.push(Run { range, item: Item::Text(shaped) }); }; let mut prev_level = BidiLevel::ltr(); diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 51e3b03b0..b1a790358 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -539,6 +539,7 @@ impl IntoValue for CslSource { /// memoization) for the whole document. This setup is necessary because /// citation formatting is inherently stateful and we need access to all /// citations to do it. +#[derive(Debug)] pub(super) struct Works { /// Maps from the location of a citation group to its rendered content. pub citations: HashMap>, @@ -571,7 +572,7 @@ impl Works { /// Context for generating the bibliography. struct Generator<'a> { - /// The routines that is used to evaluate mathematical material in citations. + /// The routines that are used to evaluate mathematical material in citations. routines: &'a Routines, /// The world that is used to evaluate mathematical material in citations. world: Tracked<'a, dyn World + 'a>, @@ -588,7 +589,7 @@ struct Generator<'a> { /// Details about a group of merged citations. All citations are put into groups /// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). -/// Even single citations will be put into groups of length ones. +/// Even single citations will be put into groups of length one. struct GroupInfo { /// The group's location. location: Location, diff --git a/tests/ref/issue-5775-cite-order-rtl.png b/tests/ref/issue-5775-cite-order-rtl.png new file mode 100644 index 000000000..dbb1e86f8 Binary files /dev/null and b/tests/ref/issue-5775-cite-order-rtl.png differ diff --git a/tests/suite/model/cite.typ b/tests/suite/model/cite.typ index b328dda49..8c9748150 100644 --- a/tests/suite/model/cite.typ +++ b/tests/suite/model/cite.typ @@ -147,3 +147,13 @@ B #cite() #cite(). // Error: 7-17 expected label, found string // Hint: 7-17 use `label("%@&#*!\\")` to create a label #cite("%@&#*!\\") + +--- issue-5775-cite-order-rtl --- +// Test citation order in RTL text. +#set page(width: 300pt) +این است +@tolkien54 +و این یکی هست +@arrgh + +#bibliography("/assets/bib/works.bib")