Respect set rules in where selectors (#3290)

This commit is contained in:
Laurenz 2024-01-30 15:24:41 +01:00 committed by GitHub
parent a1e8560ca6
commit b744b87818
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 175 additions and 34 deletions

View File

@ -93,7 +93,7 @@ impl Elem {
/// Fields that are visible to the user. /// Fields that are visible to the user.
fn visible_fields(&self) -> impl Iterator<Item = &Field> + Clone { fn visible_fields(&self) -> impl Iterator<Item = &Field> + 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 } 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 { if field.resolve {
value = quote! { #foundations::Resolve::resolve(#value, styles) }; 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 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 { if field.resolve {
value = quote! { #foundations::Resolve::resolve(#value, styles) }; 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. /// 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 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, false) => quote! { get },
(false, true) => quote! { get_ref }, (false, true) => quote! { get_ref },
(true, _) => quote! { get_folded }, (true, _) => quote! { get_folded },
}; };
let default = if field.borrowed { let default = if borrowed {
quote! { || &#const_ident } quote! { || &#const_ident }
} else { } else {
match default { match default {
@ -821,9 +829,10 @@ fn create_capable_impl(element: &Elem) -> TokenStream {
/// Creates the element's `Fields` implementation. /// Creates the element's `Fields` implementation.
fn create_fields_impl(element: &Elem) -> TokenStream { fn create_fields_impl(element: &Elem) -> TokenStream {
let into_value = quote! { #foundations::IntoValue::into_value }; 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. // 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 Field { enum_ident, ident, .. } = field;
let expr = if field.inherent() { 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. // 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 Field { enum_ident, ident, .. } = field;
let expr = if field.inherent() { let expr = if field.inherent() {
@ -848,8 +857,29 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
quote! { Fields::#enum_ident => #expr } 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. // 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 Field { ident, name, .. } = field;
let string = quote! { #name.into() }; let string = quote! { #name.into() };
@ -873,7 +903,7 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
type Enum = Fields; type Enum = Fields;
fn has(&self, id: u8) -> bool { 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; return false;
}; };
@ -884,13 +914,21 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
} }
fn field(&self, id: u8) -> Option<#foundations::Value> { 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 { match id {
#(#field_arms,)* #(#field_arms,)*
_ => None, _ => 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 { fn fields(&self) -> #foundations::Dict {
let mut fields = #foundations::Dict::new(); let mut fields = #foundations::Dict::new();
#(#field_inserts)* #(#field_inserts)*

View File

@ -15,7 +15,8 @@ use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, func, scope, ty, Dict, Element, Fields, Finalize, Guard, IntoValue, Label, 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::introspection::{Locatable, Location, Meta, MetaElem};
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; 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 /// 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 /// if you have set the field IDs yourself or are using the field IDs
/// generated by the `#[elem]` macro. /// generated by the `#[elem]` macro.
pub fn get(&self, id: u8) -> Option<Value> { pub fn get(&self, id: u8, styles: Option<StyleChain>) -> Option<Value> {
if id == 255 { if id == 255 {
if let Some(label) = self.label() { if let Some(label) = self.label() {
return Some(label.into_value()); 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. /// Get a field by name.
@ -201,7 +205,7 @@ impl Content {
} }
} }
let id = self.elem().field_id(name)?; 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. /// 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 /// if you have set the field IDs yourself or are using the field IDs
/// generated by the `#[elem]` macro. /// generated by the `#[elem]` macro.
pub fn field(&self, id: u8) -> StrResult<Value> { pub fn field(&self, id: u8) -> StrResult<Value> {
self.get(id) self.get(id, None)
.ok_or_else(|| missing_field(self.elem().field_name(id).unwrap())) .ok_or_else(|| missing_field(self.elem().field_name(id).unwrap()))
} }
@ -400,7 +404,7 @@ impl Content {
pub fn query(&self, selector: Selector) -> Vec<Content> { pub fn query(&self, selector: Selector) -> Vec<Content> {
let mut results = Vec::new(); let mut results = Vec::new();
self.traverse(&mut |element| { self.traverse(&mut |element| {
if selector.matches(&element) { if selector.matches(&element, None) {
results.push(element); results.push(element);
} }
}); });
@ -414,7 +418,7 @@ impl Content {
pub fn query_first(&self, selector: Selector) -> Option<Content> { pub fn query_first(&self, selector: Selector) -> Option<Content> {
let mut result = None; let mut result = None;
self.traverse(&mut |element| { self.traverse(&mut |element| {
if result.is_none() && selector.matches(&element) { if result.is_none() && selector.matches(&element, None) {
result = Some(element); result = Some(element);
} }
}); });

View File

@ -220,6 +220,9 @@ pub trait Fields {
/// Get the field with the given field ID. /// Get the field with the given field ID.
fn field(&self, id: u8) -> Option<Value>; fn field(&self, id: u8) -> Option<Value>;
/// Get the field with the given ID in the presence of styles.
fn field_with_styles(&self, id: u8, styles: StyleChain) -> Option<Value>;
/// Get the fields of the element. /// Get the fields of the element.
fn fields(&self) -> Dict; fn fields(&self) -> Dict;
} }

View File

@ -7,7 +7,7 @@ use smallvec::SmallVec;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, StrResult};
use crate::foundations::{ use crate::foundations::{
cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func, 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::introspection::{Locatable, Location};
use crate::symbols::Symbol; use crate::symbols::Symbol;
@ -128,23 +128,26 @@ impl Selector {
} }
/// Whether the selector matches for the target. /// Whether the selector matches for the target.
pub fn matches(&self, target: &Content) -> bool { pub fn matches(&self, target: &Content, styles: Option<StyleChain>) -> bool {
// TODO: optimize field access to not clone.
match self { match self {
Self::Elem(element, dict) => { Self::Elem(element, dict) => {
// TODO: Optimize field access to not clone.
target.func() == *element target.func() == *element
&& dict && dict.iter().flat_map(|dict| dict.iter()).all(|(id, value)| {
.iter() target.get(*id, styles).as_ref() == Some(value)
.flat_map(|dict| dict.iter()) })
.all(|(id, value)| target.get(*id).as_ref() == Some(value))
} }
Self::Label(label) => target.label() == Some(*label), Self::Label(label) => target.label() == Some(*label),
Self::Regex(regex) => target Self::Regex(regex) => target
.to_packed::<TextElem>() .to_packed::<TextElem>()
.map_or(false, |elem| regex.is_match(elem.text())), .map_or(false, |elem| regex.is_match(elem.text())),
Self::Can(cap) => target.func().can_type_id(*cap), Self::Can(cap) => target.func().can_type_id(*cap),
Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), Self::Or(selectors) => {
Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), 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), Self::Location(location) => target.location() == Some(*location),
// Not supported here. // Not supported here.
Self::Before { .. } | Self::After { .. } => false, Self::Before { .. } | Self::After { .. } => false,

View File

@ -369,10 +369,10 @@ impl Recipe {
} }
/// Whether the recipe is applicable to the target. /// 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 self.selector
.as_ref() .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. /// Apply the recipe to the given content.

View File

@ -127,9 +127,11 @@ impl Introspector {
indices.iter().map(|&index| self.elems[index].0.clone()).collect() indices.iter().map(|&index| self.elems[index].0.clone()).collect()
}) })
.unwrap_or_default(), .unwrap_or_default(),
Selector::Elem(..) | Selector::Regex(_) | Selector::Can(_) => { Selector::Elem(..) | Selector::Regex(_) | Selector::Can(_) => self
self.all().filter(|elem| selector.matches(elem)).cloned().collect() .all()
} .filter(|elem| selector.matches(elem, None))
.cloned()
.collect(),
Selector::Location(location) => { Selector::Location(location) => {
self.get(location).cloned().into_iter().collect() self.get(location).cloned().into_iter().collect()
} }

View File

@ -80,7 +80,7 @@ pub fn applicable(target: &Content, styles: StyleChain) -> bool {
// Find out whether any recipe matches and is unguarded. // Find out whether any recipe matches and is unguarded.
for recipe in styles.recipes() { 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; return true;
} }
n -= 1; n -= 1;
@ -133,7 +133,7 @@ pub fn realize(
// Find an applicable show rule recipe. // Find an applicable show rule recipe.
for recipe in styles.recipes() { for recipe in styles.recipes() {
let guard = Guard(n); 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)? { if let Some(content) = try_apply(engine, target, recipe, guard)? {
return Ok(Some(content)); return Ok(Some(content));
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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
]