From 0a41844cc4e645e87fe48aa31ed3a4fd40a6ab11 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 7 Nov 2022 14:30:50 +0100 Subject: [PATCH] Selectors --- library/src/math/mod.rs | 2 +- library/src/prelude.rs | 2 +- library/src/structure/heading.rs | 4 +- library/src/structure/list.rs | 6 +-- library/src/structure/reference.rs | 2 +- library/src/structure/table.rs | 4 +- library/src/text/deco.rs | 4 +- library/src/text/link.rs | 4 +- library/src/text/mod.rs | 8 +-- library/src/text/raw.rs | 2 +- library/src/text/shift.rs | 2 +- src/model/cast.rs | 9 ++-- src/model/content.rs | 10 ++-- src/model/eval.rs | 16 +++--- src/model/func.rs | 37 +++++++++---- src/model/methods.rs | 1 + src/model/str.rs | 64 +++++++++++----------- src/model/styles.rs | 79 +++++++++++++++++----------- src/syntax/ast.rs | 4 +- tests/ref/style/show-selector.png | Bin 0 -> 12923 bytes tests/typ/style/show-bare.typ | 2 +- tests/typ/style/show-node.typ | 2 +- tests/typ/style/show-selector.typ | 36 +++++++++++++ tools/support/typst.tmLanguage.json | 2 +- 24 files changed, 185 insertions(+), 117 deletions(-) create mode 100644 tests/ref/style/show-selector.png create mode 100644 tests/typ/style/show-selector.typ diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 0fad2939f..a89b4953f 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -39,7 +39,7 @@ impl MathNode { } impl Show for MathNode { - fn unguard_parts(&self, _: Selector) -> Content { + fn unguard_parts(&self, _: RecipeId) -> Content { self.clone().pack() } diff --git a/library/src/prelude.rs b/library/src/prelude.rs index f51b826fd..f08604a8e 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -9,7 +9,7 @@ pub use typst::frame::*; pub use typst::geom::*; pub use typst::model::{ array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast, - Content, Dict, Finalize, Fold, Func, Key, Node, Resolve, Scope, Selector, Show, + Content, Dict, Finalize, Fold, Func, Key, Node, RecipeId, Resolve, Scope, Show, Smart, Str, StyleChain, StyleMap, StyleVec, Value, Vm, }; pub use typst::syntax::{Span, Spanned}; diff --git a/library/src/structure/heading.rs b/library/src/structure/heading.rs index 46e98c186..f93be5d98 100644 --- a/library/src/structure/heading.rs +++ b/library/src/structure/heading.rs @@ -78,8 +78,8 @@ impl HeadingNode { } impl Show for HeadingNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self { body: self.body.unguard(sel), ..*self }.pack() + fn unguard_parts(&self, id: RecipeId) -> Content { + Self { body: self.body.unguard(id), ..*self }.pack() } fn show(&self, _: Tracked, _: StyleChain) -> SourceResult { diff --git a/library/src/structure/list.rs b/library/src/structure/list.rs index 499207a4c..89dc0f355 100644 --- a/library/src/structure/list.rs +++ b/library/src/structure/list.rs @@ -94,9 +94,9 @@ impl ListNode { } impl Show for ListNode { - fn unguard_parts(&self, sel: Selector) -> Content { + fn unguard_parts(&self, id: RecipeId) -> Content { Self { - items: self.items.map(|item| item.unguard(sel)), + items: self.items.map(|item| item.unguard(id)), ..*self } .pack() @@ -208,7 +208,7 @@ impl ListItem { } } - fn unguard(&self, sel: Selector) -> Self { + fn unguard(&self, sel: RecipeId) -> Self { match self { Self::List(body) => Self::List(Box::new(body.unguard(sel))), Self::Enum(number, body) => Self::Enum(*number, Box::new(body.unguard(sel))), diff --git a/library/src/structure/reference.rs b/library/src/structure/reference.rs index 18f4eecb4..7004f49ea 100644 --- a/library/src/structure/reference.rs +++ b/library/src/structure/reference.rs @@ -20,7 +20,7 @@ impl RefNode { } impl Show for RefNode { - fn unguard_parts(&self, _: Selector) -> Content { + fn unguard_parts(&self, _: RecipeId) -> Content { Self(self.0.clone()).pack() } diff --git a/library/src/structure/table.rs b/library/src/structure/table.rs index fbf1c7c0f..8c6191bed 100644 --- a/library/src/structure/table.rs +++ b/library/src/structure/table.rs @@ -58,11 +58,11 @@ impl TableNode { } impl Show for TableNode { - fn unguard_parts(&self, sel: Selector) -> Content { + fn unguard_parts(&self, id: RecipeId) -> Content { Self { tracks: self.tracks.clone(), gutter: self.gutter.clone(), - cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(), + cells: self.cells.iter().map(|cell| cell.unguard(id)).collect(), } .pack() } diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs index fa0f05a70..bc7a312d9 100644 --- a/library/src/text/deco.rs +++ b/library/src/text/deco.rs @@ -47,8 +47,8 @@ impl DecoNode { } impl Show for DecoNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self(self.0.unguard(sel)).pack() + fn unguard_parts(&self, id: RecipeId) -> Content { + Self(self.0.unguard(id)).pack() } fn show(&self, _: Tracked, styles: StyleChain) -> SourceResult { diff --git a/library/src/text/link.rs b/library/src/text/link.rs index 4312559e9..b74ca530d 100644 --- a/library/src/text/link.rs +++ b/library/src/text/link.rs @@ -50,10 +50,10 @@ impl LinkNode { } impl Show for LinkNode { - fn unguard_parts(&self, sel: Selector) -> Content { + fn unguard_parts(&self, id: RecipeId) -> Content { Self { dest: self.dest.clone(), - body: self.body.as_ref().map(|body| body.unguard(sel)), + body: self.body.as_ref().map(|body| body.unguard(id)), } .pack() } diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs index 86c6884a2..a81647273 100644 --- a/library/src/text/mod.rs +++ b/library/src/text/mod.rs @@ -503,8 +503,8 @@ impl StrongNode { } impl Show for StrongNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self(self.0.unguard(sel)).pack() + fn unguard_parts(&self, id: RecipeId) -> Content { + Self(self.0.unguard(id)).pack() } fn show(&self, _: Tracked, _: StyleChain) -> SourceResult { @@ -531,8 +531,8 @@ impl EmphNode { } impl Show for EmphNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self(self.0.unguard(sel)).pack() + fn unguard_parts(&self, id: RecipeId) -> Content { + Self(self.0.unguard(id)).pack() } fn show(&self, _: Tracked, _: StyleChain) -> SourceResult { diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs index 5a98cf3bd..c6229d599 100644 --- a/library/src/text/raw.rs +++ b/library/src/text/raw.rs @@ -52,7 +52,7 @@ impl RawNode { } impl Show for RawNode { - fn unguard_parts(&self, _: Selector) -> Content { + fn unguard_parts(&self, _: RecipeId) -> Content { Self { text: self.text.clone(), ..*self }.pack() } diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs index 0f654b5a9..a91285bf4 100644 --- a/library/src/text/shift.rs +++ b/library/src/text/shift.rs @@ -43,7 +43,7 @@ impl ShiftNode { } impl Show for ShiftNode { - fn unguard_parts(&self, _: Selector) -> Content { + fn unguard_parts(&self, _: RecipeId) -> Content { Self(self.0.clone()).pack() } diff --git a/src/model/cast.rs b/src/model/cast.rs index 7a466b72e..d2e10a1f1 100644 --- a/src/model/cast.rs +++ b/src/model/cast.rs @@ -1,7 +1,7 @@ use std::num::NonZeroUsize; use std::str::FromStr; -use super::{Content, Pattern, Regex, Transform, Value}; +use super::{Content, Regex, Selector, Transform, Value}; use crate::diag::{with_alternative, StrResult}; use crate::font::{FontStretch, FontStyle, FontWeight}; use crate::frame::{Destination, Lang, Location, Region}; @@ -181,10 +181,9 @@ dynamic! { Regex: "regular expression", } -castable! { - Pattern, - Expected: "function, string or regular expression", - Value::Func(func) => Self::Node(func.node()?), +dynamic! { + Selector: "selector", + Value::Func(func) => Self::Node(func.node()?, None), Value::Str(text) => Self::text(&text), @regex: Regex => Self::Regex(regex.clone()), } diff --git a/src/model/content.rs b/src/model/content.rs index 0257f4da5..bc25cd79c 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -9,7 +9,7 @@ use comemo::Tracked; use siphasher::sip128::{Hasher128, SipHasher}; use typst_macros::node; -use super::{Args, Key, Property, Recipe, Selector, StyleEntry, StyleMap, Value, Vm}; +use super::{Args, Key, Property, Recipe, RecipeId, StyleEntry, StyleMap, Value, Vm}; use crate::diag::{SourceResult, StrResult}; use crate::util::ReadableTypeId; use crate::World; @@ -104,7 +104,7 @@ impl Content { world: Tracked, recipe: Recipe, ) -> SourceResult { - if recipe.pattern.is_none() { + if recipe.selector.is_none() { recipe.transform.apply(world, recipe.span, || Value::Content(self)) } else { Ok(self.styled_with_entry(StyleEntry::Recipe(recipe))) @@ -135,9 +135,9 @@ impl Content { StyledNode { sub: self, map: styles }.pack() } - /// Reenable the show rule identified by the selector. - pub fn unguard(&self, sel: Selector) -> Self { - self.clone().styled_with_entry(StyleEntry::Unguard(sel)) + /// Reenable a specific show rule recipe. + pub fn unguard(&self, id: RecipeId) -> Self { + self.clone().styled_with_entry(StyleEntry::Unguard(id)) } } diff --git a/src/model/eval.rs b/src/model/eval.rs index fd43c4c31..2ed8c13b3 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -7,7 +7,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::{ methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Flow, Func, - Pattern, Recipe, Scope, Scopes, StyleMap, Transform, Value, Vm, + Recipe, Scope, Scopes, Selector, StyleMap, Transform, Value, Vm, }; use crate::diag::{bail, error, At, SourceResult, StrResult, Trace, Tracepoint}; use crate::geom::{Abs, Angle, Em, Fr, Ratio}; @@ -831,10 +831,12 @@ impl Eval for ast::SetRule { return Ok(StyleMap::new()); } } + let target = self.target(); - let target = target.eval(vm)?.cast::().at(target.span())?; + let span = target.span(); + let target = target.eval(vm)?.cast::().at(span)?; let args = self.args().eval(vm)?; - target.set(args) + target.set(args, span) } } @@ -842,16 +844,16 @@ impl Eval for ast::ShowRule { type Output = Recipe; fn eval(&self, vm: &mut Vm) -> SourceResult { - let pattern = self - .pattern() - .map(|pattern| pattern.eval(vm)?.cast::().at(pattern.span())) + let selector = self + .selector() + .map(|selector| selector.eval(vm)?.cast::().at(selector.span())) .transpose()?; let transform = self.transform(); let span = transform.span(); let transform = transform.eval(vm)?.cast::().at(span)?; - Ok(Recipe { span, pattern, transform }) + Ok(Recipe { span, selector, transform }) } } diff --git a/src/model/func.rs b/src/model/func.rs index 8cedb158f..15434bbfe 100644 --- a/src/model/func.rs +++ b/src/model/func.rs @@ -4,10 +4,12 @@ use std::sync::Arc; use comemo::{Track, Tracked}; -use super::{Args, Eval, Flow, Node, NodeId, Route, Scope, Scopes, StyleMap, Value, Vm}; +use super::{ + Args, Eval, Flow, Node, NodeId, Route, Scope, Scopes, Selector, StyleMap, Value, Vm, +}; use crate::diag::{bail, SourceResult, StrResult}; use crate::syntax::ast::{self, Expr, TypedNode}; -use crate::syntax::{SourceId, SyntaxNode}; +use crate::syntax::{SourceId, Span, SyntaxNode}; use crate::util::EcoString; use crate::World; @@ -54,11 +56,6 @@ impl Func { Self(Arc::new(Repr::Closure(closure))) } - /// Apply the given arguments to the function. - pub fn with(self, args: Args) -> Self { - Self(Arc::new(Repr::With(self, args))) - } - /// The name of the function. pub fn name(&self) -> Option<&str> { match self.0.as_ref() { @@ -106,12 +103,18 @@ impl Func { self.call(&mut vm, args) } + /// Apply the given arguments to the function. + pub fn with(self, args: Args) -> Self { + Self(Arc::new(Repr::With(self, args))) + } + /// Execute the function's set rule and return the resulting style map. - pub fn set(&self, mut args: Args) -> SourceResult { - let styles = match self.0.as_ref() { - Repr::Native(Native { set: Some(set), .. }) => set(&mut args)?, - _ => StyleMap::new(), + pub fn set(&self, mut args: Args, span: Span) -> SourceResult { + let Repr::Native(Native { set: Some(set), .. }) = self.0.as_ref() else { + bail!(span, "this function cannot be customized with set"); }; + + let styles = set(&mut args)?; args.finish()?; Ok(styles) } @@ -123,6 +126,18 @@ impl Func { _ => Err("this function cannot be customized with show")?, } } + + /// Create a selector from this node and the given arguments. + pub fn where_(self, args: &mut Args) -> StrResult { + match self.0.as_ref() { + Repr::Native(Native { node: Some(id), .. }) => { + let named = args.to_named(); + args.items.retain(|arg| arg.name.is_none()); + Ok(Selector::Node(*id, Some(named))) + } + _ => Err("this function is not selectable")?, + } + } } impl Debug for Func { diff --git a/src/model/methods.rs b/src/model/methods.rs index 26d27dfa4..5f879eeb0 100644 --- a/src/model/methods.rs +++ b/src/model/methods.rs @@ -98,6 +98,7 @@ pub fn call( Value::Func(func) => match method { "with" => Value::Func(func.with(args.take())), + "where" => Value::dynamic(func.where_(&mut args).at(span)?), _ => return missing(), }, diff --git a/src/model/str.rs b/src/model/str.rs index 1fcf7075d..454c561f6 100644 --- a/src/model/str.rs +++ b/src/model/str.rs @@ -77,69 +77,69 @@ impl Str { } /// Whether the given pattern exists in this string. - pub fn contains(&self, pattern: StrPattern) -> bool { + pub fn contains(&self, pattern: Pattern) -> bool { match pattern { - StrPattern::Str(pat) => self.0.contains(pat.as_str()), - StrPattern::Regex(re) => re.is_match(self), + Pattern::Str(pat) => self.0.contains(pat.as_str()), + Pattern::Regex(re) => re.is_match(self), } } /// Whether this string begins with the given pattern. - pub fn starts_with(&self, pattern: StrPattern) -> bool { + pub fn starts_with(&self, pattern: Pattern) -> bool { match pattern { - StrPattern::Str(pat) => self.0.starts_with(pat.as_str()), - StrPattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0), + Pattern::Str(pat) => self.0.starts_with(pat.as_str()), + Pattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0), } } /// Whether this string ends with the given pattern. - pub fn ends_with(&self, pattern: StrPattern) -> bool { + pub fn ends_with(&self, pattern: Pattern) -> bool { match pattern { - StrPattern::Str(pat) => self.0.ends_with(pat.as_str()), - StrPattern::Regex(re) => { + Pattern::Str(pat) => self.0.ends_with(pat.as_str()), + Pattern::Regex(re) => { re.find_iter(self).last().map_or(false, |m| m.end() == self.0.len()) } } } /// The text of the pattern's first match in this string. - pub fn find(&self, pattern: StrPattern) -> Option { + pub fn find(&self, pattern: Pattern) -> Option { match pattern { - StrPattern::Str(pat) => self.0.contains(pat.as_str()).then(|| pat), - StrPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), + Pattern::Str(pat) => self.0.contains(pat.as_str()).then(|| pat), + Pattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), } } /// The position of the pattern's first match in this string. - pub fn position(&self, pattern: StrPattern) -> Option { + pub fn position(&self, pattern: Pattern) -> Option { match pattern { - StrPattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64), - StrPattern::Regex(re) => re.find(self).map(|m| m.start() as i64), + Pattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64), + Pattern::Regex(re) => re.find(self).map(|m| m.start() as i64), } } /// The start and, text and capture groups (if any) of the first match of /// the pattern in this string. - pub fn match_(&self, pattern: StrPattern) -> Option { + pub fn match_(&self, pattern: Pattern) -> Option { match pattern { - StrPattern::Str(pat) => { + Pattern::Str(pat) => { self.0.match_indices(pat.as_str()).next().map(match_to_dict) } - StrPattern::Regex(re) => re.captures(self).map(captures_to_dict), + Pattern::Regex(re) => re.captures(self).map(captures_to_dict), } } /// The start, end, text and capture groups (if any) of all matches of the /// pattern in this string. - pub fn matches(&self, pattern: StrPattern) -> Array { + pub fn matches(&self, pattern: Pattern) -> Array { match pattern { - StrPattern::Str(pat) => self + Pattern::Str(pat) => self .0 .match_indices(pat.as_str()) .map(match_to_dict) .map(Value::Dict) .collect(), - StrPattern::Regex(re) => re + Pattern::Regex(re) => re .captures_iter(self) .map(captures_to_dict) .map(Value::Dict) @@ -148,14 +148,14 @@ impl Str { } /// Split this string at whitespace or a specific pattern. - pub fn split(&self, pattern: Option) -> Array { + pub fn split(&self, pattern: Option) -> Array { let s = self.as_str(); match pattern { None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(), - Some(StrPattern::Str(pat)) => { + Some(Pattern::Str(pat)) => { s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect() } - Some(StrPattern::Regex(re)) => { + Some(Pattern::Regex(re)) => { re.split(s).map(|v| Value::Str(v.into())).collect() } } @@ -167,7 +167,7 @@ impl Str { /// pattern. pub fn trim( &self, - pattern: Option, + pattern: Option, at: Option, repeat: bool, ) -> Self { @@ -180,7 +180,7 @@ impl Str { Some(StrSide::Start) => self.0.trim_start(), Some(StrSide::End) => self.0.trim_end(), }, - Some(StrPattern::Str(pat)) => { + Some(Pattern::Str(pat)) => { let pat = pat.as_str(); let mut s = self.as_str(); if repeat { @@ -200,7 +200,7 @@ impl Str { } s } - Some(StrPattern::Regex(re)) => { + Some(Pattern::Regex(re)) => { let s = self.as_str(); let mut last = 0; let mut range = 0..s.len(); @@ -240,13 +240,13 @@ impl Str { /// Replace at most `count` occurances of the given pattern with a /// replacement string (beginning from the start). - pub fn replace(&self, pattern: StrPattern, with: Self, count: Option) -> Self { + pub fn replace(&self, pattern: Pattern, with: Self, count: Option) -> Self { match pattern { - StrPattern::Str(pat) => match count { + Pattern::Str(pat) => match count { Some(n) => self.0.replacen(pat.as_str(), &with, n).into(), None => self.0.replace(pat.as_str(), &with).into(), }, - StrPattern::Regex(re) => match count { + Pattern::Regex(re) => match count { Some(n) => re.replacen(self, n, with.as_str()).into(), None => re.replace(self, with.as_str()).into(), }, @@ -433,7 +433,7 @@ impl Hash for Regex { /// A pattern which can be searched for in a string. #[derive(Debug, Clone)] -pub enum StrPattern { +pub enum Pattern { /// Just a string. Str(Str), /// A regular expression. @@ -441,7 +441,7 @@ pub enum StrPattern { } castable! { - StrPattern, + Pattern, Expected: "string or regular expression", Value::Str(text) => Self::Str(text), @regex: Regex => Self::Regex(regex.clone()), diff --git a/src/model/styles.rs b/src/model/styles.rs index 3800490b8..8e7319420 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use comemo::{Prehashed, Tracked}; -use super::{capability, Args, Content, Func, NodeId, Regex, Smart, Value}; +use super::{capability, Args, Content, Dict, Func, NodeId, Regex, Smart, Value}; use crate::diag::SourceResult; use crate::geom::{ Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides, @@ -141,9 +141,9 @@ pub enum StyleEntry { /// A barrier for scoped styles. Barrier(Barrier), /// Guards against recursive show rules. - Guard(Selector), + Guard(RecipeId), /// Allows recursive show rules again. - Unguard(Selector), + Unguard(RecipeId), } impl StyleEntry { @@ -243,8 +243,7 @@ impl<'a> StyleChain<'a> { world: Tracked, target: Target, ) -> SourceResult> { - // Find out how many recipes there any and whether any of their patterns - // match. + // Find out how many recipes there any and whether any of them match. let mut n = 0; let mut any = true; for recipe in self.entries().filter_map(StyleEntry::recipe) { @@ -258,7 +257,7 @@ impl<'a> StyleChain<'a> { if any { for recipe in self.entries().filter_map(StyleEntry::recipe) { if recipe.applicable(target) { - let sel = Selector::Nth(n); + let sel = RecipeId::Nth(n); if self.guarded(sel) { guarded = true; } else if let Some(content) = recipe.apply(world, sel, target)? { @@ -273,7 +272,7 @@ impl<'a> StyleChain<'a> { if let Target::Node(node) = target { // Realize if there was no matching recipe. if realized.is_none() { - let sel = Selector::Base(node.id()); + let sel = RecipeId::Base(node.id()); if self.guarded(sel) { guarded = true; } else { @@ -302,7 +301,7 @@ impl<'a> StyleChain<'a> { } /// Whether the recipe identified by the selector is guarded. - fn guarded(self, sel: Selector) -> bool { + fn guarded(self, sel: RecipeId) -> bool { for entry in self.entries() { match *entry { StyleEntry::Guard(s) if s == sel => return true, @@ -976,8 +975,8 @@ impl Fold for PartialStroke { pub struct Recipe { /// The span errors are reported with. pub span: Span, - /// The pattern that the rule applies to. - pub pattern: Option, + /// Determines whether the recipe applies to a node. + pub selector: Option, /// The transformation to perform on the match. pub transform: Transform, } @@ -985,28 +984,26 @@ pub struct Recipe { impl Recipe { /// Whether the recipe is applicable to the target. pub fn applicable(&self, target: Target) -> bool { - match (&self.pattern, target) { - (Some(Pattern::Node(id)), Target::Node(node)) => *id == node.id(), - (Some(Pattern::Regex(_)), Target::Text(_)) => true, - _ => false, - } + self.selector + .as_ref() + .map_or(false, |selector| selector.matches(target)) } /// Try to apply the recipe to the target. pub fn apply( &self, world: Tracked, - sel: Selector, + sel: RecipeId, target: Target, ) -> SourceResult> { - let content = match (target, &self.pattern) { - (Target::Node(node), Some(Pattern::Node(id))) if node.id() == *id => { + let content = match (target, &self.selector) { + (Target::Node(node), Some(Selector::Node(id, _))) if node.id() == *id => { self.transform.apply(world, self.span, || { Value::Content(node.to::().unwrap().unguard_parts(sel)) })? } - (Target::Text(text), Some(Pattern::Regex(regex))) => { + (Target::Text(text), Some(Selector::Regex(regex))) => { let make = world.config().items.text; let mut result = vec![]; let mut cursor = 0; @@ -1043,8 +1040,8 @@ impl Recipe { /// Whether this recipe is for the given node. pub fn is_of(&self, node: NodeId) -> bool { - match self.pattern { - Some(Pattern::Node(id)) => id == node, + match self.selector { + Some(Selector::Node(id, _)) => id == node, _ => false, } } @@ -1052,24 +1049,42 @@ impl Recipe { impl Debug for Recipe { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Recipe matching {:?}", self.pattern) + write!(f, "Recipe matching {:?}", self.selector) } } -/// A show rule pattern that may match a target. +/// A selector in a show rule. #[derive(Debug, Clone, PartialEq, Hash)] -pub enum Pattern { - /// Defines the appearence of some node. - Node(NodeId), - /// Defines text to be replaced. +pub enum Selector { + /// Matches a specific type of node. + /// + /// If there is a dictionary, only nodes with the fields from the + /// dictionary match. + Node(NodeId, Option), + /// Matches text through a regular expression. Regex(Regex), } -impl Pattern { - /// Define a simple text replacement pattern. +impl Selector { + /// Define a simple text selector. pub fn text(text: &str) -> Self { Self::Regex(Regex::new(®ex::escape(text)).unwrap()) } + + /// Whether the selector matches for the target. + pub fn matches(&self, target: Target) -> bool { + match (self, target) { + (Self::Node(id, dict), Target::Node(node)) => { + *id == node.id() + && dict + .iter() + .flat_map(|dict| dict.iter()) + .all(|(name, value)| node.field(name).as_ref() == Some(value)) + } + (Self::Regex(_), Target::Text(_)) => true, + _ => false, + } + } } /// A show rule transformation that can be applied to a match. @@ -1113,7 +1128,7 @@ pub enum Target<'a> { /// Identifies a show rule recipe. #[derive(Debug, Copy, Clone, PartialEq, Hash)] -pub enum Selector { +pub enum RecipeId { /// The nth recipe from the top of the chain. Nth(usize), /// The base recipe for a kind of node. @@ -1123,8 +1138,8 @@ pub enum Selector { /// A node that can be realized given some styles. #[capability] pub trait Show: 'static + Sync + Send { - /// Unguard nested content against recursive show rules. - fn unguard_parts(&self, sel: Selector) -> Content; + /// Unguard nested content against a specific recipe. + fn unguard_parts(&self, id: RecipeId) -> Content; /// The base recipe for this node that is executed if there is no /// user-defined show rule. diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 1b0e8985c..547545c7c 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -1358,8 +1358,8 @@ node! { } impl ShowRule { - /// The pattern that this rule matches. - pub fn pattern(&self) -> Option { + /// Defines which nodes the show rule applies to. + pub fn selector(&self) -> Option { self.0 .children() .rev() diff --git a/tests/ref/style/show-selector.png b/tests/ref/style/show-selector.png new file mode 100644 index 0000000000000000000000000000000000000000..1cdcfa3f0bd7d492b53474a44e1023591f904c2f GIT binary patch literal 12923 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