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