Selector rework (#640)
This commit is contained in:
parent
fe2640c552
commit
1198e0cd38
@ -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}`.
|
||||
|
@ -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());
|
||||
|
@ -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(),
|
||||
]))
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
BIN
tests/ref/meta/query-before-after.png
Normal file
BIN
tests/ref/meta/query-before-after.png
Normal file
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 |
@ -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: []
|
||||
|
||||
---
|
||||
|
69
tests/typ/meta/query-before-after.typ
Normal file
69
tests/typ/meta/query-before-after.typ
Normal 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)
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user