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}