use typst::font::FontWeight; use crate::compute::NumberingPattern; use crate::layout::{BlockNode, VNode}; use crate::prelude::*; use crate::text::{SpaceNode, TextNode, TextSize}; /// # Heading /// A section heading. /// /// With headings, you can structure your document into sections. Each heading /// has a _level,_ which starts at one and is unbounded upwards. This level /// indicates the logical role of the following content (section, subsection, /// etc.) Top-level heading indicates a top-level section of the document (not /// the document's title). /// /// Typst can automatically number your headings for you. To enable numbering, /// specify how you want your headings to be numbered with a [numbering /// pattern](@numbering). /// /// Independently from the numbering, Typst can also automatically generate an /// [outline](@outline) of all headings for you. To exclude one or more headings /// from this outline, you can set the `outlined` parameter to `{false}`. /// /// ## Example /// ``` /// #set heading(numbering: "1.a)") /// /// = Introduction /// In recent years, ... /// /// == Preliminaries /// To start, ... /// ``` /// /// ## Syntax /// Headings have dedicated syntax: They can be created by starting a line with /// one or multiple equals signs, followed by a space. The number of equals /// signs determines the heading's logical nesting depth. /// /// ## Parameters /// - title: Content (positional, required) /// The heading's title. /// /// - level: NonZeroUsize (named) /// The logical nesting depth of the heading, starting from one. /// /// ## Category /// basics #[func] #[capable(Prepare, Show, Finalize)] #[derive(Debug, Hash)] pub struct HeadingNode { /// The logical nesting depth of the section, starting from one. In the /// default style, this controls the text size of the heading. pub level: NonZeroUsize, /// The heading's contents. pub title: Content, } #[node] impl HeadingNode { /// How to number the heading. Accepts a [numbering pattern](@numbering). /// /// # Example /// ``` /// #set heading(numbering: "1.a.") /// /// = A section /// == A subsection /// === A sub-subsection /// ``` #[property(referenced)] pub const NUMBERING: Option = None; /// Whether the heading should appear in the outline. /// /// # Example /// ``` /// #outline() /// /// #heading[Normal] /// This is a normal heading. /// /// #heading(outlined: false)[Hidden] /// This heading does not appear /// in the outline. /// ``` pub const OUTLINED: bool = true; fn construct(_: &Vm, args: &mut Args) -> SourceResult { Ok(Self { title: args.expect("title")?, level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()), } .pack()) } fn field(&self, name: &str) -> Option { match name { "level" => Some(Value::Int(self.level.get() as i64)), "title" => Some(Value::Content(self.title.clone())), _ => None, } } } impl Prepare for HeadingNode { fn prepare(&self, vt: &mut Vt, mut this: Content, styles: StyleChain) -> Content { let my_id = vt.identify(&this); let mut counter = HeadingCounter::new(); for (node_id, node) in vt.locate(Selector::node::()) { if node_id == my_id { break; } if matches!(node.field("numbers"), Some(Value::Str(_))) { let heading = node.to::().unwrap(); counter.advance(heading); } } let mut numbers = Value::None; if let Some(pattern) = styles.get(Self::NUMBERING) { numbers = Value::Str(pattern.apply(counter.advance(self)).into()); } this.push_field("outlined", Value::Bool(styles.get(Self::OUTLINED))); this.push_field("numbers", numbers); let meta = Meta::Node(my_id, this.clone()); this.styled(Meta::DATA, vec![meta]) } } impl Show for HeadingNode { fn show(&self, _: &mut Vt, this: &Content, _: StyleChain) -> SourceResult { let mut realized = self.title.clone(); if let Some(Value::Str(numbering)) = this.field("numbers") { realized = TextNode::packed(numbering) + SpaceNode.pack() + realized; } Ok(BlockNode(realized).pack()) } } impl Finalize for HeadingNode { fn finalize(&self, realized: Content) -> Content { let scale = match self.level.get() { 1 => 1.4, 2 => 1.2, _ => 1.0, }; let size = Em::new(scale); let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale; let below = Em::new(0.66) / scale; let mut map = StyleMap::new(); map.set(TextNode::SIZE, TextSize(size.into())); map.set(TextNode::WEIGHT, FontWeight::BOLD); map.set(BlockNode::ABOVE, VNode::block_around(above.into())); map.set(BlockNode::BELOW, VNode::block_around(below.into())); map.set(BlockNode::STICKY, true); realized.styled_with_map(map) } } /// Counters through headings with different levels. pub struct HeadingCounter(Vec); impl HeadingCounter { /// Create a new heading counter. pub fn new() -> Self { Self(vec![]) } /// Advance the counter and return the numbers for the given heading. pub fn advance(&mut self, heading: &HeadingNode) -> &[NonZeroUsize] { let level = heading.level.get(); if self.0.len() >= level { self.0[level - 1] = self.0[level - 1].saturating_add(1); self.0.truncate(level); } while self.0.len() < level { self.0.push(NonZeroUsize::new(1).unwrap()); } &self.0 } }