typst_library/model/outline.rs
1use std::num::NonZeroUsize;
2use std::str::FromStr;
3
4use comemo::Tracked;
5use smallvec::SmallVec;
6use typst_syntax::Span;
7use typst_utils::{Get, NonZeroExt};
8
9use crate::diag::{At, HintedStrResult, SourceResult, StrResult, bail, error};
10use crate::engine::Engine;
11use crate::foundations::{
12 Args, Construct, Content, Context, Func, LocatableSelector, NativeElement, Packed,
13 Resolve, ShowSet, Smart, StyleChain, Styles, cast, elem, func, scope, select_where,
14};
15use crate::introspection::{
16 Counter, CounterKey, Locatable, Location, Locator, LocatorLink,
17 PageNumberingIntrospection, QueryIntrospection, Tagged, Unqueriable,
18};
19use crate::layout::{
20 Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
21 RepeatElem, Sides,
22};
23use crate::model::{HeadingElem, NumberingPattern, ParElem, Refable};
24use crate::pdf::PdfMarkerTag;
25use crate::text::{LocalName, SpaceElem, TextElem};
26
27/// A table of contents, figures, or other elements.
28///
29/// This function generates a list of all occurrences of an element in the
30/// document, up to a given @outline.depth[`depth`]. The element's numbering and
31/// page number will be displayed in the outline alongside its title or caption.
32///
33/// = Example <example>
34/// ```example
35/// #set heading(numbering: "1.")
36/// #outline()
37///
38/// = Introduction
39/// #lorem(5)
40///
41/// = Methods
42/// == Setup
43/// #lorem(10)
44/// ```
45///
46/// = Alternative outlines <alternative-outlines>
47/// In its default configuration, this function generates a table of contents.
48/// By setting the `target` parameter, the outline can be used to generate a
49/// list of other kinds of elements than headings.
50///
51/// In the example below, we list all figures containing images by setting
52/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set
53/// it to `{figure.where(kind: table)}` to generate a list of tables.
54///
55/// We could also set it to just `figure`, without using a
56/// @function.where[`where`] selector, but then the list would contain _all_
57/// figures, be it ones containing images, tables, or other material.
58///
59/// ```example
60/// #outline(
61/// title: [List of Figures],
62/// target: figure.where(kind: image),
63/// )
64///
65/// #figure(
66/// image("tiger.jpg"),
67/// caption: [A nice figure!],
68/// )
69/// ```
70///
71/// = Styling the outline <styling-the-outline>
72/// At the most basic level, you can style the outline by setting properties on
73/// it and its entries. This way, you can customize the outline's
74/// @outline.title[title], how outline entries are @outline.indent[indented],
75/// and how the space between an entry's text and its page number should be
76/// @outline.entry.fill[filled].
77///
78/// Richer customization is possible through configuration of the outline's
79/// @outline.entry[entries]. The outline generates one entry for each outlined
80/// element.
81///
82/// == #short-or-long[Entry Spacing][Spacing the entries] <entry-spacing>
83/// Outline entries are @block[blocks], so you can adjust the spacing between
84/// them with normal block-spacing rules:
85///
86/// ```example
87/// #show outline.entry.where(
88/// level: 1
89/// ): set block(above: 1.2em)
90///
91/// #outline()
92///
93/// = About ACME Corp.
94/// == History
95/// === Origins
96/// = Products
97/// == ACME Tools
98/// ```
99///
100/// == #short-or-long[Building An Entry][
101/// Building an outline entry from its parts
102/// ] <building-an-entry>
103/// For full control, you can also write a transformational show rule on
104/// `outline.entry`. However, the logic for properly formatting and indenting
105/// outline entries is quite complex and the outline entry itself only contains
106/// two fields: The level and the outlined element.
107///
108/// For this reason, various helper functions are provided. You can mix and
109/// match these to compose an entry from just the parts you like.
110///
111/// The default show rule for an outline entry looks like this
112/// #footnote[The outline of equations is the exception to this rule as it does
113/// not have a body and thus does not use indented layout.]:
114///
115/// ```typ
116/// #show outline.entry: it => link(
117/// it.element.location(),
118/// it.indented(it.prefix(), it.inner()),
119/// )
120/// ```
121///
122/// - The @outline.entry.indented[`indented`] function takes an optional prefix
123/// and inner content and automatically applies the proper indentation to it,
124/// such that different entries align nicely and long headings wrap properly.
125///
126/// - The @outline.entry.prefix[`prefix`] function formats the element's
127/// numbering (if any). It also appends a supplement for certain elements.
128///
129/// - The @outline.entry.inner[`inner`] function combines the element's
130/// @outline.entry.body[`body`], the filler, and the
131/// @outline.entry.page[`page` number].
132///
133/// You can use these individual functions to format the outline entry in
134/// different ways. Let's say, you'd like to fully remove the filler and page
135/// numbers. To achieve this, you could write a show rule like this:
136///
137/// ```example
138/// #show outline.entry: it => link(
139/// it.element.location(),
140/// // Keep just the body, dropping
141/// // the fill and the page.
142/// it.indented(it.prefix(), it.body()),
143/// )
144///
145/// #outline()
146///
147/// = About ACME Corp.
148/// == History
149/// ```
150#[elem(scope, keywords = ["Table of Contents", "toc"], ShowSet, LocalName, Locatable, Tagged)]
151pub struct OutlineElem {
152 /// The title of the outline.
153 ///
154 /// - When set to `{auto}`, an appropriate title for the
155 /// @text.lang[text language] will be used.
156 /// - When set to `{none}`, the outline will not have a title.
157 /// - A custom title can be set by passing content.
158 ///
159 /// The outline's heading will not be numbered by default, but you can force
160 /// it to be with a show-set rule:
161 /// `{show outline: set heading(numbering: "1.")}`
162 pub title: Smart<Option<Content>>,
163
164 /// The type of element to include in the outline.
165 ///
166 /// To list figures containing a specific kind of element, like an image or
167 /// a table, you can specify the desired kind in a @function.where[`where`]
168 /// selector. See the section on
169 /// @outline:alternative-outlines[alternative outlines] for more details.
170 ///
171 /// ```example
172 /// #outline(
173 /// title: [List of Tables],
174 /// target: figure.where(kind: table),
175 /// )
176 ///
177 /// #figure(
178 /// table(
179 /// columns: 4,
180 /// [t], [1], [2], [3],
181 /// [y], [0.3], [0.7], [0.5],
182 /// ),
183 /// caption: [Experiment results],
184 /// )
185 /// ```
186 #[default(LocatableSelector(HeadingElem::ELEM.select()))]
187 pub target: LocatableSelector,
188
189 /// The maximum level up to which elements are included in the outline. When
190 /// this argument is `{none}`, all elements are included.
191 ///
192 /// ```example
193 /// #set heading(numbering: "1.")
194 /// #outline(depth: 2)
195 ///
196 /// = Yes
197 /// Top-level section.
198 ///
199 /// == Still
200 /// Subsection.
201 ///
202 /// === Nope
203 /// Not included.
204 /// ```
205 pub depth: Option<NonZeroUsize>,
206
207 /// How to indent the outline's entries.
208 ///
209 /// - `{auto}`: Indents the numbering/prefix of a nested entry with the
210 /// title of its parent entry. If the entries are not numbered (e.g., via
211 /// @heading.numbering[heading numbering]), this instead simply inserts a
212 /// fixed amount of `{1.2em}` indent per level.
213 ///
214 /// - @relative[Relative length]: Indents the entry by the specified length
215 /// per nesting level. Specifying `{2em}`, for instance, would indent
216 /// top-level headings by `{0em}` (not nested), second level headings by
217 /// `{2em}` (nested once), third-level headings by `{4em}` (nested twice)
218 /// and so on.
219 ///
220 /// - @function[Function]: You can further customize this setting with a
221 /// function. That function receives the nesting level as a parameter
222 /// (starting at 0 for top-level headings/elements) and should return a
223 /// (relative) length. For example, `{n => n * 2em}` would be equivalent
224 /// to just specifying `{2em}`.
225 ///
226 /// ```example
227 /// >>> #show heading: none
228 /// #set heading(numbering: "I-I.")
229 /// #set outline(title: none)
230 ///
231 /// #outline()
232 /// #line(length: 100%)
233 /// #outline(indent: 3em)
234 ///
235 /// = Software engineering technologies
236 /// == Requirements
237 /// == Tools and technologies
238 /// === Code editors
239 /// == Analyzing alternatives
240 /// = Designing software components
241 /// = Testing and integration
242 /// ```
243 pub indent: Smart<OutlineIndent>,
244}
245
246#[scope]
247impl OutlineElem {
248 #[elem]
249 type OutlineEntry;
250}
251
252impl Packed<OutlineElem> {
253 /// Produces the heading for the outline, if any.
254 pub fn realize_title(&self, styles: StyleChain) -> Option<Content> {
255 let span = self.span();
256 self.title
257 .get_cloned(styles)
258 .unwrap_or_else(|| {
259 Some(
260 TextElem::packed(Packed::<OutlineElem>::local_name_in(styles))
261 .spanned(span),
262 )
263 })
264 .map(|title| {
265 HeadingElem::new(title)
266 .with_depth(NonZeroUsize::ONE)
267 .pack()
268 .spanned(span)
269 })
270 }
271
272 /// Realizes the entries in a flat fashion.
273 pub fn realize_flat(
274 &self,
275 engine: &mut Engine,
276 styles: StyleChain,
277 ) -> SourceResult<Vec<Packed<OutlineEntry>>> {
278 let mut entries = vec![];
279 for result in self.realize_iter(engine, styles) {
280 let (entry, _, included) = result?;
281 if included {
282 entries.push(entry);
283 }
284 }
285 Ok(entries)
286 }
287
288 /// Realizes the entries in a tree fashion.
289 pub fn realize_tree(
290 &self,
291 engine: &mut Engine,
292 styles: StyleChain,
293 ) -> SourceResult<Vec<OutlineNode>> {
294 let flat = self.realize_iter(engine, styles).collect::<SourceResult<Vec<_>>>()?;
295 Ok(OutlineNode::build_tree(flat))
296 }
297
298 /// Realizes the entries as a lazy iterator.
299 fn realize_iter(
300 &self,
301 engine: &mut Engine,
302 styles: StyleChain,
303 ) -> impl Iterator<Item = SourceResult<(Packed<OutlineEntry>, NonZeroUsize, bool)>>
304 {
305 let span = self.span();
306 let elems =
307 engine.introspect(QueryIntrospection(self.target.get_cloned(styles).0, span));
308 let depth = self.depth.get(styles).unwrap_or(NonZeroUsize::MAX);
309 elems.into_iter().map(move |elem| {
310 let Some(outlinable) = elem.with::<dyn Outlinable>() else {
311 bail!(self.span(), "cannot outline {}", elem.func().name());
312 };
313 let level = outlinable.level();
314 let include = outlinable.outlined() && level <= depth;
315 let entry = Packed::new(OutlineEntry::new(level, elem)).spanned(span);
316 Ok((entry, level, include))
317 })
318 }
319}
320
321/// A node in a tree of outline entry.
322#[derive(Debug)]
323pub struct OutlineNode<T = Packed<OutlineEntry>> {
324 /// The entry itself.
325 pub entry: T,
326 /// The entry's level.
327 pub level: NonZeroUsize,
328 /// Its descendants.
329 pub children: Vec<OutlineNode<T>>,
330}
331
332impl<T> OutlineNode<T> {
333 /// Turns a flat list of entries into a tree.
334 ///
335 /// Each entry in the iterator should be accompanied by
336 /// - a level
337 /// - a boolean indicating whether it is included (`true`) or skipped (`false`)
338 pub fn build_tree(
339 flat: impl IntoIterator<Item = (T, NonZeroUsize, bool)>,
340 ) -> Vec<Self> {
341 // Stores the level of the topmost skipped ancestor of the next included
342 // heading.
343 let mut last_skipped_level = None;
344 let mut tree: Vec<OutlineNode<T>> = vec![];
345
346 for (entry, level, include) in flat {
347 if include {
348 let mut children = &mut tree;
349
350 // Descend the tree through the latest included heading of each
351 // level until either:
352 // - reaching a node whose children would be siblings of this
353 // heading (=> add the current heading as a child of this
354 // node)
355 // - reaching a node with no children (=> this heading probably
356 // skipped a few nesting levels in Typst, or one or more
357 // ancestors of this heading weren't included, so add it as a
358 // child of this node, which is its deepest included ancestor)
359 // - or, if the latest heading(s) was(/were) skipped, then stop
360 // if reaching a node whose children would be siblings of the
361 // latest skipped heading of lowest level (=> those skipped
362 // headings would be ancestors of the current heading, so add
363 // it as a sibling of the least deep skipped ancestor among
364 // them, as those ancestors weren't added to the tree, and the
365 // current heading should not be mistakenly added as a
366 // descendant of a siblibg of that ancestor.)
367 //
368 // That is, if you had an included heading of level N, a skipped
369 // heading of level N, a skipped heading of level N + 1, and
370 // then an included heading of level N + 2, that last one is
371 // included as a level N heading (taking the place of its
372 // topmost skipped ancestor), so that it is not mistakenly added
373 // as a descendant of the previous level N heading.
374 while children.last().is_some_and(|last| {
375 last_skipped_level.is_none_or(|l| last.level < l)
376 && last.level < level
377 }) {
378 children = &mut children.last_mut().unwrap().children;
379 }
380
381 // Since this heading was bookmarked, the next heading (if it is
382 // a child of this one) won't have a skipped direct ancestor.
383 last_skipped_level = None;
384 children.push(OutlineNode { entry, level, children: vec![] });
385 } else if last_skipped_level.is_none_or(|l| level < l) {
386 // Only the topmost / lowest-level skipped heading matters when
387 // we have consecutive skipped headings, hence the condition
388 // above.
389 last_skipped_level = Some(level);
390 }
391 }
392
393 tree
394 }
395}
396
397impl ShowSet for Packed<OutlineElem> {
398 fn show_set(&self, styles: StyleChain) -> Styles {
399 let mut out = Styles::new();
400 out.set(HeadingElem::outlined, false);
401 out.set(HeadingElem::numbering, None);
402 out.set(ParElem::justify, false);
403 out.set(BlockElem::above, Smart::Custom(styles.get(ParElem::leading).into()));
404 // Makes the outline itself available to its entries. Should be
405 // superseded by a proper ancestry mechanism in the future.
406 out.set(OutlineEntry::parent, Some(self.clone()));
407 out
408 }
409}
410
411impl LocalName for Packed<OutlineElem> {
412 const KEY: &'static str = "outline";
413}
414
415/// Defines how an outline is indented.
416#[derive(Debug, Clone, PartialEq, Hash)]
417pub enum OutlineIndent {
418 /// Indents by the specified length per level.
419 Rel(Rel),
420 /// Resolve the indent for a specific level through the given function.
421 Func(Func),
422}
423
424impl OutlineIndent {
425 /// Resolve the indent for an entry with the given level.
426 fn resolve(
427 &self,
428 engine: &mut Engine,
429 context: Tracked<Context>,
430 level: NonZeroUsize,
431 span: Span,
432 ) -> SourceResult<Rel> {
433 let depth = level.get() - 1;
434 match self {
435 Self::Rel(length) => Ok(*length * depth as f64),
436 Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span),
437 }
438 }
439}
440
441cast! {
442 OutlineIndent,
443 self => match self {
444 Self::Rel(v) => v.into_value(),
445 Self::Func(v) => v.into_value()
446 },
447 v: Rel<Length> => Self::Rel(v),
448 v: Func => Self::Func(v),
449}
450
451/// Marks an element as being able to be outlined.
452pub trait Outlinable: Refable {
453 /// Whether this element should be included in the outline.
454 fn outlined(&self) -> bool;
455
456 /// The nesting level of this element.
457 fn level(&self) -> NonZeroUsize {
458 NonZeroUsize::ONE
459 }
460
461 /// Constructs the default prefix given the formatted numbering.
462 fn prefix(&self, numbers: Content) -> Content;
463
464 /// The body of the entry.
465 fn body(&self) -> Content;
466}
467
468/// Represents an entry line in an outline.
469///
470/// With show-set and show rules on outline entries, you can richly customize
471/// the outline's appearance. See the
472/// @outline:styling-the-outline[section on styling the outline] for details.
473#[elem(scope, name = "entry", title = "Outline Entry", Locatable, Tagged)]
474pub struct OutlineEntry {
475 /// The nesting level of this outline entry. Starts at `{1}` for top-level
476 /// entries.
477 #[required]
478 pub level: NonZeroUsize,
479
480 /// The element this entry refers to. Its location will be available through
481 /// the @content.location[`location`] method on the content and can be
482 /// @link[linked] to.
483 #[required]
484 pub element: Content,
485
486 /// Content to fill the space between the title and the page number. Can be
487 /// set to `{none}` to disable filling.
488 ///
489 /// The `fill` will be placed into a fractionally sized box that spans the
490 /// space between the entry's body and the page number. When using show
491 /// rules to override outline entries, it is thus recommended to wrap the
492 /// fill in a @box with fractional width, i.e. `{box(width: 1fr, it.fill)}`.
493 ///
494 /// When using @repeat, the @repeat.gap[`gap`] property can be useful to
495 /// tweak the visual weight of the fill.
496 ///
497 /// ```example
498 /// #set outline.entry(fill: line(length: 100%))
499 /// #outline()
500 ///
501 /// = A New Beginning
502 /// ```
503 #[default(Some(
504 RepeatElem::new(TextElem::packed("."))
505 .with_gap(Em::new(0.15).into())
506 .pack()
507 ))]
508 pub fill: Option<Content>,
509
510 /// Lets outline entries access the outline they are part of. This is a bit
511 /// of a hack and should be superseded by a proper ancestry mechanism.
512 #[ghost]
513 #[internal]
514 pub parent: Option<Packed<OutlineElem>>,
515}
516
517#[scope]
518impl OutlineEntry {
519 /// A helper function for producing an indented entry layout: Lays out a
520 /// prefix and the rest of the entry in an indent-aware way.
521 ///
522 /// If the parent outline's @outline.indent[`indent`] is `{auto}`, the inner
523 /// content of all entries at level `N` is aligned with the prefix of all
524 /// entries at level `N + 1`, leaving at least `gap` space between the
525 /// prefix and inner parts. Furthermore, the `inner` contents of all entries
526 /// at the same level are aligned.
527 ///
528 /// If the outline's indent is a fixed value or a function, the prefixes are
529 /// indented, but the inner contents are simply offset from the prefix by
530 /// the specified `gap`, rather than aligning outline-wide. For a visual
531 /// explanation, see @outline.indent.
532 #[func(contextual)]
533 pub fn indented(
534 &self,
535 engine: &mut Engine,
536 context: Tracked<Context>,
537 span: Span,
538 /// The `prefix` is aligned with the `inner` content of entries that
539 /// have level one less.
540 ///
541 /// In the default show rule, this is just `it.prefix()`, but it can be
542 /// freely customized.
543 prefix: Option<Content>,
544 /// The formatted inner content of the entry.
545 ///
546 /// In the default show rule, this is just `it.inner()`, but it can be
547 /// freely customized.
548 inner: Content,
549 /// The gap between the prefix and the inner content.
550 #[named]
551 #[default(Em::new(0.5).into())]
552 gap: Length,
553 ) -> SourceResult<Content> {
554 let styles = context.styles().at(span)?;
555 let outline = styles
556 .get_ref(Self::parent)
557 .as_ref()
558 .ok_or("must be called within the context of an outline")
559 .at(span)?;
560 let outline_loc = outline.location().unwrap();
561
562 let prefix_width = prefix
563 .as_ref()
564 .map(|prefix| measure_prefix(engine, prefix, outline_loc, styles, span))
565 .transpose()?;
566 let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles));
567
568 let indent = outline.indent.get_ref(styles);
569 let (base_indent, hanging_indent) = match &indent {
570 Smart::Auto => compute_auto_indents(
571 engine,
572 outline_loc,
573 styles,
574 self.level,
575 prefix_inset,
576 span,
577 ),
578 Smart::Custom(amount) => {
579 let base = amount.resolve(engine, context, self.level, span)?;
580 (base, prefix_inset)
581 }
582 };
583
584 let body = if let (
585 Some(prefix),
586 Some(prefix_width),
587 Some(prefix_inset),
588 Some(hanging_indent),
589 ) = (prefix, prefix_width, prefix_inset, hanging_indent)
590 {
591 // Save information about our prefix that other outline entries
592 // can query for (within `compute_auto_indent`) to align
593 // themselves).
594 let mut seq = Vec::with_capacity(5);
595 if indent.is_auto() {
596 seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack());
597 }
598
599 // Dedent the prefix by the amount of hanging indent and then skip
600 // ahead so that the inner contents are aligned.
601 seq.extend([
602 HElem::new((-hanging_indent).into()).pack(),
603 PdfMarkerTag::Label(prefix),
604 HElem::new((hanging_indent - prefix_width).into()).pack(),
605 inner,
606 ]);
607 Content::sequence(seq)
608 } else {
609 inner
610 };
611
612 let inset = Sides::default().with(
613 styles.resolve(TextElem::dir).start(),
614 Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())),
615 );
616
617 Ok(BlockElem::new()
618 .with_inset(inset)
619 .with_body(Some(BlockBody::Content(body)))
620 .pack()
621 .spanned(span))
622 }
623
624 /// Formats the element's numbering (if any).
625 ///
626 /// This also appends the element's supplement in case of figures or
627 /// equations. For instance, it would output `1.1` for a heading, but
628 /// `Figure 1` for a figure, as is usual for outlines.
629 #[func(contextual)]
630 pub fn prefix(
631 &self,
632 engine: &mut Engine,
633 context: Tracked<Context>,
634 span: Span,
635 ) -> SourceResult<Option<Content>> {
636 let outlinable = self.outlinable().at(span)?;
637 let Some(numbering) = outlinable.numbering() else { return Ok(None) };
638 let loc = self.element_location().at(span)?;
639 let styles = context.styles().at(span)?;
640 let numbers = outlinable
641 .counter()
642 .display_at(engine, loc, styles, numbering, span)?;
643 Ok(Some(outlinable.prefix(numbers)))
644 }
645
646 /// Creates the default inner content of the entry.
647 ///
648 /// This includes the body, the fill, and page number.
649 #[func(contextual)]
650 pub fn inner(
651 &self,
652 engine: &mut Engine,
653 context: Tracked<Context>,
654 span: Span,
655 ) -> SourceResult<Content> {
656 let body = self.body().at(span)?;
657 let page = self.page(engine, context, span)?;
658 self.build_inner(context, span, body, page)
659 }
660
661 /// The content which is displayed in place of the referred element at its
662 /// entry in the outline. For a heading, this is its @heading.body[`body`];
663 /// for a figure a caption and for equations, it is empty.
664 #[func]
665 pub fn body(&self) -> StrResult<Content> {
666 Ok(self.outlinable()?.body())
667 }
668
669 /// The page number of this entry's element, formatted with the numbering
670 /// set for the referenced page.
671 #[func(contextual)]
672 pub fn page(
673 &self,
674 engine: &mut Engine,
675 context: Tracked<Context>,
676 span: Span,
677 ) -> SourceResult<Content> {
678 let loc = self.element_location().at(span)?;
679 let styles = context.styles().at(span)?;
680 let numbering = engine
681 .introspect(PageNumberingIntrospection(loc, span))
682 .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
683 Counter::new(CounterKey::Page).display_at(engine, loc, styles, &numbering, span)
684 }
685}
686
687impl OutlineEntry {
688 pub fn build_inner(
689 &self,
690 context: Tracked<Context>,
691 span: Span,
692 body: Content,
693 page: Content,
694 ) -> SourceResult<Content> {
695 let styles = context.styles().at(span)?;
696
697 let mut seq = vec![];
698
699 // Isolate the entry body in RTL because the page number is typically
700 // LTR. I'm not sure whether LTR should conceptually also be isolated,
701 // but in any case we don't do it for now because the text shaping
702 // pipeline does tend to choke a bit on default ignorables (in
703 // particular the CJK-Latin spacing).
704 //
705 // See also:
706 // - https://github.com/typst/typst/issues/4476
707 // - https://github.com/typst/typst/issues/5176
708 let rtl = styles.resolve(TextElem::dir) == Dir::RTL;
709 if rtl {
710 // "Right-to-Left Embedding"
711 seq.push(TextElem::packed("\u{202B}"));
712 }
713
714 seq.push(body);
715
716 if rtl {
717 // "Pop Directional Formatting"
718 seq.push(TextElem::packed("\u{202C}"));
719 }
720
721 // Add the filler between the section name and page number.
722 if let Some(filler) = self.fill.get_cloned(styles) {
723 seq.push(SpaceElem::shared().clone());
724 seq.push(
725 BoxElem::new()
726 .with_body(Some(filler))
727 .with_width(Fr::one().into())
728 .pack()
729 .spanned(span),
730 );
731 seq.push(SpaceElem::shared().clone());
732 } else {
733 seq.push(HElem::new(Fr::one().into()).pack().spanned(span));
734 }
735
736 // Add the page number. The word joiner in front ensures that the page
737 // number doesn't stand alone in its line.
738 seq.push(TextElem::packed("\u{2060}"));
739 seq.push(page);
740
741 Ok(Content::sequence(seq))
742 }
743
744 fn outlinable(&self) -> StrResult<&dyn Outlinable> {
745 self.element
746 .with::<dyn Outlinable>()
747 .ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
748 }
749
750 /// Returns the location of the outlined element.
751 pub fn element_location(&self) -> HintedStrResult<Location> {
752 let elem = &self.element;
753 elem.location().ok_or_else(|| {
754 if elem.can::<dyn Outlinable>() {
755 error!(
756 "{} must have a location", elem.func().name();
757 hint: "try using a show rule to customize the outline.entry instead";
758 )
759 } else {
760 error!("cannot outline {}", elem.func().name())
761 }
762 })
763 }
764}
765
766cast! {
767 OutlineEntry,
768 v: Content => v.unpack::<Self>().map_err(|_| "expected outline entry")?
769}
770
771/// Measures the width of a prefix.
772fn measure_prefix(
773 engine: &mut Engine,
774 prefix: &Content,
775 loc: Location,
776 styles: StyleChain,
777 span: Span,
778) -> SourceResult<Abs> {
779 let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
780 let link = LocatorLink::measure(loc, span);
781 Ok((engine.library.routines.layout_frame)(
782 engine,
783 prefix,
784 Locator::link(&link),
785 styles,
786 pod,
787 )?
788 .width())
789}
790
791/// Compute the base indent and hanging indent for an auto-indented outline
792/// entry of the given level, with the given prefix inset.
793fn compute_auto_indents(
794 engine: &mut Engine,
795 outline_loc: Location,
796 styles: StyleChain,
797 level: NonZeroUsize,
798 prefix_inset: Option<Abs>,
799 span: Span,
800) -> (Rel, Option<Abs>) {
801 let elems = engine.introspect(QueryIntrospection(
802 select_where!(PrefixInfo, key => outline_loc),
803 span,
804 ));
805 let indents = determine_prefix_widths(&elems);
806
807 let fallback = Em::new(1.2).resolve(styles);
808 let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback);
809
810 let last = level.get() - 1;
811 let base: Abs = (0..last).map(get).sum();
812 let hang = prefix_inset.map(|p| p.max(get(last)));
813
814 (base.into(), hang)
815}
816
817/// Determines the maximum prefix inset (prefix width + gap) at each outline
818/// level, for the outline with the given `loc`. Levels for which there is no
819/// information available yield `None`.
820#[comemo::memoize]
821fn determine_prefix_widths(elems: &[Content]) -> SmallVec<[Option<Abs>; 4]> {
822 let mut widths = SmallVec::<[Option<Abs>; 4]>::new();
823 for elem in elems {
824 let info = elem.to_packed::<PrefixInfo>().unwrap();
825 let level = info.level.get();
826 if widths.len() < level {
827 widths.resize(level, None);
828 }
829 widths[level - 1].get_or_insert(info.inset).set_max(info.inset);
830 }
831 widths
832}
833
834/// Helper type for introspection-based prefix alignment.
835#[elem(Construct, Unqueriable, Locatable)]
836pub(crate) struct PrefixInfo {
837 /// The location of the outline this prefix is part of. This is used to
838 /// scope prefix computations to a specific outline.
839 #[required]
840 key: Location,
841
842 /// The level of this prefix's entry.
843 #[required]
844 #[internal]
845 level: NonZeroUsize,
846
847 /// The width of the prefix, including the gap.
848 #[required]
849 #[internal]
850 inset: Abs,
851}
852
853impl Construct for PrefixInfo {
854 fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
855 bail!(args.span, "cannot be constructed manually");
856 }
857}