summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-02-01 14:30:17 +0100
committerGitHub <noreply@github.com>2024-02-01 13:30:17 +0000
commit7d33436e55f8b1aec06d136ebe095dd86bf23e57 (patch)
tree2c7b0673ef7a1992c03ef0b3aad4c25a29490400
parent426445edfc8d32d9ff8fcb79cda5b7765209f567 (diff)
Fix show-set semantics (#3311)
-rw-r--r--crates/typst-macros/src/elem.rs26
-rw-r--r--crates/typst/src/foundations/content.rs41
-rw-r--r--crates/typst/src/foundations/element.rs14
-rw-r--r--crates/typst/src/foundations/styles.rs10
-rw-r--r--crates/typst/src/math/equation.rs32
-rw-r--r--crates/typst/src/model/bibliography.rs21
-rw-r--r--crates/typst/src/model/cite.rs3
-rw-r--r--crates/typst/src/model/figure.rs34
-rw-r--r--crates/typst/src/model/footnote.rs27
-rw-r--r--crates/typst/src/model/heading.rs30
-rw-r--r--crates/typst/src/model/outline.rs17
-rw-r--r--crates/typst/src/model/quote.rs30
-rw-r--r--crates/typst/src/model/reference.rs5
-rw-r--r--crates/typst/src/realize/mod.rs339
-rw-r--r--crates/typst/src/text/raw.rs43
-rw-r--r--tests/ref/compiler/content-field.pngbin24702 -> 8252 bytes
-rw-r--r--tests/ref/compiler/show-set-func.pngbin0 -> 5772 bytes
-rw-r--r--tests/ref/compiler/show-set.pngbin0 -> 22168 bytes
-rw-r--r--tests/typ/compiler/content-field.typ29
-rw-r--r--tests/typ/compiler/show-selector.typ2
-rw-r--r--tests/typ/compiler/show-set-func.typ16
-rw-r--r--tests/typ/compiler/show-set.typ55
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
index 9fdcb667..3095ba8c 100644
--- a/tests/ref/compiler/content-field.png
+++ b/tests/ref/compiler/content-field.png
Binary files differ
diff --git a/tests/ref/compiler/show-set-func.png b/tests/ref/compiler/show-set-func.png
new file mode 100644
index 00000000..c5ff2489
--- /dev/null
+++ b/tests/ref/compiler/show-set-func.png
Binary files differ
diff --git a/tests/ref/compiler/show-set.png b/tests/ref/compiler/show-set.png
new file mode 100644
index 00000000..e87fc600
--- /dev/null
+++ b/tests/ref/compiler/show-set.png
Binary files differ
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])