diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-02-01 14:30:17 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-02-01 13:30:17 +0000 |
| commit | 7d33436e55f8b1aec06d136ebe095dd86bf23e57 (patch) | |
| tree | 2c7b0673ef7a1992c03ef0b3aad4c25a29490400 | |
| parent | 426445edfc8d32d9ff8fcb79cda5b7765209f567 (diff) | |
Fix show-set semantics (#3311)
22 files changed, 477 insertions, 297 deletions
diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index 3a31d3ef..eab359d6 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -866,6 +866,28 @@ fn create_fields_impl(element: &Elem) -> TokenStream { quote! { Fields::#enum_ident => #expr } }); + // Sets fields from the style chain. + let materializes = visible_non_ghost() + .filter(|field| !field.required && !field.synthesized) + .map(|field| { + let Field { ident, .. } = field; + let value = create_style_chain_access( + field, + false, + if field.ghost { quote!(None) } else { quote!(self.#ident.as_ref()) }, + ); + + if field.fold { + quote! { self.#ident = Some(#value); } + } else { + quote! { + if self.#ident.is_none() { + self.#ident = Some(#value); + } + } + } + }); + // Creation of the `fields` dictionary for inherent fields. let field_inserts = visible_non_ghost().map(|field| { let Field { ident, name, .. } = field; @@ -917,6 +939,10 @@ fn create_fields_impl(element: &Elem) -> TokenStream { } } + fn materialize(&mut self, styles: #foundations::StyleChain) { + #(#materializes)* + } + fn fields(&self) -> #foundations::Dict { let mut fields = #foundations::Dict::new(); #(#field_inserts)* diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index 49497b8f..6f912086 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -14,11 +14,10 @@ use smallvec::smallvec; use crate::diag::{SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - elem, func, scope, ty, Dict, Element, Fields, Finalize, Guard, IntoValue, Label, - NativeElement, Recipe, Repr, Selector, Str, Style, StyleChain, Styles, Synthesize, - Value, + elem, func, scope, ty, Dict, Element, Fields, Guard, IntoValue, Label, NativeElement, + Recipe, Repr, Selector, Str, Style, StyleChain, Styles, Value, }; -use crate::introspection::{Locatable, Location, Meta, MetaElem}; +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::syntax::Span; @@ -142,6 +141,16 @@ impl Content { self } + /// Check whether a show rule recipe is disabled. + pub fn is_guarded(&self, guard: Guard) -> bool { + self.inner.lifecycle.contains(guard.0) + } + + /// Whether this content has already been prepared. + pub fn is_prepared(&self) -> bool { + self.inner.lifecycle.contains(0) + } + /// Set the location of the content. pub fn set_location(&mut self, location: Location) { self.make_mut().location = Some(location); @@ -153,25 +162,6 @@ impl Content { self } - /// Whether the content needs to be realized specially. - pub fn needs_preparation(&self) -> bool { - !self.is_prepared() - && (self.can::<dyn Locatable>() - || self.can::<dyn Synthesize>() - || self.can::<dyn Finalize>() - || self.label().is_some()) - } - - /// Check whether a show rule recipe is disabled. - pub fn is_guarded(&self, guard: Guard) -> bool { - self.inner.lifecycle.contains(guard.0) - } - - /// Whether this content has already been prepared. - pub fn is_prepared(&self) -> bool { - self.inner.lifecycle.contains(0) - } - /// Mark this content as prepared. pub fn mark_prepared(&mut self) { self.make_mut().lifecycle.insert(0); @@ -227,6 +217,11 @@ impl Content { self.get_by_name(name).ok_or_else(|| missing_field(name)) } + /// Resolve all fields with the styles and save them in-place. + pub fn materialize(&mut self, styles: StyleChain) { + self.make_mut().elem.materialize(styles); + } + /// Create a new sequence element from multiples elements. pub fn sequence(iter: impl IntoIterator<Item = Self>) -> Self { let mut iter = iter.into_iter(); diff --git a/crates/typst/src/foundations/element.rs b/crates/typst/src/foundations/element.rs index 489077e4..6d73a896 100644 --- a/crates/typst/src/foundations/element.rs +++ b/crates/typst/src/foundations/element.rs @@ -223,6 +223,9 @@ pub trait Fields { /// Get the field with the given ID in the presence of styles. fn field_with_styles(&self, id: u8, styles: StyleChain) -> Option<Value>; + /// Resolve all fields with the styles and save them in-place. + fn materialize(&mut self, styles: StyleChain); + /// Get the fields of the element. fn fields(&self) -> Dict; } @@ -282,17 +285,20 @@ pub trait Synthesize { -> SourceResult<()>; } -/// The base recipe for an element. +/// Defines a built-in show rule for an element. pub trait Show { /// Execute the base recipe for this element. fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content>; } -/// Post-process an element after it was realized. -pub trait Finalize { +/// Defines built-in show set rules for an element. +/// +/// This is a bit more powerful than a user-defined show-set because it can +/// access the element's fields. +pub trait ShowSet { /// Finalize the fully realized form of the element. Use this for effects /// that should work even in the face of a user-defined show rule. - fn finalize(&self, realized: Content, styles: StyleChain) -> Content; + fn show_set(&self, styles: StyleChain) -> Styles; } /// How the element interacts with other elements. diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs index 6946d076..12ba2876 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -147,9 +147,15 @@ impl Styles { } } +impl From<Prehashed<Style>> for Styles { + fn from(style: Prehashed<Style>) -> Self { + Self(eco_vec![style]) + } +} + impl From<Style> for Styles { - fn from(entry: Style) -> Self { - Self(eco_vec![Prehashed::new(entry)]) + fn from(style: Style) -> Self { + Self(eco_vec![Prehashed::new(style)]) } } diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs index 91ff2519..59aff4d5 100644 --- a/crates/typst/src/math/equation.rs +++ b/crates/typst/src/math/equation.rs @@ -5,7 +5,7 @@ use unicode_math_class::MathClass; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, Content, Finalize, NativeElement, Packed, Resolve, Smart, StyleChain, + elem, Content, NativeElement, Packed, Resolve, ShowSet, Smart, StyleChain, Styles, Synthesize, }; use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; @@ -49,7 +49,7 @@ use crate::World; #[elem( Locatable, Synthesize, - Finalize, + ShowSet, LayoutSingle, LayoutMath, Count, @@ -145,27 +145,23 @@ impl Synthesize for Packed<EquationElem> { } }; - let elem = self.as_mut(); - elem.push_block(elem.block(styles)); - elem.push_numbering(elem.numbering(styles)); - elem.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); - + self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); Ok(()) } } -impl Finalize for Packed<EquationElem> { - fn finalize(&self, realized: Content, style: StyleChain) -> Content { - let mut realized = realized; - if self.block(style) { - realized = realized.styled(AlignElem::set_alignment(Alignment::CENTER)); - realized = realized.styled(EquationElem::set_size(MathSize::Display)); +impl ShowSet for Packed<EquationElem> { + fn show_set(&self, styles: StyleChain) -> Styles { + let mut out = Styles::new(); + if self.block(styles) { + out.set(AlignElem::set_alignment(Alignment::CENTER)); + out.set(EquationElem::set_size(MathSize::Display)); } - realized - .styled(TextElem::set_weight(FontWeight::from_number(450))) - .styled(TextElem::set_font(FontList(vec![FontFamily::new( - "New Computer Modern Math", - )]))) + out.set(TextElem::set_weight(FontWeight::from_number(450))); + out.set(TextElem::set_font(FontList(vec![FontFamily::new( + "New Computer Modern Math", + )]))); + out } } diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs index e64bd538..6f31fd07 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -23,9 +23,9 @@ use crate::diag::{bail, error, At, FileError, SourceResult, StrResult}; use crate::engine::Engine; use crate::eval::{eval_string, EvalMode}; use crate::foundations::{ - cast, elem, ty, Args, Array, Bytes, CastInfo, Content, Finalize, FromValue, - IntoValue, Label, NativeElement, Packed, Reflect, Repr, Scope, Show, Smart, Str, - StyleChain, Synthesize, Type, Value, + cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label, + NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain, + Styles, Synthesize, Type, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ @@ -84,7 +84,7 @@ use crate::World; /// /// #bibliography("works.bib") /// ``` -#[elem(Locatable, Synthesize, Show, Finalize, LocalName)] +#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] pub struct BibliographyElem { /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. #[required] @@ -199,8 +199,6 @@ impl BibliographyElem { impl Synthesize for Packed<BibliographyElem> { fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { let elem = self.as_mut(); - elem.push_full(elem.full(styles)); - elem.push_style(elem.style(styles)); elem.push_lang(TextElem::lang_in(styles)); elem.push_region(TextElem::region_in(styles)); Ok(()) @@ -275,12 +273,13 @@ impl Show for Packed<BibliographyElem> { } } -impl Finalize for Packed<BibliographyElem> { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { +impl ShowSet for Packed<BibliographyElem> { + fn show_set(&self, _: StyleChain) -> Styles { const INDENT: Em = Em::new(1.0); - realized - .styled(HeadingElem::set_numbering(None)) - .styled(PadElem::set_left(INDENT.into())) + let mut out = Styles::new(); + out.set(HeadingElem::set_numbering(None)); + out.set(PadElem::set_left(INDENT.into())); + out } } diff --git a/crates/typst/src/model/cite.rs b/crates/typst/src/model/cite.rs index e61056e8..2cd86b00 100644 --- a/crates/typst/src/model/cite.rs +++ b/crates/typst/src/model/cite.rs @@ -111,9 +111,6 @@ pub struct CiteElem { impl Synthesize for Packed<CiteElem> { fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { let elem = self.as_mut(); - elem.push_supplement(elem.supplement(styles)); - elem.push_form(elem.form(styles)); - elem.push_style(elem.style(styles)); elem.push_lang(TextElem::lang_in(styles)); elem.push_region(TextElem::region_in(styles)); Ok(()) diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs index b6e743df..cac9a379 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -7,8 +7,8 @@ use ecow::EcoString; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, select_where, Content, Element, Finalize, NativeElement, Packed, - Selector, Show, Smart, StyleChain, Synthesize, + cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector, + Show, ShowSet, Smart, StyleChain, Styles, Synthesize, }; use crate::introspection::{ Count, Counter, CounterKey, CounterUpdate, Locatable, Location, @@ -101,7 +101,7 @@ use crate::visualize::ImageElem; /// caption: [I'm up here], /// ) /// ``` -#[elem(scope, Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)] +#[elem(scope, Locatable, Synthesize, Count, Show, ShowSet, Refable, Outlinable)] pub struct FigureElem { /// The content of the figure. Often, an [image]($image). #[required] @@ -165,7 +165,6 @@ pub struct FigureElem { /// supplement: [Atom], /// ) /// ``` - #[default(Smart::Auto)] pub kind: Smart<FigureKind>, /// The figure's supplement. @@ -230,9 +229,7 @@ impl Synthesize for Packed<FigureElem> { ) -> SourceResult<()> { let span = self.span(); let location = self.location(); - let elem = self.as_mut(); - let placement = elem.placement(styles); let numbering = elem.numbering(styles); // Determine the figure's kind. @@ -295,13 +292,10 @@ impl Synthesize for Packed<FigureElem> { caption.push_figure_location(location); } - elem.push_placement(placement); - elem.push_caption(caption); elem.push_kind(Smart::Custom(kind)); elem.push_supplement(Smart::Custom(supplement.map(Supplement::Content))); - elem.push_numbering(numbering); - elem.push_outlined(elem.outlined(styles)); elem.push_counter(Some(counter)); + elem.push_caption(caption); Ok(()) } @@ -342,10 +336,11 @@ impl Show for Packed<FigureElem> { } } -impl Finalize for Packed<FigureElem> { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - // Allow breakable figures with `show figure: set block(breakable: true)`. - realized.styled(BlockElem::set_breakable(false)) +impl ShowSet for Packed<FigureElem> { + fn show_set(&self, _: StyleChain) -> Styles { + // Still allows breakable figures with + // `show figure: set block(breakable: true)`. + BlockElem::set_breakable(false).wrap().into() } } @@ -434,7 +429,7 @@ impl Outlinable for Packed<FigureElem> { /// caption: [A rectangle], /// ) /// ``` -#[elem(name = "caption", Synthesize, Show)] +#[elem(name = "caption", Show)] pub struct FigureCaption { /// The caption's position in the figure. Either `{top}` or `{bottom}`. /// @@ -551,15 +546,6 @@ impl FigureCaption { } } -impl Synthesize for Packed<FigureCaption> { - fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { - let elem = self.as_mut(); - elem.push_position(elem.position(styles)); - elem.push_separator(Smart::Custom(elem.get_separator(styles))); - Ok(()) - } -} - impl Show for Packed<FigureCaption> { #[typst_macros::time(name = "figure.caption", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { diff --git a/crates/typst/src/model/footnote.rs b/crates/typst/src/model/footnote.rs index ba4d2f47..dbff2c1e 100644 --- a/crates/typst/src/model/footnote.rs +++ b/crates/typst/src/model/footnote.rs @@ -4,8 +4,8 @@ use std::str::FromStr; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Content, Finalize, Label, NativeElement, Packed, Show, Smart, - StyleChain, Synthesize, + cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart, + StyleChain, Styles, }; use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location}; use crate::layout::{Abs, Em, HElem, Length, Ratio}; @@ -50,7 +50,7 @@ use crate::visualize::{LineElem, Stroke}; /// apply to the footnote's content. See [here][issue] for more information. /// /// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440 -#[elem(scope, Locatable, Synthesize, Show, Count)] +#[elem(scope, Locatable, Show, Count)] pub struct FootnoteElem { /// How to number footnotes. /// @@ -123,14 +123,6 @@ impl Packed<FootnoteElem> { } } -impl Synthesize for Packed<FootnoteElem> { - fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { - let elem = self.as_mut(); - elem.push_numbering(elem.numbering(styles).clone()); - Ok(()) - } -} - impl Show for Packed<FootnoteElem> { #[typst_macros::time(name = "footnote", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { @@ -188,7 +180,7 @@ cast! { /// #footnote[It's down here] /// has red text! /// ``` -#[elem(name = "entry", title = "Footnote Entry", Show, Finalize)] +#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)] pub struct FootnoteEntry { /// The footnote for this entry. It's location can be used to determine /// the footnote counter state. @@ -303,13 +295,14 @@ impl Show for Packed<FootnoteEntry> { } } -impl Finalize for Packed<FootnoteEntry> { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { +impl ShowSet for Packed<FootnoteEntry> { + fn show_set(&self, _: StyleChain) -> Styles { let text_size = Em::new(0.85); let leading = Em::new(0.5); - realized - .styled(ParElem::set_leading(leading.into())) - .styled(TextElem::set_size(TextSize(text_size.into()))) + let mut out = Styles::new(); + out.set(ParElem::set_leading(leading.into())); + out.set(TextElem::set_size(TextSize(text_size.into()))); + out } } diff --git a/crates/typst/src/model/heading.rs b/crates/typst/src/model/heading.rs index ef5e4007..c3ed5275 100644 --- a/crates/typst/src/model/heading.rs +++ b/crates/typst/src/model/heading.rs @@ -3,7 +3,7 @@ use std::num::NonZeroUsize; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - elem, Content, Finalize, NativeElement, Packed, Show, Smart, StyleChain, Styles, + elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, }; use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; @@ -43,7 +43,7 @@ use crate::util::{option_eq, NonZeroExt}; /// Headings have dedicated syntax: They can be created by starting a line with /// one or multiple equals signs, followed by a space. The number of equals /// signs determines the heading's logical nesting depth. -#[elem(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable, Outlinable)] +#[elem(Locatable, Synthesize, Count, Show, ShowSet, LocalName, Refable, Outlinable)] pub struct HeadingElem { /// The logical nesting depth of the heading, starting from one. #[default(NonZeroUsize::ONE)] @@ -140,13 +140,7 @@ impl Synthesize for Packed<HeadingElem> { } }; - let elem = self.as_mut(); - elem.push_level(elem.level(styles)); - elem.push_numbering(elem.numbering(styles).clone()); - elem.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); - elem.push_outlined(elem.outlined(styles)); - elem.push_bookmarked(elem.bookmarked(styles)); - + self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); Ok(()) } } @@ -166,8 +160,8 @@ impl Show for Packed<HeadingElem> { } } -impl Finalize for Packed<HeadingElem> { - fn finalize(&self, realized: Content, styles: StyleChain) -> Content { +impl ShowSet for Packed<HeadingElem> { + fn show_set(&self, styles: StyleChain) -> Styles { let level = (**self).level(styles).get(); let scale = match level { 1 => 1.4, @@ -179,13 +173,13 @@ impl Finalize for Packed<HeadingElem> { let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale; let below = Em::new(0.75) / scale; - let mut styles = Styles::new(); - styles.set(TextElem::set_size(TextSize(size.into()))); - styles.set(TextElem::set_weight(FontWeight::BOLD)); - styles.set(BlockElem::set_above(VElem::block_around(above.into()))); - styles.set(BlockElem::set_below(VElem::block_around(below.into()))); - styles.set(BlockElem::set_sticky(true)); - realized.styled_with_map(styles) + let mut out = Styles::new(); + out.set(TextElem::set_size(TextSize(size.into()))); + out.set(TextElem::set_weight(FontWeight::BOLD)); + out.set(BlockElem::set_above(VElem::block_around(above.into()))); + out.set(BlockElem::set_below(VElem::block_around(below.into()))); + out.set(BlockElem::set_sticky(true)); + out } } diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs index 833b035c..14f711c6 100644 --- a/crates/typst/src/model/outline.rs +++ b/crates/typst/src/model/outline.rs @@ -4,8 +4,8 @@ use std::str::FromStr; use crate::diag::{bail, At, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, select_where, Content, Finalize, Func, LocatableSelector, - NativeElement, Packed, Show, Smart, StyleChain, + cast, elem, scope, select_where, Content, Func, LocatableSelector, NativeElement, + Packed, Show, ShowSet, Smart, StyleChain, Styles, }; use crate::introspection::{Counter, CounterKey, Locatable}; use crate::layout::{BoxElem, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing}; @@ -57,7 +57,7 @@ use crate::util::{option_eq, NonZeroExt}; /// `title` and `indent` parameters. If desired, however, it is possible to have /// more control over the outline's look and style through the /// [`outline.entry`]($outline.entry) element. -#[elem(scope, keywords = ["Table of Contents"], Show, Finalize, LocalName)] +#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)] pub struct OutlineElem { /// The title of the outline. /// @@ -250,11 +250,12 @@ impl Show for Packed<OutlineElem> { } } -impl Finalize for Packed<OutlineElem> { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - realized - .styled(HeadingElem::set_outlined(false)) - .styled(HeadingElem::set_numbering(None)) +impl ShowSet for Packed<OutlineElem> { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(HeadingElem::set_outlined(false)); + out.set(HeadingElem::set_numbering(None)); + out } } diff --git a/crates/typst/src/model/quote.rs b/crates/typst/src/model/quote.rs index 311fd464..883f876e 100644 --- a/crates/typst/src/model/quote.rs +++ b/crates/typst/src/model/quote.rs @@ -1,8 +1,8 @@ use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Finalize, Label, NativeElement, Packed, Show, Smart, StyleChain, - Synthesize, + cast, elem, Content, Label, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, + Styles, }; use crate::layout::{Alignment, BlockElem, Em, HElem, PadElem, Spacing, VElem}; use crate::model::{CitationForm, CiteElem}; @@ -40,7 +40,7 @@ use crate::text::{SmartQuoteElem, SpaceElem, TextElem}; /// flame of Udûn. Go back to the Shadow! You cannot pass. /// ] /// ``` -#[elem(Finalize, Show, Synthesize)] +#[elem(ShowSet, Show)] pub struct QuoteElem { /// Whether this is a block quote. /// @@ -145,15 +145,6 @@ cast! { label: Label => Self::Label(label), } -impl Synthesize for Packed<QuoteElem> { - fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { - let elem = self.as_mut(); - elem.push_block(elem.block(styles)); - elem.push_quotes(elem.quotes(styles)); - Ok(()) - } -} - impl Show for Packed<QuoteElem> { #[typst_macros::time(name = "quote", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { @@ -205,15 +196,16 @@ impl Show for Packed<QuoteElem> { } } -impl Finalize for Packed<QuoteElem> { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { +impl ShowSet for Packed<QuoteElem> { + fn show_set(&self, _: StyleChain) -> Styles { let x = Em::new(1.0).into(); let above = Em::new(2.4).into(); let below = Em::new(1.8).into(); - realized - .styled(PadElem::set_left(x)) - .styled(PadElem::set_right(x)) - .styled(BlockElem::set_above(VElem::block_around(above))) - .styled(BlockElem::set_below(VElem::block_around(below))) + let mut out = Styles::new(); + out.set(PadElem::set_left(x)); + out.set(PadElem::set_right(x)); + out.set(BlockElem::set_above(VElem::block_around(above))); + out.set(BlockElem::set_below(VElem::block_around(below))); + out } } diff --git a/crates/typst/src/model/reference.rs b/crates/typst/src/model/reference.rs index b96d0a53..c6ec93a0 100644 --- a/crates/typst/src/model/reference.rs +++ b/crates/typst/src/model/reference.rs @@ -251,8 +251,11 @@ fn to_citation( }, )); + if let Some(loc) = reference.location() { + elem.set_location(loc); + } + elem.synthesize(engine, styles)?; - elem.set_location(reference.location().unwrap()); Ok(elem) } diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index f5555597..b7614b7e 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -5,6 +5,7 @@ mod behave; pub use self::behave::BehavedBuilder; use std::borrow::Cow; +use std::cell::OnceCell; use std::mem; use smallvec::smallvec; @@ -13,8 +14,9 @@ use typed_arena::Arena; use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; use crate::foundations::{ - Behave, Behaviour, Content, Finalize, Guard, NativeElement, Packed, Recipe, Selector, - Show, StyleChain, StyleVec, StyleVecBuilder, Styles, Synthesize, + Behave, Behaviour, Content, Guard, NativeElement, Packed, Recipe, Regex, Selector, + Show, ShowSet, StyleChain, StyleVec, StyleVecBuilder, Styles, Synthesize, + Transformation, }; use crate::introspection::{Locatable, Meta, MetaElem}; use crate::layout::{ @@ -56,7 +58,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>() && !applicable(content, styles) { + if content.can::<dyn LayoutMultiple>() && verdict(engine, content, styles).is_none() { return Ok((Cow::Borrowed(content), styles)); } @@ -69,161 +71,260 @@ pub fn realize_block<'a>( Ok((Cow::Owned(FlowElem::new(children.to_vec()).pack().spanned(span)), shared)) } -/// Whether the target is affected by show rules in the given style chain. -pub fn applicable(target: &Content, styles: StyleChain) -> bool { - if target.needs_preparation() || target.can::<dyn Show>() { - return true; +/// 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)?; } - // Find out how many recipes there are. - let mut n = styles.recipes().count(); + // Apply the step. + let mut output = match step { + // Apply a user-defined show rule. + Some(Step::Recipe(recipe, guard)) => show(engine, target, recipe, guard)?, - // Find out whether any recipe matches and is unguarded. - for recipe in styles.recipes() { - if !target.is_guarded(Guard(n)) && recipe.applicable(target, styles) { - return true; + // If the verdict picks this step, the `target` is guaranteed + // to have a built-in show rule. + Some(Step::Builtin) => { + target.with::<dyn Show>().unwrap().show(engine, styles.chain(&map))? } - n -= 1; + + // Nothing to do. + None => target, + }; + + // If necessary, apply metadata generated in the preparation. + if let Some(meta) = meta { + output += meta.pack(); } - false + Ok(Some(output.styled_with_map(map))) } -/// Apply the show rules in the given style chain to a target. -pub fn realize( +/// 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 transformation step to apply to the element. + step: Option<Step<'a>>, +} + +/// An optional transformation step to apply to an element. +enum Step<'a> { + /// A user-defined transformational show rule. + Recipe(&'a Recipe, Guard), + /// 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: &Content, - styles: StyleChain, -) -> SourceResult<Option<Content>> { - // Pre-process. - if target.needs_preparation() { - let mut elem = target.clone(); - if target.can::<dyn Locatable>() || target.label().is_some() { - let location = engine.locator.locate(hash128(target)); - elem.set_location(location); + target: &'a Content, + styles: StyleChain<'a>, +) -> Option<Verdict<'a>> { + let mut target = target; + let mut map = Styles::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; + } + + for (i, recipe) in styles.recipes().enumerate() { + // We're not interested in recipes that don't match. + if !recipe.applicable(target, styles) { + continue; } - if let Some(synthesizable) = elem.with_mut::<dyn Synthesize>() { - synthesizable.synthesize(engine, styles)?; + 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.recipes().count()); + let guard = Guard(depth - i); + + if !target.is_guarded(guard) { + // 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(Step::Recipe(recipe, guard)); + + // If we found a show rule and are already prepared, there is + // nothing else to do, so we can just break. + if prepared { + break; + } + } } + } - elem.mark_prepared(); + // If we found no user-defined rule, also consider the built-in show rule. + if step.is_none() && target.can::<dyn Show>() { + step = Some(Step::Builtin); + } - let span = elem.span(); - let meta = elem.location().is_some().then(|| Meta::Elem(elem.clone())); + // 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; + } - let mut content = elem; - if let Some(finalizable) = target.with::<dyn Finalize>() { - content = finalizable.finalize(content, styles); - } + Some(Verdict { prepared, map, step }) +} - if let Some(meta) = meta { - return Ok(Some( - (content + MetaElem::new().pack().spanned(span)) - .styled(MetaElem::set_data(smallvec![meta])), - )); - } else { - return Ok(Some(content)); - } +/// 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); } - // Find out how many recipes there are. - let mut n = styles.recipes().count(); + // 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)); + } - // Find an applicable show rule recipe. - for recipe in styles.recipes() { - let guard = Guard(n); - if !target.is_guarded(guard) && recipe.applicable(target, styles) { - if let Some(content) = try_apply(engine, target, recipe, guard)? { - return Ok(Some(content)); - } - } - n -= 1; + // 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))?; } - // Apply the built-in show rule if there was no matching recipe. - if let Some(showable) = target.with::<dyn Show>() { - return Ok(Some(showable.show(engine, styles)?)); + // 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) } -/// Try to apply a recipe to the target. -fn try_apply( +/// Apply a user-defined show rule. +fn show( engine: &mut Engine, - target: &Content, + target: Content, recipe: &Recipe, guard: Guard, -) -> SourceResult<Option<Content>> { +) -> SourceResult<Content> { match &recipe.selector { - Some(Selector::Elem(element, _)) => { - if target.func() != *element { - return Ok(None); - } - - recipe.apply(engine, target.clone().guarded(guard)).map(Some) - } - - Some(Selector::Label(label)) => { - if target.label() != Some(*label) { - return Ok(None); - } - - recipe.apply(engine, target.clone().guarded(guard)).map(Some) - } - Some(Selector::Regex(regex)) => { - let Some(elem) = target.to_packed::<TextElem>() else { - return Ok(None); - }; - - 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])); - } + // If the verdict picks this rule, the `target` is guaranteed + // to be a text element. + let text = target.into_packed::<TextElem>().unwrap(); + show_regex(engine, &text, regex, recipe, guard) + } + _ => recipe.apply(engine, target.guarded(guard)), + } +} - let piece = make(m.as_str()).guarded(guard); - let transformed = recipe.apply(engine, piece)?; - result.push(transformed); - cursor = m.end(); - } +/// Apply a regex show rule recipe to a target. +fn show_regex( + engine: &mut Engine, + elem: &Packed<TextElem>, + regex: &Regex, + recipe: &Recipe, + guard: Guard, +) -> SourceResult<Content> { + let make = |s: &str| { + let mut fresh = elem.clone(); + fresh.push_text(s.into()); + fresh.pack() + }; - if result.is_empty() { - return Ok(None); - } + let mut result = vec![]; + let mut cursor = 0; - if cursor < text.len() { - result.push(make(&text[cursor..])); - } + let text = elem.text(); - Ok(Some(Content::sequence(result))) + for m in regex.find_iter(elem.text()) { + let start = m.start(); + if cursor < start { + result.push(make(&text[cursor..start])); } - // Not supported here. - Some( - Selector::Or(_) - | Selector::And(_) - | Selector::Location(_) - | Selector::Can(_) - | Selector::Before { .. } - | Selector::After { .. }, - ) => Ok(None), + let piece = make(m.as_str()).guarded(guard); + let transformed = recipe.apply(engine, piece)?; + result.push(transformed); + cursor = m.end(); + } - None => Ok(None), + if cursor < text.len() { + result.push(make(&text[cursor..])); } + + Ok(Content::sequence(result)) } /// Builds a document or a flow element from content. diff --git a/crates/typst/src/text/raw.rs b/crates/typst/src/text/raw.rs index 519adbe3..e6918a7d 100644 --- a/crates/typst/src/text/raw.rs +++ b/crates/typst/src/text/raw.rs @@ -12,8 +12,8 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, FileError, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Args, Array, Bytes, Content, Finalize, Fold, NativeElement, - Packed, PlainText, Show, Smart, StyleChain, Styles, Synthesize, Value, + cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed, + PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, }; use crate::layout::{BlockElem, Em, HAlignment}; use crate::model::Figurable; @@ -74,7 +74,7 @@ type LineFn<'a> = &'a mut dyn FnMut(i64, Range<usize>, &mut Vec<Content>); title = "Raw Text / Code", Synthesize, Show, - Finalize, + ShowSet, LocalName, Figurable, PlainText @@ -289,11 +289,17 @@ impl RawElem { impl Synthesize for Packed<RawElem> { fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { - let span = self.span(); - let elem = self.as_mut(); + let seq = self.highlight(styles); + self.push_lines(seq); + Ok(()) + } +} - let lang = elem.lang(styles).clone(); - elem.push_lang(lang); +impl Packed<RawElem> { + #[comemo::memoize] + fn highlight(&self, styles: StyleChain) -> Vec<Packed<RawLine>> { + let elem = self.as_ref(); + let span = self.span(); let mut text = elem.text().clone(); if text.contains('\t') { @@ -389,9 +395,7 @@ impl Synthesize for Packed<RawElem> { })); }; - elem.push_lines(seq); - - Ok(()) + seq } } @@ -421,16 +425,15 @@ impl Show for Packed<RawElem> { } } -impl Finalize for Packed<RawElem> { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - let mut styles = Styles::new(); - styles.set(TextElem::set_overhang(false)); - styles.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); - styles.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); - styles - .set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); - styles.set(SmartQuoteElem::set_enabled(false)); - realized.styled_with_map(styles) +impl ShowSet for Packed<RawElem> { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(TextElem::set_overhang(false)); + 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)); + out } } diff --git a/tests/ref/compiler/content-field.png b/tests/ref/compiler/content-field.png Binary files differindex 9fdcb667..3095ba8c 100644 --- a/tests/ref/compiler/content-field.png +++ b/tests/ref/compiler/content-field.png diff --git a/tests/ref/compiler/show-set-func.png b/tests/ref/compiler/show-set-func.png Binary files differnew file mode 100644 index 00000000..c5ff2489 --- /dev/null +++ b/tests/ref/compiler/show-set-func.png diff --git a/tests/ref/compiler/show-set.png b/tests/ref/compiler/show-set.png Binary files differnew file mode 100644 index 00000000..e87fc600 --- /dev/null +++ b/tests/ref/compiler/show-set.png diff --git a/tests/typ/compiler/content-field.typ b/tests/typ/compiler/content-field.typ index f8adfc42..dab4ec4b 100644 --- a/tests/typ/compiler/content-field.typ +++ b/tests/typ/compiler/content-field.typ @@ -1,19 +1,30 @@ -// Tests for field introspection. +// Tests content field access. --- -// Verify that non-inherent fields are hidden if not set. -#show figure: it => [ - `repr(it)`: #repr(it) \ - `it.has("gap"): `#repr(it.has("gap")) \ -] +// Ensure that fields from set rules are materialized into the element before +// a show rule runs. +#set table(columns: (10pt, auto)) +#show table: it => it.columns +#table[A][B][C][D] -#figure[] +--- +// Test it again with a different element. +#set heading(numbering: "(I)") +#show heading: set text(size: 11pt, weight: "regular") +#show heading: it => it.numbering += Heading -#figure([], gap: 1pt) +--- +// Test it with query. +#set raw(lang: "rust") +#locate(loc => { + let elem = query(<myraw>, loc).first() + elem.lang +}) +`raw` <myraw> --- // Integrated test for content fields. - #let compute(equation, ..vars) = { let vars = vars.named() let f(elem) = { diff --git a/tests/typ/compiler/show-selector.typ b/tests/typ/compiler/show-selector.typ index 6bc08f7a..48a26014 100644 --- a/tests/typ/compiler/show-selector.typ +++ b/tests/typ/compiler/show-selector.typ @@ -31,9 +31,9 @@ You can use the ```rs *const T``` pointer or the ```rs &mut T``` reference. --- +#show heading: set text(green) #show heading.where(level: 1): set text(red) #show heading.where(level: 2): set text(blue) -#show heading: set text(green) = Red == Blue === Green diff --git a/tests/typ/compiler/show-set-func.typ b/tests/typ/compiler/show-set-func.typ new file mode 100644 index 00000000..0447d946 --- /dev/null +++ b/tests/typ/compiler/show-set-func.typ @@ -0,0 +1,16 @@ +// Test set rules on an element in show rules for said element. + +--- +// These are both red because in the expanded form, `set text(red)` ends up +// closer to the content than `set text(blue)`. +#show strong: it => { set text(red); it } +Hello *World* + +#show strong: it => { set text(blue); it } +Hello *World* + +--- +// This doesn't have an effect. An element is materialized before any show +// rules run. +#show heading: it => { set heading(numbering: "(I)"); it } += Heading diff --git a/tests/typ/compiler/show-set.typ b/tests/typ/compiler/show-set.typ new file mode 100644 index 00000000..e336f517 --- /dev/null +++ b/tests/typ/compiler/show-set.typ @@ -0,0 +1,55 @@ +// Test show-set rules. + +--- +// Test overriding show-set rules. +#show strong: set text(red) +Hello *World* + +#show strong: set text(blue) +Hello *World* + +--- +// Test show-set rule on the same element. +#set figure(supplement: [Default]) +#show figure.where(kind: table): set figure(supplement: [Tableau]) +#figure( + table(columns: 2)[A][B][C][D], + caption: [Four letters], +) + +--- +// Test both things at once. +#show heading: set text(red) += Level 1 +== Level 2 + +#show heading.where(level: 1): set text(blue) +#show heading.where(level: 1): set text(green) +#show heading.where(level: 1): set heading(numbering: "(I)") += Level 1 +== Level 2 + +--- +// Test setting the thing we just matched on. +// This is quite cursed, but it works. +#set heading(numbering: "(I)") +#show heading.where(numbering: "(I)"): set heading(numbering: "1.") += Heading + +--- +// Same thing, but even more cursed, because `kind` is synthesized. +#show figure.where(kind: table): set figure(kind: raw) +#figure(table[A], caption: [Code]) + +--- +// Test that show-set rules on the same element don't affect each other. This +// could be implemented, but isn't as of yet. +#show heading.where(level: 1): set heading(numbering: "(I)") +#show heading.where(numbering: "(I)"): set text(red) += Heading + +--- +// Test show-set rules on layoutable element to ensure it is realized +// even though it implements `LayoutMultiple`. +#show table: set text(red) +#pad(table(columns: 4)[A][B][C][D]) |
