summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/math/accent.rs190
-rw-r--r--library/src/math/mod.rs1
2 files changed, 126 insertions, 65 deletions
diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs
index bf40a332..b8c31c19 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: `´`
+/// - Plus: `` + ``
+/// - Overline: `` - ``, `‾`
+/// - Dot: `.`
/// - Circumflex: `^`
+/// - Acute: `´`
+/// - Low Line: `_`
+/// - Grave: `` ` ``
/// - Tilde: `~`
-/// - Macron: `¯`
-/// - Overline: `‾`
-/// - Breve: `˘`
-/// - Dot: `.`
/// - 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<Content> {
let base = args.expect("base")?;
- let Spanned { v, span } = args.expect::<Spanned<Content>>("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<Result<char, &'static str>> {
- let MathNode { children, .. } = content.to::<MathNode>()?;
- let [child] = children.as_slice() else { return None };
- let c = if let Some(atom) = child.to::<AtomNode>() {
- let mut chars = atom.0.chars();
- chars.next().filter(|_| chars.next().is_none())?
- } else if let Some(symbol) = child.to::<SymbolNode>() {
- 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<char> {
+ let atom = accent.to::<MathNode>()?.body.to::<AtomNode>()?;
+ 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 35e005d0..c7b25db3 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::<MathNode>("math");
+ scope.def_func::<AccentNode>("accent");
scope.def_func::<FracNode>("frac");
scope.def_func::<BinomNode>("binom");
scope.def_func::<ScriptNode>("script");