use std::collections::{HashMap, HashSet, VecDeque}; use comemo::Track; use ecow::{EcoString, eco_format}; use typst_library::foundations::{Label, NativeElement}; use typst_library::introspection::{Introspector, Location, Tag}; use typst_library::layout::{Frame, FrameItem, Point}; use typst_library::model::{Destination, LinkElem}; use typst_utils::PicoStr; use crate::{HtmlElement, HtmlNode, attr, tag}; /// Searches for links within a frame. /// /// If all links are created via `LinkElem` in the future, this can be removed /// in favor of the query in `identify_link_targets`. For the time being, some /// links are created without existence of a `LinkElem`, so this is /// unfortunately necessary. pub fn introspect_frame_links(frame: &Frame, targets: &mut HashSet) { for (_, item) in frame.items() { match item { FrameItem::Link(Destination::Location(loc), _) => { targets.insert(*loc); } FrameItem::Group(group) => introspect_frame_links(&group.frame, targets), _ => {} } } } /// Attaches IDs to nodes produced by link targets to make them linkable. /// /// May produce ``s for link targets that turned into text nodes or no /// nodes at all. See the [`LinkElem`] documentation for more details. pub fn identify_link_targets( root: &mut HtmlElement, introspector: &mut Introspector, mut targets: HashSet, ) { // Query for all links with an intra-doc (i.e. `Location`) destination to // know what needs IDs. targets.extend( introspector .query(&LinkElem::ELEM.select()) .iter() .map(|elem| elem.to_packed::().unwrap()) .filter_map(|elem| match elem.dest.resolve(introspector.track()) { Ok(Destination::Location(loc)) => Some(loc), _ => None, }), ); if targets.is_empty() { // Nothing to do. return; } // Assign IDs to all link targets. let mut work = Work::new(); traverse( &mut work, &targets, &mut Identificator::new(introspector), &mut root.children, ); // Add the mapping from locations to IDs to the introspector to make it // available to links in the next iteration. introspector.set_html_ids(work.ids); } /// Traverses a list of nodes. fn traverse( work: &mut Work, targets: &HashSet, identificator: &mut Identificator<'_>, nodes: &mut Vec, ) { let mut i = 0; while i < nodes.len() { let node = &mut nodes[i]; match node { // When visiting a start tag, we check whether the element needs an // ID and if so, add it to the queue, so that its first child node // receives an ID. HtmlNode::Tag(Tag::Start(elem)) => { let loc = elem.location().unwrap(); if targets.contains(&loc) { work.enqueue(loc, elem.label()); } } // When we reach an end tag, we check whether it closes an element // that is still in our queue. If so, that means the element // produced no nodes and we need to insert an empty span. HtmlNode::Tag(Tag::End(loc, _)) => { work.remove(*loc, |label| { let mut element = HtmlElement::new(tag::span); let id = identificator.assign(&mut element, label); nodes.insert(i + 1, HtmlNode::Element(element)); id }); } // When visiting an element and the queue is non-empty, we assign an // ID. Then, we traverse its children. HtmlNode::Element(element) => { work.drain(|label| identificator.assign(element, label)); traverse(work, targets, identificator, &mut element.children); } // When visiting text and the queue is non-empty, we generate a span // and assign an ID. HtmlNode::Text(..) => { work.drain(|label| { let mut element = HtmlElement::new(tag::span).with_children(vec![node.clone()]); let id = identificator.assign(&mut element, label); *node = HtmlNode::Element(element); id }); } // When visiting a frame and the queue is non-empty, we assign an // ID to it (will be added to the resulting SVG element). HtmlNode::Frame(frame) => { work.drain(|label| { frame.id.get_or_insert_with(|| identificator.identify(label)).clone() }); traverse_frame( work, targets, identificator, &frame.inner, &mut frame.link_points, ); } } i += 1; } } /// Traverses a frame embedded in HTML. fn traverse_frame( work: &mut Work, targets: &HashSet, identificator: &mut Identificator<'_>, frame: &Frame, link_points: &mut Vec<(Point, EcoString)>, ) { for (_, item) in frame.items() { match item { FrameItem::Tag(Tag::Start(elem)) => { let loc = elem.location().unwrap(); if targets.contains(&loc) { let pos = identificator.introspector.position(loc).point; let id = identificator.identify(elem.label()); work.ids.insert(loc, id.clone()); link_points.push((pos, id)); } } FrameItem::Group(group) => { traverse_frame(work, targets, identificator, &group.frame, link_points); } _ => {} } } } /// Keeps track of the work to be done during ID generation. struct Work { /// The locations and labels of elements we need to assign an ID to right /// now. queue: VecDeque<(Location, Option