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