mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Custom outline indenting (#1157)
This commit is contained in:
parent
644bbf9914
commit
47f81f0da5
@ -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()
|
||||
}
|
||||
}
|
||||
|
BIN
tests/ref/meta/outline-indent.png
Normal file
BIN
tests/ref/meta/outline-indent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 303 KiB |
60
tests/typ/meta/outline-indent.typ
Normal file
60
tests/typ/meta/outline-indent.typ
Normal 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
|
Loading…
x
Reference in New Issue
Block a user