diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-04-02 21:55:25 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-04-03 13:55:58 +0200 |
| commit | 23d108c8e099798dc4d35ce9cbcd3e37fb50f3b2 (patch) | |
| tree | aa068b11b9ac0a4795fb6e86bb8283b1d4718e95 /src/library | |
| parent | beca01c826ee51c9ee6d5eadd7e5ef10f7fb9f58 (diff) | |
Font fallback
Diffstat (limited to 'src/library')
| -rw-r--r-- | src/library/math/mod.rs | 28 | ||||
| -rw-r--r-- | src/library/mod.rs | 3 | ||||
| -rw-r--r-- | src/library/structure/heading.rs | 8 | ||||
| -rw-r--r-- | src/library/text/deco.rs | 10 | ||||
| -rw-r--r-- | src/library/text/mod.rs | 70 | ||||
| -rw-r--r-- | src/library/text/raw.rs | 34 | ||||
| -rw-r--r-- | src/library/text/shaping.rs | 408 |
7 files changed, 274 insertions, 287 deletions
diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs index f20d6543..666e40a7 100644 --- a/src/library/math/mod.rs +++ b/src/library/math/mod.rs @@ -1,6 +1,7 @@ //! Mathematical formulas. use crate::library::prelude::*; +use crate::library::text::FontFamily; /// A mathematical formula. #[derive(Debug, Hash)] @@ -13,6 +14,10 @@ pub struct MathNode { #[node(showable)] impl MathNode { + /// The raw text's font family. Just the normal text family if `none`. + pub const FAMILY: Smart<FontFamily> = + Smart::Custom(FontFamily::new("Latin Modern Math")); + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { Ok(Content::show(Self { formula: args.expect("formula")?, @@ -23,17 +28,24 @@ impl MathNode { impl Show for MathNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Content> { - Ok(styles + let mut content = styles .show(self, ctx, [ Value::Str(self.formula.clone()), Value::Bool(self.display), ])? - .unwrap_or_else(|| { - let mut content = Content::Text(self.formula.trim().into()); - if self.display { - content = Content::Block(content.pack()); - } - content.monospaced() - })) + .unwrap_or_else(|| Content::Text(self.formula.trim().into())); + + let mut map = StyleMap::new(); + if let Smart::Custom(family) = styles.get_cloned(Self::FAMILY) { + map.set_family(family, styles); + } + + content = content.styled_with_map(map); + + if self.display { + content = Content::Block(content.pack()); + } + + Ok(content) } } diff --git a/src/library/mod.rs b/src/library/mod.rs index 528a2ce7..bba002de 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -119,9 +119,6 @@ pub fn new() -> Scope { std.def_const("top", Align::Top); std.def_const("horizon", Align::Horizon); std.def_const("bottom", Align::Bottom); - std.def_const("serif", text::FontFamily::Serif); - std.def_const("sans-serif", text::FontFamily::SansSerif); - std.def_const("monospace", text::FontFamily::Monospace); std } diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index f1bc795f..7d3273f5 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -63,12 +63,7 @@ impl Show for HeadingNode { map.set(TextNode::SIZE, resolve!(Self::SIZE)); if let Smart::Custom(family) = resolve!(Self::FAMILY) { - map.set( - TextNode::FAMILY, - std::iter::once(family) - .chain(styles.get_ref(TextNode::FAMILY).iter().cloned()) - .collect(), - ); + map.set_family(family, styles); } if let Smart::Custom(fill) = resolve!(Self::FILL) { @@ -101,6 +96,7 @@ impl Show for HeadingNode { } let mut content = Content::sequence(seq).styled_with_map(map); + if resolve!(Self::BLOCK) { content = Content::block(content); } diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs index 29c04b2d..b98eb0b2 100644 --- a/src/library/text/deco.rs +++ b/src/library/text/deco.rs @@ -94,10 +94,11 @@ pub fn decorate( width: Length, ) { let face = fonts.get(text.face_id); + let face_metrics = face.metrics(); let metrics = match deco.line { - STRIKETHROUGH => face.strikethrough, - OVERLINE => face.overline, - UNDERLINE | _ => face.underline, + STRIKETHROUGH => face_metrics.strikethrough, + OVERLINE => face_metrics.overline, + UNDERLINE | _ => face_metrics.underline, }; let evade = deco.evade && deco.line != STRIKETHROUGH; @@ -146,7 +147,8 @@ pub fn decorate( for glyph in text.glyphs.iter() { let dx = glyph.x_offset.resolve(text.size) + x; - let mut builder = BezPathBuilder::new(face.units_per_em, text.size, dx.to_raw()); + let mut builder = + BezPathBuilder::new(face_metrics.units_per_em, text.size, dx.to_raw()); let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); let path = builder.finish(); diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index eef7f6fb..2c163a59 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -29,13 +29,7 @@ pub struct TextNode; impl TextNode { /// A prioritized sequence of font families. #[variadic] - pub const FAMILY: Vec<FontFamily> = vec![FontFamily::SansSerif]; - /// The serif font family/families. - pub const SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")]; - /// The sans-serif font family/families. - pub const SANS_SERIF: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")]; - /// The monospace font family/families. - pub const MONOSPACE: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Mono")]; + 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; @@ -100,9 +94,6 @@ impl TextNode { #[skip] #[fold(bool::bitxor)] pub const EMPH: bool = false; - /// Whether a monospace font should be preferred. - #[skip] - pub const MONOSPACED: bool = false; /// The case transformation that should be applied to the next. #[skip] pub const CASE: Option<Case> = None; @@ -160,50 +151,11 @@ impl Show for EmphNode { } } -/// A generic or named font family. -#[derive(Clone, Eq, PartialEq, Hash)] -pub enum FontFamily { - /// A family that has "serifs", small strokes attached to letters. - Serif, - /// A family in which glyphs do not have "serifs", small attached strokes. - SansSerif, - /// A family in which (almost) all glyphs are of equal width. - Monospace, - /// A specific font family like "Arial". - Named(NamedFamily), -} - -impl Debug for FontFamily { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Serif => f.pad("serif"), - Self::SansSerif => f.pad("sans-serif"), - Self::Monospace => f.pad("monospace"), - Self::Named(s) => s.fmt(f), - } - } -} - -dynamic! { - FontFamily: "font family", - Value::Str(string) => Self::Named(NamedFamily::new(&string)), -} - -castable! { - Vec<FontFamily>, - Expected: "string, generic family or array thereof", - Value::Str(string) => vec![FontFamily::Named(NamedFamily::new(&string))], - Value::Array(values) => { - values.into_iter().filter_map(|v| v.cast().ok()).collect() - }, - @family: FontFamily => vec![family.clone()], -} - -/// A specific font family like "Arial". +/// A font family like "Arial". #[derive(Clone, Eq, PartialEq, Hash)] -pub struct NamedFamily(EcoString); +pub struct FontFamily(EcoString); -impl NamedFamily { +impl FontFamily { /// Create a named font family variant. pub fn new(string: &str) -> Self { Self(string.to_lowercase().into()) @@ -215,20 +167,26 @@ impl NamedFamily { } } -impl Debug for NamedFamily { +impl Debug for FontFamily { fn fmt(&self, f: &mut Formatter) -> fmt::Result { self.0.fmt(f) } } castable! { - Vec<NamedFamily>, + FontFamily, + Expected: "string", + Value::Str(string) => Self::new(&string), +} + +castable! { + Vec<FontFamily>, Expected: "string or array of strings", - Value::Str(string) => vec![NamedFamily::new(&string)], + Value::Str(string) => vec![FontFamily::new(&string)], Value::Array(values) => values .into_iter() .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| NamedFamily::new(&string)) + .map(|string: EcoString| FontFamily::new(&string)) .collect(), } diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index e225803f..5c2133c2 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -3,8 +3,8 @@ use syntect::easy::HighlightLines; use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet}; use syntect::parsing::SyntaxSet; +use super::{FontFamily, TextNode}; use crate::library::prelude::*; -use crate::library::text::TextNode; use crate::source::SourceId; use crate::syntax::{self, RedNode}; @@ -26,6 +26,8 @@ pub struct RawNode { #[node(showable)] impl RawNode { + /// The raw text's font family. Just the normal text family if `none`. + pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono")); /// The language to syntax-highlight in. pub const LANG: Option<EcoString> = None; @@ -40,8 +42,14 @@ impl RawNode { impl Show for RawNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Content> { let lang = styles.get_ref(Self::LANG).as_ref(); + let foreground = THEME + .settings + .foreground + .map(Color::from) + .unwrap_or(Color::BLACK) + .into(); - if let Some(content) = styles.show(self, ctx, [ + let mut content = if let Some(content) = styles.show(self, ctx, [ Value::Str(self.text.clone()), match lang { Some(lang) => Value::Str(lang.clone()), @@ -49,17 +57,8 @@ impl Show for RawNode { }, Value::Bool(self.block), ])? { - return Ok(content); - } - - let foreground = THEME - .settings - .foreground - .map(Color::from) - .unwrap_or(Color::BLACK) - .into(); - - let mut content = if matches!( + content + } else if matches!( lang.map(|s| s.to_lowercase()).as_deref(), Some("typ" | "typst") ) { @@ -93,11 +92,18 @@ impl Show for RawNode { Content::Text(self.text.clone()) }; + let mut map = StyleMap::new(); + if let Smart::Custom(family) = styles.get_cloned(Self::FAMILY) { + map.set_family(family, styles); + } + + content = content.styled_with_map(map); + if self.block { content = Content::Block(content.pack()); } - Ok(content.monospaced()) + Ok(content) } } diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs index 29141331..6087032f 100644 --- a/src/library/text/shaping.rs +++ b/src/library/text/shaping.rs @@ -236,6 +236,18 @@ impl<'a> ShapedText<'a> { } } +/// Holds shaping results and metadata common to all shaped segments. +struct ShapingContext<'a> { + fonts: &'a mut FontStore, + glyphs: Vec<ShapedGlyph>, + used: Vec<FaceId>, + styles: StyleChain<'a>, + variant: FontVariant, + tags: Vec<rustybuzz::Feature>, + fallback: bool, + dir: Dir, +} + /// Shape text into [`ShapedText`]. pub fn shape<'a>( fonts: &mut FontStore, @@ -248,28 +260,24 @@ pub fn shape<'a>( None => Cow::Borrowed(text), }; - let mut glyphs = vec![]; + let mut ctx = ShapingContext { + fonts, + glyphs: vec![], + used: vec![], + styles, + variant: variant(styles), + tags: tags(styles), + fallback: styles.get(TextNode::FALLBACK), + dir, + }; + if !text.is_empty() { - shape_segment( - fonts, - &mut glyphs, - 0, - &text, - variant(styles), - families(styles), - None, - dir, - &tags(styles), - ); - } - - track_and_space( - &mut glyphs, - styles.get(TextNode::TRACKING), - styles.get(TextNode::SPACING), - ); + shape_segment(&mut ctx, 0, &text, families(styles)); + } + + track_and_space(&mut ctx); - let (size, baseline) = measure(fonts, &glyphs, styles); + let (size, baseline) = measure(ctx.fonts, &ctx.glyphs, styles); ShapedText { text, @@ -277,186 +285,62 @@ pub fn shape<'a>( styles, size, baseline, - glyphs: Cow::Owned(glyphs), + glyphs: Cow::Owned(ctx.glyphs), } } -/// Resolve the font variant with `STRONG` and `EMPH` factored in. -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::STRONG) { - variant.weight = variant.weight.thicken(300); - } - - if styles.get(TextNode::EMPH) { - 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 { - let head = if styles.get(TextNode::MONOSPACED) { - styles.get_ref(TextNode::MONOSPACE).as_slice() - } else { - &[] - }; - - let core = styles.get_ref(TextNode::FAMILY).iter().flat_map(move |family| { - match family { - FontFamily::Named(name) => std::slice::from_ref(name), - FontFamily::Serif => styles.get_ref(TextNode::SERIF), - FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF), - FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE), - } - }); - - let tail: &[&str] = if styles.get(TextNode::FALLBACK) { - &["ibm plex sans", "latin modern math", "twitter color emoji"] - } else { - &[] - }; - - head.iter() - .chain(core) - .map(|named| named.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), - } - - match styles.get(TextNode::NUMBER_POSITION) { - NumberPosition::Normal => {} - NumberPosition::Subscript => feat(b"subs", 1), - NumberPosition::Superscript => feat(b"sups", 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_ref(TextNode::FEATURES).iter() { - tags.push(Feature::new(tag, value, ..)) - } - - tags -} - /// Shape text with font fallback using the `families` iterator. fn shape_segment<'a>( - fonts: &mut FontStore, - glyphs: &mut Vec<ShapedGlyph>, + ctx: &mut ShapingContext, base: usize, text: &str, - variant: FontVariant, mut families: impl Iterator<Item = &'a str> + Clone, - mut first_face: Option<FaceId>, - dir: Dir, - tags: &[rustybuzz::Feature], ) { - // No font has newlines. - if text.chars().all(|c| c == '\n') { + // Fonts dont have newlines and tabs. + if text.chars().all(|c| c == '\n' || c == '\t') { return; } - // Select the font family. - let (face_id, fallback) = loop { - // Try to load the next available font family. - match families.next() { - Some(family) => { - if let Some(id) = fonts.select(family, variant) { - break (id, true); - } - } - // We're out of families, so we don't do any more fallback and just - // shape the tofus with the first face we originally used. - None => match first_face { - Some(id) => break (id, false), - None => return, - }, + // Find the next available family. + let mut selection = families.find_map(|family| { + ctx.fonts + .select(family, ctx.variant) + .filter(|id| !ctx.used.contains(id)) + }); + + // Do font fallback if the families are exhausted and fallback is enabled. + if selection.is_none() && ctx.fallback { + let first = ctx.used.first().copied(); + selection = ctx + .fonts + .select_fallback(first, ctx.variant, text) + .filter(|id| !ctx.used.contains(id)); + } + + // Extract the face id or shape notdef glyphs if we couldn't find any face. + let face_id = if let Some(id) = selection { + id + } else { + if let Some(&face_id) = ctx.used.first() { + shape_tofus(ctx, base, text, face_id); } + return; }; - // Remember the id if this the first available face since we use that one to - // shape tofus. - first_face.get_or_insert(face_id); + ctx.used.push(face_id); // Fill the buffer with our text. let mut buffer = UnicodeBuffer::new(); buffer.push_str(text); - buffer.set_direction(match dir { + buffer.set_direction(match ctx.dir { Dir::LTR => rustybuzz::Direction::LeftToRight, Dir::RTL => rustybuzz::Direction::RightToLeft, - _ => unimplemented!(), + _ => unimplemented!("vertical text layout"), }); // Shape! - let mut face = fonts.get(face_id); - let buffer = rustybuzz::shape(face.ttf(), tags, buffer); + let mut face = ctx.fonts.get(face_id); + let buffer = rustybuzz::shape(face.ttf(), &ctx.tags, buffer); let infos = buffer.glyph_infos(); let pos = buffer.glyph_positions(); @@ -467,10 +351,10 @@ fn shape_segment<'a>( let info = &infos[i]; let cluster = info.cluster as usize; - if info.glyph_id != 0 || !fallback { + if info.glyph_id != 0 { // Add the glyph to the shaped output. // TODO: Don't ignore y_advance and y_offset. - glyphs.push(ShapedGlyph { + ctx.glyphs.push(ShapedGlyph { face_id, glyph_id: info.glyph_id as u16, x_advance: face.to_em(pos[i].x_advance), @@ -506,7 +390,7 @@ fn shape_segment<'a>( // Glyphs: E C _ _ A // Clusters: 8 6 4 2 0 // k=2 i=3 - let ltr = dir.is_positive(); + 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) }; @@ -517,33 +401,50 @@ fn shape_segment<'a>( 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( - fonts, - glyphs, - base + range.start, - &text[range], - variant, - families.clone(), - first_face, - dir, - tags, - ); - - face = fonts.get(face_id); + shape_segment(ctx, base + range.start, &text[range], families.clone()); + + face = ctx.fonts.get(face_id); } i += 1; } + + ctx.used.pop(); +} + +/// Shape the text with tofus from the given face. +fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, face_id: FaceId) { + let face = ctx.fonts.get(face_id); + let x_advance = face.advance(0).unwrap_or_default(); + for (cluster, c) in text.char_indices() { + ctx.glyphs.push(ShapedGlyph { + face_id, + glyph_id: 0, + x_advance, + x_offset: Em::zero(), + cluster: base + cluster, + safe_to_break: true, + c, + }); + } } /// Apply tracking and spacing to a slice of shaped glyphs. -fn track_and_space(glyphs: &mut [ShapedGlyph], tracking: Em, spacing: Relative) { +fn track_and_space(ctx: &mut ShapingContext) { + let tracking = ctx.styles.get(TextNode::TRACKING); + let spacing = ctx.styles.get(TextNode::SPACING); if tracking.is_zero() && spacing.is_one() { return; } - let mut glyphs = glyphs.iter_mut().peekable(); + let mut glyphs = ctx.glyphs.iter_mut().peekable(); while let Some(glyph) = glyphs.next() { if glyph.is_space() { glyph.x_advance *= spacing.get(); @@ -572,8 +473,9 @@ fn measure( // Expand top and bottom by reading the face's vertical metrics. let mut expand = |face: &Face| { - top.set_max(face.vertical_metric(top_edge, size)); - bottom.set_max(-face.vertical_metric(bottom_edge, size)); + let metrics = face.metrics(); + top.set_max(metrics.vertical(top_edge, size)); + bottom.set_max(-metrics.vertical(bottom_edge, size)); }; if glyphs.is_empty() { @@ -599,3 +501,117 @@ fn measure( (Size::new(width, top + bottom), top) } + +/// Resolve the font variant with `STRONG` and `EMPH` factored in. +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::STRONG) { + variant.weight = variant.weight.thicken(300); + } + + if styles.get(TextNode::EMPH) { + 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_ref(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), + } + + match styles.get(TextNode::NUMBER_POSITION) { + NumberPosition::Normal => {} + NumberPosition::Subscript => feat(b"subs", 1), + NumberPosition::Superscript => feat(b"sups", 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_ref(TextNode::FEATURES).iter() { + tags.push(Feature::new(tag, value, ..)) + } + + tags +} |
