typst_library/text/
mod.rs

1//! Text handling.
2
3mod case;
4mod deco;
5mod font;
6mod item;
7mod lang;
8mod linebreak;
9#[path = "lorem.rs"]
10mod lorem_;
11mod raw;
12mod shift;
13#[path = "smallcaps.rs"]
14mod smallcaps_;
15mod smartquote;
16mod space;
17
18pub use self::case::*;
19pub use self::deco::*;
20pub use self::font::*;
21pub use self::item::*;
22pub use self::lang::*;
23pub use self::linebreak::*;
24pub use self::lorem_::*;
25pub use self::raw::*;
26pub use self::shift::*;
27pub use self::smallcaps_::*;
28pub use self::smartquote::*;
29pub use self::space::*;
30
31use std::fmt::{self, Debug, Formatter};
32use std::hash::Hash;
33use std::str::FromStr;
34use std::sync::LazyLock;
35
36use ecow::{EcoString, eco_format};
37use icu_properties::sets::CodePointSetData;
38use icu_provider::AsDeserializingBufferProvider;
39use icu_provider_blob::BlobDataProvider;
40use rustybuzz::Feature;
41use smallvec::SmallVec;
42use ttf_parser::Tag;
43use typst_syntax::Spanned;
44use typst_utils::singleton;
45
46use crate::World;
47use crate::diag::{HintedStrResult, SourceResult, StrResult, bail, warning};
48use crate::engine::Engine;
49use crate::foundations::{
50    Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue, NativeElement, Never,
51    NoneValue, Packed, PlainText, Regex, Repr, Resolve, Scope, Set, Smart, StyleChain,
52    cast, dict, elem,
53};
54use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
55use crate::math::{EquationElem, MathSize};
56use crate::visualize::{Color, Paint, RelativeTo, Stroke};
57
58/// Hook up all `text` definitions.
59pub(super) fn define(global: &mut Scope) {
60    global.start_category(crate::Category::Text);
61    global.define_elem::<TextElem>();
62    global.define_elem::<LinebreakElem>();
63    global.define_elem::<SmartQuoteElem>();
64    global.define_elem::<SubElem>();
65    global.define_elem::<SuperElem>();
66    global.define_elem::<UnderlineElem>();
67    global.define_elem::<OverlineElem>();
68    global.define_elem::<StrikeElem>();
69    global.define_elem::<HighlightElem>();
70    global.define_elem::<SmallcapsElem>();
71    global.define_elem::<RawElem>();
72    global.define_func::<lower>();
73    global.define_func::<upper>();
74    global.define_func::<lorem>();
75    global.reset_category();
76}
77
78/// Customizes the look and layout of text in a variety of ways.
79///
80/// This function is used frequently, both with set rules and directly. While
81/// the set rule is often the simpler choice, calling the `text` function
82/// directly can be useful when passing text as an argument to another function.
83///
84/// # Example
85/// ```example
86/// #set text(18pt)
87/// With a set rule.
88///
89/// #emph(text(blue)[
90///   With a function call.
91/// ])
92/// ```
93#[elem(Debug, Construct, PlainText, Repr)]
94pub struct TextElem {
95    /// A font family descriptor or priority list of font family descriptors.
96    ///
97    /// A font family descriptor can be a plain string representing the family
98    /// name or a dictionary with the following keys:
99    ///
100    /// - `name` (required): The font family name.
101    /// - `covers` (optional): Defines the Unicode codepoints for which the
102    ///   family shall be used. This can be:
103    ///   - A predefined coverage set:
104    ///     - `{"latin-in-cjk"}` covers all codepoints except for those which
105    ///       exist in Latin fonts, but should preferably be taken from CJK
106    ///       fonts.
107    ///   - A [regular expression]($regex) that defines exactly which codepoints
108    ///     shall be covered. Accepts only the subset of regular expressions
109    ///     which consist of exactly one dot, letter, or character class.
110    ///
111    /// When processing text, Typst tries all specified font families in order
112    /// until it finds a font that has the necessary glyphs. In the example
113    /// below, the font `Inria Serif` is preferred, but since it does not
114    /// contain Arabic glyphs, the arabic text uses `Noto Sans Arabic` instead.
115    ///
116    /// The collection of available fonts differs by platform:
117    ///
118    /// - In the web app, you can see the list of available fonts by clicking on
119    ///   the "Ag" button. You can provide additional fonts by uploading `.ttf`
120    ///   or `.otf` files into your project. They will be discovered
121    ///   automatically. The priority is: project fonts > server fonts.
122    ///
123    /// - Locally, Typst uses your installed system fonts or embedded fonts in
124    ///   the CLI, which are `Libertinus Serif`, `New Computer Modern`,
125    ///   `New Computer Modern Math`, and `DejaVu Sans Mono`. In addition, you
126    ///   can use the `--font-path` argument or `TYPST_FONT_PATHS` environment
127    ///   variable to add directories that should be scanned for fonts. The
128    ///   priority is: `--font-paths` > system fonts > embedded fonts. Run
129    ///   `typst fonts` to see the fonts that Typst has discovered on your
130    ///   system. Note that you can pass the `--ignore-system-fonts` parameter
131    ///   to the CLI to ensure Typst won't search for system fonts.
132    ///
133    /// ```example
134    /// #set text(font: "PT Sans")
135    /// This is sans-serif.
136    ///
137    /// #set text(font: (
138    ///   "Inria Serif",
139    ///   "Noto Sans Arabic",
140    /// ))
141    ///
142    /// This is Latin. \
143    /// هذا عربي.
144    ///
145    /// // Change font only for numbers.
146    /// #set text(font: (
147    ///   (name: "PT Sans", covers: regex("[0-9]")),
148    ///   "Libertinus Serif"
149    /// ))
150    ///
151    /// The number 123.
152    ///
153    /// // Mix Latin and CJK fonts.
154    /// #set text(font: (
155    ///   (name: "Inria Serif", covers: "latin-in-cjk"),
156    ///   "Noto Serif CJK SC"
157    /// ))
158    /// 分别设置“中文”和English字体
159    /// ```
160    #[parse({
161        let font_list: Option<Spanned<FontList>> = args.named("font")?;
162        if let Some(list) = &font_list {
163            check_font_list(engine, list);
164        }
165        font_list.map(|font_list| font_list.v)
166    })]
167    #[default(FontList(vec![FontFamily::new("Libertinus Serif")]))]
168    #[ghost]
169    pub font: FontList,
170
171    /// Whether to allow last resort font fallback when the primary font list
172    /// contains no match. This lets Typst search through all available fonts
173    /// for the most similar one that has the necessary glyphs.
174    ///
175    /// _Note:_ Currently, there are no warnings when fallback is disabled and
176    /// no glyphs are found. Instead, your text shows up in the form of "tofus":
177    /// Small boxes that indicate the lack of an appropriate glyph. In the
178    /// future, you will be able to instruct Typst to issue warnings so you know
179    /// something is up.
180    ///
181    /// ```example
182    /// #set text(font: "Inria Serif")
183    /// هذا عربي
184    ///
185    /// #set text(fallback: false)
186    /// هذا عربي
187    /// ```
188    #[default(true)]
189    #[ghost]
190    pub fallback: bool,
191
192    /// The desired font style.
193    ///
194    /// When an italic style is requested and only an oblique one is available,
195    /// it is used. Similarly, the other way around, an italic style can stand
196    /// in for an oblique one.  When neither an italic nor an oblique style is
197    /// available, Typst selects the normal style. Since most fonts are only
198    /// available either in an italic or oblique style, the difference between
199    /// italic and oblique style is rarely observable.
200    ///
201    /// If you want to emphasize your text, you should do so using the [emph]
202    /// function instead. This makes it easy to adapt the style later if you
203    /// change your mind about how to signify the emphasis.
204    ///
205    /// ```example
206    /// #text(font: "Libertinus Serif", style: "italic")[Italic]
207    /// #text(font: "DejaVu Sans", style: "oblique")[Oblique]
208    /// ```
209    #[ghost]
210    pub style: FontStyle,
211
212    /// The desired thickness of the font's glyphs. Accepts an integer between
213    /// `{100}` and `{900}` or one of the predefined weight names. When the
214    /// desired weight is not available, Typst selects the font from the family
215    /// that is closest in weight.
216    ///
217    /// If you want to strongly emphasize your text, you should do so using the
218    /// [strong] function instead. This makes it easy to adapt the style later
219    /// if you change your mind about how to signify the strong emphasis.
220    ///
221    /// ```example
222    /// #set text(font: "IBM Plex Sans")
223    ///
224    /// #text(weight: "light")[Light] \
225    /// #text(weight: "regular")[Regular] \
226    /// #text(weight: "medium")[Medium] \
227    /// #text(weight: 500)[Medium] \
228    /// #text(weight: "bold")[Bold]
229    /// ```
230    #[ghost]
231    pub weight: FontWeight,
232
233    /// The desired width of the glyphs. Accepts a ratio between `{50%}` and
234    /// `{200%}`. When the desired width is not available, Typst selects the
235    /// font from the family that is closest in stretch. This will only stretch
236    /// the text if a condensed or expanded version of the font is available.
237    ///
238    /// If you want to adjust the amount of space between characters instead of
239    /// stretching the glyphs itself, use the [`tracking`]($text.tracking)
240    /// property instead.
241    ///
242    /// ```example
243    /// #text(stretch: 75%)[Condensed] \
244    /// #text(stretch: 100%)[Normal]
245    /// ```
246    #[ghost]
247    pub stretch: FontStretch,
248
249    /// The size of the glyphs. This value forms the basis of the `em` unit:
250    /// `{1em}` is equivalent to the font size.
251    ///
252    /// You can also give the font size itself in `em` units. Then, it is
253    /// relative to the previous font size.
254    ///
255    /// ```example
256    /// #set text(size: 20pt)
257    /// very #text(1.5em)[big] text
258    /// ```
259    #[parse(args.named_or_find("size")?)]
260    #[fold]
261    #[default(TextSize(Abs::pt(11.0).into()))]
262    #[ghost]
263    pub size: TextSize,
264
265    /// The glyph fill paint.
266    ///
267    /// ```example
268    /// #set text(fill: red)
269    /// This text is red.
270    /// ```
271    #[parse({
272        let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
273        if let Some(paint) = &paint
274            && paint.v.relative() == Smart::Custom(RelativeTo::Self_) {
275                bail!(
276                    paint.span,
277                    "gradients and tilings on text must be relative to the parent";
278                    hint: "make sure to set `relative: auto` on your text fill"
279                );
280            }
281        paint.map(|paint| paint.v)
282    })]
283    #[default(Color::BLACK.into())]
284    #[ghost]
285    pub fill: Paint,
286
287    /// How to stroke the text.
288    ///
289    /// ```example
290    /// #text(stroke: 0.5pt + red)[Stroked]
291    /// ```
292    #[ghost]
293    pub stroke: Option<Stroke>,
294
295    /// The amount of space that should be added between characters.
296    ///
297    /// ```example
298    /// #set text(tracking: 1.5pt)
299    /// Distant text.
300    /// ```
301    #[ghost]
302    pub tracking: Length,
303
304    /// The amount of space between words.
305    ///
306    /// Can be given as an absolute length, but also relative to the width of
307    /// the space character in the font.
308    ///
309    /// If you want to adjust the amount of space between characters rather than
310    /// words, use the [`tracking`]($text.tracking) property instead.
311    ///
312    /// ```example
313    /// #set text(spacing: 200%)
314    /// Text with distant words.
315    /// ```
316    #[default(Rel::one())]
317    #[ghost]
318    pub spacing: Rel<Length>,
319
320    /// Whether to automatically insert spacing between CJK and Latin characters.
321    ///
322    /// ```example
323    /// #set text(cjk-latin-spacing: auto)
324    /// 第4章介绍了基本的API。
325    ///
326    /// #set text(cjk-latin-spacing: none)
327    /// 第4章介绍了基本的API。
328    /// ```
329    #[ghost]
330    pub cjk_latin_spacing: Smart<Option<Never>>,
331
332    /// An amount to shift the text baseline by.
333    ///
334    /// ```example
335    /// A #text(baseline: 3pt)[lowered]
336    /// word.
337    /// ```
338    #[ghost]
339    pub baseline: Length,
340
341    /// Whether certain glyphs can hang over into the margin in justified text.
342    /// This can make justification visually more pleasing.
343    ///
344    /// ```example
345    /// #set page(width: 220pt)
346    ///
347    /// #set par(justify: true)
348    /// This justified text has a hyphen in
349    /// the paragraph's second line. Hanging
350    /// the hyphen slightly into the margin
351    /// results in a clearer paragraph edge.
352    ///
353    /// #set text(overhang: false)
354    /// This justified text has a hyphen in
355    /// the paragraph's second line. Hanging
356    /// the hyphen slightly into the margin
357    /// results in a clearer paragraph edge.
358    /// ```
359    #[default(true)]
360    #[ghost]
361    pub overhang: bool,
362
363    /// The top end of the conceptual frame around the text used for layout and
364    /// positioning. This affects the size of containers that hold text.
365    ///
366    /// ```example
367    /// #set rect(inset: 0pt)
368    /// #set text(size: 20pt)
369    ///
370    /// #set text(top-edge: "ascender")
371    /// #rect(fill: aqua)[Typst]
372    ///
373    /// #set text(top-edge: "cap-height")
374    /// #rect(fill: aqua)[Typst]
375    /// ```
376    #[default(TopEdge::Metric(TopEdgeMetric::CapHeight))]
377    #[ghost]
378    pub top_edge: TopEdge,
379
380    /// The bottom end of the conceptual frame around the text used for layout
381    /// and positioning. This affects the size of containers that hold text.
382    ///
383    /// ```example
384    /// #set rect(inset: 0pt)
385    /// #set text(size: 20pt)
386    ///
387    /// #set text(bottom-edge: "baseline")
388    /// #rect(fill: aqua)[Typst]
389    ///
390    /// #set text(bottom-edge: "descender")
391    /// #rect(fill: aqua)[Typst]
392    /// ```
393    #[default(BottomEdge::Metric(BottomEdgeMetric::Baseline))]
394    #[ghost]
395    pub bottom_edge: BottomEdge,
396
397    /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639)
398    ///
399    /// Setting the correct language affects various parts of Typst:
400    ///
401    /// - The text processing pipeline can make more informed choices.
402    /// - Hyphenation will use the correct patterns for the language.
403    /// - [Smart quotes]($smartquote) turns into the correct quotes for the
404    ///   language.
405    /// - And all other things which are language-aware.
406    ///
407    /// Choosing the correct language is important for accessibility. For
408    /// example, screen readers will use it to choose a voice that matches the
409    /// language of the text. If your document is in another language than
410    /// English (the default), you should set the text language at the start of
411    /// your document, before any other content. You can, for example, put it
412    /// right after the `[#set document(/* ... */)]` rule that [sets your
413    /// document's title]($document.title).
414    ///
415    /// If your document contains passages in a different language than the main
416    /// language, you should locally change the text language just for those parts,
417    /// either with a set rule [scoped to a block]($scripting/#blocks) or using
418    /// a direct text function call such as `[#text(lang: "de")[...]]`.
419    ///
420    /// If multiple codes are available for your language, you should prefer the
421    /// two-letter code (ISO 639-1) over the three-letter codes (ISO 639-2/3).
422    /// When you have to use a three-letter code and your language differs
423    /// between ISO 639-2 and ISO 639-3, use ISO 639-2 for PDF 1.7 (Typst's
424    /// default for PDF export) and below and ISO 639-3 for PDF 2.0 and HTML
425    /// export.
426    ///
427    /// The language code is case-insensitive, and will be lowercased when
428    /// accessed through [context]($context).
429    ///
430    /// ```example:"Setting the text language to German"
431    /// #set text(lang: "de")
432    /// #outline()
433    ///
434    /// = Einleitung
435    /// In diesem Dokument, ...
436    /// ```
437    #[default(Lang::ENGLISH)]
438    #[ghost]
439    pub lang: Lang,
440
441    /// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
442    ///
443    /// This lets the text processing pipeline make more informed choices.
444    ///
445    /// The region code is case-insensitive, and will be uppercased when
446    /// accessed through [context]($context).
447    #[ghost]
448    pub region: Option<Region>,
449
450    /// The OpenType writing script.
451    ///
452    /// The combination of `{lang}` and `{script}` determine how font features,
453    /// such as glyph substitution, are implemented. Frequently the value is a
454    /// modified (all-lowercase) ISO 15924 script identifier, and the `math`
455    /// writing script is used for features appropriate for mathematical
456    /// symbols.
457    ///
458    /// When set to `{auto}`, the default and recommended setting, an
459    /// appropriate script is chosen for each block of characters sharing a
460    /// common Unicode script property.
461    ///
462    /// ```example
463    /// #set text(
464    ///   font: "Libertinus Serif",
465    ///   size: 20pt,
466    /// )
467    ///
468    /// #let scedilla = [Ş]
469    /// #scedilla // S with a cedilla
470    ///
471    /// #set text(lang: "ro", script: "latn")
472    /// #scedilla // S with a subscript comma
473    ///
474    /// #set text(lang: "ro", script: "grek")
475    /// #scedilla // S with a cedilla
476    /// ```
477    #[ghost]
478    pub script: Smart<WritingScript>,
479
480    /// The dominant direction for text and inline objects. Possible values are:
481    ///
482    /// - `{auto}`: Automatically infer the direction from the `lang` property.
483    /// - `{ltr}`: Layout text from left to right.
484    /// - `{rtl}`: Layout text from right to left.
485    ///
486    /// When writing in right-to-left scripts like Arabic or Hebrew, you should
487    /// set the [text language]($text.lang) or direction. While individual runs
488    /// of text are automatically layouted in the correct direction, setting the
489    /// dominant direction gives the bidirectional reordering algorithm the
490    /// necessary information to correctly place punctuation and inline objects.
491    /// Furthermore, setting the direction affects the alignment values `start`
492    /// and `end`, which are equivalent to `left` and `right` in `ltr` text and
493    /// the other way around in `rtl` text.
494    ///
495    /// If you set this to `rtl` and experience bugs or in some way bad looking
496    /// output, please get in touch with us through the
497    /// [Forum](https://forum.typst.app/),
498    /// [Discord server](https://discord.gg/2uDybryKPe),
499    /// or our [contact form](https://typst.app/contact).
500    ///
501    /// ```example
502    /// #set text(dir: rtl)
503    /// هذا عربي.
504    /// ```
505    #[ghost]
506    pub dir: TextDir,
507
508    /// Whether to hyphenate text to improve line breaking. When `{auto}`, text
509    /// will be hyphenated if and only if justification is enabled.
510    ///
511    /// Setting the [text language]($text.lang) ensures that the correct
512    /// hyphenation patterns are used.
513    ///
514    /// ```example
515    /// #set page(width: 200pt)
516    ///
517    /// #set par(justify: true)
518    /// This text illustrates how
519    /// enabling hyphenation can
520    /// improve justification.
521    ///
522    /// #set text(hyphenate: false)
523    /// This text illustrates how
524    /// enabling hyphenation can
525    /// improve justification.
526    /// ```
527    #[ghost]
528    pub hyphenate: Smart<bool>,
529
530    /// The "cost" of various choices when laying out text. A higher cost means
531    /// the layout engine will make the choice less often. Costs are specified
532    /// as a ratio of the default cost, so `{50%}` will make text layout twice
533    /// as eager to make a given choice, while `{200%}` will make it half as
534    /// eager.
535    ///
536    /// Currently, the following costs can be customized:
537    /// - `hyphenation`: splitting a word across multiple lines
538    /// - `runt`: ending a paragraph with a line with a single word
539    /// - `widow`: leaving a single line of paragraph on the next page
540    /// - `orphan`: leaving single line of paragraph on the previous page
541    ///
542    /// Hyphenation is generally avoided by placing the whole word on the next
543    /// line, so a higher hyphenation cost can result in awkward justification
544    /// spacing. Note: Hyphenation costs will only be applied when the
545    /// [`linebreaks`]($par.linebreaks) are set to "optimized". (For example
546    /// by default implied by [`justify`]($par.justify).)
547    ///
548    /// Runts are avoided by placing more or fewer words on previous lines, so a
549    /// higher runt cost can result in more awkward in justification spacing.
550    ///
551    /// Text layout prevents widows and orphans by default because they are
552    /// generally discouraged by style guides. However, in some contexts they
553    /// are allowed because the prevention method, which moves a line to the
554    /// next page, can result in an uneven number of lines between pages. The
555    /// `widow` and `orphan` costs allow disabling these modifications.
556    /// (Currently, `{0%}` allows widows/orphans; anything else, including the
557    /// default of `{100%}`, prevents them. More nuanced cost specification for
558    /// these modifications is planned for the future.)
559    ///
560    /// ```example
561    /// #set text(hyphenate: true, size: 11.4pt)
562    /// #set par(justify: true)
563    ///
564    /// #lorem(10)
565    ///
566    /// // Set hyphenation to ten times the normal cost.
567    /// #set text(costs: (hyphenation: 1000%))
568    ///
569    /// #lorem(10)
570    /// ```
571    #[fold]
572    #[ghost]
573    pub costs: Costs,
574
575    /// Whether to apply kerning.
576    ///
577    /// When enabled, specific letter pairings move closer together or further
578    /// apart for a more visually pleasing result. The example below
579    /// demonstrates how decreasing the gap between the "T" and "o" results in a
580    /// more natural look. Setting this to `{false}` disables kerning by turning
581    /// off the OpenType `kern` font feature.
582    ///
583    /// ```example
584    /// #set text(size: 25pt)
585    /// Totally
586    ///
587    /// #set text(kerning: false)
588    /// Totally
589    /// ```
590    #[default(true)]
591    #[ghost]
592    pub kerning: bool,
593
594    /// Whether to apply stylistic alternates.
595    ///
596    /// Sometimes fonts contain alternative glyphs for the same codepoint.
597    /// Setting this to `{true}` switches to these by enabling the OpenType
598    /// `salt` font feature.
599    ///
600    /// ```example
601    /// #set text(
602    ///   font: "IBM Plex Sans",
603    ///   size: 20pt,
604    /// )
605    ///
606    /// 0, a, g, ß
607    ///
608    /// #set text(alternates: true)
609    /// 0, a, g, ß
610    /// ```
611    #[default(false)]
612    #[ghost]
613    pub alternates: bool,
614
615    /// Which stylistic sets to apply. Font designers can categorize alternative
616    /// glyphs forms into stylistic sets. As this value is highly font-specific,
617    /// you need to consult your font to know which sets are available.
618    ///
619    /// This can be set to an integer or an array of integers, all
620    /// of which must be between `{1}` and `{20}`, enabling the
621    /// corresponding OpenType feature(s) from `ss01` to `ss20`.
622    /// Setting this to `{none}` will disable all stylistic sets.
623    ///
624    /// ```example
625    /// #set text(font: "IBM Plex Serif")
626    /// ß vs #text(stylistic-set: 5)[ß] \
627    /// 10 years ago vs #text(stylistic-set: (1, 2, 3))[10 years ago]
628    /// ```
629    #[ghost]
630    pub stylistic_set: StylisticSets,
631
632    /// Whether standard ligatures are active.
633    ///
634    /// Certain letter combinations like "fi" are often displayed as a single
635    /// merged glyph called a _ligature._ Setting this to `{false}` disables
636    /// these ligatures by turning off the OpenType `liga` and `clig` font
637    /// features.
638    ///
639    /// ```example
640    /// #set text(size: 20pt)
641    /// A fine ligature.
642    ///
643    /// #set text(ligatures: false)
644    /// A fine ligature.
645    /// ```
646    ///
647    /// Note that some programming fonts use other OpenType font features to
648    /// implement "ligatures," including the contextual alternates (`calt`)
649    /// feature, which is also enabled by default. Use the general
650    /// [`features`]($text.features) parameter to control such features.
651    #[default(true)]
652    #[ghost]
653    pub ligatures: bool,
654
655    /// Whether ligatures that should be used sparingly are active. Setting this
656    /// to `{true}` enables the OpenType `dlig` font feature.
657    #[default(false)]
658    #[ghost]
659    pub discretionary_ligatures: bool,
660
661    /// Whether historical ligatures are active. Setting this to `{true}`
662    /// enables the OpenType `hlig` font feature.
663    #[default(false)]
664    #[ghost]
665    pub historical_ligatures: bool,
666
667    /// Which kind of numbers / figures to select. When set to `{auto}`, the
668    /// default numbers for the font are used.
669    ///
670    /// ```example
671    /// #set text(font: "Noto Sans", 20pt)
672    /// #set text(number-type: "lining")
673    /// Number 9.
674    ///
675    /// #set text(number-type: "old-style")
676    /// Number 9.
677    /// ```
678    #[ghost]
679    pub number_type: Smart<NumberType>,
680
681    /// The width of numbers / figures. When set to `{auto}`, the default
682    /// numbers for the font are used.
683    ///
684    /// ```example
685    /// #set text(font: "Noto Sans", 20pt)
686    /// #set text(number-width: "proportional")
687    /// A 12 B 34. \
688    /// A 56 B 78.
689    ///
690    /// #set text(number-width: "tabular")
691    /// A 12 B 34. \
692    /// A 56 B 78.
693    /// ```
694    #[ghost]
695    pub number_width: Smart<NumberWidth>,
696
697    /// Whether to have a slash through the zero glyph. Setting this to `{true}`
698    /// enables the OpenType `zero` font feature.
699    ///
700    /// ```example
701    /// 0, #text(slashed-zero: true)[0]
702    /// ```
703    #[default(false)]
704    #[ghost]
705    pub slashed_zero: bool,
706
707    /// Whether to turn numbers into fractions. Setting this to `{true}`
708    /// enables the OpenType `frac` font feature.
709    ///
710    /// It is not advisable to enable this property globally as it will mess
711    /// with all appearances of numbers after a slash (e.g., in URLs). Instead,
712    /// enable it locally when you want a fraction.
713    ///
714    /// ```example
715    /// 1/2 \
716    /// #text(fractions: true)[1/2]
717    /// ```
718    #[default(false)]
719    #[ghost]
720    pub fractions: bool,
721
722    /// Raw OpenType features to apply.
723    ///
724    /// - If given an array of strings, sets the features identified by the
725    ///   strings to `{1}`.
726    /// - If given a dictionary mapping to numbers, sets the features
727    ///   identified by the keys to the values.
728    ///
729    /// ```example:"Give an array of strings"
730    /// // Enable the `frac` feature manually.
731    /// #set text(features: ("frac",))
732    /// 1/2
733    /// ```
734    ///
735    /// ```example:"Give a dictionary mapping to numbers"
736    /// #set text(font: "Cascadia Code")
737    /// =>
738    /// // Disable the contextual alternates (`calt`) feature.
739    /// #set text(features: (calt: 0))
740    /// =>
741    /// ```
742    #[fold]
743    #[ghost]
744    pub features: FontFeatures,
745
746    /// Content in which all text is styled according to the other arguments.
747    #[external]
748    #[required]
749    pub body: Content,
750
751    /// The text.
752    #[required]
753    pub text: EcoString,
754
755    /// The offset of the text in the text syntax node referenced by this
756    /// element's span.
757    #[internal]
758    #[ghost]
759    pub span_offset: usize,
760
761    /// A delta to apply on the font weight.
762    #[internal]
763    #[fold]
764    #[ghost]
765    pub delta: WeightDelta,
766
767    /// Whether the font style should be inverted.
768    #[internal]
769    #[fold]
770    #[default(ItalicToggle(false))]
771    #[ghost]
772    pub emph: ItalicToggle,
773
774    /// Decorative lines.
775    #[internal]
776    #[fold]
777    #[ghost]
778    pub deco: SmallVec<[Decoration; 1]>,
779
780    /// A case transformation that should be applied to the text.
781    #[internal]
782    #[ghost]
783    pub case: Option<Case>,
784
785    /// Whether small capital glyphs should be used. ("smcp", "c2sc")
786    #[internal]
787    #[ghost]
788    pub smallcaps: Option<Smallcaps>,
789
790    /// The configuration for superscripts or subscripts, if one of them is
791    /// enabled.
792    #[internal]
793    #[ghost]
794    pub shift_settings: Option<ShiftSettings>,
795}
796
797impl TextElem {
798    /// Create a new packed text element.
799    pub fn packed(text: impl Into<EcoString>) -> Content {
800        Self::new(text.into()).pack()
801    }
802}
803
804impl Debug for TextElem {
805    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
806        write!(f, "Text({})", self.text)
807    }
808}
809
810impl Repr for TextElem {
811    fn repr(&self) -> EcoString {
812        eco_format!("[{}]", self.text)
813    }
814}
815
816impl Construct for TextElem {
817    fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> {
818        // The text constructor is special: It doesn't create a text element.
819        // Instead, it leaves the passed argument structurally unchanged, but
820        // styles all text in it.
821        let styles = Self::set(engine, args)?;
822        let body = args.expect::<Content>("body")?;
823        Ok(body.styled_with_map(styles))
824    }
825}
826
827impl PlainText for Packed<TextElem> {
828    fn plain_text(&self, text: &mut EcoString) {
829        text.push_str(&self.text);
830    }
831}
832
833/// A lowercased font family like "arial".
834#[derive(Debug, Clone, PartialEq, Hash)]
835pub struct FontFamily {
836    // The name of the font family
837    name: EcoString,
838    // A regex that defines the Unicode codepoints supported by the font.
839    covers: Option<Covers>,
840}
841
842impl FontFamily {
843    /// Create a named font family variant.
844    pub fn new(string: &str) -> Self {
845        Self::with_coverage(string, None)
846    }
847
848    /// Create a font family by name and optional Unicode coverage.
849    pub fn with_coverage(string: &str, covers: Option<Covers>) -> Self {
850        Self { name: string.to_lowercase().into(), covers }
851    }
852
853    /// The lowercased family name.
854    pub fn as_str(&self) -> &str {
855        &self.name
856    }
857
858    /// The user-set coverage of the font family.
859    pub fn covers(&self) -> Option<&Regex> {
860        self.covers.as_ref().map(|covers| covers.as_regex())
861    }
862}
863
864cast! {
865    FontFamily,
866    self => self.name.into_value(),
867    string: EcoString => Self::new(&string),
868    mut v: Dict => {
869        let ret = Self::with_coverage(
870            &v.take("name")?.cast::<EcoString>()?,
871            v.take("covers").ok().map(|v| v.cast()).transpose()?
872        );
873        v.finish(&["name", "covers"])?;
874        ret
875    },
876}
877
878/// Defines which codepoints a font family will be used for.
879#[derive(Debug, Clone, PartialEq, Hash)]
880pub enum Covers {
881    /// Covers all codepoints except those used both in Latin and CJK fonts.
882    LatinInCjk,
883    /// Covers the set of codepoints for which the regex matches.
884    Regex(Regex),
885}
886
887impl Covers {
888    /// Retrieve the regex for the coverage.
889    pub fn as_regex(&self) -> &Regex {
890        match self {
891            Self::LatinInCjk => singleton!(
892                Regex,
893                Regex::new(
894                    "[^\u{00B7}\u{2013}\u{2014}\u{2018}\u{2019}\
895                       \u{201C}\u{201D}\u{2025}-\u{2027}\u{2E3A}]"
896                )
897                .unwrap()
898            ),
899            Self::Regex(regex) => regex,
900        }
901    }
902}
903
904cast! {
905    Covers,
906    self => match self {
907        Self::LatinInCjk => "latin-in-cjk".into_value(),
908        Self::Regex(regex) => regex.into_value(),
909    },
910
911    /// Covers all codepoints except those used both in Latin and CJK fonts.
912    "latin-in-cjk" => Covers::LatinInCjk,
913
914    regex: Regex => {
915        let ast = regex_syntax::ast::parse::Parser::new().parse(regex.as_str());
916        match ast {
917            Ok(
918                regex_syntax::ast::Ast::ClassBracketed(..)
919                | regex_syntax::ast::Ast::ClassUnicode(..)
920                | regex_syntax::ast::Ast::ClassPerl(..)
921                | regex_syntax::ast::Ast::Dot(..)
922                | regex_syntax::ast::Ast::Literal(..),
923            ) => {}
924            _ => bail!(
925                "coverage regex may only use dot, letters, and character classes";
926                hint: "the regex is applied to each letter individually"
927            ),
928        }
929        Covers::Regex(regex)
930    },
931}
932
933/// Font family fallback list.
934///
935/// Must contain at least one font.
936#[derive(Debug, Default, Clone, PartialEq, Hash)]
937pub struct FontList(pub Vec<FontFamily>);
938
939impl FontList {
940    pub fn new(fonts: Vec<FontFamily>) -> StrResult<Self> {
941        if fonts.is_empty() {
942            bail!("font fallback list must not be empty")
943        } else {
944            Ok(Self(fonts))
945        }
946    }
947}
948
949impl<'a> IntoIterator for &'a FontList {
950    type IntoIter = std::slice::Iter<'a, FontFamily>;
951    type Item = &'a FontFamily;
952
953    fn into_iter(self) -> Self::IntoIter {
954        self.0.iter()
955    }
956}
957
958cast! {
959    FontList,
960    self => if self.0.len() == 1 {
961        self.0.into_iter().next().unwrap().name.into_value()
962    } else {
963        self.0.into_value()
964    },
965    family: FontFamily => Self(vec![family]),
966    values: Array => Self::new(values.into_iter().map(|v| v.cast()).collect::<HintedStrResult<_>>()?)?,
967}
968
969/// Resolve a prioritized iterator over the font families.
970pub fn families(styles: StyleChain<'_>) -> impl Iterator<Item = &'_ FontFamily> + Clone {
971    let fallbacks = singleton!(Vec<FontFamily>, {
972        [
973            "libertinus serif",
974            "twitter color emoji",
975            "noto color emoji",
976            "apple color emoji",
977            "segoe ui emoji",
978        ]
979        .into_iter()
980        .map(FontFamily::new)
981        .collect()
982    });
983
984    let tail = if styles.get(TextElem::fallback) { fallbacks.as_slice() } else { &[] };
985    styles.get_ref(TextElem::font).into_iter().chain(tail.iter())
986}
987
988/// Resolve the font variant.
989pub fn variant(styles: StyleChain) -> FontVariant {
990    let mut variant = FontVariant::new(
991        styles.get(TextElem::style),
992        styles.get(TextElem::weight),
993        styles.get(TextElem::stretch),
994    );
995
996    let WeightDelta(delta) = styles.get(TextElem::delta);
997    variant.weight = variant
998        .weight
999        .thicken(delta.clamp(i16::MIN as i64, i16::MAX as i64) as i16);
1000
1001    if styles.get(TextElem::emph).0 {
1002        variant.style = match variant.style {
1003            FontStyle::Normal => FontStyle::Italic,
1004            FontStyle::Italic => FontStyle::Normal,
1005            FontStyle::Oblique => FontStyle::Normal,
1006        }
1007    }
1008
1009    variant
1010}
1011
1012/// The size of text.
1013#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1014pub struct TextSize(pub Length);
1015
1016impl Fold for TextSize {
1017    fn fold(self, outer: Self) -> Self {
1018        // Multiply the two linear functions.
1019        Self(Length {
1020            em: Em::new(self.0.em.get() * outer.0.em.get()),
1021            abs: self.0.em.get() * outer.0.abs + self.0.abs,
1022        })
1023    }
1024}
1025
1026impl Resolve for TextSize {
1027    type Output = Abs;
1028
1029    fn resolve(self, styles: StyleChain) -> Self::Output {
1030        let factor = match styles.get(EquationElem::size) {
1031            MathSize::Display | MathSize::Text => 1.0,
1032            MathSize::Script => styles.get(EquationElem::script_scale).0 as f64 / 100.0,
1033            MathSize::ScriptScript => {
1034                styles.get(EquationElem::script_scale).1 as f64 / 100.0
1035            }
1036        };
1037        factor * self.0.resolve(styles)
1038    }
1039}
1040
1041cast! {
1042    TextSize,
1043    self => self.0.into_value(),
1044    v: Length => Self(v),
1045}
1046
1047/// Specifies the top edge of text.
1048#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1049pub enum TopEdge {
1050    /// An edge specified via font metrics or bounding box.
1051    Metric(TopEdgeMetric),
1052    /// An edge specified as a length.
1053    Length(Length),
1054}
1055
1056cast! {
1057    TopEdge,
1058    self => match self {
1059        Self::Metric(metric) => metric.into_value(),
1060        Self::Length(length) => length.into_value(),
1061    },
1062    v: TopEdgeMetric => Self::Metric(v),
1063    v: Length => Self::Length(v),
1064}
1065
1066/// Metrics that describe the top edge of text.
1067#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
1068pub enum TopEdgeMetric {
1069    /// The font's ascender, which typically exceeds the height of all glyphs.
1070    Ascender,
1071    /// The approximate height of uppercase letters.
1072    CapHeight,
1073    /// The approximate height of non-ascending lowercase letters.
1074    XHeight,
1075    /// The baseline on which the letters rest.
1076    Baseline,
1077    /// The top edge of the glyph's bounding box.
1078    Bounds,
1079}
1080
1081impl TryInto<VerticalFontMetric> for TopEdgeMetric {
1082    type Error = ();
1083
1084    fn try_into(self) -> Result<VerticalFontMetric, Self::Error> {
1085        match self {
1086            Self::Ascender => Ok(VerticalFontMetric::Ascender),
1087            Self::CapHeight => Ok(VerticalFontMetric::CapHeight),
1088            Self::XHeight => Ok(VerticalFontMetric::XHeight),
1089            Self::Baseline => Ok(VerticalFontMetric::Baseline),
1090            _ => Err(()),
1091        }
1092    }
1093}
1094
1095/// Specifies the top edge of text.
1096#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1097pub enum BottomEdge {
1098    /// An edge specified via font metrics or bounding box.
1099    Metric(BottomEdgeMetric),
1100    /// An edge specified as a length.
1101    Length(Length),
1102}
1103
1104cast! {
1105    BottomEdge,
1106    self => match self {
1107        Self::Metric(metric) => metric.into_value(),
1108        Self::Length(length) => length.into_value(),
1109    },
1110    v: BottomEdgeMetric => Self::Metric(v),
1111    v: Length => Self::Length(v),
1112}
1113
1114/// Metrics that describe the bottom edge of text.
1115#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
1116pub enum BottomEdgeMetric {
1117    /// The baseline on which the letters rest.
1118    Baseline,
1119    /// The font's descender, which typically exceeds the depth of all glyphs.
1120    Descender,
1121    /// The bottom edge of the glyph's bounding box.
1122    Bounds,
1123}
1124
1125impl TryInto<VerticalFontMetric> for BottomEdgeMetric {
1126    type Error = ();
1127
1128    fn try_into(self) -> Result<VerticalFontMetric, Self::Error> {
1129        match self {
1130            Self::Baseline => Ok(VerticalFontMetric::Baseline),
1131            Self::Descender => Ok(VerticalFontMetric::Descender),
1132            _ => Err(()),
1133        }
1134    }
1135}
1136
1137/// The direction of text and inline objects in their line.
1138#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
1139pub struct TextDir(pub Smart<Dir>);
1140
1141cast! {
1142    TextDir,
1143    self => self.0.into_value(),
1144    v: Smart<Dir> => {
1145        if v.is_custom_and(|dir| dir.axis() == Axis::Y) {
1146            bail!("text direction must be horizontal");
1147        }
1148        Self(v)
1149    },
1150}
1151
1152impl Resolve for TextDir {
1153    type Output = Dir;
1154
1155    fn resolve(self, styles: StyleChain) -> Self::Output {
1156        match self.0 {
1157            Smart::Auto => styles.get(TextElem::lang).dir(),
1158            Smart::Custom(dir) => dir,
1159        }
1160    }
1161}
1162
1163/// A set of stylistic sets to enable.
1164#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
1165pub struct StylisticSets(u32);
1166
1167impl StylisticSets {
1168    /// Converts this set into a Typst array of values.
1169    pub fn into_array(self) -> Array {
1170        self.sets().map(IntoValue::into_value).collect()
1171    }
1172
1173    /// Returns whether this set contains a particular stylistic set.
1174    pub fn has(self, ss: u8) -> bool {
1175        self.0 & (1 << (ss as u32)) != 0
1176    }
1177
1178    /// Returns an iterator over all stylistic sets to enable.
1179    pub fn sets(self) -> impl Iterator<Item = u8> {
1180        (1..=20).filter(move |i| self.has(*i))
1181    }
1182}
1183
1184cast! {
1185    StylisticSets,
1186    self => self.into_array().into_value(),
1187    _: NoneValue => Self(0),
1188    v: i64 => match v {
1189        1 ..= 20 => Self(1 << (v as u32)),
1190        _ => bail!("stylistic set must be between 1 and 20"),
1191    },
1192    v: Vec<i64> => {
1193        let mut flags = 0;
1194        for i in v {
1195            match i {
1196                1 ..= 20 => flags |= 1 << (i as u32),
1197                _ => bail!("stylistic set must be between 1 and 20"),
1198            }
1199        }
1200        Self(flags)
1201    },
1202}
1203
1204/// Which kind of numbers / figures to select.
1205#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
1206pub enum NumberType {
1207    /// Numbers that fit well with capital text (the OpenType `lnum`
1208    /// font feature).
1209    Lining,
1210    /// Numbers that fit well into a flow of upper- and lowercase text (the
1211    /// OpenType `onum` font feature).
1212    OldStyle,
1213}
1214
1215/// The width of numbers / figures.
1216#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
1217pub enum NumberWidth {
1218    /// Numbers with glyph-specific widths (the OpenType `pnum` font feature).
1219    Proportional,
1220    /// Numbers of equal width (the OpenType `tnum` font feature).
1221    Tabular,
1222}
1223
1224/// OpenType font features settings.
1225#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
1226pub struct FontFeatures(pub Vec<(Tag, u32)>);
1227
1228cast! {
1229    FontFeatures,
1230    self => self.0
1231        .into_iter()
1232        .map(|(tag, num)| {
1233            let bytes = tag.to_bytes();
1234            let key = std::str::from_utf8(&bytes).unwrap_or_default();
1235            (key.into(), num.into_value())
1236        })
1237        .collect::<Dict>()
1238        .into_value(),
1239    values: Array => Self(values
1240        .into_iter()
1241        .map(|v| {
1242            let tag = v.cast::<EcoString>()?;
1243            Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1))
1244        })
1245        .collect::<HintedStrResult<_>>()?),
1246    values: Dict => Self(values
1247        .into_iter()
1248        .map(|(k, v)| {
1249            let num = v.cast::<u32>()?;
1250            let tag = Tag::from_bytes_lossy(k.as_bytes());
1251            Ok((tag, num))
1252        })
1253        .collect::<HintedStrResult<_>>()?),
1254}
1255
1256impl Fold for FontFeatures {
1257    fn fold(self, outer: Self) -> Self {
1258        Self(self.0.fold(outer.0))
1259    }
1260}
1261
1262/// Collect the OpenType features to apply.
1263pub fn features(styles: StyleChain) -> Vec<Feature> {
1264    let mut tags = vec![];
1265    let mut feat = |tag: &[u8; 4], value: u32| {
1266        tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
1267    };
1268
1269    // Features that are on by default in Harfbuzz are only added if disabled.
1270    if !styles.get(TextElem::kerning) {
1271        feat(b"kern", 0);
1272    }
1273
1274    // Features that are off by default in Harfbuzz are only added if enabled.
1275    if let Some(sc) = styles.get(TextElem::smallcaps) {
1276        feat(b"smcp", 1);
1277        if sc == Smallcaps::All {
1278            feat(b"c2sc", 1);
1279        }
1280    }
1281
1282    if styles.get(TextElem::alternates) {
1283        feat(b"salt", 1);
1284    }
1285
1286    for set in styles.get(TextElem::stylistic_set).sets() {
1287        let storage = [b's', b's', b'0' + set / 10, b'0' + set % 10];
1288        feat(&storage, 1);
1289    }
1290
1291    if !styles.get(TextElem::ligatures) {
1292        feat(b"liga", 0);
1293        feat(b"clig", 0);
1294    }
1295
1296    if styles.get(TextElem::discretionary_ligatures) {
1297        feat(b"dlig", 1);
1298    }
1299
1300    if styles.get(TextElem::historical_ligatures) {
1301        feat(b"hlig", 1);
1302    }
1303
1304    match styles.get(TextElem::number_type) {
1305        Smart::Auto => {}
1306        Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
1307        Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
1308    }
1309
1310    match styles.get(TextElem::number_width) {
1311        Smart::Auto => {}
1312        Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
1313        Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
1314    }
1315
1316    if styles.get(TextElem::slashed_zero) {
1317        feat(b"zero", 1);
1318    }
1319
1320    if styles.get(TextElem::fractions) {
1321        feat(b"frac", 1);
1322    }
1323
1324    match styles.get(EquationElem::size) {
1325        MathSize::Script => feat(b"ssty", 1),
1326        MathSize::ScriptScript => feat(b"ssty", 2),
1327        _ => {}
1328    }
1329
1330    for (tag, value) in styles.get_cloned(TextElem::features).0 {
1331        tags.push(Feature::new(tag, value, ..))
1332    }
1333
1334    tags
1335}
1336
1337/// Process the language and region of a style chain into a
1338/// rustybuzz-compatible BCP 47 language.
1339pub fn language(styles: StyleChain) -> rustybuzz::Language {
1340    let mut bcp: EcoString = styles.get(TextElem::lang).as_str().into();
1341    if let Some(region) = styles.get(TextElem::region) {
1342        bcp.push('-');
1343        bcp.push_str(region.as_str());
1344    }
1345    rustybuzz::Language::from_str(&bcp).unwrap()
1346}
1347
1348/// A toggle that turns on and off alternatingly if folded.
1349#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1350pub struct ItalicToggle(pub bool);
1351
1352impl Fold for ItalicToggle {
1353    fn fold(self, outer: Self) -> Self {
1354        Self(self.0 ^ outer.0)
1355    }
1356}
1357
1358/// A delta that is summed up when folded.
1359#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
1360pub struct WeightDelta(pub i64);
1361
1362impl Fold for WeightDelta {
1363    fn fold(self, outer: Self) -> Self {
1364        Self(outer.0 + self.0)
1365    }
1366}
1367
1368/// Costs for various layout decisions.
1369///
1370/// Costs are updated (prioritizing the later value) when folded.
1371#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
1372#[non_exhaustive]
1373pub struct Costs {
1374    hyphenation: Option<Ratio>,
1375    runt: Option<Ratio>,
1376    widow: Option<Ratio>,
1377    orphan: Option<Ratio>,
1378}
1379
1380impl Costs {
1381    #[must_use]
1382    pub fn hyphenation(&self) -> Ratio {
1383        self.hyphenation.unwrap_or(Ratio::one())
1384    }
1385
1386    #[must_use]
1387    pub fn runt(&self) -> Ratio {
1388        self.runt.unwrap_or(Ratio::one())
1389    }
1390
1391    #[must_use]
1392    pub fn widow(&self) -> Ratio {
1393        self.widow.unwrap_or(Ratio::one())
1394    }
1395
1396    #[must_use]
1397    pub fn orphan(&self) -> Ratio {
1398        self.orphan.unwrap_or(Ratio::one())
1399    }
1400}
1401
1402impl Fold for Costs {
1403    #[inline]
1404    fn fold(self, outer: Self) -> Self {
1405        Self {
1406            hyphenation: self.hyphenation.or(outer.hyphenation),
1407            runt: self.runt.or(outer.runt),
1408            widow: self.widow.or(outer.widow),
1409            orphan: self.orphan.or(outer.orphan),
1410        }
1411    }
1412}
1413
1414cast! {
1415    Costs,
1416    self => dict![
1417        "hyphenation" => self.hyphenation(),
1418        "runt" => self.runt(),
1419        "widow" => self.widow(),
1420        "orphan" => self.orphan(),
1421    ].into_value(),
1422    mut v: Dict => {
1423        let ret = Self {
1424            hyphenation: v.take("hyphenation").ok().map(|v| v.cast()).transpose()?,
1425            runt: v.take("runt").ok().map(|v| v.cast()).transpose()?,
1426            widow: v.take("widow").ok().map(|v| v.cast()).transpose()?,
1427            orphan: v.take("orphan").ok().map(|v| v.cast()).transpose()?,
1428        };
1429        v.finish(&["hyphenation", "runt", "widow", "orphan"])?;
1430        ret
1431    },
1432}
1433
1434/// Whether a codepoint is Unicode `Default_Ignorable`.
1435pub fn is_default_ignorable(c: char) -> bool {
1436    /// The set of Unicode default ignorables.
1437    static DEFAULT_IGNORABLE_DATA: LazyLock<CodePointSetData> = LazyLock::new(|| {
1438        icu_properties::sets::load_default_ignorable_code_point(
1439            &BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU)
1440                .unwrap()
1441                .as_deserializing(),
1442        )
1443        .unwrap()
1444    });
1445    DEFAULT_IGNORABLE_DATA.as_borrowed().contains(c)
1446}
1447
1448/// Checks for font families that are not available.
1449fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) {
1450    let book = engine.world.book();
1451    for family in &list.v {
1452        match book.select_family(family.as_str()).next() {
1453            Some(index) => {
1454                if book
1455                    .info(index)
1456                    .is_some_and(|x| x.flags.contains(FontFlags::VARIABLE))
1457                {
1458                    engine.sink.warn(warning!(
1459                        list.span,
1460                        "variable fonts are not currently supported and may render incorrectly";
1461                        hint: "try installing a static version of \"{}\" instead", family.as_str()
1462                    ))
1463                }
1464            }
1465            None => engine.sink.warn(warning!(
1466                list.span,
1467                "unknown font family: {}",
1468                family.as_str(),
1469            )),
1470        }
1471    }
1472}
1473
1474#[cfg(test)]
1475mod tests {
1476    use super::*;
1477
1478    #[test]
1479    fn test_text_elem_size() {
1480        assert_eq!(std::mem::size_of::<TextElem>(), std::mem::size_of::<EcoString>());
1481    }
1482}