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}