diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 04a926510..089eceb24 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -5,7 +5,7 @@ use typst::util::option_eq; use super::{ Counter, CounterKey, HeadingElem, LocalName, Numbering, NumberingPattern, Refable, }; -use crate::layout::{BoxElem, HElem, HideElem, ParbreakElem, RepeatElem}; +use crate::layout::{BoxElem, HElem, HideElem, ParbreakElem, RepeatElem, Spacing}; use crate::prelude::*; use crate::text::{LinebreakElem, SpaceElem, TextElem}; @@ -110,13 +110,54 @@ pub struct OutlineElem { /// ``` pub depth: Option, - /// Whether to indent the sub-elements to align the start of their numbering - /// with the title of their parents. This will only have an effect if a - /// [heading numbering]($func/heading.numbering) is set. + /// How to indent the outline's entry lines. This defaults to `{none}`, + /// which does not apply any indentation at all upon the outline's entries, + /// which will then all be placed at the start of each line. + /// + /// If this option is set to `{auto}`, each entry (usually headings) will + /// be indented enough to align with the last character of its parent's + /// numbering. For example, if the parent entry is "3. Top heading" and the + /// child entry is "3.1. Inner heading", the end result is that the child + /// entry's line will begin where the "3." from the parent ends (after the + /// last dot), but below. Consequently, if you specify `{auto}` indentation, + /// you will only see a visible effect if a + /// [heading numbering]($func/heading.numbering) is set for your headings + /// (if using headings), or, in general, if your entries have some form of + /// automatic numbering (generated by Typst) enabled. + /// + /// Note that specifying `{true}` (equivalent to `{auto}`) or `{false}` + /// (equivalent to `{none}`) for this option is deprecated and will be + /// removed in a future release. Please use `{none}` for no indentation + /// or `{auto}` for automatic indentation instead. + /// + /// Alternatively, you may specify a custom indentation method, which + /// doesn't depend on numbering. Setting this option to a length, such as + /// `{2em}`, will indent each nested level by that much length, multiplied + /// by the current nesting level (so a top-level heading, nested 0 times, + /// wouldn't be indented at all; a heading nested once would be `{2em}` + /// away from the start of the line, a heading nested twice would be + /// `{4em}` away, and so on). + /// + /// It is also possible to set this option to a function, allowing for a + /// more complete customization of the indentation. A function is expected + /// to take a single parameter indcating the current nesting level + /// (starting at `{0}` for top-level headings/elements), and return the + /// indentation option for that level (or `{none}`). Such a function could + /// be, for example, {n => n * 2em}` (indenting by `{2em}` times the + /// nesting level), or `{n => [*!*] * n * n}` (indenting by a bold + /// exclamation mark times the nesting level squared). Please note that the + /// function is also called for nesting level 0, so be careful to not + /// return a fixed value if you don't want to accidentally indent top-level + /// entries by it (if that's not your intention), which you can solve by + /// returning `{none}` when the received parameter is equal to `{0}`. + /// /// /// ```example /// #set heading(numbering: "1.a.") - /// #outline(indent: true) + /// + /// #outline(title: "Contents (Automatic indentation)", indent: auto) + /// #outline(title: "Contents (Length indentation)", indent: 2em) + /// #outline(title: "Contents (Function indentation)", indent: n => [*!*] * n * n) /// /// = About ACME Corp. /// @@ -126,8 +167,8 @@ pub struct OutlineElem { /// == Products /// #lorem(10) /// ``` - #[default(false)] - pub indent: bool, + #[default(None)] + pub indent: Option>, /// Content to fill the space between the title and the page number. Can be /// set to `none` to disable filling. @@ -190,27 +231,7 @@ impl Show for OutlineElem { ancestors.pop(); } - // Add hidden ancestors numberings to realize the indent. - if indent { - let mut hidden = Content::empty(); - for ancestor in &ancestors { - let ancestor_outlinable = ancestor.with::().unwrap(); - - if let Some(numbering) = ancestor_outlinable.numbering() { - let numbers = ancestor_outlinable - .counter() - .at(vt, ancestor.location().unwrap())? - .display(vt, &numbering)?; - - hidden += numbers + SpaceElem::new().pack(); - }; - } - - if !ancestors.is_empty() { - seq.push(HideElem::new(hidden).pack()); - seq.push(SpaceElem::new().pack()); - } - } + OutlineIndent::apply(&indent, vt, &ancestors, &mut seq, self.span())?; // Add the outline of the element. seq.push(outline.linked(Destination::Location(location))); @@ -301,3 +322,91 @@ pub trait Outlinable: Refable { NonZeroUsize::ONE } } + +#[derive(Debug, Clone)] +pub enum OutlineIndent { + Bool(bool), + Length(Spacing), + Function(Func), +} + +impl OutlineIndent { + fn apply( + indent: &Option>, + vt: &mut Vt, + ancestors: &Vec<&Content>, + seq: &mut Vec, + span: Span, + ) -> SourceResult<()> { + match &indent { + // 'none' | 'false' => no indenting + None | Some(Smart::Custom(OutlineIndent::Bool(false))) => {} + + // 'auto' | 'true' => use numbering alignment for indenting + Some(Smart::Auto | Smart::Custom(OutlineIndent::Bool(true))) => { + // Add hidden ancestors numberings to realize the indent. + let mut hidden = Content::empty(); + for ancestor in ancestors { + let ancestor_outlinable = ancestor.with::().unwrap(); + + if let Some(numbering) = ancestor_outlinable.numbering() { + let numbers = ancestor_outlinable + .counter() + .at(vt, ancestor.location().unwrap())? + .display(vt, &numbering)?; + + hidden += numbers + SpaceElem::new().pack(); + }; + } + + if !ancestors.is_empty() { + seq.push(HideElem::new(hidden).pack()); + seq.push(SpaceElem::new().pack()); + } + } + + // Length => indent with some fixed spacing per level + Some(Smart::Custom(OutlineIndent::Length(length))) => { + let Ok(depth): Result = ancestors.len().try_into() else { + bail!(span, "outline element depth too large"); + }; + + let hspace = HElem::new(*length).pack().repeat(depth).unwrap(); + seq.push(hspace); + } + + // Function => call function with the current depth and take + // the returned content + Some(Smart::Custom(OutlineIndent::Function(func))) => { + let depth = ancestors.len(); + let returned = func.call_vt(vt, [depth.into()])?; + let Ok(returned) = returned.cast::() else { + bail!( + span, + "indent function must return content" + ); + }; + if !returned.is_empty() { + seq.push(returned); + } + } + }; + + Ok(()) + } +} + +cast_from_value! { + OutlineIndent, + b: bool => OutlineIndent::Bool(b), + s: Spacing => OutlineIndent::Length(s), + f: Func => OutlineIndent::Function(f), +} + +cast_to_value! { + v: OutlineIndent => match v { + OutlineIndent::Bool(b) => b.into(), + OutlineIndent::Length(s) => s.into(), + OutlineIndent::Function(f) => f.into() + } +} diff --git a/tests/ref/meta/outline-indent.png b/tests/ref/meta/outline-indent.png new file mode 100644 index 000000000..816d86a5e Binary files /dev/null and b/tests/ref/meta/outline-indent.png differ diff --git a/tests/typ/meta/outline-indent.typ b/tests/typ/meta/outline-indent.typ new file mode 100644 index 000000000..a4ef6c811 --- /dev/null +++ b/tests/typ/meta/outline-indent.typ @@ -0,0 +1,60 @@ +// Tests outline 'indent' option. + +--- +// With heading numbering +#set page(width: 200pt) +#set heading(numbering: "1.a.") +#outline() +#outline(indent: false) +#outline(indent: true) +#outline(indent: none) +#outline(indent: auto) +#outline(indent: 2em) +#outline(indent: n => ([-], [], [==], [====]).at(n)) +#outline(indent: n => "!" * calc.pow(2, n)) + += About ACME Corp. + +== History +#lorem(10) + +== Products +#lorem(10) + +=== Categories +#lorem(10) + +==== General +#lorem(10) + +--- +// Without heading numbering +#set page(width: 200pt) +#outline() +#outline(indent: false) +#outline(indent: true) +#outline(indent: none) +#outline(indent: auto) +#outline(indent: 2em) +#outline(indent: n => ([-], [], [==], [====]).at(n)) +#outline(indent: n => "!" * calc.pow(2, n)) + += About ACME Corp. + +== History +#lorem(10) + +== Products +#lorem(10) + +=== Categories +#lorem(10) + +==== General +#lorem(10) + +--- +// Error: 2-35 indent function must return content +#outline(indent: n => (a: "dict")) + += Heading