Skip to content

Commit

Permalink
feat: partially support to infer book meta
Browse files Browse the repository at this point in the history
  • Loading branch information
Myriad-Dreamin committed Nov 30, 2023
1 parent bb28dda commit 5b42e5c
Show file tree
Hide file tree
Showing 6 changed files with 761 additions and 21 deletions.
66 changes: 66 additions & 0 deletions cli/src/debug_loc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use serde::{Deserialize, Serialize};
use typst::doc::Position as TypstPosition;

/// A serializable physical position in a document.
///
/// Note that it uses [`f32`] instead of [`f64`] as same as
/// [`TypstPosition`] for the coordinates to improve both performance
/// of serialization and calculation. It does sacrifice the floating
/// precision, but it is enough in our use cases.
///
/// Also see [`TypstPosition`].
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct DocumentPosition {
/// The page, starting at 1.
pub page_no: usize,
/// The exact x-coordinate on the page (from the left, as usual).
pub x: f32,
/// The exact y-coordinate on the page (from the top, as usual).
pub y: f32,
}

impl From<TypstPosition> for DocumentPosition {
fn from(position: TypstPosition) -> Self {
Self {
page_no: position.page.into(),
x: position.point.x.to_pt() as f32,
y: position.point.y.to_pt() as f32,
}
}
}

// /// Unevaluated source span.
// /// The raw source span is unsafe to serialize and deserialize.
// /// Because the real source location is only known during liveness of
// /// the compiled document.
// pub type SourceSpan = typst::syntax::Span;

// /// Raw representation of a source span.
// pub type RawSourceSpan = u64;

// /// A char position represented in form of line and column.
// /// The position is encoded in Utf-8 or Utf-16, and the encoding is
// /// determined by usage.
// ///
// pub struct CharPosition {
// /// The line number, starting at 0.
// line: usize,
// /// The column number, starting at 0.
// column: usize,
// }

// /// A resolved source (text) location.
// ///
// /// See [`CharPosition`] for the definition of the position inside a file.
// pub struct SourceLocation {
// filepath: PathBuf,
// pos: CharPosition,
// }
// /// A resolved source (text) range.
// ///
// /// See [`CharPosition`] for the definition of the position inside a file.
// pub struct SourceRange {
// filepath: PathBuf,
// start: CharPosition,
// end: CharPosition,
// }
35 changes: 34 additions & 1 deletion cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
mod debug_loc;
pub mod error;
pub mod font;
pub mod meta;
mod outline;
pub mod project;
pub mod render;
pub mod theme;
pub mod utils;
pub mod version;
use version::VersionFormat;

use core::fmt;
use std::path::PathBuf;

use clap::{ArgAction, Parser, Subcommand};
use clap::{ArgAction, Parser, Subcommand, ValueEnum};

#[derive(Debug, Parser)]
#[clap(name = "typst-book", version = "0.1.0")]
Expand Down Expand Up @@ -43,6 +46,32 @@ pub enum Subcommands {
Serve(ServeArgs),
}

/// Determine the approach to retrieving metadata of a book project.
#[derive(ValueEnum, Debug, Clone, Eq, PartialEq)]
#[value(rename_all = "kebab-case")]
pub enum MetaSource {
/// Strictly retrieve the project's meta by label queries.
/// + retrieve the book meta from `<typst-book-book-meta>`
/// + retrieve the build meta from `<typst-book-build-meta>`
Strict,
/// Infer the project's meta from the outline of main file.
/// Note: if the main file also contains `<typst-book-book-meta>` or
/// `<typst-book-build-meta>`, the manual-set meta will be used first.
Outline,
}

impl fmt::Display for MetaSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().unwrap().get_name())
}
}

impl Default for MetaSource {
fn default() -> Self {
Self::Strict
}
}

#[derive(Default, Debug, Clone, Parser)]
#[clap(next_help_heading = "Compile options")]
pub struct CompileArgs {
Expand All @@ -51,6 +80,10 @@ pub struct CompileArgs {
#[clap(default_value = "")]
pub dir: String,

/// Determine the approach to retrieving metadata of the book project.
#[clap(long, default_value = "None")]
pub meta_source: Option<MetaSource>,

/// Root directory for the typst workspace, which is same as the
/// `typst-cli`'s root. (Defaults to the root directory for the book
/// when omitted)
Expand Down
2 changes: 1 addition & 1 deletion cli/src/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub enum BookMetaElem {

/// General information about your book.
/// Book metadata in summary.typ
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct BookMeta {
/// The title of the book
pub title: String,
Expand Down
169 changes: 169 additions & 0 deletions cli/src/outline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use std::num::NonZeroUsize;

use serde::{Deserialize, Serialize};
use typst::{
geom::Smart,
model::{Content, Introspector},
};
use typst_ts_core::{vector::span_id_to_u64, TypstDocument};

use super::debug_loc::DocumentPosition;

/// A heading in the outline panel.
#[derive(Debug, Clone)]
pub(crate) struct HeadingNode {
element: Content,
position: DocumentPosition,
level: NonZeroUsize,
bookmarked: bool,
children: Vec<HeadingNode>,
}

/// Construct the outline for the document.
pub(crate) fn get_outline(introspector: &mut Introspector) -> Option<Vec<HeadingNode>> {
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 selector = typst::eval::LANG_ITEMS.get().unwrap().heading_elem.select();
for heading in introspector.query(&selector).iter() {
let leaf = HeadingNode::leaf(introspector, (**heading).clone());

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);
}
}

(!tree.is_empty()).then_some(tree)
}

impl HeadingNode {
fn leaf(introspector: &mut Introspector, element: Content) -> Self {
let position = {
let loc = element.location().unwrap();
introspector.position(loc).into()
};

HeadingNode {
level: element.expect_field::<NonZeroUsize>("level"),
position,
// '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(),
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Outline {
pub items: Vec<OutlineItem>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutlineItem {
/// Plain text title.
pub title: String,
/// Span id in hex-format.
pub span: Option<String>,
/// The resolved position in the document.
pub position: Option<DocumentPosition>,
/// The children of the outline item.
pub children: Vec<OutlineItem>,
}

pub fn outline(document: &TypstDocument) -> Outline {
let mut introspector = Introspector::new(&document.pages);
let outline = get_outline(&mut introspector);
let mut items = Vec::with_capacity(outline.as_ref().map_or(0, Vec::len));

for heading in outline.iter().flatten() {
outline_item(heading, &mut items);
}

Outline { items }
}

fn outline_item(src: &HeadingNode, res: &mut Vec<OutlineItem>) {
let body = src.element.expect_field::<Content>("body");
let title = body.plain_text().trim().to_owned();

let mut children = Vec::with_capacity(src.children.len());
for child in src.children.iter() {
outline_item(child, &mut children);
}

// use body's span first, otherwise use the element's span.
let span = body.span();
let span = if span.is_detached() {
src.element.span()
} else {
span
};

res.push(OutlineItem {
title,
span: Some(format!("{:x}", span_id_to_u64(&span))),
position: Some(src.position),
children,
});
}
Loading

0 comments on commit 5b42e5c

Please sign in to comment.