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}