Rework outline (#5735)

This commit is contained in:
Laurenz 2025-01-23 12:50:51 +01:00 committed by GitHub
parent 1bd8ff0e0f
commit 52ee33a275
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 831 additions and 522 deletions

View File

@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length};
/// Space may be inserted between the instances of the body parameter, so be
/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly.
///
/// Errors if there no bounds on the available space, as it would create
/// Errors if there are no bounds on the available space, as it would create
/// infinite content.
///
/// # Example

View File

@ -229,35 +229,20 @@ impl Refable for Packed<EquationElem> {
}
impl Outlinable for Packed<EquationElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.block(StyleChain::default()) {
return Ok(None);
}
let Some(numbering) = self.numbering() else {
return Ok(None);
};
// After synthesis, this should always be custom content.
let mut supplement = match (**self).supplement(StyleChain::default()) {
Smart::Custom(Some(Supplement::Content(content))) => content,
_ => Content::empty(),
};
fn outlined(&self) -> bool {
self.block(StyleChain::default()) && self.numbering().is_some()
}
fn prefix(&self, numbers: Content) -> Content {
let supplement = self.supplement();
if !supplement.is_empty() {
supplement += TextElem::packed("\u{a0}");
supplement + TextElem::packed('\u{a0}') + numbers
} else {
numbers
}
}
let numbers = self.counter().display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
Ok(Some(supplement + numbers))
fn body(&self) -> Content {
Content::empty()
}
}

View File

