Minor realization improvements (#3408)

This commit is contained in:
Laurenz 2024-02-13 19:35:38 +01:00 committed by GitHub
parent 40099e32e1
commit 1f68e15725
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 475 additions and 454 deletions

View File

@ -319,9 +319,9 @@ fn create_struct(element: &Elem) -> TokenStream {
/// Create a field declaration for the struct.
fn create_field(field: &Field) -> TokenStream {
let Field { ident, ty, .. } = field;
let Field { vis, ident, ty, .. } = field;
if field.required {
quote! { #ident: #ty }
quote! { #vis #ident: #ty }
} else {
quote! { #ident: ::std::option::Option<#ty> }
}

View File

@ -14,13 +14,13 @@ use smallvec::smallvec;
use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
elem, func, scope, ty, Behave, Behaviour, Dict, Element, Fields, IntoValue, Label,
NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles,
Value,
elem, func, scope, ty, Dict, Element, Fields, IntoValue, Label, NativeElement,
Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
};
use crate::introspection::{Location, Meta, MetaElem};
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
use crate::model::{Destination, EmphElem, StrongElem};
use crate::realize::{Behave, Behaviour};
use crate::syntax::Span;
use crate::text::UnderlineElem;
use crate::util::{fat, BitSet};
@ -330,34 +330,17 @@ impl Content {
sequence.children.is_empty()
}
/// Whether the content is a sequence.
pub fn is_sequence(&self) -> bool {
self.is::<SequenceElem>()
}
/// Access the children if this is a sequence.
pub fn to_sequence(&self) -> Option<impl Iterator<Item = &Prehashed<Content>>> {
let sequence = self.to_packed::<SequenceElem>()?;
Some(sequence.children.iter())
}
/// Also auto expands sequence of sequences into flat sequence
pub fn sequence_recursive_for_each<'a>(&'a self, f: &mut impl FnMut(&'a Self)) {
if let Some(children) = self.to_sequence() {
children.for_each(|c| c.sequence_recursive_for_each(f));
if let Some(sequence) = self.to_packed::<SequenceElem>() {
for child in &sequence.children {
child.sequence_recursive_for_each(f);
}
} else {
f(self);
}
}
/// Access the child and styles.
pub fn to_styled(&self) -> Option<(&Content, &Styles)> {
let styled = self.to_packed::<StyledElem>()?;
let child = styled.child();
let styles = styled.styles();
Some((child, styles))
}
/// Style this content with a recipe, eagerly applying it if possible.
pub fn styled_with_recipe(
self,
@ -886,11 +869,12 @@ impl<T: NativeElement + Debug> Debug for Packed<T> {
}
}
/// Defines the element for sequences.
/// A sequence of content.
#[elem(Debug, Repr, PartialEq)]
struct SequenceElem {
pub struct SequenceElem {
/// The elements.
#[required]
children: Vec<Prehashed<Content>>,
pub children: Vec<Prehashed<Content>>,
}
impl Debug for SequenceElem {
@ -937,13 +921,15 @@ impl Repr for SequenceElem {
}
}
/// Defines the `ElemFunc` for styled elements.
/// Content alongside styles.
#[elem(Debug, Repr, PartialEq)]
struct StyledElem {
pub struct StyledElem {
/// The content.
#[required]
child: Prehashed<Content>,
pub child: Prehashed<Content>,
/// The styles.
#[required]
styles: Styles,
pub styles: Styles,
}
impl Debug for StyledElem {

View File

@ -299,42 +299,3 @@ pub trait ShowSet {
/// that should work even in the face of a user-defined show rule.
fn show_set(&self, styles: StyleChain) -> Styles;
}
/// How the element interacts with other elements.
pub trait Behave {
/// The element's interaction behaviour.
fn behaviour(&self) -> Behaviour;
/// Whether this weak element is larger than a previous one and thus picked
/// as the maximum when the levels are the same.
#[allow(unused_variables)]
fn larger(&self, prev: &(&Content, StyleChain), styles: StyleChain) -> bool {
false
}
}
/// How an element interacts with other elements in a stream.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Behaviour {
/// A weak element which only survives when a supportive element is before
/// and after it. Furthermore, per consecutive run of weak elements, only
/// one survives: The one with the lowest weakness level (or the larger one
/// if there is a tie).
Weak(usize),
/// An element that enables adjacent weak elements to exist. The default.
Supportive,
/// An element that destroys adjacent weak elements.
Destructive,
/// An element that does not interact at all with other elements, having the
/// same effect as if it didn't exist, but has a visual representation.
Ignorant,
/// An element that does not have a visual representation.
Invisible,
}
impl Behaviour {
/// Whether this of `Weak(_)` variant.
pub fn is_weak(self) -> bool {
matches!(self, Self::Weak(_))
}
}

View File

@ -1,9 +1,8 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
elem, Behave, Behaviour, Content, Packed, Show, StyleChain, Value,
};
use crate::foundations::{elem, Content, Packed, Show, StyleChain, Value};
use crate::introspection::Locatable;
use crate::realize::{Behave, Behaviour};
/// Exposes a value to the query system without producing visible content.
///

View File

@ -27,9 +27,10 @@ use smallvec::SmallVec;
use crate::foundations::Packed;
use crate::foundations::{
category, elem, ty, Behave, Behaviour, Category, Content, Repr, Scope, Unlabellable,
category, elem, ty, Category, Content, Repr, Scope, Unlabellable,
};
use crate::model::Destination;
use crate::realize::{Behave, Behaviour};
/// Interactions between document parts.
///

View File

@ -2,11 +2,12 @@ use std::num::NonZeroUsize;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Behave, Behaviour, Content, Packed, StyleChain};
use crate::foundations::{elem, Content, Packed, StyleChain};
use crate::layout::{
Abs, Axes, Dir, Fragment, Frame, LayoutMultiple, Length, Point, Ratio, Regions, Rel,
Size,
};
use crate::realize::{Behave, Behaviour};
use crate::text::TextElem;
use crate::util::Numeric;

View File

@ -5,7 +5,7 @@ use comemo::Prehashed;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Smart, StyleChain,
elem, Content, NativeElement, Packed, Resolve, Smart, StyleChain, StyledElem,
};
use crate::introspection::{Meta, MetaElem};
use crate::layout::{
@ -46,9 +46,9 @@ impl LayoutMultiple for Packed<FlowElem> {
for mut child in self.children().iter().map(|c| &**c) {
let outer = styles;
let mut styles = styles;
if let Some((elem, map)) = child.to_styled() {
child = elem;
styles = outer.chain(map);
if let Some(styled) = child.to_packed::<StyledElem>() {
child = &styled.child;
styles = outer.chain(&styled.styles);
}
if child.is::<MetaElem>() {
@ -344,8 +344,8 @@ impl<'a> FlowLayouter<'a> {
// How to align the block.
let align = if let Some(align) = child.to_packed::<AlignElem>() {
align.alignment(styles)
} else if let Some((_, local)) = child.to_styled() {
AlignElem::alignment_in(styles.chain(local))
} else if let Some(styled) = child.to_packed::<StyledElem>() {
AlignElem::alignment_in(styles.chain(&styled.styles))
} else {
AlignElem::alignment_in(styles)
}

View File

@ -13,7 +13,7 @@ use self::shaping::{
use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route};
use crate::eval::Tracer;
use crate::foundations::{Content, Packed, Resolve, Smart, StyleChain};
use crate::foundations::{Content, Packed, Resolve, Smart, StyleChain, StyledElem};
use crate::introspection::{Introspector, Locator, MetaElem};
use crate::layout::{
Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame, HElem,
@ -435,9 +435,9 @@ fn collect<'a>(
while let Some(mut child) = iter.next() {
let outer = styles;
let mut styles = *styles;
if let Some((elem, local)) = child.to_styled() {
child = elem;
styles = outer.chain(local);
if let Some(styled) = child.to_packed::<StyledElem>() {
child = &styled.child;
styles = outer.chain(&styled.styles);
}
let segment = if child.is::<SpaceElem>() {
@ -474,9 +474,9 @@ fn collect<'a>(
region,
SmartQuoteElem::alternative_in(styles),
);
let peeked = iter.peek().and_then(|child| {
let child = if let Some((child, _)) = child.to_styled() {
child
let peeked = iter.peek().and_then(|&child| {
let child = if let Some(styled) = child.to_packed::<StyledElem>() {
&styled.child
} else {
child
};
@ -761,8 +761,8 @@ fn shared_get<T: PartialEq>(
let value = getter(styles);
children
.iter()
.filter_map(|child| child.to_styled())
.all(|(_, local)| getter(styles.chain(local)) == value)
.filter_map(|child| child.to_packed::<StyledElem>())
.all(|styled| getter(styles.chain(&styled.styles)) == value)
.then_some(value)
}

View File

@ -1,9 +1,10 @@
use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine;
use crate::foundations::{elem, Behave, Behaviour, Content, Packed, Smart, StyleChain};
use crate::foundations::{elem, Content, Packed, Smart, StyleChain};
use crate::layout::{
Alignment, Axes, Em, Fragment, LayoutMultiple, Length, Regions, Rel, VAlignment,
};
use crate::realize::{Behave, Behaviour};
/// Places content at an absolute position.
///

View File

@ -1,7 +1,6 @@
use crate::foundations::{
cast, elem, Behave, Behaviour, Content, Packed, Resolve, StyleChain,
};
use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain};
use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel};
use crate::realize::{Behave, Behaviour};
use crate::util::Numeric;
/// Inserts horizontal spacing into a paragraph.

View File

@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain};
use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain, StyledElem};
use crate::layout::{
Abs, AlignElem, Axes, Axis, Dir, FixedAlignment, Fr, Fragment, Frame, LayoutMultiple,
Point, Regions, Size, Spacing,
@ -209,8 +209,8 @@ impl<'a> StackLayouter<'a> {
// Block-axis alignment of the `AlignElement` is respected by stacks.
let align = if let Some(align) = block.to_packed::<AlignElem>() {
align.alignment(styles)
} else if let Some((_, local)) = block.to_styled() {
AlignElem::alignment_in(styles.chain(local))
} else if let Some(styled) = block.to_packed::<StyledElem>() {
AlignElem::alignment_in(styles.chain(&styled.styles))
} else {
AlignElem::alignment_in(styles)
}

View File

@ -41,11 +41,13 @@ use self::row::*;
use self::spacing::*;
use crate::diag::SourceResult;
use crate::foundations::SequenceElem;
use crate::foundations::StyledElem;
use crate::foundations::{
category, Category, Content, Module, Resolve, Scope, StyleChain,
};
use crate::layout::{BoxElem, HElem, Spacing};
use crate::realize::{realize, BehavedBuilder};
use crate::realize::{process, BehavedBuilder};
use crate::text::{LinebreakElem, SpaceElem, TextElem};
/// Typst has special [syntax]($syntax/#math) and library functions to typeset
@ -230,11 +232,11 @@ impl LayoutMath for Content {
return elem.layout_math(ctx, styles);
}
if let Some(realized) = realize(ctx.engine, self, styles)? {
if let Some(realized) = process(ctx.engine, self, styles)? {
return realized.layout_math(ctx, styles);
}
if self.is_sequence() {
if self.is::<SequenceElem>() {
let mut bb = BehavedBuilder::new();
self.sequence_recursive_for_each(&mut |child: &Content| {
bb.push(child, StyleChain::default());
@ -245,17 +247,17 @@ impl LayoutMath for Content {
return Ok(());
}
if let Some((elem, local)) = self.to_styled() {
if let Some(styled) = self.to_packed::<StyledElem>() {
let outer = styles;
let styles = outer.chain(local);
let styles = outer.chain(&styled.styles);
if TextElem::font_in(styles) != TextElem::font_in(outer) {
let frame = ctx.layout_content(elem, styles)?;
let frame = ctx.layout_content(&styled.child, styles)?;
ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true));
return Ok(());
}
elem.layout_math(ctx, styles)?;
styled.child.layout_math(ctx, styles)?;
return Ok(());
}

View File

@ -4,7 +4,7 @@ use crate::diag::{bail, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Args, Array, Construct, Content, Datetime, Packed, Smart, StyleChain,
Value,
StyledElem, Value,
};
use crate::introspection::{Introspector, ManualPageCounter};
use crate::layout::{LayoutRoot, Page, PageElem};
@ -82,16 +82,16 @@ impl LayoutRoot for Packed<DocumentElem> {
while let Some(mut child) = iter.next() {
let outer = styles;
let mut styles = styles;
if let Some((elem, local)) = child.to_styled() {
styles = outer.chain(local);
child = elem;
if let Some(styled) = child.to_packed::<StyledElem>() {
child = &styled.child;
styles = outer.chain(&styled.styles);
}
if let Some(page) = child.to_packed::<PageElem>() {
let extend_to = iter.peek().and_then(|&next| {
*next
.to_styled()
.map_or(next, |(elem, _)| elem)
.to_packed::<StyledElem>()
.map_or(next, |styled| &styled.child)
.to_packed::<PageElem>()?
.clear_to()?
});

View File

@ -1,8 +1,47 @@
//! Element interaction.
use crate::foundations::{Behave, Behaviour, Content, StyleChain, Styles};
use crate::foundations::{Content, StyleChain, Styles};
use crate::syntax::Span;
/// How an element interacts with other elements in a stream.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Behaviour {
/// A weak element which only survives when a supportive element is before
/// and after it. Furthermore, per consecutive run of weak elements, only
/// one survives: The one with the lowest weakness level (or the larger one
/// if there is a tie).
Weak(usize),
/// An element that enables adjacent weak elements to exist. The default.
Supportive,
/// An element that destroys adjacent weak elements.
Destructive,
/// An element that does not interact at all with other elements, having the
/// same effect as if it didn't exist, but has a visual representation.
Ignorant,
/// An element that does not have a visual representation.
Invisible,
}
impl Behaviour {
/// Whether this of `Weak(_)` variant.
pub fn is_weak(self) -> bool {
matches!(self, Self::Weak(_))
}
}
/// How the element interacts with other elements.
pub trait Behave {
/// The element's interaction behaviour.
fn behaviour(&self) -> Behaviour;
/// Whether this weak element is larger than a previous one and thus picked
/// as the maximum when the levels are the same.
#[allow(unused_variables)]
fn larger(&self, prev: &(&Content, StyleChain), styles: StyleChain) -> bool {
false
}
}
/// Processes a sequence of content and resolves behaviour interactions between
/// them and separates local styles for each element from the shared trunk of
/// styles.

View File

@ -1,24 +1,23 @@
//! Realization of content.
mod arenas;
mod behave;
mod behaviour;
mod process;
pub use self::arenas::Arenas;
pub use self::behave::BehavedBuilder;
pub use self::behaviour::{Behave, BehavedBuilder, Behaviour};
pub use self::process::{process, processable};
use std::borrow::Cow;
use std::cell::OnceCell;
use std::mem;
use smallvec::smallvec;
use std::mem;
use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route};
use crate::foundations::{
Content, NativeElement, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet,
Style, StyleChain, Styles, Synthesize, Transformation,
Content, NativeElement, Packed, SequenceElem, StyleChain, StyledElem, Styles,
};
use crate::introspection::{Locatable, Meta, MetaElem};
use crate::introspection::MetaElem;
use crate::layout::{
AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, HElem, LayoutMultiple,
LayoutSingle, PageElem, PagebreakElem, Parity, PlaceElem, VElem,
@ -30,7 +29,6 @@ use crate::model::{
};
use crate::syntax::Span;
use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use crate::util::{hash128, BitSet};
/// Realize into an element that is capable of root-level layout.
#[typst_macros::time(name = "realize root")]
@ -43,8 +41,8 @@ pub fn realize_root<'a>(
let mut builder = Builder::new(engine, arenas, true);
builder.accept(content, styles)?;
builder.interrupt_page(Some(styles), true)?;
let (pages, shared, span) = builder.doc.unwrap().pages.finish();
Ok((Packed::new(DocumentElem::new(pages.to_vec())).spanned(span), shared))
let (doc, trunk) = builder.doc.unwrap().finish();
Ok((doc, trunk))
}
/// Realize into an element that is capable of block-level layout.
@ -57,7 +55,7 @@ pub fn realize_block<'a>(
) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> {
// These elements implement `Layout` but still require a flow for
// proper layout.
if content.can::<dyn LayoutMultiple>() && verdict(engine, content, styles).is_none() {
if content.can::<dyn LayoutMultiple>() && !processable(engine, content, styles) {
return Ok((Cow::Borrowed(content), styles));
}
@ -65,298 +63,8 @@ pub fn realize_block<'a>(
builder.accept(content, styles)?;
builder.interrupt_par()?;
let (children, shared, span) = builder.flow.0.finish();
Ok((Cow::Owned(FlowElem::new(children).pack().spanned(span)), shared))
}
/// Apply the show rules in the given style chain to a target element.
pub fn realize(
engine: &mut Engine,
target: &Content,
styles: StyleChain,
) -> SourceResult<Option<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 meta = None;
if !prepared {
meta = prepare(engine, &mut target, &mut map, styles)?;
}
// Apply a step, if there is one.
let mut output = match step {
Some(step) => {
// Errors in show rules don't terminate compilation immediately. We
// just continue with empty content for them and show all errors
// together, if they remain by the end of the introspection loop.
//
// This way, we can ignore errors that only occur in earlier
// iterations and also show more useful errors at once.
engine.delayed(|engine| show(engine, target, step, styles.chain(&map)))
}
None => target,
};
// If necessary, apply metadata generated in the preparation.
if let Some(meta) = meta {
output += meta.pack();
}
Ok(Some(output.styled_with_map(map)))
}
/// What to do with an element when encountering it during realization.
struct Verdict<'a> {
/// Whether the element is already prepated (i.e. things that should only
/// happen once have happened).
prepared: bool,
/// A map of styles to apply to the element.
map: Styles,
/// An optional show rule transformation to apply to the element.
step: Option<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,
}
/// 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 = BitSet::new();
let mut step = None;
let mut slot;
let depth = OnceCell::new();
let prepared = target.is_prepared();
// Do pre-synthesis on a cloned element to be able to match on synthesized
// fields before real synthesis runs (during preparation). It's really
// unfortunate that we have to do this, but otherwise
// `show figure.where(kind: table)` won't work :(
if !prepared && target.can::<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;
}
if let Transformation::Style(transform) = &recipe.transform {
// If this is a show-set for an unprepared element, we need to apply
// it.
if !prepared {
map.apply(transform.clone());
}
} else if step.is_none() {
// Lazily compute the total number of recipes in the style chain. We
// need it to determine whether a particular show rule was already
// applied to the `target` previously. For this purpose, show rules
// are indexed from the top of the chain as the chain might grow to
// the bottom.
let depth =
*depth.get_or_init(|| styles.entries().filter_map(Style::recipe).count());
let index = RecipeIndex(depth - r);
if !target.is_guarded(index) && !revoked.contains(index.0) {
// If we find a matching, unguarded replacement show rule,
// remember it, but still continue searching for potential
// show-set styles that might change the verdict.
step = Some(ShowStep::Recipe(recipe, index));
// If we found a show rule and are already prepared, there is
// nothing else to do, so we can just break.
if prepared {
break;
}
}
}
r += 1;
}
// If we found no user-defined rule, also consider the built-in show rule.
if step.is_none() && target.can::<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.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,
target: &mut Content,
map: &mut Styles,
styles: StyleChain,
) -> SourceResult<Option<Packed<MetaElem>>> {
// Generate a location for the element, which uniquely identifies it in
// the document. This has some overhead, so we only do it for elements
// that are explicitly marked as locatable and labelled elements.
if target.can::<dyn Locatable>() || target.label().is_some() {
let location = engine.locator.locate(hash128(&target));
target.set_location(location);
}
// Apply built-in show-set rules. User-defined show-set rules are already
// considered in the map built while determining the verdict.
if let Some(show_settable) = target.with::<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));
// Ensure that this preparation only runs once by marking the element as
// prepared.
target.mark_prepared();
// Apply metadata be able to find the element in the frames.
// Do this after synthesis, so that it includes the synthesized fields.
if target.location().is_some() {
// Add a style to the whole element's subtree identifying it as
// belonging to the element.
map.set(MetaElem::set_data(smallvec![Meta::Elem(target.clone())]));
// Return an extra meta elem that will be attached so that the metadata
// styles are not lost in case the element's show rule results in
// nothing.
return Ok(Some(Packed::new(MetaElem::new()).spanned(target.span())));
}
Ok(None)
}
/// Apply a step.
fn show(
engine: &mut Engine,
target: Content,
step: ShowStep,
styles: StyleChain,
) -> SourceResult<Content> {
match step {
// Apply a user-defined show rule.
ShowStep::Recipe(recipe, guard) => match &recipe.selector {
// If the selector is a regex, the `target` is guaranteed to be a
// text element. This invokes special regex handling.
Some(Selector::Regex(regex)) => {
let text = target.into_packed::<TextElem>().unwrap();
show_regex(engine, &text, regex, recipe, guard)
}
// Just apply the recipe.
_ => recipe.apply(engine, target.guarded(guard)),
},
// If the verdict picks this step, the `target` is guaranteed to have a
// built-in show rule.
ShowStep::Builtin => target.with::<dyn Show>().unwrap().show(engine, styles),
}
}
/// Apply a regex show rule recipe to a target.
fn show_regex(
engine: &mut Engine,
elem: &Packed<TextElem>,
regex: &Regex,
recipe: &Recipe,
index: RecipeIndex,
) -> SourceResult<Content> {
let make = |s: &str| {
let mut fresh = elem.clone();
fresh.push_text(s.into());
fresh.pack()
};
let mut result = vec![];
let mut cursor = 0;
let text = elem.text();
for m in regex.find_iter(elem.text()) {
let start = m.start();
if cursor < start {
result.push(make(&text[cursor..start]));
}
let piece = make(m.as_str());
let transformed = recipe.apply(engine, piece)?;
result.push(transformed);
cursor = m.end();
}
if cursor < text.len() {
result.push(make(&text[cursor..]));
}
// In contrast to normal elements, which are guarded individually, for text
// show rules, we fully revoke the rule. This means that we can replace text
// with other text that rematches without running into infinite recursion
// problems.
//
// We do _not_ do this for all content because revoking e.g. a list show
// rule for all content resulting from that rule would be wrong: The list
// might contain nested lists. Moreover, replacing a normal element with one
// that rematches is bad practice: It can for instance also lead to
// surprising query results, so it's better to let the user deal with it.
// All these problems don't exist for text, so it's fine here.
Ok(Content::sequence(result).styled(Style::Revocation(index)))
let (flow, trunk) = builder.flow.finish();
Ok((Cow::Owned(flow.pack()), trunk))
}
/// Builds a document or a flow element from content.
@ -401,7 +109,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
.store(EquationElem::new(content.clone()).pack().spanned(content.span()));
}
if let Some(realized) = realize(self.engine, content, styles)? {
if let Some(realized) = process(self.engine, content, styles)? {
self.engine.route.increase();
if !self.engine.route.within(Route::MAX_SHOW_RULE_DEPTH) {
bail!(
@ -414,12 +122,12 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
return result;
}
if let Some((elem, local)) = content.to_styled() {
return self.styled(elem, local, styles);
if let Some(styled) = content.to_packed::<StyledElem>() {
return self.styled(styled, styles);
}
if let Some(children) = content.to_sequence() {
for elem in children {
if let Some(sequence) = content.to_packed::<SequenceElem>() {
for elem in &sequence.children {
self.accept(elem, styles)?;
}
return Ok(());
@ -472,15 +180,14 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
fn styled(
&mut self,
elem: &'a Content,
map: &'a Styles,
styled: &'a StyledElem,
styles: StyleChain<'a>,
) -> SourceResult<()> {
let stored = self.arenas.store(styles);
let styles = stored.chain(map);
self.interrupt_style(map, None)?;
self.accept(elem, styles)?;
self.interrupt_style(map, Some(styles))?;
let styles = stored.chain(&styled.styles);
self.interrupt_style(&styled.styles, None)?;
self.accept(&styled.child, styles)?;
self.interrupt_style(&styled.styles, Some(styles))?;
Ok(())
}
@ -490,13 +197,15 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
outer: Option<StyleChain<'a>>,
) -> SourceResult<()> {
if let Some(Some(span)) = local.interruption::<DocumentElem>() {
if self.doc.is_none() {
let Some(doc) = &self.doc else {
bail!(span, "document set rules are not allowed inside of containers");
}
};
if outer.is_none()
&& (!self.flow.0.is_empty()
&& (!doc.pages.is_empty()
|| !self.flow.0.is_empty()
|| !self.par.0.is_empty()
|| !self.list.items.is_empty())
|| !self.list.items.is_empty()
|| !self.cites.items.is_empty())
{
bail!(span, "document set rules must appear before any content");
}
@ -522,7 +231,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
if !self.cites.items.is_empty() {
let staged = mem::take(&mut self.cites.staged);
let (group, styles) = mem::take(&mut self.cites).finish();
self.accept(self.arenas.store(group), styles)?;
self.accept(self.arenas.store(group.pack()), styles)?;
for (content, styles) in staged {
self.accept(content, styles)?;
}
@ -547,7 +256,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
self.interrupt_list()?;
if !self.par.0.is_empty() {
let (par, styles) = mem::take(&mut self.par).finish();
self.accept(self.arenas.store(par), styles)?;
self.accept(self.arenas.store(par.pack()), styles)?;
}
Ok(())
@ -561,15 +270,15 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
self.interrupt_par()?;
let Some(doc) = &mut self.doc else { return Ok(()) };
if (doc.keep_next && styles.is_some()) || self.flow.0.has_strong_elements(last) {
let (children, shared, span) = mem::take(&mut self.flow).0.finish();
let styles = if shared == StyleChain::default() {
let (flow, trunk) = mem::take(&mut self.flow).finish();
let span = flow.span();
let styles = if trunk == StyleChain::default() {
styles.unwrap_or_default()
} else {
shared
trunk
};
let flow = FlowElem::new(children);
let page = PageElem::new(flow.pack().spanned(span));
self.accept(self.arenas.store(page.pack().spanned(span)), styles)?;
let page = PageElem::new(flow.pack()).pack().spanned(span);
self.accept(self.arenas.store(page), styles)?;
}
Ok(())
}
@ -614,6 +323,11 @@ impl<'a> DocBuilder<'a> {
false
}
fn finish(self) -> (Packed<DocumentElem>, StyleChain<'a>) {
let (children, trunk, span) = self.pages.finish();
(Packed::new(DocumentElem::new(children)).spanned(span), trunk)
}
}
impl Default for DocBuilder<'_> {
@ -688,6 +402,11 @@ impl<'a> FlowBuilder<'a> {
false
}
fn finish(self) -> (Packed<FlowElem>, StyleChain<'a>) {
let (children, trunk, span) = self.0.finish();
(Packed::new(FlowElem::new(children)).spanned(span), trunk)
}
}
/// Accepts paragraph content.
@ -718,9 +437,9 @@ impl<'a> ParBuilder<'a> {
false
}
fn finish(self) -> (Content, StyleChain<'a>) {
let (children, shared, span) = self.0.finish();
(ParElem::new(children).pack().spanned(span), shared)
fn finish(self) -> (Packed<ParElem>, StyleChain<'a>) {
let (children, trunk, span) = self.0.finish();
(Packed::new(ParElem::new(children)).spanned(span), trunk)
}
}
@ -761,7 +480,7 @@ impl<'a> ListBuilder<'a> {
}
fn finish(self) -> (Content, StyleChain<'a>) {
let (items, shared, span) = self.items.finish_iter();
let (items, trunk, span) = self.items.finish_iter();
let mut items = items.peekable();
let (first, _) = items.peek().unwrap();
let output = if first.is::<ListItem>() {
@ -812,7 +531,7 @@ impl<'a> ListBuilder<'a> {
} else {
unreachable!()
};
(output, shared)
(output, trunk)
}
}
@ -858,8 +577,8 @@ impl<'a> CiteGroupBuilder<'a> {
false
}
fn finish(self) -> (Content, StyleChain<'a>) {
fn finish(self) -> (Packed<CiteGroup>, StyleChain<'a>) {
let span = self.items.first().map(|cite| cite.span()).unwrap_or(Span::detached());
(CiteGroup::new(self.items).pack().spanned(span), self.styles)
(Packed::new(CiteGroup::new(self.items)).spanned(span), self.styles)
}
}

View File

@ -0,0 +1,312 @@
use std::cell::OnceCell;
use smallvec::smallvec;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
Content, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style,
StyleChain, Styles, Synthesize, Transformation,
};
use crate::introspection::{Locatable, Meta, MetaElem};
use crate::text::TextElem;
use crate::util::{hash128, BitSet};
/// What to do with an element when encountering it during realization.
struct Verdict<'a> {
/// Whether the element is already prepated (i.e. things that should only
/// happen once have happened).
prepared: bool,
/// A map of styles to apply to the element.
map: Styles,
/// An optional show rule transformation to apply to the element.
step: Option<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,
}
/// Whether the `target` element needs processing.
pub fn processable<'a>(
engine: &mut Engine,
target: &'a Content,
styles: StyleChain<'a>,
) -> bool {
verdict(engine, target, styles).is_some()
}
/// Processes the given `target` element when encountering it during realization.
pub fn process(
engine: &mut Engine,
target: &Content,
styles: StyleChain,
) -> SourceResult<Option<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 meta = None;
if !prepared {
meta = prepare(engine, &mut target, &mut map, styles)?;
}
// Apply a step, if there is one.
let mut output = match step {
Some(step) => {
// Errors in show rules don't terminate compilation immediately. We
// just continue with empty content for them and show all errors
// together, if they remain by the end of the introspection loop.
//
// This way, we can ignore errors that only occur in earlier
// iterations and also show more useful errors at once.
engine.delayed(|engine| show(engine, target, step, styles.chain(&map)))
}
None => target,
};
// If necessary, apply metadata generated in the preparation.
if let Some(meta) = meta {
output += meta.pack();
}
Ok(Some(output.styled_with_map(map)))
}
/// Inspects a target element and the current styles and determines how to
/// proceed with the styling.
fn verdict<'a>(
engine: &mut Engine,
target: &'a Content,
styles: StyleChain<'a>,
) -> Option<Verdict<'a>> {
let mut target = target;
let mut map = Styles::new();
let mut revoked = BitSet::new();
let mut step = None;
let mut slot;
let depth = OnceCell::new();
let prepared = target.is_prepared();
// Do pre-synthesis on a cloned element to be able to match on synthesized
// fields before real synthesis runs (during preparation). It's really
// unfortunate that we have to do this, but otherwise
// `show figure.where(kind: table)` won't work :(
if !prepared && target.can::<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;
}
if let Transformation::Style(transform) = &recipe.transform {
// If this is a show-set for an unprepared element, we need to apply
// it.
if !prepared {
map.apply(transform.clone());
}
} else if step.is_none() {
// Lazily compute the total number of recipes in the style chain. We
// need it to determine whether a particular show rule was already
// applied to the `target` previously. For this purpose, show rules
// are indexed from the top of the chain as the chain might grow to
// the bottom.
let depth =
*depth.get_or_init(|| styles.entries().filter_map(Style::recipe).count());
let index = RecipeIndex(depth - r);
if !target.is_guarded(index) && !revoked.contains(index.0) {
// If we find a matching, unguarded replacement show rule,
// remember it, but still continue searching for potential
// show-set styles that might change the verdict.
step = Some(ShowStep::Recipe(recipe, index));
// If we found a show rule and are already prepared, there is
// nothing else to do, so we can just break.
if prepared {
break;
}
}
}
r += 1;
}
// If we found no user-defined rule, also consider the built-in show rule.
if step.is_none() && target.can::<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.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,
target: &mut Content,
map: &mut Styles,
styles: StyleChain,
) -> SourceResult<Option<Packed<MetaElem>>> {
// Generate a location for the element, which uniquely identifies it in
// the document. This has some overhead, so we only do it for elements
// that are explicitly marked as locatable and labelled elements.
if target.can::<dyn Locatable>() || target.label().is_some() {
let location = engine.locator.locate(hash128(&target));
target.set_location(location);
}
// Apply built-in show-set rules. User-defined show-set rules are already
// considered in the map built while determining the verdict.
if let Some(show_settable) = target.with::<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));
// Ensure that this preparation only runs once by marking the element as
// prepared.
target.mark_prepared();
// Apply metadata be able to find the element in the frames.
// Do this after synthesis, so that it includes the synthesized fields.
if target.location().is_some() {
// Add a style to the whole element's subtree identifying it as
// belonging to the element.
map.set(MetaElem::set_data(smallvec![Meta::Elem(target.clone())]));
// Return an extra meta elem that will be attached so that the metadata
// styles are not lost in case the element's show rule results in
// nothing.
return Ok(Some(Packed::new(MetaElem::new()).spanned(target.span())));
}
Ok(None)
}
/// Apply a step.
fn show(
engine: &mut Engine,
target: Content,
step: ShowStep,
styles: StyleChain,
) -> SourceResult<Content> {
match step {
// Apply a user-defined show rule.
ShowStep::Recipe(recipe, guard) => match &recipe.selector {
// If the selector is a regex, the `target` is guaranteed to be a
// text element. This invokes special regex handling.
Some(Selector::Regex(regex)) => {
let text = target.into_packed::<TextElem>().unwrap();
show_regex(engine, &text, regex, recipe, guard)
}
// Just apply the recipe.
_ => recipe.apply(engine, target.guarded(guard)),
},
// If the verdict picks this step, the `target` is guaranteed to have a
// built-in show rule.
ShowStep::Builtin => target.with::<dyn Show>().unwrap().show(engine, styles),
}
}
/// Apply a regex show rule recipe to a target.
fn show_regex(
engine: &mut Engine,
elem: &Packed<TextElem>,
regex: &Regex,
recipe: &Recipe,
index: RecipeIndex,
) -> SourceResult<Content> {
let make = |s: &str| {
let mut fresh = elem.clone();
fresh.push_text(s.into());
fresh.pack()
};
let mut result = vec![];
let mut cursor = 0;
let text = elem.text();
for m in regex.find_iter(elem.text()) {
let start = m.start();
if cursor < start {
result.push(make(&text[cursor..start]));
}
let piece = make(m.as_str());
let transformed = recipe.apply(engine, piece)?;
result.push(transformed);
cursor = m.end();
}
if cursor < text.len() {
result.push(make(&text[cursor..]));
}
// In contrast to normal elements, which are guarded individually, for text
// show rules, we fully revoke the rule. This means that we can replace text
// with other text that rematches without running into infinite recursion
// problems.
//
// We do _not_ do this for all content because revoking e.g. a list show
// rule for all content resulting from that rule would be wrong: The list
// might contain nested lists. Moreover, replacing a normal element with one
// that rematches is bad practice: It can for instance also lead to
// surprising query results, so it's better to let the user deal with it.
// All these problems don't exist for text, so it's fine here.
Ok(Content::sequence(result).styled(Style::Revocation(index)))
}

View File

@ -1,4 +1,5 @@
use crate::foundations::{elem, Behave, Behaviour, Packed};
use crate::foundations::{elem, Packed};
use crate::realize::{Behave, Behaviour};
/// Inserts a line break.
///

View File

@ -2,7 +2,7 @@ use ecow::EcoString;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain};
use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain};
use crate::layout::{Em, Length};
use crate::text::{variant, SpaceElem, TextElem, TextSize};
use crate::World;
@ -134,9 +134,9 @@ fn search_text(content: &Content, sub: bool) -> Option<EcoString> {
Some(' '.into())
} else if let Some(elem) = content.to_packed::<TextElem>() {
convert_script(elem.text(), sub)
} else if let Some(children) = content.to_sequence() {
} else if let Some(sequence) = content.to_packed::<SequenceElem>() {
let mut full = EcoString::new();
for item in children {
for item in &sequence.children {
match search_text(item, sub) {
Some(text) => full.push_str(&text),
None => return None,

View File

@ -1,8 +1,8 @@
use crate::foundations::{
elem, Behave, Behaviour, Packed, PlainText, Repr, Unlabellable,
};
use ecow::EcoString;
use crate::foundations::{elem, Packed, PlainText, Repr, Unlabellable};
use crate::realize::{Behave, Behaviour};
/// A text space.
#[elem(Behave, Unlabellable, PlainText, Repr)]
pub struct SpaceElem {}