typst_library/model/
outline.rs

1use std::num::NonZeroUsize;
2use std::str::FromStr;
3
4use comemo::{Track, Tracked};
5use smallvec::SmallVec;
6use typst_syntax::Span;
7use typst_utils::{Get, NonZeroExt};
8
9use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult};
10use crate::engine::Engine;
11use crate::foundations::{
12    cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func,
13    LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
14    Styles,
15};
16use crate::introspection::{
17    Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
18};
19use crate::layout::{
20    Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
21    RepeatElem, Sides,
22};
23use crate::math::EquationElem;
24use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
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"], Show, ShowSet, LocalName, Locatable)]
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    #[borrowed]
188    pub target: LocatableSelector,
189
190    /// The maximum level up to which elements are included in the outline. When
191    /// this argument is `{none}`, all elements are included.
192    ///
193    /// ```example
194    /// #set heading(numbering: "1.")
195    /// #outline(depth: 2)
196    ///
197    /// = Yes
198    /// Top-level section.
199    ///
200    /// == Still
201    /// Subsection.
202    ///
203    /// === Nope
204    /// Not included.
205    /// ```
206    pub depth: Option<NonZeroUsize>,
207
208    /// How to indent the outline's entries.
209    ///
210    /// - `{auto}`: Indents the numbering/prefix of a nested entry with the
211    ///   title of its parent entry. If the entries are not numbered (e.g., via
212    ///   [heading numbering]($heading.numbering)), this instead simply inserts
213    ///   a fixed amount of `{1.2em}` indent per level.
214    ///
215    /// - [Relative length]($relative): Indents the entry by the specified
216    ///   length per nesting level. Specifying `{2em}`, for instance, would
217    ///   indent top-level headings by `{0em}` (not nested), second level
218    ///   headings by `{2em}` (nested once), third-level headings by `{4em}`
219    ///   (nested twice) and so on.
220    ///
221    /// - [Function]($function): You can further customize this setting with a
222    ///   function. That function receives the nesting level as a parameter
223    ///   (starting at 0 for top-level headings/elements) and should return a
224    ///   (relative) length. For example, `{n => n * 2em}` would be equivalent
225    ///   to just specifying `{2em}`.
226    ///
227    /// ```example
228    /// #set heading(numbering: "1.a.")
229    ///
230    /// #outline(
231    ///   title: [Contents (Automatic)],
232    ///   indent: auto,
233    /// )
234    ///
235    /// #outline(
236    ///   title: [Contents (Length)],
237    ///   indent: 2em,
238    /// )
239    ///
240    /// = About ACME Corp.
241    /// == History
242    /// === Origins
243    /// #lorem(10)
244    ///
245    /// == Products
246    /// #lorem(10)
247    /// ```
248    pub indent: Smart<OutlineIndent>,
249}
250
251#[scope]
252impl OutlineElem {
253    #[elem]
254    type OutlineEntry;
255}
256
257impl Show for Packed<OutlineElem> {
258    #[typst_macros::time(name = "outline", span = self.span())]
259    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
260        let span = self.span();
261
262        // Build the outline title.
263        let mut seq = vec![];
264        if let Some(title) = self.title(styles).unwrap_or_else(|| {
265            Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
266        }) {
267            seq.push(
268                HeadingElem::new(title)
269                    .with_depth(NonZeroUsize::ONE)
270                    .pack()
271                    .spanned(span),
272            );
273        }
274
275        let elems = engine.introspector.query(&self.target(styles).0);
276        let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX);
277
278        // Build the outline entries.
279        for elem in elems {
280            let Some(outlinable) = elem.with::<dyn Outlinable>() else {
281                bail!(span, "cannot outline {}", elem.func().name());
282            };
283
284            let level = outlinable.level();
285            if outlinable.outlined() && level <= depth {
286                let entry = OutlineEntry::new(level, elem);
287                seq.push(entry.pack().spanned(span));
288            }
289        }
290
291        Ok(Content::sequence(seq))
292    }
293}
294
295impl ShowSet for Packed<OutlineElem> {
296    fn show_set(&self, styles: StyleChain) -> Styles {
297        let mut out = Styles::new();
298        out.set(HeadingElem::set_outlined(false));
299        out.set(HeadingElem::set_numbering(None));
300        out.set(ParElem::set_justify(false));
301        out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into())));
302        // Makes the outline itself available to its entries. Should be
303        // superseded by a proper ancestry mechanism in the future.
304        out.set(OutlineEntry::set_parent(Some(self.clone())));
305        out
306    }
307}
308
309impl LocalName for Packed<OutlineElem> {
310    const KEY: &'static str = "outline";
311}
312
313/// Defines how an outline is indented.
314#[derive(Debug, Clone, PartialEq, Hash)]
315pub enum OutlineIndent {
316    /// Indents by the specified length per level.
317    Rel(Rel),
318    /// Resolve the indent for a specific level through the given function.
319    Func(Func),
320}
321
322impl OutlineIndent {
323    /// Resolve the indent for an entry with the given level.
324    fn resolve(
325        &self,
326        engine: &mut Engine,
327        context: Tracked<Context>,
328        level: NonZeroUsize,
329        span: Span,
330    ) -> SourceResult<Rel> {
331        let depth = level.get() - 1;
332        match self {
333            Self::Rel(length) => Ok(*length * depth as f64),
334            Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span),
335        }
336    }
337}
338
339cast! {
340    OutlineIndent,
341    self => match self {
342        Self::Rel(v) => v.into_value(),
343        Self::Func(v) => v.into_value()
344    },
345    v: Rel<Length> => Self::Rel(v),
346    v: Func => Self::Func(v),
347}
348
349/// Marks an element as being able to be outlined.
350pub trait Outlinable: Refable {
351    /// Whether this element should be included in the outline.
352    fn outlined(&self) -> bool;
353
354    /// The nesting level of this element.
355    fn level(&self) -> NonZeroUsize {
356        NonZeroUsize::ONE
357    }
358
359    /// Constructs the default prefix given the formatted numbering.
360    fn prefix(&self, numbers: Content) -> Content;
361
362    /// The body of the entry.
363    fn body(&self) -> Content;
364}
365
366/// Represents an entry line in an outline.
367///
368/// With show-set and show rules on outline entries, you can richly customize
369/// the outline's appearance. See the
370/// [section on styling the outline]($outline/#styling-the-outline) for details.
371#[elem(scope, name = "entry", title = "Outline Entry", Show)]
372pub struct OutlineEntry {
373    /// The nesting level of this outline entry. Starts at `{1}` for top-level
374    /// entries.
375    #[required]
376    pub level: NonZeroUsize,
377
378    /// The element this entry refers to. Its location will be available
379    /// through the [`location`]($content.location) method on the content
380    /// and can be [linked]($link) to.
381    #[required]
382    pub element: Content,
383
384    /// Content to fill the space between the title and the page number. Can be
385    /// set to `{none}` to disable filling.
386    ///
387    /// The `fill` will be placed into a fractionally sized box that spans the
388    /// space between the entry's body and the page number. When using show
389    /// rules to override outline entries, it is thus recommended to wrap the
390    /// fill in a [`box`] with fractional width, i.e.
391    /// `{box(width: 1fr, it.fill}`.
392    ///
393    /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
394    /// to tweak the visual weight of the fill.
395    ///
396    /// ```example
397    /// #set outline.entry(fill: line(length: 100%))
398    /// #outline()
399    ///
400    /// = A New Beginning
401    /// ```
402    #[borrowed]
403    #[default(Some(
404        RepeatElem::new(TextElem::packed("."))
405            .with_gap(Em::new(0.15).into())
406            .pack()
407    ))]
408    pub fill: Option<Content>,
409
410    /// Lets outline entries access the outline they are part of. This is a bit
411    /// of a hack and should be superseded by a proper ancestry mechanism.
412    #[ghost]
413    #[internal]
414    pub parent: Option<Packed<OutlineElem>>,
415}
416
417impl Show for Packed<OutlineEntry> {
418    #[typst_macros::time(name = "outline.entry", span = self.span())]
419    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
420        let span = self.span();
421        let context = Context::new(None, Some(styles));
422        let context = context.track();
423
424        let prefix = self.prefix(engine, context, span)?;
425        let inner = self.inner(engine, context, span)?;
426        let block = if self.element.is::<EquationElem>() {
427            let body = prefix.unwrap_or_default() + inner;
428            BlockElem::new()
429                .with_body(Some(BlockBody::Content(body)))
430                .pack()
431                .spanned(span)
432        } else {
433            self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
434        };
435
436        let loc = self.element_location().at(span)?;
437        Ok(block.linked(Destination::Location(loc)))
438    }
439}
440
441#[scope]
442impl OutlineEntry {
443    /// A helper function for producing an indented entry layout: Lays out a
444    /// prefix and the rest of the entry in an indent-aware way.
445    ///
446    /// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the
447    /// inner content of all entries at level `N` is aligned with the prefix of
448    /// all entries at level `N + 1`, leaving at least `gap` space between the
449    /// prefix and inner parts. Furthermore, the `inner` contents of all entries
450    /// at the same level are aligned.
451    ///
452    /// If the outline's indent is a fixed value or a function, the prefixes are
453    /// indented, but the inner contents are simply inset from the prefix by the
454    /// specified `gap`, rather than aligning outline-wide.
455    #[func(contextual)]
456    pub fn indented(
457        &self,
458        engine: &mut Engine,
459        context: Tracked<Context>,
460        span: Span,
461        /// The `prefix` is aligned with the `inner` content of entries that
462        /// have level one less.
463        ///
464        /// In the default show rule, this is just `it.prefix()`, but it can be
465        /// freely customized.
466        prefix: Option<Content>,
467        /// The formatted inner content of the entry.
468        ///
469        /// In the default show rule, this is just `it.inner()`, but it can be
470        /// freely customized.
471        inner: Content,
472        /// The gap between the prefix and the inner content.
473        #[named]
474        #[default(Em::new(0.5).into())]
475        gap: Length,
476    ) -> SourceResult<Content> {
477        let styles = context.styles().at(span)?;
478        let outline = Self::parent_in(styles)
479            .ok_or("must be called within the context of an outline")
480            .at(span)?;
481        let outline_loc = outline.location().unwrap();
482
483        let prefix_width = prefix
484            .as_ref()
485            .map(|prefix| measure_prefix(engine, prefix, outline_loc, styles))
486            .transpose()?;
487        let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles));
488
489        let indent = outline.indent(styles);
490        let (base_indent, hanging_indent) = match &indent {
491            Smart::Auto => compute_auto_indents(
492                engine.introspector,
493                outline_loc,
494                styles,
495                self.level,
496                prefix_inset,
497            ),
498            Smart::Custom(amount) => {
499                let base = amount.resolve(engine, context, self.level, span)?;
500                (base, prefix_inset)
501            }
502        };
503
504        let body = if let (
505            Some(prefix),
506            Some(prefix_width),
507            Some(prefix_inset),
508            Some(hanging_indent),
509        ) = (prefix, prefix_width, prefix_inset, hanging_indent)
510        {
511            // Save information about our prefix that other outline entries
512            // can query for (within `compute_auto_indent`) to align
513            // themselves).
514            let mut seq = Vec::with_capacity(5);
515            if indent.is_auto() {
516                seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack());
517            }
518
519            // Dedent the prefix by the amount of hanging indent and then skip
520            // ahead so that the inner contents are aligned.
521            seq.extend([
522                HElem::new((-hanging_indent).into()).pack(),
523                prefix,
524                HElem::new((hanging_indent - prefix_width).into()).pack(),
525                inner,
526            ]);
527            Content::sequence(seq)
528        } else {
529            inner
530        };
531
532        let inset = Sides::default().with(
533            TextElem::dir_in(styles).start(),
534            Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())),
535        );
536
537        Ok(BlockElem::new()
538            .with_inset(inset)
539            .with_body(Some(BlockBody::Content(body)))
540            .pack()
541            .spanned(span))
542    }
543
544    /// Formats the element's numbering (if any).
545    ///
546    /// This also appends the element's supplement in case of figures or
547    /// equations. For instance, it would output `1.1` for a heading, but
548    /// `Figure 1` for a figure, as is usual for outlines.
549    #[func(contextual)]
550    pub fn prefix(
551        &self,
552        engine: &mut Engine,
553        context: Tracked<Context>,
554        span: Span,
555    ) -> SourceResult<Option<Content>> {
556        let outlinable = self.outlinable().at(span)?;
557        let Some(numbering) = outlinable.numbering() else { return Ok(None) };
558        let loc = self.element_location().at(span)?;
559        let styles = context.styles().at(span)?;
560        let numbers =
561            outlinable.counter().display_at_loc(engine, loc, styles, numbering)?;
562        Ok(Some(outlinable.prefix(numbers)))
563    }
564
565    /// Creates the default inner content of the entry.
566    ///
567    /// This includes the body, the fill, and page number.
568    #[func(contextual)]
569    pub fn inner(
570        &self,
571        engine: &mut Engine,
572        context: Tracked<Context>,
573        span: Span,
574    ) -> SourceResult<Content> {
575        let styles = context.styles().at(span)?;
576
577        let mut seq = vec![];
578
579        // Isolate the entry body in RTL because the page number is typically
580        // LTR. I'm not sure whether LTR should conceptually also be isolated,
581        // but in any case we don't do it for now because the text shaping
582        // pipeline does tend to choke a bit on default ignorables (in
583        // particular the CJK-Latin spacing).
584        //
585        // See also:
586        // - https://github.com/typst/typst/issues/4476
587        // - https://github.com/typst/typst/issues/5176
588        let rtl = TextElem::dir_in(styles) == Dir::RTL;
589        if rtl {
590            // "Right-to-Left Embedding"
591            seq.push(TextElem::packed("\u{202B}"));
592        }
593
594        seq.push(self.body().at(span)?);
595
596        if rtl {
597            // "Pop Directional Formatting"
598            seq.push(TextElem::packed("\u{202C}"));
599        }
600
601        // Add the filler between the section name and page number.
602        if let Some(filler) = self.fill(styles) {
603            seq.push(SpaceElem::shared().clone());
604            seq.push(
605                BoxElem::new()
606                    .with_body(Some(filler.clone()))
607                    .with_width(Fr::one().into())
608                    .pack()
609                    .spanned(span),
610            );
611            seq.push(SpaceElem::shared().clone());
612        } else {
613            seq.push(HElem::new(Fr::one().into()).pack().spanned(span));
614        }
615
616        // Add the page number. The word joiner in front ensures that the page
617        // number doesn't stand alone in its line.
618        seq.push(TextElem::packed("\u{2060}"));
619        seq.push(self.page(engine, context, span)?);
620
621        Ok(Content::sequence(seq))
622    }
623
624    /// The content which is displayed in place of the referred element at its
625    /// entry in the outline. For a heading, this is its
626    /// [`body`]($heading.body); for a figure a caption and for equations, it is
627    /// empty.
628    #[func]
629    pub fn body(&self) -> StrResult<Content> {
630        Ok(self.outlinable()?.body())
631    }
632
633    /// The page number of this entry's element, formatted with the numbering
634    /// set for the referenced page.
635    #[func(contextual)]
636    pub fn page(
637        &self,
638        engine: &mut Engine,
639        context: Tracked<Context>,
640        span: Span,
641    ) -> SourceResult<Content> {
642        let loc = self.element_location().at(span)?;
643        let styles = context.styles().at(span)?;
644        let numbering = engine
645            .introspector
646            .page_numbering(loc)
647            .cloned()
648            .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
649        Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering)
650    }
651}
652
653impl OutlineEntry {
654    fn outlinable(&self) -> StrResult<&dyn Outlinable> {
655        self.element
656            .with::<dyn Outlinable>()
657            .ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
658    }
659
660    fn element_location(&self) -> HintedStrResult<Location> {
661        let elem = &self.element;
662        elem.location().ok_or_else(|| {
663            if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
664                error!(
665                    "{} must have a location", elem.func().name();
666                    hint: "try using a show rule to customize the outline.entry instead",
667                )
668            } else {
669                error!("cannot outline {}", elem.func().name())
670            }
671        })
672    }
673}
674
675cast! {
676    OutlineEntry,
677    v: Content => v.unpack::<Self>().map_err(|_| "expected outline entry")?
678}
679
680/// Measures the width of a prefix.
681fn measure_prefix(
682    engine: &mut Engine,
683    prefix: &Content,
684    loc: Location,
685    styles: StyleChain,
686) -> SourceResult<Abs> {
687    let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
688    let link = LocatorLink::measure(loc);
689    Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)?
690        .width())
691}
692
693/// Compute the base indent and hanging indent for an auto-indented outline
694/// entry of the given level, with the given prefix inset.
695fn compute_auto_indents(
696    introspector: Tracked<Introspector>,
697    outline_loc: Location,
698    styles: StyleChain,
699    level: NonZeroUsize,
700    prefix_inset: Option<Abs>,
701) -> (Rel, Option<Abs>) {
702    let indents = query_prefix_widths(introspector, outline_loc);
703
704    let fallback = Em::new(1.2).resolve(styles);
705    let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback);
706
707    let last = level.get() - 1;
708    let base: Abs = (0..last).map(get).sum();
709    let hang = prefix_inset.map(|p| p.max(get(last)));
710
711    (base.into(), hang)
712}
713
714/// Determines the maximum prefix inset (prefix width + gap) at each outline
715/// level, for the outline with the given `loc`. Levels for which there is no
716/// information available yield `None`.
717#[comemo::memoize]
718fn query_prefix_widths(
719    introspector: Tracked<Introspector>,
720    outline_loc: Location,
721) -> SmallVec<[Option<Abs>; 4]> {
722    let mut widths = SmallVec::<[Option<Abs>; 4]>::new();
723    let elems = introspector.query(&select_where!(PrefixInfo, Key => outline_loc));
724    for elem in &elems {
725        let info = elem.to_packed::<PrefixInfo>().unwrap();
726        let level = info.level.get();
727        if widths.len() < level {
728            widths.resize(level, None);
729        }
730        widths[level - 1].get_or_insert(info.inset).set_max(info.inset);
731    }
732    widths
733}
734
735/// Helper type for introspection-based prefix alignment.
736#[elem(Construct, Locatable, Show)]
737struct PrefixInfo {
738    /// The location of the outline this prefix is part of. This is used to
739    /// scope prefix computations to a specific outline.
740    #[required]
741    key: Location,
742
743    /// The level of this prefix's entry.
744    #[required]
745    #[internal]
746    level: NonZeroUsize,
747
748    /// The width of the prefix, including the gap.
749    #[required]
750    #[internal]
751    inset: Abs,
752}
753
754impl Construct for PrefixInfo {
755    fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
756        bail!(args.span, "cannot be constructed manually");
757    }
758}
759
760impl Show for Packed<PrefixInfo> {
761    fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
762        Ok(Content::empty())
763    }
764}