diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-11-03 11:44:53 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-11-03 13:35:39 +0100 |
| commit | 37a7afddfaffd44cb9bc013c9506599267e08983 (patch) | |
| tree | 20e7d62d3c5418baff01a21d0406b91bf3096214 /src/library/text | |
| parent | 56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff) | |
Split crates
Diffstat (limited to 'src/library/text')
| -rw-r--r-- | src/library/text/deco.rs | 249 | ||||
| -rw-r--r-- | src/library/text/link.rs | 114 | ||||
| -rw-r--r-- | src/library/text/mod.rs | 593 | ||||
| -rw-r--r-- | src/library/text/par.rs | 1266 | ||||
| -rw-r--r-- | src/library/text/quotes.rs | 148 | ||||
| -rw-r--r-- | src/library/text/raw.rs | 205 | ||||
| -rw-r--r-- | src/library/text/shaping.rs | 654 | ||||
| -rw-r--r-- | src/library/text/shift.rs | 187 |
8 files changed, 0 insertions, 3416 deletions
diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs deleted file mode 100644 index 158647f2..00000000 --- a/src/library/text/deco.rs +++ /dev/null @@ -1,249 +0,0 @@ -use kurbo::{BezPath, Line, ParamCurve}; -use ttf_parser::{GlyphId, OutlineBuilder}; - -use super::TextNode; -use crate::library::prelude::*; - -/// Typeset underline, stricken-through or overlined text. -#[derive(Debug, Hash)] -pub struct DecoNode<const L: DecoLine>(pub Content); - -/// Typeset underlined text. -pub type UnderlineNode = DecoNode<UNDERLINE>; - -/// Typeset stricken-through text. -pub type StrikethroughNode = DecoNode<STRIKETHROUGH>; - -/// Typeset overlined text. -pub type OverlineNode = DecoNode<OVERLINE>; - -#[node(Show)] -impl<const L: DecoLine> DecoNode<L> { - /// How to stroke the line. The text color and thickness are read from the - /// font tables if `auto`. - #[property(shorthand, resolve, fold)] - pub const STROKE: Smart<RawStroke> = Smart::Auto; - /// Position of the line relative to the baseline, read from the font tables - /// if `auto`. - #[property(resolve)] - pub const OFFSET: Smart<Length> = Smart::Auto; - /// Amount that the line will be longer or shorter than its associated text. - #[property(resolve)] - pub const EXTENT: Length = Length::zero(); - /// Whether the line skips sections in which it would collide - /// with the glyphs. Does not apply to strikethrough. - pub const EVADE: bool = true; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl<const L: DecoLine> Show for DecoNode<L> { - 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>, - styles: StyleChain, - ) -> SourceResult<Content> { - Ok(self.0.clone().styled(TextNode::DECO, Decoration { - line: L, - stroke: styles.get(Self::STROKE).unwrap_or_default(), - offset: styles.get(Self::OFFSET), - extent: styles.get(Self::EXTENT), - evade: styles.get(Self::EVADE), - })) - } -} - -/// Defines a line that is positioned over, under or on top of text. -/// -/// For more details, see [`DecoNode`]. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Decoration { - pub line: DecoLine, - pub stroke: RawStroke<Abs>, - pub offset: Smart<Abs>, - pub extent: Abs, - pub evade: bool, -} - -/// A kind of decorative line. -pub type DecoLine = usize; - -/// A line under text. -pub const UNDERLINE: DecoLine = 0; - -/// A line through text. -pub const STRIKETHROUGH: DecoLine = 1; - -/// A line over text. -pub const OVERLINE: DecoLine = 2; - -/// Add line decorations to a single run of shaped text. -pub fn decorate( - frame: &mut Frame, - deco: &Decoration, - text: &Text, - shift: Abs, - pos: Point, - width: Abs, -) { - let font_metrics = text.font.metrics(); - let metrics = match deco.line { - STRIKETHROUGH => font_metrics.strikethrough, - OVERLINE => font_metrics.overline, - UNDERLINE | _ => font_metrics.underline, - }; - - let evade = deco.evade && deco.line != STRIKETHROUGH; - let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift; - let stroke = deco.stroke.unwrap_or(Stroke { - paint: text.fill, - thickness: metrics.thickness.at(text.size), - }); - - let gap_padding = 0.08 * text.size; - let min_width = 0.162 * text.size; - - let mut start = pos.x - deco.extent; - let end = pos.x + (width + 2.0 * deco.extent); - - let mut push_segment = |from: Abs, to: Abs| { - let origin = Point::new(from, pos.y + offset); - let target = Point::new(to - from, Abs::zero()); - - if target.x >= min_width || !evade { - let shape = Geometry::Line(target).stroked(stroke); - frame.push(origin, Element::Shape(shape)); - } - }; - - if !evade { - push_segment(start, end); - return; - } - - let line = Line::new( - kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), - kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), - ); - - let mut x = pos.x; - let mut intersections = vec![]; - - for glyph in text.glyphs.iter() { - let dx = glyph.x_offset.at(text.size) + x; - let mut builder = - BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw()); - - let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); - let path = builder.finish(); - - x += glyph.x_advance.at(text.size); - - // Only do the costly segments intersection test if the line - // intersects the bounding box. - if bbox.map_or(false, |bbox| { - let y_min = -text.font.to_em(bbox.y_max).at(text.size); - let y_max = -text.font.to_em(bbox.y_min).at(text.size); - - offset >= y_min && offset <= y_max - }) { - // Find all intersections of segments with the line. - intersections.extend( - path.segments() - .flat_map(|seg| seg.intersect_line(line)) - .map(|is| Abs::raw(line.eval(is.line_t).x)), - ); - } - } - - // When emitting the decorative line segments, we move from left to - // right. The intersections are not necessarily in this order, yet. - intersections.sort(); - - for gap in intersections.chunks_exact(2) { - let l = gap[0] - gap_padding; - let r = gap[1] + gap_padding; - - if start >= end { - break; - } - - if start >= l { - start = r; - continue; - } - - push_segment(start, l); - start = r; - } - - if start < end { - push_segment(start, end); - } -} - -/// Builds a kurbo [`BezPath`] for a glyph. -struct BezPathBuilder { - path: BezPath, - units_per_em: f64, - font_size: Abs, - x_offset: f64, -} - -impl BezPathBuilder { - fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self { - Self { - path: BezPath::new(), - units_per_em, - font_size, - x_offset, - } - } - - fn finish(self) -> BezPath { - self.path - } - - fn p(&self, x: f32, y: f32) -> kurbo::Point { - kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) - } - - fn s(&self, v: f32) -> f64 { - Em::from_units(v, self.units_per_em).at(self.font_size).to_raw() - } -} - -impl OutlineBuilder for BezPathBuilder { - fn move_to(&mut self, x: f32, y: f32) { - self.path.move_to(self.p(x, y)); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.path.line_to(self.p(x, y)); - } - - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - self.path.quad_to(self.p(x1, y1), self.p(x, y)); - } - - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); - } - - fn close(&mut self) { - self.path.close_path(); - } -} diff --git a/src/library/text/link.rs b/src/library/text/link.rs deleted file mode 100644 index 1e9adc3e..00000000 --- a/src/library/text/link.rs +++ /dev/null @@ -1,114 +0,0 @@ -use super::TextNode; -use crate::library::prelude::*; - -/// Link text and other elements to a destination. -#[derive(Debug, Hash)] -pub struct LinkNode { - /// The destination the link points to. - pub dest: Destination, - /// How the link is represented. - pub body: Option<Content>, -} - -impl LinkNode { - /// Create a link node from a URL with its bare text. - pub fn from_url(url: EcoString) -> Self { - Self { dest: Destination::Url(url), body: None } - } -} - -#[node(Show)] -impl LinkNode { - /// The fill color of text in the link. Just the surrounding text color - /// if `auto`. - pub const FILL: Smart<Paint> = Smart::Auto; - /// Whether to underline the link. - pub const UNDERLINE: Smart<bool> = Smart::Auto; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - let dest = args.expect::<Destination>("destination")?; - let body = match dest { - Destination::Url(_) => args.eat()?, - Destination::Internal(_) => Some(args.expect("body")?), - }; - Ok(Self { dest, body }.pack()) - } -} - -castable! { - Destination, - Expected: "string or dictionary with `page`, `x`, and `y` keys", - Value::Str(string) => Self::Url(string.into()), - Value::Dict(dict) => { - let page = dict.get("page")?.clone().cast()?; - let x: Length = dict.get("x")?.clone().cast()?; - let y: Length = dict.get("y")?.clone().cast()?; - Self::Internal(Location { page, pos: Point::new(x.abs, y.abs) }) - }, -} - -impl Show for LinkNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self { - dest: self.dest.clone(), - body: self.body.as_ref().map(|body| body.unguard(sel)), - } - .pack() - } - - fn field(&self, name: &str) -> Option<Value> { - match name { - "url" => Some(match &self.dest { - Destination::Url(url) => Value::Str(url.clone().into()), - Destination::Internal(loc) => Value::Dict(loc.encode()), - }), - "body" => Some(match &self.body { - Some(body) => Value::Content(body.clone()), - None => Value::None, - }), - _ => None, - } - } - - fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> { - Ok(self - .body - .clone() - .unwrap_or_else(|| match &self.dest { - Destination::Url(url) => { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); - } - let shorter = text.len() < url.len(); - TextNode(if shorter { text.into() } else { url.clone() }).pack() - } - Destination::Internal(_) => Content::empty(), - }) - .styled(TextNode::LINK, Some(self.dest.clone()))) - } - - fn finalize( - &self, - _: Tracked<dyn World>, - styles: StyleChain, - mut realized: Content, - ) -> SourceResult<Content> { - let mut map = StyleMap::new(); - if let Smart::Custom(fill) = styles.get(Self::FILL) { - map.set(TextNode::FILL, fill); - } - - if match styles.get(Self::UNDERLINE) { - Smart::Auto => match &self.dest { - Destination::Url(_) => true, - Destination::Internal(_) => false, - }, - Smart::Custom(underline) => underline, - } { - realized = realized.underlined(); - } - - Ok(realized.styled_with_map(map)) - } -} diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs deleted file mode 100644 index 18e747d0..00000000 --- a/src/library/text/mod.rs +++ /dev/null @@ -1,593 +0,0 @@ -//! 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 crate::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; -use crate::library::prelude::*; -use crate::util::EcoString; - -/// 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: Vec<FontFamily> = 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: Smart<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: Smart<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: Vec<(Tag, u32)> = 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, 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), -} - -castable! { - Vec<FontFamily>, - Expected: "string or array of strings", - Value::Str(string) => vec![FontFamily::new(&string)], - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| FontFamily::new(&string)) - .collect(), -} - -castable! { - FontStyle, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "italic" => Self::Italic, - "oblique" => Self::Oblique, - _ => Err(r#"expected "normal", "italic" or "oblique""#)?, - }, -} - -castable! { - FontWeight, - Expected: "integer or string", - Value::Int(v) => Value::Int(v) - .cast::<usize>()? - .try_into() - .map_or(Self::BLACK, Self::from_number), - Value::Str(string) => match string.as_str() { - "thin" => Self::THIN, - "extralight" => Self::EXTRALIGHT, - "light" => Self::LIGHT, - "regular" => Self::REGULAR, - "medium" => Self::MEDIUM, - "semibold" => Self::SEMIBOLD, - "bold" => Self::BOLD, - "extrabold" => Self::EXTRABOLD, - "black" => Self::BLACK, - _ => Err("unknown font weight")?, - }, -} - -castable! { - FontStretch, - Expected: "ratio", - Value::Ratio(v) => Self::from_ratio(v.get() as f32), -} - -/// 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")?, - }), -} - -castable! { - Lang, - Expected: "string", - Value::Str(string) => Self::from_str(&string) - .ok_or("expected two or three letter language code (ISO 639-1/2/3)")?, -} - -castable! { - Region, - Expected: "string", - Value::Str(string) => Self::from_str(&string) - .ok_or("expected two letter region code (ISO 3166-1 alpha-2)")?, -} - -/// The direction of text and inline objects in their line. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct HorizontalDir(pub Dir); - -castable! { - HorizontalDir, - Expected: "direction", - @dir: Dir => match dir.axis() { - Axis::X => Self(*dir), - Axis::Y => Err("must be horizontal")?, - }, -} - -impl Resolve for Smart<HorizontalDir> { - type Output = Dir; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Smart::Auto => styles.get(TextNode::LANG).dir(), - Smart::Custom(dir) => dir.0, - } - } -} - -/// Whether to hyphenate text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Hyphenate(pub bool); - -castable!(Hyphenate: bool); - -impl Resolve for Smart<Hyphenate> { - type Output = bool; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Smart::Auto => styles.get(ParNode::JUSTIFY), - Smart::Custom(v) => v.0, - } - } -} - -/// 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""#)?, - }, -} - -castable! { - Vec<(Tag, u32)>, - Expected: "array of strings or dictionary mapping tags to integers", - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) - .collect(), - Value::Dict(values) => 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 Vec<(Tag, u32)> { - type Output = Self; - - fn fold(mut self, outer: Self::Output) -> Self::Output { - self.extend(outer); - 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 text, 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 text, 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 - } -} diff --git a/src/library/text/par.rs b/src/library/text/par.rs deleted file mode 100644 index 50089b20..00000000 --- a/src/library/text/par.rs +++ /dev/null @@ -1,1266 +0,0 @@ -use std::cmp::Ordering; - -use unicode_bidi::{BidiInfo, Level as BidiLevel}; -use unicode_script::{Script, UnicodeScript}; -use xi_unicode::LineBreakIterator; - -use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode}; -use crate::library::layout::Spacing; -use crate::library::prelude::*; -use crate::util::EcoString; - -/// Arrange text, spacing and inline-level nodes into a paragraph. -#[derive(Hash)] -pub struct ParNode(pub StyleVec<ParChild>); - -/// A uniformly styled atomic piece of a paragraph. -#[derive(Hash, PartialEq)] -pub enum ParChild { - /// A chunk of text. - Text(EcoString), - /// A single or double smart quote. - Quote { double: bool }, - /// Horizontal spacing between other children. - Spacing(Spacing), - /// Arbitrary inline-level content. - Inline(Content), -} - -#[node(LayoutBlock)] -impl ParNode { - /// The spacing between lines. - #[property(resolve)] - pub const LEADING: Length = Em::new(0.65).into(); - /// The extra spacing between paragraphs. - #[property(resolve)] - pub const SPACING: Length = Em::new(1.2).into(); - /// The indent the first line of a consecutive paragraph should have. - #[property(resolve)] - pub const INDENT: Length = Length::zero(); - /// Whether to allow paragraph spacing when there is paragraph indent. - pub const SPACING_AND_INDENT: bool = false; - - /// How to align text and inline objects in their line. - #[property(resolve)] - pub const ALIGN: HorizontalAlign = HorizontalAlign(RawAlign::Start); - /// Whether to justify text in its line. - pub const JUSTIFY: bool = false; - /// How to determine line breaks. - #[property(resolve)] - pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - // The paragraph constructor is special: It doesn't create a paragraph - // node. Instead, it just ensures that the passed content lives is in a - // separate paragraph and styles it. - Ok(Content::sequence(vec![ - ParbreakNode.pack(), - args.expect("body")?, - ParbreakNode.pack(), - ])) - } -} - -impl LayoutBlock for ParNode { - fn layout_block( - &self, - world: Tracked<dyn World>, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult<Vec<Frame>> { - // Collect all text into one string for BiDi analysis. - let (text, segments) = collect(self, &styles); - - // Perform BiDi analysis and then prepare paragraph layout by building a - // representation on which we can do line breaking without layouting - // each and every line from scratch. - let p = prepare(world, self, &text, segments, regions, styles)?; - - // Break the paragraph into lines. - let lines = linebreak(&p, world, regions.first.x); - - // Stack the lines into one frame per region. - stack(&p, world, &lines, regions) - } -} - -impl Debug for ParNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Par ")?; - self.0.fmt(f) - } -} - -impl Debug for ParChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Text(text) => write!(f, "Text({:?})", text), - Self::Quote { double } => write!(f, "Quote({double})"), - Self::Spacing(kind) => write!(f, "{:?}", kind), - Self::Inline(inline) => inline.fmt(f), - } - } -} - -impl PartialOrd for ParChild { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - match (self, other) { - (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), - _ => None, - } - } -} - -/// A horizontal alignment. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct HorizontalAlign(pub RawAlign); - -castable! { - HorizontalAlign, - Expected: "alignment", - @align: RawAlign => match align.axis() { - Axis::X => Self(*align), - Axis::Y => Err("must be horizontal")?, - }, -} - -impl Resolve for HorizontalAlign { - type Output = Align; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.0.resolve(styles) - } -} - -/// How to determine line breaks in a paragraph. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Linebreaks { - /// Determine the linebreaks in a simple first-fit style. - Simple, - /// Optimize the linebreaks for the whole paragraph. - Optimized, -} - -castable! { - Linebreaks, - Expected: "string", - Value::Str(string) => match string.as_str() { - "simple" => Self::Simple, - "optimized" => Self::Optimized, - _ => Err(r#"expected "simple" or "optimized""#)?, - }, -} - -impl Resolve for Smart<Linebreaks> { - type Output = Linebreaks; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.unwrap_or_else(|| { - if styles.get(ParNode::JUSTIFY) { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }) - } -} - -/// A paragraph break. -#[derive(Debug, Clone, Hash)] -pub struct ParbreakNode; - -#[node] -impl ParbreakNode { - fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> { - Ok(Self.pack()) - } -} - -/// Repeats content to fill a line. -#[derive(Debug, Hash)] -pub struct RepeatNode(pub Content); - -#[node(LayoutInline)] -impl RepeatNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl LayoutInline for RepeatNode { - fn layout_inline( - &self, - world: Tracked<dyn World>, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult<Vec<Frame>> { - self.0.layout_inline(world, regions, styles) - } -} - -/// Range of a substring of text. -type Range = std::ops::Range<usize>; - -// The characters by which spacing, inline content and pins are replaced in the -// paragraph's full text. -const SPACING_REPLACE: char = ' '; // Space -const NODE_REPLACE: char = '\u{FFFC}'; // Object Replacement Character - -/// A paragraph representation in which children are already layouted and text -/// is already preshaped. -/// -/// In many cases, we can directly reuse these results when constructing a line. -/// Only when a line break falls onto a text index that is not safe-to-break per -/// rustybuzz, we have to reshape that portion. -struct Preparation<'a> { - /// Bidirectional text embedding levels for the paragraph. - bidi: BidiInfo<'a>, - /// Text runs, spacing and layouted nodes. - items: Vec<Item<'a>>, - /// The styles shared by all children. - styles: StyleChain<'a>, - /// Whether to hyphenate if it's the same for all children. - hyphenate: Option<bool>, - /// The text language if it's the same for all children. - lang: Option<Lang>, - /// The resolved leading between lines. - leading: Abs, - /// The paragraph's resolved alignment. - align: Align, - /// Whether to justify the paragraph. - justify: bool, -} - -impl<'a> Preparation<'a> { - /// Find the item that contains the given `text_offset`. - fn find(&self, text_offset: usize) -> Option<&Item<'a>> { - let mut cursor = 0; - for item in &self.items { - let end = cursor + item.len(); - if (cursor .. end).contains(&text_offset) { - return Some(item); - } - cursor = end; - } - None - } - - /// Return the items that intersect the given `text_range`. - /// - /// Returns the expanded range around the items and the items. - fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) { - let mut cursor = 0; - let mut start = 0; - let mut end = 0; - let mut expanded = text_range.clone(); - - for (i, item) in self.items.iter().enumerate() { - if cursor <= text_range.start { - start = i; - expanded.start = cursor; - } - - let len = item.len(); - if cursor < text_range.end || cursor + len <= text_range.end { - end = i + 1; - expanded.end = cursor + len; - } else { - break; - } - - cursor += len; - } - - (expanded, &self.items[start .. end]) - } -} - -/// A segment of one or multiple collapsed children. -#[derive(Debug, Copy, Clone)] -enum Segment<'a> { - /// One or multiple collapsed text or text-equivalent children. Stores how - /// long the segment is (in bytes of the full text string). - Text(usize), - /// Horizontal spacing between other segments. - Spacing(Spacing), - /// Arbitrary inline-level content. - Inline(&'a Content), -} - -impl Segment<'_> { - /// The text length of the item. - fn len(&self) -> usize { - match *self { - Self::Text(len) => len, - Self::Spacing(_) => SPACING_REPLACE.len_utf8(), - Self::Inline(_) => NODE_REPLACE.len_utf8(), - } - } -} - -/// A prepared item in a paragraph layout. -#[derive(Debug)] -enum Item<'a> { - /// A shaped text run with consistent style and direction. - Text(ShapedText<'a>), - /// Absolute spacing between other items. - Absolute(Abs), - /// Fractional spacing between other items. - Fractional(Fr), - /// Layouted inline-level content. - Frame(Frame), - /// A repeating node that fills the remaining space in a line. - Repeat(&'a RepeatNode, StyleChain<'a>), -} - -impl<'a> Item<'a> { - /// If this a text item, return it. - fn text(&self) -> Option<&ShapedText<'a>> { - match self { - Self::Text(shaped) => Some(shaped), - _ => None, - } - } - - /// The text length of the item. - fn len(&self) -> usize { - match self { - Self::Text(shaped) => shaped.text.len(), - Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), - Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(), - } - } - - /// The natural layouted width of the item. - fn width(&self) -> Abs { - match self { - Self::Text(shaped) => shaped.width, - Self::Absolute(v) => *v, - Self::Frame(frame) => frame.width(), - Self::Fractional(_) | Self::Repeat(_, _) => Abs::zero(), - } - } -} - -/// A layouted line, consisting of a sequence of layouted paragraph items that -/// are mostly borrowed from the preparation phase. This type enables you to -/// measure the size of a line in a range before comitting to building the -/// line's frame. -/// -/// At most two paragraph items must be created individually for this line: The -/// first and last one since they may be broken apart by the start or end of the -/// line, respectively. But even those can partially reuse previous results when -/// the break index is safe-to-break per rustybuzz. -struct Line<'a> { - /// Bidi information about the paragraph. - bidi: &'a BidiInfo<'a>, - /// The trimmed range the line spans in the paragraph. - trimmed: Range, - /// The untrimmed end where the line ends. - end: usize, - /// A reshaped text item if the line sliced up a text item at the start. - first: Option<Item<'a>>, - /// Inner items which don't need to be reprocessed. - inner: &'a [Item<'a>], - /// A reshaped text item if the line sliced up a text item at the end. If - /// there is only one text item, this takes precedence over `first`. - last: Option<Item<'a>>, - /// The width of the line. - width: Abs, - /// Whether the line should be justified. - justify: bool, - /// Whether the line ends with a hyphen or dash, either naturally or through - /// hyphenation. - dash: bool, -} - -impl<'a> Line<'a> { - /// Iterate over the line's items. - fn items(&self) -> impl Iterator<Item = &Item<'a>> { - self.first.iter().chain(self.inner).chain(&self.last) - } - - /// Return items that intersect the given `text_range`. - fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> { - let mut cursor = self.trimmed.start; - let mut start = 0; - let mut end = 0; - - for (i, item) in self.items().enumerate() { - if cursor <= text_range.start { - start = i; - } - - let len = item.len(); - if cursor < text_range.end || cursor + len <= text_range.end { - end = i + 1; - } else { - break; - } - - cursor += len; - } - - self.items().skip(start).take(end - start) - } - - /// How many justifiable glyphs the line contains. - fn justifiables(&self) -> usize { - let mut count = 0; - for shaped in self.items().filter_map(Item::text) { - count += shaped.justifiables(); - } - count - } - - /// How much of the line is stretchable spaces. - fn stretch(&self) -> Abs { - let mut stretch = Abs::zero(); - for shaped in self.items().filter_map(Item::text) { - stretch += shaped.stretch(); - } - stretch - } - - /// The sum of fractions in the line. - fn fr(&self) -> Fr { - self.items() - .filter_map(|item| match item { - Item::Fractional(fr) => Some(*fr), - Item::Repeat(_, _) => Some(Fr::one()), - _ => None, - }) - .sum() - } -} - -/// Collect all text of the paragraph into one string. This also performs -/// string-level preprocessing like case transformations. -fn collect<'a>( - par: &'a ParNode, - styles: &'a StyleChain<'a>, -) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) { - let mut full = String::new(); - let mut quoter = Quoter::new(); - let mut segments = vec![]; - let mut iter = par.0.iter().peekable(); - - while let Some((child, map)) = iter.next() { - let styles = map.chain(&styles); - let segment = match child { - ParChild::Text(text) => { - let prev = full.len(); - if let Some(case) = styles.get(TextNode::CASE) { - full.push_str(&case.apply(text)); - } else { - full.push_str(text); - } - Segment::Text(full.len() - prev) - } - &ParChild::Quote { double } => { - let prev = full.len(); - if styles.get(TextNode::SMART_QUOTES) { - let lang = styles.get(TextNode::LANG); - let region = styles.get(TextNode::REGION); - let quotes = Quotes::from_lang(lang, region); - let peeked = iter.peek().and_then(|(child, _)| match child { - ParChild::Text(text) => text.chars().next(), - ParChild::Quote { .. } => Some('"'), - ParChild::Spacing(_) => Some(SPACING_REPLACE), - ParChild::Inline(_) => Some(NODE_REPLACE), - }); - - full.push_str(quoter.quote("es, double, peeked)); - } else { - full.push(if double { '"' } else { '\'' }); - } - Segment::Text(full.len() - prev) - } - &ParChild::Spacing(spacing) => { - full.push(SPACING_REPLACE); - Segment::Spacing(spacing) - } - ParChild::Inline(inline) => { - full.push(NODE_REPLACE); - Segment::Inline(inline) - } - }; - - if let Some(last) = full.chars().last() { - quoter.last(last); - } - - if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = - (segments.last_mut(), segment) - { - if *last_styles == styles { - *last_len += len; - continue; - } - } - - segments.push((segment, styles)); - } - - (full, segments) -} - -/// Prepare paragraph layout by shaping the whole paragraph and layouting all -/// contained inline-level content. -fn prepare<'a>( - world: Tracked<dyn World>, - par: &'a ParNode, - text: &'a str, - segments: Vec<(Segment<'a>, StyleChain<'a>)>, - regions: &Regions, - styles: StyleChain<'a>, -) -> SourceResult<Preparation<'a>> { - let bidi = BidiInfo::new(&text, match styles.get(TextNode::DIR) { - Dir::LTR => Some(BidiLevel::ltr()), - Dir::RTL => Some(BidiLevel::rtl()), - _ => None, - }); - - let mut cursor = 0; - let mut items = vec![]; - - // Shape / layout the children and collect them into items. - for (segment, styles) in segments { - let end = cursor + segment.len(); - match segment { - Segment::Text(_) => { - shape_range(&mut items, world, &bidi, cursor .. end, styles); - } - Segment::Spacing(spacing) => match spacing { - Spacing::Relative(v) => { - let resolved = v.resolve(styles).relative_to(regions.base.x); - items.push(Item::Absolute(resolved)); - } - Spacing::Fractional(v) => { - items.push(Item::Fractional(v)); - } - }, - Segment::Inline(inline) => { - if let Some(repeat) = inline.downcast::<RepeatNode>() { - items.push(Item::Repeat(repeat, styles)); - } else { - let size = Size::new(regions.first.x, regions.base.y); - let pod = Regions::one(size, regions.base, Axes::splat(false)); - let mut frame = inline.layout_inline(world, &pod, styles)?.remove(0); - frame.translate(Point::with_y(styles.get(TextNode::BASELINE))); - frame.apply_role(Role::GenericInline); - items.push(Item::Frame(frame)); - } - } - } - - cursor = end; - } - - Ok(Preparation { - bidi, - items, - styles, - hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE), - lang: shared_get(styles, &par.0, TextNode::LANG), - leading: styles.get(ParNode::LEADING), - align: styles.get(ParNode::ALIGN), - justify: styles.get(ParNode::JUSTIFY), - }) -} - -/// Group a range of text by BiDi level and script, shape the runs and generate -/// items for them. -fn shape_range<'a>( - items: &mut Vec<Item<'a>>, - world: Tracked<dyn World>, - bidi: &BidiInfo<'a>, - range: Range, - styles: StyleChain<'a>, -) { - let mut process = |text, level: BidiLevel| { - let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; - let shaped = shape(world, text, styles, dir); - items.push(Item::Text(shaped)); - }; - - let mut prev_level = BidiLevel::ltr(); - let mut prev_script = Script::Unknown; - let mut cursor = range.start; - - // Group by embedding level and script. - for i in cursor .. range.end { - if !bidi.text.is_char_boundary(i) { - continue; - } - - let level = bidi.levels[i]; - let script = - bidi.text[i ..].chars().next().map_or(Script::Unknown, |c| c.script()); - - if level != prev_level || !is_compatible(script, prev_script) { - if cursor < i { - process(&bidi.text[cursor .. i], prev_level); - } - cursor = i; - prev_level = level; - prev_script = script; - } else if is_generic_script(prev_script) { - prev_script = script; - } - } - - process(&bidi.text[cursor .. range.end], prev_level); -} - -/// Whether this is not a specific script. -fn is_generic_script(script: Script) -> bool { - matches!(script, Script::Unknown | Script::Common | Script::Inherited) -} - -/// Whether these script can be part of the same shape run. -fn is_compatible(a: Script, b: Script) -> bool { - is_generic_script(a) || is_generic_script(b) || a == b -} - -/// Get a style property, but only if it is the same for all children of the -/// paragraph. -fn shared_get<'a, K: Key<'a>>( - styles: StyleChain<'a>, - children: &StyleVec<ParChild>, - key: K, -) -> Option<K::Output> { - children - .styles() - .all(|map| !map.contains(key)) - .then(|| styles.get(key)) -} - -/// Find suitable linebreaks. -fn linebreak<'a>( - p: &'a Preparation<'a>, - world: Tracked<dyn World>, - width: Abs, -) -> Vec<Line<'a>> { - match p.styles.get(ParNode::LINEBREAKS) { - Linebreaks::Simple => linebreak_simple(p, world, width), - Linebreaks::Optimized => linebreak_optimized(p, world, width), - } -} - -/// Perform line breaking in simple first-fit style. This means that we build -/// lines greedily, always taking the longest possible line. This may lead to -/// very unbalanced line, but is fast and simple. -fn linebreak_simple<'a>( - p: &'a Preparation<'a>, - world: Tracked<dyn World>, - width: Abs, -) -> Vec<Line<'a>> { - let mut lines = vec![]; - let mut start = 0; - let mut last = None; - - for (end, mandatory, hyphen) in breakpoints(p) { - // Compute the line and its size. - let mut attempt = line(p, world, start .. end, mandatory, hyphen); - - // If the line doesn't fit anymore, we push the last fitting attempt - // into the stack and rebuild the line from the attempt's end. The - // resulting line cannot be broken up further. - if !width.fits(attempt.width) { - if let Some((last_attempt, last_end)) = last.take() { - lines.push(last_attempt); - start = last_end; - attempt = line(p, world, start .. end, mandatory, hyphen); - } - } - - // Finish the current line if there is a mandatory line break (i.e. - // due to "\n") or if the line doesn't fit horizontally already - // since then no shorter line will be possible. - if mandatory || !width.fits(attempt.width) { - lines.push(attempt); - start = end; - last = None; - } else { - last = Some((attempt, end)); - } - } - - if let Some((line, _)) = last { - lines.push(line); - } - - lines -} - -/// Perform line breaking in optimized Knuth-Plass style. Here, we use more -/// context to determine the line breaks than in the simple first-fit style. For -/// example, we might choose to cut a line short even though there is still a -/// bit of space to improve the fit of one of the following lines. The -/// Knuth-Plass algorithm is based on the idea of "cost". A line which has a -/// very tight or very loose fit has a higher cost than one that is just right. -/// Ending a line with a hyphen incurs extra cost and endings two successive -/// lines with hyphens even more. -/// -/// To find the layout with the minimal total cost the algorithm uses dynamic -/// programming: For each possible breakpoint it determines the optimal -/// paragraph layout _up to that point_. It walks over all possible start points -/// for a line ending at that point and finds the one for which the cost of the -/// line plus the cost of the optimal paragraph up to the start point (already -/// computed and stored in dynamic programming table) is minimal. The final -/// result is simply the layout determined for the last breakpoint at the end of -/// text. -fn linebreak_optimized<'a>( - p: &'a Preparation<'a>, - world: Tracked<dyn World>, - width: Abs, -) -> Vec<Line<'a>> { - /// The cost of a line or paragraph layout. - type Cost = f64; - - /// An entry in the dynamic programming table. - struct Entry<'a> { - pred: usize, - total: Cost, - line: Line<'a>, - } - - // Cost parameters. - const HYPH_COST: Cost = 0.5; - const CONSECUTIVE_DASH_COST: Cost = 30.0; - const MAX_COST: Cost = 1_000_000.0; - const MIN_COST: Cost = -MAX_COST; - const MIN_RATIO: f64 = -0.15; - - // Dynamic programming table. - let mut active = 0; - let mut table = vec![Entry { - pred: 0, - total: 0.0, - line: line(p, world, 0 .. 0, false, false), - }]; - - let em = p.styles.get(TextNode::SIZE); - - for (end, mandatory, hyphen) in breakpoints(p) { - let k = table.len(); - let eof = end == p.bidi.text.len(); - let mut best: Option<Entry> = None; - - // Find the optimal predecessor. - for (i, pred) in table.iter_mut().enumerate().skip(active) { - // Layout the line. - let start = pred.line.end; - let attempt = line(p, world, start .. end, mandatory, hyphen); - - // Determine how much the line's spaces would need to be stretched - // to make it the desired width. - let delta = width - attempt.width; - let mut ratio = delta / attempt.stretch(); - if ratio.is_infinite() { - ratio = delta / (em / 2.0); - } - - // At some point, it doesn't matter any more. - ratio = ratio.min(10.0); - - // Determine the cost of the line. - let min_ratio = if attempt.justify { MIN_RATIO } else { 0.0 }; - let mut cost = if ratio < min_ratio { - // The line is overfull. This is the case if - // - justification is on, but we'd need to shrink too much - // - justification is off and the line just doesn't fit - // Since any longer line will also be overfull, we can deactive - // this breakpoint. - active = i + 1; - MAX_COST - } else if mandatory || eof { - // This is a mandatory break and the line is not overfull, so it - // has minimum cost. All breakpoints before this one become - // inactive since no line can span above the mandatory break. - active = k; - MIN_COST + if attempt.justify { ratio.powi(3).abs() } else { 0.0 } - } else { - // Normal line with cost of |ratio^3|. - ratio.powi(3).abs() - }; - - // Penalize hyphens. - if hyphen { - cost += HYPH_COST; - } - - // Penalize two consecutive dashes (not necessarily hyphens) extra. - if attempt.dash && pred.line.dash { - cost += CONSECUTIVE_DASH_COST; - } - - // The total cost of this line and its chain of predecessors. - let total = pred.total + cost; - - // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { - best = Some(Entry { pred: i, total, line: attempt }); - } - } - - table.push(best.unwrap()); - } - - // Retrace the best path. - let mut lines = vec![]; - let mut idx = table.len() - 1; - while idx != 0 { - table.truncate(idx + 1); - let entry = table.pop().unwrap(); - lines.push(entry.line); - idx = entry.pred; - } - - lines.reverse(); - lines -} - -/// Determine all possible points in the text where lines can broken. -/// -/// Returns for each breakpoint the text index, whether the break is mandatory -/// (after `\n`) and whether a hyphen is required (when breaking inside of a -/// word). -fn breakpoints<'a>(p: &'a Preparation) -> Breakpoints<'a> { - Breakpoints { - p, - linebreaks: LineBreakIterator::new(p.bidi.text), - syllables: None, - offset: 0, - suffix: 0, - end: 0, - mandatory: false, - } -} - -/// An iterator over the line break opportunities in a text. -struct Breakpoints<'a> { - /// The paragraph's items. - p: &'a Preparation<'a>, - /// The inner iterator over the unicode line break opportunities. - linebreaks: LineBreakIterator<'a>, - /// Iterator over syllables of the current word. - syllables: Option<hypher::Syllables<'a>>, - /// The current text offset. - offset: usize, - /// The trimmed end of the current word. - suffix: usize, - /// The untrimmed end of the current word. - end: usize, - /// Whether the break after the current word is mandatory. - mandatory: bool, -} - -impl Iterator for Breakpoints<'_> { - type Item = (usize, bool, bool); - - fn next(&mut self) -> Option<Self::Item> { - // If we're currently in a hyphenated "word", process the next syllable. - if let Some(syllable) = self.syllables.as_mut().and_then(Iterator::next) { - self.offset += syllable.len(); - if self.offset == self.suffix { - self.offset = self.end; - } - - // Filter out hyphenation opportunities where hyphenation was - // actually disabled. - let hyphen = self.offset < self.end; - if hyphen && !self.hyphenate(self.offset) { - return self.next(); - } - - return Some((self.offset, self.mandatory && !hyphen, hyphen)); - } - - // Get the next "word". - (self.end, self.mandatory) = self.linebreaks.next()?; - - // Hyphenate the next word. - if self.p.hyphenate != Some(false) { - if let Some(lang) = self.lang(self.offset) { - let word = &self.p.bidi.text[self.offset .. self.end]; - let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic()); - if !trimmed.is_empty() { - self.suffix = self.offset + trimmed.len(); - self.syllables = Some(hypher::hyphenate(trimmed, lang)); - return self.next(); - } - } - } - - self.offset = self.end; - Some((self.end, self.mandatory, false)) - } -} - -impl Breakpoints<'_> { - /// Whether hyphenation is enabled at the given offset. - fn hyphenate(&self, offset: usize) -> bool { - self.p - .hyphenate - .or_else(|| { - let shaped = self.p.find(offset)?.text()?; - Some(shaped.styles.get(TextNode::HYPHENATE)) - }) - .unwrap_or(false) - } - - /// The text language at the given offset. - fn lang(&self, offset: usize) -> Option<hypher::Lang> { - let lang = self.p.lang.or_else(|| { - let shaped = self.p.find(offset)?.text()?; - Some(shaped.styles.get(TextNode::LANG)) - })?; - - let bytes = lang.as_str().as_bytes().try_into().ok()?; - hypher::Lang::from_iso(bytes) - } -} - -/// Create a line which spans the given range. -fn line<'a>( - p: &'a Preparation, - world: Tracked<dyn World>, - mut range: Range, - mandatory: bool, - hyphen: bool, -) -> Line<'a> { - let end = range.end; - let mut justify = p.justify && end < p.bidi.text.len() && !mandatory; - - if range.is_empty() { - return Line { - bidi: &p.bidi, - end, - trimmed: range, - first: None, - inner: &[], - last: None, - width: Abs::zero(), - justify, - dash: false, - }; - } - - // Slice out the relevant items. - let (expanded, mut inner) = p.slice(range.clone()); - let mut width = Abs::zero(); - - // Reshape the last item if it's split in half or hyphenated. - let mut last = None; - let mut dash = false; - if let Some((Item::Text(shaped), before)) = inner.split_last() { - // Compute the range we want to shape, trimming whitespace at the - // end of the line. - let base = expanded.end - shaped.text.len(); - let start = range.start.max(base); - let text = &p.bidi.text[start .. range.end]; - let trimmed = text.trim_end(); - range.end = start + trimmed.len(); - - // Deal with hyphens, dashes and justification. - let shy = trimmed.ends_with('\u{ad}'); - dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']); - justify |= text.ends_with('\u{2028}'); - - // Usually, we don't want to shape an empty string because: - // - We don't want the height of trimmed whitespace in a different - // font to be considered for the line height. - // - Even if it's in the same font, its unnecessary. - // - // There is one exception though. When the whole line is empty, we - // need the shaped empty string to make the line the appropriate - // height. That is the case exactly if the string is empty and there - // are no other items in the line. - if hyphen || start + shaped.text.len() > range.end { - if hyphen || start < range.end || before.is_empty() { - let shifted = start - base .. range.end - base; - let mut reshaped = shaped.reshape(world, shifted); - if hyphen || shy { - reshaped.push_hyphen(world); - } - width += reshaped.width; - last = Some(Item::Text(reshaped)); - } - - inner = before; - } - } - - // Reshape the start item if it's split in half. - let mut first = None; - if let Some((Item::Text(shaped), after)) = inner.split_first() { - // Compute the range we want to shape. - let base = expanded.start; - let end = range.end.min(base + shaped.text.len()); - - // Reshape if necessary. - if range.start + shaped.text.len() > end { - if range.start < end { - let shifted = range.start - base .. end - base; - let reshaped = shaped.reshape(world, shifted); - width += reshaped.width; - first = Some(Item::Text(reshaped)); - } - - inner = after; - } - } - - // Measure the inner items. - for item in inner { - width += item.width(); - } - - Line { - bidi: &p.bidi, - trimmed: range, - end, - first, - inner, - last, - width, - justify, - dash, - } -} - -/// Combine layouted lines into one frame per region. -fn stack( - p: &Preparation, - world: Tracked<dyn World>, - lines: &[Line], - regions: &Regions, -) -> SourceResult<Vec<Frame>> { - // Determine the paragraph's width: Full width of the region if we - // should expand or there's fractional spacing, fit-to-width otherwise. - let mut width = regions.first.x; - if !regions.expand.x && lines.iter().all(|line| line.fr().is_zero()) { - width = lines.iter().map(|line| line.width).max().unwrap_or_default(); - } - - // State for final frame building. - let mut regions = regions.clone(); - let mut finished = vec![]; - let mut first = true; - let mut output = Frame::new(Size::with_x(width)); - output.apply_role(Role::Paragraph); - - // Stack the lines into one frame per region. - for line in lines { - let frame = commit(p, world, line, ®ions, width)?; - let height = frame.size().y; - - while !regions.first.y.fits(height) && !regions.in_last() { - finished.push(output); - output = Frame::new(Size::with_x(width)); - output.apply_role(Role::Paragraph); - regions.next(); - first = true; - } - - if !first { - output.size_mut().y += p.leading; - } - - let pos = Point::with_y(output.height()); - output.size_mut().y += height; - output.push_frame(pos, frame); - - regions.first.y -= height + p.leading; - first = false; - } - - finished.push(output); - Ok(finished) -} - -/// Commit to a line and build its frame. -fn commit( - p: &Preparation, - world: Tracked<dyn World>, - line: &Line, - regions: &Regions, - width: Abs, -) -> SourceResult<Frame> { - let mut remaining = width - line.width; - let mut offset = Abs::zero(); - - // Reorder the line from logical to visual order. - let reordered = reorder(line); - - // Handle hanging punctuation to the left. - if let Some(Item::Text(text)) = reordered.first() { - if let Some(glyph) = text.glyphs.first() { - if !text.dir.is_positive() - && text.styles.get(TextNode::OVERHANG) - && (reordered.len() > 1 || text.glyphs.len() > 1) - { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); - offset -= amount; - remaining += amount; - } - } - } - - // Handle hanging punctuation to the right. - if let Some(Item::Text(text)) = reordered.last() { - if let Some(glyph) = text.glyphs.last() { - if text.dir.is_positive() - && text.styles.get(TextNode::OVERHANG) - && (reordered.len() > 1 || text.glyphs.len() > 1) - { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); - remaining += amount; - } - } - } - - // Determine how much to justify each space. - let fr = line.fr(); - let mut justification = Abs::zero(); - if remaining < Abs::zero() || (line.justify && fr.is_zero()) { - let justifiables = line.justifiables(); - if justifiables > 0 { - justification = remaining / justifiables as f64; - remaining = Abs::zero(); - } - } - - let mut top = Abs::zero(); - let mut bottom = Abs::zero(); - - // Build the frames and determine the height and baseline. - let mut frames = vec![]; - for item in reordered { - let mut push = |offset: &mut Abs, frame: Frame| { - let width = frame.width(); - top.set_max(frame.baseline()); - bottom.set_max(frame.size().y - frame.baseline()); - frames.push((*offset, frame)); - *offset += width; - }; - - match item { - Item::Absolute(v) => { - offset += *v; - } - Item::Fractional(v) => { - offset += v.share(fr, remaining); - } - Item::Text(shaped) => { - let frame = shaped.build(world, justification); - push(&mut offset, frame); - } - Item::Frame(frame) => { - push(&mut offset, frame.clone()); - } - Item::Repeat(repeat, styles) => { - let before = offset; - let fill = Fr::one().share(fr, remaining); - let size = Size::new(fill, regions.base.y); - let pod = Regions::one(size, regions.base, Axes::new(false, false)); - let frame = repeat.layout_inline(world, &pod, *styles)?.remove(0); - let width = frame.width(); - let count = (fill / width).floor(); - let remaining = fill % width; - let apart = remaining / (count - 1.0); - if count == 1.0 { - offset += p.align.position(remaining); - } - if width > Abs::zero() { - for _ in 0 .. (count as usize).min(1000) { - push(&mut offset, frame.clone()); - offset += apart; - } - } - offset = before + fill; - } - } - } - - // Remaining space is distributed now. - if !fr.is_zero() { - remaining = Abs::zero(); - } - - let size = Size::new(width, top + bottom); - let mut output = Frame::new(size); - output.set_baseline(top); - - // Construct the line's frame. - for (offset, frame) in frames { - let x = offset + p.align.position(remaining); - let y = top - frame.baseline(); - output.push_frame(Point::new(x, y), frame); - } - - Ok(output) -} - -/// Return a line's items in visual order. -fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> { - let mut reordered = vec![]; - - // The bidi crate doesn't like empty lines. - if line.trimmed.is_empty() { - return line.slice(line.trimmed.clone()).collect(); - } - - // Find the paragraph that contains the line. - let para = line - .bidi - .paragraphs - .iter() - .find(|para| para.range.contains(&line.trimmed.start)) - .unwrap(); - - // Compute the reordered ranges in visual order (left to right). - let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone()); - - // Collect the reordered items. - for run in runs { - // Skip reset L1 runs because handling them would require reshaping - // again in some cases. - if line.bidi.levels[run.start] != levels[run.start] { - continue; - } - - let prev = reordered.len(); - reordered.extend(line.slice(run.clone())); - - if levels[run.start].is_rtl() { - reordered[prev ..].reverse(); - } - } - - reordered -} - -/// How much a character should hang into the end margin. -/// -/// For more discussion, see: -/// https://recoveringphysicist.com/21/ -fn overhang(c: char) -> f64 { - match c { - // Dashes. - '–' | '—' => 0.2, - '-' => 0.55, - - // Punctuation. - '.' | ',' => 0.8, - ':' | ';' => 0.3, - - // Arabic and Ideographic - '\u{60C}' | '\u{6D4}' => 0.4, - '\u{3001}' | '\u{3002}' => 1.0, - - _ => 0.0, - } -} diff --git a/src/library/text/quotes.rs b/src/library/text/quotes.rs deleted file mode 100644 index 0a22646a..00000000 --- a/src/library/text/quotes.rs +++ /dev/null @@ -1,148 +0,0 @@ -use super::{Lang, Region}; -use crate::syntax::is_newline; - -/// State machine for smart quote subtitution. -#[derive(Debug, Clone)] -pub struct Quoter { - /// How many quotes have been opened. - quote_depth: usize, - /// Whether an opening quote might follow. - expect_opening: bool, - /// Whether the last character was numeric. - last_num: bool, -} - -impl Quoter { - /// Start quoting. - pub fn new() -> Self { - Self { - quote_depth: 0, - expect_opening: true, - last_num: false, - } - } - - /// Process the last seen character. - pub fn last(&mut self, c: char) { - self.expect_opening = is_ignorable(c) || is_opening_bracket(c); - self.last_num = c.is_numeric(); - } - - /// Process and substitute a quote. - pub fn quote<'a>( - &mut self, - quotes: &Quotes<'a>, - double: bool, - peeked: Option<char>, - ) -> &'a str { - let peeked = peeked.unwrap_or(' '); - if self.expect_opening { - self.quote_depth += 1; - quotes.open(double) - } else if self.quote_depth > 0 - && (peeked.is_ascii_punctuation() || is_ignorable(peeked)) - { - self.quote_depth -= 1; - quotes.close(double) - } else if self.last_num { - quotes.prime(double) - } else { - quotes.fallback(double) - } - } -} - -impl Default for Quoter { - fn default() -> Self { - Self::new() - } -} - -fn is_ignorable(c: char) -> bool { - c.is_whitespace() || is_newline(c) -} - -fn is_opening_bracket(c: char) -> bool { - matches!(c, '(' | '{' | '[') -} - -/// Decides which quotes to subtitute smart quotes with. -pub struct Quotes<'s> { - /// The opening single quote. - pub single_open: &'s str, - /// The closing single quote. - pub single_close: &'s str, - /// The opening double quote. - pub double_open: &'s str, - /// The closing double quote. - pub double_close: &'s str, -} - -impl<'s> Quotes<'s> { - /// Create a new `Quotes` struct with the defaults for a language and - /// region. - /// - /// The language should be specified as an all-lowercase ISO 639-1 code, the - /// region as an all-uppercase ISO 3166-alpha2 code. - /// - /// Currently, the supported languages are: English, Czech, Danish, German, - /// Swiss / Liechtensteinian German, Estonian, Icelandic, Lithuanian, - /// Latvian, Slovak, Slovenian, Bosnian, Finnish, Swedish, French, - /// Hungarian, Polish, Romanian, Japanese, Traditional Chinese, Russian, and - /// Norwegian. - /// - /// For unknown languages, the English quotes are used. - pub fn from_lang(lang: Lang, region: Option<Region>) -> Self { - let region = region.as_ref().map(Region::as_str); - let (single_open, single_close, double_open, double_close) = match lang.as_str() { - "de" if matches!(region, Some("CH" | "LI")) => ("‹", "›", "«", "»"), - "cs" | "da" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => { - ("‚", "‘", "„", "“") - } - "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"), - "bs" | "fi" | "sv" => ("’", "’", "”", "”"), - "hu" | "pl" | "ro" => ("’", "’", "„", "”"), - "ru" | "no" | "nn" => ("’", "’", "«", "»"), - _ => return Self::default(), - }; - - Self { - single_open, - single_close, - double_open, - double_close, - } - } - - /// The opening quote. - fn open(&self, double: bool) -> &'s str { - if double { self.double_open } else { self.single_open } - } - - /// The closing quote. - fn close(&self, double: bool) -> &'s str { - if double { self.double_close } else { self.single_close } - } - - /// Which character should be used as a prime. - fn prime(&self, double: bool) -> &'static str { - if double { "″" } else { "′" } - } - - /// Which character should be used as a fallback quote. - fn fallback(&self, double: bool) -> &'static str { - if double { "\"" } else { "’" } - } -} - -impl Default for Quotes<'_> { - /// Returns the english quotes as default. - fn default() -> Self { - Self { - single_open: "‘", - single_close: "’", - double_open: "“", - double_close: "”", - } - } -} diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs deleted file mode 100644 index 0c769636..00000000 --- a/src/library/text/raw.rs +++ /dev/null @@ -1,205 +0,0 @@ -use once_cell::sync::Lazy; -use syntect::easy::HighlightLines; -use syntect::highlighting::{ - Color, FontStyle, Style, StyleModifier, Theme, ThemeItem, ThemeSettings, -}; -use syntect::parsing::SyntaxSet; - -use super::{FontFamily, Hyphenate, LinebreakNode, TextNode}; -use crate::library::layout::{BlockNode, BlockSpacing}; -use crate::library::prelude::*; - -/// Monospaced text with optional syntax highlighting. -#[derive(Debug, Hash)] -pub struct RawNode { - /// The raw text. - pub text: EcoString, - /// Whether the node is block-level. - pub block: bool, -} - -#[node(Show)] -impl RawNode { - /// The language to syntax-highlight in. - #[property(referenced)] - pub const LANG: Option<EcoString> = None; - /// The raw text's font family. - #[property(referenced)] - pub const FAMILY: FontFamily = FontFamily::new("IBM Plex Mono"); - /// The spacing above block-level raw. - #[property(resolve, shorthand(around))] - pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into()); - /// The spacing below block-level raw. - #[property(resolve, shorthand(around))] - pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into()); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self { - text: args.expect("text")?, - block: args.named("block")?.unwrap_or(false), - } - .pack()) - } -} - -impl Show for RawNode { - fn unguard_parts(&self, _: Selector) -> Content { - Self { text: self.text.clone(), ..*self }.pack() - } - - fn field(&self, name: &str) -> Option<Value> { - match name { - "text" => Some(Value::Str(self.text.clone().into())), - "block" => Some(Value::Bool(self.block)), - _ => None, - } - } - - fn realize( - &self, - _: Tracked<dyn World>, - styles: StyleChain, - ) -> SourceResult<Content> { - let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase()); - let foreground = THEME - .settings - .foreground - .map(Color::from) - .unwrap_or(Color::BLACK) - .into(); - - let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { - let root = match lang.as_deref() { - Some("typc") => crate::syntax::parse_code(&self.text), - _ => crate::syntax::parse(&self.text), - }; - - let mut seq = vec![]; - crate::syntax::highlight::highlight_themed(&root, &THEME, |range, style| { - seq.push(styled(&self.text[range], foreground, style)); - }); - - Content::sequence(seq) - } else if let Some(syntax) = - lang.and_then(|token| SYNTAXES.find_syntax_by_token(&token)) - { - let mut seq = vec![]; - let mut highlighter = HighlightLines::new(syntax, &THEME); - for (i, line) in self.text.lines().enumerate() { - if i != 0 { - seq.push(LinebreakNode { justify: false }.pack()); - } - - for (style, piece) in - highlighter.highlight_line(line, &SYNTAXES).into_iter().flatten() - { - seq.push(styled(piece, foreground, style)); - } - } - - Content::sequence(seq) - } else { - TextNode(self.text.clone()).pack() - }; - - if self.block { - realized = BlockNode(realized).pack(); - } - - let mut map = StyleMap::new(); - map.set(TextNode::OVERHANG, false); - map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false))); - map.set(TextNode::SMART_QUOTES, false); - - Ok(realized.styled_with_map(map)) - } - - fn finalize( - &self, - _: Tracked<dyn World>, - styles: StyleChain, - mut realized: Content, - ) -> SourceResult<Content> { - let mut map = StyleMap::new(); - map.set_family(styles.get(Self::FAMILY).clone(), styles); - - if self.block { - realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); - } - - Ok(realized.styled_with_map(map)) - } -} - -/// Style a piece of text with a syntect style. -fn styled(piece: &str, foreground: Paint, style: Style) -> Content { - let mut body = TextNode(piece.into()).pack(); - - let paint = style.foreground.into(); - if paint != foreground { - body = body.styled(TextNode::FILL, paint); - } - - if style.font_style.contains(FontStyle::BOLD) { - body = body.strong(); - } - - if style.font_style.contains(FontStyle::ITALIC) { - body = body.emph(); - } - - if style.font_style.contains(FontStyle::UNDERLINE) { - body = body.underlined(); - } - - body -} - -/// The syntect syntax definitions. -static SYNTAXES: Lazy<SyntaxSet> = Lazy::new(|| SyntaxSet::load_defaults_newlines()); - -/// The default theme used for syntax highlighting. -#[rustfmt::skip] -pub static THEME: Lazy<Theme> = Lazy::new(|| Theme { - name: Some("Typst Light".into()), - author: Some("The Typst Project Developers".into()), - settings: ThemeSettings::default(), - scopes: vec![ - item("comment", Some("#8a8a8a"), None), - item("constant.character.escape", Some("#1d6c76"), None), - item("constant.character.shortcut", Some("#1d6c76"), None), - item("markup.bold", None, Some(FontStyle::BOLD)), - item("markup.italic", None, Some(FontStyle::ITALIC)), - item("markup.underline", None, Some(FontStyle::UNDERLINE)), - item("markup.raw", Some("#818181"), None), - item("string.other.math.typst", None, None), - item("punctuation.definition.math", Some("#298e0d"), None), - item("keyword.operator.math", Some("#1d6c76"), None), - item("markup.heading, entity.name.section", None, Some(FontStyle::BOLD)), - item("markup.heading.typst", None, Some(FontStyle::BOLD | FontStyle::UNDERLINE)), - item("punctuation.definition.list", Some("#8b41b1"), None), - item("markup.list.term", None, Some(FontStyle::BOLD)), - item("entity.name.label, markup.other.reference", Some("#1d6c76"), None), - item("keyword, constant.language, variable.language", Some("#d73a49"), None), - item("storage.type, storage.modifier", Some("#d73a49"), None), - item("constant", Some("#b60157"), None), - item("string", Some("#298e0d"), None), - item("entity.name, variable.function, support", Some("#4b69c6"), None), - item("support.macro", Some("#16718d"), None), - item("meta.annotation", Some("#301414"), None), - item("entity.other, meta.interpolation", Some("#8b41b1"), None), - item("invalid", Some("#ff0000"), None), - ], -}); - -/// Create a syntect theme item. -fn item(scope: &str, color: Option<&str>, font_style: Option<FontStyle>) -> ThemeItem { - ThemeItem { - scope: scope.parse().unwrap(), - style: StyleModifier { - foreground: color.map(|s| s.parse::<RgbaColor>().unwrap().into()), - background: None, - font_style, - }, - } -} diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs deleted file mode 100644 index 9ac3db1c..00000000 --- a/src/library/text/shaping.rs +++ /dev/null @@ -1,654 +0,0 @@ -use std::ops::Range; -use std::str::FromStr; - -use rustybuzz::{Feature, Tag, UnicodeBuffer}; - -use super::*; -use crate::font::{Font, FontVariant}; -use crate::library::prelude::*; -use crate::util::SliceExt; - -/// The result of shaping text. -/// -/// This type contains owned or borrowed shaped text runs, which can be -/// measured, used to reshape substrings more quickly and converted into a -/// frame. -pub struct ShapedText<'a> { - /// The text that was shaped. - pub text: &'a str, - /// The text direction. - pub dir: Dir, - /// The text's style properties. - pub styles: StyleChain<'a>, - /// The font variant. - pub variant: FontVariant, - /// The font size. - pub size: Abs, - /// The width of the text's bounding box. - pub width: Abs, - /// The shaped glyphs. - pub glyphs: Cow<'a, [ShapedGlyph]>, -} - -/// A single glyph resulting from shaping. -#[derive(Debug, Clone)] -pub struct ShapedGlyph { - /// The font the glyph is contained in. - pub font: Font, - /// The glyph's index in the font. - pub glyph_id: u16, - /// The advance width of the glyph. - pub x_advance: Em, - /// The horizontal offset of the glyph. - pub x_offset: Em, - /// The vertical offset of the glyph. - pub y_offset: Em, - /// The byte index in the source text where this glyph's cluster starts. A - /// cluster is a sequence of one or multiple glyphs that cannot be - /// separated and must always be treated as a union. - pub cluster: usize, - /// Whether splitting the shaping result before this glyph would yield the - /// same results as shaping the parts to both sides of `text_index` - /// separately. - pub safe_to_break: bool, - /// The first char in this glyph's cluster. - pub c: char, -} - -impl ShapedGlyph { - /// Whether the glyph is a space. - pub fn is_space(&self) -> bool { - matches!(self.c, ' ' | '\u{00A0}' | ' ') - } - - /// Whether the glyph is justifiable. - pub fn is_justifiable(&self) -> bool { - self.is_space() || matches!(self.c, ',' | '。' | '、') - } -} - -/// A side you can go toward. -enum Side { - /// To the left-hand side. - Left, - /// To the right-hand side. - Right, -} - -impl<'a> ShapedText<'a> { - /// Build the shaped text's frame. - /// - /// The `justification` defines how much extra advance width each - /// [justifiable glyph](ShapedGlyph::is_justifiable) will get. - pub fn build(&self, world: Tracked<dyn World>, justification: Abs) -> Frame { - let (top, bottom) = self.measure(world); - let size = Size::new(self.width, top + bottom); - - let mut offset = Abs::zero(); - let mut frame = Frame::new(size); - frame.set_baseline(top); - - let shift = self.styles.get(TextNode::BASELINE); - let lang = self.styles.get(TextNode::LANG); - let decos = self.styles.get(TextNode::DECO); - let fill = self.styles.get(TextNode::FILL); - let link = self.styles.get(TextNode::LINK); - - for ((font, y_offset), group) in - self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) - { - let pos = Point::new(offset, top + shift + y_offset.at(self.size)); - - let glyphs = group - .iter() - .map(|glyph| Glyph { - id: glyph.glyph_id, - x_advance: glyph.x_advance - + if glyph.is_justifiable() { - frame.size_mut().x += justification; - Em::from_length(justification, self.size) - } else { - Em::zero() - }, - x_offset: glyph.x_offset, - c: glyph.c, - }) - .collect(); - - let text = Text { - font, - size: self.size, - lang, - fill, - glyphs, - }; - - let text_layer = frame.layer(); - let width = text.width(); - - // Apply line decorations. - for deco in &decos { - decorate(&mut frame, &deco, &text, shift, pos, width); - } - - frame.insert(text_layer, pos, Element::Text(text)); - offset += width; - } - - // Apply link if it exists. - if let Some(dest) = link { - frame.link(dest.clone()); - } - - frame - } - - /// Measure the top and bottom extent of this text. - fn measure(&self, world: Tracked<dyn World>) -> (Abs, Abs) { - let mut top = Abs::zero(); - let mut bottom = Abs::zero(); - - let top_edge = self.styles.get(TextNode::TOP_EDGE); - let bottom_edge = self.styles.get(TextNode::BOTTOM_EDGE); - - // Expand top and bottom by reading the font's vertical metrics. - let mut expand = |font: &Font| { - let metrics = font.metrics(); - top.set_max(top_edge.resolve(self.styles, metrics)); - bottom.set_max(-bottom_edge.resolve(self.styles, metrics)); - }; - - if self.glyphs.is_empty() { - // When there are no glyphs, we just use the vertical metrics of the - // first available font. - for family in families(self.styles) { - if let Some(font) = world - .book() - .select(family, self.variant) - .and_then(|id| world.font(id)) - { - expand(&font); - break; - } - } - } else { - for g in self.glyphs.iter() { - expand(&g.font); - } - } - - (top, bottom) - } - - /// How many justifiable glyphs the text contains. - pub fn justifiables(&self) -> usize { - self.glyphs.iter().filter(|g| g.is_justifiable()).count() - } - - /// The width of the spaces in the text. - pub fn stretch(&self) -> Abs { - self.glyphs - .iter() - .filter(|g| g.is_justifiable()) - .map(|g| g.x_advance) - .sum::<Em>() - .at(self.size) - } - - /// Reshape a range of the shaped text, reusing information from this - /// shaping process if possible. - pub fn reshape( - &'a self, - world: Tracked<dyn World>, - text_range: Range<usize>, - ) -> ShapedText<'a> { - if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { - Self { - text: &self.text[text_range], - dir: self.dir, - styles: self.styles, - size: self.size, - variant: self.variant, - width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size), - glyphs: Cow::Borrowed(glyphs), - } - } else { - shape(world, &self.text[text_range], self.styles, self.dir) - } - } - - /// Push a hyphen to end of the text. - pub fn push_hyphen(&mut self, world: Tracked<dyn World>) { - families(self.styles).find_map(|family| { - let font = world - .book() - .select(family, self.variant) - .and_then(|id| world.font(id))?; - let ttf = font.ttf(); - let glyph_id = ttf.glyph_index('-')?; - let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?); - let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default(); - self.width += x_advance.at(self.size); - self.glyphs.to_mut().push(ShapedGlyph { - font, - glyph_id: glyph_id.0, - x_advance, - x_offset: Em::zero(), - y_offset: Em::zero(), - cluster, - safe_to_break: true, - c: '-', - }); - Some(()) - }); - } - - /// Find the subslice of glyphs that represent the given text range if both - /// sides are safe to break. - fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { - let Range { mut start, mut end } = text_range; - if !self.dir.is_positive() { - std::mem::swap(&mut start, &mut end); - } - - let left = self.find_safe_to_break(start, Side::Left)?; - let right = self.find_safe_to_break(end, Side::Right)?; - Some(&self.glyphs[left .. right]) - } - - /// Find the glyph offset matching the text index that is most towards the - /// given side and safe-to-break. - fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { - let ltr = self.dir.is_positive(); - - // Handle edge cases. - let len = self.glyphs.len(); - if text_index == 0 { - return Some(if ltr { 0 } else { len }); - } else if text_index == self.text.len() { - return Some(if ltr { len } else { 0 }); - } - - // Find any glyph with the text index. - let mut idx = self - .glyphs - .binary_search_by(|g| { - let ordering = g.cluster.cmp(&text_index); - if ltr { ordering } else { ordering.reverse() } - }) - .ok()?; - - let next = match towards { - Side::Left => usize::checked_sub, - Side::Right => usize::checked_add, - }; - - // Search for the outermost glyph with the text index. - while let Some(next) = next(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.cluster != text_index) { - break; - } - idx = next; - } - - // RTL needs offset one because the left side of the range should be - // exclusive and the right side inclusive, contrary to the normal - // behaviour of ranges. - self.glyphs[idx].safe_to_break.then(|| idx + (!ltr) as usize) - } -} - -impl Debug for ShapedText<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.text.fmt(f) - } -} - -/// Holds shaping results and metadata common to all shaped segments. -struct ShapingContext<'a> { - world: Tracked<'a, dyn World>, - glyphs: Vec<ShapedGlyph>, - used: Vec<Font>, - styles: StyleChain<'a>, - size: Abs, - variant: FontVariant, - tags: Vec<rustybuzz::Feature>, - fallback: bool, - dir: Dir, -} - -/// Shape text into [`ShapedText`]. -pub fn shape<'a>( - world: Tracked<dyn World>, - text: &'a str, - styles: StyleChain<'a>, - dir: Dir, -) -> ShapedText<'a> { - let size = styles.get(TextNode::SIZE); - - let mut ctx = ShapingContext { - world, - size, - glyphs: vec![], - used: vec![], - styles, - variant: variant(styles), - tags: tags(styles), - fallback: styles.get(TextNode::FALLBACK), - dir, - }; - - if !text.is_empty() { - shape_segment(&mut ctx, 0, &text, families(styles)); - } - - track_and_space(&mut ctx); - - ShapedText { - text, - dir, - styles, - variant: ctx.variant, - size, - width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size), - glyphs: Cow::Owned(ctx.glyphs), - } -} - -/// Shape text with font fallback using the `families` iterator. -fn shape_segment<'a>( - ctx: &mut ShapingContext, - base: usize, - text: &str, - mut families: impl Iterator<Item = &'a str> + Clone, -) { - // Fonts dont have newlines and tabs. - if text.chars().all(|c| c == '\n' || c == '\t') { - return; - } - - // Find the next available family. - let book = ctx.world.book(); - let mut selection = families.find_map(|family| { - book.select(family, ctx.variant) - .and_then(|id| ctx.world.font(id)) - .filter(|font| !ctx.used.contains(font)) - }); - - // Do font fallback if the families are exhausted and fallback is enabled. - if selection.is_none() && ctx.fallback { - let first = ctx.used.first().map(Font::info); - selection = book - .select_fallback(first, ctx.variant, text) - .and_then(|id| ctx.world.font(id)) - .filter(|font| !ctx.used.contains(font)); - } - - // Extract the font id or shape notdef glyphs if we couldn't find any font. - let font = if let Some(font) = selection { - font - } else { - if let Some(font) = ctx.used.first().cloned() { - shape_tofus(ctx, base, text, font); - } - return; - }; - - ctx.used.push(font.clone()); - - // Fill the buffer with our text. - let mut buffer = UnicodeBuffer::new(); - buffer.push_str(text); - buffer.set_language(language(ctx.styles)); - buffer.set_direction(match ctx.dir { - Dir::LTR => rustybuzz::Direction::LeftToRight, - Dir::RTL => rustybuzz::Direction::RightToLeft, - _ => unimplemented!("vertical text layout"), - }); - - // Shape! - let buffer = rustybuzz::shape(font.rusty(), &ctx.tags, buffer); - let infos = buffer.glyph_infos(); - let pos = buffer.glyph_positions(); - - // Collect the shaped glyphs, doing fallback and shaping parts again with - // the next font if necessary. - let mut i = 0; - while i < infos.len() { - let info = &infos[i]; - let cluster = info.cluster as usize; - - if info.glyph_id != 0 { - // Add the glyph to the shaped output. - // TODO: Don't ignore y_advance. - ctx.glyphs.push(ShapedGlyph { - font: font.clone(), - glyph_id: info.glyph_id as u16, - x_advance: font.to_em(pos[i].x_advance), - x_offset: font.to_em(pos[i].x_offset), - y_offset: font.to_em(pos[i].y_offset), - cluster: base + cluster, - safe_to_break: !info.unsafe_to_break(), - c: text[cluster ..].chars().next().unwrap(), - }); - } else { - // Determine the source text range for the tofu sequence. - let range = { - // First, search for the end of the tofu sequence. - let k = i; - while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) { - i += 1; - } - - // Then, determine the start and end text index. - // - // Examples: - // Everything is shown in visual order. Tofus are written as "_". - // We want to find out that the tofus span the text `2..6`. - // Note that the clusters are longer than 1 char. - // - // Left-to-right: - // Text: h a l i h a l l o - // Glyphs: A _ _ C E - // Clusters: 0 2 4 6 8 - // k=1 i=2 - // - // Right-to-left: - // Text: O L L A H I L A H - // Glyphs: E C _ _ A - // Clusters: 8 6 4 2 0 - // k=2 i=3 - let ltr = ctx.dir.is_positive(); - let first = if ltr { k } else { i }; - let start = infos[first].cluster as usize; - let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) }; - let end = last - .and_then(|last| infos.get(last)) - .map_or(text.len(), |info| info.cluster as usize); - - start .. end - }; - - // Trim half-baked cluster. - let remove = base + range.start .. base + range.end; - while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.cluster)) { - ctx.glyphs.pop(); - } - - // Recursively shape the tofu sequence with the next family. - shape_segment(ctx, base + range.start, &text[range], families.clone()); - } - - i += 1; - } - - ctx.used.pop(); -} - -/// Shape the text with tofus from the given font. -fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { - let x_advance = font.advance(0).unwrap_or_default(); - for (cluster, c) in text.char_indices() { - ctx.glyphs.push(ShapedGlyph { - font: font.clone(), - glyph_id: 0, - x_advance, - x_offset: Em::zero(), - y_offset: Em::zero(), - cluster: base + cluster, - safe_to_break: true, - c, - }); - } -} - -/// Apply tracking and spacing to the shaped glyphs. -fn track_and_space(ctx: &mut ShapingContext) { - let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), ctx.size); - let spacing = ctx - .styles - .get(TextNode::SPACING) - .map(|abs| Em::from_length(abs, ctx.size)); - - let mut glyphs = ctx.glyphs.iter_mut().peekable(); - while let Some(glyph) = glyphs.next() { - // Make non-breaking space same width as normal space. - if glyph.c == '\u{00A0}' { - glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default(); - } - - if glyph.is_space() { - glyph.x_advance = spacing.relative_to(glyph.x_advance); - } - - if glyphs.peek().map_or(false, |next| glyph.cluster != next.cluster) { - glyph.x_advance += tracking; - } - } -} - -/// Difference between non-breaking and normal space. -fn nbsp_delta(font: &Font) -> Option<Em> { - let space = font.ttf().glyph_index(' ')?.0; - let nbsp = font.ttf().glyph_index('\u{00A0}')?.0; - Some(font.advance(nbsp)? - font.advance(space)?) -} - -/// Resolve the font variant with `BOLD` and `ITALIC` factored in. -pub fn variant(styles: StyleChain) -> FontVariant { - let mut variant = FontVariant::new( - styles.get(TextNode::STYLE), - styles.get(TextNode::WEIGHT), - styles.get(TextNode::STRETCH), - ); - - if styles.get(TextNode::BOLD) { - variant.weight = variant.weight.thicken(300); - } - - if styles.get(TextNode::ITALIC) { - variant.style = match variant.style { - FontStyle::Normal => FontStyle::Italic, - FontStyle::Italic => FontStyle::Normal, - FontStyle::Oblique => FontStyle::Normal, - } - } - - variant -} - -/// Resolve a prioritized iterator over the font families. -fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { - const FALLBACKS: &[&str] = &[ - "ibm plex sans", - "twitter color emoji", - "noto color emoji", - "apple color emoji", - "segoe ui emoji", - ]; - - let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] }; - styles - .get(TextNode::FAMILY) - .iter() - .map(|family| family.as_str()) - .chain(tail.iter().copied()) -} - -/// Collect the tags of the OpenType features to apply. -fn tags(styles: StyleChain) -> Vec<Feature> { - let mut tags = vec![]; - let mut feat = |tag, value| { - tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); - }; - - // Features that are on by default in Harfbuzz are only added if disabled. - if !styles.get(TextNode::KERNING) { - feat(b"kern", 0); - } - - // Features that are off by default in Harfbuzz are only added if enabled. - if styles.get(TextNode::SMALLCAPS) { - feat(b"smcp", 1); - } - - if styles.get(TextNode::ALTERNATES) { - feat(b"salt", 1); - } - - let storage; - if let Some(set) = styles.get(TextNode::STYLISTIC_SET) { - storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; - feat(&storage, 1); - } - - if !styles.get(TextNode::LIGATURES) { - feat(b"liga", 0); - feat(b"clig", 0); - } - - if styles.get(TextNode::DISCRETIONARY_LIGATURES) { - feat(b"dlig", 1); - } - - if styles.get(TextNode::HISTORICAL_LIGATURES) { - feat(b"hilg", 1); - } - - match styles.get(TextNode::NUMBER_TYPE) { - Smart::Auto => {} - Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), - Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), - } - - match styles.get(TextNode::NUMBER_WIDTH) { - Smart::Auto => {} - Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), - Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), - } - - if styles.get(TextNode::SLASHED_ZERO) { - feat(b"zero", 1); - } - - if styles.get(TextNode::FRACTIONS) { - feat(b"frac", 1); - } - - for (tag, value) in styles.get(TextNode::FEATURES) { - tags.push(Feature::new(tag, value, ..)) - } - - tags -} - -/// Process the language and and region of a style chain into a -/// rustybuzz-compatible BCP 47 language. -fn language(styles: StyleChain) -> rustybuzz::Language { - let mut bcp: EcoString = styles.get(TextNode::LANG).as_str().into(); - if let Some(region) = styles.get(TextNode::REGION) { - bcp.push('-'); - bcp.push_str(region.as_str()); - } - rustybuzz::Language::from_str(&bcp).unwrap() -} diff --git a/src/library/text/shift.rs b/src/library/text/shift.rs deleted file mode 100644 index c3cf8b03..00000000 --- a/src/library/text/shift.rs +++ /dev/null @@ -1,187 +0,0 @@ -use super::{variant, SpaceNode, TextNode, TextSize}; -use crate::library::prelude::*; -use crate::model::SequenceNode; -use crate::util::EcoString; - -/// Sub or superscript text. -/// -/// The text is rendered smaller and its baseline is raised. To provide the best -/// typography possible, we first try to transform the text to superscript -/// codepoints. If that fails, we fall back to rendering shrunk normal letters -/// in a raised way. -#[derive(Debug, Hash)] -pub struct ShiftNode<const S: ScriptKind>(pub Content); - -/// Shift the text into superscript. -pub type SuperNode = ShiftNode<SUPERSCRIPT>; - -/// Shift the text into subscript. -pub type SubNode = ShiftNode<SUBSCRIPT>; - -#[node(Show)] -impl<const S: ScriptKind> ShiftNode<S> { - /// Whether to prefer the dedicated sub- and superscript characters of the - /// font. - pub const TYPOGRAPHIC: bool = true; - /// The baseline shift for synthetic sub- and superscripts. - pub const BASELINE: Length = - Em::new(if S == SUPERSCRIPT { -0.5 } else { 0.2 }).into(); - /// The font size for synthetic sub- and superscripts. - pub const SIZE: TextSize = TextSize(Em::new(0.6).into()); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl<const S: ScriptKind> Show for ShiftNode<S> { - fn unguard_parts(&self, _: Selector) -> Content { - Self(self.0.clone()).pack() - } - - fn field(&self, name: &str) -> Option<Value> { - match name { - "body" => Some(Value::Content(self.0.clone())), - _ => None, - } - } - - fn realize( - &self, - world: Tracked<dyn World>, - styles: StyleChain, - ) -> SourceResult<Content> { - let mut transformed = None; - if styles.get(Self::TYPOGRAPHIC) { - if let Some(text) = search_text(&self.0, S) { - if is_shapable(world, &text, styles) { - transformed = Some(TextNode(text).pack()); - } - } - }; - - Ok(transformed.unwrap_or_else(|| { - let mut map = StyleMap::new(); - map.set(TextNode::BASELINE, styles.get(Self::BASELINE)); - map.set(TextNode::SIZE, styles.get(Self::SIZE)); - self.0.clone().styled_with_map(map) - })) - } -} - -/// Find and transform the text contained in `content` to the given script kind -/// if and only if it only consists of `Text`, `Space`, and `Empty` leaf nodes. -fn search_text(content: &Content, mode: ScriptKind) -> Option<EcoString> { - if content.is_empty() { - Some(EcoString::new()) - } else if content.is::<SpaceNode>() { - Some(' '.into()) - } else if let Some(text) = content.downcast::<TextNode>() { - if let Some(sup) = convert_script(&text.0, mode) { - return Some(sup); - } - None - } else if let Some(seq) = content.downcast::<SequenceNode>() { - let mut full = EcoString::new(); - for item in seq.0.iter() { - match search_text(item, mode) { - Some(text) => full.push_str(&text), - None => return None, - } - } - Some(full) - } else { - None - } -} - -/// Checks whether the first retrievable family contains all code points of the -/// given string. -fn is_shapable(world: Tracked<dyn World>, text: &str, styles: StyleChain) -> bool { - for family in styles.get(TextNode::FAMILY).iter() { - if let Some(font) = world - .book() - .select(family.as_str(), variant(styles)) - .and_then(|id| world.font(id)) - { - return text.chars().all(|c| font.ttf().glyph_index(c).is_some()); - } - } - - false -} - -/// Convert a string to sub- or superscript codepoints if all characters -/// can be mapped to such a codepoint. -fn convert_script(text: &str, mode: ScriptKind) -> Option<EcoString> { - let mut result = EcoString::with_capacity(text.len()); - let converter = match mode { - SUPERSCRIPT => to_superscript_codepoint, - SUBSCRIPT | _ => to_subscript_codepoint, - }; - - for c in text.chars() { - match converter(c) { - Some(c) => result.push(c), - None => return None, - } - } - - Some(result) -} - -/// Convert a character to its corresponding Unicode superscript. -fn to_superscript_codepoint(c: char) -> Option<char> { - char::from_u32(match c { - '0' => 0x2070, - '1' => 0x00B9, - '2' => 0x00B2, - '3' => 0x00B3, - '4' ..= '9' => 0x2070 + (c as u32 + 4 - '4' as u32), - '+' => 0x207A, - '-' => 0x207B, - '=' => 0x207C, - '(' => 0x207D, - ')' => 0x207E, - 'n' => 0x207F, - 'i' => 0x2071, - ' ' => 0x0020, - _ => return None, - }) -} - -/// Convert a character to its corresponding Unicode subscript. -fn to_subscript_codepoint(c: char) -> Option<char> { - char::from_u32(match c { - '0' => 0x2080, - '1' ..= '9' => 0x2080 + (c as u32 - '0' as u32), - '+' => 0x208A, - '-' => 0x208B, - '=' => 0x208C, - '(' => 0x208D, - ')' => 0x208E, - 'a' => 0x2090, - 'e' => 0x2091, - 'o' => 0x2092, - 'x' => 0x2093, - 'h' => 0x2095, - 'k' => 0x2096, - 'l' => 0x2097, - 'm' => 0x2098, - 'n' => 0x2099, - 'p' => 0x209A, - 's' => 0x209B, - 't' => 0x209C, - ' ' => 0x0020, - _ => return None, - }) -} - -/// A category of script. -pub type ScriptKind = usize; - -/// Text that is rendered smaller and raised, also known as superior. -const SUPERSCRIPT: ScriptKind = 0; - -/// Text that is rendered smaller and lowered, also known as inferior. -const SUBSCRIPT: ScriptKind = 1; |
