Skip to main content

typst_library/model/
heading.rs

1use std::num::NonZeroUsize;
2
3use ecow::EcoString;
4use typst_utils::NonZeroExt;
5
6use crate::diag::SourceResult;
7use crate::engine::Engine;
8use crate::foundations::{
9    Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize, elem,
10};
11use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Tagged};
12use crate::layout::{BlockElem, Em, Length};
13use crate::model::{Numbering, Outlinable, Refable, Supplement};
14use crate::text::{FontWeight, LocalName, TextElem, TextSize};
15
16/// A section heading.
17///
18/// With headings, you can structure your document into sections. Each heading
19/// has a _level,_ which starts at one and is unbounded upwards. This level
20/// indicates the logical role of the following content (section, subsection,
21/// etc.) A top-level heading indicates a top-level section of the document (not
22/// the document's title). To insert a title, use the @title element instead.
23///
24/// Typst can automatically number your headings for you. To enable numbering,
25/// specify how you want your headings to be numbered with a
26/// @numbering[numbering pattern or function].
27///
28/// Independently of the numbering, Typst can also automatically generate an
29/// @outline[outline] of all headings for you. To exclude one or more headings
30/// from this outline, you can set the `outlined` parameter to `{false}`.
31///
32/// When writing a @reference:styling:show-rules[show rule] that accesses the
33/// @heading.body[`body` field] to create a completely custom look for headings,
34/// make sure to wrap the content in a @block (which is implicitly
35/// @block.sticky[sticky] for headings through a built-in show-set rule). This
36/// prevents headings from becoming "orphans", i.e. remaining at the end of the
37/// page with the following content being on the next page.
38///
39/// = Example <example>
40/// ```example
41/// #set heading(numbering: "1.a)")
42///
43/// = Introduction
44/// In recent years, ...
45///
46/// == Preliminaries
47/// To start, ...
48/// ```
49///
50/// = Syntax <syntax>
51/// Headings have dedicated syntax: They can be created by starting a line with
52/// one or multiple equals signs, followed by a space. The number of equals
53/// signs determines the heading's logical nesting depth. The `{offset}` field
54/// can be set to configure the starting depth.
55///
56/// = Accessibility <accessibility>
57/// Headings are important for accessibility, as they help users of Assistive
58/// Technologies (AT) like screen readers to navigate within your document.
59/// Screen reader users will be able to skip from heading to heading, or get an
60/// overview of all headings in the document.
61///
62/// To make your headings accessible, you should not skip heading levels. This
63/// means that you should start with a first-level heading. Also, when the
64/// previous heading was of level 3, the next heading should be of level 3
65/// (staying at the same depth), level 4 (going exactly one level deeper), or
66/// level 1 or 2 (new hierarchically higher headings).
67///
68/// = HTML export <html-export>
69/// As mentioned above, a top-level heading indicates a top-level section of the
70/// document rather than its title. This is in contrast to the HTML `<h1>`
71/// element of which there should be only one per document.
72///
73/// For this reason, in HTML export, a @title element will turn into an `<h1>`
74/// and headings turn into `<h2>` and lower (a level 1 heading thus turns into
75/// `<h2>`, a level 2 heading into `<h3>`, etc).
76#[elem(Locatable, Tagged, Synthesize, Count, ShowSet, LocalName, Refable, Outlinable)]
77pub struct HeadingElem {
78    /// The absolute nesting depth of the heading, starting from one. If set to
79    /// `{auto}`, it is computed from `{offset + depth}`.
80    ///
81    /// This is primarily useful for usage in
82    /// @reference:styling:show-rules[show rules] (either with
83    /// @function.where[`where`] selectors or by accessing the level directly on
84    /// a shown heading).
85    ///
86    /// ```example
87    /// #show heading.where(level: 2): set text(red)
88    ///
89    /// = Level 1
90    /// == Level 2
91    ///
92    /// #set heading(offset: 1)
93    /// = Also level 2
94    /// == Level 3
95    /// ```
96    pub level: Smart<NonZeroUsize>,
97
98    /// The relative nesting depth of the heading, starting from one. This is
99    /// combined with `{offset}` to compute the actual `{level}`.
100    ///
101    /// This is set by the heading syntax, such that `[== Heading]` creates a
102    /// heading with logical depth of 2, but actual level `{offset + 2}`. If you
103    /// construct a heading manually, you should typically prefer this over
104    /// setting the absolute level.
105    #[default(NonZeroUsize::ONE)]
106    pub depth: NonZeroUsize,
107
108    /// The starting offset of each heading's `{level}`, used to turn its
109    /// relative `{depth}` into its absolute `{level}`.
110    ///
111    /// ```example
112    /// = Level 1
113    ///
114    /// #set heading(offset: 1, numbering: "1.1")
115    /// = Level 2
116    ///
117    /// #heading(offset: 2, depth: 2)[
118    ///   I'm level 4
119    /// ]
120    /// ```
121    #[default(0)]
122    pub offset: usize,
123
124    /// How to number the heading. Accepts a
125    /// @numbering[numbering pattern or function] taking multiple numbers.
126    ///
127    /// ```example
128    /// #set heading(numbering: "1.a.")
129    ///
130    /// = A section
131    /// == A subsection
132    /// === A sub-subsection
133    /// ```
134    pub numbering: Option<Numbering>,
135
136    /// The resolved plain-text numbers.
137    ///
138    /// This field is internal and only used for creating PDF bookmarks. We
139    /// don't currently have access to `World`, `Engine`, or `styles` in export,
140    /// which is needed to resolve the counter and numbering pattern into a
141    /// concrete string.
142    ///
143    /// This remains unset if `numbering` is `None`.
144    #[internal]
145    #[synthesized]
146    pub numbers: EcoString,
147
148    /// A supplement for the heading.
149    ///
150    /// For references to headings, this is added before the referenced number.
151    ///
152    /// If a function is specified, it is passed the referenced heading and
153    /// should return content.
154    ///
155    /// ```example
156    /// #set heading(numbering: "1.", supplement: [Chapter])
157    ///
158    /// = Introduction <intro>
159    /// In @intro, we see how to turn
160    /// Sections into Chapters. And
161    /// in @intro[Part], it is done
162    /// manually.
163    /// ```
164    pub supplement: Smart<Option<Supplement>>,
165
166    /// Whether the heading should appear in the @outline[outline].
167    ///
168    /// Note that this property, if set to `{true}`, ensures the heading is also
169    /// shown as a bookmark in the exported PDF's outline (when exporting to
170    /// PDF). To change that behavior, use the `bookmarked` property.
171    ///
172    /// ```example
173    /// #outline()
174    ///
175    /// #heading[Normal]
176    /// This is a normal heading.
177    ///
178    /// #heading(outlined: false)[Hidden]
179    /// This heading does not appear
180    /// in the outline.
181    /// ```
182    #[default(true)]
183    pub outlined: bool,
184
185    /// Whether the heading should appear as a bookmark in the exported PDF's
186    /// outline. Doesn't affect other export formats, such as PNG.
187    ///
188    /// The default value of `{auto}` indicates that the heading will only
189    /// appear in the exported PDF's outline if its `outlined` property is set
190    /// to `{true}`, that is, if it would also be listed in Typst's
191    /// @outline[outline]. Setting this property to either `{true}` (bookmark)
192    /// or `{false}` (don't bookmark) bypasses that behavior.
193    ///
194    /// ```example
195    /// #heading[Normal heading]
196    /// This heading will be shown in
197    /// the PDF's bookmark outline.
198    ///
199    /// #heading(bookmarked: false)[Not bookmarked]
200    /// This heading won't be
201    /// bookmarked in the resulting
202    /// PDF.
203    /// ```
204    #[default(Smart::Auto)]
205    pub bookmarked: Smart<bool>,
206
207    /// The indent all but the first line of a heading should have.
208    ///
209    /// The default value of `{auto}` uses the width of the numbering as indent
210    /// if the heading is aligned at the @direction.start[start] of the
211    /// @text.dir[text direction], and no indent for center and other
212    /// alignments.
213    ///
214    /// ```example
215    /// #set heading(numbering: "1.")
216    /// = A very, very, very, very, very, very long heading
217    ///
218    /// #show heading: set align(center)
219    /// == A very long heading\ with center alignment
220    /// ```
221    #[default(Smart::Auto)]
222    pub hanging_indent: Smart<Length>,
223
224    /// The heading's title.
225    #[required]
226    pub body: Content,
227}
228
229impl HeadingElem {
230    pub fn resolve_level(&self, styles: StyleChain) -> NonZeroUsize {
231        self.level.get(styles).unwrap_or_else(|| {
232            NonZeroUsize::new(self.offset.get(styles) + self.depth.get(styles).get())
233                .expect("overflow to 0 on NoneZeroUsize + usize")
234        })
235    }
236}
237
238impl Synthesize for Packed<HeadingElem> {
239    fn synthesize(
240        &mut self,
241        engine: &mut Engine,
242        styles: StyleChain,
243    ) -> SourceResult<()> {
244        let supplement = match self.supplement.get_ref(styles) {
245            Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
246            Smart::Custom(None) => Content::empty(),
247            Smart::Custom(Some(supplement)) => {
248                supplement.resolve(engine, styles, [self.clone().pack()])?
249            }
250        };
251
252        if let Some((numbering, location)) =
253            self.numbering.get_ref(styles).as_ref().zip(self.location())
254            // We are not early returning on error here because of
255            // https://github.com/typst/typst/issues/7428
256            //
257            // A more comprehensive fix might introduce the error catching logic
258            // of show rules for synthesis, too.
259            && let Ok(numbers) = self.counter().display_at(
260                engine,
261                location,
262                styles,
263                numbering,
264                self.span(),
265            )
266        {
267            self.numbers = Some(numbers.plain_text());
268        }
269
270        let elem = self.as_mut();
271        elem.level.set(Smart::Custom(elem.resolve_level(styles)));
272        elem.supplement
273            .set(Smart::Custom(Some(Supplement::Content(supplement))));
274        Ok(())
275    }
276}
277
278impl ShowSet for Packed<HeadingElem> {
279    fn show_set(&self, styles: StyleChain) -> Styles {
280        let level = self.resolve_level(styles).get();
281        let scale = match level {
282            1 => 1.4,
283            2 => 1.2,
284            _ => 1.0,
285        };
286
287        let size = Em::new(scale);
288        let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale;
289        let below = Em::new(0.75) / scale;
290
291        let mut out = Styles::new();
292        out.set(TextElem::size, TextSize(size.into()));
293        out.set(TextElem::weight, FontWeight::BOLD);
294        out.set(BlockElem::above, Smart::Custom(above.into()));
295        out.set(BlockElem::below, Smart::Custom(below.into()));
296        out.set(BlockElem::sticky, true);
297        out
298    }
299}
300
301impl Count for Packed<HeadingElem> {
302    fn update(&self) -> Option<CounterUpdate> {
303        self.numbering
304            .get_ref(StyleChain::default())
305            .is_some()
306            .then(|| CounterUpdate::Step(self.resolve_level(StyleChain::default())))
307    }
308}
309
310impl Refable for Packed<HeadingElem> {
311    fn supplement(&self) -> Content {
312        // After synthesis, this should always be custom content.
313        match self.supplement.get_cloned(StyleChain::default()) {
314            Smart::Custom(Some(Supplement::Content(content))) => content,
315            _ => Content::empty(),
316        }
317    }
318
319    fn counter(&self) -> Counter {
320        Counter::of(HeadingElem::ELEM)
321    }
322
323    fn numbering(&self) -> Option<&Numbering> {
324        self.numbering.get_ref(StyleChain::default()).as_ref()
325    }
326}
327
328impl Outlinable for Packed<HeadingElem> {
329    fn outlined(&self) -> bool {
330        self.outlined.get(StyleChain::default())
331    }
332
333    fn level(&self) -> NonZeroUsize {
334        self.resolve_level(StyleChain::default())
335    }
336
337    fn prefix(&self, numbers: Content) -> Content {
338        numbers
339    }
340
341    fn body(&self) -> Content {
342        self.body.clone()
343    }
344}
345
346impl LocalName for Packed<HeadingElem> {
347    const KEY: &'static str = "heading";
348}