summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTobias Schmitz <tobiasschmitz2001@gmail.com>2025-06-26 17:11:08 +0200
committerTobias Schmitz <tobiasschmitz2001@gmail.com>2025-07-03 18:42:59 +0200
commitd6307831dd78b4ac5c2d6498b2874387331ec36f (patch)
tree57769454668ae47a65cb54e86a3dfdd641f63d7b
parent09b2cd6de51610a44bdf9f000b74a83273d279c2 (diff)
feat: hierarchical outline tags
-rw-r--r--crates/typst-pdf/src/tags.rs97
1 files changed, 92 insertions, 5 deletions
diff --git a/crates/typst-pdf/src/tags.rs b/crates/typst-pdf/src/tags.rs
index b03279a5..942335ab 100644
--- a/crates/typst-pdf/src/tags.rs
+++ b/crates/typst-pdf/src/tags.rs
@@ -42,6 +42,8 @@ pub(crate) struct StackEntry {
pub(crate) enum StackEntryKind {
Standard(Tag),
Link(LinkId, Packed<LinkMarker>),
+ Outline(OutlineCtx),
+ OutlineEntry(Packed<OutlineEntry>),
Table(TableCtx),
TableCell(Packed<TableCell>),
}
@@ -56,12 +58,82 @@ impl StackEntryKind {
}
}
+pub(crate) struct OutlineCtx {
+ stack: Vec<OutlineSection>,
+}
+
+pub(crate) struct OutlineSection {
+ entries: Vec<TagNode>,
+}
+
+impl OutlineSection {
+ const fn new() -> Self {
+ OutlineSection { entries: Vec::new() }
+ }
+
+ fn push(&mut self, entry: TagNode) {
+ self.entries.push(entry);
+ }
+
+ fn into_tag(self) -> TagNode {
+ TagNode::Group(TagKind::TOC.into(), self.entries)
+ }
+}
+
+impl OutlineCtx {
+ fn new() -> Self {
+ Self { stack: Vec::new() }
+ }
+
+ fn insert(
+ &mut self,
+ outline_nodes: &mut Vec<TagNode>,
+ entry: Packed<OutlineEntry>,
+ nodes: Vec<TagNode>,
+ ) {
+ let expected_len = entry.level.get() - 1;
+ if self.stack.len() < expected_len {
+ self.stack.resize_with(expected_len, || OutlineSection::new());
+ } else {
+ while self.stack.len() > expected_len {
+ self.finish_section(outline_nodes);
+ }
+ }
+
+ let section_entry = TagNode::Group(TagKind::TOCI.into(), nodes);
+ self.push(outline_nodes, section_entry);
+ }
+
+ fn finish_section(&mut self, outline_nodes: &mut Vec<TagNode>) {
+ let sub_section = self.stack.pop().unwrap().into_tag();
+ self.push(outline_nodes, sub_section);
+ }
+
+ fn push(&mut self, outline_nodes: &mut Vec<TagNode>, entry: TagNode) {
+ match self.stack.last_mut() {
+ Some(section) => section.push(entry),
+ None => outline_nodes.push(entry),
+ }
+ }
+
+ fn build_outline(mut self, mut outline_nodes: Vec<TagNode>) -> Vec<TagNode> {
+ while self.stack.len() > 0 {
+ self.finish_section(&mut outline_nodes);
+ }
+ outline_nodes
+ }
+}
+
pub(crate) struct TableCtx {
table: Packed<TableElem>,
rows: Vec<Vec<Option<(Packed<TableCell>, Tag, Vec<TagNode>)>>>,
}
impl TableCtx {
+ fn new(table: Packed<TableElem>) -> Self {
+ Self { table: table.clone(), rows: Vec::new() }
+ }
+
fn insert(&mut self, cell: Packed<TableCell>, nodes: Vec<TagNode>) {
let x = cell.x(StyleChain::default()).unwrap_or_else(|| unreachable!());
let y = cell.y(StyleChain::default()).unwrap_or_else(|| unreachable!());
@@ -312,9 +384,11 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
_ => TagKind::H6(Some(name)).into(),
}
} else if let Some(_) = elem.to_packed::<OutlineElem>() {
- TagKind::TOC.into()
- } else if let Some(_) = elem.to_packed::<OutlineEntry>() {
- TagKind::TOCI.into()
+ push_stack(gc, loc, StackEntryKind::Outline(OutlineCtx::new()));
+ return;
+ } else if let Some(entry) = elem.to_packed::<OutlineEntry>() {
+ push_stack(gc, loc, StackEntryKind::OutlineEntry(entry.clone()));
+ return;
} else if let Some(_) = elem.to_packed::<FigureElem>() {
let alt = None; // TODO
TagKind::Figure.with_alt_text(alt)
@@ -340,8 +414,7 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
push_stack(gc, loc, StackEntryKind::Link(link_id, link.clone()));
return;
} else if let Some(table) = elem.to_packed::<TableElem>() {
- let ctx = TableCtx { table: table.clone(), rows: Vec::new() };
- push_stack(gc, loc, StackEntryKind::Table(ctx));
+ push_stack(gc, loc, StackEntryKind::Table(TableCtx::new(table.clone())));
return;
} else if let Some(cell) = elem.to_packed::<TableCell>() {
push_stack(gc, loc, StackEntryKind::TableCell(cell.clone()));
@@ -385,6 +458,20 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) {
}
node
}
+ StackEntryKind::Outline(ctx) => {
+ let nodes = ctx.build_outline(entry.nodes);
+ TagNode::Group(TagKind::TOC.into(), nodes)
+ }
+ StackEntryKind::OutlineEntry(outline_entry) => {
+ let parent = gc.tags.stack.last_mut().expect("outline");
+ let StackEntryKind::Outline(outline_ctx) = &mut parent.kind else {
+ unreachable!("expected outline")
+ };
+
+ outline_ctx.insert(&mut parent.nodes, outline_entry, entry.nodes);
+
+ return;
+ }
StackEntryKind::Table(ctx) => {
let summary = ctx.table.summary(StyleChain::default()).map(EcoString::into);
let nodes = ctx.build_table(entry.nodes);