diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index eab359d6f..81548dd77 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -319,9 +319,9 @@ fn create_struct(element: &Elem) -> TokenStream { /// Create a field declaration for the struct. fn create_field(field: &Field) -> TokenStream { - let Field { ident, ty, .. } = field; + let Field { vis, ident, ty, .. } = field; if field.required { - quote! { #ident: #ty } + quote! { #vis #ident: #ty } } else { quote! { #ident: ::std::option::Option<#ty> } } diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index 8c093764b..86e75eb58 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -14,13 +14,13 @@ use smallvec::smallvec; use crate::diag::{SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - elem, func, scope, ty, Behave, Behaviour, Dict, Element, Fields, IntoValue, Label, - NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, - Value, + elem, func, scope, ty, Dict, Element, Fields, IntoValue, Label, NativeElement, + Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value, }; use crate::introspection::{Location, Meta, MetaElem}; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; use crate::model::{Destination, EmphElem, StrongElem}; +use crate::realize::{Behave, Behaviour}; use crate::syntax::Span; use crate::text::UnderlineElem; use crate::util::{fat, BitSet}; @@ -330,34 +330,17 @@ impl Content { sequence.children.is_empty() } - /// Whether the content is a sequence. - pub fn is_sequence(&self) -> bool { - self.is::() - } - - /// Access the children if this is a sequence. - pub fn to_sequence(&self) -> Option>> { - let sequence = self.to_packed::()?; - Some(sequence.children.iter()) - } - /// Also auto expands sequence of sequences into flat sequence pub fn sequence_recursive_for_each<'a>(&'a self, f: &mut impl FnMut(&'a Self)) { - if let Some(children) = self.to_sequence() { - children.for_each(|c| c.sequence_recursive_for_each(f)); + if let Some(sequence) = self.to_packed::() { + for child in &sequence.children { + child.sequence_recursive_for_each(f); + } } else { f(self); } } - /// Access the child and styles. - pub fn to_styled(&self) -> Option<(&Content, &Styles)> { - let styled = self.to_packed::()?; - let child = styled.child(); - let styles = styled.styles(); - Some((child, styles)) - } - /// Style this content with a recipe, eagerly applying it if possible. pub fn styled_with_recipe( self, @@ -886,11 +869,12 @@ impl Debug for Packed { } } -/// Defines the element for sequences. +/// A sequence of content. #[elem(Debug, Repr, PartialEq)] -struct SequenceElem { +pub struct SequenceElem { + /// The elements. #[required] - children: Vec>, + pub children: Vec>, } impl Debug for SequenceElem { @@ -937,13 +921,15 @@ impl Repr for SequenceElem { } } -/// Defines the `ElemFunc` for styled elements. +/// Content alongside styles. #[elem(Debug, Repr, PartialEq)] -struct StyledElem { +pub struct StyledElem { + /// The content. #[required] - child: Prehashed, + pub child: Prehashed, + /// The styles. #[required] - styles: Styles, + pub styles: Styles, } impl Debug for StyledElem { diff --git a/crates/typst/src/foundations/element.rs b/crates/typst/src/foundations/element.rs index b59a16cb3..324eceefd 100644 --- a/crates/typst/src/foundations/element.rs +++ b/crates/typst/src/foundations/element.rs @@ -299,42 +299,3 @@ pub trait ShowSet { /// that should work even in the face of a user-defined show rule. fn show_set(&self, styles: StyleChain) -> Styles; } - -/// 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 - } -} - -/// 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 as if it didn't exist, but has a visual representation. - Ignorant, - /// An element that does not have a visual representation. - Invisible, -} - -impl Behaviour { - /// Whether this of `Weak(_)` variant. - pub fn is_weak(self) -> bool { - matches!(self, Self::Weak(_)) - } -} diff --git a/crates/typst/src/introspection/metadata.rs b/crates/typst/src/introspection/metadata.rs index d66441a2c..d8fb1574b 100644 --- a/crates/typst/src/introspection/metadata.rs +++ b/crates/typst/src/introspection/metadata.rs @@ -1,9 +1,8 @@ use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{ - elem, Behave, Behaviour, Content, Packed, Show, StyleChain, Value, -}; +use crate::foundations::{elem, Content, Packed, Show, StyleChain, Value}; use crate::introspection::Locatable; +use crate::realize::{Behave, Behaviour}; /// Exposes a value to the query system without producing visible content. /// diff --git a/crates/typst/src/introspection/mod.rs b/crates/typst/src/introspection/mod.rs index 625125106..af5faf26c 100644 --- a/crates/typst/src/introspection/mod.rs +++ b/crates/typst/src/introspection/mod.rs @@ -27,9 +27,10 @@ use smallvec::SmallVec; use crate::foundations::Packed; use crate::foundations::{ - category, elem, ty, Behave, Behaviour, Category, Content, Repr, Scope, Unlabellable, + category, elem, ty, Category, Content, Repr, Scope, Unlabellable, }; use crate::model::Destination; +use crate::realize::{Behave, Behaviour}; /// Interactions between document parts. /// diff --git a/crates/typst/src/layout/columns.rs b/crates/typst/src/layout/columns.rs index 6458acac2..04dc92755 100644 --- a/crates/typst/src/layout/columns.rs +++ b/crates/typst/src/layout/columns.rs @@ -2,11 +2,12 @@ use std::num::NonZeroUsize; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{elem, Behave, Behaviour, Content, Packed, StyleChain}; +use crate::foundations::{elem, Content, Packed, StyleChain}; use crate::layout::{ Abs, Axes, Dir, Fragment, Frame, LayoutMultiple, Length, Point, Ratio, Regions, Rel, Size, }; +use crate::realize::{Behave, Behaviour}; use crate::text::TextElem; use crate::util::Numeric; diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index 747fb97c7..4308182d7 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -5,7 +5,7 @@ use comemo::Prehashed; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Smart, StyleChain, + elem, Content, NativeElement, Packed, Resolve, Smart, StyleChain, StyledElem, }; use crate::introspection::{Meta, MetaElem}; use crate::layout::{ @@ -46,9 +46,9 @@ impl LayoutMultiple for Packed { for mut child in self.children().iter().map(|c| &**c) { let outer = styles; let mut styles = styles; - if let Some((elem, map)) = child.to_styled() { - child = elem; - styles = outer.chain(map); + if let Some(styled) = child.to_packed::() { + child = &styled.child; + styles = outer.chain(&styled.styles); } if child.is::() { @@ -344,8 +344,8 @@ impl<'a> FlowLayouter<'a> { // How to align the block. let align = if let Some(align) = child.to_packed::() { align.alignment(styles) - } else if let Some((_, local)) = child.to_styled() { - AlignElem::alignment_in(styles.chain(local)) + } else if let Some(styled) = child.to_packed::() { + AlignElem::alignment_in(styles.chain(&styled.styles)) } else { AlignElem::alignment_in(styles) } diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index 43c2c509d..3e3b7a76a 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -13,7 +13,7 @@ use self::shaping::{ use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; use crate::eval::Tracer; -use crate::foundations::{Content, Packed, Resolve, Smart, StyleChain}; +use crate::foundations::{Content, Packed, Resolve, Smart, StyleChain, StyledElem}; use crate::introspection::{Introspector, Locator, MetaElem}; use crate::layout::{ Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame, HElem, @@ -435,9 +435,9 @@ fn collect<'a>( while let Some(mut child) = iter.next() { let outer = styles; let mut styles = *styles; - if let Some((elem, local)) = child.to_styled() { - child = elem; - styles = outer.chain(local); + if let Some(styled) = child.to_packed::() { + child = &styled.child; + styles = outer.chain(&styled.styles); } let segment = if child.is::() { @@ -474,9 +474,9 @@ fn collect<'a>( region, SmartQuoteElem::alternative_in(styles), ); - let peeked = iter.peek().and_then(|child| { - let child = if let Some((child, _)) = child.to_styled() { - child + let peeked = iter.peek().and_then(|&child| { + let child = if let Some(styled) = child.to_packed::() { + &styled.child } else { child }; @@ -761,8 +761,8 @@ fn shared_get( let value = getter(styles); children .iter() - .filter_map(|child| child.to_styled()) - .all(|(_, local)| getter(styles.chain(local)) == value) + .filter_map(|child| child.to_packed::()) + .all(|styled| getter(styles.chain(&styled.styles)) == value) .then_some(value) } diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs index 31876e438..b29060244 100644 --- a/crates/typst/src/layout/place.rs +++ b/crates/typst/src/layout/place.rs @@ -1,9 +1,10 @@ use crate::diag::{bail, At, Hint, SourceResult}; use crate::engine::Engine; -use crate::foundations::{elem, Behave, Behaviour, Content, Packed, Smart, StyleChain}; +use crate::foundations::{elem, Content, Packed, Smart, StyleChain}; use crate::layout::{ Alignment, Axes, Em, Fragment, LayoutMultiple, Length, Regions, Rel, VAlignment, }; +use crate::realize::{Behave, Behaviour}; /// Places content at an absolute position. /// diff --git a/crates/typst/src/layout/spacing.rs b/crates/typst/src/layout/spacing.rs index d6a1d592a..7367c1aa1 100644 --- a/crates/typst/src/layout/spacing.rs +++ b/crates/typst/src/layout/spacing.rs @@ -1,7 +1,6 @@ -use crate::foundations::{ - cast, elem, Behave, Behaviour, Content, Packed, Resolve, StyleChain, -}; +use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain}; use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel}; +use crate::realize::{Behave, Behaviour}; use crate::util::Numeric; /// Inserts horizontal spacing into a paragraph. diff --git a/crates/typst/src/layout/stack.rs b/crates/typst/src/layout/stack.rs index 863e6eabb..e1c266254 100644 --- a/crates/typst/src/layout/stack.rs +++ b/crates/typst/src/layout/stack.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain}; +use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain, StyledElem}; use crate::layout::{ Abs, AlignElem, Axes, Axis, Dir, FixedAlignment, Fr, Fragment, Frame, LayoutMultiple, Point, Regions, Size, Spacing, @@ -209,8 +209,8 @@ impl<'a> StackLayouter<'a> { // Block-axis alignment of the `AlignElement` is respected by stacks. let align = if let Some(align) = block.to_packed::() { align.alignment(styles) - } else if let Some((_, local)) = block.to_styled() { - AlignElem::alignment_in(styles.chain(local)) + } else if let Some(styled) = block.to_packed::() { + AlignElem::alignment_in(styles.chain(&styled.styles)) } else { AlignElem::alignment_in(styles) } diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs index 63333d0f5..c98cac7b4 100644 --- a/crates/typst/src/math/mod.rs +++ b/crates/typst/src/math/mod.rs @@ -41,11 +41,13 @@ use self::row::*; use self::spacing::*; use crate::diag::SourceResult; +use crate::foundations::SequenceElem; +use crate::foundations::StyledElem; use crate::foundations::{ category, Category, Content, Module, Resolve, Scope, StyleChain, }; use crate::layout::{BoxElem, HElem, Spacing}; -use crate::realize::{realize, BehavedBuilder}; +use crate::realize::{process, BehavedBuilder}; use crate::text::{LinebreakElem, SpaceElem, TextElem}; /// Typst has special [syntax]($syntax/#math) and library functions to typeset @@ -230,11 +232,11 @@ impl LayoutMath for Content { return elem.layout_math(ctx, styles); } - if let Some(realized) = realize(ctx.engine, self, styles)? { + if let Some(realized) = process(ctx.engine, self, styles)? { return realized.layout_math(ctx, styles); } - if self.is_sequence() { + if self.is::() { let mut bb = BehavedBuilder::new(); self.sequence_recursive_for_each(&mut |child: &Content| { bb.push(child, StyleChain::default()); @@ -245,17 +247,17 @@ impl LayoutMath for Content { return Ok(()); } - if let Some((elem, local)) = self.to_styled() { + if let Some(styled) = self.to_packed::() { let outer = styles; - let styles = outer.chain(local); + let styles = outer.chain(&styled.styles); if TextElem::font_in(styles) != TextElem::font_in(outer) { - let frame = ctx.layout_content(elem, styles)?; + let frame = ctx.layout_content(&styled.child, styles)?; ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true)); return Ok(()); } - elem.layout_math(ctx, styles)?; + styled.child.layout_math(ctx, styles)?; return Ok(()); } diff --git a/crates/typst/src/model/document.rs b/crates/typst/src/model/document.rs index 81086dede..edd5e6c99 100644 --- a/crates/typst/src/model/document.rs +++ b/crates/typst/src/model/document.rs @@ -4,7 +4,7 @@ use crate::diag::{bail, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, Args, Array, Construct, Content, Datetime, Packed, Smart, StyleChain, - Value, + StyledElem, Value, }; use crate::introspection::{Introspector, ManualPageCounter}; use crate::layout::{LayoutRoot, Page, PageElem}; @@ -82,16 +82,16 @@ impl LayoutRoot for Packed { while let Some(mut child) = iter.next() { let outer = styles; let mut styles = styles; - if let Some((elem, local)) = child.to_styled() { - styles = outer.chain(local); - child = elem; + if let Some(styled) = child.to_packed::() { + child = &styled.child; + styles = outer.chain(&styled.styles); } if let Some(page) = child.to_packed::() { let extend_to = iter.peek().and_then(|&next| { *next - .to_styled() - .map_or(next, |(elem, _)| elem) + .to_packed::() + .map_or(next, |styled| &styled.child) .to_packed::()? .clear_to()? }); diff --git a/crates/typst/src/realize/behave.rs b/crates/typst/src/realize/behaviour.rs similarity index 80% rename from crates/typst/src/realize/behave.rs rename to crates/typst/src/realize/behaviour.rs index 035f6644f..747ce30c2 100644 --- a/crates/typst/src/realize/behave.rs +++ b/crates/typst/src/realize/behaviour.rs @@ -1,8 +1,47 @@ //! Element interaction. -use crate::foundations::{Behave, Behaviour, Content, StyleChain, Styles}; +use crate::foundations::{Content, StyleChain, Styles}; use crate::syntax::Span; +/// 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 as if it didn't exist, but has a visual representation. + Ignorant, + /// An element that does not have a visual representation. + Invisible, +} + +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. diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index a03a209ef..e9eaae600 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -1,24 +1,23 @@ //! Realization of content. mod arenas; -mod behave; +mod behaviour; +mod process; pub use self::arenas::Arenas; -pub use self::behave::BehavedBuilder; +pub use self::behaviour::{Behave, BehavedBuilder, Behaviour}; +pub use self::process::{process, processable}; use std::borrow::Cow; -use std::cell::OnceCell; -use std::mem; -use smallvec::smallvec; +use std::mem; use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; use crate::foundations::{ - Content, NativeElement, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, - Style, StyleChain, Styles, Synthesize, Transformation, + Content, NativeElement, Packed, SequenceElem, StyleChain, StyledElem, Styles, }; -use crate::introspection::{Locatable, Meta, MetaElem}; +use crate::introspection::MetaElem; use crate::layout::{ AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, HElem, LayoutMultiple, LayoutSingle, PageElem, PagebreakElem, Parity, PlaceElem, VElem, @@ -30,7 +29,6 @@ use crate::model::{ }; use crate::syntax::Span; use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; -use crate::util::{hash128, BitSet}; /// Realize into an element that is capable of root-level layout. #[typst_macros::time(name = "realize root")] @@ -43,8 +41,8 @@ pub fn realize_root<'a>( let mut builder = Builder::new(engine, arenas, true); builder.accept(content, styles)?; builder.interrupt_page(Some(styles), true)?; - let (pages, shared, span) = builder.doc.unwrap().pages.finish(); - Ok((Packed::new(DocumentElem::new(pages.to_vec())).spanned(span), shared)) + let (doc, trunk) = builder.doc.unwrap().finish(); + Ok((doc, trunk)) } /// Realize into an element that is capable of block-level layout. @@ -57,7 +55,7 @@ pub fn realize_block<'a>( ) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> { // These elements implement `Layout` but still require a flow for // proper layout. - if content.can::() && verdict(engine, content, styles).is_none() { + if content.can::() && !processable(engine, content, styles) { return Ok((Cow::Borrowed(content), styles)); } @@ -65,298 +63,8 @@ pub fn realize_block<'a>( builder.accept(content, styles)?; builder.interrupt_par()?; - let (children, shared, span) = builder.flow.0.finish(); - Ok((Cow::Owned(FlowElem::new(children).pack().spanned(span)), shared)) -} - -/// Apply the show rules in the given style chain to a target element. -pub fn realize( - engine: &mut Engine, - target: &Content, - styles: StyleChain, -) -> SourceResult> { - 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 meta = None; - if !prepared { - meta = prepare(engine, &mut target, &mut map, styles)?; - } - - // Apply a step, if there is one. - let mut 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.delayed(|engine| show(engine, target, step, styles.chain(&map))) - } - None => target, - }; - - // If necessary, apply metadata generated in the preparation. - if let Some(meta) = meta { - output += meta.pack(); - } - - Ok(Some(output.styled_with_map(map))) -} - -/// What to do with an element when encountering it during realization. -struct Verdict<'a> { - /// Whether the element is already prepated (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>, -} - -/// 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, -} - -/// 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> { - let mut target = target; - let mut map = Styles::new(); - let mut revoked = BitSet::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::() { - slot = target.clone(); - slot.with_mut::() - .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; - } - - if let Transformation::Style(transform) = &recipe.transform { - // 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::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::() { - 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.can::() - && !target.can::() - && !target.can::() - }) - { - return None; - } - - Some(Verdict { prepared, map, step }) -} - -/// This is only executed the first time an element is visited. -fn prepare( - engine: &mut Engine, - target: &mut Content, - map: &mut Styles, - styles: StyleChain, -) -> SourceResult>> { - // 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. - if target.can::() || target.label().is_some() { - let location = engine.locator.locate(hash128(&target)); - target.set_location(location); - } - - // 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::() { - 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::() { - 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)); - - // Ensure that this preparation only runs once by marking the element as - // prepared. - target.mark_prepared(); - - // Apply metadata be able to find the element in the frames. - // Do this after synthesis, so that it includes the synthesized fields. - if target.location().is_some() { - // Add a style to the whole element's subtree identifying it as - // belonging to the element. - map.set(MetaElem::set_data(smallvec![Meta::Elem(target.clone())])); - - // Return an extra meta elem that will be attached so that the metadata - // styles are not lost in case the element's show rule results in - // nothing. - return Ok(Some(Packed::new(MetaElem::new()).spanned(target.span()))); - } - - Ok(None) -} - -/// Apply a step. -fn show( - engine: &mut Engine, - target: Content, - step: ShowStep, - styles: StyleChain, -) -> SourceResult { - match step { - // Apply a user-defined show rule. - ShowStep::Recipe(recipe, guard) => 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::().unwrap(); - show_regex(engine, &text, regex, recipe, guard) - } - - // Just apply the recipe. - _ => recipe.apply(engine, target.guarded(guard)), - }, - - // If the verdict picks this step, the `target` is guaranteed to have a - // built-in show rule. - ShowStep::Builtin => target.with::().unwrap().show(engine, styles), - } -} - -/// Apply a regex show rule recipe to a target. -fn show_regex( - engine: &mut Engine, - elem: &Packed, - regex: &Regex, - recipe: &Recipe, - index: RecipeIndex, -) -> SourceResult { - let make = |s: &str| { - let mut fresh = elem.clone(); - fresh.push_text(s.into()); - fresh.pack() - }; - - let mut result = vec![]; - let mut cursor = 0; - - let text = elem.text(); - - for m in regex.find_iter(elem.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, 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))) + let (flow, trunk) = builder.flow.finish(); + Ok((Cow::Owned(flow.pack()), trunk)) } /// Builds a document or a flow element from content. @@ -401,7 +109,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { .store(EquationElem::new(content.clone()).pack().spanned(content.span())); } - if let Some(realized) = realize(self.engine, content, styles)? { + if let Some(realized) = process(self.engine, content, styles)? { self.engine.route.increase(); if !self.engine.route.within(Route::MAX_SHOW_RULE_DEPTH) { bail!( @@ -414,12 +122,12 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { return result; } - if let Some((elem, local)) = content.to_styled() { - return self.styled(elem, local, styles); + if let Some(styled) = content.to_packed::() { + return self.styled(styled, styles); } - if let Some(children) = content.to_sequence() { - for elem in children { + if let Some(sequence) = content.to_packed::() { + for elem in &sequence.children { self.accept(elem, styles)?; } return Ok(()); @@ -472,15 +180,14 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { fn styled( &mut self, - elem: &'a Content, - map: &'a Styles, + styled: &'a StyledElem, styles: StyleChain<'a>, ) -> SourceResult<()> { let stored = self.arenas.store(styles); - let styles = stored.chain(map); - self.interrupt_style(map, None)?; - self.accept(elem, styles)?; - self.interrupt_style(map, Some(styles))?; + let styles = stored.chain(&styled.styles); + self.interrupt_style(&styled.styles, None)?; + self.accept(&styled.child, styles)?; + self.interrupt_style(&styled.styles, Some(styles))?; Ok(()) } @@ -490,13 +197,15 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { outer: Option>, ) -> SourceResult<()> { if let Some(Some(span)) = local.interruption::() { - if self.doc.is_none() { + let Some(doc) = &self.doc else { bail!(span, "document set rules are not allowed inside of containers"); - } + }; if outer.is_none() - && (!self.flow.0.is_empty() + && (!doc.pages.is_empty() + || !self.flow.0.is_empty() || !self.par.0.is_empty() - || !self.list.items.is_empty()) + || !self.list.items.is_empty() + || !self.cites.items.is_empty()) { bail!(span, "document set rules must appear before any content"); } @@ -522,7 +231,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { if !self.cites.items.is_empty() { let staged = mem::take(&mut self.cites.staged); let (group, styles) = mem::take(&mut self.cites).finish(); - self.accept(self.arenas.store(group), styles)?; + self.accept(self.arenas.store(group.pack()), styles)?; for (content, styles) in staged { self.accept(content, styles)?; } @@ -547,7 +256,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { self.interrupt_list()?; if !self.par.0.is_empty() { let (par, styles) = mem::take(&mut self.par).finish(); - self.accept(self.arenas.store(par), styles)?; + self.accept(self.arenas.store(par.pack()), styles)?; } Ok(()) @@ -561,15 +270,15 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { self.interrupt_par()?; let Some(doc) = &mut self.doc else { return Ok(()) }; if (doc.keep_next && styles.is_some()) || self.flow.0.has_strong_elements(last) { - let (children, shared, span) = mem::take(&mut self.flow).0.finish(); - let styles = if shared == StyleChain::default() { + let (flow, trunk) = mem::take(&mut self.flow).finish(); + let span = flow.span(); + let styles = if trunk == StyleChain::default() { styles.unwrap_or_default() } else { - shared + trunk }; - let flow = FlowElem::new(children); - let page = PageElem::new(flow.pack().spanned(span)); - self.accept(self.arenas.store(page.pack().spanned(span)), styles)?; + let page = PageElem::new(flow.pack()).pack().spanned(span); + self.accept(self.arenas.store(page), styles)?; } Ok(()) } @@ -614,6 +323,11 @@ impl<'a> DocBuilder<'a> { false } + + fn finish(self) -> (Packed, StyleChain<'a>) { + let (children, trunk, span) = self.pages.finish(); + (Packed::new(DocumentElem::new(children)).spanned(span), trunk) + } } impl Default for DocBuilder<'_> { @@ -688,6 +402,11 @@ impl<'a> FlowBuilder<'a> { false } + + fn finish(self) -> (Packed, StyleChain<'a>) { + let (children, trunk, span) = self.0.finish(); + (Packed::new(FlowElem::new(children)).spanned(span), trunk) + } } /// Accepts paragraph content. @@ -718,9 +437,9 @@ impl<'a> ParBuilder<'a> { false } - fn finish(self) -> (Content, StyleChain<'a>) { - let (children, shared, span) = self.0.finish(); - (ParElem::new(children).pack().spanned(span), shared) + fn finish(self) -> (Packed, StyleChain<'a>) { + let (children, trunk, span) = self.0.finish(); + (Packed::new(ParElem::new(children)).spanned(span), trunk) } } @@ -761,7 +480,7 @@ impl<'a> ListBuilder<'a> { } fn finish(self) -> (Content, StyleChain<'a>) { - let (items, shared, span) = self.items.finish_iter(); + let (items, trunk, span) = self.items.finish_iter(); let mut items = items.peekable(); let (first, _) = items.peek().unwrap(); let output = if first.is::() { @@ -812,7 +531,7 @@ impl<'a> ListBuilder<'a> { } else { unreachable!() }; - (output, shared) + (output, trunk) } } @@ -858,8 +577,8 @@ impl<'a> CiteGroupBuilder<'a> { false } - fn finish(self) -> (Content, StyleChain<'a>) { + fn finish(self) -> (Packed, StyleChain<'a>) { let span = self.items.first().map(|cite| cite.span()).unwrap_or(Span::detached()); - (CiteGroup::new(self.items).pack().spanned(span), self.styles) + (Packed::new(CiteGroup::new(self.items)).spanned(span), self.styles) } } diff --git a/crates/typst/src/realize/process.rs b/crates/typst/src/realize/process.rs new file mode 100644 index 000000000..73d5cc4fc --- /dev/null +++ b/crates/typst/src/realize/process.rs @@ -0,0 +1,312 @@ +use std::cell::OnceCell; + +use smallvec::smallvec; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + Content, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style, + StyleChain, Styles, Synthesize, Transformation, +}; +use crate::introspection::{Locatable, Meta, MetaElem}; +use crate::text::TextElem; +use crate::util::{hash128, BitSet}; + +/// What to do with an element when encountering it during realization. +struct Verdict<'a> { + /// Whether the element is already prepated (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>, +} + +/// 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, +} + +/// Whether the `target` element needs processing. +pub fn processable<'a>( + engine: &mut Engine, + target: &'a Content, + styles: StyleChain<'a>, +) -> bool { + verdict(engine, target, styles).is_some() +} + +/// Processes the given `target` element when encountering it during realization. +pub fn process( + engine: &mut Engine, + target: &Content, + styles: StyleChain, +) -> SourceResult> { + 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 meta = None; + if !prepared { + meta = prepare(engine, &mut target, &mut map, styles)?; + } + + // Apply a step, if there is one. + let mut 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.delayed(|engine| show(engine, target, step, styles.chain(&map))) + } + None => target, + }; + + // If necessary, apply metadata generated in the preparation. + if let Some(meta) = meta { + output += meta.pack(); + } + + Ok(Some(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> { + let mut target = target; + let mut map = Styles::new(); + let mut revoked = BitSet::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::() { + slot = target.clone(); + slot.with_mut::() + .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; + } + + if let Transformation::Style(transform) = &recipe.transform { + // 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::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::() { + 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.can::() + && !target.can::() + && !target.can::() + }) + { + return None; + } + + Some(Verdict { prepared, map, step }) +} + +/// This is only executed the first time an element is visited. +fn prepare( + engine: &mut Engine, + target: &mut Content, + map: &mut Styles, + styles: StyleChain, +) -> SourceResult>> { + // 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. + if target.can::() || target.label().is_some() { + let location = engine.locator.locate(hash128(&target)); + target.set_location(location); + } + + // 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::() { + 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::() { + 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)); + + // Ensure that this preparation only runs once by marking the element as + // prepared. + target.mark_prepared(); + + // Apply metadata be able to find the element in the frames. + // Do this after synthesis, so that it includes the synthesized fields. + if target.location().is_some() { + // Add a style to the whole element's subtree identifying it as + // belonging to the element. + map.set(MetaElem::set_data(smallvec![Meta::Elem(target.clone())])); + + // Return an extra meta elem that will be attached so that the metadata + // styles are not lost in case the element's show rule results in + // nothing. + return Ok(Some(Packed::new(MetaElem::new()).spanned(target.span()))); + } + + Ok(None) +} + +/// Apply a step. +fn show( + engine: &mut Engine, + target: Content, + step: ShowStep, + styles: StyleChain, +) -> SourceResult { + match step { + // Apply a user-defined show rule. + ShowStep::Recipe(recipe, guard) => 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::().unwrap(); + show_regex(engine, &text, regex, recipe, guard) + } + + // Just apply the recipe. + _ => recipe.apply(engine, target.guarded(guard)), + }, + + // If the verdict picks this step, the `target` is guaranteed to have a + // built-in show rule. + ShowStep::Builtin => target.with::().unwrap().show(engine, styles), + } +} + +/// Apply a regex show rule recipe to a target. +fn show_regex( + engine: &mut Engine, + elem: &Packed, + regex: &Regex, + recipe: &Recipe, + index: RecipeIndex, +) -> SourceResult { + let make = |s: &str| { + let mut fresh = elem.clone(); + fresh.push_text(s.into()); + fresh.pack() + }; + + let mut result = vec![]; + let mut cursor = 0; + + let text = elem.text(); + + for m in regex.find_iter(elem.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, 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 2bc315a3e..23349e253 100644 --- a/crates/typst/src/text/linebreak.rs +++ b/crates/typst/src/text/linebreak.rs @@ -1,4 +1,5 @@ -use crate::foundations::{elem, Behave, Behaviour, Packed}; +use crate::foundations::{elem, Packed}; +use crate::realize::{Behave, Behaviour}; /// Inserts a line break. /// diff --git a/crates/typst/src/text/shift.rs b/crates/typst/src/text/shift.rs index dcff1c480..2dbfd2b6d 100644 --- a/crates/typst/src/text/shift.rs +++ b/crates/typst/src/text/shift.rs @@ -2,7 +2,7 @@ use ecow::EcoString; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain}; +use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain}; use crate::layout::{Em, Length}; use crate::text::{variant, SpaceElem, TextElem, TextSize}; use crate::World; @@ -134,9 +134,9 @@ fn search_text(content: &Content, sub: bool) -> Option { Some(' '.into()) } else if let Some(elem) = content.to_packed::() { convert_script(elem.text(), sub) - } else if let Some(children) = content.to_sequence() { + } else if let Some(sequence) = content.to_packed::() { let mut full = EcoString::new(); - for item in children { + for item in &sequence.children { match search_text(item, sub) { Some(text) => full.push_str(&text), None => return None, diff --git a/crates/typst/src/text/space.rs b/crates/typst/src/text/space.rs index e584d9486..114a2e80d 100644 --- a/crates/typst/src/text/space.rs +++ b/crates/typst/src/text/space.rs @@ -1,8 +1,8 @@ -use crate::foundations::{ - elem, Behave, Behaviour, Packed, PlainText, Repr, Unlabellable, -}; use ecow::EcoString; +use crate::foundations::{elem, Packed, PlainText, Repr, Unlabellable}; +use crate::realize::{Behave, Behaviour}; + /// A text space. #[elem(Behave, Unlabellable, PlainText, Repr)] pub struct SpaceElem {}