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.
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 }
};
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)*

View File

@ -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<Value> {
pub fn get(&self, id: u8, styles: Option<StyleChain>) -> Option<Value> {
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<Value> {
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<Content> {
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<Content> {
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);
}
});

View File

@ -220,6 +220,9 @@ pub trait Fields {
/// Get the field with the given field ID.
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.
fn fields(&self) -> Dict;
}

View File

@ -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<StyleChain>) -> 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::<TextElem>()
.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,

View File

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

View File

@ -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()
}

View File

@ -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));
}

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
]