mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Add heading bookmarked
toggle (#1566)
This commit is contained in:
parent
8711b5eeed
commit
d37217aaa4
@ -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(())
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user