mod case;
mod deco;
mod font;
mod item;
mod lang;
mod linebreak;
#[path = "lorem.rs"]
mod lorem_;
mod raw;
mod shift;
#[path = "smallcaps.rs"]
mod smallcaps_;
mod smartquote;
mod space;
pub use self::case::*;
pub use self::deco::*;
pub use self::font::*;
pub use self::item::*;
pub use self::lang::*;
pub use self::linebreak::*;
pub use self::lorem_::*;
pub use self::raw::*;
pub use self::shift::*;
pub use self::smallcaps_::*;
pub use self::smartquote::*;
pub use self::space::*;
use std::fmt::{self, Debug, Formatter};
use std::hash::Hash;
use std::sync::LazyLock;
use ecow::{eco_format, EcoString};
use icu_properties::sets::CodePointSetData;
use icu_provider::AsDeserializingBufferProvider;
use icu_provider_blob::BlobDataProvider;
use rustybuzz::Feature;
use smallvec::SmallVec;
use ttf_parser::Tag;
use typst_syntax::Spanned;
use typst_utils::singleton;
use crate::diag::{bail, warning, HintedStrResult, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue,
NativeElement, Never, NoneValue, Packed, PlainText, Regex, Repr, Resolve, Scope, Set,
Smart, StyleChain,
};
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
use crate::math::{EquationElem, MathSize};
use crate::visualize::{Color, Paint, RelativeTo, Stroke};
use crate::World;
pub(super) fn define(global: &mut Scope) {
global.start_category(crate::Category::Text);
global.define_elem::<TextElem>();
global.define_elem::<LinebreakElem>();
global.define_elem::<SmartQuoteElem>();
global.define_elem::<SubElem>();
global.define_elem::<SuperElem>();
global.define_elem::<UnderlineElem>();
global.define_elem::<OverlineElem>();
global.define_elem::<StrikeElem>();
global.define_elem::<HighlightElem>();
global.define_elem::<SmallcapsElem>();
global.define_elem::<RawElem>();
global.define_func::<lower>();
global.define_func::<upper>();
global.define_func::<lorem>();
global.reset_category();
}
#[elem(Debug, Construct, PlainText, Repr)]
pub struct TextElem {
#[parse({
let font_list: Option<Spanned<FontList>> = args.named("font")?;
if let Some(list) = &font_list {
check_font_list(engine, list);
}
font_list.map(|font_list| font_list.v)
})]
#[default(FontList(vec![FontFamily::new("Libertinus Serif")]))]
#[borrowed]
#[ghost]
pub font: FontList,
#[default(true)]
#[ghost]
pub fallback: bool,
#[ghost]
pub style: FontStyle,
#[ghost]
pub weight: FontWeight,
#[ghost]
pub stretch: FontStretch,
#[parse(args.named_or_find("size")?)]
#[fold]
#[default(TextSize(Abs::pt(11.0).into()))]
#[resolve]
#[ghost]
pub size: TextSize,
#[parse({
let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
if let Some(paint) = &paint {
if paint.v.relative() == Smart::Custom(RelativeTo::Self_) {
bail!(
paint.span,
"gradients and tilings on text must be relative to the parent";
hint: "make sure to set `relative: auto` on your text fill"
);
}
}
paint.map(|paint| paint.v)
})]
#[default(Color::BLACK.into())]
#[ghost]
pub fill: Paint,
#[resolve]
#[ghost]
pub stroke: Option<Stroke>,
#[resolve]
#[ghost]
pub tracking: Length,
#[resolve]
#[default(Rel::one())]
#[ghost]
pub spacing: Rel<Length>,
#[ghost]
pub cjk_latin_spacing: Smart<Option<Never>>,
#[resolve]
#[ghost]
pub baseline: Length,
#[default(true)]
#[ghost]
pub overhang: bool,
#[default(TopEdge::Metric(TopEdgeMetric::CapHeight))]
#[ghost]
pub top_edge: TopEdge,
#[default(BottomEdge::Metric(BottomEdgeMetric::Baseline))]
#[ghost]
pub bottom_edge: BottomEdge,
#[default(Lang::ENGLISH)]
#[ghost]
pub lang: Lang,
#[ghost]
pub region: Option<Region>,
#[ghost]
pub script: Smart<WritingScript>,
#[resolve]
#[ghost]
pub dir: TextDir,
#[ghost]
pub hyphenate: Smart<bool>,
#[fold]
#[ghost]
pub costs: Costs,
#[default(true)]
#[ghost]
pub kerning: bool,
#[default(false)]
#[ghost]
pub alternates: bool,
#[ghost]
pub stylistic_set: StylisticSets,
#[default(true)]
#[ghost]
pub ligatures: bool,
#[default(false)]
#[ghost]
pub discretionary_ligatures: bool,
#[default(false)]
#[ghost]
pub historical_ligatures: bool,
#[ghost]
pub number_type: Smart<NumberType>,
#[ghost]
pub number_width: Smart<NumberWidth>,
#[default(false)]
#[ghost]
pub slashed_zero: bool,
#[default(false)]
#[ghost]
pub fractions: bool,
#[fold]
#[ghost]
pub features: FontFeatures,
#[external]
#[required]
pub body: Content,
#[required]
pub text: EcoString,
#[internal]
#[ghost]
pub span_offset: usize,
#[internal]
#[fold]
#[ghost]
pub delta: WeightDelta,
#[internal]
#[fold]
#[default(ItalicToggle(false))]
#[ghost]
pub emph: ItalicToggle,
#[internal]
#[fold]
#[ghost]
pub deco: SmallVec<[Decoration; 1]>,
#[internal]
#[ghost]
pub case: Option<Case>,
#[internal]
#[ghost]
pub smallcaps: Option<Smallcaps>,
}
impl TextElem {
pub fn packed(text: impl Into<EcoString>) -> Content {
Self::new(text.into()).pack()
}
}
impl Debug for TextElem {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Text({})", self.text)
}
}
impl Repr for TextElem {
fn repr(&self) -> EcoString {
eco_format!("[{}]", self.text)
}
}
impl Construct for TextElem {
fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> {
let styles = Self::set(engine, args)?;
let body = args.expect::<Content>("body")?;
Ok(body.styled_with_map(styles))
}
}
impl PlainText for Packed<TextElem> {
fn plain_text(&self, text: &mut EcoString) {
text.push_str(&self.text);
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct FontFamily {
name: EcoString,
covers: Option<Covers>,
}
impl FontFamily {
pub fn new(string: &str) -> Self {
Self::with_coverage(string, None)
}
pub fn with_coverage(string: &str, covers: Option<Covers>) -> Self {
Self { name: string.to_lowercase().into(), covers }
}
pub fn as_str(&self) -> &str {
&self.name
}
pub fn covers(&self) -> Option<&Regex> {
self.covers.as_ref().map(|covers| covers.as_regex())
}
}
cast! {
FontFamily,
self => self.name.into_value(),
string: EcoString => Self::new(&string),
mut v: Dict => {
let ret = Self::with_coverage(
&v.take("name")?.cast::<EcoString>()?,
v.take("covers").ok().map(|v| v.cast()).transpose()?
);
v.finish(&["name", "covers"])?;
ret
},
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Covers {
LatinInCjk,
Regex(Regex),
}
impl Covers {
pub fn as_regex(&self) -> &Regex {
match self {
Self::LatinInCjk => singleton!(
Regex,
Regex::new(
"[^\u{00B7}\u{2013}\u{2014}\u{2018}\u{2019}\
\u{201C}\u{201D}\u{2025}-\u{2027}\u{2E3A}]"
)
.unwrap()
),
Self::Regex(regex) => regex,
}
}
}
cast! {
Covers,
self => match self {
Self::LatinInCjk => "latin-in-cjk".into_value(),
Self::Regex(regex) => regex.into_value(),
},
"latin-in-cjk" => Covers::LatinInCjk,
regex: Regex => {
let ast = regex_syntax::ast::parse::Parser::new().parse(regex.as_str());
match ast {
Ok(
regex_syntax::ast::Ast::ClassBracketed(..)
| regex_syntax::ast::Ast::ClassUnicode(..)
| regex_syntax::ast::Ast::ClassPerl(..)
| regex_syntax::ast::Ast::Dot(..)
| regex_syntax::ast::Ast::Literal(..),
) => {}
_ => bail!(
"coverage regex may only use dot, letters, and character classes";
hint: "the regex is applied to each letter individually"
),
}
Covers::Regex(regex)
},
}
#[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct FontList(pub Vec<FontFamily>);
impl<'a> IntoIterator for &'a FontList {
type IntoIter = std::slice::Iter<'a, FontFamily>;
type Item = &'a FontFamily;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
cast! {
FontList,
self => if self.0.len() == 1 {
self.0.into_iter().next().unwrap().name.into_value()
} else {
self.0.into_value()
},
family: FontFamily => Self(vec![family]),
values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<HintedStrResult<_>>()?),
}
pub fn families(styles: StyleChain) -> impl Iterator<Item = &FontFamily> + Clone {
let fallbacks = singleton!(Vec<FontFamily>, {
[
"libertinus serif",
"twitter color emoji",
"noto color emoji",
"apple color emoji",
"segoe ui emoji",
]
.into_iter()
.map(FontFamily::new)
.collect()
});
let tail = if TextElem::fallback_in(styles) { fallbacks.as_slice() } else { &[] };
TextElem::font_in(styles).into_iter().chain(tail.iter())
}
pub fn variant(styles: StyleChain) -> FontVariant {
let mut variant = FontVariant::new(
TextElem::style_in(styles),
TextElem::weight_in(styles),
TextElem::stretch_in(styles),
);
let WeightDelta(delta) = TextElem::delta_in(styles);
variant.weight = variant
.weight
.thicken(delta.clamp(i16::MIN as i64, i16::MAX as i64) as i16);
if TextElem::emph_in(styles).0 {
variant.style = match variant.style {
FontStyle::Normal => FontStyle::Italic,
FontStyle::Italic => FontStyle::Normal,
FontStyle::Oblique => FontStyle::Normal,
}
}
variant
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct TextSize(pub Length);
impl Fold for TextSize {
fn fold(self, outer: Self) -> Self {
Self(Length {
em: Em::new(self.0.em.get() * outer.0.em.get()),
abs: self.0.em.get() * outer.0.abs + self.0.abs,
})
}
}
impl Resolve for TextSize {
type Output = Abs;
fn resolve(self, styles: StyleChain) -> Self::Output {
let factor = match EquationElem::size_in(styles) {
MathSize::Display | MathSize::Text => 1.0,
MathSize::Script => EquationElem::script_scale_in(styles).0 as f64 / 100.0,
MathSize::ScriptScript => {
EquationElem::script_scale_in(styles).1 as f64 / 100.0
}
};
factor * self.0.resolve(styles)
}
}
cast! {
TextSize,
self => self.0.into_value(),
v: Length => Self(v),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum TopEdge {
Metric(TopEdgeMetric),
Length(Length),
}
cast! {
TopEdge,
self => match self {
Self::Metric(metric) => metric.into_value(),
Self::Length(length) => length.into_value(),
},
v: TopEdgeMetric => Self::Metric(v),
v: Length => Self::Length(v),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum TopEdgeMetric {
Ascender,
CapHeight,
XHeight,
Baseline,
Bounds,
}
impl TryInto<VerticalFontMetric> for TopEdgeMetric {
type Error = ();
fn try_into(self) -> Result<VerticalFontMetric, Self::Error> {
match self {
Self::Ascender => Ok(VerticalFontMetric::Ascender),
Self::CapHeight => Ok(VerticalFontMetric::CapHeight),
Self::XHeight => Ok(VerticalFontMetric::XHeight),
Self::Baseline => Ok(VerticalFontMetric::Baseline),
_ => Err(()),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum BottomEdge {
Metric(BottomEdgeMetric),
Length(Length),
}
cast! {
BottomEdge,
self => match self {
Self::Metric(metric) => metric.into_value(),
Self::Length(length) => length.into_value(),
},
v: BottomEdgeMetric => Self::Metric(v),
v: Length => Self::Length(v),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum BottomEdgeMetric {
Baseline,
Descender,
Bounds,
}
impl TryInto<VerticalFontMetric> for BottomEdgeMetric {
type Error = ();
fn try_into(self) -> Result<VerticalFontMetric, Self::Error> {
match self {
Self::Baseline => Ok(VerticalFontMetric::Baseline),
Self::Descender => Ok(VerticalFontMetric::Descender),
_ => Err(()),
}
}
}
#[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.is_custom_and(|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,
}
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct StylisticSets(u32);
impl StylisticSets {
pub fn into_array(self) -> Array {
self.sets().map(IntoValue::into_value).collect()
}
pub fn has(self, ss: u8) -> bool {
self.0 & (1 << (ss as u32)) != 0
}
pub fn sets(self) -> impl Iterator<Item = u8> {
(1..=20).filter(move |i| self.has(*i))
}
}
cast! {
StylisticSets,
self => self.into_array().into_value(),
_: NoneValue => Self(0),
v: i64 => match v {
1 ..= 20 => Self(1 << (v as u32)),
_ => bail!("stylistic set must be between 1 and 20"),
},
v: Vec<i64> => {
let mut flags = 0;
for i in v {
match i {
1 ..= 20 => flags |= 1 << (i as u32),
_ => bail!("stylistic set must be between 1 and 20"),
}
}
Self(flags)
},
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum NumberType {
Lining,
OldStyle,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum NumberWidth {
Proportional,
Tabular,
}
#[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::<HintedStrResult<_>>()?),
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::<HintedStrResult<_>>()?),
}
impl Fold for FontFeatures {
fn fold(self, outer: Self) -> Self {
Self(self.0.fold(outer.0))
}
}
pub fn features(styles: StyleChain) -> Vec<Feature> {
let mut tags = vec![];
let mut feat = |tag: &[u8; 4], value: u32| {
tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
};
if !TextElem::kerning_in(styles) {
feat(b"kern", 0);
}
if let Some(sc) = TextElem::smallcaps_in(styles) {
feat(b"smcp", 1);
if sc == Smallcaps::All {
feat(b"c2sc", 1);
}
}
if TextElem::alternates_in(styles) {
feat(b"salt", 1);
}
for set in TextElem::stylistic_set_in(styles).sets() {
let storage = [b's', b's', b'0' + set / 10, b'0' + set % 10];
feat(&storage, 1);
}
if !TextElem::ligatures_in(styles) {
feat(b"liga", 0);
feat(b"clig", 0);
}
if TextElem::discretionary_ligatures_in(styles) {
feat(b"dlig", 1);
}
if TextElem::historical_ligatures_in(styles) {
feat(b"hlig", 1);
}
match TextElem::number_type_in(styles) {
Smart::Auto => {}
Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
}
match TextElem::number_width_in(styles) {
Smart::Auto => {}
Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
}
if TextElem::slashed_zero_in(styles) {
feat(b"zero", 1);
}
if TextElem::fractions_in(styles) {
feat(b"frac", 1);
}
for (tag, value) in TextElem::features_in(styles).0 {
tags.push(Feature::new(tag, value, ..))
}
tags
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct ItalicToggle(pub bool);
impl Fold for ItalicToggle {
fn fold(self, outer: Self) -> Self {
Self(self.0 ^ outer.0)
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct WeightDelta(pub i64);
impl Fold for WeightDelta {
fn fold(self, outer: Self) -> Self {
Self(outer.0 + self.0)
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub struct Costs {
hyphenation: Option<Ratio>,
runt: Option<Ratio>,
widow: Option<Ratio>,
orphan: Option<Ratio>,
}
impl Costs {
#[must_use]
pub fn hyphenation(&self) -> Ratio {
self.hyphenation.unwrap_or(Ratio::one())
}
#[must_use]
pub fn runt(&self) -> Ratio {
self.runt.unwrap_or(Ratio::one())
}
#[must_use]
pub fn widow(&self) -> Ratio {
self.widow.unwrap_or(Ratio::one())
}
#[must_use]
pub fn orphan(&self) -> Ratio {
self.orphan.unwrap_or(Ratio::one())
}
}
impl Fold for Costs {
#[inline]
fn fold(self, outer: Self) -> Self {
Self {
hyphenation: self.hyphenation.or(outer.hyphenation),
runt: self.runt.or(outer.runt),
widow: self.widow.or(outer.widow),
orphan: self.orphan.or(outer.orphan),
}
}
}
cast! {
Costs,
self => dict![
"hyphenation" => self.hyphenation(),
"runt" => self.runt(),
"widow" => self.widow(),
"orphan" => self.orphan(),
].into_value(),
mut v: Dict => {
let ret = Self {
hyphenation: v.take("hyphenation").ok().map(|v| v.cast()).transpose()?,
runt: v.take("runt").ok().map(|v| v.cast()).transpose()?,
widow: v.take("widow").ok().map(|v| v.cast()).transpose()?,
orphan: v.take("orphan").ok().map(|v| v.cast()).transpose()?,
};
v.finish(&["hyphenation", "runt", "widow", "orphan"])?;
ret
},
}
pub fn is_default_ignorable(c: char) -> bool {
static DEFAULT_IGNORABLE_DATA: LazyLock<CodePointSetData> = LazyLock::new(|| {
icu_properties::sets::load_default_ignorable_code_point(
&BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU)
.unwrap()
.as_deserializing(),
)
.unwrap()
});
DEFAULT_IGNORABLE_DATA.as_borrowed().contains(c)
}
fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) {
let book = engine.world.book();
for family in &list.v {
if !book.contains_family(family.as_str()) {
engine.sink.warn(warning!(
list.span,
"unknown font family: {}",
family.as_str(),
));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_elem_size() {
assert_eq!(std::mem::size_of::<TextElem>(), std::mem::size_of::<EcoString>());
}
}