//! 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 typst::util::EcoString; use crate::layout::ParNode; use crate::prelude::*; /// # Text /// 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 /// ``` /// #set text(18pt) /// With a set rule. /// /// #emph(text(blue)[ /// With a function call. /// ]) /// ``` /// /// ## Parameters /// - family: EcoString (positional, variadic, settable) /// 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( /// "Inria Serif", /// "Noto Sans Arabic", /// ) /// /// This is Latin. \ /// هذا عربي. /// /// ``` /// /// - body: Content (positional, required) /// Content in which all text is styled according to the other arguments. /// /// ## Category /// text #[func] #[capable] #[derive(Clone, Hash)] pub struct TextNode(pub EcoString); impl TextNode { /// Create a new packed text node. pub fn packed(text: impl Into) -> Content { Self(text.into()).pack() } } #[node] impl TextNode { /// A prioritized sequence of font families. #[property(skip, referenced)] pub const FAMILY: FallbackList = FallbackList(vec![FontFamily::new("IBM Plex Sans")]); /// 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(family: "Inria Serif") /// هذا عربي /// /// #set text(fallback: false) /// هذا عربي /// ``` pub const FALLBACK: bool = true; /// 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](@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("IBM Plex Sans", style: "italic")[Italic] /// #text("DejaVu Sans", style: "oblique")[Oblique] /// ``` pub const STYLE: FontStyle = FontStyle::Normal; /// 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](@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 const WEIGHT: FontWeight = FontWeight::REGULAR; /// 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 const STRETCH: FontStretch = FontStretch::NORMAL; /// 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 /// ``` #[property(shorthand, fold)] pub const SIZE: TextSize = Abs::pt(11.0); /// The glyph fill color. /// /// # Example /// ``` /// #set text(fill: red) /// This text is red. /// ``` #[property(shorthand)] pub const FILL: Paint = Color::BLACK.into(); /// The amount of space that should be added between characters. /// /// # Example /// ``` /// #set text(tracking: 1.5pt) /// Distant text. /// ``` #[property(resolve)] pub const TRACKING: Length = Length::zero(); /// 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. /// ``` #[property(resolve)] pub const SPACING: Rel = Rel::one(); /// An amount to shift the text baseline by. /// /// # Example /// ``` /// A #text(baseline: 3pt)[lowered] /// word. /// ``` #[property(resolve)] pub const BASELINE: Length = Length::zero(); /// 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. /// ``` pub const OVERHANG: bool = true; /// 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] /// ``` pub const TOP_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::CapHeight); /// 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] /// ``` pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline); /// 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](@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, ... /// ``` pub const LANG: Lang = Lang::ENGLISH; /// 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 const REGION: Option = None; /// 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](@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](/contact) or our [Discord server](/docs/community/#discord)! /// /// # Example /// ``` /// #set text(dir: rtl) /// هذا عربي. /// ``` #[property(resolve)] pub const DIR: HorizontalDir = HorizontalDir(Smart::Auto); /// 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](@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. /// ``` #[property(resolve)] pub const HYPHENATE: Hyphenate = Hyphenate(Smart::Auto); /// 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 /// ``` pub const KERNING: bool = true; /// 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, ß /// ``` pub const ALTERNATES: bool = false; /// 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 const STYLISTIC_SET: Option = None; /// 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. /// ``` pub const LIGATURES: bool = true; /// Whether ligatures that should be used sparingly are active. Setting this /// to `{true}` enables the OpenType `dlig` font feature. pub const DISCRETIONARY_LIGATURES: bool = false; /// Whether historical ligatures are active. Setting this to `{true}` /// enables the OpenType `hlig` font feature. pub const HISTORICAL_LIGATURES: bool = false; /// Which kind of numbers / figures to select. When set to `{auto}`, the /// default numbers for the font are used. /// /// # Example /// ``` /// #set text(20pt, "Noto Sans") /// #set text(number-type: "lining") /// Number 9. /// /// #set text(number-type: "old-style") /// Number 9. /// ``` pub const NUMBER_TYPE: Smart = Smart::Auto; /// The width of numbers / figures. When set to `{auto}`, the default /// numbers for the font are used. /// /// # Example /// ``` /// #set text(20pt, "Noto Sans") /// #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 const NUMBER_WIDTH: Smart = Smart::Auto; /// 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] /// ``` pub const SLASHED_ZERO: bool = false; /// Whether to turns numbers into fractions. Setting this to `{true}` /// enables the OpenType `frac` font feature. /// /// # Example /// ``` /// 1/2 \ /// #text(fractions: true)[1/2] /// ``` pub const FRACTIONS: bool = false; /// 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 /// ``` #[property(fold)] pub const FEATURES: FontFeatures = FontFeatures(vec![]); /// A delta to apply on the font weight. #[property(skip, fold)] pub const DELTA: Delta = 0; /// Whether the font style should be inverted. #[property(skip, fold)] pub const EMPH: Toggle = false; /// A case transformation that should be applied to the text. #[property(skip)] pub const CASE: Option = None; /// Whether small capital glyphs should be used. ("smcp") #[property(skip)] pub const SMALLCAPS: bool = false; /// Decorative lines. #[property(skip, fold)] pub const DECO: Decoration = vec![]; fn construct(_: &Vm, args: &mut Args) -> SourceResult { // The text constructor is special: It doesn't create a text node. // Instead, it leaves the passed argument structurally unchanged, but // styles all text in it. args.expect("body") } fn set(...) { if let Some(family) = args.named("family")? { styles.set(Self::FAMILY, family); } else { let mut count = 0; let mut content = false; for item in args.items.iter().filter(|item| item.name.is_none()) { if EcoString::is(&item.value) { count += 1; } else if >>::is(&item.value) { content = true; } } // Skip the final string if it's needed as the body. if constructor && !content && count > 0 { count -= 1; } if count > 0 { let mut list = Vec::with_capacity(count); for _ in 0..count { list.push(args.find()?.unwrap()); } styles.set(Self::FAMILY, FallbackList(list)); } } } fn field(&self, name: &str) -> Option { match name { "text" => Some(Value::Str(self.0.clone().into())), _ => None, } } } impl Debug for TextNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Text({:?})", self.0) } } /// 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) } } castable! { FontFamily, string: EcoString => Self::new(&string), } /// Font family fallback list. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct FallbackList(pub Vec); castable! { FallbackList, family: FontFamily => Self(vec![family]), values: Array => Self(values .into_iter() .filter_map(|v| v.cast().ok()) .collect()), } /// 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 } } castable! { TextSize, v: Length => Self(v), } /// 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), } } } castable! { TextEdge, v: Length => Self::Length(v), /// The font's ascender, which typically exceeds the height of all glyphs. "ascender" => Self::Metric(VerticalFontMetric::Ascender), /// The approximate height of uppercase letters. "cap-height" => Self::Metric(VerticalFontMetric::CapHeight), /// The approximate height of non-ascending lowercase letters. "x-height" => Self::Metric(VerticalFontMetric::XHeight), /// The baseline on which the letters rest. "baseline" => Self::Metric(VerticalFontMetric::Baseline), /// The font's ascender, which typically exceeds the depth of all glyphs. "descender" => Self::Metric(VerticalFontMetric::Descender), } /// The direction of text and inline objects in their line. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalDir(pub Smart); castable! { HorizontalDir, _: AutoValue => Self(Smart::Auto), dir: Dir => match dir.axis() { Axis::X => Self(Smart::Custom(dir)), Axis::Y => Err("must be horizontal")?, }, } impl Resolve for HorizontalDir { type Output = Dir; fn resolve(self, styles: StyleChain) -> Self::Output { match self.0 { Smart::Auto => styles.get(TextNode::LANG).dir(), Smart::Custom(dir) => dir, } } } /// Whether to hyphenate text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Hyphenate(pub Smart); castable! { Hyphenate, _: AutoValue => Self(Smart::Auto), v: bool => Self(Smart::Custom(v)), } impl Resolve for Hyphenate { type Output = bool; fn resolve(self, styles: StyleChain) -> Self::Output { match self.0 { Smart::Auto => styles.get(ParNode::JUSTIFY), 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 } } castable! { StylisticSet, v: i64 => match v { 1 ..= 20 => Self::new(v as u8), _ => Err("must be between 1 and 20")?, }, } /// Which kind of numbers / figures to select. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum NumberType { /// Numbers that fit well with capital text. ("lnum") Lining, /// Numbers that fit well into a flow of upper- and lowercase text. ("onum") OldStyle, } castable! { NumberType, /// Numbers that fit well with capital text (the OpenType `lnum` /// font feature). "lining" => Self::Lining, /// Numbers that fit well into a flow of upper- and lowercase text (the /// OpenType `onum` font feature). "old-style" => Self::OldStyle, } /// The width of numbers / figures. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum NumberWidth { /// Number widths are glyph specific. ("pnum") Proportional, /// All numbers are of equal width / monospaced. ("tnum") Tabular, } castable! { NumberWidth, /// Numbers with glyph-specific widths (the OpenType `pnum` font feature). "proportional" => Self::Proportional, /// Numbers of equal width (the OpenType `tnum` font feature). "tabular" => Self::Tabular, } /// OpenType font features settings. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct FontFeatures(pub Vec<(Tag, u32)>); castable! { 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::>()?), } impl Fold for FontFeatures { type Output = Self; fn fold(mut self, outer: Self::Output) -> Self::Output { self.0.extend(outer.0); self } }