mod common;
use common::*;
use zenith_core::default_provider;
use zenith_scene::compile;
use zenith_scene::ir::{Paint, SceneCommand};
fn significant(result: &CompileResult) -> Vec<&SceneCommand> {
result
.scene
.commands
.iter()
.filter(|c| {
matches!(
c,
SceneCommand::FillRect { .. } | SceneCommand::DrawGlyphRun { .. }
)
})
.collect()
}
#[test]
fn plain_span_emits_no_fill_rect_and_default_color() {
let src = r##"zenith version=1 {
project id="proj.cl0" name="CL0"
tokens format="zenith-token-v1" {
token id="color.ink" type="color" value="#111827"
token id="font.body" type="fontFamily" value="Noto Sans"
token id="size.body" type="dimension" value=(px)24
}
styles {}
document id="doc.cl0" title="CL0" {
page id="page.cl0" w=(px)400 h=(px)200 {
text id="t.cl0" x=(px)10 y=(px)20 w=(px)380 h=(px)60 fill=(token)"color.ink" font-family=(token)"font.body" font-size=(token)"size.body" {
span "Hello"
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
assert!(
result
.diagnostics
.iter()
.all(|d| d.code != "scene.text_unshaped"),
"no text_unshaped expected; got: {:?}",
result.diagnostics
);
let sig = significant(&result);
let rects: Vec<_> = sig
.iter()
.filter(|c| matches!(c, SceneCommand::FillRect { .. }))
.collect();
assert!(
rects.is_empty(),
"plain span must emit no FillRect; got {} rect(s)",
rects.len()
);
let runs: Vec<_> = sig
.iter()
.filter(|c| matches!(c, SceneCommand::DrawGlyphRun { .. }))
.collect();
assert_eq!(
runs.len(),
1,
"expected exactly one DrawGlyphRun; got {}",
runs.len()
);
}
#[test]
fn code_span_emits_bg_fill_rect_before_glyph_run() {
let src = r##"zenith version=1 {
project id="proj.cl1" name="CL1"
tokens format="zenith-token-v1" {
token id="color.ink" type="color" value="#111827"
token id="font.body" type="fontFamily" value="Noto Sans"
token id="size.body" type="dimension" value=(px)24
}
styles {}
document id="doc.cl1" title="CL1" {
page id="page.cl1" w=(px)600 h=(px)200 {
text id="t.cl1" x=(px)10 y=(px)20 w=(px)580 h=(px)60 fill=(token)"color.ink" font-family=(token)"font.body" font-size=(token)"size.body" {
span "hello" code=#true
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
assert!(
result
.diagnostics
.iter()
.all(|d| d.code != "scene.text_unshaped"),
"no text_unshaped expected; got: {:?}",
result.diagnostics
);
let sig = significant(&result);
assert_eq!(
sig.len(),
2,
"expected 1 FillRect + 1 DrawGlyphRun; got: {:?}",
sig
);
match sig[0] {
SceneCommand::FillRect {
paint: Paint::Solid { color },
w,
h,
..
} => {
assert_eq!(color.r, 240, "code bg rect r must be 240 (F0)");
assert_eq!(color.g, 240, "code bg rect g must be 240 (F0)");
assert_eq!(color.b, 240, "code bg rect b must be 240 (F0)");
assert!(*w > 0.0, "code bg rect width must be > 0; got {w}");
assert!(*h > 0.0, "code bg rect height must be > 0; got {h}");
}
other => panic!("expected FillRect (code bg), got {other:?}"),
}
match sig[1] {
SceneCommand::DrawGlyphRun { font_id, .. } => {
assert!(
font_id.to_lowercase().contains("mono"),
"code span glyph run must use mono font; font_id = '{font_id}'"
);
}
other => panic!("expected DrawGlyphRun after code bg FillRect; got {other:?}"),
}
}
#[test]
fn link_span_renders_with_link_color_and_underline() {
let src = r##"zenith version=1 {
project id="proj.cl2" name="CL2"
tokens format="zenith-token-v1" {
token id="font.body" type="fontFamily" value="Noto Sans"
token id="size.body" type="dimension" value=(px)24
}
styles {}
document id="doc.cl2" title="CL2" {
page id="page.cl2" w=(px)600 h=(px)200 {
text id="t.cl2" x=(px)10 y=(px)20 w=(px)580 h=(px)60 font-family=(token)"font.body" font-size=(token)"size.body" {
span "click here" link="https://example.com"
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
assert!(
result
.diagnostics
.iter()
.all(|d| d.code != "scene.text_unshaped"),
"no text_unshaped expected; got: {:?}",
result.diagnostics
);
let sig = significant(&result);
assert_eq!(
sig.len(),
2,
"expected 1 underline FillRect + 1 DrawGlyphRun; got: {:?}",
sig
);
let glyph_run = sig
.iter()
.find(|c| matches!(c, SceneCommand::DrawGlyphRun { .. }));
match glyph_run {
Some(SceneCommand::DrawGlyphRun { color, .. }) => {
assert_eq!(color.r, 0, "link color.r must be 0");
assert_eq!(color.g, 102, "link color.g must be 102");
assert_eq!(color.b, 204, "link color.b must be 204");
}
_ => panic!("expected DrawGlyphRun with link color"),
}
let text_node = doc
.body
.pages
.first()
.and_then(|p| p.children.first())
.and_then(|n| {
if let zenith_core::Node::Text(t) = n {
Some(t.as_ref())
} else {
None
}
})
.expect("text node must be present");
let span = text_node.spans.first().expect("span must be present");
assert_eq!(
span.link.as_deref(),
Some("https://example.com"),
"link URL must be retained on the parsed TextSpan"
);
}
#[test]
fn link_span_with_explicit_fill_keeps_author_color() {
let src = r##"zenith version=1 {
project id="proj.cl3" name="CL3"
tokens format="zenith-token-v1" {
token id="color.red" type="color" value="#FF0000"
token id="font.body" type="fontFamily" value="Noto Sans"
token id="size.body" type="dimension" value=(px)24
}
styles {}
document id="doc.cl3" title="CL3" {
page id="page.cl3" w=(px)600 h=(px)200 {
text id="t.cl3" x=(px)10 y=(px)20 w=(px)580 h=(px)60 font-family=(token)"font.body" font-size=(token)"size.body" {
span "red link" fill=(token)"color.red" link="https://example.com"
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
assert!(
result
.diagnostics
.iter()
.all(|d| d.code != "scene.text_unshaped"),
"no text_unshaped expected; got: {:?}",
result.diagnostics
);
let sig = significant(&result);
let glyph_run = sig
.iter()
.find(|c| matches!(c, SceneCommand::DrawGlyphRun { .. }));
match glyph_run {
Some(SceneCommand::DrawGlyphRun { color, .. }) => {
assert_eq!(color.r, 255, "explicit fill must override link color (r)");
assert_eq!(color.g, 0, "explicit fill must override link color (g)");
assert_eq!(color.b, 0, "explicit fill must override link color (b)");
}
_ => panic!("expected DrawGlyphRun"),
}
}
#[test]
fn code_and_link_span_combines_both_behaviors() {
let src = r##"zenith version=1 {
project id="proj.cl4" name="CL4"
tokens format="zenith-token-v1" {
token id="font.body" type="fontFamily" value="Noto Sans"
token id="size.body" type="dimension" value=(px)24
}
styles {}
document id="doc.cl4" title="CL4" {
page id="page.cl4" w=(px)600 h=(px)200 {
text id="t.cl4" x=(px)10 y=(px)20 w=(px)580 h=(px)60 font-family=(token)"font.body" font-size=(token)"size.body" {
span "pkg" code=#true link="https://example.com/pkg"
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
assert!(
result
.diagnostics
.iter()
.all(|d| d.code != "scene.text_unshaped"),
"no text_unshaped expected; got: {:?}",
result.diagnostics
);
let sig = significant(&result);
assert_eq!(
sig.len(),
3,
"expected CODE_BG rect + underline rect + DrawGlyphRun; got: {:?}",
sig
);
match sig[0] {
SceneCommand::FillRect {
paint: Paint::Solid { color },
..
} => {
assert_eq!(color.r, 240, "first rect must be CODE_BG (r=240)");
assert_eq!(color.g, 240, "first rect must be CODE_BG (g=240)");
assert_eq!(color.b, 240, "first rect must be CODE_BG (b=240)");
}
other => panic!("expected CODE_BG FillRect first; got {other:?}"),
}
match sig[2] {
SceneCommand::DrawGlyphRun { font_id, color, .. } => {
assert!(
font_id.to_lowercase().contains("mono"),
"code+link span must use mono font; font_id = '{font_id}'"
);
assert_eq!(color.r, 0, "link color.r must be 0");
assert_eq!(color.g, 102, "link color.g must be 102");
assert_eq!(color.b, 204, "link color.b must be 204");
}
other => panic!("expected DrawGlyphRun (mono, link color); got {other:?}"),
}
let text_node = doc
.body
.pages
.first()
.and_then(|p| p.children.first())
.and_then(|n| {
if let zenith_core::Node::Text(t) = n {
Some(t.as_ref())
} else {
None
}
})
.expect("text node must be present");
let span = text_node.spans.first().expect("span must be present");
assert_eq!(
span.link.as_deref(),
Some("https://example.com/pkg"),
"link URL must be retained on the parsed TextSpan"
);
assert_eq!(span.code, Some(true), "code flag must be retained");
}
#[test]
fn link_span_overrides_inherited_node_fill() {
let src = r##"zenith version=1 {
project id="proj.cl5" name="CL5"
tokens format="zenith-token-v1" {
token id="color.ink" type="color" value="#111827"
token id="font.body" type="fontFamily" value="Noto Sans"
token id="size.body" type="dimension" value=(px)24
}
styles {}
document id="doc.cl5" title="CL5" {
page id="page.cl5" w=(px)600 h=(px)200 {
text id="t.cl5" x=(px)10 y=(px)20 w=(px)580 h=(px)60 fill=(token)"color.ink" font-family=(token)"font.body" font-size=(token)"size.body" {
span "click here" link="https://example.com"
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
let sig = significant(&result);
let glyph_run = sig
.iter()
.find(|c| matches!(c, SceneCommand::DrawGlyphRun { .. }));
match glyph_run {
Some(SceneCommand::DrawGlyphRun { color, .. }) => {
assert_eq!(
(color.r, color.g, color.b),
(0, 102, 204),
"link span must use LINK_COLOR even when the node sets a fill"
);
}
_ => panic!("expected DrawGlyphRun with link color"),
}
}