summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-08-26 19:17:58 +0200
committerGitHub <noreply@github.com>2024-08-26 17:17:58 +0000
commit4365e18454af4f8f53fe1198182cb5dc8f3d628e (patch)
treefe37649cec71b916d5119a033028e3f620247209
parentcb98eec60924a41889988644e5122d2e13eccbf1 (diff)
Improve realization and page layout (#4840)
-rw-r--r--crates/typst-utils/src/lib.rs35
-rw-r--r--crates/typst/src/eval/rules.rs4
-rw-r--r--crates/typst/src/foundations/content.rs2
-rw-r--r--crates/typst/src/foundations/styles.rs289
-rw-r--r--crates/typst/src/introspection/counter.rs2
-rw-r--r--crates/typst/src/introspection/introspector.rs13
-rw-r--r--crates/typst/src/introspection/mod.rs74
-rw-r--r--crates/typst/src/introspection/tag.rs111
-rw-r--r--crates/typst/src/layout/flow.rs833
-rw-r--r--crates/typst/src/layout/inline/collect.rs2
-rw-r--r--crates/typst/src/layout/inline/mod.rs3
-rw-r--r--crates/typst/src/layout/page.rs148
-rw-r--r--crates/typst/src/layout/place.rs2
-rw-r--r--crates/typst/src/math/ctx.rs3
-rw-r--r--crates/typst/src/math/fragment.rs9
-rw-r--r--crates/typst/src/math/mod.rs29
-rw-r--r--crates/typst/src/model/par.rs4
-rw-r--r--crates/typst/src/realize/arenas.rs9
-rw-r--r--crates/typst/src/realize/behaviour.rs182
-rw-r--r--crates/typst/src/realize/mod.rs688
-rw-r--r--crates/typst/src/realize/process.rs28
-rw-r--r--tests/ref/counter-page-between-pages.pngbin0 -> 299 bytes
-rw-r--r--tests/ref/counter-page-footer-before-set-page.pngbin0 -> 535 bytes
-rw-r--r--tests/ref/counter-page-footer-only-update.pngbin0 -> 205 bytes
-rw-r--r--tests/ref/counter-page-header-before-set-page.pngbin0 -> 551 bytes
-rw-r--r--tests/ref/counter-page-header-only-update.pngbin0 -> 131 bytes
-rw-r--r--tests/ref/issue-1833-locate-place.pngbin0 -> 136 bytes
-rw-r--r--tests/ref/issue-1886-locate-after-metadata.pngbin0 -> 830 bytes
-rw-r--r--tests/ref/issue-2326-context-set-page.pngbin0 -> 676 bytes
-rw-r--r--tests/ref/issue-2841-pagebreak-to-weak.pngbin0 -> 391 bytes
-rw-r--r--tests/ref/issue-4029-locate-after-pagebreak.pngbin0 -> 698 bytes
-rw-r--r--tests/ref/issue-4029-locate-after-par-and-pagebreak.pngbin0 -> 785 bytes
-rw-r--r--tests/ref/issue-4029-locate-after-spacing.pngbin0 -> 706 bytes
-rw-r--r--tests/ref/issue-4363-set-page-after-tag.pngbin0 -> 278 bytes
-rw-r--r--tests/ref/list-item-styling.pngbin0 -> 1459 bytes
-rw-r--r--tests/ref/list-items-context.pngbin0 -> 382 bytes
-rw-r--r--tests/ref/locate-between-pages.pngbin0 -> 231 bytes
-rw-r--r--tests/ref/page-marginal-style-context.pngbin0 -> 348 bytes
-rw-r--r--tests/ref/page-marginal-style-empty.pngbin0 -> 152 bytes
-rw-r--r--tests/ref/page-marginal-style-page-call.pngbin0 -> 300 bytes
-rw-r--r--tests/ref/page-marginal-style-shared-initial-interaction.pngbin0 -> 504 bytes
-rw-r--r--tests/ref/page-marginal-style-show-rule-with-page-call.pngbin0 -> 1514 bytes
-rw-r--r--tests/ref/page-marginal-style-show-rule-with-pagebreak.pngbin0 -> 716 bytes
-rw-r--r--tests/ref/page-marginal-style-show-rule-with-set-page.pngbin0 -> 727 bytes
-rw-r--r--tests/ref/page-marginal-style-show-rule.pngbin0 -> 716 bytes
-rw-r--r--tests/ref/page-marginal-style-text-call-around-page-call.pngbin0 -> 529 bytes
-rw-r--r--tests/ref/page-marginal-style-text-call-around-pagebreak.pngbin0 -> 510 bytes
-rw-r--r--tests/ref/page-marginal-style-text-call-around-set-page.pngbin0 -> 531 bytes
-rw-r--r--tests/ref/page-marginal-style-text-call-code.pngbin0 -> 473 bytes
-rw-r--r--tests/ref/page-marginal-style-text-call.pngbin0 -> 473 bytes
-rw-r--r--tests/ref/page-marginal-style-text-set-first.pngbin0 -> 457 bytes
-rw-r--r--tests/ref/page-marginal-style-text-set.pngbin0 -> 457 bytes
-rw-r--r--tests/suite/introspection/counter.typ40
-rw-r--r--tests/suite/introspection/locate.typ90
-rw-r--r--tests/suite/layout/page.typ111
-rw-r--r--tests/suite/layout/pagebreak.typ9
-rw-r--r--tests/suite/model/list.typ10
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
new file mode 100644
index 00000000..f79c586d
--- /dev/null
+++ b/tests/ref/counter-page-between-pages.png
Binary files differ
diff --git a/tests/ref/counter-page-footer-before-set-page.png b/tests/ref/counter-page-footer-before-set-page.png
new file mode 100644
index 00000000..2c6cd4ad
--- /dev/null
+++ b/tests/ref/counter-page-footer-before-set-page.png
Binary files differ
diff --git a/tests/ref/counter-page-footer-only-update.png b/tests/ref/counter-page-footer-only-update.png
new file mode 100644
index 00000000..e179face
--- /dev/null
+++ b/tests/ref/counter-page-footer-only-update.png
Binary files differ
diff --git a/tests/ref/counter-page-header-before-set-page.png b/tests/ref/counter-page-header-before-set-page.png
new file mode 100644
index 00000000..960776cf
--- /dev/null
+++ b/tests/ref/counter-page-header-before-set-page.png
Binary files differ
diff --git a/tests/ref/counter-page-header-only-update.png b/tests/ref/counter-page-header-only-update.png
new file mode 100644
index 00000000..07bb19d9
--- /dev/null
+++ b/tests/ref/counter-page-header-only-update.png
Binary files differ
diff --git a/tests/ref/issue-1833-locate-place.png b/tests/ref/issue-1833-locate-place.png
new file mode 100644
index 00000000..82b74e6d
--- /dev/null
+++ b/tests/ref/issue-1833-locate-place.png
Binary files differ
diff --git a/tests/ref/issue-1886-locate-after-metadata.png b/tests/ref/issue-1886-locate-after-metadata.png
new file mode 100644
index 00000000..3fb4708d
--- /dev/null
+++ b/tests/ref/issue-1886-locate-after-metadata.png
Binary files differ
diff --git a/tests/ref/issue-2326-context-set-page.png b/tests/ref/issue-2326-context-set-page.png
new file mode 100644
index 00000000..f0307d44
--- /dev/null
+++ b/tests/ref/issue-2326-context-set-page.png
Binary files differ
diff --git a/tests/ref/issue-2841-pagebreak-to-weak.png b/tests/ref/issue-2841-pagebreak-to-weak.png
new file mode 100644
index 00000000..b6404766
--- /dev/null
+++ b/tests/ref/issue-2841-pagebreak-to-weak.png
Binary files differ
diff --git a/tests/ref/issue-4029-locate-after-pagebreak.png b/tests/ref/issue-4029-locate-after-pagebreak.png
new file mode 100644
index 00000000..3b21c5d3
--- /dev/null
+++ b/tests/ref/issue-4029-locate-after-pagebreak.png
Binary files differ
diff --git a/tests/ref/issue-4029-locate-after-par-and-pagebreak.png b/tests/ref/issue-4029-locate-after-par-and-pagebreak.png
new file mode 100644
index 00000000..9d34677d
--- /dev/null
+++ b/tests/ref/issue-4029-locate-after-par-and-pagebreak.png
Binary files differ
diff --git a/tests/ref/issue-4029-locate-after-spacing.png b/tests/ref/issue-4029-locate-after-spacing.png
new file mode 100644
index 00000000..d2e61435
--- /dev/null
+++ b/tests/ref/issue-4029-locate-after-spacing.png
Binary files differ
diff --git a/tests/ref/issue-4363-set-page-after-tag.png b/tests/ref/issue-4363-set-page-after-tag.png
new file mode 100644
index 00000000..4002b2dd
--- /dev/null
+++ b/tests/ref/issue-4363-set-page-after-tag.png
Binary files differ
diff --git a/tests/ref/list-item-styling.png b/tests/ref/list-item-styling.png
new file mode 100644
index 00000000..72af2e1d
--- /dev/null
+++ b/tests/ref/list-item-styling.png
Binary files differ
diff --git a/tests/ref/list-items-context.png b/tests/ref/list-items-context.png
new file mode 100644
index 00000000..a19cbec4
--- /dev/null
+++ b/tests/ref/list-items-context.png
Binary files differ
diff --git a/tests/ref/locate-between-pages.png b/tests/ref/locate-between-pages.png
new file mode 100644
index 00000000..aac6de86
--- /dev/null
+++ b/tests/ref/locate-between-pages.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-context.png b/tests/ref/page-marginal-style-context.png
new file mode 100644
index 00000000..02c80bb1
--- /dev/null
+++ b/tests/ref/page-marginal-style-context.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-empty.png b/tests/ref/page-marginal-style-empty.png
new file mode 100644
index 00000000..10183129
--- /dev/null
+++ b/tests/ref/page-marginal-style-empty.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-page-call.png b/tests/ref/page-marginal-style-page-call.png
new file mode 100644
index 00000000..0d53a59f
--- /dev/null
+++ b/tests/ref/page-marginal-style-page-call.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-shared-initial-interaction.png b/tests/ref/page-marginal-style-shared-initial-interaction.png
new file mode 100644
index 00000000..76a76588
--- /dev/null
+++ b/tests/ref/page-marginal-style-shared-initial-interaction.png
Binary files differ
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
new file mode 100644
index 00000000..17a17140
--- /dev/null
+++ b/tests/ref/page-marginal-style-show-rule-with-page-call.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-show-rule-with-pagebreak.png b/tests/ref/page-marginal-style-show-rule-with-pagebreak.png
new file mode 100644
index 00000000..f05c64a7
--- /dev/null
+++ b/tests/ref/page-marginal-style-show-rule-with-pagebreak.png
Binary files differ
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
new file mode 100644
index 00000000..138f2272
--- /dev/null
+++ b/tests/ref/page-marginal-style-show-rule-with-set-page.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-show-rule.png b/tests/ref/page-marginal-style-show-rule.png
new file mode 100644
index 00000000..f05c64a7
--- /dev/null
+++ b/tests/ref/page-marginal-style-show-rule.png
Binary files differ
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
new file mode 100644
index 00000000..d5a2fd64
--- /dev/null
+++ b/tests/ref/page-marginal-style-text-call-around-page-call.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-text-call-around-pagebreak.png b/tests/ref/page-marginal-style-text-call-around-pagebreak.png
new file mode 100644
index 00000000..c154a6bd
--- /dev/null
+++ b/tests/ref/page-marginal-style-text-call-around-pagebreak.png
Binary files differ
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
new file mode 100644
index 00000000..f4174cd2
--- /dev/null
+++ b/tests/ref/page-marginal-style-text-call-around-set-page.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-text-call-code.png b/tests/ref/page-marginal-style-text-call-code.png
new file mode 100644
index 00000000..1c8f155f
--- /dev/null
+++ b/tests/ref/page-marginal-style-text-call-code.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-text-call.png b/tests/ref/page-marginal-style-text-call.png
new file mode 100644
index 00000000..1c8f155f
--- /dev/null
+++ b/tests/ref/page-marginal-style-text-call.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-text-set-first.png b/tests/ref/page-marginal-style-text-set-first.png
new file mode 100644
index 00000000..6ede81de
--- /dev/null
+++ b/tests/ref/page-marginal-style-text-set-first.png
Binary files differ
diff --git a/tests/ref/page-marginal-style-text-set.png b/tests/ref/page-marginal-style-text-set.png
new file mode 100644
index 00000000..6ede81de
--- /dev/null
+++ b/tests/ref/page-marginal-style-text-set.png
Binary files differ
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]