diff --git a/library/src/lib.rs b/library/src/lib.rs index caf76ded9..ccdf448c7 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -211,6 +211,7 @@ fn items() -> LangItems { }, bibliography_keys: meta::BibliographyElem::keys, heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(), + heading_func: meta::HeadingElem::func(), list_item: |body| layout::ListItem::new(body).pack(), enum_item: |number, body| { let mut elem = layout::EnumItem::new(body); diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs index 42b32c9d0..1f1499fc2 100644 --- a/library/src/meta/figure.rs +++ b/library/src/meta/figure.rs @@ -173,14 +173,14 @@ impl Synthesize for FigureElem { // Determine the figure's kind. let kind = match self.kind(styles) { Smart::Auto => self - .find_figurable(vt, styles) + .find_figurable(styles) .map(|elem| FigureKind::Elem(elem.func())) .unwrap_or_else(|| FigureKind::Elem(ImageElem::func())), Smart::Custom(kind) => kind, }; let content = match &kind { - FigureKind::Elem(func) => self.find_of_elem(vt, *func), + FigureKind::Elem(func) => self.find_of_elem(*func), FigureKind::Name(_) => None, } .unwrap_or_else(|| self.body()); @@ -303,9 +303,9 @@ impl Refable for FigureElem { impl FigureElem { /// Determines the type of the figure by looking at the content, finding all /// [`Figurable`] elements and sorting them by priority then returning the highest. - pub fn find_figurable(&self, vt: &Vt, styles: StyleChain) -> Option { + pub fn find_figurable(&self, styles: StyleChain) -> Option { self.body() - .query(vt.introspector, Selector::can::()) + .query(Selector::can::()) .into_iter() .max_by_key(|elem| elem.with::().unwrap().priority(styles)) .cloned() @@ -313,9 +313,9 @@ impl FigureElem { /// Finds the element with the given function in the figure's content. /// Returns `None` if no element with the given function is found. - pub fn find_of_elem(&self, vt: &Vt, func: ElemFunc) -> Option { + pub fn find_of_elem(&self, func: ElemFunc) -> Option { self.body() - .query(vt.introspector, Selector::Elem(func, None)) + .query(Selector::Elem(func, None)) .into_iter() .next() .cloned() diff --git a/library/src/prelude.rs b/library/src/prelude.rs index fc71e2c2b..cb21a7329 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -23,9 +23,9 @@ pub use typst::geom::*; #[doc(no_inline)] pub use typst::model::{ element, Behave, Behaviour, Construct, Content, ElemFunc, Element, Finalize, Fold, - Introspector, Label, Locatable, LocatableSelector, Location, MetaElem, Resolve, - Selector, Set, Show, StabilityProvider, StyleChain, StyleVec, Styles, Synthesize, - Unlabellable, Vt, + Introspector, Label, Locatable, LocatableSelector, Location, MetaElem, PlainText, + Resolve, Selector, Set, Show, StabilityProvider, StyleChain, StyleVec, Styles, + Synthesize, Unlabellable, Vt, }; #[doc(no_inline)] pub use typst::syntax::{Span, Spanned}; diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs index e1d9c0f28..a707d1305 100644 --- a/library/src/text/misc.rs +++ b/library/src/text/misc.rs @@ -5,7 +5,7 @@ use crate::prelude::*; /// /// Display: Space /// Category: text -#[element(Unlabellable, Behave)] +#[element(Behave, Unlabellable, PlainText)] pub struct SpaceElem {} impl Behave for SpaceElem { @@ -16,6 +16,12 @@ impl Behave for SpaceElem { impl Unlabellable for SpaceElem {} +impl PlainText for SpaceElem { + fn plain_text(&self, text: &mut EcoString) { + text.push(' '); + } +} + /// Inserts a line break. /// /// Advances the paragraph to the next line. A single trailing line break at the diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs index 16268aad8..f4b3c0de4 100644 --- a/library/src/text/mod.rs +++ b/library/src/text/mod.rs @@ -40,7 +40,7 @@ use crate::prelude::*; /// /// Display: Text /// Category: text -#[element(Construct)] +#[element(Construct, PlainText)] pub struct TextElem { /// A prioritized sequence of font families. /// @@ -497,6 +497,12 @@ impl Construct for TextElem { } } +impl PlainText for TextElem { + fn plain_text(&self, text: &mut EcoString) { + text.push_str(&self.text()); + } +} + /// A lowercased font family like "arial". #[derive(Clone, Eq, PartialEq, Hash)] pub struct FontFamily(EcoString); diff --git a/src/eval/library.rs b/src/eval/library.rs index b93aa0dd8..0c635864a 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -72,6 +72,8 @@ pub struct LangItems { ) -> Vec<(EcoString, Option)>, /// A section heading: `= Introduction`. pub heading: fn(level: NonZeroUsize, body: Content) -> Content, + /// The heading function. + pub heading_func: ElemFunc, /// An item in a bullet list: `- ...`. pub list_item: fn(body: Content) -> Content, /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs index bdbb2bb76..8fc764715 100644 --- a/src/export/pdf/mod.rs +++ b/src/export/pdf/mod.rs @@ -13,7 +13,6 @@ use pdf_writer::types::Direction; use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr}; use xmp_writer::{LangId, RenditionClass, XmpWriter}; -use self::outline::HeadingNode; use self::page::Page; use crate::doc::{Document, Lang}; use crate::font::Font; @@ -54,7 +53,6 @@ pub struct PdfContext<'a> { image_map: Remapper, glyph_sets: HashMap>, languages: HashMap, - heading_tree: Vec, } impl<'a> PdfContext<'a> { @@ -76,36 +74,12 @@ impl<'a> PdfContext<'a> { image_map: Remapper::new(), glyph_sets: HashMap::new(), languages: HashMap::new(), - heading_tree: vec![], } } } /// Write the document catalog. fn write_catalog(ctx: &mut PdfContext) { - // Build the outline tree. - let outline_root_id = (!ctx.heading_tree.is_empty()).then(|| ctx.alloc.bump()); - let outline_start_ref = ctx.alloc; - let len = ctx.heading_tree.len(); - let mut prev_ref = None; - - for (i, node) in std::mem::take(&mut ctx.heading_tree).iter().enumerate() { - prev_ref = Some(outline::write_outline_item( - ctx, - node, - outline_root_id.unwrap(), - prev_ref, - i + 1 == len, - )); - } - - if let Some(outline_root_id) = outline_root_id { - let mut outline_root = ctx.writer.outline(outline_root_id); - outline_root.first(outline_start_ref); - outline_root.last(Ref::new(ctx.alloc.get() - 1)); - outline_root.count(ctx.heading_tree.len() as i32); - } - let lang = ctx .languages .iter() @@ -118,6 +92,9 @@ fn write_catalog(ctx: &mut PdfContext) { Direction::L2R }; + // Write the outline tree. + let outline_root_id = outline::write_outline(ctx); + // Write the document information. let mut info = ctx.writer.document_info(ctx.alloc.bump()); let mut xmp = XmpWriter::new(); diff --git a/src/export/pdf/outline.rs b/src/export/pdf/outline.rs index 1b335474b..03ca3b27e 100644 --- a/src/export/pdf/outline.rs +++ b/src/export/pdf/outline.rs @@ -1,32 +1,76 @@ -use ecow::EcoString; +use std::num::NonZeroUsize; + use pdf_writer::{Finish, Ref, TextStr}; use super::{AbsExt, PdfContext, RefExt}; -use crate::geom::{Abs, Point}; +use crate::geom::Abs; +use crate::model::Content; +use crate::util::NonZeroExt; + +/// Construct the outline for the document. +pub fn write_outline(ctx: &mut PdfContext) -> Option { + let mut tree: Vec = vec![]; + for heading in ctx.introspector.query(&item!(heading_func).select()) { + let leaf = HeadingNode::leaf(heading); + if let Some(last) = tree.last_mut() { + if last.try_insert(leaf.clone(), NonZeroUsize::ONE) { + continue; + } + } + + tree.push(leaf); + } + + if tree.is_empty() { + return None; + } + + let root_id = ctx.alloc.bump(); + let start_ref = ctx.alloc; + let len = tree.len(); + + let mut prev_ref = None; + for (i, node) in tree.iter().enumerate() { + prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len)); + } + + ctx.writer + .outline(root_id) + .first(start_ref) + .last(Ref::new(ctx.alloc.get() - 1)) + .count(tree.len() as i32); + + Some(root_id) +} /// A heading in the outline panel. #[derive(Debug, Clone)] -pub struct HeadingNode { - pub content: EcoString, - pub level: usize, - pub position: Point, - pub page: Ref, - pub children: Vec, +struct HeadingNode { + element: Content, + level: NonZeroUsize, + children: Vec, } impl HeadingNode { - pub fn len(&self) -> usize { + fn leaf(element: Content) -> Self { + HeadingNode { + level: element.expect_field::("level"), + element, + children: Vec::new(), + } + } + + fn len(&self) -> usize { 1 + self.children.iter().map(Self::len).sum::() } - #[allow(unused)] - pub fn try_insert(&mut self, child: Self, level: usize) -> bool { + fn try_insert(&mut self, child: Self, level: NonZeroUsize) -> bool { if level >= child.level { return false; } if let Some(last) = self.children.last_mut() { - if last.try_insert(child.clone(), level + 1) { + if last.try_insert(child.clone(), level.saturating_add(1)) { return true; } } @@ -37,7 +81,7 @@ impl HeadingNode { } /// Write an outline item and all its children. -pub fn write_outline_item( +fn write_outline_item( ctx: &mut PdfContext, node: &HeadingNode, parent_ref: Ref, @@ -65,12 +109,19 @@ pub fn write_outline_item( outline.count(-(node.children.len() as i32)); } - outline.title(TextStr(&node.content)); - outline.dest_direct().page(node.page).xyz( - node.position.x.to_f32(), - (node.position.y + Abs::pt(3.0)).to_f32(), - None, - ); + outline.title(TextStr(node.element.plain_text().trim())); + + let loc = node.element.location().unwrap(); + let pos = ctx.introspector.position(loc); + let index = pos.page.get() - 1; + if let Some(&height) = ctx.page_heights.get(index) { + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + outline.dest_direct().page(ctx.page_refs[index]).xyz( + pos.point.x.to_f32(), + height - y.to_f32(), + None, + ); + } outline.finish(); diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs index 27c6c2a4d..62816cbce 100644 --- a/src/ide/analyze.rs +++ b/src/ide/analyze.rs @@ -96,16 +96,16 @@ pub fn analyze_labels( // Labels in the document. for elem in introspector.all() { - let Some(label) = elem.label() else { continue }; + let Some(label) = elem.label().cloned() else { continue }; let details = elem .field("caption") - .or_else(|| elem.field("body")) .and_then(|field| match field { Value::Content(content) => Some(content), _ => None, }) - .and_then(|content| (items.text_str)(&content)); - output.push((label.clone(), details)); + .unwrap_or(elem) + .plain_text(); + output.push((label, Some(details))); } let split = output.len(); diff --git a/src/model/content.rs b/src/model/content.rs index 3f02369fe..4300790cd 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -3,12 +3,12 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::iter::Sum; use std::ops::{Add, AddAssign}; -use comemo::{Prehashed, Tracked}; +use comemo::Prehashed; use ecow::{eco_format, EcoString, EcoVec}; use super::{ - element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Introspector, Label, - Locatable, Location, Recipe, Selector, Style, Styles, Synthesize, + element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable, + Location, PlainText, Recipe, Selector, Style, Styles, Synthesize, }; use crate::diag::{SourceResult, StrResult}; use crate::doc::Meta; @@ -359,52 +359,53 @@ impl Content { /// Queries the content tree for all elements that match the given selector. /// - /// # Show rules /// Elements produced in `show` rules will not be included in the results. - pub fn query( - &self, - introspector: Tracked, - selector: Selector, - ) -> Vec<&Content> { + pub fn query(&self, selector: Selector) -> Vec<&Content> { let mut results = Vec::new(); - self.query_into(introspector, &selector, &mut results); + self.traverse(&mut |element| { + if selector.matches(element) { + results.push(element); + } + }); results } - /// Queries the content tree for all elements that match the given selector - /// and stores the results inside of the `results` vec. - fn query_into<'a>( - &'a self, - introspector: Tracked, - selector: &Selector, - results: &mut Vec<&'a Content>, - ) { - if selector.matches(self) { - results.push(self); - } + /// Extracts the plain text of this content. + pub fn plain_text(&self) -> EcoString { + let mut text = EcoString::new(); + self.traverse(&mut |element| { + if let Some(textable) = element.with::() { + textable.plain_text(&mut text); + } + }); + text + } + + /// Traverse this content. + fn traverse<'a, F>(&'a self, f: &mut F) + where + F: FnMut(&'a Content), + { + f(self); for attr in &self.attrs { match attr { - Attr::Child(child) => child.query_into(introspector, selector, results), - Attr::Value(value) => walk_value(introspector, value, selector, results), + Attr::Child(child) => child.traverse(f), + Attr::Value(value) => walk_value(value, f), _ => {} } } /// Walks a given value to find any content that matches the selector. - fn walk_value<'a>( - introspector: Tracked, - value: &'a Value, - selector: &Selector, - results: &mut Vec<&'a Content>, - ) { + fn walk_value<'a, F>(value: &'a Value, f: &mut F) + where + F: FnMut(&'a Content), + { match value { - Value::Content(content) => { - content.query_into(introspector, selector, results) - } + Value::Content(content) => content.traverse(f), Value::Array(array) => { for value in array { - walk_value(introspector, value, selector, results); + walk_value(value, f); } } _ => {} diff --git a/src/model/element.rs b/src/model/element.rs index 4c825a202..e26848b11 100644 --- a/src/model/element.rs +++ b/src/model/element.rs @@ -151,3 +151,9 @@ impl Debug for Label { /// Indicates that an element cannot be labelled. pub trait Unlabellable {} + +/// Tries to extract the plain-text representation of the element. +pub trait PlainText { + /// Write this element's plain text into the given buffer. + fn plain_text(&self, text: &mut EcoString); +} diff --git a/tests/ref/meta/outline.png b/tests/ref/meta/outline.png index 5d05b015c..c485ca9e2 100644 Binary files a/tests/ref/meta/outline.png and b/tests/ref/meta/outline.png differ diff --git a/tests/typ/meta/outline.typ b/tests/typ/meta/outline.typ index 83b2e9067..9f45b2f32 100644 --- a/tests/typ/meta/outline.typ +++ b/tests/typ/meta/outline.typ @@ -32,5 +32,5 @@ Ok ... #set heading(numbering: "(I)") -= Zusammenfassung += #text(blue)[Zusammen]fassung #lorem(10)