diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index a060c175f..e247c3220 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -117,7 +117,7 @@ struct HeadingNode<'a> { impl<'a> HeadingNode<'a> { fn leaf(element: &'a Packed) -> Self { HeadingNode { - level: element.level(StyleChain::default()), + level: element.resolve_level(StyleChain::default()), // 'bookmarked' set to 'auto' falls back to the value of 'outlined'. bookmarked: element .bookmarked(StyleChain::default()) diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 26a26160f..8f8eaac47 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -695,7 +695,7 @@ impl<'a> Heading<'a> { } /// The section depth (number of equals signs). - pub fn level(self) -> NonZeroUsize { + pub fn depth(self) -> NonZeroUsize { self.0 .children() .find(|node| node.kind() == SyntaxKind::HeadingMarker) diff --git a/crates/typst/src/eval/markup.rs b/crates/typst/src/eval/markup.rs index d7d400e71..1bb12d493 100644 --- a/crates/typst/src/eval/markup.rs +++ b/crates/typst/src/eval/markup.rs @@ -208,9 +208,9 @@ impl Eval for ast::Heading<'_> { type Output = Content; fn eval(self, vm: &mut Vm) -> SourceResult { - let level = self.level(); + let depth = self.depth(); let body = self.body().eval(vm)?; - Ok(HeadingElem::new(body).with_level(level).pack()) + Ok(HeadingElem::new(body).with_depth(depth).pack()) } } diff --git a/crates/typst/src/foundations/func.rs b/crates/typst/src/foundations/func.rs index bb8399942..7871e2971 100644 --- a/crates/typst/src/foundations/func.rs +++ b/crates/typst/src/foundations/func.rs @@ -341,6 +341,13 @@ impl Func { /// Returns a selector that filters for elements belonging to this function /// whose fields have the values of the given arguments. + /// + /// ```example + /// #show heading.where(level: 2): set text(blue) + /// = Section + /// == Subsection + /// === Sub-subection + /// ``` #[func] pub fn where_( self, diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs index 19fc44ea1..e34640262 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -220,7 +220,7 @@ impl Show for Packed { seq.push( HeadingElem::new(title) - .with_level(NonZeroUsize::ONE) + .with_level(Smart::Custom(NonZeroUsize::ONE)) .pack() .spanned(self.span()), ); diff --git a/crates/typst/src/model/heading.rs b/crates/typst/src/model/heading.rs index b958438be..cd342bec7 100644 --- a/crates/typst/src/model/heading.rs +++ b/crates/typst/src/model/heading.rs @@ -17,7 +17,7 @@ use crate::util::{option_eq, NonZeroExt}; /// 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.) A top-level heading indicates a top-level section of the document +/// etc.) A 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, @@ -42,12 +42,54 @@ use crate::util::{option_eq, NonZeroExt}; /// # 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. +/// signs determines the heading's logical nesting depth. The `{offset}` field +/// can be set to configure the starting depth. #[elem(Locatable, Synthesize, Count, Show, ShowSet, LocalName, Refable, Outlinable)] pub struct HeadingElem { - /// The logical nesting depth of the heading, starting from one. + /// The absolute nesting depth of the heading, starting from one. If set + /// to `{auto}`, it is computed from `{offset + depth}`. + /// + /// This is primarily useful for usage in [show rules]($styling/#show-rules) + /// (either with [`where`]($function.where) selectors or by accessing the + /// level directly on a shown heading). + /// + /// ```example + /// #show heading.where(level: 2): set text(red) + /// + /// = Level 1 + /// == Level 2 + /// + /// #set heading(offset: 1) + /// = Also level 2 + /// == Level 3 + /// ``` + pub level: Smart, + + /// The relative nesting depth of the heading, starting from one. This is + /// combined with `{offset}` to compute the actual `{level}`. + /// + /// This is set by the heading syntax, such that `[== Heading]` creates a + /// heading with logical depth 2, but actual level `{offset + 2}`. If you + /// construct a heading manually, you should typically prefer this over + /// setting the absolute `level`. #[default(NonZeroUsize::ONE)] - pub level: NonZeroUsize, + pub depth: NonZeroUsize, + + /// The starting offset of each heading's level, used to turn its relative + /// `{depth}` into its absolute `{level}`. + /// + /// ```example + /// = Level 1 + /// + /// #set heading(offset: 1, numbering: "1.1") + /// = Level 2 + /// + /// #heading(offset: 2, depth: 2)[ + /// I'm level 4 + /// ] + /// ``` + #[default(0)] + pub offset: usize, /// How to number the heading. Accepts a /// [numbering pattern or function]($numbering). @@ -126,6 +168,15 @@ pub struct HeadingElem { pub body: Content, } +impl HeadingElem { + pub fn resolve_level(&self, styles: StyleChain) -> NonZeroUsize { + self.level(styles).unwrap_or_else(|| { + NonZeroUsize::new(self.offset(styles) + self.depth(styles).get()) + .expect("overflow to 0 on NoneZeroUsize + usize") + }) + } +} + impl Synthesize for Packed { fn synthesize( &mut self, @@ -140,7 +191,9 @@ impl Synthesize for Packed { } }; - self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); + let elem = self.as_mut(); + elem.push_level(Smart::Custom(elem.resolve_level(styles))); + elem.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); Ok(()) } } @@ -163,7 +216,7 @@ impl Show for Packed { impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { - let level = (**self).level(styles).get(); + let level = (**self).resolve_level(styles).get(); let scale = match level { 1 => 1.4, 2 => 1.2, @@ -189,7 +242,7 @@ impl Count for Packed { (**self) .numbering(StyleChain::default()) .is_some() - .then(|| CounterUpdate::Step((**self).level(StyleChain::default()))) + .then(|| CounterUpdate::Step((**self).resolve_level(StyleChain::default()))) } } @@ -236,7 +289,7 @@ impl Outlinable for Packed { } fn level(&self) -> NonZeroUsize { - (**self).level(StyleChain::default()) + (**self).resolve_level(StyleChain::default()) } } diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs index cb8d55638..bec98b7dc 100644 --- a/crates/typst/src/model/outline.rs +++ b/crates/typst/src/model/outline.rs @@ -197,7 +197,7 @@ impl Show for Packed { seq.push( HeadingElem::new(title) - .with_level(NonZeroUsize::ONE) + .with_depth(NonZeroUsize::ONE) .pack() .spanned(self.span()), ); diff --git a/docs/reference/scripting.md b/docs/reference/scripting.md index 9a04bfb9d..b66b98968 100644 --- a/docs/reference/scripting.md +++ b/docs/reference/scripting.md @@ -248,7 +248,7 @@ can be either: #let it = [= Heading] #it.body \ -#it.level +#it.depth ``` ## Methods diff --git a/tests/ref/meta/heading.png b/tests/ref/meta/heading.png index 28b419072..8467ea538 100644 Binary files a/tests/ref/meta/heading.png and b/tests/ref/meta/heading.png differ diff --git a/tests/typ/meta/heading.typ b/tests/typ/meta/heading.typ index 7db2a5cfd..a253913e6 100644 --- a/tests/typ/meta/heading.typ +++ b/tests/typ/meta/heading.typ @@ -45,6 +45,26 @@ multiline. ===== Heading 🌍 #heading(level: 5)[Heading] +--- +// Test setting the starting offset. +#set heading(numbering: "1.1") +#show heading.where(level: 2): set text(blue) += Level 1 + +#heading(depth: 1)[We're twins] +#heading(level: 1)[We're twins] + +== Real level 2 + +#set heading(offset: 1) += Fake level 2 +== Fake level 3 + +--- +// Passing level directly still overrides all other set values +#set heading(numbering: "1.1", offset: 1) +#heading(level: 1)[Still level 1] + --- // Edge cases. #set heading(numbering: "1.")