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