Custom outline indenting (#1157)

This commit is contained in:
Pg Biel 2023-05-30 05:46:39 -03:00 committed by GitHub
parent 644bbf9914
commit 47f81f0da5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 197 additions and 28 deletions

View File

@ -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()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

View File

@ -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