Skip to main content

typst_library/introspection/
query.rs

1use comemo::Tracked;
2use ecow::{EcoString, EcoVec, eco_format};
3use typst_syntax::Span;
4
5use super::{History, Introspect};
6use crate::diag::{HintedStrResult, SourceDiagnostic, StrResult, warning};
7use crate::engine::Engine;
8use crate::foundations::{
9    Array, Content, Context, Label, LocatableSelector, Repr, Selector, Value, func,
10};
11use crate::introspection::Introspector;
12
13/// Finds elements in the document.
14///
15/// The `query` function lets you search your document for elements of a
16/// particular type or with a particular label. To use it, you first need to
17/// ensure that @reference:context[context] is available.
18///
19/// = Finding elements <finding-elements>
20/// In the example below, we manually create a table of contents instead of
21/// using the @outline function.
22///
23/// To do this, we first query for all headings in the document at level 1 and
24/// where `outlined` is true. Querying only for headings at level 1 ensures
25/// that, for the purpose of this example, sub-headings are not included in the
26/// table of contents. The `outlined` field is used to exclude the "Table of
27/// Contents" heading itself.
28///
29/// Note that we open a `context` to be able to use the `query` function.
30///
31/// ```example
32/// >>> #set page(
33/// >>>  width: 240pt,
34/// >>>  height: 180pt,
35/// >>>  margin: (top: 20pt, bottom: 35pt)
36/// >>> )
37/// #set page(numbering: "1")
38///
39/// #heading(outlined: false)[
40///   Table of Contents
41/// ]
42/// #context {
43///   let chapters = query(
44///     heading.where(
45///       level: 1,
46///       outlined: true,
47///     )
48///   )
49///   for chapter in chapters {
50///     let loc = chapter.location()
51///     let nr = counter(page).display(at: loc)
52///     [#chapter.body #h(1fr) #nr \ ]
53///   }
54/// }
55///
56/// = Introduction
57/// #lorem(10)
58/// #pagebreak()
59///
60/// == Sub-Heading
61/// #lorem(8)
62///
63/// = Discussion
64/// #lorem(18)
65/// ```
66///
67/// To get the page numbers, we first get the location of the elements returned
68/// by `query` with @content.location[`location`]. We then also retrieve the
69/// @location.page-numbering[page numbering] and
70/// @counter:page-counter[page counter] at that location and apply the numbering
71/// to the counter.
72///
73/// = #short-or-long[Caution][A word of caution] <caution>
74/// To resolve all your queries, Typst evaluates and layouts parts of the
75/// document multiple times. However, there is no guarantee that your queries
76/// can actually be completely resolved. If you aren't careful a query can
77/// affect itself—leading to a result that never stabilizes.
78///
79/// In the example below, we query for all headings in the document. We then
80/// generate as many headings. In the beginning, there's just one heading,
81/// titled `Real`. Thus, `count` is `1` and one `Fake` heading is generated.
82/// Typst sees that the query's result has changed and processes it again. This
83/// time, `count` is `2` and two `Fake` headings are generated. This goes on and
84/// on. As we can see, the output has a finite amount of headings. This is
85/// because Typst simply gives up after a few attempts.
86///
87/// In general, you should try not to write queries that affect themselves. The
88/// same words of caution also apply to other introspection features like
89/// @counter[counters] and @state[state].
90///
91/// #example(
92///   ```
93///   = Real
94///   #context {
95///     let elems = query(heading)
96///     let count = elems.len()
97///     count * [= Fake]
98///   }
99///   ```,
100///   warnings: false,
101/// )
102///
103/// = Command line queries <command-line-queries>
104/// You can also perform queries from the command line, using the `typst eval`
105/// command. This command evaluates Typst code, potentially in the context of a
106/// document, and outputs the resulting value in serialized form. It takes the
107/// code to evaluate as its first argument and (optionally) the path to a
108/// document via `--in`.
109///
110/// Consider the following `example.typ` file which contains some invisible
111/// @metadata[metadata]:
112///
113/// ```typ
114/// #metadata("This is a note") <note>
115/// ```
116///
117/// You can execute a query on it as follows using Typst's CLI.
118///
119/// ```sh
120/// $ typst eval 'query(<note>)' --in example.typ
121/// [
122///   {
123///     "func": "metadata",
124///     "value": "This is a note",
125///     "label": "<note>"
126///   }
127/// ]
128/// ```
129///
130/// This command tells Typst to compile `example.typ` and then run the code
131/// `{query(<note>)}` with access to the resulting document.
132///
133/// *Note:* The code is surrounded with quotes to avoid special characters being
134/// interpreted by the shell. How to quote strings depends on your
135/// platform/shell.
136///
137/// == Retrieving a specific field <retrieving-a-specific-field>
138/// Frequently, you're interested in only one specific field of the resulting
139/// elements. In the case of the `metadata` element, the `value` field is the
140/// interesting one. You can extract just this field by adjusting the code.
141///
142/// ```sh
143/// $ typst eval 'query(<note>).map(it => it.value)' --in example.typ
144/// ["This is a note"]
145/// ```
146///
147/// If you are interested in just a single element, you can also use the
148/// @array.first[`first()`] method to extract just it.
149///
150/// ```sh
151/// $ typst eval 'query(<note>).first().value' --in example.typ
152/// "This is a note"
153/// ```
154///
155/// == Querying for a specific export target <querying-for-a-specific-export-target>
156/// In case you need to query a document when exporting for a specific target,
157/// you can use the `--target` argument. Valid values are `paged`, and `html`
158/// (if the @html feature is enabled).
159#[func(contextual)]
160pub fn query(
161    engine: &mut Engine,
162    context: Tracked<Context>,
163    span: Span,
164    /// Can be
165    /// - an element function like a `heading` or `figure`,
166    /// - a `{<label>}`,
167    /// - a more complex selector like `{heading.where(level: 1)}`,
168    /// - or `{selector(heading).before(here())}`.
169    ///
170    /// Only @location:locatable[locatable] element functions are supported.
171    target: LocatableSelector,
172) -> HintedStrResult<Array> {
173    context.introspect()?;
174    let vec = engine.introspect(QueryIntrospection(target.0, span));
175    Ok(vec.into_iter().map(Value::Content).collect())
176}
177
178/// Retrieves all matches of a selector in the document.
179#[derive(Debug, Clone, PartialEq, Hash)]
180pub struct QueryIntrospection(pub Selector, pub Span);
181
182impl Introspect for QueryIntrospection {
183    type Output = EcoVec<Content>;
184
185    fn introspect(
186        &self,
187        _: &mut Engine,
188        introspector: Tracked<dyn Introspector + '_>,
189    ) -> Self::Output {
190        introspector.query(&self.0)
191    }
192
193    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
194        let lengths = history.as_ref().map(|vec| vec.len());
195        let things = format_selector(&self.0, "elements");
196        let what = if !lengths.converged() {
197            eco_format!("number of {things}")
198        } else {
199            eco_format!("query for {things}")
200        };
201        format_convergence_warning(self.1, &lengths, &what)
202    }
203}
204
205/// Retrieves the first match of a selector in the document.
206#[derive(Debug, Clone, PartialEq, Hash)]
207pub struct QueryFirstIntrospection(pub Selector, pub Span);
208
209impl Introspect for QueryFirstIntrospection {
210    type Output = Option<Content>;
211
212    fn introspect(
213        &self,
214        _: &mut Engine,
215        introspector: Tracked<dyn Introspector + '_>,
216    ) -> Self::Output {
217        introspector.query_first(&self.0)
218    }
219
220    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
221        let lengths = history.as_ref().map(|vec| vec.is_some() as usize);
222        let thing = format_selector(&self.0, "element");
223        let what = eco_format!("query for the first {thing}");
224        format_convergence_warning(self.1, &lengths, &what)
225    }
226}
227
228/// Retrieves the only match of a selector in the document.
229///
230/// Fails if there are multiple occurrences.
231#[derive(Debug, Clone, PartialEq, Hash)]
232pub struct QueryUniqueIntrospection(pub Selector, pub Span);
233
234impl Introspect for QueryUniqueIntrospection {
235    type Output = StrResult<Content>;
236
237    fn introspect(
238        &self,
239        _: &mut Engine,
240        introspector: Tracked<dyn Introspector + '_>,
241    ) -> Self::Output {
242        introspector.query_unique(&self.0)
243    }
244
245    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
246        let lengths = history.as_ref().map(|vec| vec.is_ok() as usize);
247        let thing = format_selector(&self.0, "element");
248        let what = eco_format!("query for a unique {thing}");
249        format_convergence_warning(self.1, &lengths, &what)
250    }
251}
252
253/// Retrieves the only occurrence of a label in the document.
254///
255/// Fails if there are multiple occurrences.
256#[derive(Debug, Clone, PartialEq, Hash)]
257pub struct QueryLabelIntrospection(pub Label, pub Span);
258
259impl Introspect for QueryLabelIntrospection {
260    type Output = StrResult<Content>;
261
262    fn introspect(
263        &self,
264        _: &mut Engine,
265        introspector: Tracked<dyn Introspector + '_>,
266    ) -> Self::Output {
267        introspector.query_label(self.0).cloned()
268    }
269
270    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
271        QueryUniqueIntrospection(Selector::Label(self.0), self.1).diagnose(history)
272    }
273}
274
275/// The warning when an introspection on a [`Location`](super::Location) did not
276/// converge.
277fn format_convergence_warning(
278    span: Span,
279    lengths: &History<usize>,
280    what: &str,
281) -> SourceDiagnostic {
282    let mut diag = warning!(span, "{what} did not stabilize");
283    if !lengths.converged() {
284        diag.hint(lengths.hint("numbers of elements", |c| eco_format!("{c}")));
285    }
286    diag
287}
288
289/// Formats a selector human-readably.
290fn format_selector(selector: &Selector, kind: &str) -> EcoString {
291    match selector {
292        Selector::Elem(elem, None) => eco_format!("{} {kind}", elem.name()),
293        Selector::Elem(elem, _) => eco_format!("matching {} {kind}", elem.name()),
294        Selector::Label(label) => eco_format!("{kind} labelled `{}`", label.repr()),
295        other => eco_format!("{kind} matching `{}`", other.repr()),
296    }
297}