use rustyfi_backend::{
default_script_space_map, Context, FontKey, FontMetrics, HorzBox, Length, PureHorzBox, Script,
};
use rustyfi_lang::ast::Ast;
use rustyfi_lang::eval::Interp;
use rustyfi_lang::primitives;
use rustyfi_lang::quoted::IText;
use rustyfi_lang::value::{Env, Value};
use rustyfi_syntax::Span;
struct Mono;
impl FontMetrics for Mono {
fn advance(&self, _f: FontKey, _c: char, size: Length) -> Option<Length> {
Some(size * 0.5)
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size * 0.75
}
fn descender(&self, _f: FontKey, size: Length) -> Length {
size * 0.25
}
}
fn boxes_for(ctx: &Context, text: &str) -> Vec<HorzBox> {
let mono = Mono;
let mut interp = Interp::new(&mono);
let elems = vec![IText::Text(text.to_string())];
primitives::read_inline(&mut interp, ctx, &elems, &Env::root()).expect("read_inline")
}
fn ctx_with_split_ratios() -> Context {
let mut ctx = Context::initial(Length::pt(200.0));
ctx.font_scheme[Script::HanIdeographic as usize].ratio = 0.88;
ctx.font_scheme[Script::Kana as usize].ratio = 0.5;
assert_eq!(
ctx.font_size,
Length::pt(12.0),
"the arithmetic below assumes it"
);
ctx
}
fn glues(boxes: &[HorzBox]) -> Vec<PureHorzBox> {
boxes
.iter()
.filter_map(|HorzBox::Pure(p)| match p {
PureHorzBox::OuterEmpty { .. } => Some(p.clone()),
_ => None,
})
.collect()
}
fn no_break_boxes(boxes: &[HorzBox]) -> Vec<PureHorzBox> {
boxes
.iter()
.flat_map(|HorzBox::Pure(p)| match p {
PureHorzBox::Discretionary { no_break, .. } => no_break.clone(),
_ => Vec::new(),
})
.collect()
}
fn assert_pt(actual: Length, expected: f64, what: &str) {
assert!(
(actual.0 - expected).abs() < 1e-9,
"{what}: expected {expected}pt, got {}pt",
actual.0
);
}
#[test]
fn interscript_space_is_rigid() {
let ctx = Context::initial(Length::pt(200.0));
let boxes = boxes_for(&ctx, "Aあ");
let gs = glues(&boxes);
assert_eq!(gs.len(), 1, "exactly one inter-script glue: {gs:?}");
match &gs[0] {
PureHorzBox::OuterEmpty {
natural,
shrinkable,
stretchable,
} => {
assert_pt(*natural, 0.24 * 12.0, "inter-script natural");
assert_eq!(
*shrinkable,
Length::ZERO,
"`natural (size *% r0)` has zero shrink; r1 is a HEIGHT upstream"
);
assert_eq!(
*stretchable,
Length::ZERO,
"…and zero stretch; r2 is a DEPTH upstream"
);
}
other => panic!("expected glue, got {other:?}"),
}
}
#[test]
fn interscript_space_is_rigid_cjk_to_latin_and_still_breakable() {
let ctx = Context::initial(Length::pt(200.0));
let boxes = boxes_for(&ctx, "あA");
let gs = glues(&boxes);
assert_eq!(gs.len(), 1, "exactly one inter-script glue: {gs:?}");
assert!(
gs[0].is_break_point(),
"a rigid glue is still a break opportunity"
);
match &gs[0] {
PureHorzBox::OuterEmpty {
natural,
shrinkable,
stretchable,
} => {
assert_pt(*natural, 0.24 * 12.0, "inter-script natural");
assert_eq!(*shrinkable, Length::ZERO);
assert_eq!(*stretchable, Length::ZERO);
}
other => panic!("expected glue, got {other:?}"),
}
}
#[test]
fn interscript_space_honours_the_script_space_map() {
let mut ctx = Context::initial(Length::pt(200.0));
ctx.script_space_map[Script::Latin as usize][Script::Kana as usize] = 0.0;
let boxes = boxes_for(&ctx, "Aあ");
let gs = glues(&boxes);
assert_eq!(gs.len(), 1, "the box survives at ratio 0: {gs:?}");
assert!(gs[0].is_break_point(), "…and is still a break opportunity");
match &gs[0] {
PureHorzBox::OuterEmpty { natural, .. } => {
assert_eq!(*natural, Length::ZERO, "ratio 0 means no space")
}
other => panic!("expected glue, got {other:?}"),
}
}
#[test]
fn script_space_map_is_directional() {
let mut ctx = Context::initial(Length::pt(200.0));
ctx.script_space_map[Script::Latin as usize][Script::Kana as usize] = 0.0;
match &glues(&boxes_for(&ctx, "あA"))[0] {
PureHorzBox::OuterEmpty { natural, .. } => {
assert_pt(*natural, 0.24 * 12.0, "the reverse direction is untouched")
}
other => panic!("expected glue, got {other:?}"),
}
}
#[test]
fn set_space_ratio_between_scripts_writes_the_map() {
fn script(name: &str) -> Ast {
Ast::Ctor(name.to_string(), None)
}
fn apply(f: Ast, a: Ast) -> Ast {
Ast::Apply(Box::new(f), Box::new(a))
}
let mut ast = Ast::Var(
"set-space-ratio-between-scripts".to_string(),
Span::default(),
);
for arg in [
Ast::Float(0.0),
Ast::Float(0.0),
Ast::Float(0.0),
script("Latin"),
script("Kana"),
apply(
apply(
Ast::Var("get-initial-context".to_string(), Span::default()),
Ast::Length(Length::pt(100.0)),
),
Ast::Unit,
),
] {
ast = apply(ast, arg);
}
let env = primitives::base_env();
let mono = Mono;
let mut interp = Interp::new(&mono);
match interp.eval(&env, &ast).expect("evaluation should succeed") {
Value::Context(ctx) => {
assert_eq!(
ctx.script_space_map[Script::Latin as usize][Script::Kana as usize],
0.0,
"the call must reach the map"
);
assert_eq!(
ctx.script_space_map[Script::Kana as usize][Script::Latin as usize],
0.24,
"and must not touch the other three directions"
);
}
other => panic!("expected a context, got {other:?}"),
}
}
#[test]
fn default_script_space_map_is_the_four_latin_cjk_directions() {
let m = default_script_space_map();
for (l, r) in [
(Script::Latin, Script::Kana),
(Script::Kana, Script::Latin),
(Script::Latin, Script::HanIdeographic),
(Script::HanIdeographic, Script::Latin),
] {
assert_eq!(m[l as usize][r as usize], 0.24, "{l:?} -> {r:?}");
}
let mut set = 0;
for row in &m {
for v in row {
if *v != 0.0 {
set += 1;
}
}
}
assert_eq!(set, 4, "exactly four entries: {m:?}");
}
#[test]
fn class_space_elasticity_uses_the_left_chars_corrected_size() {
let ctx = ctx_with_split_ratios();
let nb = no_break_boxes(&boxes_for(&ctx, "、あ"));
let glue = nb
.iter()
.find_map(|b| match b {
PureHorzBox::OuterEmpty {
shrinkable,
stretchable,
..
} => Some((*shrinkable, *stretchable)),
_ => None,
})
.unwrap_or_else(|| panic!("no class-space glue in {nb:?}"));
assert_pt(glue.0, 0.25 * 10.56, "hwsoft shrink");
assert_pt(glue.1, 0.25 * 10.56, "hwsoft stretch");
}
#[test]
fn nakaten_kern_natural_width_uses_corrected_size() {
let ctx = ctx_with_split_ratios();
let nb = no_break_boxes(&boxes_for(&ctx, "・あ"));
let kern = nb
.iter()
.find_map(|b| match b {
PureHorzBox::FixedEmpty { width } => Some(*width),
_ => None,
})
.unwrap_or_else(|| panic!("no kern in {nb:?}"));
assert_pt(kern, -0.25 * 6.0, "`・`'s quarterwidth_kern");
}
#[test]
fn class_space_before_an_open_bracket_uses_the_right_chars_corrected_size() {
let ctx = ctx_with_split_ratios();
let nb = no_break_boxes(&boxes_for(&ctx, "あ("));
let glue = nb
.iter()
.find_map(|b| match b {
PureHorzBox::OuterEmpty {
shrinkable,
stretchable,
..
} => Some((*shrinkable, *stretchable)),
_ => None,
})
.unwrap_or_else(|| panic!("no class-space glue in {nb:?}"));
assert_pt(glue.0, 0.25 * 10.56, "hwsoft2 shrink");
assert_pt(glue.1, 0.25 * 10.56, "hwsoft2 stretch");
}
#[test]
fn adjacent_space_still_uses_the_raw_font_size() {
let ctx = ctx_with_split_ratios();
let nb = no_break_boxes(&boxes_for(&ctx, "あい"));
let glue = nb
.iter()
.find_map(|b| match b {
PureHorzBox::OuterEmpty {
natural,
shrinkable,
stretchable,
} => Some((*natural, *shrinkable, *stretchable)),
_ => None,
})
.unwrap_or_else(|| panic!("no adjacent_space glue in {nb:?}"));
assert_eq!(glue.0, Length::ZERO, "adjacent_space has no natural width");
assert_eq!(glue.1, Length::ZERO, "…and no shrink");
assert_pt(glue.2, 12.0 * 0.025, "adjacent_stretch off the RAW size");
}