Add outline.entry (#1423)

This commit is contained in:
Pg Biel 2023-06-12 06:50:59 -03:00 committed by GitHub
parent 4e4413e7be
commit c58a8fbd1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 42 deletions

View File

@ -47,10 +47,20 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem};
/// )
/// ```
///
/// ## Styling the outline { #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`]($func/outline.entry) element.
///
/// Display: Outline
/// Category: meta
/// Keywords: Table of Contents
#[element(Show, Finalize, LocalName)]
#[scope(
scope.define("entry", OutlineEntry::func());
scope
)]
pub struct OutlineElem {
/// The title of the outline.
///
@ -199,64 +209,29 @@ impl Show for OutlineElem {
let elems = vt.introspector.query(&self.target(styles).0);
for elem in &elems {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(self.span(), "cannot outline {}", elem.func().name());
let Some(entry) = OutlineEntry::from_outlinable(vt, self.span(), elem.clone().into_inner(), self.fill(styles))? else {
continue;
};
if depth < outlinable.level() {
let level = entry.level();
if depth < level {
continue;
}
let Some(outline) = outlinable.outline(vt)? else {
continue;
};
let location = elem.location().unwrap();
// 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>())
.map_or(false, |last| last.level() >= outlinable.level())
.map_or(false, |last| last.level() >= level)
{
ancestors.pop();
}
OutlineIndent::apply(&indent, vt, &ancestors, &mut seq, self.span())?;
// Add the outline of the element.
seq.push(outline.linked(Destination::Location(location)));
let page_numbering = vt
.introspector
.page_numbering(location)
.cast::<Option<Numbering>>()
.unwrap()
.unwrap_or_else(|| {
Numbering::Pattern(NumberingPattern::from_str("1").unwrap())
});
// Add filler symbols between the section name and page number.
if let Some(filler) = self.fill(styles) {
seq.push(SpaceElem::new().pack());
seq.push(
BoxElem::new()
.with_body(Some(filler.clone()))
.with_width(Fr::one().into())
.pack(),
);
seq.push(SpaceElem::new().pack());
} else {
seq.push(HElem::new(Fr::one().into()).pack());
}
// Add the page number and linebreak.
let page = Counter::new(CounterKey::Page)
.at(vt, location)?
.display(vt, &page_numbering)?;
seq.push(page.linked(Destination::Location(location)));
// Add the overridable outline entry, followed by a line break.
seq.push(entry.pack());
seq.push(LinebreakElem::new().pack());
ancestors.push(elem);
@ -399,3 +374,140 @@ cast! {
v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()),
v: Content => Self(v),
}
/// Represents each entry line in an outline, including the reference to the
/// outlined element, its page number, and the filler content between both.
///
/// This element is intended for use with show rules to control the appearance
/// of outlines.
///
/// ## Example { #example }
/// The example below shows how to style entries for top-level sections to make
/// them stand out.
///
/// ```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
/// ```
///
/// To completely customize an entry's line, you can also build it from
/// scratch by accessing the `level`, `element`, `outline`, and `fill`
/// fields on the entry.
///
/// Display: Outline Entry
/// Category: meta
#[element(Show)]
pub struct OutlineEntry {
/// The nesting level of this outline entry. Starts at `{1}` for top-level
/// entries.
#[required]
pub level: NonZeroUsize,
/// The element this entry refers to. Its location will be available
/// through the [`location`]($type/content.location) method on content
/// and can be [linked]($func/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.
///
/// Note that, when using show rules to override outline entries, it is
/// recommended to wrap the filling content in a [`box`]($func/box) with
/// fractional width. For example, `{box(width: 1fr, repeat[-])}` would show
/// precisely as many `-` characters as necessary to fill a particular gap.
#[required]
pub fill: Option<Content>,
}
impl OutlineEntry {
/// Generates an OutlineEntry from the given element, if possible
/// (errors if the element does not implement Outlinable).
/// If the element cannot be outlined (e.g. heading with 'outlined: false'),
/// does not generate an entry instance (returns Ok(None)).
fn from_outlinable(
vt: &mut Vt,
span: Span,
elem: Content,
fill: Option<Content>,
) -> SourceResult<Option<Self>> {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
let Some(body) = outlinable.outline(vt)? else {
return Ok(None);
};
Ok(Some(Self::new(outlinable.level(), elem, body, fill)))
}
}
impl Show for OutlineEntry {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
let mut seq = vec![];
let elem = self.element();
// In case a user constructs an outline entry with an arbitrary element.
let Some(location) = elem.location() else {
bail!(self.span(), "cannot outline {}", elem.func().name())
};
// The body text remains overridable.
seq.push(self.body().linked(Destination::Location(location)));
// Add filler symbols between the section name and page number.
if let Some(filler) = self.fill() {
seq.push(SpaceElem::new().pack());
seq.push(
BoxElem::new()
.with_body(Some(filler))
.with_width(Fr::one().into())
.pack(),
);
seq.push(SpaceElem::new().pack());
} else {
seq.push(HElem::new(Fr::one().into()).pack());
}
let page_numbering = vt
.introspector
.page_numbering(location)
.cast::<Option<Numbering>>()
.unwrap()
.unwrap_or_else(|| {
Numbering::Pattern(NumberingPattern::from_str("1").unwrap())
});
let page = Counter::new(CounterKey::Page)
.at(vt, location)?
.display(vt, &page_numbering)?;
// Add the page number.
seq.push(page.linked(Destination::Location(location)));
Ok(Content::sequence(seq))
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -0,0 +1,60 @@
// Tests 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)
#set text(8pt)
#show heading: set block(spacing: 0.65em)
= Introduction
= Background
== History
== State of the Art
= Analysis
== Setup
---
#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[--])
#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
---
// Error: 2-23 cannot outline cite
#outline(target: cite)
#cite("arrgh", "distress", [p. 22])
#bibliography("/works.bib")