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