Skip to main content

typst_library/introspection/
location.rs

1use std::fmt::{self, Debug, Formatter};
2use std::num::NonZeroUsize;
3
4use comemo::Tracked;
5use ecow::{EcoString, eco_format};
6use typst_syntax::{Span, VirtualPath};
7use typst_utils::NonZeroExt;
8
9use crate::diag::{SourceDiagnostic, warning};
10use crate::engine::Engine;
11use crate::foundations::{Content, IntoValue, Repr, Selector, func, repr, scope, ty};
12use crate::introspection::{
13    DocumentPosition, History, Introspect, Introspector, PagedPosition,
14};
15use crate::layout::Abs;
16use crate::model::Numbering;
17
18/// Makes an element available in the introspector.
19pub trait Locatable {}
20
21/// Marks an element as not queriable for the user.
22pub trait Unqueriable: Locatable {}
23
24/// Marks an element as tagged in PDF files.
25pub trait Tagged {}
26
27/// Identifies an element in the document.
28///
29/// A location uniquely identifies an element in the document and lets you
30/// access its absolute position on the pages. You can retrieve the current
31/// location with the @here function and the location of a queried or shown
32/// element with the @content.location[`location()`] method on content.
33///
34/// = #short-or-long[Locatable][Locatable elements] <locatable>
35/// Elements that are automatically assigned a location are called _locatable._
36/// For efficiency reasons, not all elements are locatable.
37///
38/// - In the @reference:model[Model category], most elements are locatable. This
39///   is because semantic elements like @heading[headings] and @figure[figures]
40///   are often used with introspection.
41///
42/// - In the @reference:text[Text category], the @raw element, and the
43///   decoration elements @underline, @overline, @strike, and @highlight are
44///   locatable as these are also quite semantic in nature.
45///
46/// - In the @reference:introspection[Introspection category], the @metadata
47///   element is locatable as being queried for is its primary purpose.
48///
49/// - In the other categories, most elements are not locatable. Exceptions are
50///   @math.equation and @image.
51///
52/// To find out whether a specific element is locatable, you can try to @query
53/// for it.
54///
55/// Note that you can still observe elements that are not locatable in queries
56/// through other means, for instance, when they have a label attached to them.
57#[ty(scope)]
58#[derive(Copy, Clone, Eq, PartialEq, Hash)]
59pub struct Location(u128);
60
61impl Location {
62    /// Create a new location from a unique hash.
63    pub fn new(hash: u128) -> Self {
64        Self(hash)
65    }
66
67    /// Extract the raw hash.
68    pub fn hash(self) -> u128 {
69        self.0
70    }
71
72    /// Produces a well-known variant of this location.
73    ///
74    /// This is a synthetic location created from another one and is used, for
75    /// example, in bibliography management to create individual linkable
76    /// locations for reference entries from the bibliography's location.
77    pub fn variant(self, n: usize) -> Self {
78        Self(typst_utils::hash128(&(self.0, n)))
79    }
80}
81
82#[scope]
83impl Location {
84    /// Returns the page number for this location.
85    ///
86    /// Note that this does not return the value of the @counter[page counter]
87    /// at this location, but the true page number (starting from one).
88    ///
89    /// If you want to know the value of the page counter, use
90    /// `{counter(page).at(loc)}` instead.
91    ///
92    /// Can be used with @here to retrieve the physical page position of the
93    /// current context:
94    ///
95    /// ```example
96    /// #context [
97    ///   I am located on
98    ///   page #here().page()
99    /// ]
100    /// ```
101    #[func]
102    pub fn page(self, engine: &mut Engine, span: Span) -> NonZeroUsize {
103        engine.introspect(PageIntrospection(self, span))
104    }
105
106    /// Returns a dictionary with the page number and the x, y position for this
107    /// location. The page number starts at one and the coordinates are measured
108    /// from the top-left of the page.
109    ///
110    /// If you only need the page number, use `page()` instead as it allows
111    /// Typst to skip unnecessary work.
112    #[func]
113    pub fn position(self, engine: &mut Engine, span: Span) -> PagedPosition {
114        engine.introspect(PositionIntrospection(self, span))
115    }
116
117    /// Returns the page numbering pattern of the page at this location. This
118    /// can be used when displaying the page counter in order to obtain the
119    /// local numbering. This is useful if you are building custom indices or
120    /// outlines.
121    ///
122    /// If the page numbering is set to `{none}` at that location, this function
123    /// returns `{none}`.
124    #[func]
125    pub fn page_numbering(self, engine: &mut Engine, span: Span) -> Option<Numbering> {
126        engine.introspect(PageNumberingIntrospection(self, span))
127    }
128}
129
130impl Debug for Location {
131    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
132        if f.alternate() {
133            write!(f, "Location({})", self.0)
134        } else {
135            // Print a shorter version by default to make it more readable.
136            let truncated = self.0 as u16;
137            write!(f, "Location({truncated})")
138        }
139    }
140}
141
142impl Repr for Location {
143    fn repr(&self) -> EcoString {
144        "location(..)".into()
145    }
146}
147
148/// Can be used to have a location as a key in an ordered set or map.
149///
150/// [`Location`] itself does not implement [`Ord`] because comparing hashes like
151/// this has no semantic meaning. The potential for misuse (e.g. checking
152/// whether locations have a particular relative ordering) is relatively high.
153///
154/// Still, it can be useful to have orderable locations for things like sets.
155/// That's where this type comes in.
156#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
157pub struct LocationKey(u128);
158
159impl LocationKey {
160    /// Create a location key from a location.
161    pub fn new(location: Location) -> Self {
162        Self(location.0)
163    }
164}
165
166impl From<Location> for LocationKey {
167    fn from(location: Location) -> Self {
168        Self::new(location)
169    }
170}
171
172/// Retrieves the exact position of an element in the document.
173#[derive(Debug, Clone, PartialEq, Hash)]
174pub struct PositionIntrospection(pub Location, pub Span);
175
176impl Introspect for PositionIntrospection {
177    type Output = PagedPosition;
178
179    fn introspect(
180        &self,
181        _: &mut Engine,
182        introspector: Tracked<dyn Introspector + '_>,
183    ) -> Self::Output {
184        match introspector.position(self.0) {
185            Some(DocumentPosition::Paged(pos)) => pos,
186            // Maybe error here instead?
187            Some(DocumentPosition::Html(_)) | None => PagedPosition::ORIGIN,
188        }
189    }
190
191    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
192        format_convergence_warning(
193            self.0,
194            self.1,
195            history,
196            "positions",
197            |element| eco_format!("{element} position"),
198            |pos| {
199                let coord = |v: Abs| repr::format_float(v.to_pt(), Some(0), false, "pt");
200                eco_format!(
201                    "page {} at ({}, {})",
202                    pos.page,
203                    coord(pos.point.x),
204                    coord(pos.point.y)
205                )
206            },
207        )
208    }
209}
210
211/// Retrieves the number of the page where an element is located.
212#[derive(Debug, Clone, PartialEq, Hash)]
213pub struct PageIntrospection(pub Location, pub Span);
214
215impl Introspect for PageIntrospection {
216    type Output = NonZeroUsize;
217
218    fn introspect(
219        &self,
220        _: &mut Engine,
221        introspector: Tracked<dyn Introspector + '_>,
222    ) -> Self::Output {
223        // Maybe error here instead of calling `unwrap_or`?
224        introspector.page(self.0).unwrap_or(NonZeroUsize::ONE)
225    }
226
227    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
228        format_convergence_warning(
229            self.0,
230            self.1,
231            history,
232            "page numbers",
233            |element| eco_format!("page number of the {element}"),
234            |n| eco_format!("page {n}"),
235        )
236    }
237}
238
239/// Retrieves the numbering of the page where an element is located.
240#[derive(Debug, Clone, PartialEq, Hash)]
241pub struct PageNumberingIntrospection(pub Location, pub Span);
242
243impl Introspect for PageNumberingIntrospection {
244    type Output = Option<Numbering>;
245
246    fn introspect(
247        &self,
248        _: &mut Engine,
249        introspector: Tracked<dyn Introspector + '_>,
250    ) -> Self::Output {
251        introspector.page_numbering(self.0).cloned()
252    }
253
254    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
255        format_convergence_warning(
256            self.0,
257            self.1,
258            history,
259            "numberings",
260            |element| {
261                eco_format!("numbering of the page on which the {element} is located")
262            },
263            |numbering| eco_format!("`{}`", numbering.clone().into_value().repr()),
264        )
265    }
266}
267
268/// Retrieves the supplement of the page where an element is located.
269#[derive(Debug, Clone, PartialEq, Hash)]
270pub struct PageSupplementIntrospection(pub Location, pub Span);
271
272impl Introspect for PageSupplementIntrospection {
273    type Output = Content;
274
275    fn introspect(
276        &self,
277        _: &mut Engine,
278        introspector: Tracked<dyn Introspector + '_>,
279    ) -> Self::Output {
280        // Maybe returns `None` here instead of empty content if no supplement
281        // was specified?
282        introspector.page_supplement(self.0).cloned().unwrap_or_default()
283    }
284
285    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
286        format_convergence_warning(
287            self.0,
288            self.1,
289            history,
290            "supplements",
291            |element| {
292                eco_format!("supplement of the page on which the {element} is located")
293            },
294            |supplement| eco_format!("`{}`", supplement.repr()),
295        )
296    }
297}
298
299/// Retrieves the file path of the document/asset which has or contains the
300/// given location.
301#[derive(Debug, Clone, PartialEq, Hash)]
302pub struct PathIntrospection(pub Location, pub Span);
303
304impl Introspect for PathIntrospection {
305    type Output = Option<VirtualPath>;
306
307    fn introspect(
308        &self,
309        _: &mut Engine,
310        introspector: Tracked<dyn Introspector + '_>,
311    ) -> Self::Output {
312        introspector.path(self.0).cloned()
313    }
314
315    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
316        format_convergence_warning(
317            self.0,
318            self.1,
319            history,
320            "path",
321            |element| {
322                eco_format!("path of the document in which the {element} is located")
323            },
324            |path| {
325                eco_format!(
326                    "`{}`",
327                    path.as_ref().map(|p| p.get_with_slash()).into_value().repr()
328                )
329            },
330        )
331    }
332}
333
334/// Retrieves the location of the document in which an element is located.
335#[derive(Debug, Clone, PartialEq, Hash)]
336pub struct DocumentIntrospection(pub Location, pub Span);
337
338impl Introspect for DocumentIntrospection {
339    type Output = Option<Location>;
340
341    fn introspect(
342        &self,
343        _: &mut Engine,
344        introspector: Tracked<dyn Introspector + '_>,
345    ) -> Self::Output {
346        introspector.document(self.0)
347    }
348
349    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
350        format_convergence_warning(
351            self.0,
352            self.1,
353            history,
354            "path",
355            |element| eco_format!("document in which the {element} is located"),
356            |_loc| eco_format!("TODO"),
357        )
358    }
359}
360
361/// The warning when an introspection on a [`Location`] did not converge.
362fn format_convergence_warning<T>(
363    loc: Location,
364    span: Span,
365    history: &History<T>,
366    output_kind_plural: &str,
367    format_output_kind: impl FnOnce(&str) -> EcoString,
368    format_output: impl FnMut(&T) -> EcoString,
369) -> SourceDiagnostic {
370    let elem = history.final_introspector().query_first(&Selector::Location(loc));
371    let kind = match &elem {
372        Some(content) => content.elem().name(),
373        None => "element",
374    };
375
376    let what = format_output_kind(kind);
377    let mut diag = warning!(span, "{what} did not stabilize");
378
379    if let Some(elem) = elem
380        && !elem.span().is_detached()
381    {
382        diag.spanned_hint(eco_format!("{kind} was created here"), elem.span());
383    }
384
385    diag.with_hint(history.hint(output_kind_plural, format_output))
386}