diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index a3e2a0079..d689aa11d 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -93,7 +93,7 @@ impl Elem { /// Fields that are visible to the user. fn visible_fields(&self) -> impl Iterator + Clone { - self.real_fields().filter(|field| !field.internal && !field.ghost) + self.real_fields().filter(|field| !field.internal) } } @@ -509,7 +509,11 @@ fn create_field_method(field: &Field) -> TokenStream { quote! { (&self, styles: #foundations::StyleChain) -> #output } }; - let mut value = create_style_chain_access(field, quote! { self.#ident.as_ref() }); + let mut value = create_style_chain_access( + field, + field.borrowed, + quote! { self.#ident.as_ref() }, + ); if field.resolve { value = quote! { #foundations::Resolve::resolve(#value, styles) }; } @@ -530,7 +534,7 @@ fn create_field_in_method(field: &Field) -> TokenStream { let ref_ = field.borrowed.then(|| quote! { & }); - let mut value = create_style_chain_access(field, quote! { None }); + let mut value = create_style_chain_access(field, field.borrowed, quote! { None }); if field.resolve { value = quote! { #foundations::Resolve::resolve(#value, styles) }; } @@ -560,16 +564,20 @@ fn create_set_field_method(field: &Field) -> TokenStream { } /// Create a style chain access method for a field. -fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStream { +fn create_style_chain_access( + field: &Field, + borrowed: bool, + inherent: TokenStream, +) -> TokenStream { let Field { ty, default, enum_ident, const_ident, .. } = field; - let getter = match (field.fold, field.borrowed) { + let getter = match (field.fold, borrowed) { (false, false) => quote! { get }, (false, true) => quote! { get_ref }, (true, _) => quote! { get_folded }, }; - let default = if field.borrowed { + let default = if borrowed { quote! { || &#const_ident } } else { match default { @@ -821,9 +829,10 @@ fn create_capable_impl(element: &Elem) -> TokenStream { /// Creates the element's `Fields` implementation. fn create_fields_impl(element: &Elem) -> TokenStream { let into_value = quote! { #foundations::IntoValue::into_value }; + let visible_non_ghost = || element.visible_fields().filter(|field| !field.ghost); // Fields that can be checked using the `has` method. - let has_arms = element.visible_fields().map(|field| { + let has_arms = visible_non_ghost().map(|field| { let Field { enum_ident, ident, .. } = field; let expr = if field.inherent() { @@ -836,7 +845,7 @@ fn create_fields_impl(element: &Elem) -> TokenStream { }); // Fields that can be accessed using the `field` method. - let field_arms = element.visible_fields().map(|field| { + let field_arms = visible_non_ghost().filter(|field| !field.ghost).map(|field| { let Field { enum_ident, ident, .. } = field; let expr = if field.inherent() { @@ -848,8 +857,29 @@ fn create_fields_impl(element: &Elem) -> TokenStream { quote! { Fields::#enum_ident => #expr } }); + // Fields that can be accessed using the `field_with_styles` method. + let field_with_styles_arms = element.visible_fields().map(|field| { + let Field { enum_ident, ident, .. } = field; + + let expr = if field.inherent() { + quote! { Some(#into_value(self.#ident.clone())) } + } else if field.synthesized && field.default.is_none() { + quote! { self.#ident.clone().map(#into_value) } + } else { + let value = create_style_chain_access( + field, + false, + if field.ghost { quote!(None) } else { quote!(self.#ident.as_ref()) }, + ); + + quote! { Some(#into_value(#value)) } + }; + + quote! { Fields::#enum_ident => #expr } + }); + // Creation of the `fields` dictionary for inherent fields. - let field_inserts = element.visible_fields().map(|field| { + let field_inserts = visible_non_ghost().map(|field| { let Field { ident, name, .. } = field; let string = quote! { #name.into() }; @@ -873,7 +903,7 @@ fn create_fields_impl(element: &Elem) -> TokenStream { type Enum = Fields; fn has(&self, id: u8) -> bool { - let Ok(id) = <#ident as #foundations::Fields>::Enum::try_from(id) else { + let Ok(id) = Fields::try_from(id) else { return false; }; @@ -884,13 +914,21 @@ fn create_fields_impl(element: &Elem) -> TokenStream { } fn field(&self, id: u8) -> Option<#foundations::Value> { - let id = <#ident as #foundations::Fields>::Enum::try_from(id).ok()?; + let id = Fields::try_from(id).ok()?; match id { #(#field_arms,)* _ => None, } } + fn field_with_styles(&self, id: u8, styles: #foundations::StyleChain) -> Option<#foundations::Value> { + let id = Fields::try_from(id).ok()?; + match id { + #(#field_with_styles_arms,)* + _ => None, + } + } + 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 8a7346350..49497b8ff 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -15,7 +15,8 @@ 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, Styles, Synthesize, Value, + NativeElement, Recipe, Repr, Selector, Str, Style, StyleChain, Styles, Synthesize, + Value, }; use crate::introspection::{Locatable, Location, Meta, MetaElem}; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; @@ -181,13 +182,16 @@ impl Content { /// This is the preferred way to access fields. However, you can only use it /// if you have set the field IDs yourself or are using the field IDs /// generated by the `#[elem]` macro. - pub fn get(&self, id: u8) -> Option { + pub fn get(&self, id: u8, styles: Option) -> Option { if id == 255 { if let Some(label) = self.label() { return Some(label.into_value()); } } - self.inner.elem.field(id) + match styles { + Some(styles) => self.inner.elem.field_with_styles(id, styles), + None => self.inner.elem.field(id), + } } /// Get a field by name. @@ -201,7 +205,7 @@ impl Content { } } let id = self.elem().field_id(name)?; - self.get(id) + self.get(id, None) } /// Get a field by ID, returning a missing field error if it does not exist. @@ -210,7 +214,7 @@ impl Content { /// if you have set the field IDs yourself or are using the field IDs /// generated by the `#[elem]` macro. pub fn field(&self, id: u8) -> StrResult { - self.get(id) + self.get(id, None) .ok_or_else(|| missing_field(self.elem().field_name(id).unwrap())) } @@ -400,7 +404,7 @@ impl Content { pub fn query(&self, selector: Selector) -> Vec { let mut results = Vec::new(); self.traverse(&mut |element| { - if selector.matches(&element) { + if selector.matches(&element, None) { results.push(element); } }); @@ -414,7 +418,7 @@ impl Content { pub fn query_first(&self, selector: Selector) -> Option { let mut result = None; self.traverse(&mut |element| { - if result.is_none() && selector.matches(&element) { + if result.is_none() && selector.matches(&element, None) { result = Some(element); } }); diff --git a/crates/typst/src/foundations/element.rs b/crates/typst/src/foundations/element.rs index 24e5cf007..489077e4a 100644 --- a/crates/typst/src/foundations/element.rs +++ b/crates/typst/src/foundations/element.rs @@ -220,6 +220,9 @@ pub trait Fields { /// Get the field with the given field ID. fn field(&self, id: u8) -> Option; + /// Get the field with the given ID in the presence of styles. + fn field_with_styles(&self, id: u8, styles: StyleChain) -> Option; + /// Get the fields of the element. fn fields(&self) -> Dict; } diff --git a/crates/typst/src/foundations/selector.rs b/crates/typst/src/foundations/selector.rs index 2321f317e..16bd721d6 100644 --- a/crates/typst/src/foundations/selector.rs +++ b/crates/typst/src/foundations/selector.rs @@ -7,7 +7,7 @@ use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{ cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func, - Label, Reflect, Regex, Repr, Str, Type, Value, + Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value, }; use crate::introspection::{Locatable, Location}; use crate::symbols::Symbol; @@ -128,23 +128,26 @@ impl Selector { } /// Whether the selector matches for the target. - pub fn matches(&self, target: &Content) -> bool { - // TODO: optimize field access to not clone. + pub fn matches(&self, target: &Content, styles: Option) -> bool { match self { Self::Elem(element, dict) => { + // TODO: Optimize field access to not clone. target.func() == *element - && dict - .iter() - .flat_map(|dict| dict.iter()) - .all(|(id, value)| target.get(*id).as_ref() == Some(value)) + && dict.iter().flat_map(|dict| dict.iter()).all(|(id, value)| { + target.get(*id, styles).as_ref() == Some(value) + }) } Self::Label(label) => target.label() == Some(*label), Self::Regex(regex) => target .to_packed::() .map_or(false, |elem| regex.is_match(elem.text())), Self::Can(cap) => target.func().can_type_id(*cap), - Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), - Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), + Self::Or(selectors) => { + selectors.iter().any(move |sel| sel.matches(target, styles)) + } + Self::And(selectors) => { + selectors.iter().all(move |sel| sel.matches(target, styles)) + } Self::Location(location) => target.location() == Some(*location), // Not supported here. Self::Before { .. } | Self::After { .. } => false, diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs index bac92887b..6946d076d 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -369,10 +369,10 @@ impl Recipe { } /// Whether the recipe is applicable to the target. - pub fn applicable(&self, target: &Content) -> bool { + pub fn applicable(&self, target: &Content, styles: StyleChain) -> bool { self.selector .as_ref() - .map_or(false, |selector| selector.matches(target)) + .map_or(false, |selector| selector.matches(target, Some(styles))) } /// Apply the recipe to the given content. diff --git a/crates/typst/src/introspection/introspector.rs b/crates/typst/src/introspection/introspector.rs index 75882edf0..f8270025c 100644 --- a/crates/typst/src/introspection/introspector.rs +++ b/crates/typst/src/introspection/introspector.rs @@ -127,9 +127,11 @@ impl Introspector { indices.iter().map(|&index| self.elems[index].0.clone()).collect() }) .unwrap_or_default(), - Selector::Elem(..) | Selector::Regex(_) | Selector::Can(_) => { - self.all().filter(|elem| selector.matches(elem)).cloned().collect() - } + Selector::Elem(..) | Selector::Regex(_) | Selector::Can(_) => self + .all() + .filter(|elem| selector.matches(elem, None)) + .cloned() + .collect(), Selector::Location(location) => { self.get(location).cloned().into_iter().collect() } diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index cd40ac866..f55555978 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -80,7 +80,7 @@ pub fn applicable(target: &Content, styles: StyleChain) -> bool { // Find out whether any recipe matches and is unguarded. for recipe in styles.recipes() { - if !target.is_guarded(Guard(n)) && recipe.applicable(target) { + if !target.is_guarded(Guard(n)) && recipe.applicable(target, styles) { return true; } n -= 1; @@ -133,7 +133,7 @@ pub fn realize( // Find an applicable show rule recipe. for recipe in styles.recipes() { let guard = Guard(n); - if !target.is_guarded(guard) && recipe.applicable(target) { + if !target.is_guarded(guard) && recipe.applicable(target, styles) { if let Some(content) = try_apply(engine, target, recipe, guard)? { return Ok(Some(content)); } diff --git a/tests/ref/compiler/select-where-styles.png b/tests/ref/compiler/select-where-styles.png new file mode 100644 index 000000000..ffdc4babf Binary files /dev/null and b/tests/ref/compiler/select-where-styles.png differ diff --git a/tests/typ/compiler/select-where-styles.typ b/tests/typ/compiler/select-where-styles.typ new file mode 100644 index 000000000..028be2e92 --- /dev/null +++ b/tests/typ/compiler/select-where-styles.typ @@ -0,0 +1,91 @@ +// Test that where selectors also work with settable fields. + +--- +// Test that where selectors also trigger on set rule fields. +#show raw.where(block: false): box.with( + fill: luma(220), + inset: (x: 3pt, y: 0pt), + outset: (y: 3pt), + radius: 2pt, +) + +This is #raw("fn main() {}") some text. + +--- +// Note: This show rule is horribly inefficient because it triggers for +// every individual text element. But it should still work. +#show text.where(lang: "de"): set text(red) + +#set text(lang: "es") +Hola, mundo! + +#set text(lang: "de") +Hallo Welt! + +#set text(lang: "en") +Hello World! + +--- +// Test that folding is taken into account. +#set text(5pt) +#set text(2em) + +#[ + #show text.where(size: 2em): set text(blue) + 2em not blue +] + +#[ + #show text.where(size: 10pt): set text(blue) + 10pt blue +] + +--- +// Test again that folding is taken into account. +#set rect(width: 40pt, height: 10pt) +#set rect(stroke: blue) +#set rect(stroke: 2pt) + +#{ + show rect.where(stroke: blue): "Not Triggered" + rect() +} +#{ + show rect.where(stroke: 2pt): "Not Triggered" + rect() +} +#{ + show rect.where(stroke: 2pt + blue): "Triggered" + rect() +} + +--- +// Test that resolving is *not* taken into account. +#set line(start: (1em, 1em + 2pt)) + +#{ + show line.where(start: (1em, 1em + 2pt)): "Triggered" + line() +} +#{ + show line.where(start: (10pt, 12pt)): "Not Triggered" + line() +} + + +--- +// Test again that resolving is *not* taken into account. +#set text(hyphenate: auto) + +#[ + #show text.where(hyphenate: auto): underline + Auto +] +#[ + #show text.where(hyphenate: true): underline + True +] +#[ + #show text.where(hyphenate: false): underline + False +]