Add heading bookmarked toggle (#1566)

This commit is contained in:
Pg Biel 2023-07-05 11:16:00 -03:00 committed by GitHub
parent 8711b5eeed
commit d37217aaa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 8 deletions

View File

@ -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(())
}

View File

@ -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;
children.push(leaf);
// 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);
}
}
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(),
}

View File

@ -30,7 +30,8 @@ fn main() {
==== Deep Stuff
Ok ...
#set heading(numbering: "(I)")
// Ensure 'bookmarked' option doesn't affect the outline
#set heading(numbering: "(I)", bookmarked: false)
= #text(blue)[Zusammen]fassung
#lorem(10)