diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-12-02 15:48:42 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-12-02 15:53:01 +0100 |
| commit | f57ce8643178c533a43c42baff3553f8613fb5ac (patch) | |
| tree | 4aca4f5636f1330963264fa3f1e1d5dca60df0ad /library/src/meta | |
| parent | 56923ee472f1eaa67d3543e19372823139205885 (diff) | |
Heading numbering and outline
Diffstat (limited to 'library/src/meta')
| -rw-r--r-- | library/src/meta/mod.rs | 2 | ||||
| -rw-r--r-- | library/src/meta/outline.rs | 142 |
2 files changed, 144 insertions, 0 deletions
diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs index 31a69ccc..4612274c 100644 --- a/library/src/meta/mod.rs +++ b/library/src/meta/mod.rs @@ -3,7 +3,9 @@ mod document; mod link; mod reference; +mod outline; pub use self::document::*; +pub use self::outline::*; pub use self::link::*; pub use self::reference::*; diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs new file mode 100644 index 00000000..b680a1ac --- /dev/null +++ b/library/src/meta/outline.rs @@ -0,0 +1,142 @@ +use crate::basics::HeadingNode; +use crate::layout::{BlockNode, HNode, HideNode, RepeatNode, Spacing}; +use crate::prelude::*; +use crate::text::{LinebreakNode, SpaceNode, TextNode}; + +/// A section outline (table of contents). +#[derive(Debug, Hash)] +pub struct OutlineNode; + +#[node(Prepare, Show)] +impl OutlineNode { + /// The title of the outline. + #[property(referenced)] + pub const TITLE: Option<Smart<Content>> = Some(Smart::Auto); + + /// The maximum depth up to which headings are included in the outline. + pub const DEPTH: Option<NonZeroUsize> = None; + + /// Whether to indent the subheadings to match their parents. + pub const INDENT: bool = false; + + /// The fill symbol. + #[property(referenced)] + pub const FILL: Option<EcoString> = Some('.'.into()); + + fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> { + Ok(Self.pack()) + } +} + +impl Prepare for OutlineNode { + fn prepare(&self, vt: &mut Vt, mut this: Content, _: StyleChain) -> Content { + let headings = vt + .locate(Selector::node::<HeadingNode>()) + .into_iter() + .map(|(_, node)| node) + .filter(|node| node.field("outlined").unwrap() == Value::Bool(true)) + .map(|node| Value::Content(node.clone())) + .collect(); + + this.push_field("headings", Value::Array(Array::from_vec(headings))); + this + } +} + +impl Show for OutlineNode { + fn show(&self, vt: &mut Vt, _: &Content, styles: StyleChain) -> Content { + let mut seq = vec![]; + if let Some(title) = styles.get(Self::TITLE) { + let body = title.clone().unwrap_or_else(|| { + TextNode::packed(match styles.get(TextNode::LANG) { + Lang::GERMAN => "Inhaltsverzeichnis", + Lang::ENGLISH | _ => "Contents", + }) + }); + + seq.push( + HeadingNode { body, level: NonZeroUsize::new(1).unwrap() } + .pack() + .styled(HeadingNode::NUMBERING, None) + .styled(HeadingNode::OUTLINED, false), + ); + } + + let indent = styles.get(Self::INDENT); + let depth = styles.get(Self::DEPTH); + + let mut ancestors: Vec<&Content> = vec![]; + for (_, node) in vt.locate(Selector::node::<HeadingNode>()) { + if node.field("outlined").unwrap() != Value::Bool(true) { + continue; + } + + let heading = node.to::<HeadingNode>().unwrap(); + if let Some(depth) = depth { + if depth < heading.level { + continue; + } + } + + while ancestors.last().map_or(false, |last| { + last.to::<HeadingNode>().unwrap().level >= heading.level + }) { + ancestors.pop(); + } + + // Adjust the link destination a bit to the topleft so that the + // heading is fully visible. + let mut loc = node.field("loc").unwrap().cast::<Location>().unwrap(); + loc.pos -= Point::splat(Abs::pt(10.0)); + + // Add hidden ancestors numberings to realize the indent. + if indent { + let text = ancestors + .iter() + .filter_map(|node| match node.field("numbers").unwrap() { + Value::Str(numbering) => { + Some(EcoString::from(numbering) + ' '.into()) + } + _ => None, + }) + .collect::<EcoString>(); + + if !text.is_empty() { + seq.push(HideNode(TextNode::packed(text)).pack()); + seq.push(SpaceNode.pack()); + } + } + + // Format the numbering. + let numbering = match node.field("numbers").unwrap() { + Value::Str(numbering) => { + TextNode::packed(EcoString::from(numbering) + ' '.into()) + } + _ => Content::empty(), + }; + + // Add the numbering and section name. + let start = numbering + heading.body.clone(); + seq.push(start.linked(Destination::Internal(loc))); + + // Add filler symbols between the section name and page number. + if let Some(filler) = styles.get(Self::FILL) { + seq.push(SpaceNode.pack()); + seq.push(RepeatNode(TextNode::packed(filler.clone())).pack()); + seq.push(SpaceNode.pack()); + } else { + let amount = Spacing::Fractional(Fr::one()); + seq.push(HNode { amount, weak: false }.pack()); + } + + // Add the page number and linebreak. + let end = TextNode::packed(format_eco!("{}", loc.page)); + seq.push(end.linked(Destination::Internal(loc))); + seq.push(LinebreakNode { justify: false }.pack()); + + ancestors.push(node); + } + + BlockNode(Content::sequence(seq)).pack() + } +} |
