Selector rework (#640)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-04-12 12:47:51 +02:00 committed by GitHub
parent fe2640c552
commit 1198e0cd38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 454 additions and 116 deletions

View File

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

View File

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

View File

@ -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(),
]))

View File

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

View File

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

View File

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

View File

@ -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,
/// 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,
) -> 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()
let _ = location;
vm.vt.introspector.query(&target.0).into()
}
/// 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 {
target.into()
}

View File

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

View File

@ -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),

View File

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

View File

@ -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
}
/// 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 elements.
pub fn query(&self, selector: Selector) -> Vec<Content> {
self.all().filter(|elem| selector.matches(elem)).cloned().collect()
}
/// 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;
}
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)
}
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

After

Width:  |  Height:  |  Size: 542 KiB

View File

@ -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: []
---

View File

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

View File

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