Skip to main content

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}