From f355cada932decd37e47e94bbfc4b0924add514f Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Thu, 30 Nov 2023 09:29:17 +0800 Subject: [PATCH] feat: partially support to infer book meta --- cli/src/debug_loc.rs | 66 +++++++ cli/src/lib.rs | 35 +++- cli/src/meta.rs | 2 +- cli/src/outline.rs | 169 +++++++++++++++++ cli/src/project.rs | 99 ++++++++-- cli/src/render/typst.rs | 411 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 761 insertions(+), 21 deletions(-) create mode 100644 cli/src/debug_loc.rs create mode 100644 cli/src/outline.rs diff --git a/cli/src/debug_loc.rs b/cli/src/debug_loc.rs new file mode 100644 index 0000000..1bb2b73 --- /dev/null +++ b/cli/src/debug_loc.rs @@ -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 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, +// } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index c9812d1..c0e3bb2 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,6 +1,8 @@ +mod debug_loc; pub mod error; pub mod font; pub mod meta; +mod outline; pub mod project; pub mod render; pub mod theme; @@ -8,9 +10,10 @@ 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")] @@ -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 `` + /// + retrieve the build meta from `` + Strict, + /// Infer the project's meta from the outline of main file. + /// Note: if the main file also contains `` or + /// ``, 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 { @@ -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, + /// 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) diff --git a/cli/src/meta.rs b/cli/src/meta.rs index 3e4d2c3..fbd1ad5 100644 --- a/cli/src/meta.rs +++ b/cli/src/meta.rs @@ -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, diff --git a/cli/src/outline.rs b/cli/src/outline.rs new file mode 100644 index 0000000..10b2a79 --- /dev/null +++ b/cli/src/outline.rs @@ -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, +} + +/// Construct the outline for the document. +pub(crate) fn get_outline(introspector: &mut Introspector) -> Option> { + let mut tree: Vec = 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::("level"), + position, + // 'bookmarked' set to 'auto' falls back to the value of 'outlined'. + bookmarked: element + .expect_field::>("bookmarked") + .unwrap_or_else(|| element.expect_field::("outlined")), + element, + children: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Outline { + pub items: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OutlineItem { + /// Plain text title. + pub title: String, + /// Span id in hex-format. + pub span: Option, + /// The resolved position in the document. + pub position: Option, + /// The children of the outline item. + pub children: Vec, +} + +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) { + let body = src.element.expect_field::("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, + }); +} diff --git a/cli/src/project.rs b/cli/src/project.rs index 92e9acb..dee01e7 100644 --- a/cli/src/project.rs +++ b/cli/src/project.rs @@ -10,8 +10,10 @@ use crate::{ error::prelude::*, meta::{BookMeta, BookMetaContent, BookMetaElem, BuildMeta}, render::{DataDict, HtmlRenderer, TypstRenderer}, - utils::{copy_dir_embedded, create_dirs, release_packages, write_file}, - CompileArgs, + utils::{ + copy_dir_embedded, create_dirs, make_absolute, release_packages, write_file, UnwrapOrExit, + }, + CompileArgs, MetaSource, }; use include_dir::include_dir; @@ -48,9 +50,11 @@ pub struct Project { pub hr: HtmlRenderer, pub book_meta: Option, pub build_meta: Option, + pub chapters: Vec, pub dest_dir: PathBuf, pub path_to_root: String, + pub meta_source: MetaSource, } impl Project { @@ -66,6 +70,23 @@ impl Project { args.path_to_root.push('/'); } + let meta_source = args.meta_source.clone().unwrap_or(MetaSource::Strict); + + args.dir = make_absolute(Path::new(&args.dir)) + .to_str() + .unwrap() + .to_owned(); + + let dir = Path::new(&args.dir); + let mut entry_file = None; + if dir.is_file() { + if meta_source == MetaSource::Strict { + return Err(error_once!("project dir is a file", dir: dir.display())); + } + entry_file = Some(dir.to_owned()); + args.dir = dir.parent().unwrap().to_str().unwrap().to_owned(); + } + if args.workspace.is_empty() { args.workspace = args.dir.clone(); } @@ -79,7 +100,9 @@ impl Project { hr, book_meta: None, build_meta: None, + chapters: vec![], path_to_root, + meta_source, }; release_packages( @@ -92,8 +115,6 @@ impl Project { include_dir!("$CARGO_MANIFEST_DIR/../contrib/typst/variables"), ); - proj.compile_meta()?; - if final_dest_dir.is_empty() { if let Some(dest_dir) = proj.build_meta.as_ref().map(|b| b.dest_dir.clone()) { final_dest_dir = dest_dir; @@ -107,6 +128,17 @@ impl Project { proj.tr.fix_dest_dir(Path::new(&final_dest_dir)); proj.dest_dir = proj.tr.dest_dir.clone(); + match proj.meta_source { + MetaSource::Strict => { + assert!(entry_file.is_none()); + proj.compile_meta()?; + } + MetaSource::Outline => { + assert!(entry_file.is_some()); + proj.infer_meta_by_outline(entry_file.unwrap())?; + } + } + Ok(proj) } @@ -209,6 +241,33 @@ impl Project { Ok(()) } + pub fn infer_meta_by_outline(&mut self, entry: PathBuf) -> ZResult<()> { + // println!("entry = {:?}, root = {:?}", entry, self.tr.root_dir); + let entry = entry.strip_prefix(&self.tr.root_dir).unwrap_or_exit(); + let doc = self.tr.compile_book(entry)?; + + // let outline = crate::outline::outline(&doc); + // println!("outline: {:#?}", outline); + + let chapters = self.tr.compile_pages_by_outline(entry)?; + self.chapters = self.generate_chapters(&chapters); + + self.book_meta = Some(BookMeta { + title: doc + .title + .as_ref() + .map(|t| t.as_str()) + .unwrap_or("Typst Document") + .to_owned(), + authors: doc.author.iter().map(|a| a.as_str().to_owned()).collect(), + language: "en".to_owned(), + summary: chapters, + ..Default::default() + }); + + Ok(()) + } + pub fn build(&mut self) -> ZResult<()> { let mut write_index = false; @@ -261,7 +320,8 @@ impl Project { include_bytes!("../../themes/mdbook/index.js"), )?; - for ch in self.iter_chapters() { + self.prepare_chapters(); + for ch in self.chapters.clone() { if let Some(path) = ch.get("path") { let raw_path: String = serde_json::from_value(path.clone()) .map_err(error_once_map_string!("retrieve path in book.toml", value: path))?; @@ -277,8 +337,10 @@ impl Project { write_index = true; } - // cleanup cache - comemo::evict(5); + if self.need_compile() { + // cleanup cache + comemo::evict(5); + } } } @@ -345,10 +407,19 @@ impl Project { } } - pub fn iter_chapters(&self) -> Vec { + pub fn prepare_chapters(&mut self) { + match self.meta_source { + MetaSource::Strict => { + self.chapters = self.generate_chapters(&self.book_meta.as_ref().unwrap().summary) + } + MetaSource::Outline => {} + } + } + + pub fn generate_chapters(&self, meta: &[BookMetaElem]) -> Vec { let mut chapters = vec![]; - for item in self.book_meta.as_ref().unwrap().summary.iter() { + for item in meta.iter() { self.iter_chapters_dfs(item, &mut chapters); } @@ -364,7 +435,9 @@ impl Project { // windows .replace('\\', "/"); - self.tr.compile_page(Path::new(path))?; + if self.need_compile() { + self.tr.compile_page(Path::new(path))?; + } let dynamic_load_trampoline = self .hr @@ -392,7 +465,7 @@ impl Project { .map_err(map_string_err("render_chapter,convert_to"))?; // inject chapters - data.insert("chapters".to_owned(), json!(self.iter_chapters())); + data.insert("chapters".to_owned(), json!(self.chapters)); let renderer_module = format!("{}renderer/typst_ts_renderer_bg.wasm", self.path_to_root); data.insert("renderer_module".to_owned(), json!(renderer_module)); @@ -411,6 +484,10 @@ impl Project { Ok(index_html) } + fn need_compile(&self) -> bool { + matches!(self.meta_source, MetaSource::Strict) + } + // pub fn auto_order_section(&mut self) { // fn dfs_elem(elem: &mut BookMetaElem, order: &mut Vec) { // match elem { diff --git a/cli/src/render/typst.rs b/cli/src/render/typst.rs index 5bc7be2..f9b85a6 100644 --- a/cli/src/render/typst.rs +++ b/cli/src/render/typst.rs @@ -1,11 +1,16 @@ use std::{ + cell::RefCell, + collections::HashMap, path::{Path, PathBuf}, + rc::Rc, sync::Arc, }; use crate::{ error::prelude::*, font::EMBEDDED_FONT, + meta::BookMetaElem, + outline::OutlineItem, utils::{make_absolute, make_absolute_from, UnwrapOrExit}, CompileArgs, }; @@ -18,10 +23,18 @@ use typst_ts_compiler::{ TypstSystemWorld, }; use typst_ts_core::{config::CompileOpts, path::PathClean, TakeAs, TypstAbs, TypstDocument}; -use typst_ts_svg_exporter::flat_ir::{LayoutRegionNode, PageMetadata}; +use typst_ts_svg_exporter::{ + flat_ir::{serialize_doc, LayoutRegionNode, Page, PageMetadata}, + Module, ModuleBuilder, MultiSvgDocument, +}; const THEME_LIST: [&str; 5] = ["light", "rust", "coal", "navy", "ayu"]; +#[derive(Debug, Clone, Default)] +pub struct CompilePageSetting { + pub with_outline: bool, +} + pub struct TypstRenderer { pub status_env: Arc, pub compiler: CompileReporter>, @@ -125,12 +138,387 @@ impl TypstRenderer { self.setup_entry(path); self.set_theme_target(""); - self.compiler - .pure_compile(&mut self.fork_env::()) - .map_err(|_| error_once!("compile book.typ")) + let res = self.compiler.pure_compile(&mut self.fork_env::()); + let res = self.report(res); + + res.ok_or_else(|| error_once!("compile book.typ")) + } + + pub fn compile_pages_by_outline(&mut self, path: &Path) -> ZResult> { + // compile entry file as a single webpage + self.compile_page_with(path, CompilePageSetting { with_outline: true })?; + self.setup_entry(path); + + let mut res = None; + for theme in THEME_LIST { + self.set_theme_target(theme); + let incoming = self.compile_pages_by_outline_(theme)?; + + // todo: compare incoming with res + res = Some(incoming); + } + + res.ok_or_else(|| error_once!("compile pages by outline")) + } + + fn compile_pages_by_outline_(&mut self, theme: &'static str) -> ZResult> { + // read ir from disk + let module_output = self.compiler_layer_mut().module_dest_path(); + let module_bin = std::fs::read(module_output).unwrap_or_exit(); + + let doc = MultiSvgDocument::from_slice(&module_bin); + // println!("layouts: {:#?}", doc.layouts); + + // todo(warning): warn if the relationship is not stable across layouts + // todo(warning): warn if there is a single layout + // todo: deduplicate layout if possible + + type PagesRef = Rc>>; + + #[derive(Default)] + struct ModuleInterner { + inner: ModuleBuilder, + pages_list: Vec>, + } + + #[derive(Debug)] + struct OutlineItemRef { + item: BookMetaElem, + pages: PagesRef, + children: Vec, + } + + struct OutlineChapter { + item: BookMetaElem, + content: Option, + children: Vec, + } + + struct BuiltOutline { + prefix: Option, + chapters: Vec, + } + impl BuiltOutline { + fn intern_pages( + interner: &mut Option, + module: &Module, + pages: &[Page], + page_idxs: impl Iterator, + ) { + let mut builder = interner.take().unwrap_or_default(); + let page_idxs = page_idxs.collect::>(); + for idx in &page_idxs { + builder.inner.intern(module, &pages[*idx - 1].content); + } + builder.pages_list.push(page_idxs); + *interner = Some(builder); + } + + fn init( + module: &Module, + builder: ItemRefBuilder, + pages: &[Page], + items: Vec, + ) -> BuiltOutline { + let mut prefix = None; + Self::intern_pages( + &mut prefix, + module, + pages, + builder.prefix.borrow().iter().cloned(), + ); + + let chapters = Self::init_items(module, pages, items); + + BuiltOutline { prefix, chapters } + } + + fn init_items( + module: &Module, + pages: &[Page], + items: Vec, + ) -> Vec { + items + .into_iter() + .map(|item| { + let mut content = None; + Self::intern_pages( + &mut content, + module, + pages, + item.pages.borrow().iter().cloned(), + ); + + OutlineChapter { + item: item.item, + content, + children: Self::init_items(module, pages, item.children), + } + }) + .collect() + } + + fn merge( + &mut self, + module: &Module, + builder: ItemRefBuilder, + pages: &[Page], + items: Vec, + ) { + Self::intern_pages( + &mut self.prefix, + module, + pages, + builder.prefix.borrow().iter().cloned(), + ); + + Self::merge_items(module, pages, &mut self.chapters, items); + } + + fn merge_items( + module: &Module, + pages: &[Page], + chapters: &mut Vec, + items: Vec, + ) { + if items.len() != chapters.len() { + panic!("cannot merge outline with different chapter count"); + } + for (idx, item) in items.into_iter().enumerate() { + let chapter = &mut chapters[idx]; + + if chapter.item != item.item { + panic!("cannot merge outline with different chapter"); + } + + Self::intern_pages( + &mut chapter.content, + module, + pages, + item.pages.borrow().iter().cloned(), + ); + + Self::merge_items(module, pages, &mut chapter.children, item.children); + } + } + } + + #[derive(Default)] + struct ItemRefBuilder { + prefix: PagesRef, + first: HashMap, + lasts: HashMap, + } + + impl ItemRefBuilder { + fn collect_item(&mut self, item: &OutlineItem) -> OutlineItemRef { + let pages = Rc::new(RefCell::new(Vec::new())); + + if let Some(pos) = item.position.as_ref() { + let page_no = pos.page_no; + self.first + .entry(page_no) + .or_insert_with(|| Rc::clone(&pages)); + self.lasts.insert(page_no, Rc::clone(&pages)); + } + + OutlineItemRef { + item: BookMetaElem::Chapter { + title: crate::meta::BookMetaContent::PlainText { + content: item.title.clone(), + }, + link: None, + sub: vec![], + section: None, + }, + pages: pages.clone(), + children: self.collect_items(&item.children), + } + } + + fn collect_items(&mut self, item: &[OutlineItem]) -> Vec { + item.iter() + .map(|item| self.collect_item(item)) + .collect::>() + } + } + + let mut built_outline: Option = None; + + for l in doc.layouts.iter() { + l.visit_pages(&mut |t| { + let mut builder = ItemRefBuilder::default(); + let outline = LayoutRegionNode::customs(&t.0) + .find(|(k, _)| k.as_ref() == "outline") + .unwrap(); + let outline = + serde_json::from_slice::(outline.1.as_ref()).unwrap(); + let items = builder.collect_items(&outline.items); + builder + .first + .entry(1) + .or_insert_with(|| Rc::clone(&builder.prefix)); + for idx in 1..=t.1.len() { + if let Some(pages) = builder.first.get(&idx) { + pages.borrow_mut().push(idx); + } else if let Some(pages) = builder.lasts.get(&idx) { + pages.borrow_mut().push(idx); + } + + if let Some(pages) = builder.lasts.get(&idx).cloned() { + builder.lasts.entry(idx + 1).or_insert(pages); + } + } + // println!("{:#?} of pages {:#?}", items, t.1); + if let Some(built_outline) = built_outline.as_mut() { + built_outline.merge(&doc.module, builder, &t.1, items); + } else { + built_outline = Some(BuiltOutline::init(&doc.module, builder, &t.1, items)); + } + }); + } + + let built_outline = built_outline.unwrap(); + + // todo: separate pages into multiple files + + #[derive(Default)] + struct SeparatedChapters { + theme: String, + content: HashMap, + } + + impl SeparatedChapters { + fn finalize_chapter( + interner: ModuleInterner, + origin: &MultiSvgDocument, + ) -> MultiSvgDocument { + let ModuleInterner { inner, pages_list } = interner; + let mut pages_list = pages_list.into_iter(); + let layouts = origin.layouts.iter().cloned().map(|l| { + l.mutate_pages(&mut |(meta, pages)| { + // delete outline + for c in meta { + if let PageMetadata::Custom(c) = c { + c.retain(|(k, _)| k.as_ref() != "outline"); + } + } + + let page_idxs = pages_list.next(); + if let Some(page_idxs) = page_idxs { + *pages = page_idxs + .into_iter() + .map(|idx| pages[idx - 1].clone()) + .collect::>(); + } + }) + }); + + // todo: deduplicate layout if possible + let _ = inner; + MultiSvgDocument { + module: origin.module.clone(), + layouts: layouts.collect(), + } + } + + fn finalize( + &mut self, + origin: MultiSvgDocument, + outline: BuiltOutline, + inferred: &mut Vec, + ) { + if let Some(prefix) = outline.prefix { + self.content.insert( + format!("pre.{theme}.multi.sir.in", theme = self.theme), + Self::finalize_chapter(prefix, &origin), + ); + inferred.push(BookMetaElem::Chapter { + title: crate::meta::BookMetaContent::PlainText { + content: "Preface".into(), + }, + link: Some("pre.typ".to_owned()), + sub: vec![], + section: None, + }); + + inferred.push(BookMetaElem::Separator {}); + } + + let mut numbering = vec![]; + self.finalize_items(&origin, outline.chapters, inferred, &mut numbering); + } + + fn finalize_items( + &mut self, + origin: &MultiSvgDocument, + items: Vec, + inferred: &mut Vec, + numbering: &mut Vec, + ) { + numbering.push(0); + for OutlineChapter { + mut item, + content, + children, + } in items + { + let BookMetaElem::Chapter { + title: _, + link, + sub, + section, + } = &mut item + else { + unreachable!(); + }; + + if let Some(prefix) = content { + let link_path = format!("{}", self.content.len()); + self.content.insert( + format!("{link_path}.{theme}.multi.sir.in", theme = self.theme), + Self::finalize_chapter(prefix, origin), + ); + *link = Some(format!("{}.typ", link_path)); + } + + *numbering.last_mut().unwrap() += 1; + self.finalize_items(origin, children, sub, numbering); + *section = Some( + numbering + .iter() + .map(|s| s.to_string()) + .collect::>() + .join("."), + ); + inferred.push(item); + } + numbering.pop(); + } + } + + let mut separated_chapters = SeparatedChapters { + theme: theme.to_owned(), + ..Default::default() + }; + let mut inferred = Vec::new(); + separated_chapters.finalize(doc, built_outline, &mut inferred); + + // write multiple files to disk + for chp in separated_chapters.content { + let mut path = self.dest_dir.clone(); + path.push(chp.0); + std::fs::write(path, serialize_doc(chp.1)).unwrap_or_exit(); + } + + Ok(inferred) } pub fn compile_page(&mut self, path: &Path) -> ZResult<()> { + self.compile_page_with(path, CompilePageSetting::default()) + } + + pub fn compile_page_with(&mut self, path: &Path, settings: CompilePageSetting) -> ZResult<()> { self.setup_entry(path); for theme in THEME_LIST { @@ -173,11 +561,18 @@ impl TypstRenderer { // println!("{:#?}", labels); let labels = serde_json::to_vec(&labels).unwrap_or_exit(); + let sema_label_meta = ("sema-label".into(), labels.into()); + + let mut custom = vec![sema_label_meta]; + + if settings.with_outline { + let outline = crate::outline::outline(&doc); + let outline = serde_json::to_vec(&outline).unwrap_or_exit(); + let outline_meta = ("outline".into(), outline.into()); + custom.push(outline_meta); + } - meta.push(PageMetadata::Custom(vec![( - "sema-label".into(), - labels.into(), - )])); + meta.push(PageMetadata::Custom(custom)); LayoutRegionNode::Pages(Arc::new((meta, pages))) });