@ -156,6 +156,7 @@ pub struct FigureElem {
pub scope: PlacementScope,
/// The figure's caption.
#[borrowed]
pub caption: Option<Packed<FigureCaption>>,
/// The kind of figure this is.
@ -305,7 +306,7 @@ impl Synthesize for Packed<FigureElem> {
));
// Fill the figure's caption.
let mut caption = elem.caption(styles);
let mut caption = elem.caption(styles).clone();
if let Some(caption) = &mut caption {
caption.synthesize(engine, styles)?;
caption.push_kind(kind.clone());
@ -331,7 +332,7 @@ impl Show for Packed<FigureElem> {
let mut realized = self.body.clone();
// Build the caption, if any.
if let Some(caption) = self.caption(styles) {
if let Some(caption) = self.caption(styles).clone() {
let (first, second) = match caption.position(styles) {
OuterVAlignment::Top => (caption.pack(), realized),
OuterVAlignment::Bottom => (realized, caption.pack()),
@ -423,46 +424,26 @@ impl Refable for Packed<FigureElem> {
}
impl Outlinable for Packed<FigureElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.outlined(StyleChain::default()) {
return Ok(None);
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
&& (self.caption(StyleChain::default()).is_some()
|| self.numbering().is_some())
}
fn prefix(&self, numbers: Content) -> Content {
let supplement = self.supplement();
if !supplement.is_empty() {
supplement + TextElem::packed('\u{a0}') + numbers
} else {
numbers
}
}
let Some(caption) = self.caption(StyleChain::default()) else {
return Ok(None);
};
let mut realized = caption.body.clone();
if let (
Smart::Custom(Some(Supplement::Content(mut supplement))),
Some(Some(counter)),
Some(numbering),
) = (
(**self).supplement(StyleChain::default()).clone(),
(**self).counter(),
self.numbering(),
) {
let numbers = counter.display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}');
}
let separator = caption.get_separator(StyleChain::default());
realized = supplement + numbers + separator + caption.body.clone();
}
Ok(Some(realized))
fn body(&self) -> Content {
self.caption(StyleChain::default())
.as_ref()
.map(|caption| caption.body.clone())
.unwrap_or_default()
}
}

View File

@ -1,7 +1,7 @@
use std::num::NonZeroUsize;
use ecow::eco_format;
use typst_utils::NonZeroExt;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{warning, SourceResult};
use crate::engine::Engine;
@ -13,8 +13,8 @@ use crate::html::{attr, tag, HtmlElem};
use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region};
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides};
use crate::model::{Numbering, Outlinable, Refable, Supplement};
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
/// A section heading.
@ -264,10 +264,6 @@ impl Show for Packed<HeadingElem> {
realized = numbering + spacing + realized;
}
if indent != Abs::zero() && !html {
realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
}
Ok(if html {
// HTML's h1 is closer to a title element. There should only be one.
// Meanwhile, a level 1 Typst heading is a section heading. For this
@ -294,8 +290,17 @@ impl Show for Packed<HeadingElem> {
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
}
} else {
let realized = BlockBody::Content(realized);
BlockElem::new().with_body(Some(realized)).pack().spanned(span)
let block = if indent != Abs::zero() {
let body = HElem::new((-indent).into()).pack() + realized;
let inset = Sides::default()
.with(TextElem::dir_in(styles).start(), Some(indent.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
block.pack().spanned(span)
})
}
}
@ -351,32 +356,21 @@ impl Refable for Packed<HeadingElem> {
}
impl Outlinable for Packed<HeadingElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.outlined(StyleChain::default()) {
return Ok(None);
}
let mut content = self.body.clone();
if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() {
let numbers = Counter::of(HeadingElem::elem()).display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
content = numbers + SpaceElem::shared().clone() + content;
};
Ok(Some(content))
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
}
fn level(&self) -> NonZeroUsize {
(**self).resolve_level(StyleChain::default())
}
fn prefix(&self, numbers: Content) -> Content {
numbers
}
fn body(&self) -> Content {
self.body.clone()
}
}
impl LocalName for Packed<HeadingElem> {

View File

@ -1,50 +1,61 @@
use std::num::NonZeroUsize;
use std::str::FromStr;
use comemo::Track;
use comemo::{Track, Tracked};
use smallvec::SmallVec;
use typst_syntax::Span;
use typst_utils::NonZeroExt;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{bail, At, SourceResult};
use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, select_where, Content, Context, Func, LocatableSelector,
NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func,
LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles,
};
use crate::introspection::{
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
};
use crate::introspection::{Counter, CounterKey, Locatable};
use crate::layout::{
BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing,
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
RepeatElem, Sides,
};
use crate::model::{
Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable,
};
use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
use crate::math::EquationElem;
use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
use crate::text::{LocalName, SpaceElem, TextElem};
/// A table of contents, figures, or other elements.
///
/// This function generates a list of all occurrences of an element in the
/// document, up to a given depth. The element's numbering and page number will
/// be displayed in the outline alongside its title or caption. By default this
/// generates a table of contents.
/// document, up to a given [`depth`]($outline.depth). The element's numbering
/// and page number will be displayed in the outline alongside its title or
/// caption.
///
/// # Example
/// ```example
/// #set heading(numbering: "1.")
/// #outline()
///
/// = Introduction
/// #lorem(5)
///
/// = Prior work
/// = Methods
/// == Setup
/// #lorem(10)
/// ```
///
/// # Alternative outlines
/// In its default configuration, this function generates a table of contents.
/// By setting the `target` parameter, the outline can be used to generate a
/// list of other kinds of elements than headings. In the example below, we list
/// all figures containing images by setting `target` to `{figure.where(kind:
/// image)}`. We could have also set it to just `figure`, but then the list
/// would also include figures containing tables or other material. For more
/// details on the `where` selector, [see here]($function.where).
/// list of other kinds of elements than headings.
///
/// In the example below, we list all figures containing images by setting
/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set
/// it to `{figure.where(kind: table)}` to generate a list of tables.
///
/// We could also set it to just `figure`, without using a [`where`]($function.where)
/// selector, but then the list would contain _all_ figures, be it ones
/// containing images, tables, or other material.
///
/// ```example
/// #outline(
@ -59,16 +70,89 @@ use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
/// ```
///
/// # Styling the outline
/// The outline element has several options for customization, such as its
/// `title` and `indent` parameters. If desired, however, it is possible to have
/// more control over the outline's look and style through the
/// [`outline.entry`]($outline.entry) element.
#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)]
/// At the most basic level, you can style the outline by setting properties on
/// it and its entries. This way, you can customize the outline's
/// [title]($outline.title), how outline entries are
/// [indented]($outline.indent), and how the space between an entry's text and
/// its page number should be [filled]($outline.entry.fill).
///
/// Richer customization is possible through configuration of the outline's
/// [entries]($outline.entry). The outline generates one entry for each outlined
/// element.
///
/// ## Spacing the entries { #entry-spacing }
/// Outline entries are [blocks]($block), so you can adjust the spacing between
/// them with normal block-spacing rules:
///
/// ```example
/// #show outline.entry.where(
/// level: 1
/// ): set block(above: 1.2em)
///
/// #outline()
///
/// = About ACME Corp.
/// == History
/// === Origins
/// = Products
/// == ACME Tools
/// ```
///
/// ## Building an outline entry from its parts { #building-an-entry }
/// For full control, you can also write a transformational show rule on
/// `outline.entry`. However, the logic for properly formatting and indenting
/// outline entries is quite complex and the outline entry itself only contains
/// two fields: The level and the outlined element.
///
/// For this reason, various helper functions are provided. You can mix and
/// match these to compose an entry from just the parts you like.
///
/// The default show rule for an outline entry looks like this[^1]:
/// ```typ
/// #show outline.entry: it => link(
/// it.element.location(),
/// it.indented(it.prefix(), it.inner()),
/// )
/// ```
///
/// - The [`indented`]($outline.entry.indented) function takes an optional
/// prefix and inner content and automatically applies the proper indentation
/// to it, such that different entries align nicely and long headings wrap
/// properly.
///
/// - The [`prefix`]($outline.entry.prefix) function formats the element's
/// numbering (if any). It also appends a supplement for certain elements.
///
/// - The [`inner`]($outline.entry.inner) function combines the element's
/// [`body`]($outline.entry.body), the filler, and the
/// [`page` number]($outline.entry.page).
///
/// You can use these individual functions to format the outline entry in
/// different ways. Let's say, you'd like to fully remove the filler and page
/// numbers. To achieve this, you could write a show rule like this:
///
/// ```example
/// #show outline.entry: it => link(
/// it.element.location(),
/// // Keep just the body, dropping
/// // the fill and the page.
/// it.indented(it.prefix(), it.body()),
/// )
///
/// #outline()
///
/// = About ACME Corp.
/// == History
/// ```
///
/// [^1]: The outline of equations is the exception to this rule as it does not
/// have a body and thus does not use indented layout.
#[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)]
pub struct OutlineElem {
/// The title of the outline.
///
/// - When set to `{auto}`, an appropriate title for the
/// [text language]($text.lang) will be used. This is the default.
/// [text language]($text.lang) will be used.
/// - When set to `{none}`, the outline will not have a title.
/// - A custom title can be set by passing content.
///
@ -79,8 +163,10 @@ pub struct OutlineElem {
/// The type of element to include in the outline.
///
/// To list figures containing a specific kind of element, like a table, you
/// can write `{figure.where(kind: table)}`.
/// To list figures containing a specific kind of element, like an image or
/// a table, you can specify the desired kind in a [`where`]($function.where)
/// selector. See the section on [alternative outlines]($outline/#alternative-outlines)
/// for more details.
///
/// ```example
/// #outline(
@ -97,7 +183,7 @@ pub struct OutlineElem {
/// caption: [Experiment results],
/// )
/// ```
#[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))]
#[default(LocatableSelector(HeadingElem::elem().select()))]
#[borrowed]
pub target: LocatableSelector,
@ -121,21 +207,22 @@ pub struct OutlineElem {
/// How to indent the outline's entries.
///
/// - `{none}`: No indent
/// - `{auto}`: Indents the numbering of the nested entry with the title of
/// its parent entry. This only has an effect if the entries are numbered
/// (e.g., via [heading numbering]($heading.numbering)).
/// - [Relative length]($relative): Indents the item by this length
/// multiplied by its nesting level. Specifying `{2em}`, for instance,
/// would indent top-level headings (not nested) by `{0em}`, second level
/// - `{auto}`: Indents the numbering/prefix of a nested entry with the
/// title of its parent entry. If the entries are not numbered (e.g., via
/// [heading numbering]($heading.numbering)), this instead simply inserts
/// a fixed amount of `{1.2em}` indent per level.
///
/// - [Relative length]($relative): Indents the entry by the specified
/// length per nesting level. Specifying `{2em}`, for instance, would
/// indent top-level headings by `{0em}` (not nested), second level
/// headings by `{2em}` (nested once), third-level headings by `{4em}`
/// (nested twice) and so on.
/// - [Function]($function): You can completely customize this setting with
/// a function. That function receives the nesting level as a parameter
/// (starting at 0 for top-level headings/elements) and can return a
/// relative length or content making up the indent. For example,
/// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while
/// `{n => [→ ] * n}` would indent with one arrow per nesting level.
///
/// - [Function]($function): You can further customize this setting with a
/// function. That function receives the nesting level as a parameter
/// (starting at 0 for top-level headings/elements) and should return a
/// (relative) length. For example, `{n => n * 2em}` would be equivalent
/// to just specifying `{2em}`.
///
/// ```example
/// #set heading(numbering: "1.a.")
@ -150,11 +237,6 @@ pub struct OutlineElem {
/// indent: 2em,
/// )
///
/// #outline(
/// title: [Contents (Function)],
/// indent: n => [→ ] * n,
/// )
///
/// = About ACME Corp.
/// == History
/// === Origins
@ -163,20 +245,7 @@ pub struct OutlineElem {
/// == Products
/// #lorem(10)
/// ```
#[default(None)]
#[borrowed]
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.
///
/// ```example
/// #outline(fill: line(length: 100%))
///
/// = A New Beginning
/// ```
#[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))]
pub fill: Option<Content>,
pub indent: Smart<OutlineIndent>,
}
#[scope]
@ -188,79 +257,52 @@ impl OutlineElem {
impl Show for Packed<OutlineElem> {
#[typst_macros::time(name = "outline", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![ParbreakElem::shared().clone()];
let span = self.span();
// Build the outline title.
let mut seq = vec![];
if let Some(title) = self.title(styles).unwrap_or_else(|| {
Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span()))
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(self.span()),
.spanned(span),
);
}
let indent = self.indent(styles);
let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap());
let mut ancestors: Vec<&Content> = vec![];
let elems = engine.introspector.query(&self.target(styles).0);
let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX);
for elem in &elems {
let Some(entry) = OutlineEntry::from_outlinable(
engine,
self.span(),
elem.clone(),
self.fill(styles),
styles,
)?
else {
continue;
// Build the outline entries.
for elem in elems {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
if depth < entry.level {
continue;
let level = outlinable.level();
if outlinable.outlined() && level <= depth {
let entry = OutlineEntry::new(level, elem);
seq.push(entry.pack().spanned(span));
}
// Deals with the ancestors of the current element.
// This is only applicable for elements with a hierarchy/level.
while ancestors
.last()
.and_then(|ancestor| ancestor.with::<dyn Outlinable>())
.is_some_and(|last| last.level() >= entry.level)
{
ancestors.pop();
}
OutlineIndent::apply(
indent,
engine,
&ancestors,
&mut seq,
styles,
self.span(),
)?;
// Add the overridable outline entry, followed by a line break.
seq.push(entry.pack().spanned(self.span()));
seq.push(LinebreakElem::shared().clone());
ancestors.push(elem);
}
seq.push(ParbreakElem::shared().clone());
Ok(Content::sequence(seq))
}
}
impl ShowSet for Packed<OutlineElem> {
fn show_set(&self, _: StyleChain) -> Styles {
fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(HeadingElem::set_outlined(false));
out.set(HeadingElem::set_numbering(None));
out.set(ParElem::set_first_line_indent(Em::new(0.0).into()));
out.set(ParElem::set_justify(false));
out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into())));
// Makes the outline itself available to its entries. Should be
// superseded by a proper ancestry mechanism in the future.
out.set(OutlineEntry::set_parent(Some(self.clone())));
out
}
}
@ -269,93 +311,29 @@ impl LocalName for Packed<OutlineElem> {
const KEY: &'static str = "outline";
}
/// Marks an element as being able to be outlined. This is used to implement the
/// `#outline()` element.
pub trait Outlinable: Refable {
/// Produce an outline item for this element.
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>>;
/// Returns the nesting level of this element.
fn level(&self) -> NonZeroUsize {
NonZeroUsize::ONE
}
}
/// Defines how an outline is indented.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum OutlineIndent {
Rel(Rel<Length>),
/// Indents by the specified length per level.
Rel(Rel),
/// Resolve the indent for a specific level through the given function.
Func(Func),
}
impl OutlineIndent {
fn apply(
indent: &Option<Smart<Self>>,
/// Resolve the indent for an entry with the given level.
fn resolve(
&self,
engine: &mut Engine,
ancestors: &Vec<&Content>,
seq: &mut Vec<Content>,
styles: StyleChain,
context: Tracked<Context>,
level: NonZeroUsize,
span: Span,
) -> SourceResult<()> {
match indent {
// 'none' | 'false' => no indenting
None => {}
// 'auto' | 'true' => use numbering alignment for indenting
Some(Smart::Auto) => {
// 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().display_at_loc(
engine,
ancestor.location().unwrap(),
styles,
numbering,
)?;
hidden += numbers + SpaceElem::shared().clone();
};
}
if !ancestors.is_empty() {
seq.push(HideElem::new(hidden).pack().spanned(span));
seq.push(SpaceElem::shared().clone().spanned(span));
}
}
// Length => indent with some fixed spacing per level
Some(Smart::Custom(OutlineIndent::Rel(length))) => {
seq.push(
HElem::new(Spacing::Rel(*length))
.pack()
.spanned(span)
.repeat(ancestors.len()),
);
}
// Function => call function with the current depth and take
// the returned content
Some(Smart::Custom(OutlineIndent::Func(func))) => {
let depth = ancestors.len();
let LengthOrContent(content) = func
.call(engine, Context::new(None, Some(styles)).track(), [depth])?
.cast()
.at(span)?;
if !content.is_empty() {
seq.push(content);
}
}
};
Ok(())
) -> SourceResult<Rel> {
let depth = level.get() - 1;
match self {
Self::Rel(length) => Ok(*length * depth as f64),
Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span),
}
}
}
@ -365,46 +343,33 @@ cast! {
Self::Rel(v) => v.into_value(),
Self::Func(v) => v.into_value()
},
v: Rel<Length> => OutlineIndent::Rel(v),
v: Func => OutlineIndent::Func(v),
v: Rel<Length> => Self::Rel(v),
v: Func => Self::Func(v),
}
struct LengthOrContent(Content);
/// Marks an element as being able to be outlined.
pub trait Outlinable: Refable {
/// Whether this element should be included in the outline.
fn outlined(&self) -> bool;
cast! {
LengthOrContent,
v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()),
v: Content => Self(v),
/// The nesting level of this element.
fn level(&self) -> NonZeroUsize {
NonZeroUsize::ONE
}
/// Constructs the default prefix given the formatted numbering.
fn prefix(&self, numbers: Content) -> Content;
/// The body of the entry.
fn body(&self) -> Content;
}
/// Represents each entry line in an outline, including the reference to the
/// outlined element, its page number, and the filler content between both.
/// Represents an entry line in an outline.
///
/// This element is intended for use with show rules to control the appearance
/// of outlines. To customize an entry's line, you can build it from scratch by
/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the
/// entry.
///
/// ```example
/// #set heading(numbering: "1.")
///
/// #show outline.entry.where(
/// level: 1
/// ): it => {
/// v(12pt, weak: true)
/// strong(it)
/// }
///
/// #outline(indent: auto)
///
/// = Introduction
/// = Background
/// == History
/// == State of the Art
/// = Analysis
/// == Setup
/// ```
#[elem(name = "entry", title = "Outline Entry", Show)]
/// With show-set and show rules on outline entries, you can richly customize
/// the outline's appearance. See the
/// [section on styling the outline]($outline/#styling-the-outline) for details.
#[elem(scope, name = "entry", title = "Outline Entry", Show)]
pub struct OutlineEntry {
/// The nesting level of this outline entry. Starts at `{1}` for top-level
/// entries.
@ -412,90 +377,206 @@ pub struct OutlineEntry {
pub level: NonZeroUsize,
/// The element this entry refers to. Its location will be available
/// through the [`location`]($content.location) method on content
/// through the [`location`]($content.location) method on the content
/// and can be [linked]($link) to.
#[required]
pub element: Content,
/// The content which is displayed in place of the referred element at its
/// entry in the outline. For a heading, this would be its number followed
/// by the heading's title, for example.
#[required]
pub body: Content,
/// The content used to fill the space between the element's outline and
/// its page number, as defined by the outline element this entry is
/// located in. When `{none}`, empty space is inserted in that gap instead.
/// Content to fill the space between the title and the page number. Can be
/// set to `{none}` to disable filling.
///
/// Note that, when using show rules to override outline entries, it is
/// recommended to wrap the filling content in a [`box`] with fractional
/// width. For example, `{box(width: 1fr, repeat[-])}` would show precisely
/// as many `-` characters as necessary to fill a particular gap.
#[required]
/// The `fill` will be placed into a fractionally sized box that spans the
/// space between the entry's body and the page number. When using show
/// rules to override outline entries, it is thus recommended to wrap the
/// fill in a [`box`] with fractional width, i.e.
/// `{box(width: 1fr, it.fill}`.
///
/// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
/// to tweak the visual weight of the fill.
///
/// ```example
/// #set outline.entry(fill: line(length: 100%))
/// #outline()
///
/// = A New Beginning
/// ```
#[borrowed]
#[default(Some(
RepeatElem::new(TextElem::packed("."))
.with_gap(Em::new(0.15).into())
.pack()
))]
pub fill: Option<Content>,
/// The page number of the element this entry links to, formatted with the
/// numbering set for the referenced page.
#[required]
pub page: Content,
}
impl OutlineEntry {
/// Generates an OutlineEntry from the given element, if possible (errors if
/// the element does not implement `Outlinable`). If the element should not
/// be outlined (e.g. heading with 'outlined: false'), does not generate an
/// entry instance (returns `Ok(None)`).
fn from_outlinable(
engine: &mut Engine,
span: Span,
elem: Content,
fill: Option<Content>,
styles: StyleChain,
) -> SourceResult<Option<Self>> {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
let Some(body) = outlinable.outline(engine, styles)? else {
return Ok(None);
};
let location = elem.location().unwrap();
let page_numbering = engine
.introspector
.page_numbering(location)
.cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
let page = Counter::new(CounterKey::Page).display_at_loc(
engine,
location,
styles,
&page_numbering,
)?;
Ok(Some(Self::new(outlinable.level(), elem, body, fill, page)))
}
/// Lets outline entries access the outline they are part of. This is a bit
/// of a hack and should be superseded by a proper ancestry mechanism.
#[ghost]
#[internal]
pub parent: Option<Packed<OutlineElem>>,
}
impl Show for Packed<OutlineEntry> {
#[typst_macros::time(name = "outline.entry", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![];
let elem = &self.element;
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let context = Context::new(None, Some(styles));
let context = context.track();
// In case a user constructs an outline entry with an arbitrary element.
let Some(location) = elem.location() else {
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
bail!(
self.span(), "{} must have a location", elem.func().name();
hint: "try using a query or a show rule to customize the outline.entry instead",
)
} else {
bail!(self.span(), "cannot outline {}", elem.func().name())
let prefix = self.prefix(engine, context, span)?;
let inner = self.inner(engine, context, span)?;
let block = if self.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner;
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span)
} else {
self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
};
let loc = self.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc)))
}
}
#[scope]
impl OutlineEntry {
/// A helper function for producing an indented entry layout: Lays out a
/// prefix and the rest of the entry in an indent-aware way.
///
/// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the
/// inner content of all entries at level `N` is aligned with the prefix of
/// all entries at with level `N + 1`, leaving at least `gap` space between
/// the prefix and inner parts. Furthermore, the `inner` contents of all
/// entries at the same level are aligned.
///
/// If the outline's indent is a fixed value or a function, the prefixes are
/// indented, but the inner contents are simply inset from the prefix by the
/// specified `gap`, rather than aligning outline-wide.
#[func(contextual)]
pub fn indented(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
/// The `prefix` is aligned with the `inner` content of entries that
/// have level one less.
///
/// In the default show rule, this is just to `it.prefix()`, but it can
/// be freely customized.
prefix: Option<Content>,
/// The formatted inner content of the entry.
///
/// In the default show rule, this is just to `it.inner()`, but it can
/// be freely customized.
inner: Content,
/// The gap between the prefix and the inner content.
#[named]
#[default(Em::new(0.5).into())]
gap: Length,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
let outline = Self::parent_in(styles)
.ok_or("must be called within the context of an outline")
.at(span)?;
let outline_loc = outline.location().unwrap();
let prefix_width = prefix
.as_ref()
.map(|prefix| measure_prefix(engine, prefix, outline_loc, styles))
.transpose()?;
let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles));
let indent = outline.indent(styles);
let (base_indent, hanging_indent) = match &indent {
Smart::Auto => compute_auto_indents(
engine.introspector,
outline_loc,
styles,
self.level,
prefix_inset,
),
Smart::Custom(amount) => {
let base = amount.resolve(engine, context, self.level, span)?;
(base, prefix_inset)
}
};
let body = if let (
Some(prefix),
Some(prefix_width),
Some(prefix_inset),
Some(hanging_indent),
) = (prefix, prefix_width, prefix_inset, hanging_indent)
{
// Save information about our prefix that other outline entries
// can query for (within `compute_auto_indent`) to align
// themselves).
let mut seq = Vec::with_capacity(5);
if indent.is_auto() {
seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack());
}
// Dedent the prefix by the amount of hanging indent and then skip
// ahead so that the inner contents are aligned.
seq.extend([
HElem::new((-hanging_indent).into()).pack(),
prefix,
HElem::new((hanging_indent - prefix_width).into()).pack(),
inner,
]);
Content::sequence(seq)
} else {
inner
};
let inset = Sides::default().with(
TextElem::dir_in(styles).start(),
Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())),
);
Ok(BlockElem::new()
.with_inset(inset)
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span))
}
/// Formats the element's numbering (if any).
///
/// This also appends the element's supplement in case of figures or
/// equations. For instance, it would output `1.1` for a heading, but
/// `Figure 1` for a figure, as is usual for outlines.
#[func(contextual)]
pub fn prefix(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Option<Content>> {
let outlinable = self.outlinable().at(span)?;
let Some(numbering) = outlinable.numbering() else { return Ok(None) };
let loc = self.element_location().at(span)?;
let styles = context.styles().at(span)?;
let numbers =
outlinable.counter().display_at_loc(engine, loc, styles, numbering)?;
Ok(Some(outlinable.prefix(numbers)))
}
/// Creates the default inner content of the entry.
///
/// This includes the body, the fill, and page number.
#[func(contextual)]
pub fn inner(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
let mut seq = vec![];
// Isolate the entry body in RTL because the page number is typically
// LTR. I'm not sure whether LTR should conceptually also be isolated,
// but in any case we don't do it for now because the text shaping
@ -511,32 +592,174 @@ impl Show for Packed<OutlineEntry> {
seq.push(TextElem::packed("\u{202B}"));
}
seq.push(self.body.clone().linked(Destination::Location(location)));
seq.push(self.body().at(span)?);
if rtl {
// "Pop Directional Formatting"
seq.push(TextElem::packed("\u{202C}"));
}
// Add filler symbols between the section name and page number.
if let Some(filler) = &self.fill {
// Add the filler between the section name and page number.
if let Some(filler) = self.fill(styles) {
seq.push(SpaceElem::shared().clone());
seq.push(
BoxElem::new()
.with_body(Some(filler.clone()))
.with_width(Fr::one().into())
.pack()
.spanned(self.span()),
.spanned(span),
);
seq.push(SpaceElem::shared().clone());
} else {
seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span()));
seq.push(HElem::new(Fr::one().into()).pack().spanned(span));
}
// Add the page number.
let page = self.page.clone().linked(Destination::Location(location));
seq.push(page);
// Add the page number. The word joiner in front ensures that the page
// number doesn't stand alone in its line.
seq.push(TextElem::packed("\u{2060}"));
seq.push(self.page(engine, context, span)?);
Ok(Content::sequence(seq))
}
/// The content which is displayed in place of the referred element at its
/// entry in the outline. For a heading, this is its
/// [`body`]($heading.body), for a figure a caption, and for equations it is
/// empty.
#[func]
pub fn body(&self) -> StrResult<Content> {
Ok(self.outlinable()?.body())
}
/// The page number of this entry's element, formatted with the numbering
/// set for the referenced page.
#[func(contextual)]
pub fn page(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Content> {
let loc = self.element_location().at(span)?;
let styles = context.styles().at(span)?;
let numbering = engine
.introspector
.page_numbering(loc)
.cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering)
}
}
impl OutlineEntry {
fn outlinable(&self) -> StrResult<&dyn Outlinable> {
self.element
.with::<dyn Outlinable>()
.ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
}
fn element_location(&self) -> HintedStrResult<Location> {
let elem = &self.element;
elem.location().ok_or_else(|| {
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
error!(
"{} must have a location", elem.func().name();
hint: "try using a show rule to customize the outline.entry instead",
)
} else {
error!("cannot outline {}", elem.func().name())
}
})
}
}
cast! {
OutlineEntry,
v: Content => v.unpack::<Self>().map_err(|_| "expected outline entry")?
}
/// Measures the width of a prefix.
fn measure_prefix(
engine: &mut Engine,
prefix: &Content,
loc: Location,
styles: StyleChain,
) -> SourceResult<Abs> {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
let link = LocatorLink::measure(loc);
Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)?
.width())
}
/// Compute the base indent and hanging indent for an auto-indented outline
/// entry of the given level, with the given prefix inset.
fn compute_auto_indents(
introspector: Tracked<Introspector>,
outline_loc: Location,
styles: StyleChain,
level: NonZeroUsize,
prefix_inset: Option<Abs>,
) -> (Rel, Option<Abs>) {
let indents = query_prefix_widths(introspector, outline_loc);
let fallback = Em::new(1.2).resolve(styles);
let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback);
let last = level.get() - 1;
let base: Abs = (0..last).map(get).sum();
let hang = prefix_inset.map(|p| p.max(get(last)));
(base.into(), hang)
}
/// Determines the maximum prefix inset (prefix width + gap) at each outline
/// level, for the outline with the given `loc`. Levels for which there is no
/// information available yield `None`.
#[comemo::memoize]
fn query_prefix_widths(
introspector: Tracked<Introspector>,
outline_loc: Location,
) -> SmallVec<[Option<Abs>; 4]> {
let mut widths = SmallVec::<[Option<Abs>; 4]>::new();
let elems = introspector.query(&select_where!(PrefixInfo, Key => outline_loc));
for elem in &elems {
let info = elem.to_packed::<PrefixInfo>().unwrap();
let level = info.level.get();
if widths.len() < level {
widths.resize(level, None);
}
widths[level - 1].get_or_insert(info.inset).set_max(info.inset);
}
widths
}
/// Helper type for introspection-based prefix alignment.
#[elem(Construct, Locatable, Show)]
struct PrefixInfo {
/// The location of the outline this prefix is part of. This is used to
/// scope prefix computations to a specific outline.
#[required]
key: Location,
/// The level of this prefix's entry.
#[required]
#[internal]
level: NonZeroUsize,
/// The width of the prefix, including the gap.
#[required]
#[internal]
inset: Abs,
}
impl Construct for PrefixInfo {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
impl Show for Packed<PrefixInfo> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}

