Repository: typst/typst
File: crates/typst-pdf/src/outline.rs (Rust)

Code before:
use std::num::NonZeroUsize;

use pdf_writer::{Finish, Ref, TextStr};
use typst::foundations::{NativeElement, Packed, StyleChain};
use typst::layout::Abs;
use typst::model::HeadingElem;

use crate::{AbsExt, PdfContext};

/// Construct the outline for the document.
pub(crate) 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;
    let elements = ctx.document.introspector.query(&HeadingElem::elem().select());

    for elem in elements.iter() {
        if let Some(page_ranges) = &ctx.exported_pages {
            if !page_ranges.page_included(
                ctx.document.introspector.page(elem.location().unwrap()).get() - 1,
            ) {
                // Don't bookmark headings in non-exported pages
                continue;
            }
        }

        let heading = elem.to_packed::<HeadingElem>().unwrap();
        let leaf = HeadingNode::leaf(heading);

        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

Review comment (suggestion):
Maybe we could have `includes_page` and `includes_page_index`, so that we don't need to have `-1` sprinkled randomly.

Code after:
use std::num::NonZeroUsize;

use pdf_writer::{Finish, Ref, TextStr};
use typst::foundations::{NativeElement, Packed, StyleChain};
use typst::layout::Abs;
use typst::model::HeadingElem;

use crate::{AbsExt, PdfContext};

/// Construct the outline for the document.
pub(crate) 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;
    let elements = ctx.document.introspector.query(&HeadingElem::elem().select());

    for elem in elements.iter() {
        if let Some(page_ranges) = &ctx.exported_pages {
            if !page_ranges
                .includes_page(ctx.document.introspector.page(elem.location().unwrap()))
            {
                // Don't bookmark headings in non-exported pages
                continue;
            }
        }

        let heading = elem.to_packed::<HeadingElem>().unwrap();
        let leaf = HeadingNode::leaf(heading);

        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