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}