View File

@ -1,4 +1,4 @@
use typst_utils::Numeric;
use typst_utils::{Get, Numeric};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
@ -7,7 +7,7 @@ use crate::foundations::{
Styles, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem};
use crate::text::TextElem;
@ -160,12 +160,7 @@ impl Show for Packed<TermsElem> {
children.push(StackChild::Block(Content::sequence(seq)));
}
let mut padding = Sides::default();
if TextElem::dir_in(styles) == Dir::LTR {
padding.left = pad.into();
} else {
padding.right = pad.into();
}
let padding = Sides::default().with(TextElem::dir_in(styles).start(), pad.into());
let mut realized = StackElem::new(children)
.with_spacing(Some(gutter.into()))

View File

@ -276,6 +276,15 @@ pub trait Get<Index> {
fn set(&mut self, index: Index, component: Self::Component) {
*self.get_mut(index) = component;
}
/// Builder-style method for setting a component.
fn with(mut self, index: Index, component: Self::Component) -> Self
where
Self: Sized,
{
self.set(index, component);
self
}
}
/// A numeric type.

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -289,9 +289,3 @@ HI#footnote.entry(clearance: 2.5em)[There]
)
#c
--- issue-5370-figure-caption-separator-outline ---
// Test that language-dependant caption separator is respected in outline.
#outline(title: none, target: figure)
#set text(lang: "ru")
#figure(rect(), caption: [Rectangle])

