diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-09-02 20:43:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-09-02 18:43:59 +0000 |
| commit | cfde809feb33bc057e860c5128c74fb9df6f030c (patch) | |
| tree | 5b367da382b9dab2840ce99ec05bad03f3039168 | |
| parent | 9fd796e0e24ef9a76f8f7328e0a1eed350e91320 (diff) | |
New realization / Text show rules now work across elements (#4876)
60 files changed, 1726 insertions, 1566 deletions
@@ -2632,8 +2632,10 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" name = "typst" version = "0.11.0" dependencies = [ + "arrayvec", "az", "bitflags 2.6.0", + "bumpalo", "chinese-number", "ciborium", "comemo", @@ -33,6 +33,7 @@ arrayvec = "0.7.4" az = "1.2" base64 = "0.22" bitflags = { version = "2", features = ["serde"] } +bumpalo = { version = "3", features = ["collections"] } bytemuck = "1" chinese-number = { version = "0.7.2", default-features = false, features = ["number-to-chinese"] } chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index b9078037..2b9c5d1a 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -18,8 +18,10 @@ typst-macros = { workspace = true } typst-syntax = { workspace = true } typst-timing = { workspace = true } typst-utils = { workspace = true } +arrayvec = { workspace = true } az = { workspace = true } bitflags = { workspace = true } +bumpalo = { workspace = true } chinese-number = { workspace = true } ciborium = { workspace = true } comemo = { workspace = true } diff --git a/crates/typst/src/engine.rs b/crates/typst/src/engine.rs index d0ab7d2c..9e4d9d70 100644 --- a/crates/typst/src/engine.rs +++ b/crates/typst/src/engine.rs @@ -29,15 +29,11 @@ pub struct Engine<'a> { } impl Engine<'_> { - /// Performs a fallible operation that does not immediately terminate further - /// execution. Instead it produces a delayed error that is only promoted to - /// a fatal one if it remains at the end of the introspection loop. - pub fn delay<F, T>(&mut self, f: F) -> T - where - F: FnOnce(&mut Self) -> SourceResult<T>, - T: Default, - { - match f(self) { + /// Handles a result without immediately terminating execution. Instead, it + /// produces a delayed error that is only promoted to a fatal one if it + /// remains by the end of the introspection loop. + pub fn delay<T: Default>(&mut self, result: SourceResult<T>) -> T { + match result { Ok(value) => value, Err(errors) => { self.sink.delay(errors); diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index b9d1b189..91c911ed 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -21,10 +21,9 @@ use crate::foundations::{ use crate::introspection::Location; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; -use crate::realize::{Behave, Behaviour}; use crate::syntax::Span; use crate::text::UnderlineElem; -use crate::utils::{fat, LazyHash, SmallBitSet}; +use crate::utils::{fat, singleton, LazyHash, SmallBitSet}; /// A piece of document content. /// @@ -109,9 +108,9 @@ impl Content { } } - /// Creates a new empty sequence content. + /// Creates a empty sequence content. pub fn empty() -> Self { - Self::new(SequenceElem::default()) + singleton!(Content, SequenceElem::default().pack()).clone() } /// Get the element of this content. @@ -185,12 +184,6 @@ impl Content { self.make_mut().lifecycle.insert(0); } - /// How this element interacts with other elements in a stream. - pub fn behaviour(&self) -> Behaviour { - self.with::<dyn Behave>() - .map_or(Behaviour::Supportive, Behave::behaviour) - } - /// Get a field by ID. /// /// This is the preferred way to access fields. However, you can only use it diff --git a/crates/typst/src/foundations/selector.rs b/crates/typst/src/foundations/selector.rs index 55fd9555..3a9ab308 100644 --- a/crates/typst/src/foundations/selector.rs +++ b/crates/typst/src/foundations/selector.rs @@ -12,7 +12,6 @@ use crate::foundations::{ }; use crate::introspection::{Introspector, Locatable, Location}; use crate::symbols::Symbol; -use crate::text::TextElem; /// A helper macro to create a field selector used in [`Selector::Elem`] #[macro_export] @@ -126,15 +125,12 @@ impl Selector { pub fn matches(&self, target: &Content, styles: Option<StyleChain>) -> bool { match self { Self::Elem(element, dict) => { - target.func() == *element + target.elem() == *element && dict.iter().flat_map(|dict| dict.iter()).all(|(id, value)| { target.get(*id, styles).as_ref().ok() == Some(value) }) } Self::Label(label) => target.label() == Some(*label), - Self::Regex(regex) => target - .to_packed::<TextElem>() - .is_some_and(|elem| regex.is_match(elem.text())), Self::Can(cap) => target.func().can_type_id(*cap), Self::Or(selectors) => { selectors.iter().any(move |sel| sel.matches(target, styles)) @@ -144,7 +140,7 @@ impl Selector { } Self::Location(location) => target.location() == Some(*location), // Not supported here. - Self::Before { .. } | Self::After { .. } => false, + Self::Regex(_) | Self::Before { .. } | Self::After { .. } => false, } } } diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs index e74a1649..e297db7d 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -164,12 +164,6 @@ impl Styles { .any(|property| property.is_of(elem) && property.id == field) } - /// Returns `Some(_)` with an optional span if this list contains - /// styles for the given element. - pub fn interruption<T: NativeElement>(&self) -> Option<Span> { - self.0.iter().find_map(|entry| entry.interruption::<T>()) - } - /// Set a font family composed of a preferred family and existing families /// from a style chain. pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) { @@ -229,6 +223,10 @@ pub enum Style { /// A show rule recipe. Recipe(Recipe), /// Disables a specific show rule recipe. + /// + /// Note: This currently only works for regex recipes since it's the only + /// place we need it for the moment. Normal show rules use guards directly + /// on elements instead. Revocation(RecipeIndex), } @@ -249,13 +247,24 @@ impl Style { } } - /// Returns `Some(_)` with an optional span if this style is of + /// The style's span, if any. + pub fn span(&self) -> Span { + match self { + Self::Property(property) => property.span, + Self::Recipe(recipe) => recipe.span, + Self::Revocation(_) => Span::detached(), + } + } + + /// Returns `Some(_)` with an optional span if this style is for /// the given element. - pub fn interruption<T: NativeElement>(&self) -> Option<Span> { - let elem = T::elem(); + pub fn element(&self) -> Option<Element> { match self { - Style::Property(property) => property.is_of(elem).then_some(property.span), - Style::Recipe(recipe) => recipe.is_of(elem).then_some(recipe.span), + Style::Property(property) => Some(property.elem), + Style::Recipe(recipe) => match recipe.selector { + Some(Selector::Elem(elem, _)) => Some(elem), + _ => None, + }, Style::Revocation(_) => None, } } @@ -279,6 +288,11 @@ impl Style { Self::Revocation(_) => false, } } + + /// Turn this style into prehashed style. + pub fn wrap(self) -> LazyHash<Style> { + LazyHash::new(self) + } } impl Debug for Style { @@ -349,7 +363,7 @@ impl Property { /// Turn this property into prehashed style. pub fn wrap(self) -> LazyHash<Style> { - LazyHash::new(Style::Property(self)) + Style::Property(self).wrap() } } @@ -474,21 +488,6 @@ impl Recipe { &self.transform } - /// Whether this recipe is for the given type of element. - pub fn is_of(&self, element: Element) -> bool { - match self.selector { - Some(Selector::Elem(own, _)) => own == element, - _ => false, - } - } - - /// Whether the recipe is applicable to the target. - pub fn applicable(&self, target: &Content, styles: StyleChain) -> bool { - self.selector - .as_ref() - .is_some_and(|selector| selector.matches(target, Some(styles))) - } - /// Apply the recipe to the given content. pub fn apply( &self, @@ -669,6 +668,11 @@ impl<'a> StyleChain<'a> { Entries { inner: [].as_slice().iter(), links: self.links() } } + /// Iterate over the recipes in the chain. + pub fn recipes(self) -> impl Iterator<Item = &'a Recipe> { + self.entries().filter_map(|style| style.recipe()) + } + /// Iterate over the links of the chain. pub fn links(self) -> Links<'a> { Links(Some(self)) diff --git a/crates/typst/src/introspection/tag.rs b/crates/typst/src/introspection/tag.rs index 51a6454a..7cdea403 100644 --- a/crates/typst/src/introspection/tag.rs +++ b/crates/typst/src/introspection/tag.rs @@ -6,7 +6,6 @@ use crate::foundations::{ elem, Args, Construct, Content, NativeElement, Packed, Unlabellable, }; use crate::introspection::Location; -use crate::realize::{Behave, Behaviour}; /// Holds a locatable element that was realized. #[derive(Clone, PartialEq, Hash)] @@ -78,7 +77,7 @@ pub enum TagKind { /// /// The `TagElem` is handled by all layouters. The held element becomes /// available for introspection in the next compiler iteration. -#[elem(Behave, Unlabellable, Construct)] +#[elem(Construct, Unlabellable)] pub struct TagElem { /// The introspectible element. #[required] @@ -103,9 +102,3 @@ impl Construct for TagElem { } impl Unlabellable for Packed<TagElem> {} - -impl Behave for Packed<TagElem> { - fn behaviour(&self) -> Behaviour { - Behaviour::Ignorant - } -} diff --git a/crates/typst/src/layout/columns.rs b/crates/typst/src/layout/columns.rs index 746212dd..b46351f8 100644 --- a/crates/typst/src/layout/columns.rs +++ b/crates/typst/src/layout/columns.rs @@ -7,7 +7,6 @@ use crate::introspection::Locator; use crate::layout::{ layout_fragment_with_columns, BlockElem, Fragment, Length, Ratio, Regions, Rel, }; -use crate::realize::{Behave, Behaviour}; /// Separates a region into multiple equally sized columns. /// @@ -109,20 +108,10 @@ fn layout_columns( /// understanding of the fundamental /// laws of nature. /// ``` -#[elem(title = "Column Break", Behave)] +#[elem(title = "Column Break")] pub struct ColbreakElem { /// If `{true}`, the column break is skipped if the current column is /// already empty. #[default(false)] pub weak: bool, } - -impl Behave for Packed<ColbreakElem> { - fn behaviour(&self) -> Behaviour { - if self.weak(StyleChain::default()) { - Behaviour::Weak(1) - } else { - Behaviour::Destructive - } - } -} diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index fdec898d..f8034bff 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -6,6 +6,7 @@ use std::collections::HashSet; use std::num::NonZeroUsize; use comemo::{Track, Tracked, TrackedMut}; +use once_cell::unsync::Lazy; use crate::diag::{bail, At, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; @@ -28,7 +29,7 @@ use crate::model::{ Document, DocumentInfo, FootnoteElem, FootnoteEntry, Numbering, ParElem, ParLine, ParLineMarker, ParLineNumberingScope, }; -use crate::realize::{first_span, realize, Arenas, Pair}; +use crate::realize::{realize, Arenas, Pair, RealizationKind}; use crate::syntax::Span; use crate::text::TextElem; use crate::utils::{NonZeroExt, Numeric}; @@ -116,8 +117,14 @@ fn layout_document_impl( let arenas = Arenas::default(); let mut info = DocumentInfo::default(); - let mut children = - realize(&mut engine, &mut locator, &arenas, Some(&mut info), content, styles)?; + let mut children = realize( + RealizationKind::Root(&mut info), + &mut engine, + &mut locator, + &arenas, + content, + styles, + )?; let pages = layout_pages(&mut engine, &mut children, locator, styles)?; @@ -393,7 +400,6 @@ fn layout_page_run_impl( // Determine the page-wide styles. let styles = determine_page_styles(children, initial); let styles = StyleChain::new(&styles); - let span = first_span(children); // When one of the lengths is infinite the page fits its content along // that axis. @@ -449,8 +455,7 @@ fn layout_page_run_impl( Smart::Custom(numbering.clone()), both, ) - .pack() - .spanned(span); + .pack(); // We interpret the Y alignment as selecting header or footer // and then ignore it for aligning the actual number. @@ -473,12 +478,12 @@ fn layout_page_run_impl( let fragment = FlowLayouter::new( &mut engine, children, - locator.next(&span).split(), + &mut locator, styles, regions, PageElem::columns_in(styles), ColumnsElem::gutter_in(styles), - span, + Span::detached(), &mut vec![], ) .layout(regions)?; @@ -733,12 +738,19 @@ fn layout_fragment_impl( engine.route.check_layout_depth().at(content.span())?; let arenas = Arenas::default(); - let children = realize(&mut engine, &mut locator, &arenas, None, content, styles)?; + let children = realize( + RealizationKind::Container, + &mut engine, + &mut locator, + &arenas, + content, + styles, + )?; FlowLayouter::new( &mut engine, &children, - locator, + &mut locator, styles, regions, columns, @@ -750,9 +762,9 @@ fn layout_fragment_impl( } /// Layouts a collection of block-level elements. -struct FlowLayouter<'a, 'e> { +struct FlowLayouter<'a, 'b> { /// The engine. - engine: &'a mut Engine<'e>, + engine: &'a mut Engine<'b>, /// The children that will be arranged into a flow. children: &'a [Pair<'a>], /// A span to use for errors. @@ -760,7 +772,7 @@ struct FlowLayouter<'a, 'e> { /// Whether this is the root flow. root: bool, /// Provides unique locations to the flow's children. - locator: SplitLocator<'a>, + locator: &'a mut SplitLocator<'b>, /// The shared styles. shared: StyleChain<'a>, /// The number of columns. @@ -811,8 +823,8 @@ struct CollectedParLine { /// A prepared item in a flow layout. #[derive(Debug)] enum FlowItem { - /// Spacing between other items and whether it is weak. - Absolute(Abs, bool), + /// Spacing between other items and its weakness level. + Absolute(Abs, u8), /// Fractional spacing between other items. Fractional(Fr), /// A frame for a layouted block. @@ -874,13 +886,13 @@ impl FlowItem { } } -impl<'a, 'e> FlowLayouter<'a, 'e> { +impl<'a, 'b> FlowLayouter<'a, 'b> { /// Create a new flow layouter. #[allow(clippy::too_many_arguments)] fn new( - engine: &'a mut Engine<'e>, + engine: &'a mut Engine<'b>, children: &'a [Pair<'a>], - locator: SplitLocator<'a>, + locator: &'a mut SplitLocator<'b>, shared: StyleChain<'a>, mut regions: Regions<'a>, columns: NonZeroUsize, @@ -986,8 +998,10 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { self.handle_place(elem, styles)?; } else if let Some(elem) = child.to_packed::<FlushElem>() { self.handle_flush(elem)?; + } else if child.is::<PagebreakElem>() { + bail!(child.span(), "pagebreaks are not allowed inside of containers"); } else { - bail!(child.span(), "unexpected flow child"); + bail!(child.span(), "{} is not allowed here", child.func().name()); } } @@ -1001,16 +1015,41 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { /// Layout vertical spacing. fn handle_v(&mut self, v: &'a Packed<VElem>, styles: StyleChain) -> SourceResult<()> { - self.handle_item(match v.amount { + self.layout_spacing(v.amount, styles, v.weak(styles) as u8) + } + + /// Layout spacing, handling weakness. + fn layout_spacing( + &mut self, + amount: impl Into<Spacing>, + styles: StyleChain, + weakness: u8, + ) -> SourceResult<()> { + self.handle_item(match amount.into() { Spacing::Rel(rel) => FlowItem::Absolute( // Resolve the spacing relative to the current base height. rel.resolve(styles).relative_to(self.initial.y), - v.weakness(styles) > 0, + weakness, ), Spacing::Fr(fr) => FlowItem::Fractional(fr), }) } + /// Trim trailing weak spacing from the items. + fn trim_weak_spacing(&mut self) { + for (i, item) in self.items.iter().enumerate().rev() { + match item { + FlowItem::Absolute(amount, 1..) => { + self.regions.size.y += *amount; + self.items.remove(i); + return; + } + FlowItem::Frame { .. } => return, + _ => {} + } + } + } + /// Layout a column break. fn handle_colbreak(&mut self, _: &'a Packed<ColbreakElem>) -> SourceResult<()> { // If there is still an available region, skip to it. @@ -1031,6 +1070,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { // Fetch properties. let align = AlignElem::alignment_in(styles).resolve(styles); let leading = ParElem::leading_in(styles); + let spacing = ParElem::spacing_in(styles); let costs = TextElem::costs_in(styles); // Layout the paragraph into lines. This only depends on the base size, @@ -1075,10 +1115,12 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { let back_2 = height_at(len.saturating_sub(2)); let back_1 = height_at(len.saturating_sub(1)); + self.layout_spacing(spacing, styles, 4)?; + // Layout the lines. for (i, mut frame) in lines.into_iter().enumerate() { if i > 0 { - self.handle_item(FlowItem::Absolute(leading, true))?; + self.layout_spacing(leading, styles, 5)?; } // To prevent widows and orphans, we require enough space for @@ -1114,7 +1156,9 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { })?; } + self.layout_spacing(spacing, styles, 4)?; self.last_was_par = true; + Ok(()) } @@ -1128,6 +1172,11 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { let sticky = block.sticky(styles); let align = AlignElem::alignment_in(styles).resolve(styles); let rootable = block.rootable(styles); + let spacing = Lazy::new(|| (ParElem::spacing_in(styles).into(), 4)); + let (above, above_weakness) = + block.above(styles).map(|v| (v, 3)).unwrap_or_else(|| *spacing); + let (below, below_weakness) = + block.below(styles).map(|v| (v, 3)).unwrap_or_else(|| *spacing); // If the block is "rootable" it may host footnotes. In that case, we // defer rootness to it temporarily. We disable our own rootness to @@ -1143,6 +1192,8 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { self.finish_region(false)?; } + self.layout_spacing(above, styles, above_weakness)?; + // Layout the block itself. let fragment = block.layout( self.engine, @@ -1174,6 +1225,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { } self.try_handle_footnotes(notes)?; + self.layout_spacing(below, styles, below_weakness)?; self.root = is_root; self.regions.root = false; @@ -1232,18 +1284,40 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { /// Layout a finished frame. fn handle_item(&mut self, mut item: FlowItem) -> SourceResult<()> { match item { - FlowItem::Absolute(v, weak) => { - if weak - && !self - .items - .iter() - .any(|item| matches!(item, FlowItem::Frame { .. },)) - { - return Ok(()); + FlowItem::Absolute(v, weakness) => { + if weakness > 0 { + let mut has_frame = false; + for prev in self.items.iter_mut().rev() { + match prev { + FlowItem::Frame { .. } => { + has_frame = true; + break; + } + FlowItem::Absolute(prev_amount, prev_level) + if *prev_level > 0 => + { + if *prev_level >= weakness { + let diff = v - *prev_amount; + if *prev_level > weakness || diff > Abs::zero() { + self.regions.size.y -= diff; + *prev = item; + } + } + return Ok(()); + } + FlowItem::Fractional(_) => return Ok(()), + _ => {} + } + } + if !has_frame { + return Ok(()); + } } - self.regions.size.y -= v + self.regions.size.y -= v; + } + FlowItem::Fractional(..) => { + self.trim_weak_spacing(); } - FlowItem::Fractional(..) => {} FlowItem::Frame { ref frame, movable, .. } => { let height = frame.height(); while !self.regions.size.y.fits(height) && !self.regions.in_last() { @@ -1289,13 +1363,16 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { // Select the closer placement, top or bottom. if y_align.is_auto() { - let ratio = (self.regions.size.y - - (frame.height() + clearance) / 2.0) - / self.regions.full; + // When the figure's vertical midpoint would be above the + // middle of the page if it were layouted in-flow, we use + // top alignment. Otherwise, we use bottom alignment. + let used = self.regions.full - self.regions.size.y; + let half = (frame.height() + clearance) / 2.0; + let ratio = (used + half) / self.regions.full; let better_align = if ratio <= 0.5 { - FixedAlignment::End - } else { FixedAlignment::Start + } else { + FixedAlignment::End }; *y_align = Smart::Custom(Some(better_align)); } @@ -1365,6 +1442,8 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { /// only (this is used to force the creation of a frame in case the /// remaining elements are all out-of-flow). fn finish_region(&mut self, force: bool) -> SourceResult<()> { + self.trim_weak_spacing(); + // Early return if we don't have any relevant items. if !force && !self.items.is_empty() @@ -1383,15 +1462,6 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { return Ok(()); } - // Trim weak spacing. - while self - .items - .last() - .is_some_and(|item| matches!(item, FlowItem::Absolute(_, true))) - { - self.items.pop(); - } - // Determine the used size. let mut fr = Fr::zero(); let mut used = Size::zero(); diff --git a/crates/typst/src/layout/inline/collect.rs b/crates/typst/src/layout/inline/collect.rs index dbebcf91..f1bb5869 100644 --- a/crates/typst/src/layout/inline/collect.rs +++ b/crates/typst/src/layout/inline/collect.rs @@ -274,16 +274,24 @@ impl<'a> Collector<'a> { } fn push_segment(&mut self, segment: Segment<'a>) { - if let (Some(Segment::Text(last_len, last_styles)), Segment::Text(len, styles)) = - (self.segments.last_mut(), &segment) - { - if *last_styles == *styles { + match (self.segments.last_mut(), &segment) { + // Merge adjacent text segments with the same styles. + (Some(Segment::Text(last_len, last_styles)), Segment::Text(len, styles)) + if *last_styles == *styles => + { *last_len += *len; - return; } - } - self.segments.push(segment); + // Merge adjacent weak spacing by taking the maximum. + ( + Some(Segment::Item(Item::Absolute(prev_amount, true))), + Segment::Item(Item::Absolute(amount, true)), + ) => { + *prev_amount = (*prev_amount).max(*amount); + } + + _ => self.segments.push(segment), + } } } diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst/src/layout/inline/line.rs index b1ac11ca..d930f707 100644 --- a/crates/typst/src/layout/inline/line.rs +++ b/crates/typst/src/layout/inline/line.rs @@ -181,7 +181,7 @@ pub fn line<'a>( /// example, the `range` may span "hello\n", but the `trim` specifies that the /// linebreak is trimmed. /// -/// We do not factor the `trim` diredctly into the `range` because we still want +/// We do not factor the `trim` directly into the `range` because we still want /// to keep non-text items after the trim (e.g. tags). fn collect_items<'a>( engine: &Engine, diff --git a/crates/typst/src/layout/page.rs b/crates/typst/src/layout/page.rs index 3d94ab79..ac7834bb 100644 --- a/crates/typst/src/layout/page.rs +++ b/crates/typst/src/layout/page.rs @@ -9,14 +9,13 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func, - NativeElement, Packed, Set, Smart, StyleChain, Value, + NativeElement, Set, Smart, StyleChain, Value, }; use crate::layout::{ Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel, Sides, SpecificAlignment, }; use crate::model::Numbering; -use crate::realize::{Behave, Behaviour}; use crate::utils::{singleton, NonZeroExt, Scalar}; use crate::visualize::{Color, Paint}; @@ -388,7 +387,7 @@ impl Construct for PageElem { /// == Compound Theory /// In 1984, the first ... /// ``` -#[elem(title = "Page Break", Behave)] +#[elem(title = "Page Break")] pub struct PagebreakElem { /// If `{true}`, the page break is skipped if the current page is already /// empty. @@ -417,12 +416,6 @@ pub struct PagebreakElem { pub boundary: bool, } -impl Behave for Packed<PagebreakElem> { - fn behaviour(&self) -> Behaviour { - Behaviour::Destructive - } -} - impl PagebreakElem { /// Get the globally shared weak pagebreak element. pub fn shared_weak() -> &'static Content { diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs index 75a13fef..e557e8d0 100644 --- a/crates/typst/src/layout/place.rs +++ b/crates/typst/src/layout/place.rs @@ -1,11 +1,10 @@ use crate::diag::{bail, At, Hint, SourceResult}; use crate::engine::Engine; -use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain, Unlabellable}; +use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain}; use crate::introspection::Locator; use crate::layout::{ layout_frame, Alignment, Axes, Em, Frame, Length, Region, Rel, Size, VAlignment, }; -use crate::realize::{Behave, Behaviour}; /// Places content at an absolute position. /// @@ -27,7 +26,7 @@ use crate::realize::{Behave, Behaviour}; /// ), /// ) /// ``` -#[elem(scope, Behave)] +#[elem(scope)] pub struct PlaceElem { /// Relative to which position in the parent container to place the content. /// @@ -140,12 +139,6 @@ impl Packed<PlaceElem> { } } -impl Behave for Packed<PlaceElem> { - fn behaviour(&self) -> Behaviour { - Behaviour::Ignorant - } -} - /// Asks the layout algorithm to place pending floating elements before /// continuing with the content. /// @@ -172,13 +165,5 @@ impl Behave for Packed<PlaceElem> { /// Some conclusive text that must occur /// after the figure. /// ``` -#[elem(Behave, Unlabellable)] +#[elem] pub struct FlushElem {} - -impl Behave for Packed<FlushElem> { - fn behaviour(&self) -> Behaviour { - Behaviour::Ignorant - } -} - -impl Unlabellable for Packed<FlushElem> {} diff --git a/crates/typst/src/layout/spacing.rs b/crates/typst/src/layout/spacing.rs index 60e9b59b..af0b1e64 100644 --- a/crates/typst/src/layout/spacing.rs +++ b/crates/typst/src/layout/spacing.rs @@ -1,6 +1,5 @@ -use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain}; +use crate::foundations::{cast, elem, Content}; use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel}; -use crate::realize::{Behave, Behaviour}; use crate::utils::Numeric; /// Inserts horizontal spacing into a paragraph. @@ -20,7 +19,7 @@ use crate::utils::Numeric; /// In [mathematical formulas]($category/math), you can additionally use these /// constants to add spacing between elements: `thin` (1/6Â em), `med`(2/9Â em), /// `thick` (5/18Â em), `quad` (1Â em), `wide` (2Â em). -#[elem(title = "Spacing (H)", Behave)] +#[elem(title = "Spacing (H)")] pub struct HElem { /// How much spacing to insert. #[required] @@ -62,29 +61,6 @@ impl HElem { } } -impl Behave for Packed<HElem> { - fn behaviour(&self) -> Behaviour { - if self.amount().is_fractional() { - Behaviour::Destructive - } else if self.weak(StyleChain::default()) { - Behaviour::Weak(1) - } else { - Behaviour::Ignorant - } - } - - fn larger(&self, prev: &(&Content, StyleChain), styles: StyleChain) -> bool { - let Some(other) = prev.0.to_packed::<HElem>() else { return false }; - match (self.amount(), other.amount()) { - (Spacing::Fr(this), Spacing::Fr(other)) => this > other, - (Spacing::Rel(this), Spacing::Rel(other)) => { - this.resolve(styles) > other.resolve(prev.1) - } - _ => false, - } - } -} - /// Inserts vertical spacing into a flow of blocks. /// /// The spacing can be absolute, relative, or fractional. In the last case, @@ -105,7 +81,7 @@ impl Behave for Packed<HElem> { /// [A #v(1fr) B], /// ) /// ``` -#[elem(title = "Spacing (V)", Behave)] +#[elem(title = "Spacing (V)")] pub struct VElem { /// How much spacing to insert. #[required] @@ -124,14 +100,8 @@ pub struct VElem { /// #v(4pt, weak: true) /// The proof is simple: /// ``` - #[external] pub weak: bool, - /// The spacing's weakness level, see also [`Behaviour`]. - #[internal] - #[parse(args.named("weak")?.map(|v: bool| v as usize))] - pub weakness: usize, - /// Whether the spacing collapses if not immediately preceded by a /// paragraph. #[internal] @@ -139,56 +109,6 @@ pub struct VElem { pub attach: bool, } -impl VElem { - /// Normal strong spacing. - pub fn strong(amount: Spacing) -> Self { - Self::new(amount).with_weakness(0) - } - - /// User-created weak spacing. - pub fn weak(amount: Spacing) -> Self { - Self::new(amount).with_weakness(1) - } - - /// Weak spacing with list attach weakness. - pub fn list_attach(amount: Spacing) -> Self { - Self::new(amount).with_weakness(2).with_attach(true) - } - - /// Weak spacing with `BlockElem::spacing` weakness. - pub fn block_spacing(amount: Spacing) -> Self { - Self::new(amount).with_weakness(3) - } - - /// Weak spacing with `ParElem::spacing` weakness. - pub fn par_spacing(amount: Spacing) -> Self { - Self::new(amount).with_weakness(4) - } -} - -impl Behave for Packed<VElem> { - fn behaviour(&self) -> Behaviour { - if self.amount().is_fractional() { - Behaviour::Destructive - } else if self.weakness(StyleChain::default()) > 0 { - Behaviour::Weak(self.weakness(StyleChain::default())) - } else { - Behaviour::Ignorant - } - } - - fn larger(&self, prev: &(&Content, StyleChain), styles: StyleChain) -> bool { - let Some(other) = prev.0.to_packed::<VElem>() else { return false }; - match (self.amount(), other.amount()) { - (Spacing::Fr(this), Spacing::Fr(other)) => this > other, - (Spacing::Rel(this), Spacing::Rel(other)) => { - this.resolve(styles) > other.resolve(prev.1) - } - _ => false, - } - } -} - cast! { VElem, v: Content => v.unpack::<Self>().map_err(|_| "expected `v` element")?, diff --git a/crates/typst/src/math/ctx.rs b/crates/typst/src/math/ctx.rs index 06e35257..6da16406 100644 --- a/crates/typst/src/math/ctx.rs +++ b/crates/typst/src/math/ctx.rs @@ -11,17 +11,20 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{Content, Packed, StyleChain, StyleVec}; -use crate::introspection::{Locator, SplitLocator}; -use crate::layout::{layout_frame, Abs, Axes, BoxElem, Em, Frame, Region, Size}; +use crate::foundations::{Content, Packed, Resolve, StyleChain, StyleVec}; +use crate::introspection::{SplitLocator, TagElem}; +use crate::layout::{ + layout_frame, Abs, Axes, BoxElem, Em, Frame, HElem, PlaceElem, Region, Size, Spacing, +}; use crate::math::{ scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment, LayoutMath, MathFragment, MathRun, MathSize, THICK, }; +use crate::realize::{realize, Arenas, RealizationKind}; use crate::syntax::{is_newline, Span}; use crate::text::{ - features, BottomEdge, BottomEdgeMetric, Font, TextElem, TextSize, TopEdge, - TopEdgeMetric, + features, BottomEdge, BottomEdgeMetric, Font, LinebreakElem, SpaceElem, TextElem, + TextSize, TopEdge, TopEdgeMetric, }; macro_rules! scaled { @@ -45,10 +48,10 @@ macro_rules! percent { } /// The context for math layout. -pub struct MathContext<'a, 'b, 'v> { +pub struct MathContext<'a, 'v, 'e> { // External. - pub engine: &'v mut Engine<'b>, - pub locator: SplitLocator<'v>, + pub engine: &'v mut Engine<'e>, + pub locator: &'v mut SplitLocator<'a>, pub region: Region, // Font-related. pub font: &'a Font, @@ -62,10 +65,11 @@ pub struct MathContext<'a, 'b, 'v> { pub fragments: Vec<MathFragment>, } -impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { +impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { + /// Create a new math context. pub fn new( - engine: &'v mut Engine<'b>, - locator: Locator<'v>, + engine: &'v mut Engine<'e>, + locator: &'v mut SplitLocator<'a>, styles: StyleChain<'a>, base: Size, font: &'a Font, @@ -104,7 +108,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { Self { engine, - locator: locator.split(), + locator, region: Region::new(base, Axes::splat(false)), font, ttf: font.ttf(), @@ -117,18 +121,29 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { } } + /// Push a fragment. pub fn push(&mut self, fragment: impl Into<MathFragment>) { self.fragments.push(fragment.into()); } - pub fn extend(&mut self, fragments: Vec<MathFragment>) { + /// Push multiple fragments. + pub fn extend(&mut self, fragments: impl IntoIterator<Item = MathFragment>) { self.fragments.extend(fragments); } + /// Layout the given element and return the result as a [`MathRun`]. + pub fn layout_into_run( + &mut self, + elem: &Content, + styles: StyleChain, + ) -> SourceResult<MathRun> { + Ok(MathRun::new(self.layout_into_fragments(elem, styles)?)) + } + /// Layout the given element and return the resulting [`MathFragment`]s. pub fn layout_into_fragments( &mut self, - elem: &dyn LayoutMath, + elem: &Content, styles: StyleChain, ) -> SourceResult<Vec<MathFragment>> { // The element's layout_math() changes the fragments held in this @@ -136,24 +151,15 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { // them, so we restore the MathContext's fragments after obtaining the // layout result. let prev = std::mem::take(&mut self.fragments); - elem.layout_math(self, styles)?; + self.layout(elem, styles)?; Ok(std::mem::replace(&mut self.fragments, prev)) } - /// Layout the given element and return the result as a [`MathRun`]. - pub fn layout_into_run( - &mut self, - elem: &dyn LayoutMath, - styles: StyleChain, - ) -> SourceResult<MathRun> { - Ok(MathRun::new(self.layout_into_fragments(elem, styles)?)) - } - /// Layout the given element and return the result as a /// unified [`MathFragment`]. pub fn layout_into_fragment( &mut self, - elem: &dyn LayoutMath, + elem: &Content, styles: StyleChain, ) -> SourceResult<MathFragment> { Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles)) @@ -162,14 +168,89 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { /// Layout the given element and return the result as a [`Frame`]. pub fn layout_into_frame( &mut self, - elem: &dyn LayoutMath, + elem: &Content, styles: StyleChain, ) -> SourceResult<Frame> { Ok(self.layout_into_fragment(elem, styles)?.into_frame()) } +} + +impl MathContext<'_, '_, '_> { + /// Layout arbitrary content. + fn layout(&mut self, content: &Content, styles: StyleChain) -> SourceResult<()> { + let arenas = Arenas::default(); + let pairs = realize( + RealizationKind::Math, + self.engine, + self.locator, + &arenas, + content, + styles, + )?; + + let outer = styles; + for (elem, styles) in pairs { + // Hack because the font is fixed in math. + if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { + let frame = self.layout_external(elem, styles)?; + self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); + continue; + } + + self.layout_realized(elem, styles)?; + } + + Ok(()) + } + + /// Layout an element resulting from realization. + fn layout_realized( + &mut self, + elem: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + if let Some(elem) = elem.to_packed::<TagElem>() { + self.push(MathFragment::Tag(elem.tag.clone())); + } else if elem.is::<SpaceElem>() { + let font_size = scaled_font_size(self, styles); + self.push(MathFragment::Space(self.space_width.at(font_size))); + } else if elem.is::<LinebreakElem>() { + self.push(MathFragment::Linebreak); + } else if let Some(elem) = elem.to_packed::<HElem>() { + if let Spacing::Rel(rel) = elem.amount() { + if rel.rel.is_zero() { + self.push(MathFragment::Spacing( + rel.abs.resolve(styles), + elem.weak(styles), + )); + } + } + } else if let Some(elem) = elem.to_packed::<TextElem>() { + let fragment = self.layout_text(elem, styles)?; + self.push(fragment); + } else if let Some(boxed) = elem.to_packed::<BoxElem>() { + let frame = self.layout_box(boxed, styles)?; + self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); + } else if let Some(elem) = elem.with::<dyn LayoutMath>() { + elem.layout_math(self, styles)?; + } else { + let mut frame = self.layout_external(elem, styles)?; + if !frame.has_baseline() { + let axis = scaled!(self, styles, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + } + self.push( + FrameFragment::new(self, styles, frame) + .with_spaced(true) + .with_ignorant(elem.is::<PlaceElem>()), + ); + } + + Ok(()) + } - /// Layout the given [`BoxElem`] into a [`Frame`]. - pub fn layout_box( + /// Layout a box into a frame. + fn layout_box( &mut self, boxed: &Packed<BoxElem>, styles: StyleChain, @@ -184,8 +265,8 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { ) } - /// Layout the given [`Content`] into a [`Frame`]. - pub fn layout_content( + /// Layout into a frame with normal layout. + fn layout_external( &mut self, content: &Content, styles: StyleChain, @@ -202,7 +283,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { } /// Layout the given [`TextElem`] into a [`MathFragment`]. - pub fn layout_text( + fn layout_text( &mut self, elem: &Packed<TextElem>, styles: StyleChain, @@ -316,6 +397,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { } } +/// Converts some unit to an absolute length with the current font & font size. pub(super) trait Scaled { fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs; } diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs index 208540cd..ffefea74 100644 --- a/crates/typst/src/math/equation.rs +++ b/crates/typst/src/math/equation.rs @@ -15,7 +15,7 @@ use crate::layout::{ Size, SpecificAlignment, VAlignment, }; use crate::math::{ - scaled_font_size, LayoutMath, MathContext, MathRunFrameBuilder, MathSize, MathVariant, + scaled_font_size, MathContext, MathRunFrameBuilder, MathSize, MathVariant, }; use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; use crate::syntax::Span; @@ -48,10 +48,7 @@ use crate::World; /// least one space lifts it into a separate block that is centered /// horizontally. For more details about math syntax, see the /// [main math page]($category/math). -#[elem( - Locatable, Synthesize, Show, ShowSet, LayoutMath, Count, LocalName, Refable, - Outlinable -)] +#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] pub struct EquationElem { /// Whether the equation is displayed as a separate block. #[default(false)] @@ -258,13 +255,6 @@ impl Outlinable for Packed<EquationElem> { } } -impl LayoutMath for Packed<EquationElem> { - #[typst_macros::time(name = "math.equation", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - self.body().layout_math(ctx, styles) - } -} - /// Layout an inline equation (in a paragraph). #[typst_macros::time(span = elem.span())] fn layout_equation_inline( @@ -278,8 +268,9 @@ fn layout_equation_inline( let font = find_math_font(engine, styles, elem.span())?; - let mut ctx = MathContext::new(engine, locator, styles, region, &font); - let run = ctx.layout_into_run(elem, styles)?; + let mut locator = locator.split(); + let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font); + let run = ctx.layout_into_run(&elem.body, styles)?; let mut items = if run.row_count() == 1 { run.into_par_items() @@ -326,10 +317,9 @@ fn layout_equation_block( let font = find_math_font(engine, styles, span)?; let mut locator = locator.split(); - let mut ctx = - MathContext::new(engine, locator.next(&()), styles, regions.base(), &font); + let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font); let full_equation_builder = ctx - .layout_into_run(elem, styles)? + .layout_into_run(&elem.body, styles)? .multiline_frame_builder(&ctx, styles); let width = full_equation_builder.size.x; let can_break = diff --git a/crates/typst/src/math/fragment.rs b/crates/typst/src/math/fragment.rs index 959e4c8f..17e98878 100644 --- a/crates/typst/src/math/fragment.rs +++ b/crates/typst/src/math/fragment.rs @@ -23,7 +23,7 @@ pub enum MathFragment { Glyph(GlyphFragment), Variant(VariantFragment), Frame(FrameFragment), - Spacing(SpacingFragment), + Spacing(Abs, bool), Space(Abs), Linebreak, Align, @@ -40,7 +40,7 @@ impl MathFragment { Self::Glyph(glyph) => glyph.width, Self::Variant(variant) => variant.frame.width(), Self::Frame(fragment) => fragment.frame.width(), - Self::Spacing(spacing) => spacing.width, + Self::Spacing(amount, _) => *amount, Self::Space(amount) => *amount, _ => Abs::zero(), } @@ -86,7 +86,7 @@ impl MathFragment { Self::Glyph(glyph) => glyph.class, Self::Variant(variant) => variant.class, Self::Frame(fragment) => fragment.class, - Self::Spacing(_) => MathClass::Space, + Self::Spacing(_, _) => MathClass::Space, Self::Space(_) => MathClass::Space, Self::Linebreak => MathClass::Space, Self::Align => MathClass::Special, @@ -225,12 +225,6 @@ impl From<FrameFragment> for MathFragment { } } -impl From<SpacingFragment> for MathFragment { - fn from(fragment: SpacingFragment) -> Self { - Self::Spacing(fragment) - } -} - #[derive(Clone)] pub struct GlyphFragment { pub id: GlyphId, @@ -525,12 +519,6 @@ impl FrameFragment { } } -#[derive(Debug, Clone)] -pub struct SpacingFragment { - pub width: Abs, - pub weak: bool, -} - /// Look up the italics correction for a glyph. fn italics_correction(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option<Abs> { Some( diff --git a/crates/typst/src/math/lr.rs b/crates/typst/src/math/lr.rs index e542b8db..70fab12d 100644 --- a/crates/typst/src/math/lr.rs +++ b/crates/typst/src/math/lr.rs @@ -5,9 +5,7 @@ use crate::foundations::{ elem, func, Content, NativeElement, Packed, Resolve, Smart, StyleChain, }; use crate::layout::{Abs, Em, Length, Rel}; -use crate::math::{ - GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled, SpacingFragment, -}; +use crate::math::{GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled}; use crate::text::TextElem; use super::delimiter_alignment; @@ -90,10 +88,7 @@ impl LayoutMath for Packed<LrElem> { fragments.retain(|fragment| { index += 1; (index != 2 && index + 1 != original_len) - || !matches!( - fragment, - MathFragment::Spacing(SpacingFragment { weak: true, .. }) - ) + || !matches!(fragment, MathFragment::Spacing(_, true)) }); ctx.extend(fragments); diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs index 72b426b5..ab998975 100644 --- a/crates/typst/src/math/mod.rs +++ b/crates/typst/src/math/mod.rs @@ -42,15 +42,10 @@ use self::fragment::*; use self::row::*; use self::spacing::*; -use crate::diag::{At, SourceResult}; -use crate::foundations::{ - category, Category, Content, Module, Resolve, Scope, SequenceElem, StyleChain, - StyledElem, -}; -use crate::introspection::{TagElem, TagKind}; -use crate::layout::{BoxElem, HElem, Spacing, VAlignment}; -use crate::realize::{process, BehavedBuilder, Behaviour}; -use crate::text::{LinebreakElem, SpaceElem, TextElem}; +use crate::diag::SourceResult; +use crate::foundations::{category, Category, Module, Scope, StyleChain}; +use crate::layout::VAlignment; +use crate::text::TextElem; /// Typst has special [syntax]($syntax/#math) and library functions to typeset /// mathematical formulas. Math formulas can be displayed inline with text or as @@ -223,122 +218,6 @@ pub trait LayoutMath { fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()>; } -impl LayoutMath for Content { - #[typst_macros::time(name = "math", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - // Directly layout the body of nested equations instead of handling it - // like a normal equation so that things like this work: - // ``` - // #let my = $pi$ - // $ my r^2 $ - // ``` - if let Some(elem) = self.to_packed::<EquationElem>() { - return elem.layout_math(ctx, styles); - } - - if let Some((tag, realized)) = - process(ctx.engine, &mut ctx.locator, self, styles)? - { - ctx.engine.route.increase(); - ctx.engine.route.check_show_depth().at(self.span())?; - - if let Some(tag) = &tag { - ctx.push(MathFragment::Tag(tag.clone())); - } - realized.layout_math(ctx, styles)?; - if let Some(tag) = tag { - ctx.push(MathFragment::Tag(tag.with_kind(TagKind::End))); - } - - ctx.engine.route.decrease(); - return Ok(()); - } - - if self.is::<SequenceElem>() { - let mut bb = BehavedBuilder::new(); - self.sequence_recursive_for_each(&mut |child: &Content| { - bb.push(child, StyleChain::default()); - }); - for (child, _) in bb.finish() { - child.layout_math(ctx, styles)?; - } - return Ok(()); - } - - if let Some(styled) = self.to_packed::<StyledElem>() { - let outer = styles; - let styles = outer.chain(&styled.styles); - - if TextElem::font_in(styles) != TextElem::font_in(outer) { - let frame = ctx.layout_content(&styled.child, styles)?; - ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true)); - return Ok(()); - } - - styled.child.layout_math(ctx, styles)?; - return Ok(()); - } - - if self.is::<SpaceElem>() { - let font_size = scaled_font_size(ctx, styles); - ctx.push(MathFragment::Space(ctx.space_width.at(font_size))); - return Ok(()); - } - - if self.is::<LinebreakElem>() { - ctx.push(MathFragment::Linebreak); - return Ok(()); - } - - if let Some(elem) = self.to_packed::<HElem>() { - if let Spacing::Rel(rel) = elem.amount() { - if rel.rel.is_zero() { - ctx.push(SpacingFragment { - width: rel.abs.resolve(styles), - weak: elem.weak(styles), - }); - } - } - return Ok(()); - } - - if let Some(elem) = self.to_packed::<TextElem>() { - let fragment = ctx.layout_text(elem, styles)?; - ctx.push(fragment); - return Ok(()); - } - - if let Some(boxed) = self.to_packed::<BoxElem>() { - let frame = ctx.layout_box(boxed, styles)?; - ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true)); - return Ok(()); - } - - if let Some(elem) = self.to_packed::<TagElem>() { - ctx.push(MathFragment::Tag(elem.tag.clone())); - return Ok(()); - } - - if let Some(elem) = self.with::<dyn LayoutMath>() { - return elem.layout_math(ctx, styles); - } - - let mut frame = ctx.layout_content(self, styles)?; - if !frame.has_baseline() { - let axis = scaled!(ctx, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - } - - ctx.push( - FrameFragment::new(ctx, styles, frame) - .with_spaced(true) - .with_ignorant(self.behaviour() == Behaviour::Ignorant), - ); - - Ok(()) - } -} - fn delimiter_alignment(delimiter: char) -> VAlignment { match delimiter { '\u{231c}' | '\u{231d}' => VAlignment::Top, diff --git a/crates/typst/src/math/row.rs b/crates/typst/src/math/row.rs index cafffdc5..a8422b1e 100644 --- a/crates/typst/src/math/row.rs +++ b/crates/typst/src/math/row.rs @@ -10,8 +10,6 @@ use crate::math::{ }; use crate::model::ParElem; -use super::fragment::SpacingFragment; - pub const TIGHT_LEADING: Em = Em::new(0.25); /// A linear collection of [`MathFragment`]s. @@ -37,9 +35,21 @@ impl MathRun { } // Explicit spacing disables automatic spacing. - MathFragment::Spacing(_) => { + MathFragment::Spacing(width, weak) => { last = None; space = None; + + if weak { + match resolved.last_mut() { + None => continue, + Some(MathFragment::Spacing(prev, true)) => { + *prev = (*prev).max(width); + continue; + } + Some(_) => {} + } + } + resolved.push(fragment); continue; } @@ -91,6 +101,10 @@ impl MathRun { resolved.push(fragment); } + if let Some(MathFragment::Spacing(_, true)) = resolved.last() { + resolved.pop(); + } + Self(resolved) } @@ -290,15 +304,14 @@ impl MathRun { let is_relation = |f: &MathFragment| matches!(f.class(), MathClass::Relation); let is_space = |f: &MathFragment| { - matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_)) + matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_, _)) }; let mut iter = self.0.into_iter().peekable(); while let Some(fragment) = iter.next() { if space_is_visible { match fragment { - MathFragment::Space(width) - | MathFragment::Spacing(SpacingFragment { width, .. }) => { + MathFragment::Space(width) | MathFragment::Spacing(width, _) => { items.push(InlineItem::Space(width, true)); continue; } diff --git a/crates/typst/src/math/spacing.rs b/crates/typst/src/math/spacing.rs index 6f3dc8cc..377a2248 100644 --- a/crates/typst/src/math/spacing.rs +++ b/crates/typst/src/math/spacing.rs @@ -2,7 +2,7 @@ use unicode_math_class::MathClass; use crate::foundations::{NativeElement, Scope}; use crate::layout::{Abs, Em, HElem}; -use crate::math::{MathFragment, MathSize, SpacingFragment}; +use crate::math::{MathFragment, MathSize}; pub(super) const THIN: Em = Em::new(1.0 / 6.0); pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0); @@ -29,7 +29,7 @@ pub(super) fn spacing( let resolve = |v: Em, size_ref: &MathFragment| -> Option<MathFragment> { let width = size_ref.font_size().map_or(Abs::zero(), |size| v.at(size)); - Some(SpacingFragment { width, weak: false }.into()) + Some(MathFragment::Spacing(width, false)) }; let script = |f: &MathFragment| f.math_size().is_some_and(|s| s <= MathSize::Script); diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs index 71616616..f003252d 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -231,7 +231,9 @@ impl Show for Packed<BibliographyElem> { .ok_or("CSL style is not suitable for bibliographies") .at(span)?; - let row_gutter = ParElem::spacing_in(styles).into(); + let row_gutter = ParElem::spacing_in(styles); + let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack(); + if references.iter().any(|(prefix, _)| prefix.is_some()) { let mut cells = vec![]; for (prefix, reference) in references { @@ -244,18 +246,18 @@ impl Show for Packed<BibliographyElem> { ))); } - seq.push(VElem::new(row_gutter).with_weakness(3).pack()); + seq.push(row_gutter_elem.clone()); seq.push( GridElem::new(cells) .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) - .with_row_gutter(TrackSizings(smallvec![(row_gutter).into()])) + .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) .pack() .spanned(self.span()), ); } else { for (_, reference) in references { - seq.push(VElem::new(row_gutter).with_weakness(3).pack()); + seq.push(row_gutter_elem.clone()); seq.push(reference.clone()); } } diff --git a/crates/typst/src/model/enum.rs b/crates/typst/src/model/enum.rs index 336a8fc9..32a400e8 100644 --- a/crates/typst/src/model/enum.rs +++ b/crates/typst/src/model/enum.rs @@ -14,7 +14,7 @@ use crate::layout::{ Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Length, Regions, Sizing, VAlignment, VElem, }; -use crate::model::{Numbering, NumberingPattern, ParElem}; +use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; use crate::text::TextElem; /// A numbered list. @@ -224,7 +224,8 @@ impl Show for Packed<EnumElem> { if self.tight(styles) { let leading = ParElem::leading_in(styles); - let spacing = VElem::list_attach(leading.into()).pack(); + let spacing = + VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); realized = spacing + realized; } @@ -325,14 +326,6 @@ pub struct EnumItem { pub body: Content, } -impl Packed<EnumItem> { - /// Apply styles to this enum item. - pub fn styled(mut self, styles: Styles) -> Self { - self.body.style_in_place(styles); - self - } -} - cast! { EnumItem, array: Array => { @@ -345,3 +338,18 @@ cast! { }, v: Content => v.unpack::<Self>().unwrap_or_else(Self::new), } + +impl ListLike for EnumElem { + type Item = EnumItem; + + fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self { + Self::new(children).with_tight(tight) + } +} + +impl ListItemLike for EnumItem { + fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> { + item.body.style_in_place(styles); + item + } +} diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs index 164d1b48..2d7fc131 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -309,7 +309,7 @@ impl Show for Packed<FigureElem> { // Build the caption, if any. if let Some(caption) = self.caption(styles) { - let v = VElem::weak(self.gap(styles).into()).pack(); + let v = VElem::new(self.gap(styles).into()).with_weak(true).pack(); realized = match caption.position(styles) { OuterVAlignment::Top => caption.pack() + v + realized, OuterVAlignment::Bottom => realized + v + caption.pack(), diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs index a0d76770..0c55b7ca 100644 --- a/crates/typst/src/model/list.rs +++ b/crates/typst/src/model/list.rs @@ -148,7 +148,8 @@ impl Show for Packed<ListElem> { if self.tight(styles) { let leading = ParElem::leading_in(styles); - let spacing = VElem::list_attach(leading.into()).pack(); + let spacing = + VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); realized = spacing + realized; } @@ -218,14 +219,6 @@ pub struct ListItem { pub body: Content, } -impl Packed<ListItem> { - /// Apply styles to this list item. - pub fn styled(mut self, styles: Styles) -> Self { - self.body.style_in_place(styles); - self - } -} - cast! { ListItem, v: Content => v.unpack::<Self>().unwrap_or_else(Self::new) @@ -276,3 +269,33 @@ cast! { }, v: Func => Self::Func(v), } + +/// A list, enum, or term list. +pub trait ListLike: NativeElement { + /// The kind of list item this list is composed of. + type Item: ListItemLike; + + /// Create this kind of list from its children and tightness. + fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self; +} + +/// A list item, enum item, or term list item. +pub trait ListItemLike: NativeElement { + /// Apply styles to the element's body. + fn styled(item: Packed<Self>, styles: Styles) -> Packed<Self>; +} + +impl ListLike for ListElem { + type Item = ListItem; + + fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self { + Self::new(children).with_tight(tight) + } +} + +impl ListItemLike for ListItem { + fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> { + item.body.style_in_place(styles); + item + } +} diff --git a/crates/typst/src/model/quote.rs b/crates/typst/src/model/quote.rs index 528c0998..ff6ae110 100644 --- a/crates/typst/src/model/quote.rs +++ b/crates/typst/src/model/quote.rs @@ -207,8 +207,9 @@ impl Show for Packed<QuoteElem> { // Use v(0.9em, weak: true) bring the attribution closer to the // quote. - let weak_v = VElem::weak(Spacing::Rel(Em::new(0.9).into())).pack(); - realized += weak_v + Content::sequence(seq).aligned(Alignment::END); + let gap = Spacing::Rel(Em::new(0.9).into()); + let v = VElem::new(gap).with_weak(true).pack(); + realized += v + Content::sequence(seq).aligned(Alignment::END); } realized = PadElem::new(realized).pack(); diff --git a/crates/typst/src/model/terms.rs b/crates/typst/src/model/terms.rs index 98fe3ff1..75facc8d 100644 --- a/crates/typst/src/model/terms.rs +++ b/crates/typst/src/model/terms.rs @@ -5,7 +5,7 @@ use crate::foundations::{ Styles, }; use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; -use crate::model::ParElem; +use crate::model::{ListItemLike, ListLike, ParElem}; use crate::text::TextElem; use crate::utils::Numeric; @@ -150,7 +150,8 @@ impl Show for Packed<TermsElem> { if self.tight(styles) { let leading = ParElem::leading_in(styles); - let spacing = VElem::list_attach(leading.into()).pack(); + let spacing = + VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); realized = spacing + realized; } @@ -170,15 +171,6 @@ pub struct TermItem { pub description: Content, } -impl Packed<TermItem> { - /// Apply styles to this term item. - pub fn styled(mut self, styles: Styles) -> Self { - self.term.style_in_place(styles.clone()); - self.description.style_in_place(styles); - self - } -} - cast! { TermItem, array: Array => { @@ -191,3 +183,19 @@ cast! { }, v: Content => v.unpack::<Self>().map_err(|_| "expected term item or array")?, } + +impl ListLike for TermsElem { + type Item = TermItem; + + fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self { + Self::new(children).with_tight(tight) + } +} + +impl ListItemLike for TermItem { + fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> { + item.term.style_in_place(styles.clone()); + item.description.style_in_place(styles); + item + } +} diff --git a/crates/typst/src/realize.rs b/crates/typst/src/realize.rs new file mode 100644 index 00000000..9ebdd87a --- /dev/null +++ b/crates/typst/src/realize.rs @@ -0,0 +1,1238 @@ +//! Realization of content. +//! +//! *Realization* is the process of recursively applying styling and, in +//! particular, show rules to produce well-known elements that can be laid out. + +use std::borrow::Cow; + +use arrayvec::ArrayVec; +use bumpalo::collections::{String as BumpString, Vec as BumpVec}; +use comemo::Track; +use ecow::EcoString; +use once_cell::unsync::Lazy; + +use crate::diag::{bail, At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector, + SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles, + Synthesize, Transformation, +}; +use crate::introspection::{Locatable, SplitLocator, Tag, TagElem, TagKind}; +use crate::layout::{ + AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem, +}; +use crate::math::{EquationElem, LayoutMath}; +use crate::model::{ + CiteElem, CiteGroup, DocumentElem, DocumentInfo, EnumElem, ListElem, ListItemLike, + ListLike, ParElem, ParbreakElem, TermsElem, +}; +use crate::syntax::Span; +use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; +use crate::utils::{SliceExt, SmallBitSet}; + +/// A pair of content and a style chain that applies to it. +pub type Pair<'a> = (&'a Content, StyleChain<'a>); + +/// Realize content into a flat list of well-known, styled items. +#[typst_macros::time(name = "realize")] +pub fn realize<'a>( + kind: RealizationKind, + engine: &mut Engine, + locator: &mut SplitLocator, + arenas: &'a Arenas, + content: &'a Content, + styles: StyleChain<'a>, +) -> SourceResult<Vec<Pair<'a>>> { + let mut s = State { + engine, + locator, + arenas, + rules: match kind { + RealizationKind::Root(_) | RealizationKind::Container => NORMAL_RULES, + RealizationKind::Math => MATH_RULES, + }, + sink: vec![], + groupings: ArrayVec::new(), + outside: matches!(kind, RealizationKind::Root(_)), + may_attach: false, + kind, + }; + + visit(&mut s, content, styles)?; + finish(&mut s)?; + + Ok(s.sink) +} + +/// Defines what kind of realization we are performing. +pub enum RealizationKind<'a> { + /// This the root realization for the document. Requires a mutable reference + /// to document metadata that will be filled from `set document` rules. + Root(&'a mut DocumentInfo), + /// A nested realization in a container (e.g. a `block`). + Container, + /// A realization within math. + Math, +} + +/// Temporary storage arenas for lifetime extension during realization. +/// +/// Must be kept live while the content returned from realization is processed. +#[derive(Default)] +pub struct Arenas { + /// A typed arena for owned content. + content: typed_arena::Arena<Content>, + /// A typed arena for owned styles. + styles: typed_arena::Arena<Styles>, + /// An untyped arena for everything that is `Copy`. + bump: bumpalo::Bump, +} + +/// Mutable state for realization. +/// +/// Sadly, we need that many lifetimes because &mut references are invariant and +/// it would force the lifetimes of e.g. engine and locator to be equal if they +/// shared a lifetime. We can get around it by enforcing the lifetimes on +/// `fn realize`, but that makes it less flexible on the call site, which isn't +/// worth it. +/// +/// The only interesting lifetime is 'a, which is that of the content that comes +/// in and goes out. It's the same 'a as on `fn realize`. +struct State<'a, 'x, 'y, 'z> { + /// Defines what kind of realization we are performing. + kind: RealizationKind<'x>, + /// The engine. + engine: &'x mut Engine<'y>, + /// Assigns unique locations to elements. + locator: &'x mut SplitLocator<'z>, + /// Temporary storage arenas for lifetime extension during realization. + arenas: &'a Arenas, + /// The output elements of well-known types. + sink: Vec<Pair<'a>>, + /// Grouping rules used for realization. + rules: &'x [&'x GroupingRule], + /// Currently active groupings. + groupings: ArrayVec<Grouping<'x>, MAX_GROUP_NESTING>, + /// Whether we are currently not within any container or show rule output. + /// This is used to determine page styles during layout. + outside: bool, + /// Whether now following attach spacing can survive. + may_attach: bool, +} + +/// Defines a rule for how certain elements shall be grouped during realization. +struct GroupingRule { + /// When an element is visited that matches a rule with higher priority + /// than one that is currently grouped, we start a nested group. + priority: u8, + /// Whether the grouping handles tags itself. If this is set to `false`, + /// realization will transparently take care of tags and they will not + /// be visible to `finish`. + tags: bool, + /// Defines which kinds of elements start and make up this kind of grouping. + trigger: fn(Element) -> bool, + /// Defines elements that may appear in the interior of the grouping, but + /// not at the edges. + inner: fn(Element) -> bool, + /// Defines whether styles for this kind of element interrupt the grouping. + interrupt: fn(Element) -> bool, + /// Should convert the accumulated elements in `s.sink[start..]` into + /// the grouped element. + finish: fn(Grouped) -> SourceResult<()>, +} + +/// A started grouping of some elements. +struct Grouping<'a> { + /// The position in `s.sink` where the group starts. + start: usize, + /// The rule used for this grouping. + rule: &'a GroupingRule, +} + +/// The result of grouping. +struct Grouped<'a, 'x, 'y, 'z, 's> { + /// The realization state. + s: &'s mut State<'a, 'x, 'y, 'z>, + /// The position in `s.sink` where the group starts. + start: usize, +} + +/// What to do with an element when encountering it during realization. +struct Verdict<'a> { + /// Whether the element is already prepared (i.e. things that should only + /// happen once have happened). + prepared: bool, + /// A map of styles to apply to the element. + map: Styles, + /// An optional show rule transformation to apply to the element. + step: Option<ShowStep<'a>>, +} + +/// A show rule transformation to apply to the element. +enum ShowStep<'a> { + /// A user-defined transformational show rule. + Recipe(&'a Recipe, RecipeIndex), + /// The built-in show rule. + Builtin, +} + +/// A match of a regex show rule. +struct RegexMatch<'a> { + /// The offset in the string that matched. + offset: usize, + /// The text that matched. + text: EcoString, + /// The style chain of the matching grouping. + styles: StyleChain<'a>, + /// The index of the recipe that matched. + id: RecipeIndex, + /// The recipe that matched. + recipe: &'a Recipe, +} + +/// State kept for space collapsing. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum SpaceState { + /// A following space will be collapsed. + Destructive, + /// A following space will be kept unless a destructive element follows. + Supportive, + /// A space exists at this index. + Space(usize), +} + +impl<'a> State<'a, '_, '_, '_> { + /// Lifetime-extends some content. + fn store(&self, content: Content) -> &'a Content { + self.arenas.content.alloc(content) + } + + /// Lifetime-extends some pairs. + /// + /// By using a `BumpVec` instead of a `alloc_slice_copy` we can reuse + /// the space if no other bump allocations have been made by the time + /// the `BumpVec` is dropped. + fn store_slice(&self, pairs: &[Pair<'a>]) -> BumpVec<'a, Pair<'a>> { + let mut vec = BumpVec::new_in(&self.arenas.bump); + vec.extend_from_slice_copy(pairs); + vec + } +} + +impl<'a, 'x, 'y, 'z, 's> Grouped<'a, 'x, 'y, 'z, 's> { + /// Accesses the grouped elements. + fn get(&self) -> &[Pair<'a>] { + &self.s.sink[self.start..] + } + + /// Accesses the grouped elements mutably. + fn get_mut(&mut self) -> (&mut Vec<Pair<'a>>, usize) { + (&mut self.s.sink, self.start) + } + + /// Removes the grouped elements from the sink and retrieves back the state + /// with which resulting elements can be visited. + fn end(self) -> &'s mut State<'a, 'x, 'y, 'z> { + self.s.sink.truncate(self.start); + self.s + } +} + +/// Handles an arbitrary piece of content during realization. +fn visit<'a>( + s: &mut State<'a, '_, '_, '_>, + content: &'a Content, + styles: StyleChain<'a>, +) -> SourceResult<()> { + // Tags can always simply be pushed. + if content.is::<TagElem>() { + s.sink.push((content, styles)); + return Ok(()); + } + + // Transformations for math content based on the realization kind. Needs + // to happen before show rules. + if visit_math_rules(s, content, styles)? { + return Ok(()); + } + + // Apply show rules rules and preparation. + if visit_show_rules(s, content, styles)? { + return Ok(()); + } + + // Recurse into sequences. Styled elements and sequences can currently also + // have labels, so this needs to happen before they are handled. + if let Some(sequence) = content.to_packed::<SequenceElem>() { + for elem in &sequence.children { + visit(s, elem, styles)?; + } + return Ok(()); + } + + // Recurse into styled elements. + if let Some(styled) = content.to_packed::<StyledElem>() { + return visit_styled(s, &styled.child, Cow::Borrowed(&styled.styles), styles); + } + + // Apply grouping --- where multiple elements are collected and then + // processed together (typically being transformed into one). + if visit_grouping_rules(s, content, styles)? { + return Ok(()); + } + + // Some elements are skipped based on specific circumstances. + if visit_filter_rules(s, content, styles)? { + return Ok(()); + } + + // No further transformations to apply, so we can finally just push it to + // the output! + s.sink.push((content, styles)); + + Ok(()) +} + +// Handles special cases for math in normal content and nested equations in +// math. +fn visit_math_rules<'a>( + s: &mut State<'a, '_, '_, '_>, + content: &'a Content, + styles: StyleChain<'a>, +) -> SourceResult<bool> { + if let RealizationKind::Math = s.kind { + // Transparently recurse into equations nested in math, so that things + // like this work: + // ``` + // #let my = $pi$ + // $ my r^2 $ + // ``` + if let Some(elem) = content.to_packed::<EquationElem>() { + visit(s, &elem.body, styles)?; + return Ok(true); + } + + // In normal realization, we apply regex show rules to consecutive + // textual elements via `TEXTUAL` grouping. However, in math, this is + // not desirable, so we just do it on a per-element basis. + if let Some(elem) = content.to_packed::<TextElem>() { + if let Some(m) = find_regex_match_in_str(&elem.text, styles) { + visit_regex_match(s, &[(content, styles)], m)?; + return Ok(true); + } + } + } else { + // Transparently wrap math-y content into equations. + if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() { + let eq = EquationElem::new(content.clone()).pack().spanned(content.span()); + visit(s, s.store(eq), styles)?; + return Ok(true); + } + } + + Ok(false) +} + +/// Tries to apply show rules to or prepare content. Returns `true` if the +/// element was handled. +fn visit_show_rules<'a>( + s: &mut State<'a, '_, '_, '_>, + content: &'a Content, + styles: StyleChain<'a>, +) -> SourceResult<bool> { + // Determines whether and how to proceed with show rule application. + let Some(Verdict { prepared, mut map, step }) = verdict(s.engine, content, styles) + else { + return Ok(false); + }; + + // Create a fresh copy that we can mutate. + let mut output = Cow::Borrowed(content); + + // If the element isn't yet prepared (we're seeing it for the first time), + // prepare it. + let mut tag = None; + if !prepared { + tag = prepare(s.engine, s.locator, output.to_mut(), &mut map, styles)?; + } + + // Apply a show rule step, if there is one. + if let Some(step) = step { + let chained = styles.chain(&map); + let result = match step { + // Apply a user-defined show rule. + ShowStep::Recipe(recipe, guard) => { + let context = Context::new(output.location(), Some(chained)); + recipe.apply( + s.engine, + context.track(), + output.into_owned().guarded(guard), + ) + } + + // Apply a built-in show rule. + ShowStep::Builtin => { + output.with::<dyn Show>().unwrap().show(s.engine, chained) + } + }; + + // Errors in show rules don't terminate compilation immediately. We just + // continue with empty content for them and show all errors together, if + // they remain by the end of the introspection loop. + // + // This way, we can ignore errors that only occur in earlier iterations + // and also show more useful errors at once. + output = Cow::Owned(s.engine.delay(result)); + } + + // Lifetime-extend the realized content if necessary. + let realized = match output { + Cow::Borrowed(realized) => realized, + Cow::Owned(realized) => s.store(realized), + }; + + // Push start tag. + if let Some(tag) = &tag { + let start_tag = TagElem::packed(tag.clone()); + visit(s, s.store(start_tag), styles)?; + } + + let prev_outside = s.outside; + s.outside &= content.is::<ContextElem>(); + s.engine.route.increase(); + s.engine.route.check_show_depth().at(content.span())?; + + visit_styled(s, realized, Cow::Owned(map), styles)?; + + s.outside = prev_outside; + s.engine.route.decrease(); + + // Push end tag. + if let Some(tag) = tag { + let end_tag = TagElem::packed(tag.with_kind(TagKind::End)); + visit(s, s.store(end_tag), styles)?; + } + + Ok(true) +} + +/// Inspects a target element and the current styles and determines how to +/// proceed with the styling. +fn verdict<'a>( + engine: &mut Engine, + target: &'a Content, + styles: StyleChain<'a>, +) -> Option<Verdict<'a>> { + let prepared = target.is_prepared(); + let mut map = Styles::new(); + let mut step = None; + + // Do pre-synthesis on a cloned element to be able to match on synthesized + // fields before real synthesis runs (during preparation). It's really + // unfortunate that we have to do this, but otherwise + // `show figure.where(kind: table)` won't work :( + let mut target = target; + let mut slot; + if !prepared && target.can::<dyn Synthesize>() { + slot = target.clone(); + slot.with_mut::<dyn Synthesize>() + .unwrap() + .synthesize(engine, styles) + .ok(); + target = &slot; + } + + // Lazily computes the total number of recipes in the style chain. We need + // it to determine whether a particular show rule was already applied to the + // `target` previously. For this purpose, show rules are indexed from the + // top of the chain as the chain might grow to the bottom. + let depth = Lazy::new(|| styles.recipes().count()); + + for (r, recipe) in styles.recipes().enumerate() { + // We're not interested in recipes that don't match. + if !recipe + .selector() + .is_some_and(|selector| selector.matches(target, Some(styles))) + { + continue; + } + + // Special handling for show-set rules. + if let Transformation::Style(transform) = recipe.transform() { + if !prepared { + map.apply(transform.clone()); + } + continue; + } + + // If we already have a show step, don't look for one. + if step.is_some() { + continue; + } + + // Check whether this show rule was already applied to the target. + let index = RecipeIndex(*depth - r); + if target.is_guarded(index) { + continue; + } + + // We'll apply this recipe. + step = Some(ShowStep::Recipe(recipe, index)); + + // If we found a show rule and are already prepared, there is nothing + // else to do, so we can just break. If we are not yet prepared, + // continue searching for potential show-set styles. + if prepared { + break; + } + } + + // If we found no user-defined rule, also consider the built-in show rule. + if step.is_none() && target.can::<dyn Show>() { + step = Some(ShowStep::Builtin); + } + + // If there's no nothing to do, there is also no verdict. + if step.is_none() + && map.is_empty() + && (prepared || { + target.label().is_none() + && target.location().is_none() + && !target.can::<dyn ShowSet>() + && !target.can::<dyn Locatable>() + && !target.can::<dyn Synthesize>() + }) + { + return None; + } + + Some(Verdict { prepared, map, step }) +} + +/// This is only executed the first time an element is visited. +fn prepare( + engine: &mut Engine, + locator: &mut SplitLocator, + target: &mut Content, + map: &mut Styles, + styles: StyleChain, +) -> SourceResult<Option<Tag>> { + // Generate a location for the element, which uniquely identifies it in + // the document. This has some overhead, so we only do it for elements + // that are explicitly marked as locatable and labelled elements. + // + // The element could already have a location even if it is not prepared + // when it stems from a query. + let mut key = None; + if target.location().is_some() { + key = Some(crate::utils::hash128(&target)); + } else if target.can::<dyn Locatable>() || target.label().is_some() { + let hash = crate::utils::hash128(&target); + let location = locator.next_location(engine.introspector, hash); + target.set_location(location); + key = Some(hash); + } + + // Apply built-in show-set rules. User-defined show-set rules are already + // considered in the map built while determining the verdict. + if let Some(show_settable) = target.with::<dyn ShowSet>() { + map.apply(show_settable.show_set(styles)); + } + + // If necessary, generated "synthesized" fields (which are derived from + // other fields or queries). Do this after show-set so that show-set styles + // are respected. + if let Some(synthesizable) = target.with_mut::<dyn Synthesize>() { + synthesizable.synthesize(engine, styles.chain(map))?; + } + + // Copy style chain fields into the element itself, so that they are + // available in rules. + target.materialize(styles.chain(map)); + + // If the element is locatable, create a tag element to be able to find the + // element in the frames after layout. Do this after synthesis and + // materialization, so that it includes the synthesized fields. Do it before + // marking as prepared so that show-set rules will apply to this element + // when queried. + let tag = key.map(|key| Tag::new(target.clone(), key)); + + // Ensure that this preparation only runs once by marking the element as + // prepared. + target.mark_prepared(); + + Ok(tag) +} + +/// Handles a styled element. +fn visit_styled<'a>( + s: &mut State<'a, '_, '_, '_>, + content: &'a Content, + mut local: Cow<'a, Styles>, + outer: StyleChain<'a>, +) -> SourceResult<()> { + // Nothing to do if the styles are actually empty. + if local.is_empty() { + return visit(s, content, outer); + } + + // Check for document and page styles. + let mut pagebreak = false; + for style in local.iter() { + let Some(elem) = style.element() else { continue }; + if elem == DocumentElem::elem() { + let RealizationKind::Root(info) = &mut s.kind else { + let span = style.span(); + bail!(span, "document set rules are not allowed inside of containers"); + }; + + info.populate(&local); + } else if elem == PageElem::elem() { + let RealizationKind::Root(_) = s.kind else { + let span = style.span(); + bail!(span, "page configuration is not allowed inside of containers"); + }; + + // When there are page styles, we "break free" from our show rule cage. + pagebreak = true; + s.outside = true; + } + } + + // If we are not within a container or show rule, mark the styles as + // "outside". This will allow them to be lifted to the page level. + if s.outside { + local = Cow::Owned(local.into_owned().outside()); + } + + // Lifetime-extend the styles if necessary. + let outer = s.arenas.bump.alloc(outer); + let local = match local { + Cow::Borrowed(map) => map, + Cow::Owned(owned) => &*s.arenas.styles.alloc(owned), + }; + + // Generate a weak pagebreak if there is a page interruption. For the + // starting pagebreak we only want the styles before and including the + // interruptions, not trailing styles that happen to be in the same `Styles` + // list, so we trim the local styles. + if pagebreak { + let relevant = local + .as_slice() + .trim_end_matches(|style| style.element() != Some(PageElem::elem())); + visit(s, PagebreakElem::shared_weak(), outer.chain(relevant))?; + } + + finish_interrupted(s, local)?; + visit(s, content, outer.chain(local))?; + finish_interrupted(s, local)?; + + // Generate a weak "boundary" pagebreak at the end. In comparison to a + // normal weak pagebreak, the styles of this are ignored during layout, so + // it doesn't really matter what we use here. + if pagebreak { + visit(s, PagebreakElem::shared_boundary(), *outer)?; + } + + Ok(()) +} + +/// Tries to group the content in an active group or start a new one if any +/// grouping rule matches. Returns `true` if the element was grouped. +fn visit_grouping_rules<'a>( + s: &mut State<'a, '_, '_, '_>, + content: &'a Content, + styles: StyleChain<'a>, +) -> SourceResult<bool> { + let elem = content.elem(); + let matching = s.rules.iter().find(|&rule| (rule.trigger)(elem)); + + // Try to continue or finish an existing grouping. + while let Some(active) = s.groupings.last() { + // Start a nested group if a rule with higher priority matches. + if matching.is_some_and(|rule| rule.priority > active.rule.priority) { + break; + } + + // If the element can be added to the active grouping, do it. + if (active.rule.trigger)(elem) || (active.rule.inner)(elem) { + s.sink.push((content, styles)); + return Ok(true); + } + + finish_innermost_grouping(s)?; + } + + // Start a new grouping. + if let Some(rule) = matching { + let start = s.sink.len(); + s.groupings.push(Grouping { start, rule }); + s.sink.push((content, styles)); + return Ok(true); + } + + Ok(false) +} + +/// Some elements don't make it to the sink depending on the realization kind +/// and current state. +fn visit_filter_rules<'a>( + s: &mut State<'a, '_, '_, '_>, + content: &'a Content, + styles: StyleChain<'a>, +) -> SourceResult<bool> { + if content.is::<SpaceElem>() && !matches!(s.kind, RealizationKind::Math) { + // Outside of maths, spaces that were not collected by the paragraph + // grouper don't interest us. + return Ok(true); + } else if content.is::<ParbreakElem>() { + // Paragraph breaks are only a boundary for paragraph grouping, we don't + // need to store them. + s.may_attach = false; + return Ok(true); + } else if !s.may_attach + && content.to_packed::<VElem>().is_some_and(|elem| elem.attach(styles)) + { + // Delete attach spacing collapses if not immediately following a + // paragraph. + return Ok(true); + } + + // Remember whether following attach spacing can survive. + s.may_attach = content.is::<ParElem>(); + + Ok(false) +} + +/// Finishes all grouping. +fn finish(s: &mut State) -> SourceResult<()> { + finish_grouping_while(s, |s| !s.groupings.is_empty())?; + + // In math, spaces are top-level. + if let RealizationKind::Math = s.kind { + collapse_spaces(&mut s.sink, 0); + } + + Ok(()) +} + +/// Finishes groupings while any active group is interrupted by the styles. +fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { + let mut last = None; + for elem in local.iter().filter_map(|style| style.element()) { + if last == Some(elem) { + continue; + } + finish_grouping_while(s, |s| { + s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem)) + })?; + last = Some(elem); + } + Ok(()) +} + +/// Finishes groupings while `f` returns `true`. +fn finish_grouping_while<F>(s: &mut State, f: F) -> SourceResult<()> +where + F: Fn(&State) -> bool, +{ + // Finishing of a group may result in new content and new grouping. This + // can, in theory, go on for a bit. To prevent it from becoming an infinite + // loop, we keep track of the iteration count. + let mut i = 0; + while f(s) { + finish_innermost_grouping(s)?; + i += 1; + if i > 512 { + bail!(Span::detached(), "maximum grouping depth exceeded"); + } + } + Ok(()) +} + +/// Finishes the currently innermost grouping. +fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> { + // The grouping we are interrupting. + let Grouping { start, rule } = s.groupings.pop().unwrap(); + + // Trim trailing non-trigger elements. + let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c.elem())); + let end = start + trimmed.len(); + let tail = s.store_slice(&s.sink[end..]); + s.sink.truncate(end); + + // If the grouping is not interested in tags, remove and collect them. + let mut tags = BumpVec::<Pair>::new_in(&s.arenas.bump); + if !rule.tags { + let mut k = start; + for i in start..end { + if s.sink[i].0.is::<TagElem>() { + tags.push(s.sink[i]); + continue; + } + + if k < i { + s.sink[k] = s.sink[i]; + } + k += 1; + } + s.sink.truncate(k); + } + + // Execute the grouping's finisher rule. + (rule.finish)(Grouped { s, start })?; + + // Visit the tags and staged elements again. + for &(content, styles) in tags.iter().chain(&tail) { + visit(s, content, styles)?; + } + + Ok(()) +} + +/// The maximum number of nested groups that are possible. Corresponds to the +/// number of unique priority levels. +const MAX_GROUP_NESTING: usize = 3; + +/// Grouping rules used in normal realizations. +static NORMAL_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; + +/// Grouping rules used in math realization. +static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS]; + +/// Groups adjacent textual elements for text show rule application. +static TEXTUAL: GroupingRule = GroupingRule { + priority: 3, + tags: true, + trigger: |elem| { + elem == TextElem::elem() + || elem == LinebreakElem::elem() + || elem == SmartQuoteElem::elem() + }, + inner: |elem| elem == SpaceElem::elem(), + // Any kind of style interrupts this kind of grouping since regex show + // rules cannot match over style changes anyway. + interrupt: |_| true, + finish: finish_textual, +}; + +/// Collects inline-level elements into a `ParElem`. +static PAR: GroupingRule = GroupingRule { + priority: 1, + tags: true, + trigger: |elem| { + elem == TextElem::elem() + || elem == HElem::elem() + || elem == LinebreakElem::elem() + || elem == SmartQuoteElem::elem() + || elem == InlineElem::elem() + || elem == BoxElem::elem() + }, + inner: |elem| elem == SpaceElem::elem(), + interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(), + finish: finish_par, +}; + +/// Collects `CiteElem`s into `CiteGroup`s. +static CITES: GroupingRule = GroupingRule { + priority: 2, + tags: false, + trigger: |elem| elem == CiteElem::elem(), + inner: |elem| elem == SpaceElem::elem(), + interrupt: |elem| elem == CiteGroup::elem(), + finish: finish_cites, +}; + +/// Builds a `ListElem` from grouped `ListItems`s. +static LIST: GroupingRule = list_like_grouping::<ListElem>(); + +/// Builds an `EnumElem` from grouped `EnumItem`s. +static ENUM: GroupingRule = list_like_grouping::<EnumElem>(); + +/// Builds a `TermsElem` from grouped `TermItem`s. +static TERMS: GroupingRule = list_like_grouping::<TermsElem>(); + +/// Collects `ListItemLike` elements into a `ListLike` element. +const fn list_like_grouping<T: ListLike>() -> GroupingRule { + GroupingRule { + priority: 2, + tags: false, + trigger: |elem| elem == T::Item::elem(), + inner: |elem| elem == SpaceElem::elem() || elem == ParbreakElem::elem(), + interrupt: |elem| elem == T::elem(), + finish: finish_list_like::<T>, + } +} + +/// Processes grouped textual elements. +/// +/// Specificaly, it searches for regex matches in grouped textual elements and +/// - if there was a match, visits the results recursively, +/// - if there was no match, tries to simply implicitly use the grouped elements +/// as part of a paragraph grouping, +/// - if that's not possible because another grouping is active, temporarily +/// disables textual grouping and revisits the elements. +fn finish_textual(Grouped { s, start }: Grouped) -> SourceResult<()> { + // Try to find a regex match in the grouped textual elements. + if visit_textual(s, start)? { + return Ok(()); + } + + // No regex match. + match s.groupings.last() { + // Transparently apply the grouped content to an active paragraph. This + // is more efficient than revisiting everything. Checking the priority + // is a bit of a hack, but the simplest way to check which rule is + // active for now. + Some(grouping) if std::ptr::eq(grouping.rule, &PAR) => {} + + // Start a new paragraph based on this textual group. + None => s.groupings.push(Grouping { rule: &PAR, start }), + + // If a non-paragraph grouping is top-level, revisit the grouped + // content with the `TEXTUAL` rule disabled. + _ => { + let elems = s.store_slice(&s.sink[start..]); + let rules = s.rules; + s.sink.truncate(start); + s.rules = &s.rules[1..]; + for &(content, styles) in &elems { + visit(s, content, styles)?; + } + s.rules = rules; + } + } + + Ok(()) +} + +/// Builds the `ParElem` from inline-level elements. +fn finish_par(mut grouped: Grouped) -> SourceResult<()> { + // Collapse unsupported spaces in-place. + let (sink, start) = grouped.get_mut(); + collapse_spaces(sink, start); + + // Collect the children. + let elems = grouped.get(); + let span = select_span(elems); + let (children, trunk) = StyleVec::create(elems); + + // Create and visit the paragraph. + let s = grouped.end(); + let elem = ParElem::new(children).pack().spanned(span); + visit(s, s.store(elem), trunk) +} + +/// Builds the `CiteGroup` from `CiteElem`s. +fn finish_cites(grouped: Grouped) -> SourceResult<()> { + // Collect the children. + let elems = grouped.get(); + let span = select_span(elems); + let trunk = elems[0].1; + let children = elems + .iter() + .filter_map(|(c, _)| c.to_packed::<CiteElem>()) + .cloned() + .collect(); + + // Create and visit the citation group. + let s = grouped.end(); + let elem = CiteGroup::new(children).pack().spanned(span); + visit(s, s.store(elem), trunk) +} + +/// Builds the `ListLike` element from `ListItemLike` elements. +fn finish_list_like<T: ListLike>(grouped: Grouped) -> SourceResult<()> { + // Collect the children. + let elems = grouped.get(); + let span = select_span(elems); + let tight = !elems.iter().any(|(c, _)| c.is::<ParbreakElem>()); + let styles = elems.iter().filter(|(c, _)| c.is::<T::Item>()).map(|&(_, s)| s); + let trunk = StyleChain::trunk(styles).unwrap(); + let trunk_depth = trunk.links().count(); + let children = elems + .iter() + .copied() + .filter_map(|(c, s)| { + let item = c.to_packed::<T::Item>()?.clone(); + let local = s.suffix(trunk_depth); + Some(T::Item::styled(item, local)) + }) + .collect(); + + // Create and visit the list. + let s = grouped.end(); + let elem = T::create(children, tight).pack().spanned(span); + visit(s, s.store(elem), trunk) +} + +/// Visit textual elements in `s.sink[start..]` and apply regex show rules to +/// them. +fn visit_textual(s: &mut State, start: usize) -> SourceResult<bool> { + // Try to find a regex match in the grouped textual elements. + if let Some(m) = find_regex_match_in_elems(s, &s.sink[start..]) { + collapse_spaces(&mut s.sink, start); + let elems = s.store_slice(&s.sink[start..]); + s.sink.truncate(start); + visit_regex_match(s, &elems, m)?; + return Ok(true); + } + + Ok(false) +} + +/// Finds the leftmost regex match for this style chain in the given textual +/// elements. +/// +/// Collects the element's merged textual representation into the bump arena. +/// This merging also takes into account space collapsing so that we don't need +/// to call `collapse_spaces` on every textual group, performing yet another +/// linear pass. We only collapse the spaces elements themselves on the cold +/// path where there is an actual match. +fn find_regex_match_in_elems<'a>( + s: &State, + elems: &[Pair<'a>], +) -> Option<RegexMatch<'a>> { + let mut buf = BumpString::new_in(&s.arenas.bump); + let mut base = 0; + let mut leftmost = None; + let mut current = StyleChain::default(); + let mut space = SpaceState::Destructive; + + for &(content, styles) in elems { + if content.is::<TagElem>() { + continue; + } + + let linebreak = content.is::<LinebreakElem>(); + if linebreak { + if let SpaceState::Space(_) = space { + buf.pop(); + } + } + + if styles != current && !buf.is_empty() { + leftmost = find_regex_match_in_str(&buf, current); + if leftmost.is_some() { + break; + } + base += buf.len(); + buf.clear(); + } + + current = styles; + space = if content.is::<SpaceElem>() { + if space != SpaceState::Supportive { + continue; + } + buf.push(' '); + SpaceState::Space(0) + } else if linebreak { + buf.push('\n'); + SpaceState::Destructive + } else if let Some(elem) = content.to_packed::<SmartQuoteElem>() { + buf.push(if elem.double(styles) { '"' } else { '\'' }); + SpaceState::Supportive + } else if let Some(elem) = content.to_packed::<TextElem>() { + buf.push_str(&elem.text); + SpaceState::Supportive + } else { + panic!("tried to find regex match in non-textual elements"); + }; + } + + if leftmost.is_none() { + leftmost = find_regex_match_in_str(&buf, current); + } + + leftmost.map(|m| RegexMatch { offset: base + m.offset, ..m }) +} + +/// Finds the leftmost regex match for this style chain in the given text. +fn find_regex_match_in_str<'a>( + text: &str, + styles: StyleChain<'a>, +) -> Option<RegexMatch<'a>> { + let mut r = 0; + let mut revoked = SmallBitSet::new(); + let mut leftmost: Option<(regex::Match, RecipeIndex, &Recipe)> = None; + + let depth = Lazy::new(|| styles.recipes().count()); + + for entry in styles.entries() { + let recipe = match &**entry { + Style::Recipe(recipe) => recipe, + Style::Property(_) => continue, + Style::Revocation(index) => { + revoked.insert(index.0); + continue; + } + }; + r += 1; + + let Some(Selector::Regex(regex)) = recipe.selector() else { continue }; + let Some(m) = regex.find(text) else { continue }; + + // Make sure we don't get any empty matches. + if m.range().is_empty() { + continue; + } + + // If we already have a match that is equally or more to the left, we're + // not interested in this new match. + if leftmost.is_some_and(|(p, ..)| p.start() <= m.start()) { + continue; + } + + // Check whether the rule is already revoked. Do it only now to not + // compute the depth unnecessarily. We subtract 1 from r because we + // already incremented it. + let index = RecipeIndex(*depth - (r - 1)); + if revoked.contains(index.0) { + continue; + } + + leftmost = Some((m, index, recipe)); + } + + leftmost.map(|(m, id, recipe)| RegexMatch { + offset: m.start(), + text: m.as_str().into(), + id, + recipe, + styles, + }) +} + +/// Visit a match of a regular expression. +/// +/// This first revisits all elements before the match, potentially slicing up +/// a text element, then the transformed match, and then the remaining elements +/// after the match. +fn visit_regex_match<'a>( + s: &mut State<'a, '_, '_, '_>, + elems: &[Pair<'a>], + m: RegexMatch<'a>, +) -> SourceResult<()> { + let matched = m.offset..m.offset + m.text.len(); + let piece = TextElem::packed(m.text); + let context = Context::new(None, Some(m.styles)); + let output = m.recipe.apply(s.engine, context.track(), piece)?; + + let mut output = Some(output); + let mut cursor = 0; + + for &(content, styles) in elems { + // Just forward tags. + if content.is::<TagElem>() { + visit(s, content, styles)?; + continue; + } + + // Determine the range of the element. + let len = content.to_packed::<TextElem>().map_or(1, |elem| elem.text.len()); + let subrange = cursor..cursor + len; + + // If the element starts before the start of match, visit it fully or + // sliced. + if subrange.start < matched.start { + if subrange.end <= matched.start { + visit(s, content, styles)?; + } else { + let mut elem = content.to_packed::<TextElem>().unwrap().clone(); + elem.text = elem.text[..matched.start - subrange.start].into(); + visit(s, s.store(elem.pack()), styles)?; + } + } + + // When the match starts at or before this element ends, visit the + // match. + if matched.start <= subrange.end { + if let Some(output) = output.take() { + let revocation = Style::Revocation(m.id).into(); + let outer = s.arenas.bump.alloc(m.styles); + let chained = outer.chain(s.arenas.styles.alloc(revocation)); + visit(s, s.store(output), chained)?; + } + } + + // If the element ends after the end of the match, visit if fully or + // sliced. + if subrange.end > matched.end { + if subrange.start >= matched.end { + visit(s, content, styles)?; + } else { + let mut elem = content.to_packed::<TextElem>().unwrap().clone(); + elem.text = elem.text[matched.end - subrange.start..].into(); + visit(s, s.store(elem.pack()), styles)?; + } + } + + cursor = subrange.end; + } + + Ok(()) +} + +/// Collapses all spaces within `buf[start..]` that are at the edges or in the +/// vicinity of destructive elements. +fn collapse_spaces(buf: &mut Vec<Pair>, start: usize) { + let mut state = SpaceState::Destructive; + let mut k = start; + + // We do one pass over the elements, backshifting everything as necessary + // when a space collapses. The variable `i` is our cursor in the original + // elements. The variable `k` is our cursor in the result. At all times, we + // have `k <= i`, so we can do it in place. + for i in start..buf.len() { + let (content, styles) = buf[i]; + + // Determine the next state. + if content.is::<TagElem>() { + // Nothing to do. + } else if content.is::<SpaceElem>() { + if state != SpaceState::Supportive { + continue; + } + state = SpaceState::Space(k); + } else if content.is::<LinebreakElem>() { + destruct_space(buf, &mut k, &mut state); + } else if let Some(elem) = content.to_packed::<HElem>() { + if elem.amount.is_fractional() || elem.weak(styles) { + destruct_space(buf, &mut k, &mut state); + } + } else { + state = SpaceState::Supportive; + }; + + // Copy over normal elements (in place). + if k < i { + buf[k] = buf[i]; + } + k += 1; + } + + destruct_space(buf, &mut k, &mut state); + + // Delete all the excess that's left due to the gaps produced by spaces. + buf.truncate(k); +} + +/// Deletes a preceding space if any. +fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) { + if let SpaceState::Space(s) = *state { + buf.copy_within(s + 1..*end, s); + *end -= 1; + } + *state = SpaceState::Destructive; +} + +/// Finds the first non-detached span in the list. +fn select_span(children: &[Pair]) -> Span { + children + .iter() + .map(|(c, _)| c.span()) + .find(|span| !span.is_detached()) + .unwrap_or(Span::detached()) +} diff --git a/crates/typst/src/realize/arenas.rs b/crates/typst/src/realize/arenas.rs deleted file mode 100644 index 61b19858..00000000 --- a/crates/typst/src/realize/arenas.rs +++ /dev/null @@ -1,41 +0,0 @@ -use typed_arena::Arena; - -use crate::foundations::{Content, StyleChain, Styles}; - -/// Temporary storage arenas for building. -#[derive(Default)] -pub struct Arenas<'a> { - chains: Arena<StyleChain<'a>>, - content: Arena<Content>, - styles: Arena<Styles>, -} - -impl<'a> Arenas<'a> { - /// Store a value in the matching arena. - pub fn store<T: Store<'a>>(&'a self, val: T) -> &'a T { - val.store(self) - } -} - -/// Implemented by storable types. -pub trait Store<'a> { - fn store(self, arenas: &'a Arenas<'a>) -> &'a Self; -} - -impl<'a> Store<'a> for Content { - fn store(self, arenas: &'a Arenas<'a>) -> &'a Self { - arenas.content.alloc(self) - } -} - -impl<'a> Store<'a> for StyleChain<'a> { - fn store(self, arenas: &'a Arenas<'a>) -> &'a Self { - arenas.chains.alloc(self) - } -} - -impl<'a> Store<'a> for Styles { - fn store(self, arenas: &'a Arenas<'a>) -> &'a Self { - arenas.styles.alloc(self) - } -} diff --git a/crates/typst/src/realize/behaviour.rs b/crates/typst/src/realize/behaviour.rs deleted file mode 100644 index 15a0a284..00000000 --- a/crates/typst/src/realize/behaviour.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Element interaction. - -use crate::foundations::{Content, StyleChain}; - -/// How an element interacts with other elements in a stream. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Behaviour { - /// A weak element which only survives when a supportive element is before - /// and after it. Furthermore, per consecutive run of weak elements, only - /// one survives: The one with the lowest weakness level (or the larger one - /// if there is a tie). - Weak(usize), - /// An element that enables adjacent weak elements to exist. The default. - Supportive, - /// An element that destroys adjacent weak elements. - Destructive, - /// An element that does not interact at all with other elements, having the - /// same effect on them as if it didn't exist. - Ignorant, -} - -impl Behaviour { - /// Whether this of `Weak(_)` variant. - pub fn is_weak(self) -> bool { - matches!(self, Self::Weak(_)) - } -} - -/// How the element interacts with other elements. -pub trait Behave { - /// The element's interaction behaviour. - fn behaviour(&self) -> Behaviour; - - /// Whether this weak element is larger than a previous one and thus picked - /// as the maximum when the levels are the same. - #[allow(unused_variables)] - fn larger(&self, prev: &(&Content, StyleChain), styles: StyleChain) -> bool { - false - } -} - -/// Processes a sequence of content and resolves behaviour interactions between -/// them and separates local styles for each element from the shared trunk of -/// styles. -#[derive(Debug)] -pub struct BehavedBuilder<'a> { - /// The collected content with its styles. - buf: Vec<(&'a Content, StyleChain<'a>)>, - /// What the last non-ignorant, visible item was. - last: Behaviour, -} - -impl<'a> BehavedBuilder<'a> { - /// Create a new style-vec builder. - pub fn new() -> Self { - Self { buf: vec![], last: Behaviour::Destructive } - } - - /// Whether the builder is totally empty. - pub fn is_empty(&self) -> bool { - self.buf.is_empty() - } - - /// Push an item into the builder. - pub fn push(&mut self, content: &'a Content, styles: StyleChain<'a>) { - let mut behaviour = content.behaviour(); - match behaviour { - Behaviour::Supportive => {} - Behaviour::Weak(level) => match self.last { - // Remove either this or the preceding weak item. - Behaviour::Weak(prev_level) => { - if level > prev_level { - return; - } - - let i = self.find_last_weak().unwrap(); - if level == prev_level - && !content - .with::<dyn Behave>() - .unwrap() - .larger(&self.buf[i], styles) - { - return; - } - - self.buf.remove(i); - } - Behaviour::Destructive => return, - _ => {} - }, - Behaviour::Destructive => { - // Remove preceding weak item. - if self.last.is_weak() { - let i = self.find_last_weak().unwrap(); - self.buf.remove(i); - } - } - Behaviour::Ignorant => { - behaviour = self.last; - } - } - - self.last = behaviour; - self.buf.push((content, styles)); - } - - /// Iterate over the content that has been pushed so far. - pub fn items(&self) -> impl Iterator<Item = &'a Content> + '_ { - self.buf.iter().map(|&(c, _)| c) - } - - /// Return the built content (possibly styled with local styles) plus a - /// trunk style chain and a span for the collection. - pub fn finish(mut self) -> Vec<(&'a Content, StyleChain<'a>)> { - self.trim_weak(); - self.buf - } - - /// Trim a possibly remaining weak item. - fn trim_weak(&mut self) { - if self.last.is_weak() { - let i = self.find_last_weak().unwrap(); - self.buf.remove(i); - } - } - - /// Get the position of the right most weak item. - fn find_last_weak(&self) -> Option<usize> { - self.buf.iter().rposition(|(c, _)| c.behaviour().is_weak()) - } -} - -impl<'a> Default for BehavedBuilder<'a> { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs deleted file mode 100644 index 29dd1e40..00000000 --- a/crates/typst/src/realize/mod.rs +++ /dev/null @@ -1,567 +0,0 @@ -//! Realization of content. -//! -//! *Realization* is the process of recursively applying styling and, in -//! particular, show rules to produce well-known elements that can be laid out. - -mod arenas; -mod behaviour; -mod process; - -use once_cell::unsync::Lazy; - -pub use self::arenas::Arenas; -pub use self::behaviour::{Behave, BehavedBuilder, Behaviour}; -pub use self::process::process; - -use std::mem; - -use crate::diag::{bail, At, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - Content, ContextElem, NativeElement, Packed, SequenceElem, Smart, StyleChain, - StyleVec, StyledElem, Styles, -}; -use crate::introspection::{SplitLocator, TagElem, TagKind}; -use crate::layout::{ - AlignElem, BlockElem, BoxElem, ColbreakElem, FlushElem, HElem, InlineElem, PageElem, - PagebreakElem, PlaceElem, VElem, -}; -use crate::math::{EquationElem, LayoutMath}; -use crate::model::{ - CiteElem, CiteGroup, DocumentElem, DocumentInfo, EnumElem, EnumItem, ListElem, - ListItem, ParElem, ParbreakElem, TermItem, TermsElem, -}; -use crate::syntax::Span; -use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; -use crate::utils::SliceExt; - -/// A pair of content and a style chain that applies to it. -pub type Pair<'a> = (&'a Content, StyleChain<'a>); - -/// Realize content into a flat list of well-known, styled items. -#[typst_macros::time(name = "realize")] -pub fn realize<'a>( - engine: &mut Engine<'a>, - locator: &mut SplitLocator<'a>, - arenas: &'a Arenas<'a>, - doc_info: Option<&mut DocumentInfo>, - content: &'a Content, - styles: StyleChain<'a>, -) -> SourceResult<Vec<Pair<'a>>> { - let mut builder = Builder::new(engine, locator, arenas, doc_info); - builder.accept(content, styles)?; - builder.interrupt_par()?; - Ok(builder.sink.finish()) -} - -/// Realizes content into a flat list of well-known, styled elements. -struct Builder<'a, 'v> { - /// The engine. - engine: &'v mut Engine<'a>, - /// Assigns unique locations to elements. - locator: &'v mut SplitLocator<'a>, - /// Scratch arenas for building. - arenas: &'a Arenas<'a>, - - /// Document metadata we have collected from `set document` rules. If this - /// is `None`, we are in a container. - doc_info: Option<&'v mut DocumentInfo>, - /// The output elements of well-known types collected by the builder. - sink: BehavedBuilder<'a>, - - /// A builder for a paragraph that might be under construction. - par: ParBuilder<'a>, - /// A builder for a list that might be under construction. - list: ListBuilder<'a>, - /// A builder for a citation group that might be under construction. - cites: CiteGroupBuilder<'a>, - - /// Whether we are currently not within any container or show rule output. - /// This is used to determine page styles during layout. - outside: bool, - /// Whether the last item that we visited was a paragraph (with no parbreak - /// in between). This is used for attach spacing. - last_was_par: bool, -} - -impl<'a, 'v> Builder<'a, 'v> { - /// Creates a new builder. - fn new( - engine: &'v mut Engine<'a>, - locator: &'v mut SplitLocator<'a>, - arenas: &'a Arenas<'a>, - doc_info: Option<&'v mut DocumentInfo>, - ) -> Self { - let outside = doc_info.is_some(); - Self { - engine, - locator, - arenas, - doc_info, - sink: BehavedBuilder::default(), - par: ParBuilder::default(), - list: ListBuilder::default(), - cites: CiteGroupBuilder::default(), - outside, - last_was_par: false, - } - } - - /// Adds a piece of content to this builder. - fn accept( - &mut self, - mut content: &'a Content, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - // Implicitly wrap math content in an equation if needed - if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() { - content = self - .arenas - .store(EquationElem::new(content.clone()).pack().spanned(content.span())); - } - - // Styled elements and sequences can (at least currently) also have - // labels, so this needs to happen before they are handled. - if let Some((tag, realized)) = - process(self.engine, self.locator, content, styles)? - { - self.engine.route.increase(); - self.engine.route.check_show_depth().at(content.span())?; - - if let Some(tag) = &tag { - self.accept(self.arenas.store(TagElem::packed(tag.clone())), styles)?; - } - - let prev_outside = self.outside; - self.outside &= content.is::<ContextElem>(); - self.accept(self.arenas.store(realized), styles)?; - self.outside = prev_outside; - - if let Some(tag) = tag { - let end = tag.with_kind(TagKind::End); - self.accept(self.arenas.store(TagElem::packed(end)), styles)?; - } - - self.engine.route.decrease(); - return Ok(()); - } - - if let Some(styled) = content.to_packed::<StyledElem>() { - return self.styled(styled, styles); - } - - if let Some(sequence) = content.to_packed::<SequenceElem>() { - for elem in &sequence.children { - self.accept(elem, styles)?; - } - return Ok(()); - } - - // Try to merge `content` with an element under construction - // (cite group, list, or par). - - if self.cites.accept(content, styles) { - return Ok(()); - } - - self.interrupt_cites()?; - - if self.list.accept(content, styles) { - return Ok(()); - } - - self.interrupt_list()?; - - // Try again because it could be another kind of list. - if self.list.accept(content, styles) { - return Ok(()); - } - - if self.par.accept(content, styles) { - return Ok(()); - } - - self.interrupt_par()?; - - self.save(content, styles) - } - - /// Tries to save a piece of content into the sink. - fn save(&mut self, content: &'a Content, styles: StyleChain<'a>) -> SourceResult<()> { - let last_was_par = std::mem::replace(&mut self.last_was_par, false); - let par_spacing = Lazy::new(|| { - self.arenas - .store(VElem::par_spacing(ParElem::spacing_in(styles).into()).pack()) - }); - - if content.is::<TagElem>() - || content.is::<PlaceElem>() - || content.is::<FlushElem>() - || content.is::<ColbreakElem>() - { - self.sink.push(content, styles); - } else if content.is::<PagebreakElem>() { - if self.doc_info.is_none() { - bail!(content.span(), "pagebreaks are not allowed inside of containers"); - } - self.sink.push(content, styles); - } else if let Some(elem) = content.to_packed::<VElem>() { - if !elem.attach(styles) || last_was_par { - self.sink.push(content, styles); - } - } else if content.is::<ParbreakElem>() { - // It's only a boundary, so we can ignore it. - } else if content.is::<ParElem>() { - self.sink.push(*par_spacing, styles); - self.sink.push(content, styles); - self.sink.push(*par_spacing, styles); - self.last_was_par = true; - } else if let Some(elem) = content.to_packed::<BlockElem>() { - let above = match elem.above(styles) { - Smart::Auto => *par_spacing, - Smart::Custom(above) => { - self.arenas.store(VElem::block_spacing(above).pack()) - } - }; - - let below = match elem.below(styles) { - Smart::Auto => *par_spacing, - Smart::Custom(below) => { - self.arenas.store(VElem::block_spacing(below).pack()) - } - }; - - self.sink.push(above, styles); - self.sink.push(content, styles); - self.sink.push(below, styles); - } else { - bail!(content.span(), "{} is not allowed here", content.func().name()); - } - - Ok(()) - } - - /// Handles a styled element. - fn styled( - &mut self, - styled: &'a StyledElem, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - if let Some(span) = styled.styles.interruption::<DocumentElem>() { - let Some(info) = &mut self.doc_info else { - bail!(span, "document set rules are not allowed inside of containers"); - }; - info.populate(&styled.styles); - } - - let page_interruption = styled.styles.interruption::<PageElem>(); - if let Some(span) = page_interruption { - if self.doc_info.is_none() { - bail!(span, "page configuration is not allowed inside of containers"); - } - - // When there are page styles, we "break free" from our show rule - // cage. - self.outside = true; - } - - // If we are not within a container or show rule, mark the styles as - // "outside". This will allow them to be lifted to the page level. - let outer = self.arenas.store(styles); - let local = if self.outside { - self.arenas.store(styled.styles.clone().outside()) - } else { - &styled.styles - }; - - if page_interruption.is_some() { - // For the starting pagebreak we only want the styles before and - // including the interruptions, not trailing styles that happen to - // be in the same `Styles` list. - let relevant = local - .as_slice() - .trim_end_matches(|style| style.interruption::<PageElem>().is_none()); - self.accept(PagebreakElem::shared_weak(), outer.chain(relevant))?; - } - - self.interrupt_styles(local)?; - self.accept(&styled.child, outer.chain(local))?; - self.interrupt_styles(local)?; - - if page_interruption.is_some() { - // For the ending pagebreak, the styles don't really matter because - // the styles of a "boundary" pagebreak are ignored during layout. - self.accept(PagebreakElem::shared_boundary(), *outer)?; - } - - Ok(()) - } - - /// Inspects the styles and dispatches to the different interruption - /// handlers. - fn interrupt_styles(&mut self, local: &Styles) -> SourceResult<()> { - if local.interruption::<ParElem>().is_some() - || local.interruption::<AlignElem>().is_some() - { - self.interrupt_par()?; - } else if local.interruption::<ListElem>().is_some() - || local.interruption::<EnumElem>().is_some() - || local.interruption::<TermsElem>().is_some() - { - self.interrupt_list()?; - } else if local.interruption::<CiteElem>().is_some() { - self.interrupt_cites()?; - } - Ok(()) - } - - /// Interrupts paragraph building and adds the resulting paragraph element - /// to the builder. - fn interrupt_par(&mut self) -> SourceResult<()> { - self.interrupt_list()?; - if !self.par.0.is_empty() { - mem::take(&mut self.par).finish(self)?; - } - Ok(()) - } - - /// Interrupts list building and adds the resulting list element to the - /// builder. - fn interrupt_list(&mut self) -> SourceResult<()> { - self.interrupt_cites()?; - if !self.list.0.is_empty() { - mem::take(&mut self.list).finish(self)?; - } - Ok(()) - } - - /// Interrupts citation grouping and adds the resulting citation group to - /// the builder. - fn interrupt_cites(&mut self) -> SourceResult<()> { - if !self.cites.0.is_empty() { - mem::take(&mut self.cites).finish(self)?; - } - Ok(()) - } -} - -/// Builds a [paragraph][ParElem] from paragraph content. -#[derive(Default)] -struct ParBuilder<'a>(BehavedBuilder<'a>); - -impl<'a> ParBuilder<'a> { - /// Tries to accept a piece of content. - /// - /// Returns true if this content could be merged into the paragraph. If this - /// function returns false, then the content could not be merged, and - /// paragraph building should be interrupted so that the content can be - /// added elsewhere. - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if Self::is_primary(content) || (!self.0.is_empty() && Self::is_inner(content)) { - self.0.push(content, styles); - return true; - } - - false - } - - /// Whether this content is of interest to the builder. - fn is_primary(content: &'a Content) -> bool { - content.is::<SpaceElem>() - || content.is::<TextElem>() - || content.is::<HElem>() - || content.is::<LinebreakElem>() - || content.is::<SmartQuoteElem>() - || content.is::<InlineElem>() - || content.is::<BoxElem>() - } - - /// Whether this content can merely exist in between interesting items. - fn is_inner(content: &'a Content) -> bool { - content.is::<TagElem>() - } - - /// Turns this builder into the resulting list, along with - /// its [style chain][StyleChain]. - fn finish(self, builder: &mut Builder<'a, '_>) -> SourceResult<()> { - let buf = self.0.finish(); - let trimmed = buf.trim_end_matches(|(c, _)| c.is::<TagElem>()); - let staged = &buf[trimmed.len()..]; - - let span = first_span(trimmed); - let (children, trunk) = StyleVec::create(trimmed); - let elem = Packed::new(ParElem::new(children)).spanned(span); - builder.accept(builder.arenas.store(elem.pack()), trunk)?; - - for &(tag, styles) in staged { - builder.accept(tag, styles)?; - } - - Ok(()) - } -} - -/// Builds a list (either [`ListElem`], [`EnumElem`], or [`TermsElem`]) from -/// list or enum items, spaces, and paragraph breaks. -#[derive(Default)] -struct ListBuilder<'a>(Vec<Pair<'a>>); - -impl<'a> ListBuilder<'a> { - /// Tries to accept a piece of content. - /// - /// Returns true if this content could be merged into the list. If this - /// function returns false, then the content could not be merged, and list - /// building should be interrupted so that the content can be added - /// elsewhere. - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if (Self::is_primary(content) && self.is_compatible(content)) - || (!self.0.is_empty() && Self::is_inner(content)) - { - self.0.push((content, styles)); - return true; - } - - false - } - - /// Whether this content is of interest to the builder. - fn is_primary(content: &'a Content) -> bool { - content.is::<ListItem>() || content.is::<EnumItem>() || content.is::<TermItem>() - } - - /// Whether this content can merely exist in between interesting items. - fn is_inner(content: &'a Content) -> bool { - content.is::<TagElem>() - || content.is::<SpaceElem>() - || content.is::<ParbreakElem>() - } - - /// Whether this kind of list items is compatible with the builder's type. - fn is_compatible(&self, content: &'a Content) -> bool { - self.0 - .first() - .map_or(true, |(first, _)| first.func() == content.func()) - } - - /// Turns this builder into the resulting list, along with - /// its [style chain][StyleChain]. - fn finish(self, builder: &mut Builder<'a, '_>) -> SourceResult<()> { - let trimmed = self.0.trim_end_matches(|(c, _)| Self::is_inner(c)); - let tags = trimmed.iter().filter(|(c, _)| c.is::<TagElem>()); - let staged = &self.0[trimmed.len()..]; - let items = trimmed.iter().copied().filter(|(c, _)| Self::is_primary(c)); - let first = items.clone().next().unwrap().0; - let tight = !trimmed.iter().any(|(c, _)| c.is::<ParbreakElem>()); - - // Determine the styles that are shared by all items. These will be - // used for the list itself. - let trunk = StyleChain::trunk(items.clone().map(|(_, s)| s)).unwrap(); - let depth = trunk.links().count(); - - // Builder the correct element. - let iter = items.map(|(c, s)| (c, s.suffix(depth))); - let elem = if first.is::<ListItem>() { - let children = iter - .map(|(item, local)| { - item.to_packed::<ListItem>().unwrap().clone().styled(local) - }) - .collect(); - ListElem::new(children).with_tight(tight).pack() - } else if first.is::<EnumItem>() { - let children = iter - .map(|(item, local)| { - item.to_packed::<EnumItem>().unwrap().clone().styled(local) - }) - .collect(); - EnumElem::new(children).with_tight(tight).pack() - } else if first.is::<TermItem>() { - let children = iter - .map(|(item, local)| { - item.to_packed::<TermItem>().unwrap().clone().styled(local) - }) - .collect(); - TermsElem::new(children).with_tight(tight).pack() - } else { - unreachable!() - }; - - // Add the list to the builder. - let span = first_span(&self.0); - let stored = builder.arenas.store(elem.spanned(span)); - builder.accept(stored, trunk)?; - - // Add the tags and staged elements to the builder. - for &(content, styles) in tags.chain(staged) { - builder.accept(content, styles)?; - } - - Ok(()) - } -} - -/// Builds a [citation group][CiteGroup] from citations. -#[derive(Default)] -struct CiteGroupBuilder<'a>(Vec<Pair<'a>>); - -impl<'a> CiteGroupBuilder<'a> { - /// Tries to accept a piece of content. - /// - /// Returns true if this content could be merged into the citation group. If - /// this function returns false, then the content could not be merged, and - /// citation grouping should be interrupted so that the content can be added - /// elsewhere. - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if Self::is_primary(content) || (!self.0.is_empty() && Self::is_inner(content)) { - self.0.push((content, styles)); - return true; - } - - false - } - - /// Whether this content is of interest to the builder. - fn is_primary(content: &'a Content) -> bool { - content.is::<CiteElem>() - } - - /// Whether this content can merely exist in between interesting items. - fn is_inner(content: &'a Content) -> bool { - content.is::<TagElem>() || content.is::<SpaceElem>() - } - - /// Turns this builder into the resulting citation group, along with - /// its [style chain][StyleChain]. - fn finish(self, builder: &mut Builder<'a, '_>) -> SourceResult<()> { - let trimmed = self.0.trim_end_matches(|(c, _)| Self::is_inner(c)); - let tags = trimmed.iter().filter(|(c, _)| c.is::<TagElem>()); - let staged = &self.0[trimmed.len()..]; - let trunk = trimmed[0].1; - let children = trimmed - .iter() - .filter_map(|(c, _)| c.to_packed::<CiteElem>()) - .cloned() - .collect(); - - // Add the citation group to the builder. - let span = first_span(&self.0); - let elem = CiteGroup::new(children).pack(); - let stored = builder.arenas.store(elem.spanned(span)); - builder.accept(stored, trunk)?; - - // Add the tags and staged elements to the builder. - for &(content, styles) in tags.chain(staged) { - builder.accept(content, styles)?; - } - - Ok(()) - } -} - -/// Determine a span for the built collection. -pub fn first_span(children: &[(&Content, StyleChain)]) -> Span { - children - .iter() - .map(|(c, _)| c.span()) - .find(|span| !span.is_detached()) - .unwrap_or(Span::detached()) -} diff --git a/crates/typst/src/realize/process.rs b/crates/typst/src/realize/process.rs deleted file mode 100644 index 83c2dd4d..00000000 --- a/crates/typst/src/realize/process.rs +++ /dev/null @@ -1,312 +0,0 @@ -use std::cell::OnceCell; - -use comemo::{Track, Tracked}; - -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - Content, Context, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style, - StyleChain, Styles, Synthesize, Transformation, -}; -use crate::introspection::{Locatable, SplitLocator, Tag}; -use crate::text::TextElem; -use crate::utils::SmallBitSet; - -/// What to do with an element when encountering it during realization. -struct Verdict<'a> { - /// Whether the element is already prepared (i.e. things that should only - /// happen once have happened). - prepared: bool, - /// A map of styles to apply to the element. - map: Styles, - /// An optional show rule transformation to apply to the element. - step: Option<ShowStep<'a>>, -} - -/// An optional show rule transformation to apply to the element. -enum ShowStep<'a> { - /// A user-defined transformational show rule. - Recipe(&'a Recipe, RecipeIndex), - /// The built-in show rule. - Builtin, -} - -/// Processes the given `target` element when encountering it during realization. -pub fn process( - engine: &mut Engine, - locator: &mut SplitLocator, - target: &Content, - styles: StyleChain, -) -> SourceResult<Option<(Option<Tag>, Content)>> { - let Some(Verdict { prepared, mut map, step }) = verdict(engine, target, styles) - else { - return Ok(None); - }; - - // Create a fresh copy that we can mutate. - let mut target = target.clone(); - - // If the element isn't yet prepared (we're seeing it for the first time), - // prepare it. - let mut tag = None; - if !prepared { - tag = prepare(engine, locator, &mut target, &mut map, styles)?; - } - - // Apply a step, if there is one. - let output = match step { - Some(step) => { - // Errors in show rules don't terminate compilation immediately. We - // just continue with empty content for them and show all errors - // together, if they remain by the end of the introspection loop. - // - // This way, we can ignore errors that only occur in earlier - // iterations and also show more useful errors at once. - engine.delay(|engine| show(engine, target, step, styles.chain(&map))) - } - None => target, - }; - - Ok(Some((tag, output.styled_with_map(map)))) -} - -/// Inspects a target element and the current styles and determines how to -/// proceed with the styling. -fn verdict<'a>( - engine: &mut Engine, - target: &'a Content, - styles: StyleChain<'a>, -) -> Option<Verdict<'a>> { - let mut target = target; - let mut map = Styles::new(); - let mut revoked = SmallBitSet::new(); - let mut step = None; - let mut slot; - - let depth = OnceCell::new(); - let prepared = target.is_prepared(); - - // Do pre-synthesis on a cloned element to be able to match on synthesized - // fields before real synthesis runs (during preparation). It's really - // unfortunate that we have to do this, but otherwise - // `show figure.where(kind: table)` won't work :( - if !prepared && target.can::<dyn Synthesize>() { - slot = target.clone(); - slot.with_mut::<dyn Synthesize>() - .unwrap() - .synthesize(engine, styles) - .ok(); - target = &slot; - } - - let mut r = 0; - for entry in styles.entries() { - let recipe = match &**entry { - Style::Recipe(recipe) => recipe, - Style::Property(_) => continue, - Style::Revocation(index) => { - revoked.insert(index.0); - continue; - } - }; - - // We're not interested in recipes that don't match. - if !recipe.applicable(target, styles) { - r += 1; - continue; - } - - // Special handling for show-set rules. Exception: Regex show rules, - // those need to be handled like normal transformations. - if let (Transformation::Style(transform), false) = - (recipe.transform(), matches!(recipe.selector(), Some(Selector::Regex(_)))) - { - // If this is a show-set for an unprepared element, we need to apply - // it. - if !prepared { - map.apply(transform.clone()); - } - } else if step.is_none() { - // Lazily compute the total number of recipes in the style chain. We - // need it to determine whether a particular show rule was already - // applied to the `target` previously. For this purpose, show rules - // are indexed from the top of the chain as the chain might grow to - // the bottom. - let depth = *depth.get_or_init(|| { - styles.entries().filter_map(|style| style.recipe()).count() - }); - let index = RecipeIndex(depth - r); - - if !target.is_guarded(index) && !revoked.contains(index.0) { - // If we find a matching, unguarded replacement show rule, - // remember it, but still continue searching for potential - // show-set styles that might change the verdict. - step = Some(ShowStep::Recipe(recipe, index)); - - // If we found a show rule and are already prepared, there is - // nothing else to do, so we can just break. - if prepared { - break; - } - } - } - - r += 1; - } - - // If we found no user-defined rule, also consider the built-in show rule. - if step.is_none() && target.can::<dyn Show>() { - step = Some(ShowStep::Builtin); - } - - // If there's no nothing to do, there is also no verdict. - if step.is_none() - && map.is_empty() - && (prepared || { - target.label().is_none() - && target.location().is_none() - && !target.can::<dyn ShowSet>() - && !target.can::<dyn Locatable>() - && !target.can::<dyn Synthesize>() - }) - { - return None; - } - - Some(Verdict { prepared, map, step }) -} - -/// This is only executed the first time an element is visited. -fn prepare( - engine: &mut Engine, - locator: &mut SplitLocator, - target: &mut Content, - map: &mut Styles, - styles: StyleChain, -) -> SourceResult<Option<Tag>> { - // Generate a location for the element, which uniquely identifies it in - // the document. This has some overhead, so we only do it for elements - // that are explicitly marked as locatable and labelled elements. - // - // The element could already have a location even if it is not prepared - // when it stems from a query. - let mut key = None; - if target.location().is_some() { - key = Some(crate::utils::hash128(&target)); - } else if target.can::<dyn Locatable>() || target.label().is_some() { - let hash = crate::utils::hash128(&target); - let location = locator.next_location(engine.introspector, hash); - target.set_location(location); - key = Some(hash); - } - - // Apply built-in show-set rules. User-defined show-set rules are already - // considered in the map built while determining the verdict. - if let Some(show_settable) = target.with::<dyn ShowSet>() { - map.apply(show_settable.show_set(styles)); - } - - // If necessary, generated "synthesized" fields (which are derived from - // other fields or queries). Do this after show-set so that show-set styles - // are respected. - if let Some(synthesizable) = target.with_mut::<dyn Synthesize>() { - synthesizable.synthesize(engine, styles.chain(map))?; - } - - // Copy style chain fields into the element itself, so that they are - // available in rules. - target.materialize(styles.chain(map)); - - // If the element is locatable, create a tag element to be able to find the - // element in the frames after layout. Do this after synthesis and - // materialization, so that it includes the synthesized fields. Do it before - // marking as prepared so that show-set rules will apply to this element - // when queried. - let tag = key.map(|key| Tag::new(target.clone(), key)); - - // Ensure that this preparation only runs once by marking the element as - // prepared. - target.mark_prepared(); - - Ok(tag) -} - -/// Apply a step. -fn show( - engine: &mut Engine, - target: Content, - step: ShowStep, - styles: StyleChain, -) -> SourceResult<Content> { - match step { - // Apply a user-defined show rule. - ShowStep::Recipe(recipe, guard) => { - let context = Context::new(target.location(), Some(styles)); - match recipe.selector() { - // If the selector is a regex, the `target` is guaranteed to be a - // text element. This invokes special regex handling. - Some(Selector::Regex(regex)) => { - let text = target.into_packed::<TextElem>().unwrap(); - show_regex(engine, &text, regex, recipe, guard, context.track()) - } - - // Just apply the recipe. - _ => recipe.apply(engine, context.track(), target.guarded(guard)), - } - } - - // If the verdict picks this step, the `target` is guaranteed to have a - // built-in show rule. - ShowStep::Builtin => target.with::<dyn Show>().unwrap().show(engine, styles), - } -} - -/// Apply a regex show rule recipe to a target. -fn show_regex( - engine: &mut Engine, - target: &Packed<TextElem>, - regex: &Regex, - recipe: &Recipe, - index: RecipeIndex, - context: Tracked<Context>, -) -> SourceResult<Content> { - let make = |s: &str| { - let mut fresh = target.clone(); - fresh.push_text(s.into()); - fresh.pack() - }; - - let mut result = vec![]; - let mut cursor = 0; - - let text = target.text(); - - for m in regex.find_iter(target.text()) { - let start = m.start(); - if cursor < start { - result.push(make(&text[cursor..start])); - } - - let piece = make(m.as_str()); - let transformed = recipe.apply(engine, context, piece)?; - result.push(transformed); - cursor = m.end(); - } - - if cursor < text.len() { - result.push(make(&text[cursor..])); - } - - // In contrast to normal elements, which are guarded individually, for text - // show rules, we fully revoke the rule. This means that we can replace text - // with other text that rematches without running into infinite recursion - // problems. - // - // We do _not_ do this for all content because revoking e.g. a list show - // rule for all content resulting from that rule would be wrong: The list - // might contain nested lists. Moreover, replacing a normal element with one - // that rematches is bad practice: It can for instance also lead to - // surprising query results, so it's better to let the user deal with it. - // All these problems don't exist for text, so it's fine here. - Ok(Content::sequence(result).styled(Style::Revocation(index))) -} diff --git a/crates/typst/src/text/linebreak.rs b/crates/typst/src/text/linebreak.rs index ea54da98..f32b9223 100644 --- a/crates/typst/src/text/linebreak.rs +++ b/crates/typst/src/text/linebreak.rs @@ -1,5 +1,4 @@ -use crate::foundations::{elem, Content, NativeElement, Packed}; -use crate::realize::{Behave, Behaviour}; +use crate::foundations::{elem, Content, NativeElement}; use crate::utils::singleton; /// Inserts a line break. @@ -19,7 +18,7 @@ use crate::utils::singleton; /// This function also has dedicated syntax: To insert a line break, simply write /// a backslash followed by whitespace. This always creates an unjustified /// break. -#[elem(title = "Line Break", Behave)] +#[elem(title = "Line Break")] pub struct LinebreakElem { /// Whether to justify the line before the break. /// @@ -44,9 +43,3 @@ impl LinebreakElem { singleton!(Content, LinebreakElem::new().pack()) } } - -impl Behave for Packed<LinebreakElem> { - fn behaviour(&self) -> Behaviour { - Behaviour::Destructive - } -} diff --git a/crates/typst/src/text/raw.rs b/crates/typst/src/text/raw.rs index 8132688a..b949dce3 100644 --- a/crates/typst/src/text/raw.rs +++ b/crates/typst/src/text/raw.rs @@ -20,8 +20,7 @@ use crate::layout::{BlockChild, BlockElem, Em, HAlignment}; use crate::model::{Figurable, ParElem}; use crate::syntax::{split_newlines, LinkedNode, Span, Spanned}; use crate::text::{ - FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, SmartQuoteElem, TextElem, - TextSize, + FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, }; use crate::visualize::Color; use crate::{syntax, World}; @@ -468,7 +467,6 @@ impl ShowSet for Packed<RawElem> { out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); - out.set(SmartQuoteElem::set_enabled(false)); if self.block(styles) { out.set(ParElem::set_shrink(false)); } diff --git a/crates/typst/src/text/space.rs b/crates/typst/src/text/space.rs index b2afdc35..2ebc3b33 100644 --- a/crates/typst/src/text/space.rs +++ b/crates/typst/src/text/space.rs @@ -3,11 +3,10 @@ use ecow::EcoString; use crate::foundations::{ elem, Content, NativeElement, Packed, PlainText, Repr, Unlabellable, }; -use crate::realize::{Behave, Behaviour}; use crate::utils::singleton; /// A text space. -#[elem(Behave, Unlabellable, PlainText, Repr)] +#[elem(Unlabellable, PlainText, Repr)] pub struct SpaceElem {} impl SpaceElem { @@ -23,12 +22,6 @@ impl Repr for SpaceElem { } } -impl Behave for Packed<SpaceElem> { - fn behaviour(&self) -> Behaviour { - Behaviour::Weak(2) - } -} - impl Unlabellable for Packed<SpaceElem> {} impl PlainText for Packed<SpaceElem> { diff --git a/tests/ref/flow-first-region-counter-update-placed-and-line.png b/tests/ref/flow-first-region-counter-update-placed-and-line.png Binary files differindex 95ca518e..c5950783 100644 --- a/tests/ref/flow-first-region-counter-update-placed-and-line.png +++ b/tests/ref/flow-first-region-counter-update-placed-and-line.png diff --git a/tests/ref/grid-breaking-expand-vertically.png b/tests/ref/grid-breaking-expand-vertically.png Binary files differindex 14434d7c..2e1b2db2 100644 --- a/tests/ref/grid-breaking-expand-vertically.png +++ b/tests/ref/grid-breaking-expand-vertically.png diff --git a/tests/ref/label-on-text.png b/tests/ref/label-on-text.png Binary files differdeleted file mode 100644 index 67fb1aa8..00000000 --- a/tests/ref/label-on-text.png +++ /dev/null diff --git a/tests/ref/page-marginal-style-show-rule-with-set-page.png b/tests/ref/page-marginal-style-show-rule-with-set-page.png Binary files differindex 138f2272..f05c64a7 100644 --- a/tests/ref/page-marginal-style-show-rule-with-set-page.png +++ b/tests/ref/page-marginal-style-show-rule-with-set-page.png diff --git a/tests/ref/show-set-text-order-contained-1.png b/tests/ref/show-set-text-order-contained-1.png Binary files differindex 8deaaacd..1bc95e3b 100644 --- a/tests/ref/show-set-text-order-contained-1.png +++ b/tests/ref/show-set-text-order-contained-1.png diff --git a/tests/ref/show-set-text-order-contained-2.png b/tests/ref/show-set-text-order-contained-2.png Binary files differindex 00ea3fb8..89309370 100644 --- a/tests/ref/show-set-text-order-contained-2.png +++ b/tests/ref/show-set-text-order-contained-2.png diff --git a/tests/ref/show-set-text-order-overlapping-1.png b/tests/ref/show-set-text-order-overlapping-1.png Binary files differindex 71222567..83983b4d 100644 --- a/tests/ref/show-set-text-order-overlapping-1.png +++ b/tests/ref/show-set-text-order-overlapping-1.png diff --git a/tests/ref/show-set-text-order-overlapping-2.png b/tests/ref/show-set-text-order-overlapping-2.png Binary files differindex f1b658f2..83983b4d 100644 --- a/tests/ref/show-set-text-order-overlapping-2.png +++ b/tests/ref/show-set-text-order-overlapping-2.png diff --git a/tests/ref/show-text-after-normal-show.png b/tests/ref/show-text-after-normal-show.png Binary files differnew file mode 100644 index 00000000..517ad638 --- /dev/null +++ b/tests/ref/show-text-after-normal-show.png diff --git a/tests/ref/show-text-apostrophe.png b/tests/ref/show-text-apostrophe.png Binary files differnew file mode 100644 index 00000000..7c328836 --- /dev/null +++ b/tests/ref/show-text-apostrophe.png diff --git a/tests/ref/show-text-citation-smartquote.png b/tests/ref/show-text-citation-smartquote.png Binary files differnew file mode 100644 index 00000000..d3a2b5c3 --- /dev/null +++ b/tests/ref/show-text-citation-smartquote.png diff --git a/tests/ref/show-text-citation.png b/tests/ref/show-text-citation.png Binary files differnew file mode 100644 index 00000000..9a402dfe --- /dev/null +++ b/tests/ref/show-text-citation.png diff --git a/tests/ref/show-text-linebreak.png b/tests/ref/show-text-linebreak.png Binary files differnew file mode 100644 index 00000000..716eca42 --- /dev/null +++ b/tests/ref/show-text-linebreak.png diff --git a/tests/ref/show-text-list.png b/tests/ref/show-text-list.png Binary files differnew file mode 100644 index 00000000..4d04a8cf --- /dev/null +++ b/tests/ref/show-text-list.png diff --git a/tests/ref/show-text-outer-space.png b/tests/ref/show-text-outer-space.png Binary files differnew file mode 100644 index 00000000..d497af83 --- /dev/null +++ b/tests/ref/show-text-outer-space.png diff --git a/tests/ref/show-text-smartquote.png b/tests/ref/show-text-smartquote.png Binary files differnew file mode 100644 index 00000000..9a93c66c --- /dev/null +++ b/tests/ref/show-text-smartquote.png diff --git a/tests/ref/show-text-space-collapsing.png b/tests/ref/show-text-space-collapsing.png Binary files differnew file mode 100644 index 00000000..5794cefe --- /dev/null +++ b/tests/ref/show-text-space-collapsing.png diff --git a/tests/ref/show-text-style-boundary.png b/tests/ref/show-text-style-boundary.png Binary files differnew file mode 100644 index 00000000..39a440f2 --- /dev/null +++ b/tests/ref/show-text-style-boundary.png diff --git a/tests/ref/show-text-within-par.png b/tests/ref/show-text-within-par.png Binary files differnew file mode 100644 index 00000000..e7bccc49 --- /dev/null +++ b/tests/ref/show-text-within-par.png diff --git a/tests/suite/foundations/label.typ b/tests/suite/foundations/label.typ index 4875f697..af6d2380 100644 --- a/tests/suite/foundations/label.typ +++ b/tests/suite/foundations/label.typ @@ -19,15 +19,6 @@ The end. #let b = [*B*] #a <v> #b ---- label-on-text --- -// Test labelled text. -#show "t": it => { - set text(blue) if it.has("label") and it.label == <last> - it -} - -This is a thing #[that <last>] happened. - --- label-dynamic-show-set --- // Test abusing dynamic labels for styling. #show <red>: set text(red) diff --git a/tests/suite/layout/flow/invisibles.typ b/tests/suite/layout/flow/invisibles.typ index 28118cb9..e9c2436f 100644 --- a/tests/suite/layout/flow/invisibles.typ +++ b/tests/suite/layout/flow/invisibles.typ @@ -56,6 +56,6 @@ Mix-and-match all the previous tests. #counter("dummy").step() #place(dx: -0.5cm, dy: -0.75cm, box(width: 200%)[OOF]) #line(length: 100%) - #place(dy: -0.8em)[OOF] + #place(dy: 0.2em)[OOF] #rect(height: 2cm, fill: gray) ] diff --git a/tests/suite/layout/grid/grid.typ b/tests/suite/layout/grid/grid.typ index 2d45095d..d35adca5 100644 --- a/tests/suite/layout/grid/grid.typ +++ b/tests/suite/layout/grid/grid.typ @@ -184,8 +184,8 @@ Top #align(bottom)[ Bottom \ - Bottom \ - #v(0pt) + Bottom + Top ] ], diff --git a/tests/suite/layout/place.typ b/tests/suite/layout/place.typ index 0922800d..c29d2ddd 100644 --- a/tests/suite/layout/place.typ +++ b/tests/suite/layout/place.typ @@ -54,9 +54,9 @@ Second --- place-float --- #set page(height: 140pt) #set place(clearance: 5pt) -#lorem(6) #place(auto, float: true, rect[A]) #place(auto, float: true, rect[B]) +#lorem(6) #place(auto, float: true, rect[C]) #place(auto, float: true, rect[D]) diff --git a/tests/suite/styling/show-text.typ b/tests/suite/styling/show-text.typ index 56b659b2..d073f72e 100644 --- a/tests/suite/styling/show-text.typ +++ b/tests/suite/styling/show-text.typ @@ -131,3 +131,66 @@ Heya #show "Heya": set text(red) #show "yaho": set text(weight: "bold") Heyaho + +--- show-text-smartquote --- +#show "up,\" she": set text(red) +"What's up," she asked. + +--- show-text-apostrophe --- +#show regex("Who's|We've"): highlight +Who's got it? \ +We've got it. + +--- show-text-citation --- +#show "hey": [@arrgh] +@netwok hey + +#show bibliography: none +#bibliography("/assets/bib/works.bib") + +--- show-text-list --- +#show "hi": [- B] +- A +hi +- C + +--- show-text-citation-smartquote --- +#show "hey \"": [@arrgh] +#show "dis": [@distress] +@netwok hey " dis + +#show bibliography: none +#bibliography("/assets/bib/works.bib") + +--- show-text-linebreak --- +#show "lo\nwo": set text(red) +Hello #[ ] \ +#[ ] #[ ] world! + +--- show-text-after-normal-show --- +#show rect: "world" +#show "lo wo": set text(red) +hello #rect() + +--- show-text-space-collapsing --- +#show "i ther": set text(red) +hi#[ ]#[ ]the#"re" + +--- show-text-style-boundary --- +#show "What's up": set text(blue) +#show "your party": underline +What's #[ ] up at #"your" #text(red)[party?] + +--- show-text-within-par --- +#show "Pythagoras'": highlight +$a^2 + b^2 = c^2$ is Pythagoras' theorem. + +--- show-text-outer-space --- +// Spaces must be interior to strong textual elements for matching to work. +// For outer spaces, it is hard to say whether they would collapse. +#show "a\n": set text(blue) +#show "b\n ": set text(blue) +#show " c ": set text(blue) +a \ #h(0pt, weak: true) +b \ #h(0pt, weak: true) +$x$ c $y$ |
