use std::path::{Path, PathBuf};
use std::process::Command;
use rustyfi_backend::{FontKey, FontMetrics, HorzBox, Length, PureHorzBox};
use rustyfi_lang::value::Value;
use rustyfi_lang::{elaborate, eval, primitives, typecheck, CompileError};
use rustyfi_pdf::{Base14Metrics, TtfFontStore};
fn find_math_font() -> Option<PathBuf> {
let bundled_lmmath = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../lib-rustyfi/dist/fonts/latinmodern-math.otf");
if bundled_lmmath.is_file() {
return Some(bundled_lmmath);
}
let bundled_dejavu = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../lib-rustyfi/dist/fonts/DejaVuMathTeXGyre.ttf");
if bundled_dejavu.is_file() {
return Some(bundled_dejavu);
}
for family in ["DejaVu Math TeX Gyre", "Noto Sans Math"] {
if let Ok(output) = Command::new("fc-match")
.args(["--format=%{file}", family])
.output()
{
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty()
&& Path::new(&path).is_file()
&& (path.contains("Math") || path.contains("math"))
{
return Some(PathBuf::from(path));
}
}
}
}
for candidate in [
"/usr/share/texmf/fonts/opentype/public/dejavu-otf/DejaVuMathTeXGyre.ttf",
"/usr/share/fonts/opentype/dejavu-math-tex-gyre/DejaVuMathTeXGyre.ttf",
"/usr/share/fonts/truetype/tex-gyre/texgyredejavu-math.otf",
"/usr/share/fonts/noto/NotoSansMath-Regular.ttf",
"/usr/share/fonts/truetype/noto/NotoSansMath-Regular.ttf",
"/usr/share/fonts/opentype/noto/NotoSansMath-Regular.ttf",
"/usr/share/fonts/OTF/NotoSansMath-Regular.otf",
"/run/current-system/sw/share/fonts/truetype/NotoSansMath-Regular.ttf",
"/run/current-system/sw/share/X11/fonts/NotoSansMath-Regular.ttf",
] {
if Path::new(candidate).is_file() {
return Some(PathBuf::from(candidate));
}
}
None
}
macro_rules! need_math_font {
() => {
match find_math_font() {
Some(path) => path,
None => {
eprintln!(
"skipping: no math-capable OpenType font found on this system \
(tried `fc-match` for \"DejaVu Math TeX Gyre\"/\"Noto Sans Math\" \
and common nix/distro paths)"
);
return;
}
}
};
}
#[test]
fn math_font_exposes_plausible_math_constants() {
let path = need_math_font!();
let store = TtfFontStore::load(&path, None, None).expect("load math font");
let mc = store
.math_constants(FontKey(0))
.expect("a real MATH font must expose MathConstants");
for (name, v) in [
("axis_height", mc.axis_height),
("superscript_shift_up", mc.superscript_shift_up),
("fraction_rule_thickness", mc.fraction_rule_thickness),
] {
assert!(
v > 0.0 && v < 1.0,
"{name} = {v} should be a nonzero (0,1) ratio of the font size"
);
}
assert!(
mc.script_scale_down > 0.0 && mc.script_scale_down < 1.0,
"script_scale_down = {} should be a nonzero (0,1) ratio",
mc.script_scale_down
);
}
#[test]
fn base14_overrides_none_of_the_math_methods() {
let base14 = Base14Metrics;
assert!(
base14.math_constants(FontKey(0)).is_none(),
"Base14Metrics must inherit the trait's defaulted None (§B1 base-14 \
regression floor)"
);
assert!(base14
.italic_correction(FontKey(0), 'f', Length::pt(12.0))
.is_none());
assert!(base14
.math_kern(
FontKey(0),
'f',
Length::pt(12.0),
rustyfi_backend::MathCorner::TopRight,
Length::ZERO,
)
.is_none());
}
#[test]
fn math_font_has_positive_italic_correction_for_f() {
let path = need_math_font!();
let store = TtfFontStore::load(&path, None, None).expect("load math font");
let ic = store
.italic_correction(FontKey(0), 'f', Length::pt(12.0))
.expect("MATH font should have an italic correction for 'f'");
assert!(ic.0 > 0.0, "expected a positive italic correction, got {ic:?}");
}
#[test]
fn math_kern_does_not_panic_and_degrades_gracefully() {
let path = need_math_font!();
let store = TtfFontStore::load(&path, None, None).expect("load math font");
for corner in [
rustyfi_backend::MathCorner::TopRight,
rustyfi_backend::MathCorner::TopLeft,
rustyfi_backend::MathCorner::BottomRight,
rustyfi_backend::MathCorner::BottomLeft,
] {
let _ = store.math_kern(FontKey(0), 'f', Length::pt(12.0), corner, Length::ZERO);
}
}
fn run_math(src: &str, metrics: &dyn FontMetrics) -> Result<Value, CompileError> {
let file = rustyfi_syntax::parse_file(src)?;
let env = primitives::base_env();
let store = rustyfi_lang::symbol::SymbolStore::new();
let scope = elaborate::Scope::new(&store, env.names());
let program = elaborate::elaborate_program(&file, &scope)?;
typecheck::typecheck(&program)?;
let mut interp = eval::Interp::new(metrics);
Ok(interp.eval(&env, &rustyfi_lang::ast::debrand(&program.body, &store))?)
}
fn with_ctx(body: &str) -> String {
format!(
"let-inline ctx \\dummy m = inline-nil\n\
in\n\
let ctx = get-initial-context 200pt (command \\dummy) in\n\
{body}"
)
}
fn sup_dy_and_extents(v: Value) -> (Length, Length, Length) {
match v {
Value::InlineBoxes(boxes) => {
assert_eq!(boxes.len(), 1, "expected exactly one box, got {boxes:?}");
match boxes.into_iter().next().unwrap() {
HorzBox::Pure(PureHorzBox::Math { glyphs, .. }) => {
assert_eq!(
glyphs.len(),
2,
"expected 2 glyphs (base 'x', script '2'), got {glyphs:?}"
);
assert_eq!(glyphs[1].text, "2");
(glyphs[1].dy, glyphs[0].height, glyphs[1].depth)
}
other => panic!("expected a PureHorzBox::Math, got {other:?}"),
}
}
other => panic!("expected inline-boxes, got {other:?}"),
}
}
fn sup_dy(v: Value) -> Length {
match v {
Value::InlineBoxes(boxes) => {
assert_eq!(boxes.len(), 1, "expected exactly one box, got {boxes:?}");
match boxes.into_iter().next().unwrap() {
HorzBox::Pure(PureHorzBox::Math { glyphs, .. }) => {
assert_eq!(
glyphs.len(),
2,
"expected 2 glyphs (base 'x', script '2'), got {glyphs:?}"
);
assert_eq!(glyphs[1].text, "2");
glyphs[1].dy
}
other => panic!("expected a PureHorzBox::Math, got {other:?}"),
}
}
other => panic!("expected inline-boxes, got {other:?}"),
}
}
#[test]
fn headline_math_font_superscript_shift_differs_from_flat_heuristic() {
let path = need_math_font!();
let store = TtfFontStore::load(&path, None, None).expect("load math font");
let size = Length::pt(12.0);
let base14_src = with_ctx("embed-math ctx ${x^2}");
let v = run_math(&base14_src, &Base14Metrics)
.expect("${x^2} should compile and evaluate under base-14");
let base14_dy = sup_dy(v);
assert_eq!(
base14_dy,
Length::pt(6.0),
"base-14 (no MATH table) should keep the flat 12pt*SUP_SHIFT heuristic"
);
let mc = store
.math_constants(FontKey(0))
.expect("MATH font should expose MathConstants");
let script_size = size * mc.script_scale_down;
let math_src = with_ctx("embed-math ctx ${x^2}");
let v = run_math(&math_src, &store)
.expect("${x^2} should compile and evaluate under a real MATH font");
let (math_dy, h_base, d_sup) = sup_dy_and_extents(v);
let cand1 = size * mc.superscript_shift_up;
let cand2 = h_base - size * mc.superscript_baseline_drop_max;
let cand3 = size * mc.superscript_bottom_min + d_sup;
let expected = cand1.max(cand2).max(cand3);
assert_ne!(
math_dy, base14_dy,
"a real MATH font's clamped superscript shift ({math_dy:?}) should differ \
from the flat 6.0pt heuristic"
);
assert_eq!(
math_dy, expected,
"the wired-up pipeline's shift should equal the independently \
recomputed math.ml:524-533 clamp"
);
assert!(
script_size < size,
"script_scale_down should shrink the script size below font_size"
);
assert_eq!(script_size, size * mc.script_scale_down);
}