1use std::collections::VecDeque;
2use std::sync::Arc;
3
4use ecow::{EcoString, EcoVec, eco_vec};
5use rustc_hash::{FxHashMap, FxHashSet};
6use typst_library::foundations::Label;
7use typst_library::introspection::{DocumentPosition, InnerHtmlPosition, Location, Tag};
8use typst_library::layout::{Frame, FrameItem, Point};
9use typst_library::model::AnchorGenerator;
10
11use crate::{HtmlDocument, HtmlElement, HtmlNode, attr, tag};
12
13pub fn create_link_anchors(
27 document: &mut HtmlDocument,
28 targets: &FxHashSet<Location>,
29) -> FxHashMap<Location, EcoString> {
30 if targets.is_empty() {
31 return FxHashMap::default();
33 }
34
35 let mut work = Work::new();
37 let introspector = Arc::clone(document.introspector());
38 traverse(
39 &mut work,
40 targets,
41 &mut AnchorGenerator::new(introspector.as_ref()),
42 &mut document.root_mut().children,
43 );
44 work.ids
45}
46
47fn traverse(
49 work: &mut Work,
50 targets: &FxHashSet<Location>,
51 generator: &mut AnchorGenerator<'_>,
52 nodes: &mut EcoVec<HtmlNode>,
53) {
54 let mut i = 0;
55 while i < nodes.len() {
56 let node = &mut nodes.make_mut()[i];
57 match node {
58 HtmlNode::Tag(Tag::Start(elem, _)) => {
62 let loc = elem.location().unwrap();
63 if targets.contains(&loc) {
64 work.enqueue(loc, elem.label());
65 }
66 }
67
68 HtmlNode::Tag(Tag::End(loc, _, _)) => {
72 work.remove(*loc, |label| {
73 let mut element = HtmlElement::new(tag::span);
74 let id = generator.assign(&mut element, label);
75 nodes.insert(i + 1, HtmlNode::Element(element));
76 id
77 });
78 }
79
80 HtmlNode::Element(element) => {
83 work.drain(|label| generator.assign(element, label));
84 traverse(work, targets, generator, &mut element.children);
85 }
86
87 HtmlNode::Text(..) => {
90 work.drain(|label| {
91 let mut element =
92 HtmlElement::new(tag::span).with_children(eco_vec![node.clone()]);
93 let id = generator.assign(&mut element, label);
94 *node = HtmlNode::Element(element);
95 id
96 });
97 }
98
99 HtmlNode::Frame(frame) => {
102 work.drain(|label| {
103 frame.id.get_or_insert_with(|| generator.identify(label)).clone()
104 });
105 traverse_frame(
106 work,
107 targets,
108 generator,
109 &frame.inner,
110 &mut frame.anchors,
111 );
112 }
113 }
114
115 i += 1;
116 }
117}
118
119fn traverse_frame(
121 work: &mut Work,
122 targets: &FxHashSet<Location>,
123 generator: &mut AnchorGenerator<'_>,
124 frame: &Frame,
125 anchors: &mut EcoVec<(Point, EcoString)>,
126) {
127 for (_, item) in frame.items() {
128 match item {
129 FrameItem::Tag(Tag::Start(elem, _)) => {
130 let loc = elem.location().unwrap();
131 if targets.contains(&loc)
132 && let Some(DocumentPosition::Html(position)) =
133 generator.introspector().position(loc)
134 && let Some(InnerHtmlPosition::Frame(point)) = position.details()
135 {
136 let id = generator.identify(elem.label());
137 work.ids.insert(loc, id.clone());
138 anchors.push((*point, id));
139 }
140 }
141 FrameItem::Group(group) => {
142 traverse_frame(work, targets, generator, &group.frame, anchors);
143 }
144 _ => {}
145 }
146 }
147}
148
149struct Work {
151 queue: VecDeque<(Location, Option<Label>)>,
154 ids: FxHashMap<Location, EcoString>,
156}
157
158impl Work {
159 fn new() -> Self {
161 Self { queue: VecDeque::new(), ids: FxHashMap::default() }
162 }
163
164 fn enqueue(&mut self, loc: Location, label: Option<Label>) {
167 self.queue.push_back((loc, label))
168 }
169
170 fn drain(&mut self, f: impl FnOnce(Option<Label>) -> EcoString) {
174 if let Some(&(_, label)) = self.queue.front() {
175 let id = f(label);
176 for (loc, _) in self.queue.drain(..) {
177 self.ids.insert(loc, id.clone());
178 }
179 }
180 }
181
182 fn remove(&mut self, loc: Location, f: impl FnOnce(Option<Label>) -> EcoString) {
184 if let Some(i) = self.queue.iter().position(|&(l, _)| l == loc) {
185 let (_, label) = self.queue.remove(i).unwrap();
186 let id = f(label);
187 self.ids.insert(loc, id.clone());
188 }
189 }
190}
191
192trait AnchorGeneratorExt {
193 fn assign(&mut self, element: &mut HtmlElement, label: Option<Label>) -> EcoString;
195}
196
197impl AnchorGeneratorExt for AnchorGenerator<'_> {
198 fn assign(&mut self, element: &mut HtmlElement, label: Option<Label>) -> EcoString {
199 element.attrs.get(attr::id).cloned().unwrap_or_else(|| {
200 let id = self.identify(label);
201 element.attrs.push_front(attr::id, id.clone());
202 id
203 })
204 }
205}