diff options
| author | Sébastien d'Herbais de Thun <sebastien.d.herbais@gmail.com> | 2023-04-12 12:47:51 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-04-12 12:47:51 +0200 |
| commit | 1198e0cd385737efc38dbd8ba13db802a68e0dc7 (patch) | |
| tree | ad15c6173f196cc6df30faca1e94bac2aa400894 | |
| parent | fe2640c55268f167d8749f77b37e52b7b17f21dd (diff) | |
Selector rework (#640)
| -rw-r--r-- | docs/src/reference/types.md | 70 | ||||
| -rw-r--r-- | library/src/lib.rs | 1 | ||||
| -rw-r--r-- | library/src/meta/bibliography.rs | 6 | ||||
| -rw-r--r-- | library/src/meta/counter.rs | 17 | ||||
| -rw-r--r-- | library/src/meta/figure.rs | 12 | ||||
| -rw-r--r-- | library/src/meta/outline.rs | 5 | ||||
| -rw-r--r-- | library/src/meta/query.rs | 57 | ||||
| -rw-r--r-- | library/src/meta/state.rs | 7 | ||||
| -rw-r--r-- | src/eval/methods.rs | 29 | ||||
| -rw-r--r-- | src/model/content.rs | 31 | ||||
| -rw-r--r-- | src/model/introspect.rs | 75 | ||||
| -rw-r--r-- | src/model/realize.rs | 9 | ||||
| -rw-r--r-- | src/model/styles.rs | 170 | ||||
| -rw-r--r-- | tests/ref/meta/query-before-after.png | bin | 0 -> 180629 bytes | |||
| -rw-r--r-- | tests/ref/meta/query.png | bin | 445229 -> 554447 bytes | |||
| -rw-r--r-- | tests/typ/compiler/show-node.typ | 2 | ||||
| -rw-r--r-- | tests/typ/meta/query-before-after.typ | 69 | ||||
| -rw-r--r-- | tests/typ/meta/query.typ | 6 |
18 files changed, 452 insertions, 114 deletions
diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md index 7183bac4..f5f7e6e4 100644 --- a/docs/src/reference/types.md +++ b/docs/src/reference/types.md @@ -910,3 +910,73 @@ You can access definitions from the module using >>> >>> #(-3) ``` + +# Selector +A filter for selecting elements within the document. + +You can construct a selector in the following ways: +- you can use an element function +- you can filter for an element function with + [specific fields]($type/function.where) +- you can use a [string]($type/string) or [regular expression]($func/regex) +- you can use a [`{<label>}`]($func/label) +- you can use a [`location`]($func/locate) +- call the [`selector`]($func/selector) function to convert any of the above + types into a selector value and use the methods below to refine it + +A selector is what you can use to query the document for certain types +of elements. It can also be used to apply styling rules to element. You can +combine multiple selectors using the methods shown below. + +Selectors can also be passed to several of Typst's built-in functions to +configure their behaviour. One such example is the [outline]($func/outline) +where it can be use to change which elements are listed within the outline. + +## Example +```example +#locate(loc => query( + heading.where(level: 1) + .or(heading.where(level: 2)), + loc, +)) + += This will be found +== So will this +=== But this will not. +``` + +## Methods +### or() +Allows combining any of a series of selectors. This is used to +select multiple components or components with different properties +all at once. + +- other: selector (variadic, required) + The list of selectors to match on. + +### and() +Allows combining all of a series of selectors. This is used to check +whether a component meets multiple selection rules simultaneously. + +- other: selector (variadic, required) + The list of selectors to match on. + +### before() +Returns a modified selector that will only match elements that occur before the +first match of the selector argument. + +- end: selector (positional, required) + The original selection will end at the first match of `end`. +- inclusive: boolean (named) + Whether `end` itself should match or not. This is only relevant if both + selectors match the same type of element. Defaults to `{true}`. + +### after() +Returns a modified selector that will only match elements that occur after the +first match of the selector argument. + +- start: selector (positional, required) + The original selection will start at the first match of `start`. +- inclusive: boolean (named) + Whether `start` itself should match or not. This is only relevant if both + selectors match the same type of element. Defaults to `{true}`. diff --git a/library/src/lib.rs b/library/src/lib.rs index ac99425f..9b4e9644 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -102,6 +102,7 @@ fn global(math: Module, calc: Module) -> Module { global.define("numbering", meta::numbering); global.define("state", meta::state); global.define("query", meta::query); + global.define("selector", meta::selector); // Symbols. global.define("sym", symbols::sym()); diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs index cd167049..8fa2ee34 100644 --- a/library/src/meta/bibliography.rs +++ b/library/src/meta/bibliography.rs @@ -91,7 +91,7 @@ cast_to_value! { impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> { - let mut iter = introspector.query(Self::func().select()).into_iter(); + let mut iter = introspector.query(&Self::func().select()).into_iter(); let Some(elem) = iter.next() else { return Err("the document does not contain a bibliography".into()); }; @@ -106,7 +106,7 @@ impl BibliographyElem { /// Whether the bibliography contains the given key. pub fn has(vt: &Vt, key: &str) -> bool { vt.introspector - .query(Self::func().select()) + .query(&Self::func().select()) .into_iter() .flat_map(|elem| load(vt.world, &elem.to::<Self>().unwrap().path())) .flatten() @@ -395,7 +395,7 @@ impl Works { let bibliography = BibliographyElem::find(vt.introspector)?; let citations = vt .introspector - .query(Selector::Any(eco_vec![ + .query(&Selector::Or(eco_vec![ RefElem::func().select(), CiteElem::func().select(), ])) diff --git a/library/src/meta/counter.rs b/library/src/meta/counter.rs index d771168d..b1d2e3bd 100644 --- a/library/src/meta/counter.rs +++ b/library/src/meta/counter.rs @@ -335,7 +335,10 @@ impl Counter { /// Get the value of the state at the given location. pub fn at(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> { let sequence = self.sequence(vt)?; - let offset = vt.introspector.query_before(self.selector(), location).len(); + let offset = vt + .introspector + .query(&Selector::before(self.selector(), location, true)) + .len(); let (mut state, page) = sequence[offset].clone(); if self.is_page() { let delta = vt.introspector.page(location).get().saturating_sub(page.get()); @@ -359,7 +362,10 @@ impl Counter { /// Get the current and final value of the state combined in one state. pub fn both(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> { let sequence = self.sequence(vt)?; - let offset = vt.introspector.query_before(self.selector(), location).len(); + let offset = vt + .introspector + .query(&Selector::before(self.selector(), location, true)) + .len(); let (mut at_state, at_page) = sequence[offset].clone(); let (mut final_state, final_page) = sequence.last().unwrap().clone(); if self.is_page() { @@ -412,11 +418,10 @@ impl Counter { let mut page = NonZeroUsize::ONE; let mut stops = eco_vec![(state.clone(), page)]; - for elem in introspector.query(self.selector()) { + for elem in introspector.query(&self.selector()) { if self.is_page() { - let location = elem.location().unwrap(); let prev = page; - page = introspector.page(location); + page = introspector.page(elem.location().unwrap()); let delta = page.get() - prev.get(); if delta > 0 { @@ -446,7 +451,7 @@ impl Counter { Selector::Elem(UpdateElem::func(), Some(dict! { "counter" => self.clone() })); if let CounterKey::Selector(key) = &self.0 { - selector = Selector::Any(eco_vec![selector, key.clone()]); + selector = Selector::Or(eco_vec![selector, key.clone()]); } selector diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs index 964bb4ec..a838b9a5 100644 --- a/library/src/meta/figure.rs +++ b/library/src/meta/figure.rs @@ -173,14 +173,14 @@ impl Synthesize for FigureElem { // Determine the figure's kind. let kind = match self.kind(styles) { Smart::Auto => self - .find_figurable(styles) + .find_figurable(vt, styles) .map(|elem| FigureKind::Elem(elem.func())) .unwrap_or_else(|| FigureKind::Elem(ImageElem::func())), Smart::Custom(kind) => kind, }; let content = match &kind { - FigureKind::Elem(func) => self.find_of_elem(*func), + FigureKind::Elem(func) => self.find_of_elem(vt, *func), FigureKind::Name(_) => None, } .unwrap_or_else(|| self.body()); @@ -303,9 +303,9 @@ impl Refable for FigureElem { impl FigureElem { /// Determines the type of the figure by looking at the content, finding all /// [`Figurable`] elements and sorting them by priority then returning the highest. - pub fn find_figurable(&self, styles: StyleChain) -> Option<Content> { + pub fn find_figurable(&self, vt: &Vt, styles: StyleChain) -> Option<Content> { self.body() - .query(Selector::can::<dyn Figurable>()) + .query(vt.introspector, Selector::can::<dyn Figurable>()) .into_iter() .max_by_key(|elem| elem.with::<dyn Figurable>().unwrap().priority(styles)) .cloned() @@ -313,9 +313,9 @@ impl FigureElem { /// Finds the element with the given function in the figure's content. /// Returns `None` if no element with the given function is found. - pub fn find_of_elem(&self, func: ElemFunc) -> Option<Content> { + pub fn find_of_elem(&self, vt: &Vt, func: ElemFunc) -> Option<Content> { self.body() - .query(Selector::Elem(func, None)) + .query(vt.introspector, Selector::Elem(func, None)) .into_iter() .next() .cloned() diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 4c12da75..099c3bdf 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -154,14 +154,13 @@ impl Show for OutlineElem { let lang = TextElem::lang_in(styles); let mut ancestors: Vec<&Content> = vec![]; - let elems = vt.introspector.query(self.target(styles)); + let elems = vt.introspector.query(&self.target(styles)); for elem in &elems { let Some(refable) = elem.with::<dyn Refable>() else { bail!(elem.span(), "outlined elements must be referenceable"); }; - let location = elem.location().expect("missing location"); if depth < refable.level() { continue; } @@ -170,6 +169,8 @@ impl Show for OutlineElem { continue; }; + let location = elem.location().unwrap(); + // Deals with the ancestors of the current element. // This is only applicable for elements with a hierarchy/level. while ancestors diff --git a/library/src/meta/query.rs b/library/src/meta/query.rs index 4241c19d..f256af23 100644 --- a/library/src/meta/query.rs +++ b/library/src/meta/query.rs @@ -38,8 +38,8 @@ use crate::prelude::*; /// >>> ) /// #set page(header: locate(loc => { /// let elems = query( -/// heading, -/// before: loc, +/// selector(heading).before(loc), +/// loc, /// ) /// let academy = smallcaps[ /// Typst Academy @@ -102,8 +102,7 @@ pub fn query( /// elements with an explicit label. As a result, you _can_ query for e.g. /// [`strong`]($func/strong) elements, but you will find only those that /// have an explicit label attached to them. This limitation will be - /// resolved - /// in the future. + /// resolved in the future. target: LocatableSelector, /// Can be any location. Why is it required then? As noted before, Typst has @@ -115,39 +114,25 @@ pub fn query( /// could depend on the query's result. /// /// Only one of this, `before`, and `after` shall be given. - #[external] - #[default] location: Location, +) -> Value { + let _ = location; + vm.vt.introspector.query(&target.0).into() +} - /// If given, returns only those elements that are before the given - /// location. A suitable location can be retrieved from - /// [`locate`]($func/locate), but also through the - /// [`location()`]($type/content.location) method on content returned by - /// another query. Only one of `location`, this, and `after` shall be given. - #[named] - #[external] - #[default] - before: Location, - - /// If given, returns only those elements that are after the given location. - /// A suitable location can be retrieved from [`locate`]($func/locate), but - /// also through the [`location()`]($type/content.location) method on - /// content returned by another query. Only one of `location`, `before`, and - /// this shall be given. - #[named] - #[external] - #[default] - after: Location, +/// Turns a value into a selector. The following values are accepted: +/// - An element function like a `heading` or `figure`. +/// - A `{<label>}`. +/// - A more complex selector like `{heading.where(level: 1)}`. +/// +/// Display: Selector +/// Category: meta +/// Returns: content +#[func] +pub fn selector( + /// Can be an element function like a `heading` or `figure`, a `{<label>}` + /// or a more complex selector like `{heading.where(level: 1)}`. + target: Selector, ) -> Value { - let selector = target.0; - let introspector = vm.vt.introspector; - let elements = if let Some(location) = args.named("before")? { - introspector.query_before(selector, location) - } else if let Some(location) = args.named("after")? { - introspector.query_after(selector, location) - } else { - let _: Location = args.expect("location")?; - introspector.query(selector) - }; - elements.into() + target.into() } diff --git a/library/src/meta/state.rs b/library/src/meta/state.rs index 3e6d65b2..781d4294 100644 --- a/library/src/meta/state.rs +++ b/library/src/meta/state.rs @@ -282,7 +282,10 @@ impl State { /// Get the value of the state at the given location. pub fn at(self, vt: &mut Vt, location: Location) -> SourceResult<Value> { let sequence = self.sequence(vt)?; - let offset = vt.introspector.query_before(self.selector(), location).len(); + let offset = vt + .introspector + .query(&Selector::before(self.selector(), location, true)) + .len(); Ok(sequence[offset].clone()) } @@ -323,7 +326,7 @@ impl State { let mut state = self.init.clone(); let mut stops = eco_vec![state.clone()]; - for elem in introspector.query(self.selector()) { + for elem in introspector.query(&self.selector()) { let elem = elem.to::<UpdateElem>().unwrap(); match elem.update() { StateUpdate::Set(value) => state = value, diff --git a/src/eval/methods.rs b/src/eval/methods.rs index 452b90da..56d1c7b7 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -4,7 +4,7 @@ use ecow::EcoString; use super::{Args, Str, Value, Vm}; use crate::diag::{At, SourceResult}; -use crate::model::Location; +use crate::model::{Location, Selector}; use crate::syntax::Span; /// Call a method on a value. @@ -151,11 +151,29 @@ pub fn call( }, Value::Dyn(dynamic) => { - if let Some(&location) = dynamic.downcast::<Location>() { + if let Some(location) = dynamic.downcast::<Location>() { match method { - "page" => vm.vt.introspector.page(location).into(), - "position" => vm.vt.introspector.position(location).into(), - "page-numbering" => vm.vt.introspector.page_numbering(location), + "page" => vm.vt.introspector.page(*location).into(), + "position" => vm.vt.introspector.position(*location).into(), + "page-numbering" => vm.vt.introspector.page_numbering(*location), + _ => return missing(), + } + } else if let Some(selector) = dynamic.downcast::<Selector>() { + match method { + "or" => selector.clone().or(args.all::<Selector>()?).into(), + "and" => selector.clone().and(args.all::<Selector>()?).into(), + "before" => { + let location = args.expect::<Selector>("selector")?; + let inclusive = + args.named_or_find::<bool>("inclusive")?.unwrap_or(true); + selector.clone().before(location, inclusive).into() + } + "after" => { + let location = args.expect::<Selector>("selector")?; + let inclusive = + args.named_or_find::<bool>("inclusive")?.unwrap_or(true); + selector.clone().after(location, inclusive).into() + } _ => return missing(), } } else { @@ -312,6 +330,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { "function" => &[("where", true), ("with", true)], "arguments" => &[("named", false), ("pos", false)], "location" => &[("page", false), ("position", false), ("page-numbering", false)], + "selector" => &[("or", true), ("and", true), ("before", true), ("after", true)], "counter" => &[ ("display", true), ("at", true), diff --git a/src/model/content.rs b/src/model/content.rs index da3b8b75..2ce12a51 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -3,11 +3,12 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::iter::Sum; use std::ops::{Add, AddAssign}; +use comemo::Tracked; use ecow::{eco_format, EcoString, EcoVec}; use super::{ - element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable, - Location, Recipe, Selector, Style, Styles, Synthesize, + element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Introspector, Label, + Locatable, Location, Recipe, Selector, Style, Styles, Synthesize, }; use crate::diag::{SourceResult, StrResult}; use crate::doc::Meta; @@ -358,38 +359,50 @@ impl Content { /// /// # Show rules /// Elements produced in `show` rules will not be included in the results. - pub fn query(&self, selector: Selector) -> Vec<&Content> { + pub fn query( + &self, + introspector: Tracked<Introspector>, + selector: Selector, + ) -> Vec<&Content> { let mut results = Vec::new(); - self.query_into(&selector, &mut results); + self.query_into(introspector, &selector, &mut results); results } /// Queries the content tree for all elements that match the given selector /// and stores the results inside of the `results` vec. - fn query_into<'a>(&'a self, selector: &Selector, results: &mut Vec<&'a Content>) { + fn query_into<'a>( + &'a self, + introspector: Tracked<Introspector>, + selector: &Selector, + results: &mut Vec<&'a Content>, + ) { if selector.matches(self) { results.push(self); } for attr in &self.attrs { match attr { - Attr::Child(child) => child.query_into(selector, results), - Attr::Value(value) => walk_value(&value, selector, results), + Attr::Child(child) => child.query_into(introspector, selector, results), + Attr::Value(value) => walk_value(introspector, &value, selector, results), _ => {} } } /// Walks a given value to find any content that matches the selector. fn walk_value<'a>( + introspector: Tracked<Introspector>, value: &'a Value, selector: &Selector, results: &mut Vec<&'a Content>, ) { match value { - Value::Content(content) => content.query_into(selector, results), + Value::Content(content) => { + content.query_into(introspector, selector, results) + } Value::Array(array) => { for value in array { - walk_value(value, selector, results); + walk_value(introspector, value, selector, results); } } _ => {} diff --git a/src/model/introspect.rs b/src/model/introspect.rs index 9697f850..031f2d5e 100644 --- a/src/model/introspect.rs +++ b/src/model/introspect.rs @@ -84,9 +84,11 @@ impl StabilityProvider { /// Can be queried for elements and their positions. pub struct Introspector { + /// The number of pages in the document. pages: usize, - elems: IndexMap<Option<Location>, (Content, Position)>, - // Indexed by page number. + /// All introspectable elements. + elems: IndexMap<Location, (Content, Position)>, + /// The page numberings, indexed by page number minus 1. page_numberings: Vec<Value>, } @@ -106,8 +108,8 @@ impl Introspector { } /// Iterate over all elements. - pub fn all(&self) -> impl Iterator<Item = &Content> { - self.elems.values().map(|(elem, _)| elem) + pub fn all(&self) -> impl Iterator<Item = Content> + '_ { + self.elems.values().map(|(c, _)| c).cloned() } /// Extract metadata from a frame. @@ -121,11 +123,11 @@ impl Introspector { self.extract(&group.frame, page, ts); } FrameItem::Meta(Meta::Elem(content), _) - if !self.elems.contains_key(&content.location()) => + if !self.elems.contains_key(&content.location().unwrap()) => { let pos = pos.transform(ts); let ret = self.elems.insert( - content.location(), + content.location().unwrap(), (content.clone(), Position { page, point: pos }), ); assert!(ret.is_none(), "duplicate locations"); @@ -146,32 +148,33 @@ impl Introspector { self.pages > 0 } - /// Query for all matching elements. - pub fn query(&self, selector: Selector) -> Vec<Content> { - self.all().filter(|elem| selector.matches(elem)).cloned().collect() + /// Get an element from the position cache. + pub fn location(&self, location: &Location) -> Option<Content> { + self.elems.get(location).map(|(c, _)| c).cloned() } - /// Query for all matching element up to the given location. - pub fn query_before(&self, selector: Selector, location: Location) -> Vec<Content> { - let mut matches = vec![]; - for elem in self.all() { - if selector.matches(elem) { - matches.push(elem.clone()); - } - if elem.location() == Some(location) { - break; - } + /// Query for all matching elements. + pub fn query<'a>(&'a self, selector: &'a Selector) -> Vec<Content> { + match selector { + Selector::Location(location) => self + .elems + .get(location) + .map(|(content, _)| content) + .cloned() + .into_iter() + .collect(), + _ => selector.match_iter(self).collect(), } - matches } - /// Query for all matching elements starting from the given location. - pub fn query_after(&self, selector: Selector, location: Location) -> Vec<Content> { - self.all() - .skip_while(|elem| elem.location() != Some(location)) - .filter(|elem| selector.matches(elem)) - .cloned() - .collect() + /// Query for the first matching element. + pub fn query_first<'a>(&'a self, selector: &'a Selector) -> Option<Content> { + match selector { + Selector::Location(location) => { + self.elems.get(location).map(|(content, _)| content).cloned() + } + _ => selector.match_iter(self).next(), + } } /// Query for a unique element with the label. @@ -205,8 +208,24 @@ impl Introspector { /// Find the position for the given location. pub fn position(&self, location: Location) -> Position { self.elems - .get(&Some(location)) + .get(&location) .map(|(_, loc)| *loc) .unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() }) } + + /// Checks whether `a` is before `b` in the document. + pub fn is_before(&self, a: Location, b: Location, inclusive: bool) -> bool { + let a = self.elems.get_index_of(&a).unwrap(); + let b = self.elems.get_index_of(&b).unwrap(); + if inclusive { + a <= b + } else { + a < b + } + } + + /// Checks whether `a` is after `b` in the document. + pub fn is_after(&self, a: Location, b: Location, inclusive: bool) -> bool { + !self.is_before(a, b, !inclusive) + } } diff --git a/src/model/realize.rs b/src/model/realize.rs index 48a0fbdc..ee9049ad 100644 --- a/src/model/realize.rs +++ b/src/model/realize.rs @@ -152,7 +152,14 @@ fn try_apply( } // Not supported here. - Some(Selector::Any(_) | Selector::All(_) | Selector::Can(_)) => Ok(None), + Some( + Selector::Or(_) + | Selector::And(_) + | Selector::Location(_) + | Selector::Can(_) + | Selector::Before { .. } + | Selector::After { .. }, + ) => Ok(None), None => Ok(None), } diff --git a/src/model/styles.rs b/src/model/styles.rs index 097c5138..a841ca56 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -2,10 +2,11 @@ use std::any::{Any, TypeId}; use std::fmt::{self, Debug, Formatter, Write}; use std::iter; use std::mem; +use std::sync::Arc; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; -use super::{Content, ElemFunc, Element, Label, Vt}; +use super::{Content, ElemFunc, Element, Introspector, Label, Location, Vt}; use crate::diag::{SourceResult, StrResult, Trace, Tracepoint}; use crate::eval::{cast_from_value, Args, Cast, CastInfo, Dict, Func, Regex, Value, Vm}; use crate::model::Locatable; @@ -258,6 +259,8 @@ pub enum Selector { /// If there is a dictionary, only elements with the fields from the /// dictionary match. Elem(ElemFunc, Option<Dict>), + /// Matches the element at the specified location. + Location(Location), /// Matches elements with a specific label. Label(Label), /// Matches text elements through a regular expression. @@ -265,9 +268,13 @@ pub enum Selector { /// Matches elements with a specific capability. Can(TypeId), /// Matches if any of the subselectors match. - Any(EcoVec<Self>), + Or(EcoVec<Self>), /// Matches if all of the subselectors match. - All(EcoVec<Self>), + And(EcoVec<Self>), + /// Matches all matches of `selector` before `end`. + Before { selector: Arc<Self>, end: Arc<Self>, inclusive: bool }, + /// Matches all matches of `selector` after `start`. + After { selector: Arc<Self>, start: Arc<Self>, inclusive: bool }, } impl Selector { @@ -281,6 +288,107 @@ impl Selector { Self::Can(TypeId::of::<T>()) } + /// Transforms this selector and an iterator of other selectors into a + /// [`Selector::Or`] selector. + pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self { + Self::And(others.into_iter().chain(Some(self)).collect()) + } + + /// Transforms this selector and an iterator of other selectors into a + /// [`Selector::And`] selector. + pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self { + Self::Or(others.into_iter().chain(Some(self)).collect()) + } + + /// Transforms this selector into a [`Selector::Before`] selector. + pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self { + Self::Before { + selector: Arc::new(self), + end: Arc::new(location.into()), + inclusive, + } + } + + /// Transforms this selector into a [`Selector::After`] selector. + pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self { + Self::After { + selector: Arc::new(self), + start: Arc::new(location.into()), + inclusive, + } + } + + /// Matches the selector for an introspector. + pub fn match_iter<'a>( + &'a self, + introspector: &'a Introspector, + ) -> Box<dyn Iterator<Item = Content> + 'a> { + self.match_iter_inner(introspector, introspector.all()) + } + + /// Match the selector against the given list of elements. Returns an + /// iterator over the matching elements. + fn match_iter_inner<'a>( + &'a self, + introspector: &'a Introspector, + parent: impl Iterator<Item = Content> + 'a, + ) -> Box<dyn Iterator<Item = Content> + 'a> { + match self { + Self::Location(location) => { + Box::new(introspector.location(location).into_iter()) + } + Self::Or(selectors) => Box::new(parent.filter(|element| { + selectors.iter().any(|selector| { + selector + .match_iter_inner(introspector, std::iter::once(element.clone())) + .next() + .is_some() + }) + })), + Self::And(selectors) => Box::new(parent.filter(|element| { + selectors.iter().all(|selector| { + selector + .match_iter_inner(introspector, std::iter::once(element.clone())) + .next() + .is_some() + }) + })), + Self::Before { selector, end: location, inclusive } => { + if let Some(content) = introspector.query_first(location) { + let loc = content.location().unwrap(); + Box::new(selector.match_iter_inner(introspector, parent).filter( + move |elem| { + introspector.is_before( + elem.location().unwrap(), + loc, + *inclusive, + ) + }, + )) + } else { + Box::new(selector.match_iter_inner(introspector, parent)) + } + } + Self::After { selector, start: location, inclusive } => { + if let Some(content) = introspector.query_first(location) { + let loc = content.location().unwrap(); + Box::new(selector.match_iter_inner(introspector, parent).filter( + move |elem| { + introspector.is_after( + elem.location().unwrap(), + loc, + *inclusive, + ) + }, + )) + } else { + Box::new(std::iter::empty()) + } + } + other => Box::new(parent.filter(move |content| other.matches(content))), + } + } + /// Whether the selector matches for the target. pub fn matches(&self, target: &Content) -> bool { match self { @@ -297,12 +405,22 @@ impl Selector { && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) } Self::Can(cap) => target.can_type_id(*cap), - Self::Any(selectors) => selectors.iter().any(|sel| sel.matches(target)), - Self::All(selectors) => selectors.iter().all(|sel| sel.matches(target)), + Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), + Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), + Self::Location(location) => target.location() == Some(*location), + Self::Before { .. } | Self::After { .. } => { + panic!("Cannot match a `Selector::Before` or `Selector::After` selector") + } } } } +impl From<Location> for Selector { + fn from(value: Location) -> Self { + Self::Location(value) + } +} + impl Debug for Selector { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { @@ -317,12 +435,29 @@ impl Debug for Selector { Self::Label(label) => label.fmt(f), Self::Regex(regex) => regex.fmt(f), Self::Can(cap) => cap.fmt(f), - Self::Any(selectors) | Self::All(selectors) => { - f.write_str(if matches!(self, Self::Any(_)) { "any" } else { "all" })?; + Self::Or(selectors) | Self::And(selectors) => { + f.write_str(if matches!(self, Self::Or(_)) { "or" } else { "and" })?; let pieces: Vec<_> = selectors.iter().map(|sel| eco_format!("{sel:?}")).collect(); f.write_str(&pretty_array_like(&pieces, false)) } + Self::Location(loc) => loc.fmt(f), + Self::Before { selector, end: split, inclusive } + | Self::After { selector, start: split, inclusive } => { + selector.fmt(f)?; + + if matches!(self, Self::Before { .. }) { + f.write_str(".before(")?; + } else { + f.write_str(".after(")?; + } + + split.fmt(f)?; + if !*inclusive { + f.write_str(", inclusive: false")?; + } + f.write_char(')') + } } } } @@ -336,9 +471,10 @@ cast_from_value! { label: Label => Self::Label(label), text: EcoString => Self::text(&text), regex: Regex => Self::Regex(regex), + location: Location => Self::Location(location), } -/// A selector that can be used with `query`. Hopefully, this is made obsolote +/// A selector that can be used with `query`. Hopefully, this is made obsolete /// by a more powerful query mechanism in the future. #[derive(Clone, PartialEq, Hash)] pub struct LocatableSelector(pub Selector); @@ -352,16 +488,26 @@ impl Cast for LocatableSelector { fn cast(value: Value) -> StrResult<Self> { fn validate(selector: &Selector) -> StrResult<()> { match &selector { - Selector::Elem(elem, _) if !elem.can::<dyn Locatable>() => { - Err(eco_format!("{} is not locatable", elem.name()))? + Selector::Elem(elem, _) => { + if !elem.can::<dyn Locatable>() { + Err(eco_format!("{} is not locatable", elem.name()))? + } } + Selector::Location(_) => {} + Selector::Label(_) => {} Selector::Regex(_) => Err("text is not locatable")?, - Selector::Any(list) | Selector::All(list) => { + Selector::Can(_) => Err("capability is not locatable")?, + Selector::Or(list) | Selector::And(list) => { for selector in list { validate(selector)?; } } - _ => {} + Selector::Before { selector, end: split, .. } + | Selector::After { selector, start: split, .. } => { + for selector in [selector, split] { + validate(selector)?; + } + } } Ok(()) } diff --git a/tests/ref/meta/query-before-after.png b/tests/ref/meta/query-before-after.png Binary files differnew file mode 100644 index 00000000..8757ce8e --- /dev/null +++ b/tests/ref/meta/query-before-after.png diff --git a/tests/ref/meta/query.png b/tests/ref/meta/query.png Binary files differindex facf2b89..7801d22b 100644 --- a/tests/ref/meta/query.png +++ b/tests/ref/meta/query.png diff --git a/tests/typ/compiler/show-node.typ b/tests/typ/compiler/show-node.typ index 99a4364e..42aee506 100644 --- a/tests/typ/compiler/show-node.typ +++ b/tests/typ/compiler/show-node.typ @@ -96,7 +96,7 @@ Hey = Heading --- -// Error: 7-10 expected function, label, string, regular expression, or selector, found color +// Error: 7-10 expected function, label, string, regular expression, location, or selector, found color #show red: [] --- diff --git a/tests/typ/meta/query-before-after.typ b/tests/typ/meta/query-before-after.typ new file mode 100644 index 00000000..734e84b4 --- /dev/null +++ b/tests/typ/meta/query-before-after.typ @@ -0,0 +1,69 @@ + +--- +#set page( + paper: "a7", + numbering: "1 / 1", + margin: (bottom: 1cm, rest: 0.5cm), +) + +#show heading.where(level: 1, outlined: true): it => [ + #it + + #set text(size: 12pt, weight: "regular") + #outline( + title: "Chapter outline", + indent: true, + target: heading + .where(level: 1) + .or(heading.where(level: 2)) + .after(it.location(), inclusive: true) + .before( + heading + .where(level: 1, outlined: true) + .after(it.location(), inclusive: false), + inclusive: false, + ) + ) +] + +#set heading(outlined: true, numbering: "1.") + += Section 1 +== Subsection 1 +== Subsection 2 +=== Subsubsection 1 +=== Subsubsection 2 +== Subsection 3 += Section 2 +== Subsection 1 +== Subsection 2 + += Section 3 +== Subsection 1 +== Subsection 2 +=== Subsubsection 1 +=== Subsubsection 2 +=== Subsubsection 3 +== Subsection 3 + +--- + +#set page( + paper: "a7", + numbering: "1 / 1", + margin: (bottom: 1cm, rest: 0.5cm), +) + +#set heading(outlined: true, numbering: "1.") + +// This is purposefully an empty +#locate(loc => [ + Non-outlined elements: + #(query(selector(heading).and(heading.where(outlined: false)), loc) + .map(it => it.body).join(", ")) +]) + +#heading("A", outlined: false) +#heading("B", outlined: true) +#heading("C", outlined: true) +#heading("D", outlined: false) diff --git a/tests/typ/meta/query.typ b/tests/typ/meta/query.typ index 7dfbbcfc..91325b61 100644 --- a/tests/typ/meta/query.typ +++ b/tests/typ/meta/query.typ @@ -8,8 +8,8 @@ smallcaps[Typst Academy] h(1fr) locate(it => { - let after = query(heading, after: it) - let before = query(heading, before: it) + let after = query(selector(heading).after(it), it) + let before = query(selector(heading).before(it), it) let elem = if before.len() != 0 { before.last() } else if after.len() != 0 { @@ -43,7 +43,7 @@ = List of Figures #locate(it => { - let elements = query(figure, after: it) + let elements = query(selector(figure).after(it), it) for it in elements [ Figure #numbering(it.numbering, |
