Skip to main content

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