diff --git a/library/src/lib.rs b/library/src/lib.rs index 83dbe17aa..0850faf4a 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -95,6 +95,7 @@ fn global(math: Module, calc: Module) -> Module { global.define("counter", meta::counter); global.define("numbering", meta::numbering); global.define("state", meta::state); + global.define("query", meta::query); // Symbols. global.define("sym", symbols::sym()); diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs index 1d7740580..50b8627e2 100644 --- a/library/src/meta/mod.rs +++ b/library/src/meta/mod.rs @@ -8,6 +8,7 @@ mod heading; mod link; mod numbering; mod outline; +mod query; mod reference; mod state; @@ -19,6 +20,7 @@ pub use self::heading::*; pub use self::link::*; pub use self::numbering::*; pub use self::outline::*; +pub use self::query::*; pub use self::reference::*; pub use self::state::*; diff --git a/library/src/meta/query.rs b/library/src/meta/query.rs new file mode 100644 index 000000000..c91f0d1ae --- /dev/null +++ b/library/src/meta/query.rs @@ -0,0 +1,69 @@ +use crate::prelude::*; + +/// Find elements in the document. +/// +/// Display: Query +/// Category: meta +/// Returns: content +#[func] +pub fn query( + /// The thing to search for. + target: Target, + /// A function to format the results with. + format: Func, +) -> Value { + QueryNode::new(target.0, format).pack().into() +} + +/// A query target. +struct Target(Selector); + +cast_from_value! { + Target, + label: Label => Self(Selector::Label(label)), + func: Func => { + let Some(id) = func.id() else { + return Err("this function is not selectable".into()); + }; + + if !Content::new(id).can::() { + Err(eco_format!("cannot query for {}s", id.name))?; + } + + Self(Selector::Node(id, None)) + } +} + +/// Executes a query. +/// +/// Display: Query +/// Category: special +#[node(Locatable, Show)] +pub struct QueryNode { + /// The thing to search for. + #[required] + pub target: Selector, + + /// The function to format the results with. + #[required] + pub format: Func, +} + +impl Show for QueryNode { + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { + if !vt.introspector.init() { + return Ok(Content::empty()); + } + + let id = self.0.stable_id().unwrap(); + let target = self.target(); + let (before, after) = vt.introspector.query_split(target, id); + let func = self.format(); + let args = Args::new(func.span(), [encode(before), encode(after)]); + Ok(func.call_detached(vt.world, args)?.display()) + } +} + +fn encode(list: Vec<&Content>) -> Value { + Value::Array(list.into_iter().cloned().map(Value::Content).collect()) +} diff --git a/src/model/typeset.rs b/src/model/typeset.rs index 4c8be1356..e9cb3d2c6 100644 --- a/src/model/typeset.rs +++ b/src/model/typeset.rs @@ -166,6 +166,22 @@ impl Introspector { self.all().filter(|node| selector.matches(node)).collect() } + /// Query for all metadata matches before the given id. + pub fn query_split( + &self, + selector: Selector, + id: StableId, + ) -> (Vec<&Content>, Vec<&Content>) { + let mut iter = self.all(); + let before = iter + .by_ref() + .take_while(|node| node.stable_id() != Some(id)) + .filter(|node| selector.matches(node)) + .collect(); + let after = iter.filter(|node| selector.matches(node)).collect(); + (before, after) + } + /// Find the page number for the given stable id. pub fn page(&self, id: StableId) -> NonZeroUsize { self.location(id).page diff --git a/tests/ref/meta/query.png b/tests/ref/meta/query.png new file mode 100644 index 000000000..809812020 Binary files /dev/null and b/tests/ref/meta/query.png differ diff --git a/tests/typ/meta/query.typ b/tests/typ/meta/query.typ new file mode 100644 index 000000000..85608e8e8 --- /dev/null +++ b/tests/typ/meta/query.typ @@ -0,0 +1,30 @@ +// Test the query function. + +--- +#set page( + paper: "a7", + margin: (y: 1cm, x: 0.5cm), + header: { + smallcaps[Typst Academy] + h(1fr) + query(heading, (before, after) => { + let elem = if before.len() != 0 { + before.last() + } else if after.len() != 0 { + after.first() + } + emph(elem.body) + }) + } +) + +#outline() + += Introduction +#lorem(35) + += Background +#lorem(35) + += Approach +#lorem(60)