Skip to main content

typst_library/introspection/
position.rs

1use std::num::NonZeroUsize;
2
3use ecow::EcoVec;
4use typst_utils::NonZeroExt;
5
6use crate::foundations::{Dict, Value, cast, dict};
7use crate::layout::{Length, Point};
8
9/// Physical position in a document, be it paged or HTML.
10///
11/// This type exists to make it possible to write functions that are generic
12/// over the document target.
13#[derive(Clone, Debug, Hash)]
14pub enum DocumentPosition {
15    /// If the document is paged, the position is expressed as coordinates
16    /// inside of a page.
17    Paged(PagedPosition),
18    /// If the document is an HTML document, the position points to a specific
19    /// node in the DOM tree.
20    Html(HtmlPosition),
21}
22
23impl DocumentPosition {
24    /// Returns the [`PagedPosition`] if this is one.
25    pub fn as_paged(self) -> Option<PagedPosition> {
26        match self {
27            DocumentPosition::Paged(position) => Some(position),
28            _ => None,
29        }
30    }
31
32    /// Returns the [`PagedPosition`] or a position at page 1, point `(0, 0)` if
33    /// this is not a paged position.
34    pub fn as_paged_or_default(self) -> PagedPosition {
35        self.as_paged().unwrap_or(PagedPosition::ORIGIN)
36    }
37
38    /// Returns the [`HtmlPosition`] if available.
39    pub fn as_html(self) -> Option<HtmlPosition> {
40        match self {
41            DocumentPosition::Html(position) => Some(position),
42            _ => None,
43        }
44    }
45}
46
47impl From<PagedPosition> for DocumentPosition {
48    fn from(value: PagedPosition) -> Self {
49        Self::Paged(value)
50    }
51}
52
53impl From<HtmlPosition> for DocumentPosition {
54    fn from(value: HtmlPosition) -> Self {
55        Self::Html(value)
56    }
57}
58
59/// A physical position in a paged document.
60#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
61pub struct PagedPosition {
62    /// The page, starting at 1.
63    pub page: NonZeroUsize,
64    /// The exact coordinates on the page (from the top left, as usual).
65    pub point: Point,
66}
67
68impl PagedPosition {
69    /// A position at the origin of the first page.
70    pub const ORIGIN: PagedPosition =
71        PagedPosition { page: NonZeroUsize::ONE, point: Point::zero() };
72}
73
74cast! {
75    PagedPosition,
76    self => Value::Dict(self.into()),
77    mut dict: Dict => {
78        let page = dict.take("page")?.cast()?;
79        let x: Length = dict.take("x")?.cast()?;
80        let y: Length = dict.take("y")?.cast()?;
81        dict.finish(&["page", "x", "y"])?;
82        Self { page, point: Point::new(x.abs, y.abs) }
83    },
84}
85
86impl From<PagedPosition> for Dict {
87    fn from(pos: PagedPosition) -> Self {
88        dict! {
89            "page" => pos.page,
90            "x" => pos.point.x,
91            "y" => pos.point.y,
92        }
93    }
94}
95
96/// A position in an HTML tree.
97#[derive(Clone, Debug, Hash)]
98pub struct HtmlPosition {
99    /// Indices that can be used to traverse the tree from the root.
100    element: EcoVec<usize>,
101    /// The precise position inside of the specified element.
102    inner: Option<InnerHtmlPosition>,
103}
104
105impl HtmlPosition {
106    /// A position in an HTML document pointing to a specific node as a whole.
107    ///
108    /// The items of the vector corresponds to indices that can be used to
109    /// traverse the DOM tree from the root to reach the node. In practice, this
110    /// means that the first item of the vector will often be `1` for the
111    /// `<body>` tag (`0` being the `<head>` tag in a typical HTML document).
112    ///
113    /// Consecutive text nodes in Typst's HTML representation are grouped for
114    /// the purpose of this indexing as the segmentation is not observable in
115    /// the resulting DOM.
116    pub fn new(element: EcoVec<usize>) -> Self {
117        Self { element, inner: None }
118    }
119
120    /// Specifies a character offset inside of the node, to build a position
121    /// pointing to a specific point in text.
122    ///
123    /// This only makes sense if the node is a text node, not an element or a
124    /// frame.
125    ///
126    /// The offset is expressed in codepoints, not in bytes, to be
127    /// encoding-independent.
128    pub fn at_char(self, offset: usize) -> Self {
129        Self {
130            element: self.element,
131            inner: Some(InnerHtmlPosition::Character(offset)),
132        }
133    }
134
135    /// Specifies a point in a frame, to build a more precise position.
136    ///
137    /// This only makes sense if the node is a frame.
138    pub fn in_frame(self, point: Point) -> Self {
139        Self {
140            element: self.element,
141            inner: Some(InnerHtmlPosition::Frame(point)),
142        }
143    }
144
145    /// Extra-information for a more precise location inside of the node
146    /// designated by [`HtmlPosition::element`].
147    pub fn details(&self) -> Option<&InnerHtmlPosition> {
148        self.inner.as_ref()
149    }
150
151    /// Indices for traversing an HTML tree to reach the node corresponding to
152    /// this position.
153    ///
154    /// See [`HtmlPosition::new`] for more details.
155    pub fn element(&self) -> impl Iterator<Item = usize> {
156        self.element.iter().copied()
157    }
158}
159
160/// A precise position inside of an HTML node.
161#[derive(Clone, Debug, Hash)]
162pub enum InnerHtmlPosition {
163    /// If the node is a frame, the coordinates of the position.
164    Frame(Point),
165    /// If the node is a text node, the index of the codepoint at the position.
166    Character(usize),
167}