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, Introspector, Locatable, Location, Locator, LocatorLink, Tagged,
17    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 [`depth`]($outline.depth). The element's numbering
31/// and page number will be displayed in the outline alongside its title or
32/// caption.
33///
34/// # Example
35/// ```example
36/// #set heading(numbering: "1.")
37/// #outline()
38///
39/// = Introduction
40/// #lorem(5)
41///
42/// = Methods
43/// == Setup
44/// #lorem(10)
45/// ```
46///
47/// # Alternative outlines
48/// In its default configuration, this function generates a table of contents.
49/// By setting the `target` parameter, the outline can be used to generate a
50/// list of other kinds of elements than headings.
51///
52/// In the example below, we list all figures containing images by setting
53/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set
54/// it to `{figure.where(kind: table)}` to generate a list of tables.
55///
56/// We could also set it to just `figure`, without using a [`where`]($function.where)
57/// selector, but then the list would contain _all_ figures, be it ones
58/// containing images, tables, or other material.
59///
60/// ```example
61/// #outline(
62///   title: [List of Figures],
63///   target: figure.where(kind: image),
64/// )
65///
66/// #figure(
67///   image("tiger.jpg"),
68///   caption: [A nice figure!],
69/// )
70/// ```
71///
72/// # Styling the outline
73/// At the most basic level, you can style the outline by setting properties on
74/// it and its entries. This way, you can customize the outline's
75/// [title]($outline.title), how outline entries are
76/// [indented]($outline.indent), and how the space between an entry's text and
77/// its page number should be [filled]($outline.entry.fill).
78///
79/// Richer customization is possible through configuration of the outline's
80/// [entries]($outline.entry). The outline generates one entry for each outlined
81/// element.
82///
83/// ## Spacing the entries { #entry-spacing }
84/// Outline entries are [blocks]($block), so you can adjust the spacing between
85/// them with normal block-spacing rules:
86///
87/// ```example
88/// #show outline.entry.where(
89///   level: 1
90/// ): set block(above: 1.2em)
91///
92/// #outline()
93///
94/// = About ACME Corp.
95/// == History
96/// === Origins
97/// = Products
98/// == ACME Tools
99/// ```
100///
101/// ## Building an outline entry from its parts { #building-an-entry }
102/// For full control, you can also write a transformational show rule on
103/// `outline.entry`. However, the logic for properly formatting and indenting
104/// outline entries is quite complex and the outline entry itself only contains
105/// two fields: The level and the outlined element.
106///
107/// For this reason, various helper functions are provided. You can mix and
108/// match these to compose an entry from just the parts you like.
109///
110/// The default show rule for an outline entry looks like this[^1]:
111/// ```typ
112/// #show outline.entry: it => link(
113///   it.element.location(),
114///   it.indented(it.prefix(), it.inner()),
115/// )
116/// ```
117///
118/// - The [`indented`]($outline.entry.indented) function takes an optional
119///   prefix and inner content and automatically applies the proper indentation
120///   to it, such that different entries align nicely and long headings wrap
121///   properly.
122///
123/// - The [`prefix`]($outline.entry.prefix) function formats the element's
124///   numbering (if any). It also appends a supplement for certain elements.
125///
126/// - The [`inner`]($outline.entry.inner) function combines the element's
127///   [`body`]($outline.entry.body), the filler, and the
128///   [`page` number]($outline.entry.page).
129///
130/// You can use these individual functions to format the outline entry in
131/// different ways. Let's say, you'd like to fully remove the filler and page
132/// numbers. To achieve this, you could write a show rule like this:
133///
134/// ```example
135/// #show outline.entry: it => link(
136///   it.element.location(),
137///   // Keep just the body, dropping
138///   // the fill and the page.
139///   it.indented(it.prefix(), it.body()),
140/// )
141///
142/// #outline()
143///
144/// = About ACME Corp.
145/// == History
146/// ```
147///
148/// [^1]: The outline of equations is the exception to this rule as it does not
149///       have a body and thus does not use indented layout.
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 language]($text.lang) 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
160    /// force 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 [`where`]($function.where)
168    /// selector. See the section on [alternative outlines]($outline/#alternative-outlines)
169    /// 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
212    ///   a fixed amount of `{1.2em}` indent per level.
213    ///
214    /// - [Relative length]($relative): Indents the entry by the specified
215    ///   length per nesting level. Specifying `{2em}`, for instance, would
216    ///   indent top-level headings by `{0em}` (not nested), second level
217    ///   headings by `{2em}` (nested once), third-level headings by `{4em}`
218    ///   (nested twice) 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 = engine.introspector.query(&self.target.get_ref(styles).0);
307        let depth = self.depth.get(styles).unwrap_or(NonZeroUsize::MAX);
308        elems.into_iter().map(move |elem| {
309            let Some(outlinable) = elem.with::<dyn Outlinable>() else {
310                bail!(self.span(), "cannot outline {}", elem.func().name());
311            };
312            let level = outlinable.level();
313            let include = outlinable.outlined() && level <= depth;
314            let entry = Packed::new(OutlineEntry::new(level, elem)).spanned(span);
315            Ok((entry, level, include))
316        })
317    }
318}
319
320/// A node in a tree of outline entry.
321#[derive(Debug)]
322pub struct OutlineNode<T = Packed<OutlineEntry>> {
323    /// The entry itself.
324    pub entry: T,
325    /// The entry's level.
326    pub level: NonZeroUsize,
327    /// Its descendants.
328    pub children: Vec<OutlineNode<T>>,
329}
330
331impl<T> OutlineNode<T> {
332    /// Turns a flat list of entries into a tree.
333    ///
334    /// Each entry in the iterator should be accompanied by
335    /// - a level
336    /// - a boolean indicating whether it is included (`true`) or skipped (`false`)
337    pub fn build_tree(
338        flat: impl IntoIterator<Item = (T, NonZeroUsize, bool)>,
339    ) -> Vec<Self> {
340        // Stores the level of the topmost skipped ancestor of the next included
341        // heading.
342        let mut last_skipped_level = None;
343        let mut tree: Vec<OutlineNode<T>> = vec![];
344
345        for (entry, level, include) in flat {
346            if include {
347                let mut children = &mut tree;
348
349                // Descend the tree through the latest included heading of each
350                // level until either:
351                // - reaching a node whose children would be siblings of this
352                //   heading (=> add the current heading as a child of this
353                //   node)
354                // - reaching a node with no children (=> this heading probably
355                //   skipped a few nesting levels in Typst, or one or more
356                //   ancestors of this heading weren't included, so add it as a
357                //   child of this node, which is its deepest included ancestor)
358                // - or, if the latest heading(s) was(/were) skipped, then stop
359                //   if reaching a node whose children would be siblings of the
360                //   latest skipped heading of lowest level (=> those skipped
361                //   headings would be ancestors of the current heading, so add
362                //   it as a sibling of the least deep skipped ancestor among
363                //   them, as those ancestors weren't added to the tree, and the
364                //   current heading should not be mistakenly added as a
365                //   descendant of a siblibg of that ancestor.)
366                //
367                // That is, if you had an included heading of level N, a skipped
368                // heading of level N, a skipped heading of level N + 1, and
369                // then an included heading of level N + 2, that last one is
370                // included as a level N heading (taking the place of its
371                // topmost skipped ancestor), so that it is not mistakenly added
372                // as a descendant of the previous level N heading.
373                while children.last().is_some_and(|last| {
374                    last_skipped_level.is_none_or(|l| last.level < l)
375                        && last.level < level
376                }) {
377                    children = &mut children.last_mut().unwrap().children;
378                }
379
380                // Since this heading was bookmarked, the next heading (if it is
381                // a child of this one) won't have a skipped direct ancestor.
382                last_skipped_level = None;
383                children.push(OutlineNode { entry, level, children: vec![] });
384            } else if last_skipped_level.is_none_or(|l| level < l) {
385                // Only the topmost / lowest-level skipped heading matters when
386                // we have consecutive skipped headings, hence the condition
387                // above.
388                last_skipped_level = Some(level);
389            }
390        }
391
392        tree
393    }
394}
395
396impl ShowSet for Packed<OutlineElem> {
397    fn show_set(&self, styles: StyleChain) -> Styles {
398        let mut out = Styles::new();
399        out.set(HeadingElem::outlined, false);
400        out.set(HeadingElem::numbering, None);
401        out.set(ParElem::justify, false);
402        out.set(BlockElem::above, Smart::Custom(styles.get(ParElem::leading).into()));
403        // Makes the outline itself available to its entries. Should be
404        // superseded by a proper ancestry mechanism in the future.
405        out.set(OutlineEntry::parent, Some(self.clone()));
406        out
407    }
408}
409
410impl LocalName for Packed<OutlineElem> {
411    const KEY: &'static str = "outline";
412}
413
414/// Defines how an outline is indented.
415#[derive(Debug, Clone, PartialEq, Hash)]
416pub enum OutlineIndent {
417    /// Indents by the specified length per level.
418    Rel(Rel),
419    /// Resolve the indent for a specific level through the given function.
420    Func(Func),
421}
422
423impl OutlineIndent {
424    /// Resolve the indent for an entry with the given level.
425    fn resolve(
426        &self,
427        engine: &mut Engine,
428        context: Tracked<Context>,
429        level: NonZeroUsize,
430        span: Span,
431    ) -> SourceResult<Rel> {
432        let depth = level.get() - 1;
433        match self {
434            Self::Rel(length) => Ok(*length * depth as f64),
435            Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span),
436        }
437    }
438}
439
440cast! {
441    OutlineIndent,
442    self => match self {
443        Self::Rel(v) => v.into_value(),
444        Self::Func(v) => v.into_value()
445    },
446    v: Rel<Length> => Self::Rel(v),
447    v: Func => Self::Func(v),
448}
449
450/// Marks an element as being able to be outlined.
451pub trait Outlinable: Refable {
452    /// Whether this element should be included in the outline.
453    fn outlined(&self) -> bool;
454
455    /// The nesting level of this element.
456    fn level(&self) -> NonZeroUsize {
457        NonZeroUsize::ONE
458    }
459
460    /// Constructs the default prefix given the formatted numbering.
461    fn prefix(&self, numbers: Content) -> Content;
462
463    /// The body of the entry.
464    fn body(&self) -> Content;
465}
466
467/// Represents an entry line in an outline.
468///
469/// With show-set and show rules on outline entries, you can richly customize
470/// the outline's appearance. See the
471/// [section on styling the outline]($outline/#styling-the-outline) for details.
472#[elem(scope, name = "entry", title = "Outline Entry", Locatable, Tagged)]
473pub struct OutlineEntry {
474    /// The nesting level of this outline entry. Starts at `{1}` for top-level
475    /// entries.
476    #[required]
477    pub level: NonZeroUsize,
478
479    /// The element this entry refers to. Its location will be available
480    /// through the [`location`]($content.location) method on the content
481    /// and can be [linked]($link) to.
482    #[required]
483    pub element: Content,
484
485    /// Content to fill the space between the title and the page number. Can be
486    /// set to `{none}` to disable filling.
487    ///
488    /// The `fill` will be placed into a fractionally sized box that spans the
489    /// space between the entry's body and the page number. When using show
490    /// rules to override outline entries, it is thus recommended to wrap the
491    /// fill in a [`box`] with fractional width, i.e.
492    /// `{box(width: 1fr, it.fill)}`.
493    ///
494    /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
495    /// to 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 [`indent`]($outline.indent) is `{auto}`, the
523    /// inner content of all entries at level `N` is aligned with the prefix of
524    /// all 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))
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.introspector,
572                outline_loc,
573                styles,
574                self.level,
575                prefix_inset,
576            ),
577            Smart::Custom(amount) => {
578                let base = amount.resolve(engine, context, self.level, span)?;
579                (base, prefix_inset)
580            }
581        };
582
583        let body = if let (
584            Some(prefix),
585            Some(prefix_width),
586            Some(prefix_inset),
587            Some(hanging_indent),
588        ) = (prefix, prefix_width, prefix_inset, hanging_indent)
589        {
590            // Save information about our prefix that other outline entries
591            // can query for (within `compute_auto_indent`) to align
592            // themselves).
593            let mut seq = Vec::with_capacity(5);
594            if indent.is_auto() {
595                seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack());
596            }
597
598            // Dedent the prefix by the amount of hanging indent and then skip
599            // ahead so that the inner contents are aligned.
600            seq.extend([
601                HElem::new((-hanging_indent).into()).pack(),
602                PdfMarkerTag::Label(prefix),
603                HElem::new((hanging_indent - prefix_width).into()).pack(),
604                inner,
605            ]);
606            Content::sequence(seq)
607        } else {
608            inner
609        };
610
611        let inset = Sides::default().with(
612            styles.resolve(TextElem::dir).start(),
613            Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())),
614        );
615
616        Ok(BlockElem::new()
617            .with_inset(inset)
618            .with_body(Some(BlockBody::Content(body)))
619            .pack()
620            .spanned(span))
621    }
622
623    /// Formats the element's numbering (if any).
624    ///
625    /// This also appends the element's supplement in case of figures or
626    /// equations. For instance, it would output `1.1` for a heading, but
627    /// `Figure 1` for a figure, as is usual for outlines.
628    #[func(contextual)]
629    pub fn prefix(
630        &self,
631        engine: &mut Engine,
632        context: Tracked<Context>,
633        span: Span,
634    ) -> SourceResult<Option<Content>> {
635        let outlinable = self.outlinable().at(span)?;
636        let Some(numbering) = outlinable.numbering() else { return Ok(None) };
637        let loc = self.element_location().at(span)?;
638        let styles = context.styles().at(span)?;
639        let numbers =
640            outlinable.counter().display_at_loc(engine, loc, styles, numbering)?;
641        Ok(Some(outlinable.prefix(numbers)))
642    }
643
644    /// Creates the default inner content of the entry.
645    ///
646    /// This includes the body, the fill, and page number.
647    #[func(contextual)]
648    pub fn inner(
649        &self,
650        engine: &mut Engine,
651        context: Tracked<Context>,
652        span: Span,
653    ) -> SourceResult<Content> {
654        let body = self.body().at(span)?;
655        let page = self.page(engine, context, span)?;
656        self.build_inner(context, span, body, page)
657    }
658
659    /// The content which is displayed in place of the referred element at its
660    /// entry in the outline. For a heading, this is its
661    /// [`body`]($heading.body); for a figure a caption and for equations, it is
662    /// empty.
663    #[func]
664    pub fn body(&self) -> StrResult<Content> {
665        Ok(self.outlinable()?.body())
666    }
667
668    /// The page number of this entry's element, formatted with the numbering
669    /// set for the referenced page.
670    #[func(contextual)]
671    pub fn page(
672        &self,
673        engine: &mut Engine,
674        context: Tracked<Context>,
675        span: Span,
676    ) -> SourceResult<Content> {
677        let loc = self.element_location().at(span)?;
678        let styles = context.styles().at(span)?;
679        let numbering = engine
680            .introspector
681            .page_numbering(loc)
682            .cloned()
683            .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
684        Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering)
685    }
686}
687
688impl OutlineEntry {
689    pub fn build_inner(
690        &self,
691        context: Tracked<Context>,
692        span: Span,
693        body: Content,
694        page: Content,
695    ) -> SourceResult<Content> {
696        let styles = context.styles().at(span)?;
697
698        let mut seq = vec![];
699
700        // Isolate the entry body in RTL because the page number is typically
701        // LTR. I'm not sure whether LTR should conceptually also be isolated,
702        // but in any case we don't do it for now because the text shaping
703        // pipeline does tend to choke a bit on default ignorables (in
704        // particular the CJK-Latin spacing).
705        //
706        // See also:
707        // - https://github.com/typst/typst/issues/4476
708        // - https://github.com/typst/typst/issues/5176
709        let rtl = styles.resolve(TextElem::dir) == Dir::RTL;
710        if rtl {
711            // "Right-to-Left Embedding"
712            seq.push(TextElem::packed("\u{202B}"));
713        }
714
715        seq.push(body);
716
717        if rtl {
718            // "Pop Directional Formatting"
719            seq.push(TextElem::packed("\u{202C}"));
720        }
721
722        // Add the filler between the section name and page number.
723        if let Some(filler) = self.fill.get_cloned(styles) {
724            seq.push(SpaceElem::shared().clone());
725            seq.push(
726                BoxElem::new()
727                    .with_body(Some(filler))
728                    .with_width(Fr::one().into())
729                    .pack()
730                    .spanned(span),
731            );
732            seq.push(SpaceElem::shared().clone());
733        } else {
734            seq.push(HElem::new(Fr::one().into()).pack().spanned(span));
735        }
736
737        // Add the page number. The word joiner in front ensures that the page
738        // number doesn't stand alone in its line.
739        seq.push(TextElem::packed("\u{2060}"));
740        seq.push(page);
741
742        Ok(Content::sequence(seq))
743    }
744
745    fn outlinable(&self) -> StrResult<&dyn Outlinable> {
746        self.element
747            .with::<dyn Outlinable>()
748            .ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
749    }
750
751    /// Returns the location of the outlined element.
752    pub fn element_location(&self) -> HintedStrResult<Location> {
753        let elem = &self.element;
754        elem.location().ok_or_else(|| {
755            if elem.can::<dyn Outlinable>() {
756                error!(
757                    "{} must have a location", elem.func().name();
758                    hint: "try using a show rule to customize the outline.entry instead",
759                )
760            } else {
761                error!("cannot outline {}", elem.func().name())
762            }
763        })
764    }
765}
766
767cast! {
768    OutlineEntry,
769    v: Content => v.unpack::<Self>().map_err(|_| "expected outline entry")?
770}
771
772/// Measures the width of a prefix.
773fn measure_prefix(
774    engine: &mut Engine,
775    prefix: &Content,
776    loc: Location,
777    styles: StyleChain,
778) -> SourceResult<Abs> {
779    let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
780    let link = LocatorLink::measure(loc);
781    Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)?
782        .width())
783}
784
785/// Compute the base indent and hanging indent for an auto-indented outline
786/// entry of the given level, with the given prefix inset.
787fn compute_auto_indents(
788    introspector: Tracked<Introspector>,
789    outline_loc: Location,
790    styles: StyleChain,
791    level: NonZeroUsize,
792    prefix_inset: Option<Abs>,
793) -> (Rel, Option<Abs>) {
794    let indents = query_prefix_widths(introspector, outline_loc);
795
796    let fallback = Em::new(1.2).resolve(styles);
797    let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback);
798
799    let last = level.get() - 1;
800    let base: Abs = (0..last).map(get).sum();
801    let hang = prefix_inset.map(|p| p.max(get(last)));
802
803    (base.into(), hang)
804}
805
806/// Determines the maximum prefix inset (prefix width + gap) at each outline
807/// level, for the outline with the given `loc`. Levels for which there is no
808/// information available yield `None`.
809#[comemo::memoize]
810fn query_prefix_widths(
811    introspector: Tracked<Introspector>,
812    outline_loc: Location,
813) -> SmallVec<[Option<Abs>; 4]> {
814    let mut widths = SmallVec::<[Option<Abs>; 4]>::new();
815    let elems = introspector.query(&select_where!(PrefixInfo, key => outline_loc));
816    for elem in &elems {
817        let info = elem.to_packed::<PrefixInfo>().unwrap();
818        let level = info.level.get();
819        if widths.len() < level {
820            widths.resize(level, None);
821        }
822        widths[level - 1].get_or_insert(info.inset).set_max(info.inset);
823    }
824    widths
825}
826
827/// Helper type for introspection-based prefix alignment.
828#[elem(Construct, Unqueriable, Locatable)]
829pub(crate) struct PrefixInfo {
830    /// The location of the outline this prefix is part of. This is used to
831    /// scope prefix computations to a specific outline.
832    #[required]
833    key: Location,
834
835    /// The level of this prefix's entry.
836    #[required]
837    #[internal]
838    level: NonZeroUsize,
839
840    /// The width of the prefix, including the gap.
841    #[required]
842    #[internal]
843    inset: Abs,
844}
845
846impl Construct for PrefixInfo {
847    fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
848        bail!(args.span, "cannot be constructed manually");
849    }
850}