summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorPg Biel <9021226+PgBiel@users.noreply.github.com>2023-07-05 11:16:00 -0300
committerGitHub <noreply@github.com>2023-07-05 16:16:00 +0200
commitd37217aaa4b5029d7ec8405739b8deda4792696f (patch)
tree5cd465964e89eac5bbb6b7c6e1db48275ee06f71 /crates
parent8711b5eeede405e4d0a4190c7e5ab8447dc3fa3a (diff)
Add heading `bookmarked` toggle (#1566)
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-library/src/meta/heading.rs30
-rw-r--r--crates/typst/src/export/pdf/outline.rs75
2 files changed, 98 insertions, 7 deletions
diff --git a/crates/typst-library/src/meta/heading.rs b/crates/typst-library/src/meta/heading.rs
index d70bc0a9..00b93993 100644
--- a/crates/typst-library/src/meta/heading.rs
+++ b/crates/typst-library/src/meta/heading.rs
@@ -78,7 +78,11 @@ pub struct HeadingElem {
/// ```
pub supplement: Smart<Option<Supplement>>,
- /// Whether the heading should appear in the outline.
+ /// Whether the heading should appear in the [outline]($func/outline).
+ ///
+ /// Note that this property, if set to `{true}`, ensures the heading is
+ /// also shown as a bookmark in the exported PDF's outline (when exporting
+ /// to PDF). To change that behavior, use the `bookmarked` property.
///
/// ```example
/// #outline()
@@ -93,6 +97,29 @@ pub struct HeadingElem {
#[default(true)]
pub outlined: bool,
+ /// Whether the heading should appear as a bookmark in the exported PDF's
+ /// outline. Doesn't affect other export formats, such as PNG.
+ ///
+ /// The default value of `{auto}` indicates that the heading will only
+ /// appear in the exported PDF's outline if its `outlined` property is set
+ /// to `{true}`, that is, if it would also be listed in Typst's
+ /// [outline]($func/outline). Setting this property to either
+ /// `{true}` (bookmark) or `{false}` (don't bookmark) bypasses that
+ /// behavior.
+ ///
+ /// ```example
+ /// #heading[Normal heading]
+ /// This heading will be shown in
+ /// the PDF's bookmark outline.
+ ///
+ /// #heading(bookmarked: false)[Not bookmarked]
+ /// This heading won't be
+ /// bookmarked in the resulting
+ /// PDF.
+ /// ```
+ #[default(Smart::Auto)]
+ pub bookmarked: Smart<bool>,
+
/// The heading's title.
#[required]
pub body: Content,
@@ -111,6 +138,7 @@ impl Synthesize for HeadingElem {
self.push_numbering(self.numbering(styles));
self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
self.push_outlined(self.outlined(styles));
+ self.push_bookmarked(self.bookmarked(styles));
Ok(())
}
diff --git a/crates/typst/src/export/pdf/outline.rs b/crates/typst/src/export/pdf/outline.rs
index 539647eb..89d01135 100644
--- a/crates/typst/src/export/pdf/outline.rs
+++ b/crates/typst/src/export/pdf/outline.rs
@@ -3,22 +3,80 @@ use std::num::NonZeroUsize;
use pdf_writer::{Finish, Ref, TextStr};
use super::{AbsExt, PdfContext, RefExt};
-use crate::geom::Abs;
+use crate::geom::{Abs, Smart};
use crate::model::Content;
/// Construct the outline for the document.
#[tracing::instrument(skip_all)]
pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
let mut tree: Vec<HeadingNode> = vec![];
+
+ // Stores the level of the topmost skipped ancestor of the next bookmarked
+ // heading. A skipped heading is a heading with 'bookmarked: false', that
+ // is, it is not added to the PDF outline, and so is not in the tree.
+ // Therefore, its next descendant must be added at its level, which is
+ // enforced in the manner shown below.
+ let mut last_skipped_level = None;
for heading in ctx.introspector.query(&item!(heading_func).select()) {
let leaf = HeadingNode::leaf((*heading).clone());
- let mut children = &mut tree;
- while children.last().map_or(false, |last| last.level < leaf.level) {
- children = &mut children.last_mut().unwrap().children;
+ if leaf.bookmarked {
+ let mut children = &mut tree;
+
+ // Descend the tree through the latest bookmarked heading of each
+ // level until either:
+ // - you reach a node whose children would be brothers of this
+ // heading (=> add the current heading as a child of this node);
+ // - you reach a node with no children (=> this heading probably
+ // skipped a few nesting levels in Typst, or one or more ancestors
+ // of this heading weren't bookmarked, so add it as a child of this
+ // node, which is its deepest bookmarked ancestor);
+ // - or, if the latest heading(s) was(/were) skipped
+ // ('bookmarked: false'), then stop if you reach a node whose
+ // children would be brothers of the latest skipped heading
+ // of lowest level (=> those skipped headings would be ancestors
+ // of the current heading, so add it as a 'brother' of the least
+ // deep skipped ancestor among them, as those ancestors weren't
+ // added to the bookmark tree, and the current heading should not
+ // be mistakenly added as a descendant of a brother of that
+ // ancestor.)
+ //
+ // That is, if you had a bookmarked heading of level N, a skipped
+ // heading of level N, a skipped heading of level N + 1, and then
+ // a bookmarked heading of level N + 2, that last one is bookmarked
+ // as a level N heading (taking the place of its topmost skipped
+ // ancestor), so that it is not mistakenly added as a descendant of
+ // the previous level N heading.
+ //
+ // In other words, a heading can be added to the bookmark tree
+ // at most as deep as its topmost skipped direct ancestor (if it
+ // exists), or at most as deep as its actual nesting level in Typst
+ // (not exceeding whichever is the most restrictive depth limit
+ // of those two).
+ while children.last().map_or(false, |last| {
+ last_skipped_level.map_or(true, |l| last.level < l)
+ && last.level < leaf.level
+ }) {
+ children = &mut children.last_mut().unwrap().children;
+ }
+
+ // Since this heading was bookmarked, the next heading, if it is a
+ // child of this one, won't have a skipped direct ancestor (indeed,
+ // this heading would be its most direct ancestor, and wasn't
+ // skipped). Therefore, it can be added as a child of this one, if
+ // needed, following the usual rules listed above.
+ last_skipped_level = None;
+ children.push(leaf);
+ } else if last_skipped_level.map_or(true, |l| leaf.level < l) {
+ // Only the topmost / lowest-level skipped heading matters when you
+ // have consecutive skipped headings (since none of them are being
+ // added to the bookmark tree), hence the condition above.
+ // This ensures the next bookmarked heading will be placed
+ // at most as deep as its topmost skipped ancestors. Deeper
+ // ancestors do not matter as the nesting structure they create
+ // won't be visible in the PDF outline.
+ last_skipped_level = Some(leaf.level);
}
-
- children.push(leaf);
}
if tree.is_empty() {
@@ -48,6 +106,7 @@ pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
struct HeadingNode {
element: Content,
level: NonZeroUsize,
+ bookmarked: bool,
children: Vec<HeadingNode>,
}
@@ -55,6 +114,10 @@ impl HeadingNode {
fn leaf(element: Content) -> Self {
HeadingNode {
level: element.expect_field::<NonZeroUsize>("level"),
+ // 'bookmarked' set to 'auto' falls back to the value of 'outlined'.
+ bookmarked: element
+ .expect_field::<Smart<bool>>("bookmarked")
+ .unwrap_or_else(|| element.expect_field::<bool>("outlined")),
element,
children: Vec::new(),
}