mod common;
use common::*;
use zenith_core::default_provider;
use zenith_scene::compile;
use zenith_scene::ir::SceneCommand;
fn min_glyph_y(v_align_attr: Option<&str>) -> f64 {
let v_attr = v_align_attr.map_or(String::new(), |v| format!(" v-align=\"{v}\""));
let src = format!(
r##"zenith version=1 {{
project id="proj.va" name="VA"
tokens format="zenith-token-v1" {{}}
styles {{}}
document id="doc.va" title="VA" {{
page id="page.va" w=(px)800 h=(px)600 {{
text id="t1" x=(px)100 y=(px)50 w=(px)400 h=(px)300 font-size=(px)20{v_attr} {{
span "Hello"
}}
}}
}}
}}
"##
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
result
.scene
.commands
.iter()
.filter_map(|c| {
if let SceneCommand::DrawGlyphRun { y, .. } = c {
Some(*y)
} else {
None
}
})
.fold(f64::INFINITY, f64::min)
}
#[test]
fn text_v_align_absent_is_top() {
let y_absent = min_glyph_y(None);
let y_top = min_glyph_y(Some("top"));
assert!(
(y_absent - y_top).abs() < 1.0,
"absent v-align must match top: absent={y_absent}, top={y_top}"
);
}
#[test]
fn text_v_align_top_sits_near_box_top() {
let y = min_glyph_y(Some("top"));
assert!(
y < 200.0,
"v-align=top baseline must be in the upper half of the box (< 200); got {y}"
);
}
#[test]
fn text_v_align_middle_is_below_top() {
let y_top = min_glyph_y(Some("top"));
let y_mid = min_glyph_y(Some("middle"));
assert!(
y_mid > y_top + 1.0,
"v-align=middle baseline must be strictly below top: top={y_top}, middle={y_mid}"
);
}
#[test]
fn text_v_align_bottom_is_below_middle() {
let y_mid = min_glyph_y(Some("middle"));
let y_bot = min_glyph_y(Some("bottom"));
assert!(
y_bot > y_mid + 1.0,
"v-align=bottom baseline must be below middle: middle={y_mid}, bottom={y_bot}"
);
}
#[test]
fn text_v_align_bottom_sits_near_box_bottom() {
let y = min_glyph_y(Some("bottom"));
assert!(
y > 200.0,
"v-align=bottom baseline must be in the lower half of the box (> 200); got {y}"
);
}
#[test]
fn text_v_align_middle_is_halfway_to_bottom() {
let y_top = min_glyph_y(Some("top"));
let y_mid = min_glyph_y(Some("middle"));
let y_bot = min_glyph_y(Some("bottom"));
let offset_mid = y_mid - y_top;
let offset_bot = y_bot - y_top;
let expected_mid = offset_bot / 2.0;
assert!(
(offset_mid - expected_mid).abs() < 1.5,
"middle offset ({offset_mid}) must be ≈ bottom_offset/2 ({expected_mid})"
);
}
#[test]
fn text_v_align_absent_byte_identical_to_top() {
fn glyph_ys(v_align: Option<&str>) -> Vec<f64> {
let v_attr = v_align.map_or(String::new(), |v| format!(" v-align=\"{v}\""));
let src = format!(
r##"zenith version=1 {{
project id="proj.va2" name="VA2"
tokens format="zenith-token-v1" {{}}
styles {{}}
document id="doc.va2" title="VA2" {{
page id="page.va2" w=(px)800 h=(px)600 {{
text id="t2" x=(px)100 y=(px)50 w=(px)400 h=(px)300 font-size=(px)20{v_attr} {{
span "Byte identical check"
}}
}}
}}
}}
"##
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
result
.scene
.commands
.iter()
.filter_map(|c| {
if let SceneCommand::DrawGlyphRun { y, .. } = c {
Some(*y)
} else {
None
}
})
.collect()
}
let ys_absent = glyph_ys(None);
let ys_top = glyph_ys(Some("top"));
assert_eq!(
ys_absent, ys_top,
"absent v-align must produce identical glyph-y positions as v-align=top"
);
}
#[test]
fn text_v_align_round_trips() {
use zenith_core::format::format_document;
use zenith_core::{KdlAdapter, KdlSource, Node};
let src = r##"zenith version=1 {
project id="proj.vart" name="VART"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.vart" title="VART" {
page id="p1" w=(px)400 h=(px)400 {
text id="t1" x=(px)10 y=(px)10 w=(px)200 h=(px)300 v-align="middle" {
span "Round-trip"
}
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse");
let out = format_document(&doc).expect("format");
let text = String::from_utf8(out).unwrap();
let text_line = text
.lines()
.find(|l| l.trim_start().starts_with("text"))
.expect("must find text line");
assert!(
text_line.contains("v-align=\"middle\""),
"formatted text line must contain v-align=\"middle\"; got: {text_line:?}"
);
let doc2 = adapter.parse(text.as_bytes()).expect("re-parse");
match &doc2.body.pages[0].children[0] {
Node::Text(t) => assert_eq!(
t.v_align.as_deref(),
Some("middle"),
"v_align must survive the format round-trip"
),
other => panic!("expected Text, got {other:?}"),
}
}
#[test]
fn text_v_align_absent_not_emitted() {
use zenith_core::format::format_document;
use zenith_core::{KdlAdapter, KdlSource};
let src = r##"zenith version=1 {
project id="proj.vane" name="VANE"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.vane" title="VANE" {
page id="p1" w=(px)400 h=(px)400 {
text id="t1" x=(px)10 y=(px)10 w=(px)200 h=(px)200 {
span "No v-align here"
}
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse");
let out = format_document(&doc).expect("format");
let text = String::from_utf8(out).unwrap();
let text_line = text
.lines()
.find(|l| l.trim_start().starts_with("text"))
.expect("must find text line");
assert!(
!text_line.contains("v-align"),
"text without v-align must NOT emit v-align attribute; got: {text_line:?}"
);
}