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::{
|
use super::{
|
||||||
Counter, CounterKey, HeadingElem, LocalName, Numbering, NumberingPattern, Refable,
|
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::prelude::*;
|
||||||
use crate::text::{LinebreakElem, SpaceElem, TextElem};
|
use crate::text::{LinebreakElem, SpaceElem, TextElem};
|
||||||
|
|
||||||
@ -110,13 +110,54 @@ pub struct OutlineElem {
|
|||||||
/// ```
|
/// ```
|
||||||
pub depth: Option<NonZeroUsize>,
|
pub depth: Option<NonZeroUsize>,
|
||||||
|
|
||||||
/// Whether to indent the sub-elements to align the start of their numbering
|
/// How to indent the outline's entry lines. This defaults to `{none}`,
|
||||||
/// with the title of their parents. This will only have an effect if a
|
/// which does not apply any indentation at all upon the outline's entries,
|
||||||
/// [heading numbering]($func/heading.numbering) is set.
|
/// 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
|
/// ```example
|
||||||
/// #set heading(numbering: "1.a.")
|
/// #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.
|
/// = About ACME Corp.
|
||||||
///
|
///
|
||||||
@ -126,8 +167,8 @@ pub struct OutlineElem {
|
|||||||
/// == Products
|
/// == Products
|
||||||
/// #lorem(10)
|
/// #lorem(10)
|
||||||
/// ```
|
/// ```
|
||||||
#[default(false)]
|
#[default(None)]
|
||||||
pub indent: bool,
|
pub indent: Option<Smart<OutlineIndent>>,
|
||||||
|
|
||||||
/// Content to fill the space between the title and the page number. Can be
|
/// Content to fill the space between the title and the page number. Can be
|
||||||
/// set to `none` to disable filling.
|
/// set to `none` to disable filling.
|
||||||
@ -190,27 +231,7 @@ impl Show for OutlineElem {
|
|||||||
ancestors.pop();
|
ancestors.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add hidden ancestors numberings to realize the indent.
|
OutlineIndent::apply(&indent, vt, &ancestors, &mut seq, self.span())?;
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the outline of the element.
|
// Add the outline of the element.
|
||||||
seq.push(outline.linked(Destination::Location(location)));
|
seq.push(outline.linked(Destination::Location(location)));
|
||||||
@ -301,3 +322,91 @@ pub trait Outlinable: Refable {
|
|||||||
NonZeroUsize::ONE
|
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