mirror of
https://github.com/typst/typst
synced 2025-05-15 17:45:27 +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>>,
|
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
|
/// ```example
|
||||||
/// #outline()
|
/// #outline()
|
||||||
@ -93,6 +97,29 @@ pub struct HeadingElem {
|
|||||||
#[default(true)]
|
#[default(true)]
|
||||||
pub outlined: bool,
|
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.
|
/// The heading's title.
|
||||||
#[required]
|
#[required]
|
||||||
pub body: Content,
|
pub body: Content,
|
||||||
@ -111,6 +138,7 @@ impl Synthesize for HeadingElem {
|
|||||||
self.push_numbering(self.numbering(styles));
|
self.push_numbering(self.numbering(styles));
|
||||||
self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
|
self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
|
||||||
self.push_outlined(self.outlined(styles));
|
self.push_outlined(self.outlined(styles));
|
||||||
|
self.push_bookmarked(self.bookmarked(styles));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -3,22 +3,80 @@ use std::num::NonZeroUsize;
|
|||||||
use pdf_writer::{Finish, Ref, TextStr};
|
use pdf_writer::{Finish, Ref, TextStr};
|
||||||
|
|
||||||
use super::{AbsExt, PdfContext, RefExt};
|
use super::{AbsExt, PdfContext, RefExt};
|
||||||
use crate::geom::Abs;
|
use crate::geom::{Abs, Smart};
|
||||||
use crate::model::Content;
|
use crate::model::Content;
|
||||||
|
|
||||||
/// Construct the outline for the document.
|
/// Construct the outline for the document.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
|
pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
|
||||||
let mut tree: Vec<HeadingNode> = vec![];
|
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()) {
|
for heading in ctx.introspector.query(&item!(heading_func).select()) {
|
||||||
let leaf = HeadingNode::leaf((*heading).clone());
|
let leaf = HeadingNode::leaf((*heading).clone());
|
||||||
|
|
||||||
|
if leaf.bookmarked {
|
||||||
let mut children = &mut tree;
|
let mut children = &mut tree;
|
||||||
while children.last().map_or(false, |last| last.level < leaf.level) {
|
|
||||||
|
// 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;
|
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);
|
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() {
|
if tree.is_empty() {
|
||||||
@ -48,6 +106,7 @@ pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
|
|||||||
struct HeadingNode {
|
struct HeadingNode {
|
||||||
element: Content,
|
element: Content,
|
||||||
level: NonZeroUsize,
|
level: NonZeroUsize,
|
||||||
|
bookmarked: bool,
|
||||||
children: Vec<HeadingNode>,
|
children: Vec<HeadingNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +114,10 @@ impl HeadingNode {
|
|||||||
fn leaf(element: Content) -> Self {
|
fn leaf(element: Content) -> Self {
|
||||||
HeadingNode {
|
HeadingNode {
|
||||||
level: element.expect_field::<NonZeroUsize>("level"),
|
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,
|
element,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,8 @@ fn main() {
|
|||||||
==== Deep Stuff
|
==== Deep Stuff
|
||||||
Ok ...
|
Ok ...
|
||||||
|
|
||||||
#set heading(numbering: "(I)")
|
// Ensure 'bookmarked' option doesn't affect the outline
|
||||||
|
#set heading(numbering: "(I)", bookmarked: false)
|
||||||
|
|
||||||
= #text(blue)[Zusammen]fassung
|
= #text(blue)[Zusammen]fassung
|
||||||
#lorem(10)
|
#lorem(10)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user