summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
5 files changed, 259 insertions, 55 deletions
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(())
}