From 3455ac7dd2f5c3cb8785692d45eac8dd4110ce79 Mon Sep 17 00:00:00 2001 From: "Y.D.X." <73375426+YDX-2147483647@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:17:54 +0800 Subject: [PATCH] Include numbering in PDF bookmark (#6622) Co-authored-by: Laurenz --- crates/typst-library/src/model/heading.rs | 23 +++++++++++++++++++++++ crates/typst-pdf/src/outline.rs | 8 +++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index f5e7f4f37..d61aa415d 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -1,5 +1,6 @@ use std::num::NonZeroUsize; +use ecow::EcoString; use typst_utils::NonZeroExt; use crate::diag::SourceResult; @@ -103,6 +104,18 @@ pub struct HeadingElem { /// ``` pub numbering: Option, + /// The resolved plain-text numbers. + /// + /// This field is internal and only used for creating PDF bookmarks. We + /// don't currently have access to `World`, `Engine`, or `styles` in export, + /// which is needed to resolve the counter and numbering pattern into a + /// concrete string. + /// + /// This remains unset if `numbering` is `None`. + #[internal] + #[synthesized] + pub numbers: EcoString, + /// A supplement for the heading. /// /// For references to headings, this is added before the referenced number. @@ -202,6 +215,16 @@ impl Synthesize for Packed { } }; + if let Some((numbering, location)) = + self.numbering.get_ref(styles).as_ref().zip(self.location()) + { + self.numbers = Some( + self.counter() + .display_at_loc(engine, location, styles, numbering)? + .plain_text(), + ); + } + let elem = self.as_mut(); elem.level.set(Smart::Custom(elem.resolve_level(styles))); elem.supplement diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index 7228f0f32..dc9216e20 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -124,10 +124,16 @@ impl<'a> HeadingNode<'a> { fn to_krilla(&self, gc: &GlobalContext) -> Option { let loc = self.element.location().unwrap(); - let title = self.element.body.plain_text().to_string(); let pos = gc.document.introspector.position(loc); let page_index = pos.page.get() - 1; + // Prepend the numbers to the title if they exist. + let text = self.element.body.plain_text(); + let title = match &self.element.numbers { + Some(num) => format!("{num} {text}"), + None => text.to_string(), + }; + if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); let dest = XyzDestination::new(