From bcf20610fc4d5dcac43bf076c1ea0c9b433de0a0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 22 Jan 2023 13:31:28 +0100 Subject: [PATCH] Math accent handling --- library/src/math/accent.rs | 192 ++++++++++++++++++++++++------------- library/src/math/mod.rs | 1 + 2 files changed, 127 insertions(+), 66 deletions(-) diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs index bf40a332d..b8c31c19b 100644 --- a/library/src/math/accent.rs +++ b/library/src/math/accent.rs @@ -1,12 +1,15 @@ use super::*; +/// How much the accent can be shorter than the base. +const ACCENT_SHORT_FALL: Em = Em::new(0.5); + /// # Accent /// An accented node. /// /// ## Example /// ``` -/// $acc(a, ->) != acc(a, ~)$ \ -/// $acc(a, `) = acc(a, grave)$ +/// $accent(a, ->) != accent(a, ~)$ \ +/// $accent(a, `) = accent(a, grave)$ /// ``` /// /// ## Parameters @@ -16,97 +19,154 @@ use super::*; /// /// ### Example /// ``` -/// $acc(A B C, ->)$ +/// $accent(A B C, ->)$ /// ``` /// /// - accent: Content (positional, required) /// The accent to apply to the base. /// /// Supported accents include: -/// - Grave: `` ` `` -/// - Acute: `´` -/// - Circumflex: `^` -/// - Tilde: `~` -/// - Macron: `¯` -/// - Overline: `‾` -/// - Breve: `˘` +/// - Plus: `` + `` +/// - Overline: `` - ``, `‾` /// - Dot: `.` +/// - Circumflex: `^` +/// - Acute: `´` +/// - Low Line: `_` +/// - Grave: `` ` `` +/// - Tilde: `~` /// - Diaeresis: `¨` +/// - Macron: `¯` +/// - Acute: `´` +/// - Cedilla: `¸` /// - Caron: `ˇ` -/// - Arrow: `→` +/// - Breve: `˘` +/// - Double acute: `˝` +/// - Left arrow: `<-` +/// - Right arrow: `->` /// /// ## Category /// math #[func] -#[capable(Texify)] +#[capable(LayoutMath)] #[derive(Debug, Hash)] -pub struct AccNode { +pub struct AccentNode { /// The accent base. pub base: Content, - /// The Unicode accent character. - pub accent: char, + /// The accent. + pub accent: Content, } #[node] -impl AccNode { +impl AccentNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let base = args.expect("base")?; - let Spanned { v, span } = args.expect::>("accent")?; - let accent = match extract(&v) { - Some(Ok(c)) => c, - Some(Err(msg)) => bail!(span, "{}", msg), - None => bail!(span, "not an accent"), - }; + let accent = args.expect("accent")?; Ok(Self { base, accent }.pack()) } } -#[rustfmt::skip] -fn extract(content: &Content) -> Option> { - let MathNode { children, .. } = content.to::()?; - let [child] = children.as_slice() else { return None }; - let c = if let Some(atom) = child.to::() { - let mut chars = atom.0.chars(); - chars.next().filter(|_| chars.next().is_none())? - } else if let Some(symbol) = child.to::() { - match symmie::get(&symbol.0) { - Some(c) => c, - None => return Some(Err("unknown symbol")), - } - } else { - return None; - }; +impl LayoutMath for AccentNode { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + ctx.style(ctx.style.with_cramped(true)); + let base = ctx.layout_fragment(&self.base)?; + ctx.unstyle(); - Some(Ok(match c { - '`' | '\u{300}' => '\u{300}', // Grave - '´' | '\u{301}' => '\u{301}', // Acute - '^' | '\u{302}' => '\u{302}', // Circumflex - '~' | '\u{223C}' | '\u{303}' => '\u{303}', // Tilde - '¯' | '\u{304}' => '\u{304}', // Macron - '‾' | '\u{305}' => '\u{305}', // Overline - '˘' | '\u{306}' => '\u{306}', // Breve - '.' | '\u{22C5}' | '\u{307}' => '\u{307}', // Dot - '¨' | '\u{308}' => '\u{308}', // Diaeresis - 'ˇ' | '\u{30C}' => '\u{30C}', // Caron - '→' | '\u{20D7}' => '\u{20D7}', // Arrow - _ => return None, - })) -} + let base_attach = match base { + MathFragment::Glyph(base) => { + attachment(ctx, base.id, base.italics_correction) + } + _ => (base.width() + base.italics_correction()) / 2.0, + }; + + let Some(c) = extract(&self.accent) else { + ctx.push(base); + if let Some(span) = self.accent.span() { + bail!(span, "not an accent"); + } + return Ok(()); + }; + + // Forcing the accent to be at least as large as the base makes it too + // wide in many case. + let glyph = GlyphFragment::new(ctx, c); + let short_fall = ACCENT_SHORT_FALL.scaled(ctx); + let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall); + let accent = variant.frame; + let accent_attach = match variant.id { + Some(id) => attachment(ctx, id, variant.italics_correction), + None => accent.width() / 2.0, + }; + + // Descent is negative because the accent's ink bottom is above the + // baseline. Therefore, the default gap is the accent's negated descent + // minus the accent base height. Only if the base is very small, we need + // a larger gap so that the accent doesn't move too low. + let accent_base_height = scaled!(ctx, accent_base_height); + let gap = -accent.descent() - base.height().min(accent_base_height); + let size = Size::new(base.width(), accent.height() + gap + base.height()); + let accent_pos = Point::with_x(base_attach - accent_attach); + let base_pos = Point::with_y(accent.height() + gap); + let baseline = base_pos.y + base.ascent(); + + let mut frame = Frame::new(size); + frame.set_baseline(baseline); + frame.push_frame(accent_pos, accent); + frame.push_frame(base_pos, base.to_frame(ctx)); + ctx.push(frame); -impl Texify for AccNode { - fn texify(&self, t: &mut Texifier) -> SourceResult<()> { - if let Some(sym) = unicode_math::SYMBOLS.iter().find(|sym| { - sym.codepoint == self.accent - && sym.atom_type == unicode_math::AtomType::Accent - }) { - t.push_str("\\"); - t.push_str(sym.name); - t.push_str("{"); - self.base.texify(t)?; - t.push_str("}"); - } else { - self.base.texify(t)?; - } Ok(()) } } + +/// The horizontal attachment position for the given glyph. +fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs { + ctx.table + .glyph_info + .and_then(|info| info.top_accent_attachments) + .and_then(|attachments| attachments.get(id)) + .map(|record| record.value.scaled(ctx)) + .unwrap_or_else(|| { + let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); + (advance.scaled(ctx) + italics_correction) / 2.0 + }) +} + +/// Extract a single character from content. +fn extract(accent: &Content) -> Option { + let atom = accent.to::()?.body.to::()?; + let mut chars = atom.0.chars(); + let c = chars.next().filter(|_| chars.next().is_none())?; + Some(combining(c)) +} + +/// Convert from a non-combining accent to a combining one. +/// +/// https://www.w3.org/TR/mathml-core/#combining-character-equivalences +fn combining(c: char) -> char { + match c { + '\u{002b}' => '\u{031f}', + '\u{002d}' => '\u{0305}', + '\u{002e}' => '\u{0307}', + '\u{005e}' => '\u{0302}', + '\u{005f}' => '\u{0332}', + '\u{0060}' => '\u{0300}', + '\u{007e}' => '\u{0303}', + '\u{00a8}' => '\u{0308}', + '\u{00af}' => '\u{0304}', + '\u{00b4}' => '\u{0301}', + '\u{00b8}' => '\u{0327}', + '\u{02c6}' => '\u{0302}', + '\u{02c7}' => '\u{030c}', + '\u{02d8}' => '\u{0306}', + '\u{02d9}' => '\u{0307}', + '\u{02db}' => '\u{0328}', + '\u{02dc}' => '\u{0303}', + '\u{02dd}' => '\u{030b}', + '\u{203e}' => '\u{0305}', + '\u{2190}' => '\u{20d6}', + '\u{2192}' => '\u{20d7}', + '\u{2212}' => '\u{0305}', + '\u{27f6}' => '\u{20d7}', + _ => c, + } +} diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 35e005d09..c7b25db3a 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -47,6 +47,7 @@ use crate::text::{families, variant, FallbackList, FontFamily, SpaceNode, Symbol /// Hook up all math definitions. pub fn define(scope: &mut Scope) { scope.def_func::("math"); + scope.def_func::("accent"); scope.def_func::("frac"); scope.def_func::("binom"); scope.def_func::("script");