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