//! Text handling. mod deco; mod misc; mod quotes; mod raw; mod shaping; mod shift; pub use self::deco::*; pub use self::misc::*; pub use self::quotes::*; pub use self::raw::*; pub use self::shaping::*; pub use self::shift::*; use std::borrow::Cow; use rustybuzz::Tag; use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use crate::layout::ParElem; use crate::prelude::*; /// Customize the look and layout of text in a variety of ways. /// /// This function is used often, both with set rules and directly. While the set /// rule is often the simpler choice, calling the text function directly can be /// useful when passing text as an argument to another function. /// /// ## Example /// ```example /// #set text(18pt) /// With a set rule. /// /// #emph(text(blue)[ /// With a function call. /// ]) /// ``` /// /// Display: Text /// Category: text #[element(Construct)] pub struct TextElem { /// A prioritized sequence of font families. /// /// When processing text, Typst tries all specified font families in order /// until it finds a font that has the necessary glyphs. In the example /// below, the font `Inria Serif` is preferred, but since it does not /// contain Arabic glyphs, the arabic text uses `Noto Sans Arabic` instead. /// /// ```example /// #set text(font: ( /// "Inria Serif", /// "Noto Sans Arabic", /// )) /// /// This is Latin. \ /// هذا عربي. /// /// ``` #[default(FontList(vec![FontFamily::new("Linux Libertine")]))] pub font: FontList, /// Whether to allow last resort font fallback when the primary font list /// contains no match. This lets Typst search through all available fonts /// for the most similar one that has the necessary glyphs. /// /// _Note:_ Currently, there are no warnings when fallback is disabled and /// no glyphs are found. Instead, your text shows up in the form of "tofus": /// Small boxes that indicate the lack of an appropriate glyph. In the /// future, you will be able to instruct Typst to issue warnings so you know /// something is up. /// /// ```example /// #set text(font: "Inria Serif") /// هذا عربي /// /// #set text(fallback: false) /// هذا عربي /// ``` #[default(true)] pub fallback: bool, /// The desired font style. /// /// When an italic style is requested and only an oblique one is available, /// it is used. Similarly, the other way around, an italic style can stand /// in for an oblique one. When neither an italic nor an oblique style is /// available, Typst selects the normal style. Since most fonts are only /// available either in an italic or oblique style, the difference between /// italic and oblique style is rarely observable. /// /// If you want to emphasize your text, you should do so using the /// [emph]($func/emph) function instead. This makes it easy to adapt the /// style later if you change your mind about how to signify the emphasis. /// /// ```example /// #text(font: "Linux Libertine", style: "italic")[Italic] /// #text(font: "DejaVu Sans", style: "oblique")[Oblique] /// ``` pub style: FontStyle, /// The desired thickness of the font's glyphs. Accepts an integer between /// `{100}` and `{900}` or one of the predefined weight names. When the /// desired weight is not available, Typst selects the font from the family /// that is closest in weight. /// /// If you want to strongly emphasize your text, you should do so using the /// [strong]($func/strong) function instead. This makes it easy to adapt the /// style later if you change your mind about how to signify the strong /// emphasis. /// /// ```example /// #text(weight: "light")[Light] \ /// #text(weight: "regular")[Regular] \ /// #text(weight: "medium")[Medium] \ /// #text(weight: 500)[Medium] \ /// #text(weight: "bold")[Bold] /// ``` pub weight: FontWeight, /// The desired width of the glyphs. Accepts a ratio between `{50%}` and /// `{200%}`. When the desired weight is not available, Typst selects the /// font from the family that is closest in stretch. /// /// ```example /// #text(stretch: 75%)[Condensed] \ /// #text(stretch: 100%)[Normal] /// ``` pub stretch: FontStretch, /// The size of the glyphs. This value forms the basis of the `em` unit: /// `{1em}` is equivalent to the font size. /// /// You can also give the font size itself in `em` units. Then, it is /// relative to the previous font size. /// /// ```example /// #set text(size: 20pt) /// very #text(1.5em)[big] text /// ``` #[parse(args.named_or_find("size")?)] #[fold] #[default(Abs::pt(11.0))] pub size: TextSize, /// The glyph fill color. /// /// ```example /// #set text(fill: red) /// This text is red. /// ``` #[parse(args.named_or_find("fill")?)] #[default(Color::BLACK.into())] pub fill: Paint, /// The amount of space that should be added between characters. /// /// ```example /// #set text(tracking: 1.5pt) /// Distant text. /// ``` #[resolve] pub tracking: Length, /// The amount of space between words. /// /// Can be given as an absolute length, but also relative to the width of /// the space character in the font. /// /// ```example /// #set text(spacing: 200%) /// Text with distant words. /// ``` #[resolve] #[default(Rel::one())] pub spacing: Rel, /// An amount to shift the text baseline by. /// /// ```example /// A #text(baseline: 3pt)[lowered] /// word. /// ``` #[resolve] pub baseline: Length, /// Whether certain glyphs can hang over into the margin in justified text. /// This can make justification visually more pleasing. /// /// ```example /// #set par(justify: true) /// In this particular text, the /// justification produces a hyphen /// in the first line. Letting this /// hyphen hang slightly into the /// margin makes for a clear /// paragraph edge. /// /// #set text(overhang: false) /// In this particular text, the /// justification produces a hyphen /// in the first line. This time the /// hyphen does not hang into the /// margin, making the paragraph's /// edge less clear. /// ``` #[default(true)] pub overhang: bool, /// The top end of the conceptual frame around the text used for layout and /// positioning. This affects the size of containers that hold text. /// /// ```example /// #set rect(inset: 0pt) /// #set text(size: 20pt) /// /// #set text(top-edge: "ascender") /// #rect(fill: aqua)[Typst] /// /// #set text(top-edge: "cap-height") /// #rect(fill: aqua)[Typst] /// ``` #[default(TextEdge::Metric(VerticalFontMetric::CapHeight))] pub top_edge: TextEdge, /// The bottom end of the conceptual frame around the text used for layout /// and positioning. This affects the size of containers that hold text. /// /// ```example /// #set rect(inset: 0pt) /// #set text(size: 20pt) /// /// #set text(bottom-edge: "baseline") /// #rect(fill: aqua)[Typst] /// /// #set text(bottom-edge: "descender") /// #rect(fill: aqua)[Typst] /// ``` #[default(TextEdge::Metric(VerticalFontMetric::Baseline))] pub bottom_edge: TextEdge, /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639) /// /// Setting the correct language affects various parts of Typst: /// /// - The text processing pipeline can make more informed choices. /// - Hyphenation will use the correct patterns for the language. /// - [Smart quotes]($func/smartquote) turns into the correct quotes for the /// language. /// - And all other things which are language-aware. /// /// ```example /// #set text(lang: "de") /// #outline() /// /// = Einleitung /// In diesem Dokument, ... /// ``` #[default(Lang::ENGLISH)] pub lang: Lang, /// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) /// /// This lets the text processing pipeline make more informed choices. pub region: Option, /// The dominant direction for text and inline objects. Possible values are: /// /// - `{auto}`: Automatically infer the direction from the `lang` property. /// - `{ltr}`: Layout text from left to right. /// - `{rtl}`: Layout text from right to left. /// /// When writing in right-to-left scripts like Arabic or Hebrew, you should /// set the [text language]($func/text.lang) or direction. While individual /// runs of text are automatically layouted in the correct direction, /// setting the dominant direction gives the bidirectional reordering /// algorithm the necessary information to correctly place punctuation and /// inline objects. Furthermore, setting the direction affects the alignment /// values `start` and `end`, which are equivalent to `left` and `right` in /// `ltr` text and the other way around in `rtl` text. /// /// If you set this to `rtl` and experience bugs or in some way bad looking /// output, please do get in touch with us through the /// [contact form](https://typst.app/contact) or our /// [Discord server]($community/#discord)! /// /// ```example /// #set text(dir: rtl) /// هذا عربي. /// ``` #[resolve] pub dir: TextDir, /// Whether to hyphenate text to improve line breaking. When `{auto}`, text /// will be hyphenated if and only if justification is enabled. /// /// Setting the [text language]($func/text.lang) ensures that the correct /// hyphenation patterns are used. /// /// ```example /// #set par(justify: true) /// This text illustrates how /// enabling hyphenation can /// improve justification. /// /// #set text(hyphenate: false) /// This text illustrates how /// enabling hyphenation can /// improve justification. /// ``` #[resolve] pub hyphenate: Hyphenate, /// Whether to apply kerning. /// /// When enabled, specific letter pairings move closer together or further /// apart for a more visually pleasing result. The example below /// demonstrates how decreasing the gap between the "T" and "o" results in a /// more natural look. Setting this to `{false}` disables kerning by turning /// off the OpenType `kern` font feature. /// /// ```example /// #set text(size: 25pt) /// Totally /// /// #set text(kerning: false) /// Totally /// ``` #[default(true)] pub kerning: bool, /// Whether to apply stylistic alternates. /// /// Sometimes fonts contain alternative glyphs for the same codepoint. /// Setting this to `{true}` switches to these by enabling the OpenType /// `salt` font feature. /// /// ```example /// #set text(size: 20pt) /// 0, a, g, ß /// /// #set text(alternates: true) /// 0, a, g, ß /// ``` #[default(false)] pub alternates: bool, /// Which stylistic set to apply. Font designers can categorize alternative /// glyphs forms into stylistic sets. As this value is highly font-specific, /// you need to consult your font to know which sets are available. When set /// to an integer between `{1}` and `{20}`, enables the corresponding /// OpenType font feature from `ss01`, ..., `ss20`. pub stylistic_set: Option, /// Whether standard ligatures are active. /// /// Certain letter combinations like "fi" are often displayed as a single /// merged glyph called a _ligature._ Setting this to `{false}` disables /// these ligatures by turning off the OpenType `liga` and `clig` font /// features. /// /// ```example /// #set text(size: 20pt) /// A fine ligature. /// /// #set text(ligatures: false) /// A fine ligature. /// ``` #[default(true)] pub ligatures: bool, /// Whether ligatures that should be used sparingly are active. Setting this /// to `{true}` enables the OpenType `dlig` font feature. #[default(false)] pub discretionary_ligatures: bool, /// Whether historical ligatures are active. Setting this to `{true}` /// enables the OpenType `hlig` font feature. #[default(false)] pub historical_ligatures: bool, /// Which kind of numbers / figures to select. When set to `{auto}`, the /// default numbers for the font are used. /// /// ```example /// #set text(font: "Noto Sans", 20pt) /// #set text(number-type: "lining") /// Number 9. /// /// #set text(number-type: "old-style") /// Number 9. /// ``` pub number_type: Smart, /// The width of numbers / figures. When set to `{auto}`, the default /// numbers for the font are used. /// /// ```example /// #set text(font: "Noto Sans", 20pt) /// #set text(number-width: "proportional") /// A 12 B 34. \ /// A 56 B 78. /// /// #set text(number-width: "tabular") /// A 12 B 34. \ /// A 56 B 78. /// ``` pub number_width: Smart, /// Whether to have a slash through the zero glyph. Setting this to `{true}` /// enables the OpenType `zero` font feature. /// /// ```example /// 0, #text(slashed-zero: true)[0] /// ``` #[default(false)] pub slashed_zero: bool, /// Whether to turns numbers into fractions. Setting this to `{true}` /// enables the OpenType `frac` font feature. /// /// ```example /// 1/2 \ /// #text(fractions: true)[1/2] /// ``` #[default(false)] pub fractions: bool, /// Raw OpenType features to apply. /// /// - If given an array of strings, sets the features identified by the /// strings to `{1}`. /// - If given a dictionary mapping to numbers, sets the features /// identified by the keys to the values. /// /// ```example /// // Enable the `frac` feature manually. /// #set text(features: ("frac",)) /// 1/2 /// ``` #[fold] pub features: FontFeatures, /// Content in which all text is styled according to the other arguments. #[external] #[required] pub body: Content, /// The text. #[internal] #[required] pub text: EcoString, /// A delta to apply on the font weight. #[internal] #[fold] pub delta: Delta, /// Whether the font style should be inverted. #[internal] #[fold] #[default(false)] pub emph: Toggle, /// Decorative lines. #[internal] #[fold] pub deco: Decoration, /// A case transformation that should be applied to the text. #[internal] pub case: Option, /// Whether small capital glyphs should be used. ("smcp") #[internal] #[default(false)] pub smallcaps: bool, } impl TextElem { /// Create a new packed text element. pub fn packed(text: impl Into) -> Content { Self::new(text.into()).pack() } } impl Construct for TextElem { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { // The text constructor is special: It doesn't create a text element. // Instead, it leaves the passed argument structurally unchanged, but // styles all text in it. let styles = Self::set(args)?; let body = args.expect::("body")?; Ok(body.styled_with_map(styles)) } } /// A lowercased font family like "arial". #[derive(Clone, Eq, PartialEq, Hash)] pub struct FontFamily(EcoString); impl FontFamily { /// Create a named font family variant. pub fn new(string: &str) -> Self { Self(string.to_lowercase().into()) } /// The lowercased family name. pub fn as_str(&self) -> &str { &self.0 } } impl Debug for FontFamily { fn fmt(&self, f: &mut Formatter) -> fmt::Result { self.0.fmt(f) } } cast_from_value! { FontFamily, string: EcoString => Self::new(&string), } cast_to_value! { v: FontFamily => v.0.into() } /// Font family fallback list. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct FontList(pub Vec); impl IntoIterator for FontList { type IntoIter = std::vec::IntoIter; type Item = FontFamily; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } cast_from_value! { FontList, family: FontFamily => Self(vec![family]), values: Array => Self(values.into_iter().map(|v| v.cast()).collect::>()?), } cast_to_value! { v: FontList => v.0.into() } /// The size of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct TextSize(pub Length); impl Fold for TextSize { type Output = Abs; fn fold(self, outer: Self::Output) -> Self::Output { self.0.em.at(outer) + self.0.abs } } cast_from_value! { TextSize, v: Length => Self(v), } cast_to_value! { v: TextSize => v.0.into() } /// Specifies the bottom or top edge of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum TextEdge { /// An edge specified using one of the well-known font metrics. Metric(VerticalFontMetric), /// An edge specified as a length. Length(Length), } impl TextEdge { /// Resolve the value of the text edge given a font's metrics. pub fn resolve(self, styles: StyleChain, metrics: &FontMetrics) -> Abs { match self { Self::Metric(metric) => metrics.vertical(metric).resolve(styles), Self::Length(length) => length.resolve(styles), } } } cast_from_value! { TextEdge, v: VerticalFontMetric => Self::Metric(v), v: Length => Self::Length(v), } cast_to_value! { v: TextEdge => match v { TextEdge::Metric(metric) => metric.into(), TextEdge::Length(length) => length.into(), } } /// The direction of text and inline objects in their line. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct TextDir(pub Smart); cast_from_value! { TextDir, v: Smart => { if v.map_or(false, |dir| dir.axis() == Axis::Y) { Err("text direction must be horizontal")?; } Self(v) }, } cast_to_value! { v: TextDir => v.0.into() } impl Resolve for TextDir { type Output = Dir; fn resolve(self, styles: StyleChain) -> Self::Output { match self.0 { Smart::Auto => TextElem::lang_in(styles).dir(), Smart::Custom(dir) => dir, } } } /// Whether to hyphenate text. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct Hyphenate(pub Smart); cast_from_value! { Hyphenate, v: Smart => Self(v), } cast_to_value! { v: Hyphenate => v.0.into() } impl Resolve for Hyphenate { type Output = bool; fn resolve(self, styles: StyleChain) -> Self::Output { match self.0 { Smart::Auto => ParElem::justify_in(styles), Smart::Custom(v) => v, } } } /// A stylistic set in a font. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct StylisticSet(u8); impl StylisticSet { /// Create a new set, clamping to 1-20. pub fn new(index: u8) -> Self { Self(index.clamp(1, 20)) } /// Get the value, guaranteed to be 1-20. pub fn get(self) -> u8 { self.0 } } cast_from_value! { StylisticSet, v: i64 => match v { 1 ..= 20 => Self::new(v as u8), _ => Err("stylistic set must be between 1 and 20")?, }, } cast_to_value! { v: StylisticSet => v.0.into() } /// Which kind of numbers / figures to select. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum NumberType { /// Numbers that fit well with capital text (the OpenType `lnum` /// font feature). Lining, /// Numbers that fit well into a flow of upper- and lowercase text (the /// OpenType `onum` font feature). OldStyle, } /// The width of numbers / figures. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum NumberWidth { /// Numbers with glyph-specific widths (the OpenType `pnum` font feature). Proportional, /// Numbers of equal width (the OpenType `tnum` font feature). Tabular, } /// OpenType font features settings. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct FontFeatures(pub Vec<(Tag, u32)>); cast_from_value! { FontFeatures, values: Array => Self(values .into_iter() .map(|v| { let tag = v.cast::()?; Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1)) }) .collect::>()?), values: Dict => Self(values .into_iter() .map(|(k, v)| { let num = v.cast::()?; let tag = Tag::from_bytes_lossy(k.as_bytes()); Ok((tag, num)) }) .collect::>()?), } cast_to_value! { v: FontFeatures => Value::Dict( v.0.into_iter() .map(|(tag, num)| { let bytes = tag.to_bytes(); let key = std::str::from_utf8(&bytes).unwrap_or_default(); (key.into(), num.into()) }) .collect(), ) } impl Fold for FontFeatures { type Output = Self; fn fold(mut self, outer: Self::Output) -> Self::Output { self.0.extend(outer.0); self } }