View File

@ -97,6 +97,18 @@ comment spans lines
= Fake level 2
== Fake level 3
--- heading-hanging-indent-auto ---
#set heading(numbering: "1.1.a.")
= State of the Art
--- heading-hanging-indent-zero ---
#set heading(numbering: "1.1.a.", hanging-indent: 0pt)
= State of the Art
--- heading-hanging-indent-length ---
#set heading(numbering: "1.1.a.", hanging-indent: 2em)
= State of the Art In Multi-Line
--- heading-offset-and-level ---
// Passing level directly still overrides all other set values
#set heading(numbering: "1.1", offset: 1)

View File

@ -1,10 +1,195 @@
--- outline ---
#set page(height: 200pt, margin: (bottom: 20pt), numbering: "1")
--- outline-spacing ---
#set heading(numbering: "1.a.")
#set outline.entry(fill: none)
#show outline.entry.where(level: 1): set block(above: 1.2em)
#outline()
#show heading: none
= A
== B
== C
= D
== E
--- outline-indent-auto ---
#set heading(numbering: "I.i.")
#set page(width: 150pt)
#show heading: none
#context test(outline.indent, auto)
#outline()
= A
== B
== C
== D
=== Title that breaks across lines
= E
== F
=== Aligned
--- outline-indent-auto-mixed-prefix ---
#show heading: none
#show outline.entry.where(level: 1): strong
#outline()
#set heading(numbering: "I.i.")
= A
== B
=== Title that breaks
= C
== D
= E
#[
#set heading(numbering: none)
= F
== Numberless title that breaks
=== G
]
= H
--- outline-indent-auto-mixed-prefix-short ---
#show heading: none
#outline()
#set heading(numbering: "I.i.")
= A
#set heading(numbering: none)
= B
--- outline-indent-auto-no-prefix ---
#show heading: none
#outline()
= A
== B
=== Title that breaks across lines
= C
== D
=== E
--- outline-indent-zero ---
#set heading(numbering: "1.a.")
#show heading: none
#outline(indent: 0pt)
= A
== B
=== C
==== Title that breaks across lines
#set heading(numbering: none)
== E
= F
--- outline-indent-fixed ---
#set heading(numbering: "1.a.")
#show heading: none
#outline(indent: 1em)
= A
== B
=== C
==== Title that breaks
#set heading(numbering: none)
== E
= F
--- outline-indent-func ---
#set heading(numbering: "1.a.")
#show heading: none
#outline(indent: n => (0pt, 1em, 2.5em, 3em).at(n))
= A
== B
=== C
==== Title breaks
#set heading(numbering: none)
== E
= F
--- outline-indent-bad-type ---
// Error: 2-35 expected relative length, found dictionary
#outline(indent: n => (a: "dict"))
= Heading
--- outline-entry ---
#set page(width: 150pt)
#set heading(numbering: "1.")
#show outline.entry.where(level: 1): set block(above: 12pt)
#show outline.entry.where(level: 1): strong
#outline(indent: auto)
#show heading: none
= Introduction
= Background
== History
== State of the Art
= Analysis
== Setup
--- outline-entry-complex ---
#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt))
#set heading(numbering: "1.")
#set outline.entry(fill: repeat[--])
#show outline.entry.where(level: 1): it => link(
it.element.location(),
it.indented(it.prefix(), {
emph(it.body())
[ ]
text(luma(100), box(width: 1fr, repeat[--·--]))
[ ]
it.page()
})
)
#counter(page).update(3)
#outline()
#show heading: none
= Top heading
== Not top heading
=== Lower heading
=== Lower too
== Also not top
#pagebreak()
#set page(numbering: "1")
= Another top heading
== Middle heading
=== Lower heading
--- outline-entry-inner ---
#set heading(numbering: "1.")
#show outline.entry: it => block(it.inner())
#show heading: none
#set outline.entry(fill: repeat[ -- ])
#outline()
= A
= B
--- outline-heading-start-of-page ---
#set page(width: 140pt, height: 200pt, margin: (bottom: 20pt), numbering: "1")
#set heading(numbering: "(1/a)")
#show heading.where(level: 1): set text(12pt)
#show heading.where(level: 2): set text(10pt)
#outline(fill: none)
#set outline.entry(fill: none)
#outline()
= A
= B
@ -23,66 +208,28 @@ A
== F
==== G
--- outline-bookmark ---
// Ensure that `bookmarked` option doesn't affect the outline
#set heading(numbering: "(I)", bookmarked: false)
#set outline.entry(fill: none)
#show heading: none
#outline()
= A
--- outline-styled-text ---
#outline(title: none)
= #text(blue)[He]llo
--- outline-bookmark ---
#outline(title: none, fill: none)
// Ensure 'bookmarked' option doesn't affect the outline
#set heading(numbering: "(I)", bookmarked: false)
= A
--- outline-indent-numbering ---
// With heading numbering
#set page(width: 200pt)
#set heading(numbering: "1.a.")
#show heading: none
#set outline(fill: none)
#context test(outline.indent, none)
#outline(indent: none)
#outline(indent: auto)
#outline(indent: 2em)
#outline(indent: n => ([-], [], [==], [====]).at(n))
= A
== B
== C
=== D
==== E
--- outline-indent-no-numbering ---
// Without heading numbering
#set page(width: 200pt)
#show heading: none
#set outline(fill: none)
#outline(indent: none)
#outline(indent: auto)
#outline(indent: n => 2em * n)
= About
== History
--- outline-indent-bad-type ---
// Error: 2-35 expected relative length or content, found dictionary
#outline(indent: n => (a: "dict"))
= Heading
--- outline-first-line-indent ---
#set par(first-line-indent: 1.5em)
#set heading(numbering: "1.1.a.")
#show outline.entry.where(level: 1): it => {
v(0.5em, weak: true)
strong(it)
}
#show outline.entry.where(level: 1): strong
#outline()
#show heading: none
= Introduction
= Background
== History
@ -90,85 +237,54 @@ A
= Analysis
== Setup
--- outline-entry ---
#set page(width: 150pt)
#set heading(numbering: "1.")
#show outline.entry.where(
level: 1
): it => {
v(12pt, weak: true)
strong(it)
}
#outline(indent: auto)
#v(1.2em, weak: true)
#set text(8pt)
#show heading: set block(spacing: 0.65em)
= Introduction
= Background
== History
== State of the Art
= Analysis
== Setup
--- outline-entry-complex ---
#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt))
#set heading(numbering: "1.")
#show outline.entry.where(level: 1): it => [
#let loc = it.element.location()
#let num = numbering(loc.page-numbering(), ..counter(page).at(loc))
#emph(link(loc, it.body))
#text(luma(100), box(width: 1fr, repeat[#it.fill.body;·]))
#link(loc, num)
]
#counter(page).update(3)
#outline(indent: auto, fill: repeat[--])
#v(1.2em, weak: true)
#set text(8pt)
#show heading: set block(spacing: 0.65em)
= Top heading
== Not top heading
=== Lower heading
=== Lower too
== Also not top
#pagebreak()
#set page(numbering: "1")
= Another top heading
== Middle heading
=== Lower heading
--- outline-bad-element ---
// Error: 2-27 cannot outline metadata
#outline(target: metadata)
#metadata("hello")
--- issue-2048-outline-multiline ---
// Without the word joiner between the dots and the page number,
// the page number would be alone in its line.
#set page(width: 125pt)
#set heading(numbering: "1.a.")
#show heading: none
#outline()
= A
== This just fits here
--- issue-2530-outline-entry-panic-text ---
// Outline entry (pre-emptive)
// Error: 2-48 cannot outline text
#outline.entry(1, [Hello], [World!], none, [1])
// Error: 2-27 cannot outline text
#outline.entry(1, [Hello])
--- issue-2530-outline-entry-panic-heading ---
// Outline entry (pre-emptive, improved error)
// Error: 2-55 heading must have a location
// Hint: 2-55 try using a query or a show rule to customize the outline.entry instead
#outline.entry(1, heading[Hello], [World!], none, [1])
// Error: 2-34 heading must have a location
// Hint: 2-34 try using a show rule to customize the outline.entry instead
#outline.entry(1, heading[Hello])
--- issue-4476-rtl-title-ending-in-ltr-text ---
--- issue-4476-outline-rtl-title-ending-in-ltr-text ---
#set text(lang: "he")
#outline()
#show heading: none
= הוקוס Pocus
= זוהי כותרת שתורגמה על ידי מחשב
--- issue-5176-cjk-title ---
--- issue-4859-outline-entry-show-set ---
#set heading(numbering: "1.a.")
#show outline.entry.where(level: 1): set outline.entry(fill: none)
#show heading: none
#outline()
= A
== B
--- issue-5176-outline-cjk-title ---
#set text(font: "Noto Serif CJK SC")
#show heading: none