mirror of
https://github.com/typst/typst
synced 2025-06-28 08:12:53 +08:00
Math accent handling
This commit is contained in:
parent
2c48c8d7a1
commit
bcf20610fc
@ -1,12 +1,15 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// How much the accent can be shorter than the base.
|
||||||
|
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
|
||||||
|
|
||||||
/// # Accent
|
/// # Accent
|
||||||
/// An accented node.
|
/// An accented node.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
/// ```
|
/// ```
|
||||||
/// $acc(a, ->) != acc(a, ~)$ \
|
/// $accent(a, ->) != accent(a, ~)$ \
|
||||||
/// $acc(a, `) = acc(a, grave)$
|
/// $accent(a, `) = accent(a, grave)$
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Parameters
|
/// ## Parameters
|
||||||
@ -16,97 +19,154 @@ use super::*;
|
|||||||
///
|
///
|
||||||
/// ### Example
|
/// ### Example
|
||||||
/// ```
|
/// ```
|
||||||
/// $acc(A B C, ->)$
|
/// $accent(A B C, ->)$
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// - accent: Content (positional, required)
|
/// - accent: Content (positional, required)
|
||||||
/// The accent to apply to the base.
|
/// The accent to apply to the base.
|
||||||
///
|
///
|
||||||
/// Supported accents include:
|
/// Supported accents include:
|
||||||
/// - Grave: `` ` ``
|
/// - Plus: `` + ``
|
||||||
/// - Acute: `´`
|
/// - Overline: `` - ``, `‾`
|
||||||
/// - Circumflex: `^`
|
|
||||||
/// - Tilde: `~`
|
|
||||||
/// - Macron: `¯`
|
|
||||||
/// - Overline: `‾`
|
|
||||||
/// - Breve: `˘`
|
|
||||||
/// - Dot: `.`
|
/// - Dot: `.`
|
||||||
|
/// - Circumflex: `^`
|
||||||
|
/// - Acute: `´`
|
||||||
|
/// - Low Line: `_`
|
||||||
|
/// - Grave: `` ` ``
|
||||||
|
/// - Tilde: `~`
|
||||||
/// - Diaeresis: `¨`
|
/// - Diaeresis: `¨`
|
||||||
|
/// - Macron: `¯`
|
||||||
|
/// - Acute: `´`
|
||||||
|
/// - Cedilla: `¸`
|
||||||
/// - Caron: `ˇ`
|
/// - Caron: `ˇ`
|
||||||
/// - Arrow: `→`
|
/// - Breve: `˘`
|
||||||
|
/// - Double acute: `˝`
|
||||||
|
/// - Left arrow: `<-`
|
||||||
|
/// - Right arrow: `->`
|
||||||
///
|
///
|
||||||
/// ## Category
|
/// ## Category
|
||||||
/// math
|
/// math
|
||||||
#[func]
|
#[func]
|
||||||
#[capable(Texify)]
|
#[capable(LayoutMath)]
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
pub struct AccNode {
|
pub struct AccentNode {
|
||||||
/// The accent base.
|
/// The accent base.
|
||||||
pub base: Content,
|
pub base: Content,
|
||||||
/// The Unicode accent character.
|
/// The accent.
|
||||||
pub accent: char,
|
pub accent: Content,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node]
|
#[node]
|
||||||
impl AccNode {
|
impl AccentNode {
|
||||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
||||||
let base = args.expect("base")?;
|
let base = args.expect("base")?;
|
||||||
let Spanned { v, span } = args.expect::<Spanned<Content>>("accent")?;
|
let accent = args.expect("accent")?;
|
||||||
let accent = match extract(&v) {
|
|
||||||
Some(Ok(c)) => c,
|
|
||||||
Some(Err(msg)) => bail!(span, "{}", msg),
|
|
||||||
None => bail!(span, "not an accent"),
|
|
||||||
};
|
|
||||||
Ok(Self { base, accent }.pack())
|
Ok(Self { base, accent }.pack())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
impl LayoutMath for AccentNode {
|
||||||
fn extract(content: &Content) -> Option<Result<char, &'static str>> {
|
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||||
let MathNode { children, .. } = content.to::<MathNode>()?;
|
ctx.style(ctx.style.with_cramped(true));
|
||||||
let [child] = children.as_slice() else { return None };
|
let base = ctx.layout_fragment(&self.base)?;
|
||||||
let c = if let Some(atom) = child.to::<AtomNode>() {
|
ctx.unstyle();
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Ok(match c {
|
let base_attach = match base {
|
||||||
'`' | '\u{300}' => '\u{300}', // Grave
|
MathFragment::Glyph(base) => {
|
||||||
'´' | '\u{301}' => '\u{301}', // Acute
|
attachment(ctx, base.id, base.italics_correction)
|
||||||
'^' | '\u{302}' => '\u{302}', // Circumflex
|
}
|
||||||
'~' | '\u{223C}' | '\u{303}' => '\u{303}', // Tilde
|
_ => (base.width() + base.italics_correction()) / 2.0,
|
||||||
'¯' | '\u{304}' => '\u{304}', // Macron
|
};
|
||||||
'‾' | '\u{305}' => '\u{305}', // Overline
|
|
||||||
'˘' | '\u{306}' => '\u{306}', // Breve
|
let Some(c) = extract(&self.accent) else {
|
||||||
'.' | '\u{22C5}' | '\u{307}' => '\u{307}', // Dot
|
ctx.push(base);
|
||||||
'¨' | '\u{308}' => '\u{308}', // Diaeresis
|
if let Some(span) = self.accent.span() {
|
||||||
'ˇ' | '\u{30C}' => '\u{30C}', // Caron
|
bail!(span, "not an accent");
|
||||||
'→' | '\u{20D7}' => '\u{20D7}', // Arrow
|
}
|
||||||
_ => return None,
|
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(())
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -47,6 +47,7 @@ use crate::text::{families, variant, FallbackList, FontFamily, SpaceNode, Symbol
|
|||||||
/// Hook up all math definitions.
|
/// Hook up all math definitions.
|
||||||
pub fn define(scope: &mut Scope) {
|
pub fn define(scope: &mut Scope) {
|
||||||
scope.def_func::<MathNode>("math");
|
scope.def_func::<MathNode>("math");
|
||||||
|
scope.def_func::<AccentNode>("accent");
|
||||||
scope.def_func::<FracNode>("frac");
|
scope.def_func::<FracNode>("frac");
|
||||||
scope.def_func::<BinomNode>("binom");
|
scope.def_func::<BinomNode>("binom");
|
||||||
scope.def_func::<ScriptNode>("script");
|
scope.def_func::<ScriptNode>("script");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user