summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/src/reference/types.md70
-rw-r--r--library/src/lib.rs1
-rw-r--r--library/src/meta/bibliography.rs6
-rw-r--r--library/src/meta/counter.rs17
-rw-r--r--library/src/meta/figure.rs12
-rw-r--r--library/src/meta/outline.rs5
-rw-r--r--library/src/meta/query.rs57
-rw-r--r--library/src/meta/state.rs7
-rw-r--r--src/eval/methods.rs29
-rw-r--r--src/model/content.rs31
-rw-r--r--src/model/introspect.rs75
-rw-r--r--src/model/realize.rs9
-rw-r--r--src/model/styles.rs170
-rw-r--r--tests/ref/meta/query-before-after.pngbin0 -> 180629 bytes
-rw-r--r--tests/ref/meta/query.pngbin445229 -> 554447 bytes
-rw-r--r--tests/typ/compiler/show-node.typ2
-rw-r--r--tests/typ/meta/query-before-after.typ69
-rw-r--r--tests/typ/meta/query.typ6
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
new file mode 100644
index 00000000..8757ce8e
--- /dev/null
+++ b/tests/ref/meta/query-before-after.png
Binary files differ
diff --git a/tests/ref/meta/query.png b/tests/ref/meta/query.png
index facf2b89..7801d22b 100644
--- a/tests/ref/meta/query.png
+++ b/tests/ref/meta/query.png
Binary files differ
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,