Skip to main content

typst_layout/
introspect.rs

1use std::fmt::{self, Debug, Formatter};
2use std::num::NonZeroUsize;
3
4use ecow::{EcoString, EcoVec};
5
6use rustc_hash::FxHashSet;
7use typst_library::diag::StrResult;
8use typst_library::foundations::{Content, Label, Selector};
9use typst_library::introspection::{
10    DocumentPosition, ElementIntrospector, ElementIntrospectorBuilder, Introspector,
11    Location, PagedPosition,
12};
13use typst_library::layout::{Frame, FrameItem, Point, Transform};
14use typst_library::model::{Destination, Numbering};
15use typst_syntax::VirtualPath;
16use typst_utils::NonZeroExt;
17
18use crate::Page;
19
20/// An introspector implementation for paged documents.
21#[derive(Clone)]
22pub struct PagedIntrospector {
23    /// The underlying target-agnostic introspector used for most queries.
24    elements: ElementIntrospector<PagedPosition>,
25    /// Locations that are linked to via `FrameItem::Link`.
26    frame_link_targets: FxHashSet<Location>,
27    /// The number of pages in the document.
28    pages: NonZeroUsize,
29    /// The page numberings, indexed by page number minus 1.
30    page_numberings: Vec<Option<Numbering>>,
31    /// The page supplements, indexed by page number minus 1.
32    page_supplements: Vec<Content>,
33}
34
35impl PagedIntrospector {
36    /// Creates an introspector for a paged document.
37    #[typst_macros::time(name = "introspect pages")]
38    pub fn new(pages: &[Page]) -> PagedIntrospector {
39        let mut builder = PagedIntrospectorBuilder::default();
40        let mut page_numberings = Vec::with_capacity(pages.len());
41        let mut page_supplements = Vec::with_capacity(pages.len());
42
43        // Discover all elements.
44        for (i, page) in pages.iter().enumerate() {
45            let nr = NonZeroUsize::new(1 + i).unwrap();
46            page_numberings.push(page.numbering.clone());
47            page_supplements.push(page.supplement.clone());
48            builder.discover_frame(&page.frame, Transform::identity(), &mut |point| {
49                PagedPosition { page: nr, point }
50            });
51        }
52
53        builder.finish(
54            NonZeroUsize::new(pages.len()).unwrap_or(NonZeroUsize::ONE),
55            page_numberings,
56            page_supplements,
57        )
58    }
59
60    /// Resolves the position of the location in the pages.
61    pub fn position(&self, location: Location) -> Option<PagedPosition> {
62        self.elements.position(location).copied()
63    }
64
65    /// The underlying element introspector.
66    pub fn elements(&self) -> &ElementIntrospector<PagedPosition> {
67        &self.elements
68    }
69
70    /// Returns the locations that the paged document links to via
71    /// `FrameItem::Link`.
72    pub fn frame_link_targets(&self) -> &FxHashSet<Location> {
73        &self.frame_link_targets
74    }
75}
76
77impl Introspector for PagedIntrospector {
78    fn query(&self, selector: &Selector) -> EcoVec<Content> {
79        self.elements.query(selector)
80    }
81
82    fn query_first(&self, selector: &Selector) -> Option<Content> {
83        self.elements.query_first(selector)
84    }
85
86    fn query_unique(&self, selector: &Selector) -> StrResult<Content> {
87        self.elements.query_unique(selector)
88    }
89
90    fn query_label(&self, label: Label) -> StrResult<&Content> {
91        self.elements.query_label(label)
92    }
93
94    fn query_labelled(&self) -> EcoVec<Content> {
95        self.elements.query_labelled()
96    }
97
98    fn query_count_before(&self, selector: &Selector, end: Location) -> usize {
99        self.elements.query_count_before(selector, end)
100    }
101
102    fn label_count(&self, label: Label) -> usize {
103        self.elements.label_count(label)
104    }
105
106    fn locator(&self, key: u128, base: Location) -> Option<Location> {
107        self.elements.locator(key, base)
108    }
109
110    fn pages(&self, _: Location) -> Option<NonZeroUsize> {
111        Some(self.pages)
112    }
113
114    fn page(&self, location: Location) -> Option<NonZeroUsize> {
115        self.elements.position(location).map(|pos| pos.page)
116    }
117
118    fn position(&self, location: Location) -> Option<DocumentPosition> {
119        self.elements.position(location).copied().map(DocumentPosition::Paged)
120    }
121
122    fn page_numbering(&self, location: Location) -> Option<&Numbering> {
123        let page = self.page(location)?;
124        self.page_numberings
125            .get(page.get() - 1)
126            .and_then(|slot| slot.as_ref())
127    }
128
129    fn page_supplement(&self, location: Location) -> Option<&Content> {
130        let page = self.page(location)?;
131        self.page_supplements.get(page.get() - 1)
132    }
133
134    fn anchor(&self, _: Location) -> Option<&EcoString> {
135        None
136    }
137
138    fn document(&self, _: Location) -> Option<Location> {
139        None
140    }
141
142    fn path(&self, _: Location) -> Option<&VirtualPath> {
143        None
144    }
145}
146
147impl Debug for PagedIntrospector {
148    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
149        f.pad("PagedIntrospector(..)")
150    }
151}
152
153/// Builds the introspector.
154#[derive(Default)]
155struct PagedIntrospectorBuilder {
156    elements: ElementIntrospectorBuilder<PagedPosition>,
157    frame_link_targets: FxHashSet<Location>,
158}
159
160impl PagedIntrospectorBuilder {
161    /// Build a complete introspector with all acceleration structures from a
162    /// list of top-level pairs.
163    fn finish(
164        self,
165        pages: NonZeroUsize,
166        page_numberings: Vec<Option<Numbering>>,
167        page_supplements: Vec<Content>,
168    ) -> PagedIntrospector {
169        PagedIntrospector {
170            elements: self.elements.finalize(),
171            frame_link_targets: self.frame_link_targets,
172            pages,
173            page_numberings,
174            page_supplements,
175        }
176    }
177
178    /// Discovers introspectibles in a frame.
179    fn discover_frame<F>(&mut self, frame: &Frame, ts: Transform, to_pos: &mut F)
180    where
181        F: FnMut(Point) -> PagedPosition,
182    {
183        for (pos, item) in frame.items() {
184            match item {
185                FrameItem::Tag(tag) => {
186                    self.elements.discover_tag(tag, to_pos(pos.transform(ts)));
187                }
188                FrameItem::Group(group) => {
189                    let ts = ts
190                        .pre_concat(Transform::translate(pos.x, pos.y))
191                        .pre_concat(group.transform);
192
193                    if let Some(parent) = group.parent {
194                        self.elements.start_insertion();
195                        self.discover_frame(&group.frame, ts, to_pos);
196                        self.elements.end_insertion(parent.location);
197                    } else {
198                        self.discover_frame(&group.frame, ts, to_pos);
199                    }
200                }
201                FrameItem::Link(dest, _) => {
202                    if let Destination::Location(loc) = dest {
203                        self.frame_link_targets.insert(*loc);
204                    }
205                }
206                FrameItem::Text(..) | FrameItem::Shape(..) | FrameItem::Image(..) => {}
207            }
208        }
209    }
210}