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}