Skip to content

Commit 1198e0c

Browse files
authored
Selector rework (typst#640)
1 parent fe2640c commit 1198e0c

File tree

18 files changed

+452
-114
lines changed

18 files changed

+452
-114
lines changed

docs/src/reference/types.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,3 +910,73 @@ You can access definitions from the module using
910910
>>>
911911
>>> #(-3)
912912
```
913+
914+
# Selector
915+
A filter for selecting elements within the document.
916+
917+
You can construct a selector in the following ways:
918+
- you can use an element function
919+
- you can filter for an element function with
920+
[specific fields]($type/function.where)
921+
- you can use a [string]($type/string) or [regular expression]($func/regex)
922+
- you can use a [`{<label>}`]($func/label)
923+
- you can use a [`location`]($func/locate)
924+
- call the [`selector`]($func/selector) function to convert any of the above
925+
types into a selector value and use the methods below to refine it
926+
927+
A selector is what you can use to query the document for certain types
928+
of elements. It can also be used to apply styling rules to element. You can
929+
combine multiple selectors using the methods shown below.
930+
931+
Selectors can also be passed to several of Typst's built-in functions to
932+
configure their behaviour. One such example is the [outline]($func/outline)
933+
where it can be use to change which elements are listed within the outline.
934+
935+
## Example
936+
```example
937+
#locate(loc => query(
938+
heading.where(level: 1)
939+
.or(heading.where(level: 2)),
940+
loc,
941+
))
942+
943+
= This will be found
944+
== So will this
945+
=== But this will not.
946+
```
947+
948+
## Methods
949+
### or()
950+
Allows combining any of a series of selectors. This is used to
951+
select multiple components or components with different properties
952+
all at once.
953+
954+
- other: selector (variadic, required)
955+
The list of selectors to match on.
956+
957+
### and()
958+
Allows combining all of a series of selectors. This is used to check
959+
whether a component meets multiple selection rules simultaneously.
960+
961+
- other: selector (variadic, required)
962+
The list of selectors to match on.
963+
964+
### before()
965+
Returns a modified selector that will only match elements that occur before the
966+
first match of the selector argument.
967+
968+
- end: selector (positional, required)
969+
The original selection will end at the first match of `end`.
970+
- inclusive: boolean (named)
971+
Whether `end` itself should match or not. This is only relevant if both
972+
selectors match the same type of element. Defaults to `{true}`.
973+
974+
### after()
975+
Returns a modified selector that will only match elements that occur after the
976+
first match of the selector argument.
977+
978+
- start: selector (positional, required)
979+
The original selection will start at the first match of `start`.
980+
- inclusive: boolean (named)
981+
Whether `start` itself should match or not. This is only relevant if both
982+
selectors match the same type of element. Defaults to `{true}`.

library/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ fn global(math: Module, calc: Module) -> Module {
102102
global.define("numbering", meta::numbering);
103103
global.define("state", meta::state);
104104
global.define("query", meta::query);
105+
global.define("selector", meta::selector);
105106

106107
// Symbols.
107108
global.define("sym", symbols::sym());

library/src/meta/bibliography.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ cast_to_value! {
9191
impl BibliographyElem {
9292
/// Find the document's bibliography.
9393
pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> {
94-
let mut iter = introspector.query(Self::func().select()).into_iter();
94+
let mut iter = introspector.query(&Self::func().select()).into_iter();
9595
let Some(elem) = iter.next() else {
9696
return Err("the document does not contain a bibliography".into());
9797
};
@@ -106,7 +106,7 @@ impl BibliographyElem {
106106
/// Whether the bibliography contains the given key.
107107
pub fn has(vt: &Vt, key: &str) -> bool {
108108
vt.introspector
109-
.query(Self::func().select())
109+
.query(&Self::func().select())
110110
.into_iter()
111111
.flat_map(|elem| load(vt.world, &elem.to::<Self>().unwrap().path()))
112112
.flatten()
@@ -395,7 +395,7 @@ impl Works {
395395
let bibliography = BibliographyElem::find(vt.introspector)?;
396396
let citations = vt
397397
.introspector
398-
.query(Selector::Any(eco_vec![
398+
.query(&Selector::Or(eco_vec![
399399
RefElem::func().select(),
400400
CiteElem::func().select(),
401401
]))

library/src/meta/counter.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,10 @@ impl Counter {
335335
/// Get the value of the state at the given location.
336336
pub fn at(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> {
337337
let sequence = self.sequence(vt)?;
338-
let offset = vt.introspector.query_before(self.selector(), location).len();
338+
let offset = vt
339+
.introspector
340+
.query(&Selector::before(self.selector(), location, true))
341+
.len();
339342
let (mut state, page) = sequence[offset].clone();
340343
if self.is_page() {
341344
let delta = vt.introspector.page(location).get().saturating_sub(page.get());
@@ -359,7 +362,10 @@ impl Counter {
359362
/// Get the current and final value of the state combined in one state.
360363
pub fn both(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> {
361364
let sequence = self.sequence(vt)?;
362-
let offset = vt.introspector.query_before(self.selector(), location).len();
365+
let offset = vt
366+
.introspector
367+
.query(&Selector::before(self.selector(), location, true))
368+
.len();
363369
let (mut at_state, at_page) = sequence[offset].clone();
364370
let (mut final_state, final_page) = sequence.last().unwrap().clone();
365371
if self.is_page() {
@@ -412,11 +418,10 @@ impl Counter {
412418
let mut page = NonZeroUsize::ONE;
413419
let mut stops = eco_vec![(state.clone(), page)];
414420

415-
for elem in introspector.query(self.selector()) {
421+
for elem in introspector.query(&self.selector()) {
416422
if self.is_page() {
417-
let location = elem.location().unwrap();
418423
let prev = page;
419-
page = introspector.page(location);
424+
page = introspector.page(elem.location().unwrap());
420425

421426
let delta = page.get() - prev.get();
422427
if delta > 0 {
@@ -446,7 +451,7 @@ impl Counter {
446451
Selector::Elem(UpdateElem::func(), Some(dict! { "counter" => self.clone() }));
447452

448453
if let CounterKey::Selector(key) = &self.0 {
449-
selector = Selector::Any(eco_vec![selector, key.clone()]);
454+
selector = Selector::Or(eco_vec![selector, key.clone()]);
450455
}
451456

452457
selector

library/src/meta/figure.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,14 @@ impl Synthesize for FigureElem {
173173
// Determine the figure's kind.
174174
let kind = match self.kind(styles) {
175175
Smart::Auto => self
176-
.find_figurable(styles)
176+
.find_figurable(vt, styles)
177177
.map(|elem| FigureKind::Elem(elem.func()))
178178
.unwrap_or_else(|| FigureKind::Elem(ImageElem::func())),
179179
Smart::Custom(kind) => kind,
180180
};
181181

182182
let content = match &kind {
183-
FigureKind::Elem(func) => self.find_of_elem(*func),
183+
FigureKind::Elem(func) => self.find_of_elem(vt, *func),
184184
FigureKind::Name(_) => None,
185185
}
186186
.unwrap_or_else(|| self.body());
@@ -303,19 +303,19 @@ impl Refable for FigureElem {
303303
impl FigureElem {
304304
/// Determines the type of the figure by looking at the content, finding all
305305
/// [`Figurable`] elements and sorting them by priority then returning the highest.
306-
pub fn find_figurable(&self, styles: StyleChain) -> Option<Content> {
306+
pub fn find_figurable(&self, vt: &Vt, styles: StyleChain) -> Option<Content> {
307307
self.body()
308-
.query(Selector::can::<dyn Figurable>())
308+
.query(vt.introspector, Selector::can::<dyn Figurable>())
309309
.into_iter()
310310
.max_by_key(|elem| elem.with::<dyn Figurable>().unwrap().priority(styles))
311311
.cloned()
312312
}
313313

314314
/// Finds the element with the given function in the figure's content.
315315
/// Returns `None` if no element with the given function is found.
316-
pub fn find_of_elem(&self, func: ElemFunc) -> Option<Content> {
316+
pub fn find_of_elem(&self, vt: &Vt, func: ElemFunc) -> Option<Content> {
317317
self.body()
318-
.query(Selector::Elem(func, None))
318+
.query(vt.introspector, Selector::Elem(func, None))
319319
.into_iter()
320320
.next()
321321
.cloned()

library/src/meta/outline.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,13 @@ impl Show for OutlineElem {
154154
let lang = TextElem::lang_in(styles);
155155

156156
let mut ancestors: Vec<&Content> = vec![];
157-
let elems = vt.introspector.query(self.target(styles));
157+
let elems = vt.introspector.query(&self.target(styles));
158158

159159
for elem in &elems {
160160
let Some(refable) = elem.with::<dyn Refable>() else {
161161
bail!(elem.span(), "outlined elements must be referenceable");
162162
};
163163

164-
let location = elem.location().expect("missing location");
165164
if depth < refable.level() {
166165
continue;
167166
}
@@ -170,6 +169,8 @@ impl Show for OutlineElem {
170169
continue;
171170
};
172171

172+
let location = elem.location().unwrap();
173+
173174
// Deals with the ancestors of the current element.
174175
// This is only applicable for elements with a hierarchy/level.
175176
while ancestors

library/src/meta/query.rs

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ use crate::prelude::*;
3838
/// >>> )
3939
/// #set page(header: locate(loc => {
4040
/// let elems = query(
41-
/// heading,
42-
/// before: loc,
41+
/// selector(heading).before(loc),
42+
/// loc,
4343
/// )
4444
/// let academy = smallcaps[
4545
/// Typst Academy
@@ -102,8 +102,7 @@ pub fn query(
102102
/// elements with an explicit label. As a result, you _can_ query for e.g.
103103
/// [`strong`]($func/strong) elements, but you will find only those that
104104
/// have an explicit label attached to them. This limitation will be
105-
/// resolved
106-
/// in the future.
105+
/// resolved in the future.
107106
target: LocatableSelector,
108107

109108
/// Can be any location. Why is it required then? As noted before, Typst has
@@ -115,39 +114,25 @@ pub fn query(
115114
/// could depend on the query's result.
116115
///
117116
/// Only one of this, `before`, and `after` shall be given.
118-
#[external]
119-
#[default]
120117
location: Location,
118+
) -> Value {
119+
let _ = location;
120+
vm.vt.introspector.query(&target.0).into()
121+
}
121122

122-
/// If given, returns only those elements that are before the given
123-
/// location. A suitable location can be retrieved from
124-
/// [`locate`]($func/locate), but also through the
125-
/// [`location()`]($type/content.location) method on content returned by
126-
/// another query. Only one of `location`, this, and `after` shall be given.
127-
#[named]
128-
#[external]
129-
#[default]
130-
before: Location,
131-
132-
/// If given, returns only those elements that are after the given location.
133-
/// A suitable location can be retrieved from [`locate`]($func/locate), but
134-
/// also through the [`location()`]($type/content.location) method on
135-
/// content returned by another query. Only one of `location`, `before`, and
136-
/// this shall be given.
137-
#[named]
138-
#[external]
139-
#[default]
140-
after: Location,
123+
/// Turns a value into a selector. The following values are accepted:
124+
/// - An element function like a `heading` or `figure`.
125+
/// - A `{<label>}`.
126+
/// - A more complex selector like `{heading.where(level: 1)}`.
127+
///
128+
/// Display: Selector
129+
/// Category: meta
130+
/// Returns: content
131+
#[func]
132+
pub fn selector(
133+
/// Can be an element function like a `heading` or `figure`, a `{<label>}`
134+
/// or a more complex selector like `{heading.where(level: 1)}`.
135+
target: Selector,
141136
) -> Value {
142-
let selector = target.0;
143-
let introspector = vm.vt.introspector;
144-
let elements = if let Some(location) = args.named("before")? {
145-
introspector.query_before(selector, location)
146-
} else if let Some(location) = args.named("after")? {
147-
introspector.query_after(selector, location)
148-
} else {
149-
let _: Location = args.expect("location")?;
150-
introspector.query(selector)
151-
};
152-
elements.into()
137+
target.into()
153138
}

library/src/meta/state.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,10 @@ impl State {
282282
/// Get the value of the state at the given location.
283283
pub fn at(self, vt: &mut Vt, location: Location) -> SourceResult<Value> {
284284
let sequence = self.sequence(vt)?;
285-
let offset = vt.introspector.query_before(self.selector(), location).len();
285+
let offset = vt
286+
.introspector
287+
.query(&Selector::before(self.selector(), location, true))
288+
.len();
286289
Ok(sequence[offset].clone())
287290
}
288291

@@ -323,7 +326,7 @@ impl State {
323326
let mut state = self.init.clone();
324327
let mut stops = eco_vec![state.clone()];
325328

326-
for elem in introspector.query(self.selector()) {
329+
for elem in introspector.query(&self.selector()) {
327330
let elem = elem.to::<UpdateElem>().unwrap();
328331
match elem.update() {
329332
StateUpdate::Set(value) => state = value,

src/eval/methods.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use ecow::EcoString;
44

55
use super::{Args, Str, Value, Vm};
66
use crate::diag::{At, SourceResult};
7-
use crate::model::Location;
7+
use crate::model::{Location, Selector};
88
use crate::syntax::Span;
99

1010
/// Call a method on a value.
@@ -151,11 +151,29 @@ pub fn call(
151151
},
152152

153153
Value::Dyn(dynamic) => {
154-
if let Some(&location) = dynamic.downcast::<Location>() {
154+
if let Some(location) = dynamic.downcast::<Location>() {
155155
match method {
156-
"page" => vm.vt.introspector.page(location).into(),
157-
"position" => vm.vt.introspector.position(location).into(),
158-
"page-numbering" => vm.vt.introspector.page_numbering(location),
156+
"page" => vm.vt.introspector.page(*location).into(),
157+
"position" => vm.vt.introspector.position(*location).into(),
158+
"page-numbering" => vm.vt.introspector.page_numbering(*location),
159+
_ => return missing(),
160+
}
161+
} else if let Some(selector) = dynamic.downcast::<Selector>() {
162+
match method {
163+
"or" => selector.clone().or(args.all::<Selector>()?).into(),
164+
"and" => selector.clone().and(args.all::<Selector>()?).into(),
165+
"before" => {
166+
let location = args.expect::<Selector>("selector")?;
167+
let inclusive =
168+
args.named_or_find::<bool>("inclusive")?.unwrap_or(true);
169+
selector.clone().before(location, inclusive).into()
170+
}
171+
"after" => {
172+
let location = args.expect::<Selector>("selector")?;
173+
let inclusive =
174+
args.named_or_find::<bool>("inclusive")?.unwrap_or(true);
175+
selector.clone().after(location, inclusive).into()
176+
}
159177
_ => return missing(),
160178
}
161179
} else {
@@ -312,6 +330,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
312330
"function" => &[("where", true), ("with", true)],
313331
"arguments" => &[("named", false), ("pos", false)],
314332
"location" => &[("page", false), ("position", false), ("page-numbering", false)],
333+
"selector" => &[("or", true), ("and", true), ("before", true), ("after", true)],
315334
"counter" => &[
316335
("display", true),
317336
("at", true),

0 commit comments

Comments
 (0)