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`]($title) element
23/// instead.
24///
25/// Typst can automatically number your headings for you. To enable numbering,
26/// specify how you want your headings to be numbered with a
27/// [numbering pattern or function]($numbering).
28///
29/// Independently of the numbering, Typst can also automatically generate an
30/// [outline] of all headings for you. To exclude one or more headings from this
31/// outline, you can set the `outlined` parameter to `{false}`.
32///
33/// When writing a [show rule]($styling/#show-rules) that accesses the
34/// [`body` field]($heading.body) to create a completely custom look for
35/// headings, make sure to wrap the content in a [`block`]($block) (which is
36/// implicitly [sticky]($block.sticky) for headings through a built-in show-set
37/// rule). This prevents headings from becoming "orphans", i.e. remaining
38/// at the end of the page with the following content being on the next page.
39///
40/// # Example
41/// ```example
42/// #set heading(numbering: "1.a)")
43///
44/// = Introduction
45/// In recent years, ...
46///
47/// == Preliminaries
48/// To start, ...
49/// ```
50///
51/// # Syntax
52/// Headings have dedicated syntax: They can be created by starting a line with
53/// one or multiple equals signs, followed by a space. The number of equals
54/// signs determines the heading's logical nesting depth. The `{offset}` field
55/// can be set to configure the starting depth.
56///
57/// # Accessibility
58/// Headings are important for accessibility, as they help users of Assistive
59/// Technologies (AT) like screen readers to navigate within your document.
60/// Screen reader users will be able to skip from heading to heading, or get an
61/// overview of all headings in the document.
62///
63/// To make your headings accessible, you should not skip heading levels. This
64/// means that you should start with a first-level heading. Also, when the
65/// previous heading was of level 3, the next heading should be of level 3
66/// (staying at the same depth), level 4 (going exactly one level deeper), or
67/// level 1 or 2 (new hierarchically higher headings).
68///
69/// # HTML export
70/// As mentioned above, a top-level heading indicates a top-level section of
71/// the document rather than its title. This is in contrast to the HTML `<h1>`
72/// element of which there should be only one per document.
73///
74/// For this reason, in HTML export, a [`title`] element will turn into an
75/// `<h1>` and headings turn into `<h2>` and lower (a level 1 heading thus turns
76/// into `<h2>`, a level 2 heading into `<h3>`, etc).
77#[elem(Locatable, Tagged, Synthesize, Count, ShowSet, LocalName, Refable, Outlinable)]
78pub struct HeadingElem {
79 /// The absolute nesting depth of the heading, starting from one. If set
80 /// to `{auto}`, it is computed from `{offset + depth}`.
81 ///
82 /// This is primarily useful for usage in [show rules]($styling/#show-rules)
83 /// (either with [`where`]($function.where) selectors or by accessing the
84 /// level directly on 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 pattern or function]($numbering) 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].
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 [outline].
191 /// Setting this property to either `{true}` (bookmark) or `{false}` (don't
192 /// 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 [start]($direction.start) of the [text
211 /// direction]($text.dir), and no indent for center and other alignments.
212 ///
213 /// ```example
214 /// #set heading(numbering: "1.")
215 /// = A very, very, very, very, very, very long heading
216 ///
217 /// #show heading: set align(center)
218 /// == A very long heading\ with center alignment
219 /// ```
220 #[default(Smart::Auto)]
221 pub hanging_indent: Smart<Length>,
222
223 /// The heading's title.
224 #[required]
225 pub body: Content,
226}
227
228impl HeadingElem {
229 pub fn resolve_level(&self, styles: StyleChain) -> NonZeroUsize {
230 self.level.get(styles).unwrap_or_else(|| {
231 NonZeroUsize::new(self.offset.get(styles) + self.depth.get(styles).get())
232 .expect("overflow to 0 on NoneZeroUsize + usize")
233 })
234 }
235}
236
237impl Synthesize for Packed<HeadingElem> {
238 fn synthesize(
239 &mut self,
240 engine: &mut Engine,
241 styles: StyleChain,
242 ) -> SourceResult<()> {
243 let supplement = match self.supplement.get_ref(styles) {
244 Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
245 Smart::Custom(None) => Content::empty(),
246 Smart::Custom(Some(supplement)) => {
247 supplement.resolve(engine, styles, [self.clone().pack()])?
248 }
249 };
250
251 if let Some((numbering, location)) =
252 self.numbering.get_ref(styles).as_ref().zip(self.location())
253 // We are not early returning on error here because of
254 // https://github.com/typst/typst/issues/7428
255 //
256 // A more comprehensive fix might introduce the error catching logic
257 // of show rules for synthesis, too.
258 && let Ok(numbers) = self.counter().display_at_loc(
259 engine,
260 location,
261 styles,
262 numbering,
263 )
264 {
265 self.numbers = Some(numbers.plain_text());
266 }
267
268 let elem = self.as_mut();
269 elem.level.set(Smart::Custom(elem.resolve_level(styles)));
270 elem.supplement
271 .set(Smart::Custom(Some(Supplement::Content(supplement))));
272 Ok(())
273 }
274}
275
276impl ShowSet for Packed<HeadingElem> {
277 fn show_set(&self, styles: StyleChain) -> Styles {
278 let level = self.resolve_level(styles).get();
279 let scale = match level {
280 1 => 1.4,
281 2 => 1.2,
282 _ => 1.0,
283 };
284
285 let size = Em::new(scale);
286 let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale;
287 let below = Em::new(0.75) / scale;
288
289 let mut out = Styles::new();
290 out.set(TextElem::size, TextSize(size.into()));
291 out.set(TextElem::weight, FontWeight::BOLD);
292 out.set(BlockElem::above, Smart::Custom(above.into()));
293 out.set(BlockElem::below, Smart::Custom(below.into()));
294 out.set(BlockElem::sticky, true);
295 out
296 }
297}
298
299impl Count for Packed<HeadingElem> {
300 fn update(&self) -> Option<CounterUpdate> {
301 self.numbering
302 .get_ref(StyleChain::default())
303 .is_some()
304 .then(|| CounterUpdate::Step(self.resolve_level(StyleChain::default())))
305 }
306}
307
308impl Refable for Packed<HeadingElem> {
309 fn supplement(&self) -> Content {
310 // After synthesis, this should always be custom content.
311 match self.supplement.get_cloned(StyleChain::default()) {
312 Smart::Custom(Some(Supplement::Content(content))) => content,
313 _ => Content::empty(),
314 }
315 }
316
317 fn counter(&self) -> Counter {
318 Counter::of(HeadingElem::ELEM)
319 }
320
321 fn numbering(&self) -> Option<&Numbering> {
322 self.numbering.get_ref(StyleChain::default()).as_ref()
323 }
324}
325
326impl Outlinable for Packed<HeadingElem> {
327 fn outlined(&self) -> bool {
328 self.outlined.get(StyleChain::default())
329 }
330
331 fn level(&self) -> NonZeroUsize {
332 self.resolve_level(StyleChain::default())
333 }
334
335 fn prefix(&self, numbers: Content) -> Content {
336 numbers
337 }
338
339 fn body(&self) -> Content {
340 self.body.clone()
341 }
342}
343
344impl LocalName for Packed<HeadingElem> {
345 const KEY: &'static str = "heading";
346}