diff options
Diffstat (limited to 'crates/typst-library/src/text/mod.rs')
| -rw-r--r-- | crates/typst-library/src/text/mod.rs | 769 |
1 files changed, 769 insertions, 0 deletions
diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs new file mode 100644 index 00000000..ff8cbad8 --- /dev/null +++ b/crates/typst-library/src/text/mod.rs @@ -0,0 +1,769 @@ +//! 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 rustybuzz::Tag; +use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; + +use crate::layout::ParElem; +use crate::prelude::*; + +/// Hook up all text definitions. +pub(super) fn define(global: &mut Scope) { + global.define("text", TextElem::func()); + global.define("linebreak", LinebreakElem::func()); + global.define("smartquote", SmartQuoteElem::func()); + global.define("strong", StrongElem::func()); + global.define("emph", EmphElem::func()); + global.define("lower", lower_func()); + global.define("upper", upper_func()); + global.define("smallcaps", smallcaps_func()); + global.define("sub", SubElem::func()); + global.define("super", SuperElem::func()); + global.define("underline", UnderlineElem::func()); + global.define("strike", StrikeElem::func()); + global.define("overline", OverlineElem::func()); + global.define("raw", RawElem::func()); + global.define("lorem", lorem_func()); +} + +/// Customizes the look and layout of text in a variety of ways. +/// +/// This function is used frequently, 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 } +/// ```example +/// #set text(18pt) +/// With a set rule. +/// +/// #emph(text(blue)[ +/// With a function call. +/// ]) +/// ``` +/// +/// Display: Text +/// Category: text +#[element(Construct, PlainText)] +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 + /// #set text(font: "IBM Plex Sans") + /// + /// #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. This will only stretch + /// the text if a condensed or expanded version of the font is available. + /// + /// If you want to adjust the amount of space between characters instead of + /// stretching the glyphs itself, use the [`tracking`]($func/text.tracking) + /// property instead. + /// + /// ```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. + /// + /// If you want to adjust the amount of space between characters rather than + /// words, use the [`tracking`]($func/text.tracking) property instead. + /// + /// ```example + /// #set text(spacing: 200%) + /// Text with distant words. + /// ``` + #[resolve] + #[default(Rel::one())] + pub spacing: Rel<Length>, + + /// 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) + /// This justified text has a hyphen in + /// the paragraph's first line. Hanging + /// the hyphen slightly into the margin + /// results in a clearer paragraph edge. + /// + /// #set text(overhang: false) + /// This justified text has a hyphen in + /// the paragraph's first line. Hanging + /// the hyphen slightly into the margin + /// results in a clearer paragraph edge. + /// ``` + #[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<Region>, + + /// 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 page(width: 200pt) + /// + /// #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( + /// font: "IBM Plex Sans", + /// 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<StylisticSet>, + + /// 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<NumberType>, + + /// 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<NumberWidth>, + + /// 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 turn numbers into fractions. Setting this to `{true}` + /// enables the OpenType `frac` font feature. + /// + /// It is not advisable to enable this property globally as it will mess + /// with all appearances of numbers after a slash (e.g., in URLs). Instead, + /// enable it locally when you want a fraction. + /// + /// ```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<Case>, + + /// 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<EcoString>) -> Content { + Self::new(text.into()).pack() + } +} + +impl Construct for TextElem { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + // 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::<Content>("body")?; + Ok(body.styled_with_map(styles)) + } +} + +impl PlainText for TextElem { + fn plain_text(&self, text: &mut EcoString) { + text.push_str(&self.text()); + } +} + +/// 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! { + FontFamily, + self => self.0.into_value(), + string: EcoString => Self::new(&string), +} + +/// Font family fallback list. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct FontList(pub Vec<FontFamily>); + +impl IntoIterator for FontList { + type IntoIter = std::vec::IntoIter<FontFamily>; + type Item = FontFamily; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +cast! { + FontList, + self => if self.0.len() == 1 { + self.0.into_iter().next().unwrap().0.into_value() + } else { + self.0.into_value() + }, + family: FontFamily => Self(vec![family]), + values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?), +} + +/// 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! { + TextSize, + self => self.0.into_value(), + 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), + } + } +} + +cast! { + TextEdge, + self => match self { + Self::Metric(metric) => metric.into_value(), + Self::Length(length) => length.into_value(), + }, + v: VerticalFontMetric => Self::Metric(v), + v: Length => Self::Length(v), +} + +/// The direction of text and inline objects in their line. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct TextDir(pub Smart<Dir>); + +cast! { + TextDir, + self => self.0.into_value(), + v: Smart<Dir> => { + if v.map_or(false, |dir| dir.axis() == Axis::Y) { + bail!("text direction must be horizontal"); + } + Self(v) + }, +} + +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<bool>); + +cast! { + Hyphenate, + self => self.0.into_value(), + v: Smart<bool> => Self(v), +} + +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! { + StylisticSet, + self => self.0.into_value(), + v: i64 => match v { + 1 ..= 20 => Self::new(v as u8), + _ => bail!("stylistic set must be between 1 and 20"), + }, +} + +/// 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! { + FontFeatures, + self => self.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_value()) + }) + .collect::<Dict>() + .into_value(), + values: Array => Self(values + .into_iter() + .map(|v| { + let tag = v.cast::<EcoString>()?; + Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1)) + }) + .collect::<StrResult<_>>()?), + values: Dict => Self(values + .into_iter() + .map(|(k, v)| { + let num = v.cast::<u32>()?; + let tag = Tag::from_bytes_lossy(k.as_bytes()); + Ok((tag, num)) + }) + .collect::<StrResult<_>>()?), +} + +impl Fold for FontFeatures { + type Output = Self; + + fn fold(mut self, outer: Self::Output) -> Self::Output { + self.0.extend(outer.0); + self + } +} |
