summaryrefslogtreecommitdiff
path: root/library
diff options
context:
space:
mode:
authorPg Biel <9021226+PgBiel@users.noreply.github.com>2023-05-30 05:46:39 -0300
committerGitHub <noreply@github.com>2023-05-30 10:46:39 +0200
commit47f81f0da57fb36214d2db19e425f9d73309e10a (patch)
tree528a4ecd1b4106975fe5b8f42151bf512147086c /library
parent644bbf99145042266ea8cf2890cef5771bd04be4 (diff)
Custom outline indenting (#1157)
Diffstat (limited to 'library')
-rw-r--r--library/src/meta/outline.rs165
1 files changed, 137 insertions, 28 deletions
diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs
index 04a92651..089eceb2 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<NonZeroUsize>,
- /// 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<Smart<OutlineIndent>>,
/// 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::<dyn Outlinable>().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<Smart<Self>>,
+ vt: &mut Vt,
+ ancestors: &Vec<&Content>,
+ seq: &mut Vec<Content>,
+ 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::<dyn Outlinable>().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<i64, _> = 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::<Content>() 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()
+ }
+}