diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-08-26 19:17:58 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-08-26 17:17:58 +0000 |
| commit | 4365e18454af4f8f53fe1198182cb5dc8f3d628e (patch) | |
| tree | fe37649cec71b916d5119a033028e3f620247209 | |
| parent | cb98eec60924a41889988644e5122d2e13eccbf1 (diff) | |
Improve realization and page layout (#4840)
57 files changed, 1705 insertions, 1025 deletions
diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 831b2374..79d4bb09 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -106,6 +106,18 @@ impl<T> OptionExt<T> for Option<T> { /// Extra methods for [`[T]`](slice). pub trait SliceExt<T> { + /// Returns a slice with all matching elements from the start of the slice + /// removed. + fn trim_start_matches<F>(&self, f: F) -> &[T] + where + F: FnMut(&T) -> bool; + + /// Returns a slice with all matching elements from the end of the slice + /// removed. + fn trim_end_matches<F>(&self, f: F) -> &[T] + where + F: FnMut(&T) -> bool; + /// Split a slice into consecutive runs with the same key and yield for /// each such run the key and the slice of elements with that key. fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> @@ -115,6 +127,29 @@ pub trait SliceExt<T> { } impl<T> SliceExt<T> for [T] { + fn trim_start_matches<F>(&self, mut f: F) -> &[T] + where + F: FnMut(&T) -> bool, + { + let len = self.len(); + let mut i = 0; + while i < len && f(&self[i]) { + i += 1; + } + &self[i..] + } + + fn trim_end_matches<F>(&self, mut f: F) -> &[T] + where + F: FnMut(&T) -> bool, + { + let mut i = self.len(); + while i > 0 && f(&self[i - 1]) { + i -= 1; + } + &self[..i] + } + fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> { GroupByKey { slice: self, f } } diff --git a/crates/typst/src/eval/rules.rs b/crates/typst/src/eval/rules.rs index f5d7da17..61fee84a 100644 --- a/crates/typst/src/eval/rules.rs +++ b/crates/typst/src/eval/rules.rs @@ -24,7 +24,7 @@ impl Eval for ast::SetRule<'_> { }) .at(target.span())?; let args = self.args().eval(vm)?.spanned(self.span()); - Ok(target.set(&mut vm.engine, args)?.spanned(self.span())) + Ok(target.set(&mut vm.engine, args)?.spanned(self.span()).liftable()) } } @@ -46,6 +46,6 @@ impl Eval for ast::ShowRule<'_> { expr => expr.eval(vm)?.cast::<Transformation>().at(span)?, }; - Ok(Recipe { span, selector, transform }) + Ok(Recipe::new(selector, transform, span)) } } diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index 656049dd..b9d1b189 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -369,7 +369,7 @@ impl Content { context: Tracked<Context>, recipe: Recipe, ) -> SourceResult<Self> { - if recipe.selector.is_none() { + if recipe.selector().is_none() { recipe.apply(engine, context, self) } else { Ok(self.styled(recipe)) diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs index 55bb348a..e74a1649 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -93,6 +93,11 @@ impl Styles { self.0.iter().map(|style| &**style) } + /// Iterate over the contained styles. + pub fn as_slice(&self) -> &[LazyHash<Style>] { + self.0.as_slice() + } + /// Set an inner value for a style property. /// /// If the property needs folding and the value is already contained in the @@ -118,16 +123,33 @@ impl Styles { self.0.insert(0, LazyHash::new(outer)); } - /// Apply a slice of outer styles. - pub fn apply_slice(&mut self, outer: &[LazyHash<Style>]) { - self.0 = outer.iter().cloned().chain(mem::take(self).0).collect(); - } - /// Add an origin span to all contained properties. pub fn spanned(mut self, span: Span) -> Self { for entry in self.0.make_mut() { if let Style::Property(property) = &mut **entry { - property.span = Some(span); + property.span = span; + } + } + self + } + + /// Marks the styles as having been applied outside of any show rule. + pub fn outside(mut self) -> Self { + for entry in self.0.make_mut() { + match &mut **entry { + Style::Property(property) => property.outside = true, + Style::Recipe(recipe) => recipe.outside = true, + _ => {} + } + } + self + } + + /// Marks the styles as being allowed to be lifted up to the page level. + pub fn liftable(mut self) -> Self { + for entry in self.0.make_mut() { + if let Style::Property(property) = &mut **entry { + property.liftable = true; } } self @@ -144,13 +166,8 @@ impl Styles { /// Returns `Some(_)` with an optional span if this list contains /// styles for the given element. - pub fn interruption<T: NativeElement>(&self) -> Option<Option<Span>> { - let elem = T::elem(); - self.0.iter().find_map(|entry| match &**entry { - Style::Property(property) => property.is_of(elem).then_some(property.span), - Style::Recipe(recipe) => recipe.is_of(elem).then_some(Some(recipe.span)), - Style::Revocation(_) => None, - }) + 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 @@ -176,6 +193,21 @@ impl From<Style> for Styles { } } +impl IntoIterator for Styles { + type Item = LazyHash<Style>; + type IntoIter = ecow::vec::IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl FromIterator<LazyHash<Style>> for Styles { + fn from_iter<T: IntoIterator<Item = LazyHash<Style>>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + impl Debug for Styles { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("Styles ")?; @@ -216,6 +248,37 @@ impl Style { _ => None, } } + + /// Returns `Some(_)` with an optional span if this style is of + /// the given element. + pub fn interruption<T: NativeElement>(&self) -> Option<Span> { + let elem = T::elem(); + 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::Revocation(_) => None, + } + } + + /// Whether the style is allowed to be lifted up to the page level. Only + /// true for styles originating from set rules. + pub fn liftable(&self) -> bool { + match self { + Self::Property(property) => property.liftable, + Self::Recipe(_) => true, + Self::Revocation(_) => false, + } + } + + /// Whether the style was applied outside of any show rule. This is set + /// during realization. + pub fn outside(&self) -> bool { + match self { + Self::Property(property) => property.outside, + Self::Recipe(recipe) => recipe.outside, + Self::Revocation(_) => false, + } + } } impl Debug for Style { @@ -250,7 +313,11 @@ pub struct Property { /// The property's value. value: Block, /// The span of the set rule the property stems from. - span: Option<Span>, + span: Span, + /// Whether the property is allowed to be lifted up to the page level. + liftable: bool, + /// Whether the property was applied outside of any show rule. + outside: bool, } impl Property { @@ -264,7 +331,9 @@ impl Property { elem: E::elem(), id, value: Block::new(value), - span: None, + span: Span::detached(), + liftable: false, + outside: false, } } @@ -370,19 +439,41 @@ impl Hash for dyn Blockable { /// A show rule recipe. #[derive(Clone, PartialEq, Hash)] pub struct Recipe { - /// The span that errors are reported with. - pub span: Span, /// Determines whether the recipe applies to an element. /// /// If this is `None`, then this recipe is from a show rule with /// no selector (`show: rest => ...`), which is [eagerly applied][Content::styled_with_recipe] /// to the rest of the content in the scope. - pub selector: Option<Selector>, + selector: Option<Selector>, /// The transformation to perform on the match. - pub transform: Transformation, + transform: Transformation, + /// The span that errors are reported with. + span: Span, + /// Relevant properties of the kind of construct the style originated from + /// and where it was applied. + outside: bool, } impl Recipe { + /// Create a new recipe from a key-value pair. + pub fn new( + selector: Option<Selector>, + transform: Transformation, + span: Span, + ) -> Self { + Self { selector, transform, span, outside: false } + } + + /// The recipe's selector. + pub fn selector(&self) -> Option<&Selector> { + self.selector.as_ref() + } + + /// The recipe's transformation. + pub fn transform(&self) -> &Transformation { + &self.transform + } + /// Whether this recipe is for the given type of element. pub fn is_of(&self, element: Element) -> bool { match self.selector { @@ -494,7 +585,7 @@ impl<'a> StyleChain<'a> { /// `self`. For folded properties `local` contributes the inner value. pub fn chain<'b, C>(&'b self, local: &'b C) -> StyleChain<'b> where - C: Chainable, + C: Chainable + ?Sized, { Chainable::chain(local, self) } @@ -557,7 +648,7 @@ impl<'a> StyleChain<'a> { ) -> impl Iterator<Item = &'a T> { inherent.into_iter().chain( self.entries() - .filter_map(Style::property) + .filter_map(|style| style.property()) .filter(move |property| property.is(func, id)) .map(|property| &property.value) .map(move |value| { @@ -573,15 +664,6 @@ impl<'a> StyleChain<'a> { ) } - /// Convert to a style map. - pub fn to_map(self) -> Styles { - let mut suffix = Styles::new(); - for link in self.links() { - suffix.apply_slice(link); - } - suffix - } - /// Iterate over the entries of the chain. pub fn entries(self) -> Entries<'a> { Entries { inner: [].as_slice().iter(), links: self.links() } @@ -592,21 +674,59 @@ impl<'a> StyleChain<'a> { Links(Some(self)) } + /// Convert to a style map. + pub fn to_map(self) -> Styles { + let mut styles: EcoVec<_> = self.entries().cloned().collect(); + styles.make_mut().reverse(); + Styles(styles) + } + /// Build owned styles from the suffix (all links beyond the `len`) of the /// chain. pub fn suffix(self, len: usize) -> Styles { - let mut suffix = Styles::new(); + let mut styles = EcoVec::new(); let take = self.links().count().saturating_sub(len); for link in self.links().take(take) { - suffix.apply_slice(link); + styles.extend(link.iter().cloned().rev()); } - suffix + styles.make_mut().reverse(); + Styles(styles) } /// Remove the last link from the chain. pub fn pop(&mut self) { *self = self.tail.copied().unwrap_or_default(); } + + /// Determine the shared trunk of a collection of style chains. + pub fn trunk(iter: impl IntoIterator<Item = Self>) -> Option<Self> { + // Determine shared style depth and first span. + let mut iter = iter.into_iter(); + let mut trunk = iter.next()?; + let mut depth = trunk.links().count(); + + for mut chain in iter { + let len = chain.links().count(); + if len < depth { + for _ in 0..depth - len { + trunk.pop(); + } + depth = len; + } else if len > depth { + for _ in 0..len - depth { + chain.pop(); + } + } + + while depth > 0 && chain != trunk { + trunk.pop(); + chain.pop(); + depth -= 1; + } + } + + Some(trunk) + } } impl Debug for StyleChain<'_> { @@ -673,7 +793,7 @@ pub struct Entries<'a> { } impl<'a> Iterator for Entries<'a> { - type Item = &'a Style; + type Item = &'a LazyHash<Style>; fn next(&mut self) -> Option<Self::Item> { loop { @@ -702,6 +822,107 @@ impl<'a> Iterator for Links<'a> { } } +/// A sequence of elements with associated styles. +#[derive(Clone, PartialEq, Hash)] +pub struct StyleVec { + /// The elements themselves. + elements: EcoVec<Content>, + /// A run-length encoded list of style lists. + /// + /// Each element is a (styles, count) pair. Any elements whose + /// style falls after the end of this list is considered to + /// have an empty style list. + styles: EcoVec<(Styles, usize)>, +} + +impl StyleVec { + /// Create a style vector from an unstyled vector content. + pub fn wrap(elements: EcoVec<Content>) -> Self { + Self { elements, styles: EcoVec::new() } + } + + /// Create a `StyleVec` from a list of content with style chains. + pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) { + let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); + let depth = trunk.links().count(); + + let mut elements = EcoVec::with_capacity(buf.len()); + let mut styles = EcoVec::<(Styles, usize)>::new(); + let mut last: Option<(StyleChain<'a>, usize)> = None; + + for &(element, chain) in buf { + elements.push(element.clone()); + + if let Some((prev, run)) = &mut last { + if chain == *prev { + *run += 1; + } else { + styles.push((prev.suffix(depth), *run)); + last = Some((chain, 1)); + } + } else { + last = Some((chain, 1)); + } + } + + if let Some((last, run)) = last { + let skippable = styles.is_empty() && last == trunk; + if !skippable { + styles.push((last.suffix(depth), run)); + } + } + + (StyleVec { elements, styles }, trunk) + } + + /// Whether there are no elements. + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } + + /// The number of elements. + pub fn len(&self) -> usize { + self.elements.len() + } + + /// Iterate over the contained content and style chains. + pub fn iter<'a>( + &'a self, + outer: &'a StyleChain<'_>, + ) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> { + static EMPTY: Styles = Styles::new(); + self.elements + .iter() + .zip( + self.styles + .iter() + .flat_map(|(local, count)| std::iter::repeat(local).take(*count)) + .chain(std::iter::repeat(&EMPTY)), + ) + .map(|(element, local)| (element, outer.chain(local))) + } + + /// Get a style property, but only if it is the same for all children of the + /// style vector. + pub fn shared_get<T: PartialEq>( + &self, + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, + ) -> Option<T> { + let value = getter(styles); + self.styles + .iter() + .all(|(local, _)| getter(styles.chain(local)) == value) + .then_some(value) + } +} + +impl Debug for StyleVec { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + f.debug_list().entries(&self.elements).finish() + } +} + /// A property that is resolved with other properties from the style chain. pub trait Resolve { /// The type of the resolved output. diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index da44a227..e7dd2ba0 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -825,7 +825,7 @@ impl ManualPageCounter { match item { FrameItem::Group(group) => self.visit(engine, &group.frame)?, FrameItem::Tag(tag) => { - let Some(elem) = tag.elem.to_packed::<CounterUpdateElem>() else { + let Some(elem) = tag.elem().to_packed::<CounterUpdateElem>() else { continue; }; if *elem.key() == CounterKey::Page { diff --git a/crates/typst/src/introspection/introspector.rs b/crates/typst/src/introspection/introspector.rs index fe59cb00..45307768 100644 --- a/crates/typst/src/introspection/introspector.rs +++ b/crates/typst/src/introspection/introspector.rs @@ -10,7 +10,7 @@ use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::introspection::Location; +use crate::introspection::{Location, TagKind}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::model::Numbering; use crate::utils::NonZeroExt; @@ -66,20 +66,21 @@ impl Introspector { self.extract(&group.frame, page, ts); } FrameItem::Tag(tag) - if !self.elems.contains_key(&tag.elem.location().unwrap()) => + if tag.kind() == TagKind::Start + && !self.elems.contains_key(&tag.location()) => { let pos = pos.transform(ts); - let loc = tag.elem.location().unwrap(); + let loc = tag.location(); let ret = self .elems - .insert(loc, (tag.elem.clone(), Position { page, point: pos })); + .insert(loc, (tag.elem().clone(), Position { page, point: pos })); assert!(ret.is_none(), "duplicate locations"); // Build the key map. - self.keys.entry(tag.key).or_default().push(loc); + self.keys.entry(tag.key()).or_default().push(loc); // Build the label cache. - if let Some(label) = tag.elem.label() { + if let Some(label) = tag.elem().label() { self.labels.entry(label).or_default().push(self.elems.len() - 1); } } diff --git a/crates/typst/src/introspection/mod.rs b/crates/typst/src/introspection/mod.rs index 6c982afb..b1ff2e08 100644 --- a/crates/typst/src/introspection/mod.rs +++ b/crates/typst/src/introspection/mod.rs @@ -12,6 +12,7 @@ mod metadata; #[path = "query.rs"] mod query_; mod state; +mod tag; pub use self::counter::*; pub use self::here_::*; @@ -22,16 +23,9 @@ pub use self::locator::*; pub use self::metadata::*; pub use self::query_::*; pub use self::state::*; +pub use self::tag::*; -use std::fmt::{self, Debug, Formatter}; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - category, elem, Args, Category, Construct, Content, NativeElement, Packed, Scope, - Unlabellable, -}; -use crate::realize::{Behave, Behaviour}; +use crate::foundations::{category, Category, Scope}; /// Interactions between document parts. /// @@ -57,65 +51,3 @@ pub fn define(global: &mut Scope) { global.define_func::<query>(); global.define_func::<locate>(); } - -/// Holds a tag for a locatable element that was realized. -/// -/// The `TagElem` is handled by all layouters. The held element becomes -/// available for introspection in the next compiler iteration. -#[elem(Behave, Unlabellable, Construct)] -pub struct TagElem { - /// The introspectible element. - #[required] - #[internal] - pub tag: Tag, -} - -impl TagElem { - /// Create a packed tag element. - pub fn packed(tag: Tag) -> Content { - let mut content = Self::new(tag).pack(); - // We can skip preparation for the `TagElem`. - content.mark_prepared(); - content - } -} - -impl Construct for TagElem { - fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { - bail!(args.span, "cannot be constructed manually") - } -} - -impl Unlabellable for Packed<TagElem> {} - -impl Behave for Packed<TagElem> { - fn behaviour(&self) -> Behaviour { - Behaviour::Invisible - } -} - -/// Holds a locatable element that was realized. -#[derive(Clone, PartialEq, Hash)] -pub struct Tag { - /// The introspectible element. - pub elem: Content, - /// The element's key hash, which forms the base of its location (but is - /// locally disambiguated and combined with outer hashes). - /// - /// We need to retain this for introspector-assisted location assignment - /// during measurement. - pub(crate) key: u128, -} - -impl Tag { - /// Create a tag from an element and its key hash. - pub fn new(elem: Content, key: u128) -> Self { - Self { elem, key } - } -} - -impl Debug for Tag { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Tag({:?})", self.elem.elem().name()) - } -} diff --git a/crates/typst/src/introspection/tag.rs b/crates/typst/src/introspection/tag.rs new file mode 100644 index 00000000..51a6454a --- /dev/null +++ b/crates/typst/src/introspection/tag.rs @@ -0,0 +1,111 @@ +use std::fmt::{self, Debug, Formatter}; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +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)] +pub struct Tag { + /// Whether this is a start or end tag. + kind: TagKind, + /// The introspectible element. + elem: Content, + /// The element's key hash. + key: u128, +} + +impl Tag { + /// Create a start tag from an element and its key hash. + /// + /// Panics if the element does not have a [`Location`]. + #[track_caller] + pub fn new(elem: Content, key: u128) -> Self { + assert!(elem.location().is_some()); + Self { elem, key, kind: TagKind::Start } + } + + /// Returns the same tag with the given kind. + pub fn with_kind(self, kind: TagKind) -> Self { + Self { kind, ..self } + } + + /// Whether this is a start or end tag. + pub fn kind(&self) -> TagKind { + self.kind + } + + /// The locatable element that the tag holds. + pub fn elem(&self) -> &Content { + &self.elem + } + + /// Access the location of the element. + pub fn location(&self) -> Location { + self.elem.location().unwrap() + } + + /// The element's key hash, which forms the base of its location (but is + /// locally disambiguated and combined with outer hashes). + /// + /// We need to retain this for introspector-assisted location assignment + /// during measurement. + pub fn key(&self) -> u128 { + self.key + } +} + +impl Debug for Tag { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Tag({:?}, {:?})", self.kind, self.elem.elem().name()) + } +} + +/// Determines whether a tag marks the start or end of an element. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum TagKind { + /// The tag indicates that the element starts here. + Start, + /// The tag indicates that the element end here. + End, +} + +/// Holds a tag for a locatable element that was realized. +/// +/// The `TagElem` is handled by all layouters. The held element becomes +/// available for introspection in the next compiler iteration. +#[elem(Behave, Unlabellable, Construct)] +pub struct TagElem { + /// The introspectible element. + #[required] + #[internal] + pub tag: Tag, +} + +impl TagElem { + /// Create a packed tag element. + pub fn packed(tag: Tag) -> Content { + let mut content = Self::new(tag).pack(); + // We can skip preparation for the `TagElem`. + content.mark_prepared(); + content + } +} + +impl Construct for TagElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually") + } +} + +impl Unlabellable for Packed<TagElem> {} + +impl Behave for Packed<TagElem> { + fn behaviour(&self) -> Behaviour { + Behaviour::Ignorant + } +} diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index 1566febc..f1148604 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -2,35 +2,65 @@ //! - at the top-level, into a [`Document`]. //! - inside of a container, into a [`Frame`] or [`Fragment`]. -use std::fmt::{self, Debug, Formatter}; +use std::collections::HashSet; use std::num::NonZeroUsize; -use std::ptr; use comemo::{Track, Tracked, TrackedMut}; use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ - elem, Args, Construct, Content, NativeElement, Packed, Resolve, Smart, StyleChain, + Content, NativeElement, Packed, Resolve, Smart, StyleChain, Styles, }; use crate::introspection::{ - Counter, CounterDisplayElem, CounterKey, Introspector, Locator, LocatorLink, - ManualPageCounter, SplitLocator, Tag, TagElem, + Counter, CounterDisplayElem, CounterKey, Introspector, Location, Locator, + LocatorLink, ManualPageCounter, SplitLocator, Tag, TagElem, TagKind, }; use crate::layout::{ Abs, AlignElem, Alignment, Axes, Binding, BlockElem, ColbreakElem, ColumnsElem, Dir, FixedAlignment, FlushElem, Fr, Fragment, Frame, FrameItem, HAlignment, Length, - OuterVAlignment, Page, PageElem, Paper, Parity, PlaceElem, Point, Ratio, Region, - Regions, Rel, Sides, Size, Spacing, VAlignment, VElem, + OuterVAlignment, Page, PageElem, PagebreakElem, Paper, Parity, PlaceElem, Point, + Ratio, Region, Regions, Rel, Sides, Size, Spacing, VAlignment, VElem, }; -use crate::model::{Document, Numbering}; -use crate::model::{FootnoteElem, FootnoteEntry, ParElem}; -use crate::realize::StyleVec; -use crate::realize::{realize_flow, realize_root, Arenas}; +use crate::model::{Document, FootnoteElem, FootnoteEntry, Numbering, ParElem}; +use crate::realize::{first_span, realize_root, realizer_container, Arenas, Pair}; +use crate::syntax::Span; use crate::text::TextElem; use crate::utils::{NonZeroExt, Numeric}; +use crate::visualize::Paint; use crate::World; +/// An item in page layout. +enum PageItem<'a> { + /// A page run containing content. All runs will be layouted in parallel. + Run(&'a [Pair<'a>], StyleChain<'a>, Locator<'a>), + /// Tags in between pages. These will be preprended to the first start of + /// the next page, or appended at the very end of the final page if there is + /// no next page. + Tags(&'a [Pair<'a>]), + /// An instruction to possibly add a page to bring the page number parity to + /// the desired state. Can only be done at the end, sequentially, because it + /// requires knowledge of the concrete page number. + Parity(Parity, StyleChain<'a>, Locator<'a>), +} + +/// A mostly finished layout for one page. Needs only knowledge of its exact +/// page number to be finalized into a `Page`. (Because the margins can depend +/// on the page number.) +#[derive(Clone)] +struct LayoutedPage { + inner: Frame, + margin: Sides<Abs>, + binding: Binding, + two_sided: bool, + header: Option<Frame>, + footer: Option<Frame>, + background: Option<Frame>, + foreground: Option<Frame>, + fill: Smart<Option<Paint>>, + numbering: Option<Numbering>, +} + /// Layout content into a document. /// /// This first performs root-level realization and then lays out the resulting @@ -74,74 +104,297 @@ fn layout_document_impl( route: Route::extend(route).unnested(), }; + // Mark the external styles as "outside" so that they are valid at the page + // level. + let styles = styles.to_map().outside(); + let styles = StyleChain::new(&styles); + let arenas = Arenas::default(); - let (children, styles, info) = + let (mut children, info) = realize_root(&mut engine, &mut locator, &arenas, content, styles)?; - let mut peekable = children.chain(&styles).peekable(); - let iter = std::iter::from_fn(|| { - let (child, styles) = peekable.next()?; - let extend_to = peekable - .peek() - .and_then(|(next, _)| *next.to_packed::<PageElem>()?.clear_to()?); - let locator = locator.next(&child.span()); - Some((child, styles, extend_to, locator)) - }); + let pages = layout_pages(&mut engine, &mut children, locator, styles)?; - let layouts = - engine.parallelize(iter, |engine, (child, styles, extend_to, locator)| { - if let Some(page) = child.to_packed::<PageElem>() { - layout_page_run(engine, page, locator, styles, extend_to) - } else { - bail!(child.span(), "expected page element"); + Ok(Document { pages, info, introspector: Introspector::default() }) +} + +/// Layouts the document's pages. +fn layout_pages<'a>( + engine: &mut Engine, + children: &'a mut [Pair<'a>], + locator: SplitLocator<'a>, + styles: StyleChain<'a>, +) -> SourceResult<Vec<Page>> { + // Slice up the children into logical parts. + let items = collect_page_items(children, locator, styles); + + // Layout the page runs in parallel. + let mut runs = engine.parallelize( + items.iter().filter_map(|item| match item { + PageItem::Run(children, initial, locator) => { + Some((children, initial, locator.relayout())) } - }); + _ => None, + }), + |engine, (children, initial, locator)| { + layout_page_run(engine, children, locator, *initial) + }, + ); + + let mut pages = vec![]; + let mut tags = vec![]; + let mut counter = ManualPageCounter::new(); + + // Collect and finalize the runs, handling things like page parity and tags + // between pages. + for item in &items { + match item { + PageItem::Run(..) => { + let layouted = runs.next().unwrap()?; + for layouted in layouted { + let page = finalize_page(engine, &mut counter, &mut tags, layouted)?; + pages.push(page); + } + } + PageItem::Parity(parity, initial, locator) => { + if !parity.matches(pages.len()) { + continue; + } + + let layouted = layout_blank_page(engine, locator.relayout(), *initial)?; + let page = finalize_page(engine, &mut counter, &mut tags, layouted)?; + pages.push(page); + } + PageItem::Tags(items) => { + tags.extend( + items + .iter() + .filter_map(|(c, _)| c.to_packed::<TagElem>()) + .map(|elem| elem.tag.clone()), + ); + } + } + } - let mut page_counter = ManualPageCounter::new(); - let mut pages = Vec::with_capacity(children.len()); - for result in layouts { - let layout = result?; - pages.extend(finalize_page_run(&mut engine, layout, &mut page_counter)?); + // Add the remaining tags to the very end of the last page. + if !tags.is_empty() { + let last = pages.last_mut().unwrap(); + let pos = Point::with_y(last.frame.height()); + last.frame + .push_multiple(tags.into_iter().map(|tag| (pos, FrameItem::Tag(tag)))); } - Ok(Document { pages, info, introspector: Introspector::default() }) + Ok(pages) } -/// A prepared layout of a page run that can be finalized with access to the -/// page counter. -struct PageRunLayout<'a> { - page: &'a Packed<PageElem>, - locator: SplitLocator<'a>, - styles: StyleChain<'a>, - extend_to: Option<Parity>, - area: Size, - margin: Sides<Abs>, - two_sided: bool, - frames: Vec<Frame>, +/// Slices up the children into logical parts, processing styles and handling +/// things like tags and weak pagebreaks. +fn collect_page_items<'a>( + mut children: &'a mut [Pair<'a>], + mut locator: SplitLocator<'a>, + mut initial: StyleChain<'a>, +) -> Vec<PageItem<'a>> { + // The collected page-level items. + let mut items: Vec<PageItem<'a>> = vec![]; + // When this is true, an empty page should be added to `pages` at the end. + let mut staged_empty_page = true; + + // The `children` are a flat list of flow-level items and pagebreaks. This + // loops splits it up into pagebreaks and consecutive slices of + // non-pagebreaks. From these pieces, we build page items that we can then + // layout in parallel. + while let Some(&(elem, styles)) = children.first() { + if let Some(pagebreak) = elem.to_packed::<PagebreakElem>() { + // Add a blank page if we encounter a strong pagebreak and there was + // a staged empty page. + let strong = !pagebreak.weak(styles); + if strong && staged_empty_page { + let locator = locator.next(&elem.span()); + items.push(PageItem::Run(&[], initial, locator)); + } + + // Add an instruction to adjust the page parity if requested. + if let Some(parity) = pagebreak.to(styles) { + let locator = locator.next(&elem.span()); + items.push(PageItem::Parity(parity, styles, locator)); + } + + // The initial styles for the next page are ours unless this is a + // "boundary" pagebreak. Such a pagebreak is generated at the end of + // the scope of a page set rule to ensure a page boundary. It's + // styles correspond to the styles _before_ the page set rule, so we + // don't want to apply it to a potential empty page. + if !pagebreak.boundary(styles) { + initial = styles; + } + + // Stage an empty page after a strong pagebreak. + staged_empty_page |= strong; + + // Advance to the next child. + children = &mut children[1..]; + } else { + // Find the end of the consecutive non-pagebreak run. + let end = + children.iter().take_while(|(c, _)| !c.is::<PagebreakElem>()).count(); + + // Migrate start tags without accompanying end tags from before a + // pagebreak to after it. + let end = migrate_unterminated_tags(children, end); + if end == 0 { + continue; + } + + // Advance to the rest of the children. + let (group, rest) = children.split_at_mut(end); + children = rest; + + // If all that is left now are tags, then we don't want to add a + // page just for them (since no group would have been detected in a + // tagless layout and tags should never affect the layout). For this + // reason, we remember them in a `PageItem::Tags` and later insert + // them at the _very start_ of the next page, even before the + // header. + // + // We don't do this if all that's left is end boundary pagebreaks + // and if an empty page is still staged, since then we can just + // conceptually replace that final page with us. + if group.iter().all(|(c, _)| c.is::<TagElem>()) + && !(staged_empty_page + && children.iter().all(|&(c, s)| { + c.to_packed::<PagebreakElem>().is_some_and(|c| c.boundary(s)) + })) + { + items.push(PageItem::Tags(group)); + continue; + } + + // Record a page run and then disregard a staged empty page because + // we have real content now. + let locator = locator.next(&elem.span()); + items.push(PageItem::Run(group, initial, locator)); + staged_empty_page = false; + } + } + + // Flush a staged empty page. + if staged_empty_page { + items.push(PageItem::Run(&[], initial, locator.next(&()))); + } + + items } -/// A document can consist of multiple `PageElem`s, one per run of pages -/// with equal properties (not one per actual output page!). The `number` is -/// the physical page number of the first page of this run. It is mutated -/// while we post-process the pages in this function. This function returns -/// a fragment consisting of multiple frames, one per output page of this -/// page run. -#[typst_macros::time(name = "pages", span = page.span())] -fn layout_page_run<'a>( +/// Migrates trailing start tags without accompanying end tags tags from before +/// a pagebreak to after it. Returns the position right after the last +/// non-migrated tag. +/// +/// This is important because we want the positions of introspectible elements +/// that technically started before a pagebreak, but have no visible content +/// yet, to be after the pagebreak. A typical case where this happens is `show +/// heading: it => pagebreak() + it`. +fn migrate_unterminated_tags(children: &mut [Pair], mid: usize) -> usize { + // Compute the range from before the first trailing tag to after the last + // following pagebreak. + let (before, after) = children.split_at(mid); + let start = mid - before.iter().rev().take_while(|&(c, _)| c.is::<TagElem>()).count(); + let end = mid + after.iter().take_while(|&(c, _)| c.is::<PagebreakElem>()).count(); + + // Determine the set of tag locations which we won't migrate (because they + // are terminated). + let excluded: HashSet<_> = children[start..mid] + .iter() + .filter_map(|(c, _)| c.to_packed::<TagElem>()) + .filter(|elem| elem.tag.kind() == TagKind::End) + .map(|elem| elem.tag.location()) + .collect(); + + // A key function that partitions the area of interest into three groups: + // Excluded tags (-1) | Pagebreaks (0) | Migrated tags (1). + let key = |(c, _): &Pair| match c.to_packed::<TagElem>() { + Some(elem) => { + if excluded.contains(&elem.tag.location()) { + -1 + } else { + 1 + } + } + None => 0, + }; + + // Partition the children using a *stable* sort. While it would be possible + // to write a more efficient direct algorithm for this, the sort version is + // less likely to have bugs and this is absolutely not on a hot path. + children[start..end].sort_by_key(key); + + // Compute the new end index, right before the pagebreaks. + start + children[start..end].iter().take_while(|pair| key(pair) == -1).count() +} + +/// Layout a page run with uniform properties. +#[typst_macros::time(name = "page run")] +fn layout_page_run( engine: &mut Engine, - page: &'a Packed<PageElem>, - locator: Locator<'a>, - styles: StyleChain<'a>, - extend_to: Option<Parity>, -) -> SourceResult<PageRunLayout<'a>> { - let mut locator = locator.split(); + children: &[Pair], + locator: Locator, + initial: StyleChain, +) -> SourceResult<Vec<LayoutedPage>> { + layout_page_run_impl( + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + children, + locator.track(), + initial, + ) +} + +/// Layout a single page suitable for parity adjustment. +fn layout_blank_page( + engine: &mut Engine, + locator: Locator, + initial: StyleChain, +) -> SourceResult<LayoutedPage> { + let layouted = layout_page_run(engine, &[], locator, initial)?; + Ok(layouted.into_iter().next().unwrap()) +} + +/// The internal implementation of `layout_page_run`. +#[comemo::memoize] +#[allow(clippy::too_many_arguments)] +fn layout_page_run_impl( + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + traced: Tracked<Traced>, + sink: TrackedMut<Sink>, + route: Tracked<Route>, + children: &[Pair], + locator: Tracked<Locator>, + initial: StyleChain, +) -> SourceResult<Vec<LayoutedPage>> { + let link = LocatorLink::new(locator); + let mut locator = Locator::link(&link).split(); + let mut engine = Engine { + world, + introspector, + traced, + sink, + route: Route::extend(route), + }; + + // 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. - let width = page.width(styles).unwrap_or(Abs::inf()); - let height = page.height(styles).unwrap_or(Abs::inf()); + let width = PageElem::width_in(styles).unwrap_or(Abs::inf()); + let height = PageElem::height_in(styles).unwrap_or(Abs::inf()); let mut size = Size::new(width, height); - if page.flipped(styles) { + if PageElem::flipped_in(styles) { std::mem::swap(&mut size.x, &mut size.y); } @@ -152,7 +405,7 @@ fn layout_page_run<'a>( // Determine the margins. let default = Rel::<Length>::from((2.5 / 21.0) * min); - let margin = page.margin(styles); + let margin = PageElem::margin_in(styles); let two_sided = margin.two_sided.unwrap_or(false); let margin = margin .sides @@ -165,72 +418,18 @@ fn layout_page_run<'a>( let mut regions = Regions::repeat(area, area.map(Abs::is_finite)); regions.root = true; - // Layout the child. - let columns = page.columns(styles); - let fragment = if columns.get() > 1 { - layout_fragment_with_columns( - engine, - &page.body, - locator.next(&page.span()), - styles, - regions, - columns, - ColumnsElem::gutter_in(styles), - )? - } else { - layout_fragment(engine, &page.body, locator.next(&page.span()), styles, regions)? - }; - - Ok(PageRunLayout { - page, - locator, - styles, - extend_to, - area, - margin, - two_sided, - frames: fragment.into_frames(), - }) -} - -/// Finalize the layout with access to the next page counter. -#[typst_macros::time(name = "finalize pages", span = page.span())] -fn finalize_page_run( - engine: &mut Engine, - PageRunLayout { - page, - mut locator, - styles, - extend_to, - area, - margin, - two_sided, - mut frames, - }: PageRunLayout<'_>, - page_counter: &mut ManualPageCounter, -) -> SourceResult<Vec<Page>> { - // Align the child to the pagebreak's parity. - // Check for page count after adding the pending frames - if extend_to.is_some_and(|p| !p.matches(page_counter.physical().get() + frames.len())) - { - // Insert empty page after the current pages. - let size = area.map(Abs::is_finite).select(area, Size::zero()); - frames.push(Frame::hard(size)); - } - - let fill = page.fill(styles); - let foreground = page.foreground(styles); - let background = page.background(styles); - let header_ascent = page.header_ascent(styles); - let footer_descent = page.footer_descent(styles); - let numbering = page.numbering(styles); - let number_align = page.number_align(styles); + let fill = PageElem::fill_in(styles); + let foreground = PageElem::foreground_in(styles); + let background = PageElem::background_in(styles); + let header_ascent = PageElem::header_ascent_in(styles).relative_to(margin.top); + let footer_descent = PageElem::footer_descent_in(styles).relative_to(margin.bottom); + let numbering = PageElem::numbering_in(styles); + let number_align = PageElem::number_align_in(styles); let binding = - page.binding(styles) - .unwrap_or_else(|| match TextElem::dir_in(styles) { - Dir::LTR => Binding::Left, - _ => Binding::Right, - }); + PageElem::binding_in(styles).unwrap_or_else(|| match TextElem::dir_in(styles) { + Dir::LTR => Binding::Left, + _ => Binding::Right, + }); // Construct the numbering (for header or footer). let numbering_marginal = numbering.as_ref().map(|numbering| { @@ -245,7 +444,7 @@ fn finalize_page_run( both, ) .pack() - .spanned(page.span()); + .spanned(span); // We interpret the Y alignment as selecting header or footer // and then ignore it for aligning the actual number. @@ -256,96 +455,180 @@ fn finalize_page_run( counter }); - let header = page.header(styles); - let footer = page.footer(styles); + let header = PageElem::header_in(styles); + let footer = PageElem::footer_in(styles); let (header, footer) = if matches!(number_align.y(), Some(OuterVAlignment::Top)) { (header.as_ref().unwrap_or(&numbering_marginal), footer.as_ref().unwrap_or(&None)) } else { (header.as_ref().unwrap_or(&None), footer.as_ref().unwrap_or(&numbering_marginal)) }; - // Post-process pages. - let mut pages = Vec::with_capacity(frames.len()); - for mut frame in frames { - // The padded width of the page's content without margins. - let pw = frame.width(); - - // If two sided, left becomes inside and right becomes outside. - // Thus, for left-bound pages, we want to swap on even pages and - // for right-bound pages, we want to swap on odd pages. - let mut margin = margin; - if two_sided && binding.swap(page_counter.physical()) { - std::mem::swap(&mut margin.left, &mut margin.right); - } - - // Realize margins. - frame.set_size(frame.size() + margin.sum_by_axis()); - frame.translate(Point::new(margin.left, margin.top)); - - // The page size with margins. - let size = frame.size(); - - // Realize overlays. - for marginal in [header, footer, background, foreground] { - let Some(content) = marginal.as_ref() else { continue }; - - let (pos, area, align); - if ptr::eq(marginal, header) { - let ascent = header_ascent.relative_to(margin.top); - pos = Point::with_x(margin.left); - area = Size::new(pw, margin.top - ascent); - align = Alignment::BOTTOM; - } else if ptr::eq(marginal, footer) { - let descent = footer_descent.relative_to(margin.bottom); - pos = Point::new(margin.left, size.y - margin.bottom + descent); - area = Size::new(pw, margin.bottom - descent); - align = Alignment::TOP; - } else { - pos = Point::zero(); - area = size; - align = HAlignment::Center + VAlignment::Horizon; - }; - - let aligned = content.clone().styled(AlignElem::set_alignment(align)); - let sub = layout_frame( - engine, - &aligned, - locator.next(&content.span()), - styles, - Region::new(area, Axes::splat(true)), - )?; - - if ptr::eq(marginal, header) || ptr::eq(marginal, background) { - frame.prepend_frame(pos, sub); - } else { - frame.push_frame(pos, sub); - } - } + // Layout the children. + let fragment = FlowLayouter::new( + &mut engine, + children, + locator.next(&span).split(), + styles, + regions, + PageElem::columns_in(styles), + ColumnsElem::gutter_in(styles), + span, + &mut vec![], + ) + .layout(regions)?; + + // Layouts a single marginal. + let mut layout_marginal = |content: &Option<Content>, area, align| { + let Some(content) = content else { return Ok(None) }; + let aligned = content.clone().styled(AlignElem::set_alignment(align)); + layout_frame( + &mut engine, + &aligned, + locator.next(&content.span()), + styles, + Region::new(area, Axes::splat(true)), + ) + .map(Some) + }; - page_counter.visit(engine, &frame)?; - pages.push(Page { - frame, + // Layout marginals. + let mut layouted = Vec::with_capacity(fragment.len()); + for inner in fragment { + let header_size = Size::new(inner.width(), margin.top - header_ascent); + let footer_size = Size::new(inner.width(), margin.bottom - footer_descent); + let full_size = inner.size() + margin.sum_by_axis(); + let mid = HAlignment::Center + VAlignment::Horizon; + layouted.push(LayoutedPage { + inner, fill: fill.clone(), numbering: numbering.clone(), - number: page_counter.logical(), + header: layout_marginal(header, header_size, Alignment::BOTTOM)?, + footer: layout_marginal(footer, footer_size, Alignment::TOP)?, + background: layout_marginal(background, full_size, mid)?, + foreground: layout_marginal(foreground, full_size, mid)?, + margin, + binding, + two_sided, }); - - page_counter.step(); } - Ok(pages) + Ok(layouted) } -/// Layout content into a single region. -pub fn layout_frame( +/// Determines the styles used for a page run itself and page-level content like +/// marginals and footnotes. +/// +/// As a base, we collect the styles that are shared by all elements on the page +/// run. As a fallback if there are no elements, we use the styles active at the +/// pagebreak that introduced the page (at the very start, we use the default +/// styles). Then, to produce our page styles, we filter this list of styles +/// according to a few rules: +/// +/// - Other styles are only kept if they are `outside && (initial || liftable)`. +/// - "Outside" means they were not produced within a show rule or that the +/// show rule "broke free" to the page level by emitting page styles. +/// - "Initial" means they were active at the pagebreak that introduced the +/// page. Since these are intuitively already active, they should be kept even +/// if not liftable. (E.g. `text(red, page(..)`) makes the footer red.) +/// - "Liftable" means they can be lifted to the page-level even though they +/// weren't yet active at the very beginning. Set rule styles are liftable as +/// opposed to direct constructor calls: +/// - For `set page(..); set text(red)` the red text is kept even though it +/// comes after the weak pagebreak from set page. +/// - For `set page(..); text(red)[..]` the red isn't kept because the +/// constructor styles are not liftable. +fn determine_page_styles(children: &[Pair], initial: StyleChain) -> Styles { + // Determine the shared styles (excluding tags). + let tagless = children.iter().filter(|(c, _)| !c.is::<TagElem>()).map(|&(_, s)| s); + let base = StyleChain::trunk(tagless).unwrap_or(initial).to_map(); + + // Determine the initial styles that are also shared by everything. We can't + // use `StyleChain::trunk` because it currently doesn't deal with partially + // shared links (where a subslice matches). + let trunk_len = initial + .to_map() + .as_slice() + .iter() + .zip(base.as_slice()) + .take_while(|&(a, b)| a == b) + .count(); + + // Filter the base styles according to our rules. + base.into_iter() + .enumerate() + .filter(|(i, style)| { + let initial = *i < trunk_len; + style.outside() && (initial || style.liftable()) + }) + .map(|(_, style)| style) + .collect() +} + +/// Piece together the inner page frame and the marginals. We can only do this +/// at the very end because inside/outside margins require knowledge of the +/// physical page number, which is unknown during parallel layout. +fn finalize_page( engine: &mut Engine, - content: &Content, - locator: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - layout_fragment(engine, content, locator, styles, region.into()) - .map(Fragment::into_frame) + counter: &mut ManualPageCounter, + tags: &mut Vec<Tag>, + LayoutedPage { + inner, + mut margin, + binding, + two_sided, + header, + footer, + background, + foreground, + fill, + numbering, + }: LayoutedPage, +) -> SourceResult<Page> { + // If two sided, left becomes inside and right becomes outside. + // Thus, for left-bound pages, we want to swap on even pages and + // for right-bound pages, we want to swap on odd pages. + if two_sided && binding.swap(counter.physical()) { + std::mem::swap(&mut margin.left, &mut margin.right); + } + + // Create a frame for the full page. + let mut frame = Frame::hard(inner.size() + margin.sum_by_axis()); + + // Add tags. + for tag in tags.drain(..) { + frame.push(Point::zero(), FrameItem::Tag(tag)); + } + + // Add the "before" marginals. The order in which we push things here is + // important as it affects the relative ordering of introspectible elements + // and thus how counters resolve. + if let Some(background) = background { + frame.push_frame(Point::zero(), background); + } + if let Some(header) = header { + frame.push_frame(Point::with_x(margin.left), header); + } + + // Add the inner contents. + frame.push_frame(Point::new(margin.left, margin.top), inner); + + // Add the "after" marginals. + if let Some(footer) = footer { + let y = frame.height() - footer.height(); + frame.push_frame(Point::new(margin.left, y), footer); + } + if let Some(foreground) = foreground { + frame.push_frame(Point::zero(), foreground); + } + + // Apply counter updates from within the page to the manual page counter. + counter.visit(engine, &frame)?; + + // Get this page's number and then bump the counter for the next page. + let number = counter.logical(); + counter.step(); + + Ok(Page { frame, fill, numbering, number }) } /// Layout content into multiple regions. @@ -403,9 +686,21 @@ pub fn layout_fragment_with_columns( ) } +/// Layout content into a single region. +pub fn layout_frame( + engine: &mut Engine, + content: &Content, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + layout_fragment(engine, content, locator, styles, region.into()) + .map(Fragment::into_frame) +} + /// The internal implementation of [`layout_fragment`]. -#[allow(clippy::too_many_arguments)] #[comemo::memoize] +#[allow(clippy::too_many_arguments)] fn layout_fragment_impl( world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, @@ -414,7 +709,7 @@ fn layout_fragment_impl( route: Tracked<Route>, content: &Content, locator: Tracked<Locator>, - mut styles: StyleChain, + styles: StyleChain, regions: Regions, columns: NonZeroUsize, column_gutter: Rel<Abs>, @@ -435,71 +730,40 @@ fn layout_fragment_impl( hint: "try to reduce the amount of nesting in your layout", ); } - // Layout the content by first turning it into a `FlowElem` and then - // layouting that. - let arenas = Arenas::default(); // If we are in a `PageElem`, this might already be a realized flow. - let stored; - let flow = if let Some(flow) = content.to_packed::<FlowElem>() { - flow - } else { - (stored, styles) = - realize_flow(&mut engine, &mut locator, &arenas, content, styles)?; - &stored - }; + let arenas = Arenas::default(); + let children = + realizer_container(&mut engine, &mut locator, &arenas, content, styles)?; FlowLayouter::new( &mut engine, - flow, + &children, locator, - &styles, + styles, regions, columns, column_gutter, + content.span(), &mut vec![], ) .layout(regions) } -/// A collection of block-level layoutable elements. This is analogous to a -/// paragraph, which is a collection of inline-level layoutable elements. -/// -/// This element is responsible for layouting both the top-level content flow -/// and the contents of any containers. -#[elem(Debug, Construct)] -pub struct FlowElem { - /// The children that will be arranged into a flow. - #[internal] - #[variadic] - pub children: StyleVec, -} - -impl Construct for FlowElem { - fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { - bail!(args.span, "cannot be constructed manually"); - } -} - -impl Debug for FlowElem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Flow ")?; - self.children.fmt(f) - } -} - -/// Performs flow layout. +/// Layouts a collection of block-level elements. struct FlowLayouter<'a, 'e> { /// The engine. engine: &'a mut Engine<'e>, /// The children that will be arranged into a flow. - flow: &'a Packed<FlowElem>, + children: &'a [Pair<'a>], + /// A span to use for errors. + span: Span, /// Whether this is the root flow. root: bool, /// Provides unique locations to the flow's children. locator: SplitLocator<'a>, /// The shared styles. - styles: &'a StyleChain<'a>, + shared: StyleChain<'a>, /// The number of columns. columns: usize, /// The gutter between columns. @@ -526,6 +790,8 @@ struct FlowLayouter<'a, 'e> { has_footnotes: bool, /// Footnote configuration. footnote_config: FootnoteConfig, + /// Footnotes that we have already processed. + visited_footnotes: HashSet<Location>, /// Finished frames for previous regions. finished: Vec<Frame>, } @@ -602,12 +868,13 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { #[allow(clippy::too_many_arguments)] fn new( engine: &'a mut Engine<'e>, - flow: &'a Packed<FlowElem>, + children: &'a [Pair<'a>], locator: SplitLocator<'a>, - styles: &'a StyleChain<'a>, + shared: StyleChain<'a>, mut regions: Regions<'a>, columns: NonZeroUsize, column_gutter: Rel<Abs>, + span: Span, backlog: &'a mut Vec<Abs>, ) -> Self { // Separating the infinite space into infinite columns does not make @@ -650,7 +917,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { // passed all the way through the block & pad and reach the innermost // flow, so that things are properly bottom-aligned. let mut alone = false; - if let [child] = flow.children.elements() { + if let [(child, _)] = children { alone = child.is::<BlockElem>(); } @@ -666,10 +933,11 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { Self { engine, - flow, + children, + span, root, locator, - styles, + shared, columns, column_gutter, regions, @@ -681,17 +949,18 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { pending_floats: vec![], has_footnotes: false, footnote_config: FootnoteConfig { - separator: FootnoteEntry::separator_in(*styles), - clearance: FootnoteEntry::clearance_in(*styles), - gap: FootnoteEntry::gap_in(*styles), + separator: FootnoteEntry::separator_in(shared), + clearance: FootnoteEntry::clearance_in(shared), + gap: FootnoteEntry::gap_in(shared), }, + visited_footnotes: HashSet::new(), finished: vec![], } } /// Layout the flow. fn layout(mut self, regions: Regions) -> SourceResult<Fragment> { - for (child, styles) in self.flow.children.chain(self.styles) { + for &(child, styles) in self.children { if let Some(elem) = child.to_packed::<TagElem>() { self.handle_tag(elem); } else if let Some(elem) = child.to_packed::<VElem>() { @@ -873,7 +1142,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { for (i, mut frame) in fragment.into_iter().enumerate() { // Find footnotes in the frame. if self.root { - collect_footnotes(&mut notes, &frame); + self.collect_footnotes(&mut notes, &frame); } if i > 0 { @@ -966,7 +1235,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { self.regions.size.y -= height; if self.root && movable { let mut notes = Vec::new(); - collect_footnotes(&mut notes, frame); + self.collect_footnotes(&mut notes, frame); self.items.push(item); // When we are already in_last, we can directly force the @@ -1024,7 +1293,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { // Find footnotes in the frame. if self.root { let mut notes = vec![]; - collect_footnotes(&mut notes, frame); + self.collect_footnotes(&mut notes, frame); self.try_handle_footnotes(notes)?; } } @@ -1143,10 +1412,10 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { } if !self.regions.size.x.is_finite() && self.expand.x { - bail!(self.flow.span(), "cannot expand into infinite width"); + bail!(self.span, "cannot expand into infinite width"); } if !self.regions.size.y.is_finite() && self.expand.y { - bail!(self.flow.span(), "cannot expand into infinite height"); + bail!(self.span, "cannot expand into infinite height"); } let mut output = Frame::soft(size); @@ -1255,7 +1524,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { } // Stitch together the column for each region. - let dir = TextElem::dir_in(*self.styles); + let dir = TextElem::dir_in(self.shared); let total = (self.finished.len() as f32 / self.columns as f32).ceil() as usize; let mut collected = vec![]; @@ -1342,7 +1611,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { self.engine, &FootnoteEntry::new(notes[k].clone()).pack(), Locator::synthesize(notes[k].location().unwrap()), - *self.styles, + self.shared, self.regions.with_root(false), )? .into_frames(); @@ -1363,7 +1632,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { let prev = notes.len(); for (i, frame) in frames.into_iter().enumerate() { - collect_footnotes(notes, &frame); + self.collect_footnotes(notes, &frame); if i > 0 { self.finish_region(false)?; self.layout_footnote_separator()?; @@ -1394,7 +1663,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { // FIXME: Shouldn't use `root()` here. let mut frame = - layout_frame(self.engine, separator, Locator::root(), *self.styles, pod)?; + layout_frame(self.engine, separator, Locator::root(), self.shared, pod)?; frame.size_mut().y += self.footnote_config.clearance; frame.translate(Point::with_y(self.footnote_config.clearance)); @@ -1404,22 +1673,26 @@ impl<'a, 'e> FlowLayouter<'a, 'e> { Ok(()) } -} -/// Collect all footnotes in a frame. -fn collect_footnotes(notes: &mut Vec<Packed<FootnoteElem>>, frame: &Frame) { - for (_, item) in frame.items() { - match item { - FrameItem::Group(group) => collect_footnotes(notes, &group.frame), - FrameItem::Tag(tag) - if !notes.iter().any(|note| note.location() == tag.elem.location()) => - { - let Some(footnote) = tag.elem.to_packed::<FootnoteElem>() else { - continue; - }; - notes.push(footnote.clone()); + /// Collect all footnotes in a frame. + fn collect_footnotes( + &mut self, + notes: &mut Vec<Packed<FootnoteElem>>, + frame: &Frame, + ) { + for (_, item) in frame.items() { + match item { + FrameItem::Group(group) => self.collect_footnotes(notes, &group.frame), + FrameItem::Tag(tag) => { + let Some(footnote) = tag.elem().to_packed::<FootnoteElem>() else { + continue; + }; + if self.visited_footnotes.insert(tag.location()) { + notes.push(footnote.clone()); + } + } + _ => {} } - _ => {} } } } diff --git a/crates/typst/src/layout/inline/collect.rs b/crates/typst/src/layout/inline/collect.rs index 5021dc55..53c684d1 100644 --- a/crates/typst/src/layout/inline/collect.rs +++ b/crates/typst/src/layout/inline/collect.rs @@ -125,7 +125,7 @@ pub fn collect<'a>( consecutive: bool, ) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); - let mut iter = children.chain(styles).peekable(); + let mut iter = children.iter(styles).peekable(); let mut locator = locator.split(); let outer_dir = TextElem::dir_in(*styles); diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index 44b6ee3c..192b37e9 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -18,11 +18,10 @@ use self::shaping::{ }; use crate::diag::SourceResult; use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::StyleChain; +use crate::foundations::{StyleChain, StyleVec}; use crate::introspection::{Introspector, Locator, LocatorLink}; use crate::layout::{Fragment, Size}; use crate::model::ParElem; -use crate::realize::StyleVec; use crate::World; /// Range of a substring of text. diff --git a/crates/typst/src/layout/page.rs b/crates/typst/src/layout/page.rs index f52f59e9..3d94ab79 100644 --- a/crates/typst/src/layout/page.rs +++ b/crates/typst/src/layout/page.rs @@ -8,15 +8,16 @@ use comemo::Track; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, AutoValue, Cast, Content, Context, Dict, Fold, Func, Smart, StyleChain, - Value, + cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func, + NativeElement, Packed, Set, Smart, StyleChain, Value, }; use crate::layout::{ - Abs, Alignment, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel, Sides, - SpecificAlignment, + Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel, + Sides, SpecificAlignment, }; use crate::model::Numbering; -use crate::utils::{NonZeroExt, Scalar}; +use crate::realize::{Behave, Behaviour}; +use crate::utils::{singleton, NonZeroExt, Scalar}; use crate::visualize::{Color, Paint}; /// Layouts its child onto one or multiple pages. @@ -38,11 +39,12 @@ use crate::visualize::{Color, Paint}; /// /// There you go, US friends! /// ``` -#[elem] +#[elem(Construct)] pub struct PageElem { /// A standard paper size to set width and height. #[external] #[default(Paper::A4)] + #[ghost] pub paper: Paper, /// The width of the page. @@ -64,6 +66,7 @@ pub struct PageElem { .or_else(|| paper.map(|paper| Smart::Custom(paper.width().into()))) )] #[default(Smart::Custom(Paper::A4.width().into()))] + #[ghost] pub width: Smart<Length>, /// The height of the page. @@ -78,6 +81,7 @@ pub struct PageElem { .or_else(|| paper.map(|paper| Smart::Custom(paper.height().into()))) )] #[default(Smart::Custom(Paper::A4.height().into()))] + #[ghost] pub height: Smart<Length>, /// Whether the page is flipped into landscape orientation. @@ -99,6 +103,7 @@ pub struct PageElem { /// +1 555 555 5555 /// ``` #[default(false)] + #[ghost] pub flipped: bool, /// The page's margins. @@ -138,6 +143,7 @@ pub struct PageElem { /// ) /// ``` #[fold] + #[ghost] pub margin: Margin, /// On which side the pages will be bound. @@ -149,6 +155,7 @@ pub struct PageElem { /// /// This affects the meaning of the `inside` and `outside` options for /// margins. + #[ghost] pub binding: Smart<Binding>, /// How many columns the page has. @@ -169,6 +176,7 @@ pub struct PageElem { /// of a rapidly changing climate. /// ``` #[default(NonZeroUsize::ONE)] + #[ghost] pub columns: NonZeroUsize, /// The page's background fill. @@ -192,6 +200,7 @@ pub struct PageElem { /// *Dark mode enabled.* /// ``` #[borrowed] + #[ghost] pub fill: Smart<Option<Paint>>, /// How to [number]($numbering) the pages. @@ -209,6 +218,7 @@ pub struct PageElem { /// #lorem(48) /// ``` #[borrowed] + #[ghost] pub numbering: Option<Numbering>, /// The alignment of the page numbering. @@ -228,6 +238,7 @@ pub struct PageElem { /// #lorem(30) /// ``` #[default(SpecificAlignment::Both(HAlignment::Center, OuterVAlignment::Bottom))] + #[ghost] pub number_align: SpecificAlignment<HAlignment, OuterVAlignment>, /// The page's header. Fills the top margin of each page. @@ -251,11 +262,13 @@ pub struct PageElem { /// #lorem(19) /// ``` #[borrowed] + #[ghost] pub header: Smart<Option<Content>>, /// The amount the header is raised into the top margin. #[resolve] #[default(Ratio::new(0.3).into())] + #[ghost] pub header_ascent: Rel<Length>, /// The page's footer. Fills the bottom margin of each page. @@ -287,11 +300,13 @@ pub struct PageElem { /// #lorem(48) /// ``` #[borrowed] + #[ghost] pub footer: Smart<Option<Content>>, /// The amount the footer is lowered into the bottom margin. #[resolve] #[default(Ratio::new(0.3).into())] + #[ghost] pub footer_descent: Rel<Length>, /// Content in the page's background. @@ -311,6 +326,7 @@ pub struct PageElem { /// over the world (of typesetting). /// ``` #[borrowed] + #[ghost] pub background: Option<Content>, /// Content in the page's foreground. @@ -325,6 +341,7 @@ pub struct PageElem { /// not understand our approach... /// ``` #[borrowed] + #[ghost] pub foreground: Option<Content>, /// The contents of the page(s). @@ -332,13 +349,93 @@ pub struct PageElem { /// Multiple pages will be created if the content does not fit on a single /// page. A new page with the page properties prior to the function invocation /// will be created after the body has been typeset. + #[external] #[required] pub body: Content, +} + +impl Construct for PageElem { + fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> { + // The page constructor is special: It doesn't create a page element. + // Instead, it just ensures that the passed content lives in a separate + // page and styles it. + let styles = Self::set(engine, args)?; + let body = args.expect::<Content>("body")?; + Ok(Content::sequence([ + PagebreakElem::shared_weak().clone(), + // We put an effectless, invisible non-tag element on the page. + // This has two desirable consequences: + // - The page is kept even if the body is empty + // - The page doesn't inherit shared styles from the body + FlushElem::new().pack(), + body, + PagebreakElem::shared_boundary().clone(), + ]) + .styled_with_map(styles)) + } +} - /// Whether the page should be aligned to an even or odd page. +/// A manual page break. +/// +/// Must not be used inside any containers. +/// +/// # Example +/// ```example +/// The next page contains +/// more details on compound theory. +/// #pagebreak() +/// +/// == Compound Theory +/// In 1984, the first ... +/// ``` +#[elem(title = "Page Break", Behave)] +pub struct PagebreakElem { + /// If `{true}`, the page break is skipped if the current page is already + /// empty. + #[default(false)] + pub weak: bool, + + /// If given, ensures that the next page will be an even/odd page, with an + /// empty page in between if necessary. + /// + /// ```example + /// #set page(height: 30pt) + /// + /// First. + /// #pagebreak(to: "odd") + /// Third. + /// ``` + pub to: Option<Parity>, + + /// Whether this pagebreak designates an end boundary of a page run. This is + /// an even weaker version of pagebreak `weak` because it not only doesn't + /// force an empty page, but also doesn't force its initial styles onto a + /// staged empty page. #[internal] - #[synthesized] - pub clear_to: Option<Parity>, + #[parse(None)] + #[default(false)] + 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 { + singleton!(Content, PagebreakElem::new().with_weak(true).pack()) + } + + /// Get the globally shared boundary pagebreak element. + pub fn shared_boundary() -> &'static Content { + singleton!( + Content, + PagebreakElem::new().with_weak(true).with_boundary(true).pack() + ) + } } /// A finished page. @@ -598,39 +695,6 @@ impl PageRanges { } } -/// A manual page break. -/// -/// Must not be used inside any containers. -/// -/// # Example -/// ```example -/// The next page contains -/// more details on compound theory. -/// #pagebreak() -/// -/// == Compound Theory -/// In 1984, the first ... -/// ``` -#[elem(title = "Page Break")] -pub struct PagebreakElem { - /// If `{true}`, the page break is skipped if the current page is already - /// empty. - #[default(false)] - pub weak: bool, - - /// If given, ensures that the next page will be an even/odd page, with an - /// empty page in between if necessary. - /// - /// ```example - /// #set page(height: 30pt) - /// - /// First. - /// #pagebreak(to: "odd") - /// Third. - /// ``` - pub to: Option<Parity>, -} - /// Whether something should be even or odd. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum Parity { diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs index 51a1f5bf..75a13fef 100644 --- a/crates/typst/src/layout/place.rs +++ b/crates/typst/src/layout/place.rs @@ -177,7 +177,7 @@ pub struct FlushElem {} impl Behave for Packed<FlushElem> { fn behaviour(&self) -> Behaviour { - Behaviour::Invisible + Behaviour::Ignorant } } diff --git a/crates/typst/src/math/ctx.rs b/crates/typst/src/math/ctx.rs index 0eb97f9c..06e35257 100644 --- a/crates/typst/src/math/ctx.rs +++ b/crates/typst/src/math/ctx.rs @@ -11,14 +11,13 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{Content, Packed, StyleChain}; +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::math::{ scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment, LayoutMath, MathFragment, MathRun, MathSize, THICK, }; -use crate::realize::StyleVec; use crate::syntax::{is_newline, Span}; use crate::text::{ features, BottomEdge, BottomEdgeMetric, Font, TextElem, TextSize, TopEdge, diff --git a/crates/typst/src/math/fragment.rs b/crates/typst/src/math/fragment.rs index 2adf2d9d..959e4c8f 100644 --- a/crates/typst/src/math/fragment.rs +++ b/crates/typst/src/math/fragment.rs @@ -6,6 +6,7 @@ use ttf_parser::{GlyphId, Rect}; use unicode_math_class::MathClass; use crate::foundations::StyleChain; +use crate::introspection::Tag; use crate::layout::{ Abs, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment, }; @@ -26,6 +27,7 @@ pub enum MathFragment { Space(Abs), Linebreak, Align, + Tag(Tag), } impl MathFragment { @@ -74,6 +76,7 @@ impl MathFragment { pub fn is_ignorant(&self) -> bool { match self { Self::Frame(fragment) => fragment.ignorant, + Self::Tag(_) => true, _ => false, } } @@ -87,6 +90,7 @@ impl MathFragment { Self::Space(_) => MathClass::Space, Self::Linebreak => MathClass::Space, Self::Align => MathClass::Special, + Self::Tag(_) => MathClass::Special, } } @@ -172,6 +176,11 @@ impl MathFragment { Self::Glyph(glyph) => glyph.into_frame(), Self::Variant(variant) => variant.frame, Self::Frame(fragment) => fragment.frame, + Self::Tag(tag) => { + let mut frame = Frame::soft(Size::zero()); + frame.push(Point::zero(), FrameItem::Tag(tag)); + frame + } _ => Frame::soft(self.size()), } } diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs index 6cf22edb..383631a4 100644 --- a/crates/typst/src/math/mod.rs +++ b/crates/typst/src/math/mod.rs @@ -47,10 +47,9 @@ use crate::foundations::{ category, Category, Content, Module, Resolve, Scope, SequenceElem, StyleChain, StyledElem, }; -use crate::introspection::TagElem; -use crate::layout::{BoxElem, Frame, FrameItem, HElem, Point, Size, Spacing, VAlignment}; -use crate::realize::Behaviour; -use crate::realize::{process, BehavedBuilder}; +use crate::introspection::{TagElem, TagKind}; +use crate::layout::{BoxElem, HElem, Spacing, VAlignment}; +use crate::realize::{process, BehavedBuilder, Behaviour}; use crate::text::{LinebreakElem, SpaceElem, TextElem}; /// Typst has special [syntax]($syntax/#math) and library functions to typeset @@ -237,8 +236,17 @@ impl LayoutMath for Content { return elem.layout_math(ctx, styles); } - if let Some(realized) = process(ctx.engine, &mut ctx.locator, self, styles)? { - return realized.layout_math(ctx, styles); + if let Some((tag, realized)) = + process(ctx.engine, &mut ctx.locator, self, styles)? + { + 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))); + } + return Ok(()); } if self.is::<SequenceElem>() { @@ -302,9 +310,7 @@ impl LayoutMath for Content { } if let Some(elem) = self.to_packed::<TagElem>() { - let mut frame = Frame::soft(Size::zero()); - frame.push(Point::zero(), FrameItem::Tag(elem.tag.clone())); - ctx.push(FrameFragment::new(ctx, styles, frame).with_ignorant(true)); + ctx.push(MathFragment::Tag(elem.tag.clone())); return Ok(()); } @@ -321,10 +327,7 @@ impl LayoutMath for Content { ctx.push( FrameFragment::new(ctx, styles, frame) .with_spaced(true) - .with_ignorant(matches!( - self.behaviour(), - Behaviour::Invisible | Behaviour::Ignorant - )), + .with_ignorant(self.behaviour() == Behaviour::Ignorant), ); Ok(()) diff --git a/crates/typst/src/model/par.rs b/crates/typst/src/model/par.rs index 25e56332..7867c89d 100644 --- a/crates/typst/src/model/par.rs +++ b/crates/typst/src/model/par.rs @@ -3,10 +3,10 @@ use std::fmt::{self, Debug, Formatter}; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - elem, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, Unlabellable, + elem, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, StyleVec, + Unlabellable, }; use crate::layout::{Em, Length}; -use crate::realize::StyleVec; use crate::utils::singleton; /// Arranges text, spacing and inline-level elements into a paragraph. diff --git a/crates/typst/src/realize/arenas.rs b/crates/typst/src/realize/arenas.rs index 9c90091c..61b19858 100644 --- a/crates/typst/src/realize/arenas.rs +++ b/crates/typst/src/realize/arenas.rs @@ -1,12 +1,13 @@ use typed_arena::Arena; -use crate::foundations::{Content, StyleChain}; +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> { @@ -32,3 +33,9 @@ impl<'a> Store<'a> for StyleChain<'a> { 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 index 588e221f..15a0a284 100644 --- a/crates/typst/src/realize/behaviour.rs +++ b/crates/typst/src/realize/behaviour.rs @@ -1,10 +1,6 @@ //! Element interaction. -use std::fmt::{Debug, Formatter}; - -use ecow::EcoVec; - -use crate::foundations::{Content, StyleChain, Styles}; +use crate::foundations::{Content, StyleChain}; /// How an element interacts with other elements in a stream. #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -19,12 +15,8 @@ pub enum Behaviour { /// 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 layout extent and/or a visual - /// representation. + /// same effect on them as if it didn't exist. Ignorant, - /// An element that does not have any layout extent or visual - /// representation. - Invisible, } impl Behaviour { @@ -69,15 +61,6 @@ impl<'a> BehavedBuilder<'a> { self.buf.is_empty() } - /// Whether the builder has any proper (non-weak & visible) elements. - pub fn has_strong_elements(&self, last: bool) -> bool { - self.buf.iter().any(|(content, _)| { - let behaviour = content.behaviour(); - !matches!(behaviour, Behaviour::Weak(_) | Behaviour::Invisible) - || (last && behaviour == Behaviour::Invisible) - }) - } - /// Push an item into the builder. pub fn push(&mut self, content: &'a Content, styles: StyleChain<'a>) { let mut behaviour = content.behaviour(); @@ -112,7 +95,7 @@ impl<'a> BehavedBuilder<'a> { self.buf.remove(i); } } - Behaviour::Ignorant | Behaviour::Invisible => { + Behaviour::Ignorant => { behaviour = self.last; } } @@ -152,162 +135,3 @@ impl<'a> Default for BehavedBuilder<'a> { Self::new() } } - -/// A sequence of elements with associated styles. -#[derive(Clone, PartialEq, Hash)] -pub struct StyleVec { - /// The elements themselves. - elements: EcoVec<Content>, - /// A run-length encoded list of style lists. - /// - /// Each element is a (styles, count) pair. Any elements whose - /// style falls after the end of this list is considered to - /// have an empty style list. - styles: EcoVec<(Styles, usize)>, -} - -impl StyleVec { - /// Create a style vector from an unstyled vector content. - pub fn wrap(elements: EcoVec<Content>) -> Self { - Self { elements, styles: EcoVec::new() } - } - - /// Create a `StyleVec` from a list of content with style chains. - pub fn create<'a>(buf: &[(&Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) { - let (trunk, depth) = determine_style_trunk(buf); - - let mut elements = EcoVec::with_capacity(buf.len()); - let mut styles = EcoVec::<(Styles, usize)>::new(); - let mut last: Option<(StyleChain<'a>, usize)> = None; - - for &(element, chain) in buf { - elements.push(element.clone()); - - if let Some((prev, run)) = &mut last { - if chain == *prev { - *run += 1; - } else { - styles.push((prev.suffix(depth), *run)); - last = Some((chain, 1)); - } - } else { - last = Some((chain, 1)); - } - } - - if let Some((last, run)) = last { - let skippable = styles.is_empty() && last == trunk; - if !skippable { - styles.push((last.suffix(depth), run)); - } - } - - (StyleVec { elements, styles }, trunk) - } - - /// Whether there are no elements. - pub fn is_empty(&self) -> bool { - self.elements.is_empty() - } - - /// The number of elements. - pub fn len(&self) -> usize { - self.elements.len() - } - - /// The raw, unstyled elements. - pub fn elements(&self) -> &[Content] { - &self.elements - } - - /// Get a style property, but only if it is the same for all children of the - /// style vector. - pub fn shared_get<T: PartialEq>( - &self, - styles: StyleChain<'_>, - getter: fn(StyleChain) -> T, - ) -> Option<T> { - let value = getter(styles); - self.styles - .iter() - .all(|(local, _)| getter(styles.chain(local)) == value) - .then_some(value) - } - - /// Iterate over the contained content and style chains. - pub fn chain<'a>( - &'a self, - outer: &'a StyleChain<'_>, - ) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> { - self.iter().map(|(element, local)| (element, outer.chain(local))) - } - - /// Iterate over pairs of content and styles. - pub fn iter(&self) -> impl Iterator<Item = (&Content, &Styles)> { - static EMPTY: Styles = Styles::new(); - self.elements.iter().zip( - self.styles - .iter() - .flat_map(|(local, count)| std::iter::repeat(local).take(*count)) - .chain(std::iter::repeat(&EMPTY)), - ) - } - - /// Iterate over pairs of content and styles. - #[allow(clippy::should_implement_trait)] - pub fn into_iter(self) -> impl Iterator<Item = (Content, Styles)> { - self.elements.into_iter().zip( - self.styles - .into_iter() - .flat_map(|(local, count)| std::iter::repeat(local).take(count)) - .chain(std::iter::repeat(Styles::new())), - ) - } -} - -impl Debug for StyleVec { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - f.debug_list() - .entries(self.iter().map(|(element, local)| { - typst_utils::debug(|f| { - for style in local.iter() { - writeln!(f, "#{style:?}")?; - } - element.fmt(f) - }) - })) - .finish() - } -} - -/// Determine the shared trunk style chain. -fn determine_style_trunk<'a, T>(buf: &[(T, StyleChain<'a>)]) -> (StyleChain<'a>, usize) { - // Determine shared style depth and first span. - let mut trunk = match buf.first() { - Some(&(_, chain)) => chain, - None => Default::default(), - }; - - let mut depth = trunk.links().count(); - for (_, mut chain) in buf { - let len = chain.links().count(); - if len < depth { - for _ in 0..depth - len { - trunk.pop(); - } - depth = len; - } else if len > depth { - for _ in 0..len - depth { - chain.pop(); - } - } - - while depth > 0 && chain != trunk { - trunk.pop(); - chain.pop(); - depth -= 1; - } - } - - (trunk, depth) -} diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index fa7019e0..5f89e51d 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -1,10 +1,7 @@ //! Realization of content. //! -//! *Realization* is the process of applying show rules to produce -//! something that can be laid out directly. -//! -//! Currently, there are issues with the realization process, and -//! it is subject to changes in the future. +//! *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; @@ -13,7 +10,7 @@ mod process; use once_cell::unsync::Lazy; pub use self::arenas::Arenas; -pub use self::behaviour::{Behave, BehavedBuilder, Behaviour, StyleVec}; +pub use self::behaviour::{Behave, BehavedBuilder, Behaviour}; pub use self::process::process; use std::mem; @@ -21,12 +18,13 @@ use std::mem; use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; use crate::foundations::{ - Content, NativeElement, Packed, SequenceElem, Smart, StyleChain, StyledElem, Styles, + Content, ContextElem, NativeElement, Packed, SequenceElem, Smart, StyleChain, + StyleVec, StyledElem, Styles, }; -use crate::introspection::{SplitLocator, TagElem}; +use crate::introspection::{SplitLocator, TagElem, TagKind}; use crate::layout::{ - AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem, InlineElem, - PageElem, PagebreakElem, Parity, PlaceElem, VElem, + AlignElem, BlockElem, BoxElem, ColbreakElem, FlushElem, HElem, InlineElem, PageElem, + PagebreakElem, PlaceElem, VElem, }; use crate::math::{EquationElem, LayoutMath}; use crate::model::{ @@ -35,41 +33,42 @@ use crate::model::{ }; 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. -type Pair<'a> = (&'a Content, StyleChain<'a>); +pub type Pair<'a> = (&'a Content, StyleChain<'a>); -/// Realize at the root-level. -#[typst_macros::time(name = "realize root")] +/// Realize at the root level. +#[typst_macros::time(name = "realize")] pub fn realize_root<'a>( engine: &mut Engine<'a>, locator: &mut SplitLocator<'a>, arenas: &'a Arenas<'a>, content: &'a Content, styles: StyleChain<'a>, -) -> SourceResult<(StyleVec, StyleChain<'a>, DocumentInfo)> { +) -> SourceResult<(Vec<Pair<'a>>, DocumentInfo)> { let mut builder = Builder::new(engine, locator, arenas, true); builder.accept(content, styles)?; - builder.interrupt_page(Some(styles), true)?; - Ok(builder.doc.unwrap().finish()) + builder.interrupt_par()?; + Ok((builder.sink.finish(), builder.doc_info.unwrap())) } -/// Realize into a `FlowElem`, an element that is capable of block-level layout. -#[typst_macros::time(name = "realize flow")] -pub fn realize_flow<'a>( +/// Realize at the container level. +#[typst_macros::time(name = "realize")] +pub fn realizer_container<'a>( engine: &mut Engine<'a>, locator: &mut SplitLocator<'a>, arenas: &'a Arenas<'a>, content: &'a Content, styles: StyleChain<'a>, -) -> SourceResult<(Packed<FlowElem>, StyleChain<'a>)> { +) -> SourceResult<Vec<Pair<'a>>> { let mut builder = Builder::new(engine, locator, arenas, false); builder.accept(content, styles)?; builder.interrupt_par()?; - Ok(builder.flow.finish()) + Ok(builder.sink.finish()) } -/// Builds a document or a flow element from content. +/// Realizes content into a flat list of well-known, styled elements. struct Builder<'a, 'v> { /// The engine. engine: &'v mut Engine<'a>, @@ -77,34 +76,47 @@ struct Builder<'a, 'v> { locator: &'v mut SplitLocator<'a>, /// Scratch arenas for building. arenas: &'a Arenas<'a>, - /// The current document building state. - doc: Option<DocBuilder<'a>>, - /// The current flow building state. - flow: FlowBuilder<'a>, - /// The current paragraph building state. + + /// The output elements of well-known types collected by the builder. + sink: BehavedBuilder<'a>, + /// Document metadata we have collected from `set document` rules. If this + /// is `None`, we are in a container. + doc_info: Option<DocumentInfo>, + + /// A builder for a paragraph that might be under construction. par: ParBuilder<'a>, - /// The current list building state. + /// A builder for a list that might be under construction. list: ListBuilder<'a>, - /// The current citation grouping state. + /// 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>, - top: bool, + root: bool, ) -> Self { Self { engine, locator, arenas, - doc: top.then(DocBuilder::default), - flow: FlowBuilder::default(), + sink: BehavedBuilder::default(), + doc_info: root.then(DocumentInfo::default), par: ParBuilder::default(), list: ListBuilder::default(), cites: CiteGroupBuilder::default(), + outside: root, + last_was_par: false, } } @@ -123,7 +135,9 @@ impl<'a, 'v> Builder<'a, 'v> { // Styled elements and sequences can (at least currently) also have // labels, so this needs to happen before they are handled. - if let Some(realized) = process(self.engine, self.locator, content, styles)? { + if let Some((tag, realized)) = + process(self.engine, self.locator, content, styles)? + { self.engine.route.increase(); if !self.engine.route.within(Route::MAX_SHOW_RULE_DEPTH) { bail!( @@ -131,9 +145,23 @@ impl<'a, 'v> Builder<'a, 'v> { hint: "check whether the show rule matches its own output" ); } - let result = self.accept(self.arenas.store(realized), styles); + + 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 result; + return Ok(()); } if let Some(styled) = content.to_packed::<StyledElem>() { @@ -148,6 +176,7 @@ impl<'a, 'v> Builder<'a, 'v> { } // Try to merge `content` with an element under construction + // (cite group, list, or par). if self.cites.accept(content, styles) { return Ok(()); @@ -161,6 +190,7 @@ impl<'a, 'v> Builder<'a, 'v> { self.interrupt_list()?; + // Try again because it could be another kind of list. if self.list.accept(content, styles) { return Ok(()); } @@ -171,469 +201,385 @@ impl<'a, 'v> Builder<'a, 'v> { self.interrupt_par()?; - if self.flow.accept(self.arenas, content, styles) { - return Ok(()); - } - - let keep = content - .to_packed::<PagebreakElem>() - .is_some_and(|pagebreak| !pagebreak.weak(styles)); + self.save(content, styles) + } - self.interrupt_page(keep.then_some(styles), false)?; + /// 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 let Some(doc) = &mut self.doc { - if doc.accept(self.arenas, content, styles) { - return Ok(()); + 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()) + } + }; - if content.is::<PagebreakElem>() { - bail!(content.span(), "pagebreaks are not allowed inside of containers"); + 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<()> { - let local = &styled.styles; - let stored = self.arenas.store(styles); - let styles = stored.chain(local); - - if let Some(Some(span)) = local.interruption::<DocumentElem>() { - let Some(doc) = &mut self.doc else { + 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"); }; - doc.info.populate(local); + info.populate(&styled.styles); } - self.interrupt_style(local, None)?; - self.accept(&styled.child, styles)?; - self.interrupt_style(local, Some(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(()) } - fn interrupt_style( - &mut self, - local: &Styles, - outer: Option<StyleChain<'a>>, - ) -> SourceResult<()> { - if let Some(Some(span)) = local.interruption::<PageElem>() { - if self.doc.is_none() { - bail!(span, "page configuration is not allowed inside of containers"); - } - self.interrupt_page(outer, false)?; - } + /// 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()?; - } - if local.interruption::<ListElem>().is_some() + } 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 citation grouping and adds the resulting citation group to the builder. - fn interrupt_cites(&mut self) -> SourceResult<()> { - 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.pack()), styles)?; - for (content, styles) in staged { - self.accept(content, styles)?; - } - } - 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.items.is_empty() { - let staged = mem::take(&mut self.list.staged); - let (list, styles) = mem::take(&mut self.list).finish(); - self.accept(self.arenas.store(list), styles)?; - for (content, styles) in staged { - self.accept(content, styles)?; - } - } - Ok(()) - } - - /// Interrupts paragraph building and adds the resulting paragraph element to the builder. + /// 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() { - let (par, styles) = mem::take(&mut self.par).finish(); - self.accept(self.arenas.store(par.pack()), styles)?; + mem::take(&mut self.par).finish(self)?; } - Ok(()) } - /// Interrupts page building and adds the resulting page element to the builder. - fn interrupt_page( - &mut self, - styles: Option<StyleChain<'a>>, - last: bool, - ) -> SourceResult<()> { - 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 (flow, trunk) = mem::take(&mut self.flow).finish(); - let span = flow.span(); - let styles = if trunk == StyleChain::default() { - styles.unwrap_or_default() - } else { - trunk - }; - let page = PageElem::new(flow.pack()).pack().spanned(span); - self.accept(self.arenas.store(page), styles)?; + /// 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(()) } -} - -/// Builds a [document][DocumentElem] from pagebreaks and pages. -struct DocBuilder<'a> { - /// The page runs built so far. - pages: BehavedBuilder<'a>, - /// Whether to keep a following page even if it is empty. - keep_next: bool, - /// Whether the next page should be cleared to an even or odd number. - clear_next: Option<Parity>, - /// Details about the document. - info: DocumentInfo, -} - -impl<'a> DocBuilder<'a> { - /// Tries to accept a piece of content. - /// - /// Returns true if this content could be merged into the document. - /// If this function returns false, then the - /// content could not be merged, and document building should be - /// interrupted so that the content can be added elsewhere. - fn accept( - &mut self, - arenas: &'a Arenas<'a>, - content: &'a Content, - styles: StyleChain<'a>, - ) -> bool { - if let Some(pagebreak) = content.to_packed::<PagebreakElem>() { - self.keep_next = !pagebreak.weak(styles); - self.clear_next = pagebreak.to(styles); - return true; - } - - if let Some(page) = content.to_packed::<PageElem>() { - let elem = if let Some(clear_to) = self.clear_next.take() { - let mut page = page.clone(); - page.push_clear_to(Some(clear_to)); - arenas.store(page.pack()) - } else { - content - }; - - self.pages.push(elem, styles); - self.keep_next = false; - return true; - } - - false - } - - /// Turns this builder into the resulting page runs, along with - /// its [style chain][StyleChain]. - fn finish(self) -> (StyleVec, StyleChain<'a>, DocumentInfo) { - let buf = self.pages.finish(); - let (children, trunk) = StyleVec::create(&buf); - (children, trunk, self.info) - } -} -impl Default for DocBuilder<'_> { - fn default() -> Self { - Self { - pages: BehavedBuilder::new(), - keep_next: true, - clear_next: None, - info: DocumentInfo::default(), + /// 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 [flow][FlowElem] from flow content. +/// Builds a [paragraph][ParElem] from paragraph content. #[derive(Default)] -struct FlowBuilder<'a>(BehavedBuilder<'a>, bool); +struct ParBuilder<'a>(BehavedBuilder<'a>); -impl<'a> FlowBuilder<'a> { +impl<'a> ParBuilder<'a> { /// Tries to accept a piece of content. /// - /// Returns true if this content could be merged into the flow. - /// If this function returns false, then the - /// content could not be merged, and flow building should be - /// interrupted so that the content can be added elsewhere. - fn accept( - &mut self, - arenas: &'a Arenas<'a>, - content: &'a Content, - styles: StyleChain<'a>, - ) -> bool { - let last_was_par = self.1; - self.1 = false; - - if content.is::<ParbreakElem>() { - return true; - } - - if let Some(elem) = content.to_packed::<VElem>() { - if !elem.attach(styles) || last_was_par { - self.0.push(content, styles); - } - return true; - } - - if content.is::<ColbreakElem>() - || content.is::<TagElem>() - || content.is::<PlaceElem>() - || content.is::<FlushElem>() - { - self.0.push(content, styles); - return true; - } - - let par_spacing = Lazy::new(|| { - arenas.store(VElem::par_spacing(ParElem::spacing_in(styles).into()).pack()) - }); - - if let Some(elem) = content.to_packed::<BlockElem>() { - let above = match elem.above(styles) { - Smart::Auto => *par_spacing, - Smart::Custom(above) => arenas.store(VElem::block_spacing(above).pack()), - }; - - let below = match elem.below(styles) { - Smart::Auto => *par_spacing, - Smart::Custom(below) => arenas.store(VElem::block_spacing(below).pack()), - }; - - self.0.push(above, styles); - self.0.push(content, styles); - self.0.push(below, styles); - return true; - } - - if content.is::<ParElem>() { - self.0.push(*par_spacing, styles); + /// 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); - self.0.push(*par_spacing, styles); - self.1 = true; return true; } false } - /// Turns this builder into the resulting flow, along with - /// its [style chain][StyleChain]. - fn finish(self) -> (Packed<FlowElem>, StyleChain<'a>) { - let buf = self.0.finish(); - let span = determine_span(&buf); - let (children, trunk) = StyleVec::create(&buf); - (Packed::new(FlowElem::new(children)).spanned(span), trunk) - } -} - -/// 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 content.is::<TagElem>() { - if !self.0.is_empty() { - self.0.push(content, styles); - return true; - } - } else if content.is::<SpaceElem>() + /// 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>() - { - self.0.push(content, styles); - return true; - } + } - false + /// 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 paragraph, along with + /// Turns this builder into the resulting list, along with /// its [style chain][StyleChain]. - fn finish(self) -> (Packed<ParElem>, StyleChain<'a>) { + fn finish(self, builder: &mut Builder<'a, '_>) -> SourceResult<()> { let buf = self.0.finish(); - let span = determine_span(&buf); - let (children, trunk) = StyleVec::create(&buf); - (Packed::new(ParElem::new(children)).spanned(span), trunk) + 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. -struct ListBuilder<'a> { - /// The list items collected so far. - items: Vec<Pair<'a>>, - /// Trailing content for which it is unclear whether it is part of the list. - staged: Vec<Pair<'a>>, - /// Whether the list contains no paragraph breaks. - tight: bool, -} +/// 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. + /// 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.items.is_empty() - && (content.is::<SpaceElem>() || content.is::<ParbreakElem>()) - { - self.staged.push((content, styles)); - return true; - } - - if (content.is::<ListItem>() - || content.is::<EnumItem>() - || content.is::<TermItem>()) - && self - .items - .first() - .map_or(true, |(first, _)| first.func() == content.func()) + if (Self::is_primary(content) && self.is_compatible(content)) + || (!self.0.is_empty() && Self::is_inner(content)) { - self.items.push((content, styles)); - self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakElem>()); + 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) -> (Content, StyleChain<'a>) { - let span = determine_span(&self.items); - let (children, trunk) = StyleVec::create(&self.items); - - let mut iter = children.into_iter().peekable(); - let (first, _) = iter.peek().unwrap(); - let output = if first.is::<ListItem>() { + 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.into_packed::<ListItem>().unwrap().styled(local) + item.to_packed::<ListItem>().unwrap().clone().styled(local) }) .collect(); - ListElem::new(children).with_tight(self.tight).pack().spanned(span) + ListElem::new(children).with_tight(tight).pack() } else if first.is::<EnumItem>() { let children = iter .map(|(item, local)| { - item.into_packed::<EnumItem>().unwrap().styled(local) + item.to_packed::<EnumItem>().unwrap().clone().styled(local) }) .collect(); - EnumElem::new(children).with_tight(self.tight).pack().spanned(span) + EnumElem::new(children).with_tight(tight).pack() } else if first.is::<TermItem>() { let children = iter .map(|(item, local)| { - item.into_packed::<TermItem>().unwrap().styled(local) + item.to_packed::<TermItem>().unwrap().clone().styled(local) }) .collect(); - TermsElem::new(children).with_tight(self.tight).pack().spanned(span) + TermsElem::new(children).with_tight(tight).pack() } else { unreachable!() }; - (output, trunk) - } -} + // 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)?; + } -impl Default for ListBuilder<'_> { - fn default() -> Self { - Self { items: vec![], staged: vec![], tight: true } + Ok(()) } } /// Builds a [citation group][CiteGroup] from citations. #[derive(Default)] -struct CiteGroupBuilder<'a> { - /// The citations. - items: Vec<Packed<CiteElem>>, - /// Trailing content for which it is unclear whether it is part of the list. - staged: Vec<Pair<'a>>, - /// The styles. - styles: StyleChain<'a>, -} +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. + /// 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.items.is_empty() - && (content.is::<SpaceElem>() || content.is::<TagElem>()) - { - self.staged.push((content, styles)); - return true; - } - - if let Some(citation) = content.to_packed::<CiteElem>() { - if self.items.is_empty() { - self.styles = styles; - } - self.staged.retain(|(elem, _)| !elem.is::<SpaceElem>()); - self.items.push(citation.clone()); + 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) -> (Packed<CiteGroup>, StyleChain<'a>) { - let span = self.items.first().map(|cite| cite.span()).unwrap_or(Span::detached()); - (Packed::new(CiteGroup::new(self.items)).spanned(span), self.styles) + 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. -fn determine_span(buf: &[(&Content, StyleChain)]) -> Span { - let mut span = Span::detached(); - for &(content, _) in buf { - span = content.span(); - if !span.is_detached() { - break; - } - } - span +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 index 913ee8f0..83c2dd4d 100644 --- a/crates/typst/src/realize/process.rs +++ b/crates/typst/src/realize/process.rs @@ -8,7 +8,7 @@ use crate::foundations::{ Content, Context, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style, StyleChain, Styles, Synthesize, Transformation, }; -use crate::introspection::{Locatable, SplitLocator, Tag, TagElem}; +use crate::introspection::{Locatable, SplitLocator, Tag}; use crate::text::TextElem; use crate::utils::SmallBitSet; @@ -37,7 +37,7 @@ pub fn process( locator: &mut SplitLocator, target: &Content, styles: StyleChain, -) -> SourceResult<Option<Content>> { +) -> SourceResult<Option<(Option<Tag>, Content)>> { let Some(Verdict { prepared, mut map, step }) = verdict(engine, target, styles) else { return Ok(None); @@ -54,7 +54,7 @@ pub fn process( } // Apply a step, if there is one. - let mut output = match step { + 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 @@ -67,12 +67,7 @@ pub fn process( None => target, }; - // If necessary, add the tag generated in the preparation. - if let Some(tag) = tag { - output = tag + output; - } - - Ok(Some(output.styled_with_map(map))) + Ok(Some((tag, output.styled_with_map(map)))) } /// Inspects a target element and the current styles and determines how to @@ -106,7 +101,7 @@ fn verdict<'a>( let mut r = 0; for entry in styles.entries() { - let recipe = match entry { + let recipe = match &**entry { Style::Recipe(recipe) => recipe, Style::Property(_) => continue, Style::Revocation(index) => { @@ -124,7 +119,7 @@ fn verdict<'a>( // 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(_)))) + (recipe.transform(), matches!(recipe.selector(), Some(Selector::Regex(_)))) { // If this is a show-set for an unprepared element, we need to apply // it. @@ -137,8 +132,9 @@ fn verdict<'a>( // 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 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) { @@ -187,7 +183,7 @@ fn prepare( target: &mut Content, map: &mut Styles, styles: StyleChain, -) -> SourceResult<Option<Content>> { +) -> 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. @@ -226,7 +222,7 @@ fn prepare( // 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| TagElem::packed(Tag::new(target.clone(), key))); + let tag = key.map(|key| Tag::new(target.clone(), key)); // Ensure that this preparation only runs once by marking the element as // prepared. @@ -246,7 +242,7 @@ fn show( // Apply a user-defined show rule. ShowStep::Recipe(recipe, guard) => { let context = Context::new(target.location(), Some(styles)); - match &recipe.selector { + 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)) => { diff --git a/tests/ref/counter-page-between-pages.png b/tests/ref/counter-page-between-pages.png Binary files differnew file mode 100644 index 00000000..f79c586d --- /dev/null +++ b/tests/ref/counter-page-between-pages.png diff --git a/tests/ref/counter-page-footer-before-set-page.png b/tests/ref/counter-page-footer-before-set-page.png Binary files differnew file mode 100644 index 00000000..2c6cd4ad --- /dev/null +++ b/tests/ref/counter-page-footer-before-set-page.png diff --git a/tests/ref/counter-page-footer-only-update.png b/tests/ref/counter-page-footer-only-update.png Binary files differnew file mode 100644 index 00000000..e179face --- /dev/null +++ b/tests/ref/counter-page-footer-only-update.png diff --git a/tests/ref/counter-page-header-before-set-page.png b/tests/ref/counter-page-header-before-set-page.png Binary files differnew file mode 100644 index 00000000..960776cf --- /dev/null +++ b/tests/ref/counter-page-header-before-set-page.png diff --git a/tests/ref/counter-page-header-only-update.png b/tests/ref/counter-page-header-only-update.png Binary files differnew file mode 100644 index 00000000..07bb19d9 --- /dev/null +++ b/tests/ref/counter-page-header-only-update.png diff --git a/tests/ref/issue-1833-locate-place.png b/tests/ref/issue-1833-locate-place.png Binary files differnew file mode 100644 index 00000000..82b74e6d --- /dev/null +++ b/tests/ref/issue-1833-locate-place.png diff --git a/tests/ref/issue-1886-locate-after-metadata.png b/tests/ref/issue-1886-locate-after-metadata.png Binary files differnew file mode 100644 index 00000000..3fb4708d --- /dev/null +++ b/tests/ref/issue-1886-locate-after-metadata.png diff --git a/tests/ref/issue-2326-context-set-page.png b/tests/ref/issue-2326-context-set-page.png Binary files differnew file mode 100644 index 00000000..f0307d44 --- /dev/null +++ b/tests/ref/issue-2326-context-set-page.png diff --git a/tests/ref/issue-2841-pagebreak-to-weak.png b/tests/ref/issue-2841-pagebreak-to-weak.png Binary files differnew file mode 100644 index 00000000..b6404766 --- /dev/null +++ b/tests/ref/issue-2841-pagebreak-to-weak.png diff --git a/tests/ref/issue-4029-locate-after-pagebreak.png b/tests/ref/issue-4029-locate-after-pagebreak.png Binary files differnew file mode 100644 index 00000000..3b21c5d3 --- /dev/null +++ b/tests/ref/issue-4029-locate-after-pagebreak.png diff --git a/tests/ref/issue-4029-locate-after-par-and-pagebreak.png b/tests/ref/issue-4029-locate-after-par-and-pagebreak.png Binary files differnew file mode 100644 index 00000000..9d34677d --- /dev/null +++ b/tests/ref/issue-4029-locate-after-par-and-pagebreak.png diff --git a/tests/ref/issue-4029-locate-after-spacing.png b/tests/ref/issue-4029-locate-after-spacing.png Binary files differnew file mode 100644 index 00000000..d2e61435 --- /dev/null +++ b/tests/ref/issue-4029-locate-after-spacing.png diff --git a/tests/ref/issue-4363-set-page-after-tag.png b/tests/ref/issue-4363-set-page-after-tag.png Binary files differnew file mode 100644 index 00000000..4002b2dd --- /dev/null +++ b/tests/ref/issue-4363-set-page-after-tag.png diff --git a/tests/ref/list-item-styling.png b/tests/ref/list-item-styling.png Binary files differnew file mode 100644 index 00000000..72af2e1d --- /dev/null +++ b/tests/ref/list-item-styling.png diff --git a/tests/ref/list-items-context.png b/tests/ref/list-items-context.png Binary files differnew file mode 100644 index 00000000..a19cbec4 --- /dev/null +++ b/tests/ref/list-items-context.png diff --git a/tests/ref/locate-between-pages.png b/tests/ref/locate-between-pages.png Binary files differnew file mode 100644 index 00000000..aac6de86 --- /dev/null +++ b/tests/ref/locate-between-pages.png diff --git a/tests/ref/page-marginal-style-context.png b/tests/ref/page-marginal-style-context.png Binary files differnew file mode 100644 index 00000000..02c80bb1 --- /dev/null +++ b/tests/ref/page-marginal-style-context.png diff --git a/tests/ref/page-marginal-style-empty.png b/tests/ref/page-marginal-style-empty.png Binary files differnew file mode 100644 index 00000000..10183129 --- /dev/null +++ b/tests/ref/page-marginal-style-empty.png diff --git a/tests/ref/page-marginal-style-page-call.png b/tests/ref/page-marginal-style-page-call.png Binary files differnew file mode 100644 index 00000000..0d53a59f --- /dev/null +++ b/tests/ref/page-marginal-style-page-call.png diff --git a/tests/ref/page-marginal-style-shared-initial-interaction.png b/tests/ref/page-marginal-style-shared-initial-interaction.png Binary files differnew file mode 100644 index 00000000..76a76588 --- /dev/null +++ b/tests/ref/page-marginal-style-shared-initial-interaction.png diff --git a/tests/ref/page-marginal-style-show-rule-with-page-call.png b/tests/ref/page-marginal-style-show-rule-with-page-call.png Binary files differnew file mode 100644 index 00000000..17a17140 --- /dev/null +++ b/tests/ref/page-marginal-style-show-rule-with-page-call.png diff --git a/tests/ref/page-marginal-style-show-rule-with-pagebreak.png b/tests/ref/page-marginal-style-show-rule-with-pagebreak.png Binary files differnew file mode 100644 index 00000000..f05c64a7 --- /dev/null +++ b/tests/ref/page-marginal-style-show-rule-with-pagebreak.png 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 differnew file mode 100644 index 00000000..138f2272 --- /dev/null +++ b/tests/ref/page-marginal-style-show-rule-with-set-page.png diff --git a/tests/ref/page-marginal-style-show-rule.png b/tests/ref/page-marginal-style-show-rule.png Binary files differnew file mode 100644 index 00000000..f05c64a7 --- /dev/null +++ b/tests/ref/page-marginal-style-show-rule.png diff --git a/tests/ref/page-marginal-style-text-call-around-page-call.png b/tests/ref/page-marginal-style-text-call-around-page-call.png Binary files differnew file mode 100644 index 00000000..d5a2fd64 --- /dev/null +++ b/tests/ref/page-marginal-style-text-call-around-page-call.png diff --git a/tests/ref/page-marginal-style-text-call-around-pagebreak.png b/tests/ref/page-marginal-style-text-call-around-pagebreak.png Binary files differnew file mode 100644 index 00000000..c154a6bd --- /dev/null +++ b/tests/ref/page-marginal-style-text-call-around-pagebreak.png diff --git a/tests/ref/page-marginal-style-text-call-around-set-page.png b/tests/ref/page-marginal-style-text-call-around-set-page.png Binary files differnew file mode 100644 index 00000000..f4174cd2 --- /dev/null +++ b/tests/ref/page-marginal-style-text-call-around-set-page.png diff --git a/tests/ref/page-marginal-style-text-call-code.png b/tests/ref/page-marginal-style-text-call-code.png Binary files differnew file mode 100644 index 00000000..1c8f155f --- /dev/null +++ b/tests/ref/page-marginal-style-text-call-code.png diff --git a/tests/ref/page-marginal-style-text-call.png b/tests/ref/page-marginal-style-text-call.png Binary files differnew file mode 100644 index 00000000..1c8f155f --- /dev/null +++ b/tests/ref/page-marginal-style-text-call.png diff --git a/tests/ref/page-marginal-style-text-set-first.png b/tests/ref/page-marginal-style-text-set-first.png Binary files differnew file mode 100644 index 00000000..6ede81de --- /dev/null +++ b/tests/ref/page-marginal-style-text-set-first.png diff --git a/tests/ref/page-marginal-style-text-set.png b/tests/ref/page-marginal-style-text-set.png Binary files differnew file mode 100644 index 00000000..6ede81de --- /dev/null +++ b/tests/ref/page-marginal-style-text-set.png diff --git a/tests/suite/introspection/counter.typ b/tests/suite/introspection/counter.typ index 0d2be6e2..8cae32a4 100644 --- a/tests/suite/introspection/counter.typ +++ b/tests/suite/introspection/counter.typ @@ -62,6 +62,46 @@ At Beta, it was #context { #counter(page).update(1) #lorem(20) +--- counter-page-footer-before-set-page --- +#set page(numbering: "1", margin: (bottom: 20pt)) +A +#pagebreak() +#counter(page).update(5) +#set page(fill: aqua) +B + +--- counter-page-header-before-set-page --- +#set page(numbering: "1", number-align: top + center, margin: (top: 20pt)) +A +#counter(page).update(4) +#set page(fill: aqua) +B + +--- counter-page-between-pages --- +// The update happens conceptually between the pages. +#set page(numbering: "1", margin: (bottom: 20pt)) +A +#pagebreak() +#counter(page).update(5) +#set page(number-align: top + center, margin: (top: 20pt, bottom: 10pt)) +B + +--- counter-page-header-only-update --- +// Header should not be affected by default. +// To affect it, put the counter update before the `set page`. +#set page( + numbering: "1", + number-align: top + center, + margin: (top: 20pt), +) + +#counter(page).update(5) + +--- counter-page-footer-only-update --- +// Footer should be affected by default. +#set page(numbering: "1 / 1", margin: (bottom: 20pt)) +#counter(page).update(5) + --- counter-figure --- // Count figures. #figure(numbering: "A", caption: [Four 'A's], kind: image, supplement: "Figure")[_AAAA!_] diff --git a/tests/suite/introspection/locate.typ b/tests/suite/introspection/locate.typ index b3a77fde..18611dde 100644 --- a/tests/suite/introspection/locate.typ +++ b/tests/suite/introspection/locate.typ @@ -37,3 +37,93 @@ // Error: 10-25 selector matches multiple elements #context locate(heading) + +--- locate-between-pages --- +// Test locating tags that are before or between pages. +#set page(height: 30pt) +#context [ + // Before the first page. + // (= at the very start of the first page, before the header) + #test(locate(<a>).position(), (page: 1, x: 0pt, y: 0pt)) + + // On the first page. + #test(locate(<b>).position(), (page: 1, x: 10pt, y: 10pt)) + + // Between the two pages. + // (= at the very start of the first page, before the header) + #test(locate(<c>).position(), (page: 2, x: 0pt, y: 0pt)) + + // After the last page. + // (= at the very end of the last page, after the footer) + #test(locate(<d>).position(), (page: 2, x: 0pt, y: 30pt)) + #test(locate(<e>).position(), (page: 2, x: 0pt, y: 30pt)) +] + +#metadata(none) <a> +#pagebreak(weak: true) +#metadata(none) <b> +A +#pagebreak() +#metadata(none) <c> +#pagebreak(weak: true) +B +#pagebreak(weak: true) +#metadata(none) <d> +#pagebreak(weak: true) +#metadata(none) <e> + +--- issue-4029-locate-after-spacing --- +#set page(margin: 10pt) +#show heading: it => v(40pt) + it + += Introduction +#context test( + locate(heading).position(), + (page: 1, x: 10pt, y: 50pt), +) + + +--- issue-4029-locate-after-pagebreak --- +#set page(margin: 10pt) +#show heading: it => pagebreak() + it + += Introduction +#context test( + locate(heading).position(), + (page: 2, x: 10pt, y: 10pt), +) + +--- issue-4029-locate-after-par-and-pagebreak --- +// Ensure that the heading's tag isn't stuck at the end of the paragraph. +#set page(margin: 10pt) +Par +#show heading: it => pagebreak() + it += Introduction +#context test(locate(heading).page(), 2) + +--- issue-1886-locate-after-metadata --- +#show heading: it => { + metadata(it.label) + pagebreak(weak: true, to: "odd") + it +} + +Hi += Hello <hello> += World <world> + +// The metadata's position does not migrate to the next page, but the heading's +// does. +#context { + test(locate(metadata.where(value: <hello>)).page(), 1) + test(locate(<hello>).page(), 3) + test(locate(metadata.where(value: <world>)).page(), 3) + test(locate(<world>).page(), 5) +} + +--- issue-1833-locate-place --- +#set page(height: 60pt) +#context { + place(right + bottom, rect()) + test(here().position(), (page: 1, x: 10pt, y: 10pt)) +} diff --git a/tests/suite/layout/page.typ b/tests/suite/layout/page.typ index 0e1f7729..a4abaec7 100644 --- a/tests/suite/layout/page.typ +++ b/tests/suite/layout/page.typ @@ -245,6 +245,98 @@ Look, ma, no page numbers! #set page(header: auto, footer: auto) Default page numbers now. +--- page-marginal-style-text-set --- +#set page(numbering: "1", margin: (bottom: 20pt)) +#set text(red) +Red + +--- page-marginal-style-text-set-first --- +#set text(red) +#set page(numbering: "1", margin: (bottom: 20pt)) +Red + +--- page-marginal-style-text-call --- +#set page(numbering: "1", margin: (bottom: 20pt)) +#text(red)[Red] + +--- page-marginal-style-text-call-code --- +#{ + set page(numbering: "1", margin: (bottom: 20pt)) + text(red)[Red] +} + +--- page-marginal-style-text-call-around-page-call --- +#text(red, page(numbering: "1", margin: (bottom: 20pt))[Hello]) + +--- page-marginal-style-text-call-around-set-page --- +#text(red, { + set page(numbering: "1", margin: (bottom: 20pt)) + text(style: "italic")[Hello] +}) + +--- page-marginal-style-text-call-around-pagebreak --- +#set page(numbering: "1", margin: (bottom: 20pt)) +A +#text(red)[ + #pagebreak(weak: true) + B +] + +--- page-marginal-style-show-rule --- +#set page(numbering: "1", margin: (bottom: 20pt)) += Introduction + +--- page-marginal-style-show-rule-with-set-page --- +#show heading: it => { + set page(numbering: "1", margin: (bottom: 20pt)) + it +} + += Introduction + +--- page-marginal-style-show-rule-with-page-call --- +#show heading: page.with(fill: aqua) + +A += Introduction +B + +--- page-marginal-style-show-rule-with-pagebreak --- +#set page(numbering: "1", margin: (bottom: 20pt)) +#show heading: it => { + pagebreak(weak: true) + it +} + += Introduction + +--- page-marginal-style-context --- +#set page(numbering: "1", margin: (bottom: 20pt)) +#show: it => context { + set text(red) + it +} +Hi + +--- page-marginal-style-shared-initial-interaction --- +#set page(numbering: "1", margin: (bottom: 20pt)) +A +#{ + set text(fill: red) + pagebreak() +} +#text(fill: blue)[B] + +--- page-marginal-style-empty --- +#set text(red) +#set page(numbering: "1", margin: (bottom: 20pt)) + +--- page-marginal-style-page-call --- +#page(numbering: "1", margin: (bottom: 20pt))[ + #set text(red) + A +] + --- issue-2631-page-header-ordering --- #set text(6pt) #show heading: set text(6pt, weight: "regular") @@ -272,3 +364,22 @@ Hi #set page(fill: gray) text #pagebreak() + +--- issue-2326-context-set-page --- +#context [ + #set page(fill: aqua) + On page #here().page() +] + +--- issue-3671-get-from-page-call --- +#set page(margin: 5pt) +#context test(page.margin, 5pt) +#page(margin: 10pt, context test(page.margin, 10pt)) + +--- issue-4363-set-page-after-tag --- +#set page(fill: aqua) +1 +#pagebreak() +#metadata(none) +#set page(fill: red) +2 diff --git a/tests/suite/layout/pagebreak.typ b/tests/suite/layout/pagebreak.typ index a1734596..3ca48b69 100644 --- a/tests/suite/layout/pagebreak.typ +++ b/tests/suite/layout/pagebreak.typ @@ -141,3 +141,12 @@ Some text on page 2 #set page(fill: orange) // This sets the color of the page starting from page 4 Some text on page 4 + +--- issue-2591-single-weak-pagebreak --- +#pagebreak(weak: true) + +--- issue-2841-pagebreak-to-weak --- +First +#pagebreak(to: "odd") +#pagebreak(weak: true) +Odd diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index f9275517..46f4621f 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -142,6 +142,16 @@ Hello #list(tight: false)[A][B] World +--- list-items-context --- +#context [+ A] +#context [+ B] +#context [+ C] + +--- list-item-styling --- +- Hello +#text(red)[- World] +#text(green)[- What up?] + --- issue-2530-list-item-panic --- // List item (pre-emptive) #list.item[Hello] |
