summaryrefslogtreecommitdiff
path: root/library/src/text/mod.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-03 11:44:53 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-03 13:35:39 +0100
commit37a7afddfaffd44cb9bc013c9506599267e08983 (patch)
tree20e7d62d3c5418baff01a21d0406b91bf3096214 /library/src/text/mod.rs
parent56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff)
Split crates
Diffstat (limited to 'library/src/text/mod.rs')
-rw-r--r--library/src/text/mod.rs555
1 files changed, 555 insertions, 0 deletions
diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs
new file mode 100644
index 00000000..d793f614
--- /dev/null
+++ b/library/src/text/mod.rs
@@ -0,0 +1,555 @@
+//! Text handling and paragraph layout.
+
+mod deco;
+mod link;
+mod par;
+mod quotes;
+mod raw;
+mod shaping;
+mod shift;
+
+pub use deco::*;
+pub use link::*;
+pub use par::*;
+pub use quotes::*;
+pub use raw::*;
+pub use shaping::*;
+pub use shift::*;
+
+use std::borrow::Cow;
+
+use rustybuzz::Tag;
+use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
+use typst::util::EcoString;
+
+use crate::prelude::*;
+
+/// A single run of text with the same style.
+#[derive(Debug, Clone, Hash)]
+pub struct TextNode(pub EcoString);
+
+#[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 font fallback when the primary font list contains no
+ /// match.
+ pub const FALLBACK: bool = true;
+
+ /// How the font is styled.
+ pub const STYLE: FontStyle = FontStyle::Normal;
+ /// The boldness / thickness of the font's glyphs.
+ pub const WEIGHT: FontWeight = FontWeight::REGULAR;
+ /// The width of the glyphs.
+ pub const STRETCH: FontStretch = FontStretch::NORMAL;
+
+ /// The size of the glyphs.
+ #[property(shorthand, fold)]
+ pub const SIZE: TextSize = Abs::pt(11.0);
+ /// The glyph fill color.
+ #[property(shorthand)]
+ pub const FILL: Paint = Color::BLACK.into();
+ /// The amount of space that should be added between characters.
+ #[property(resolve)]
+ pub const TRACKING: Length = Length::zero();
+ /// The width of spaces relative to the font's space width.
+ #[property(resolve)]
+ pub const SPACING: Rel<Length> = Rel::one();
+ /// The offset of the baseline.
+ #[property(resolve)]
+ pub const BASELINE: Length = Length::zero();
+ /// Whether certain glyphs can hang over into the margin.
+ pub const OVERHANG: bool = true;
+ /// The top end of the text bounding box.
+ pub const TOP_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::CapHeight);
+ /// The bottom end of the text bounding box.
+ pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline);
+
+ /// An ISO 639-1/2/3 language code.
+ pub const LANG: Lang = Lang::ENGLISH;
+ /// An ISO 3166-1 alpha-2 region code.
+ pub const REGION: Option<Region> = None;
+ /// The direction for text and inline objects. When `auto`, the direction is
+ /// automatically inferred from the language.
+ #[property(resolve)]
+ pub const DIR: HorizontalDir = HorizontalDir(Smart::Auto);
+ /// Whether to hyphenate text to improve line breaking. When `auto`, words
+ /// will will be hyphenated if and only if justification is enabled.
+ #[property(resolve)]
+ pub const HYPHENATE: Hyphenate = Hyphenate(Smart::Auto);
+ /// Whether to apply smart quotes.
+ pub const SMART_QUOTES: bool = true;
+
+ /// Whether to apply kerning ("kern").
+ pub const KERNING: bool = true;
+ /// Whether to apply stylistic alternates. ("salt")
+ pub const ALTERNATES: bool = false;
+ /// Which stylistic set to apply. ("ss01" - "ss20")
+ pub const STYLISTIC_SET: Option<StylisticSet> = None;
+ /// Whether standard ligatures are active. ("liga", "clig")
+ pub const LIGATURES: bool = true;
+ /// Whether ligatures that should be used sparingly are active. ("dlig")
+ pub const DISCRETIONARY_LIGATURES: bool = false;
+ /// Whether historical ligatures are active. ("hlig")
+ pub const HISTORICAL_LIGATURES: bool = false;
+ /// Which kind of numbers / figures to select.
+ pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto;
+ /// The width of numbers / figures.
+ pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto;
+ /// Whether to have a slash through the zero glyph. ("zero")
+ pub const SLASHED_ZERO: bool = false;
+ /// Whether to convert fractions. ("frac")
+ pub const FRACTIONS: bool = false;
+ /// Raw OpenType features to apply.
+ #[property(fold)]
+ pub const FEATURES: FontFeatures = FontFeatures(vec![]);
+
+ /// Whether the font weight should be increased by 300.
+ #[property(skip, fold)]
+ pub const BOLD: Toggle = false;
+ /// Whether the font style should be inverted.
+ #[property(skip, fold)]
+ pub const ITALIC: Toggle = false;
+ /// A case transformation that should be applied to the text.
+ #[property(skip)]
+ pub const CASE: Option<Case> = None;
+ /// Whether small capital glyphs should be used. ("smcp")
+ #[property(skip)]
+ pub const SMALLCAPS: bool = false;
+ /// A destination the text should be linked to.
+ #[property(skip, referenced)]
+ pub const LINK: Option<Destination> = None;
+ /// Decorative lines.
+ #[property(skip, fold)]
+ pub const DECO: Decoration = vec![];
+
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ // 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 <Content as Cast<Spanned<Value>>>::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));
+ }
+ }
+ }
+}
+
+/// 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,
+ Expected: "string",
+ Value::Str(string) => Self::new(&string),
+}
+
+/// Font family fallback list.
+#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+pub struct FallbackList(pub Vec<FontFamily>);
+
+castable! {
+ FallbackList,
+ Expected: "string or array of strings",
+ Value::Str(string) => Self(vec![FontFamily::new(&string)]),
+ Value::Array(values) => Self(values
+ .into_iter()
+ .filter_map(|v| v.cast().ok())
+ .map(|string: EcoString| FontFamily::new(&string))
+ .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: Length);
+
+/// 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,
+ Expected: "string or length",
+ Value::Length(v) => Self::Length(v),
+ Value::Str(string) => Self::Metric(match string.as_str() {
+ "ascender" => VerticalFontMetric::Ascender,
+ "cap-height" => VerticalFontMetric::CapHeight,
+ "x-height" => VerticalFontMetric::XHeight,
+ "baseline" => VerticalFontMetric::Baseline,
+ "descender" => VerticalFontMetric::Descender,
+ _ => Err("unknown font metric")?,
+ }),
+}
+
+/// The direction of text and inline objects in their line.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct HorizontalDir(pub Smart<Dir>);
+
+castable! {
+ HorizontalDir,
+ Expected: "direction or auto",
+ Value::Auto => 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<bool>);
+
+castable! {
+ Hyphenate,
+ Expected: "boolean or auto",
+ Value::Auto => Self(Smart::Auto),
+ Value::Bool(v) => 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,
+ Expected: "integer",
+ Value::Int(v) => 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,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "lining" => Self::Lining,
+ "old-style" => Self::OldStyle,
+ _ => Err(r#"expected "lining" or "old-style""#)?,
+ },
+}
+
+/// 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,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "proportional" => Self::Proportional,
+ "tabular" => Self::Tabular,
+ _ => Err(r#"expected "proportional" or "tabular""#)?,
+ },
+}
+
+/// OpenType font features settings.
+#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+pub struct FontFeatures(pub Vec<(Tag, u32)>);
+
+castable! {
+ FontFeatures,
+ Expected: "array of strings or dictionary mapping tags to integers",
+ Value::Array(values) => Self(values
+ .into_iter()
+ .filter_map(|v| v.cast().ok())
+ .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1))
+ .collect()),
+ Value::Dict(values) => Self(values
+ .into_iter()
+ .filter_map(|(k, v)| {
+ let tag = Tag::from_bytes_lossy(k.as_bytes());
+ let num = v.cast::<i64>().ok()?.try_into().ok()?;
+ Some((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
+ }
+}
+
+/// A text space.
+#[derive(Debug, Clone, Hash)]
+pub struct SpaceNode;
+
+#[node]
+impl SpaceNode {
+ fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
+ Ok(Self.pack())
+ }
+}
+
+/// A line break.
+#[derive(Debug, Clone, Hash)]
+pub struct LinebreakNode {
+ pub justify: bool,
+}
+
+#[node]
+impl LinebreakNode {
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ let justify = args.named("justify")?.unwrap_or(false);
+ Ok(Self { justify }.pack())
+ }
+}
+
+/// A smart quote.
+#[derive(Debug, Clone, Hash)]
+pub struct SmartQuoteNode {
+ pub double: bool,
+}
+
+#[node]
+impl SmartQuoteNode {
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ let double = args.named("double")?.unwrap_or(true);
+ Ok(Self { double }.pack())
+ }
+}
+
+/// Convert a string or content to lowercase.
+pub fn lower(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ case(Case::Lower, args)
+}
+
+/// Convert a string or content to uppercase.
+pub fn upper(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ case(Case::Upper, args)
+}
+
+/// Change the case of text.
+fn case(case: Case, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v, span } = args.expect("string or content")?;
+ Ok(match v {
+ Value::Str(v) => Value::Str(case.apply(&v).into()),
+ Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))),
+ v => bail!(span, "expected string or content, found {}", v.type_name()),
+ })
+}
+
+/// A case transformation on text.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Case {
+ /// Everything is uppercased.
+ Upper,
+ /// Everything is lowercased.
+ Lower,
+}
+
+impl Case {
+ /// Apply the case to a string.
+ pub fn apply(self, text: &str) -> String {
+ match self {
+ Self::Upper => text.to_uppercase(),
+ Self::Lower => text.to_lowercase(),
+ }
+ }
+}
+
+/// Display text in small capitals.
+pub fn smallcaps(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let body: Content = args.expect("content")?;
+ Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true)))
+}
+
+/// Strong content, rendered in boldface by default.
+#[derive(Debug, Hash)]
+pub struct StrongNode(pub Content);
+
+#[node(Show)]
+impl StrongNode {
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self(args.expect("body")?).pack())
+ }
+}
+
+impl Show for StrongNode {
+ fn unguard_parts(&self, sel: Selector) -> Content {
+ Self(self.0.unguard(sel)).pack()
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "body" => Some(Value::Content(self.0.clone())),
+ _ => None,
+ }
+ }
+
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
+ Ok(self.0.clone().styled(TextNode::BOLD, Toggle))
+ }
+}
+
+/// Emphasized content, rendered with an italic font by default.
+#[derive(Debug, Hash)]
+pub struct EmphNode(pub Content);
+
+#[node(Show)]
+impl EmphNode {
+ fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self(args.expect("body")?).pack())
+ }
+}
+
+impl Show for EmphNode {
+ fn unguard_parts(&self, sel: Selector) -> Content {
+ Self(self.0.unguard(sel)).pack()
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "body" => Some(Value::Content(self.0.clone())),
+ _ => None,
+ }
+ }
+
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
+ Ok(self.0.clone().styled(TextNode::ITALIC, Toggle))
+ }
+}
+
+/// A toggle that turns on and off alternatingly if folded.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Toggle;
+
+impl Fold for Toggle {
+ type Output = bool;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ !outer
+ }
+}
+
+impl Fold for Decoration {
+ type Output = Vec<Self>;
+
+ fn fold(self, mut outer: Self::Output) -> Self::Output {
+ outer.insert(0, self);
+ outer
+ }
+}