mod common;
use common::*;
use zenith_core::format::format_document;
#[test]
fn test_image_clip_parse_format_round_trip() {
let src = r##"zenith version=1 {
project id="proj.iclip" name="IClip"
assets {
asset id="asset.pfp" kind="image" src="assets/pfp.png"
}
tokens format="zenith-token-v1" {
token id="size.radius.avatar" type="dimension" value=(px)24
}
styles {
}
document id="doc.iclip" title="IClip" {
page id="page.iclip" w=(px)400 h=(px)300 {
image id="av" asset="asset.pfp" x=(px)0 y=(px)0 w=(px)100 h=(px)100 fit="cover" clip="rounded" clip-radius=(token)"size.radius.avatar"
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
let image_node = match &doc.body.pages[0].children[0] {
Node::Image(i) => i,
other => panic!("expected Image node, got {other:?}"),
};
assert_eq!(image_node.clip.as_deref(), Some("rounded"));
use zenith_core::PropertyValue;
assert_eq!(
image_node.clip_radius,
Some(PropertyValue::TokenRef("size.radius.avatar".to_owned())),
"clip-radius must parse as a token ref"
);
let formatted = format_document(&doc).expect("format must succeed");
let formatted_str = String::from_utf8(formatted).expect("formatted must be utf8");
assert!(
formatted_str.contains("clip=\"rounded\""),
"formatter must emit clip=\"rounded\"; got:\n{formatted_str}"
);
assert!(
formatted_str.contains("clip-radius=(token)\"size.radius.avatar\""),
"formatter must emit clip-radius token; got:\n{formatted_str}"
);
let doc2 = adapter
.parse(formatted_str.as_bytes())
.expect("re-parse after format");
let image2 = match &doc2.body.pages[0].children[0] {
Node::Image(i) => i,
other => panic!("expected Image node on re-parse, got {other:?}"),
};
assert_eq!(image2.clip.as_deref(), Some("rounded"));
assert_eq!(
image2.clip_radius,
Some(PropertyValue::TokenRef("size.radius.avatar".to_owned())),
"clip-radius must survive a format → re-parse round-trip"
);
}
#[test]
fn light_node_parse_format_round_trip() {
let src = r##"zenith version=1 {
project id="proj.light" name="Light"
tokens format="zenith-token-v1" {
token id="color.glow" type="color" value="#7cc7ff"
token id="size.glow" type="dimension" value=(px)420
}
styles {
}
document id="doc.light" title="Light" {
page id="page.light" w=(px)1080 h=(px)1080 {
light id="bg.glow" kind="ambient" x=(%)85 y=(%)12 radius=(token)"size.glow" color=(token)"color.glow" opacity=0.35
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
let light = match &doc.body.pages[0].children[0] {
Node::Light(l) => l,
other => panic!("expected light node, got {other:?}"),
};
assert_eq!(light.id, "bg.glow");
assert_eq!(light.kind.as_deref(), Some("ambient"));
assert_eq!(light.opacity, Some(0.35));
let formatted = format_document(&doc).expect("format must succeed");
let formatted_str = String::from_utf8(formatted.clone()).expect("formatted must be utf8");
assert!(
formatted_str.contains(
"light id=\"bg.glow\" kind=\"ambient\" x=(%)85 y=(%)12 radius=(token)\"size.glow\" color=(token)\"color.glow\" opacity=0.35"
),
"formatted light line missing canonical attrs; got:\n{formatted_str}"
);
let reparsed = adapter
.parse(&formatted)
.expect("re-parse after format must succeed");
assert!(
matches!(&reparsed.body.pages[0].children[0], Node::Light(l) if l.id == "bg.glow"),
"light must survive format round-trip"
);
}
#[test]
fn mesh_node_parse_format_round_trip() {
let src = r##"zenith version=1 {
project id="proj.mesh" name="Mesh"
tokens format="zenith-token-v1" {
token id="color.grid" type="color" value="#203040"
token id="stroke.hairline" type="dimension" value=(px)1
}
styles {
}
document id="doc.mesh" title="Mesh" {
page id="page.mesh" w=(px)1920 h=(px)1080 {
mesh id="bg.mesh" kind="perspective" x=(px)0 y=(px)0 w=(px)1920 h=(px)1080 rows=7 columns=8 vanishing-x=(px)1260 vanishing-y=(px)-420 extend=(px)160 stroke=(token)"color.grid" stroke-width=(token)"stroke.hairline" opacity=0.34
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
let mesh = match &doc.body.pages[0].children[0] {
Node::Mesh(m) => m,
other => panic!("expected mesh node, got {other:?}"),
};
assert_eq!(mesh.id, "bg.mesh");
assert_eq!(mesh.kind.as_deref(), Some("perspective"));
assert_eq!(mesh.rows, Some(7));
assert_eq!(mesh.columns, Some(8));
let formatted = format_document(&doc).expect("format must succeed");
let formatted_str = String::from_utf8(formatted.clone()).expect("formatted must be utf8");
assert!(
formatted_str.contains(
"mesh id=\"bg.mesh\" kind=\"perspective\" x=(px)0 y=(px)0 w=(px)1920 h=(px)1080 rows=7 columns=8 vanishing-x=(px)1260 vanishing-y=(px)-420 extend=(px)160 stroke=(token)\"color.grid\" stroke-width=(token)\"stroke.hairline\" opacity=0.34"
),
"formatted mesh line missing canonical attrs; got:\n{formatted_str}"
);
let reparsed = adapter
.parse(&formatted)
.expect("re-parse after format must succeed");
assert!(
matches!(&reparsed.body.pages[0].children[0], Node::Mesh(m) if m.id == "bg.mesh"),
"mesh must survive format round-trip"
);
}
const WITH_IMAGE: &str = r##"zenith version=1 {
project id="proj.img" name="Image Test"
assets {
asset id="asset.logo" kind="image" src="assets/logo.png"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.img" title="Image Test" {
page id="page.one" w=(px)320 h=(px)200 {
image id="img.logo" asset="asset.logo" x=(px)80 y=(px)60 w=(px)160 h=(px)48 fit="contain" object-position-x="center" object-position-y=(pct)25
}
}
}
"##;
#[test]
fn image_parses_fields() {
use zenith_core::{Node, ObjectPosition, Unit};
let adapter = KdlAdapter;
let doc = adapter.parse(WITH_IMAGE.as_bytes()).expect("parse");
let node = &doc.body.pages[0].children[0];
let img = match node {
Node::Image(i) => i,
other => panic!("expected Image, got {other:?}"),
};
assert_eq!(img.id, "img.logo");
assert_eq!(img.asset, "asset.logo");
let geom_value = |pv: Option<&PropertyValue>| match pv {
Some(PropertyValue::Dimension(d)) => Some(d.value),
_ => None,
};
assert_eq!(geom_value(img.x.as_ref()), Some(80.0));
assert_eq!(geom_value(img.y.as_ref()), Some(60.0));
assert_eq!(geom_value(img.w.as_ref()), Some(160.0));
assert_eq!(geom_value(img.h.as_ref()), Some(48.0));
assert!(matches!(
img.x.as_ref(),
Some(PropertyValue::Dimension(d)) if d.unit == Unit::Px
));
assert_eq!(img.fit.as_deref(), Some("contain"));
assert_eq!(img.object_position_x, Some(ObjectPosition::Center));
assert_eq!(img.object_position_y, Some(ObjectPosition::Pct(25.0)));
}
#[test]
fn image_format_round_trip_and_idempotency() {
use zenith_core::{Node, ObjectPosition};
let adapter = KdlAdapter;
let doc1 = adapter.parse(WITH_IMAGE.as_bytes()).expect("parse 1");
let s1 = format_document(&doc1).expect("format 1");
let text = String::from_utf8(s1.clone()).unwrap();
assert!(
text.contains("object-position-y=(pct)25"),
"object-position (pct) must format as annotated number; got:\n{text}"
);
assert!(
text.contains("object-position-x=\"center\""),
"object-position anchor must format as string; got:\n{text}"
);
let doc2 = adapter.parse(&s1).expect("parse 2");
let img2 = match &doc2.body.pages[0].children[0] {
Node::Image(i) => i,
other => panic!("expected Image, got {other:?}"),
};
assert_eq!(img2.asset, "asset.logo");
assert_eq!(img2.fit.as_deref(), Some("contain"));
assert_eq!(img2.object_position_x, Some(ObjectPosition::Center));
assert_eq!(img2.object_position_y, Some(ObjectPosition::Pct(25.0)));
let s2 = format_document(&doc2).expect("format 2");
assert_eq!(
String::from_utf8(s1).unwrap(),
String::from_utf8(s2).unwrap(),
"image format must be idempotent"
);
}
#[test]
fn test_image_src_rect_round_trip() {
let src = r##"zenith version=1 {
project id="proj.srcrt" name="SrcRt"
assets {
asset id="asset.photo" kind="image" src="assets/photo.png"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.srcrt" title="SrcRt" {
page id="page.srcrt" w=(px)400 h=(px)300 {
image id="img.srcrt" asset="asset.photo" x=(px)0 y=(px)0 w=(px)200 h=(px)100 src-x=(px)10 src-y=(px)20 src-w=(px)50 src-h=(px)60 fit="stretch"
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
let image_node = match &doc.body.pages[0].children[0] {
Node::Image(i) => i,
other => panic!("expected Image node, got {other:?}"),
};
use zenith_core::{Dimension, Unit};
assert_eq!(
image_node.src_x,
Some(Dimension {
value: 10.0,
unit: Unit::Px
}),
"src-x must parse to (px)10"
);
assert_eq!(
image_node.src_y,
Some(Dimension {
value: 20.0,
unit: Unit::Px
}),
"src-y must parse to (px)20"
);
assert_eq!(
image_node.src_w,
Some(Dimension {
value: 50.0,
unit: Unit::Px
}),
"src-w must parse to (px)50"
);
assert_eq!(
image_node.src_h,
Some(Dimension {
value: 60.0,
unit: Unit::Px
}),
"src-h must parse to (px)60"
);
let formatted = format_document(&doc).expect("format must succeed");
let formatted_str = String::from_utf8(formatted.clone()).expect("formatted must be utf8");
assert!(
formatted_str.contains("src-x=(px)10"),
"formatter must emit src-x=(px)10; got:\n{formatted_str}"
);
assert!(
formatted_str.contains("src-y=(px)20"),
"formatter must emit src-y=(px)20; got:\n{formatted_str}"
);
assert!(
formatted_str.contains("src-w=(px)50"),
"formatter must emit src-w=(px)50; got:\n{formatted_str}"
);
assert!(
formatted_str.contains("src-h=(px)60"),
"formatter must emit src-h=(px)60; got:\n{formatted_str}"
);
let reparsed = adapter.parse(&formatted).expect("re-parse after format");
assert_eq!(
strip_spans(doc),
strip_spans(reparsed),
"src-rect image must round-trip identically"
);
}
#[test]
fn ellipse_stroke_round_trip() {
let src = r##"zenith version=1 {
project id="proj.es" name="ES"
tokens format="zenith-token-v1" {
token id="color.border" type="color" value="#334155"
token id="size.border" type="dimension" value=(px)3
}
styles {
}
document id="doc.es" title="ES" {
page id="p" w=(px)200 h=(px)200 {
ellipse id="e" x=(px)10 y=(px)10 w=(px)80 h=(px)80 stroke=(token)"color.border" stroke-width=(token)"size.border"
}
}
}
"##;
use zenith_core::{Node, PropertyValue};
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse");
match &doc.body.pages[0].children[0] {
Node::Ellipse(e) => {
assert_eq!(
e.stroke,
Some(PropertyValue::TokenRef("color.border".to_owned())),
"stroke must parse to TokenRef(color.border)"
);
assert_eq!(
e.stroke_width,
Some(PropertyValue::TokenRef("size.border".to_owned())),
"stroke_width must parse to TokenRef(size.border)"
);
assert!(e.fill.is_none(), "fill must be absent");
}
other => panic!("expected Ellipse, got {other:?}"),
}
let formatted = format_document(&doc).expect("format");
let formatted_str = String::from_utf8(formatted.clone()).expect("utf8");
let doc2 = adapter.parse(&formatted).expect("re-parse");
match &doc2.body.pages[0].children[0] {
Node::Ellipse(e) => {
assert_eq!(
e.stroke,
Some(PropertyValue::TokenRef("color.border".to_owned())),
"stroke must survive format round-trip"
);
assert_eq!(
e.stroke_width,
Some(PropertyValue::TokenRef("size.border".to_owned())),
"stroke_width must survive format round-trip"
);
}
other => panic!("expected Ellipse on re-parse, got {other:?}"),
}
let ellipse_line = formatted_str
.lines()
.find(|l| l.trim_start().starts_with("ellipse"))
.expect("must find ellipse line");
assert!(
ellipse_line.contains("stroke=(token)\"color.border\""),
"formatted line must contain stroke token; got: {ellipse_line}"
);
assert!(
ellipse_line.contains("stroke-width=(token)\"size.border\""),
"formatted line must contain stroke-width token; got: {ellipse_line}"
);
let pos_stroke = ellipse_line.find(" stroke=").expect("must have stroke=");
let pos_sw = ellipse_line
.find(" stroke-width=")
.expect("must have stroke-width=");
assert!(
pos_stroke < pos_sw,
"stroke= must appear before stroke-width= in canonical output"
);
let s2 = format_document(&doc2).expect("format 2");
assert_eq!(
formatted_str,
String::from_utf8(s2).unwrap(),
"ellipse stroke formatting must be idempotent"
);
}
const SAFE_ZONE_DOC: &str = r##"zenith version=1 {
project id="proj.sz" name="Safe Zone Project"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.sz" title="Safe Zone Doc" {
page id="page.one" w=(px)1500 h=(px)500 {
safe-zone id="sz.avatar" type="exclusion" x=(px)0 y=(px)358 w=(px)175 h=(px)142 label="X avatar dead zone"
rect id="logo" x=(px)600 y=(px)40 w=(px)200 h=(px)80 fill="#ffffff"
}
}
}
"##;
#[test]
fn test_safe_zone_parses_into_page_not_children() {
let adapter = KdlAdapter;
let doc = adapter
.parse(SAFE_ZONE_DOC.as_bytes())
.expect("parse must succeed");
let page = &doc.body.pages[0];
assert_eq!(page.safe_zones.len(), 1, "exactly one safe-zone parsed");
let zone = &page.safe_zones[0];
assert_eq!(zone.id, "sz.avatar");
assert_eq!(zone.zone_type, zenith_core::SafeZoneType::Exclusion);
assert_eq!(zone.label.as_deref(), Some("X avatar dead zone"));
assert_eq!(page.children.len(), 1, "only the rect is a child node");
match &page.children[0] {
Node::Rect(r) => assert_eq!(r.id, "logo"),
other => panic!("expected Rect, got {other:?}"),
}
}
#[test]
fn test_safe_zone_format_round_trip() {
let adapter = KdlAdapter;
let doc_orig = adapter
.parse(SAFE_ZONE_DOC.as_bytes())
.expect("original parse");
let formatted = format_document(&doc_orig).expect("format");
let text = String::from_utf8(formatted.clone()).expect("utf8");
assert!(
text.contains(
"safe-zone id=\"sz.avatar\" type=\"exclusion\" \
x=(px)0 y=(px)358 w=(px)175 h=(px)142 label=\"X avatar dead zone\""
),
"formatted safe-zone line missing/incorrect; output:\n{text}"
);
let doc_reparsed = adapter.parse(&formatted).expect("re-parse after format");
assert_eq!(
strip_spans(doc_orig),
strip_spans(doc_reparsed),
"safe-zone must survive a format round-trip (spans excluded)"
);
}
#[test]
fn test_safe_zone_label_escaping_round_trip() {
let src = "zenith version=1 {\n \
project id=\"proj.szesc\" name=\"SZEsc\"\n \
tokens format=\"zenith-token-v1\" {\n }\n \
styles {\n }\n \
document id=\"doc.szesc\" title=\"SZEsc\" {\n \
page id=\"page.one\" w=(px)800 h=(px)600 {\n \
safe-zone id=\"sz.q\" type=\"exclusion\" x=(px)0 y=(px)0 w=(px)10 h=(px)10 \
label=\"a \\\"q\\\" b\\nc\"\n }\n }\n}\n";
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
let label = doc.body.pages[0].safe_zones[0]
.label
.clone()
.expect("label present");
assert_eq!(
label, "a \"q\" b\nc",
"parsed label has the raw special chars"
);
let formatted = format_document(&doc).expect("format must succeed");
let doc2 = adapter
.parse(&formatted)
.expect("re-parse after format must succeed");
assert_eq!(
doc2.body.pages[0].safe_zones[0].label.as_deref(),
Some("a \"q\" b\nc"),
"safe-zone label with quote/newline must survive parse → format → parse"
);
}
const FOLD_DOC: &str = r##"zenith version=1 {
project id="proj.fold" name="Fold Project"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.fold" title="Fold Doc" {
page id="page.one" w=(px)2480 h=(px)1000 {
fold id="fold.1" orientation="vertical" position=(px)1169
rect id="logo" x=(px)600 y=(px)40 w=(px)200 h=(px)80 fill="#ffffff"
}
}
}
"##;
#[test]
fn test_fold_parses_into_page_not_children() {
let adapter = KdlAdapter;
let doc = adapter
.parse(FOLD_DOC.as_bytes())
.expect("parse must succeed");
let page = &doc.body.pages[0];
assert_eq!(page.folds.len(), 1, "exactly one fold parsed");
let fold = &page.folds[0];
assert_eq!(fold.id, "fold.1");
assert_eq!(fold.orientation, "vertical");
let pos = fold.position.as_ref().expect("position present");
assert_eq!(pos.value, 1169.0);
assert_eq!(page.children.len(), 1, "only the rect is a child node");
match &page.children[0] {
Node::Rect(r) => assert_eq!(r.id, "logo"),
other => panic!("expected Rect, got {other:?}"),
}
}
#[test]
fn test_fold_format_round_trip() {
let adapter = KdlAdapter;
let doc_orig = adapter.parse(FOLD_DOC.as_bytes()).expect("original parse");
let formatted = format_document(&doc_orig).expect("format");
let text = String::from_utf8(formatted.clone()).expect("utf8");
assert!(
text.contains("fold id=\"fold.1\" orientation=\"vertical\" position=(px)1169"),
"formatted fold line missing/incorrect; output:\n{text}"
);
let doc_reparsed = adapter.parse(&formatted).expect("re-parse after format");
assert_eq!(
strip_spans(doc_orig),
strip_spans(doc_reparsed),
"fold must survive a format round-trip (spans excluded)"
);
}
#[test]
fn test_unknown_property_all_types_round_trip() {
let src = r##"zenith version=1 {
project id="proj.rt" name="RT"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.rt" title="RT" {
page id="p" w=(px)100 h=(px)100 {
rect id="r" x=(px)0 y=(px)0 w=(px)10 h=(px)10 future-flag=#true future-float=1.5 future-int=42 future-null=#null future-str="hi"
}
}
}
"##;
let adapter = KdlAdapter;
let doc1 = adapter.parse(src.as_bytes()).expect("parse 1");
let rect = match &doc1.body.pages[0].children[0] {
zenith_core::Node::Rect(r) => r,
other => panic!("expected Rect, got {other:?}"),
};
assert_eq!(
rect.unknown_props["future-flag"].value,
zenith_core::UnknownValue::Bool(true),
"boolean must parse as UnknownValue::Bool(true), not a string"
);
assert_eq!(
rect.unknown_props["future-int"].value,
zenith_core::UnknownValue::Integer(42),
"integer must parse as UnknownValue::Integer(42)"
);
assert_eq!(
rect.unknown_props["future-float"].value,
zenith_core::UnknownValue::Float(1.5),
"float must parse as UnknownValue::Float(1.5)"
);
assert_eq!(
rect.unknown_props["future-str"].value,
zenith_core::UnknownValue::String("hi".to_owned()),
"string must parse as UnknownValue::String"
);
assert_eq!(
rect.unknown_props["future-null"].value,
zenith_core::UnknownValue::Null,
"null must parse as UnknownValue::Null"
);
let formatted1 = format_document(&doc1).expect("format 1");
let doc2 = adapter.parse(&formatted1).expect("parse 2 after format");
let rect2 = match &doc2.body.pages[0].children[0] {
zenith_core::Node::Rect(r) => r,
other => panic!("expected Rect in re-parsed doc, got {other:?}"),
};
assert_eq!(
rect2.unknown_props["future-flag"].value,
zenith_core::UnknownValue::Bool(true),
"boolean must survive format round-trip as UnknownValue::Bool(true)"
);
assert_eq!(
rect2.unknown_props["future-int"].value,
zenith_core::UnknownValue::Integer(42),
"integer must survive format round-trip as UnknownValue::Integer(42)"
);
assert_eq!(
rect2.unknown_props["future-float"].value,
zenith_core::UnknownValue::Float(1.5),
"float must survive format round-trip"
);
assert_eq!(
rect2.unknown_props["future-str"].value,
zenith_core::UnknownValue::String("hi".to_owned()),
"string must survive format round-trip"
);
assert_eq!(
rect2.unknown_props["future-null"].value,
zenith_core::UnknownValue::Null,
"null must survive format round-trip"
);
let formatted2 = format_document(&doc2).expect("format 2");
assert_eq!(
formatted1, formatted2,
"format must be idempotent for documents with unknown properties of all types"
);
}
#[test]
fn test_unknown_property_type_annotation_round_trip() {
let src = r##"zenith version=1 {
project id="proj.ann" name="Ann"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.ann" title="Ann" {
page id="p" w=(px)100 h=(px)100 {
rect id="r" x=(px)0 y=(px)0 w=(px)10 h=(px)10 mystery=(px)42 magic=(token)"color.brand" plain="hello" flag=#true
}
}
}
"##;
let adapter = KdlAdapter;
let doc1 = adapter.parse(src.as_bytes()).expect("parse 1");
let rect = match &doc1.body.pages[0].children[0] {
zenith_core::Node::Rect(r) => r,
other => panic!("expected Rect, got {other:?}"),
};
assert_eq!(
rect.unknown_props["mystery"].ty.as_deref(),
Some("px"),
"`(px)42` must capture ty = Some(\"px\")"
);
assert_eq!(
rect.unknown_props["mystery"].value,
zenith_core::UnknownValue::Integer(42),
);
assert_eq!(
rect.unknown_props["magic"].ty.as_deref(),
Some("token"),
"`(token)\"color.brand\"` must capture ty = Some(\"token\")"
);
assert_eq!(
rect.unknown_props["magic"].value,
zenith_core::UnknownValue::String("color.brand".to_owned()),
);
assert_eq!(
rect.unknown_props["plain"].ty, None,
"non-annotated `plain` must have ty = None"
);
assert_eq!(
rect.unknown_props["flag"].ty, None,
"non-annotated `flag` must have ty = None"
);
let formatted1 = format_document(&doc1).expect("format 1");
let text = String::from_utf8_lossy(&formatted1);
assert!(
text.contains("mystery=(px)42"),
"formatted output must contain `mystery=(px)42`, got:\n{text}"
);
assert!(
text.contains(r#"magic=(token)"color.brand""#),
"formatted output must contain `magic=(token)\"color.brand\"`, got:\n{text}"
);
assert!(
text.contains(r#"plain="hello""#),
"non-annotated `plain=\"hello\"` must be unchanged, got:\n{text}"
);
assert!(
text.contains("flag=#true"),
"non-annotated `flag=#true` must be unchanged, got:\n{text}"
);
let doc2 = adapter.parse(&formatted1).expect("parse 2 after format");
let rect2 = match &doc2.body.pages[0].children[0] {
zenith_core::Node::Rect(r) => r,
other => panic!("expected Rect in re-parsed doc, got {other:?}"),
};
assert_eq!(
rect.unknown_props, rect2.unknown_props,
"unknown_props (value + ty) must be byte-stable across parse→format→parse"
);
let formatted2 = format_document(&doc2).expect("format 2");
assert_eq!(
formatted1, formatted2,
"format must be idempotent for annotated unknown properties"
);
}
#[test]
fn test_unknown_property_preserved() {
let src = r##"zenith version=1 {
project id="proj.unk" name="Unk"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.unk" title="Unk" {
page id="p" w=(px)100 h=(px)100 {
rect id="r" x=(px)0 y=(px)0 w=(px)10 h=(px)10 future-prop="hello"
}
}
}
"##;
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();
assert!(
text.contains("future-prop="),
"unknown property `future-prop` must survive format; got:\n{text}"
);
}
#[test]
fn test_anchor_sibling_round_trip() {
let src = r##"zenith version=1 {
project id="proj.as" name="AS"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.as" title="AS" {
page id="p" w=(px)200 h=(px)200 {
rect id="r" anchor="top-left" anchor-sibling="some-id" x=(px)0 y=(px)0 w=(px)50 h=(px)50
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
match &doc.body.pages[0].children[0] {
Node::Rect(r) => {
assert_eq!(
r.anchor.as_deref(),
Some("top-left"),
"anchor must parse to \"top-left\""
);
assert_eq!(
r.anchor_sibling.as_deref(),
Some("some-id"),
"anchor-sibling must parse to \"some-id\""
);
}
other => panic!("expected Rect, got {other:?}"),
}
let formatted = format_document(&doc).expect("format must succeed");
let formatted_str = String::from_utf8(formatted).expect("formatted must be utf8");
assert!(
formatted_str.contains("anchor-sibling=\"some-id\""),
"formatter must emit anchor-sibling=\"some-id\"; got:\n{formatted_str}"
);
let doc2 = adapter
.parse(formatted_str.as_bytes())
.expect("re-parse after format must succeed");
match &doc2.body.pages[0].children[0] {
Node::Rect(r) => {
assert_eq!(
r.anchor_sibling.as_deref(),
Some("some-id"),
"anchor-sibling must survive a format → re-parse round-trip"
);
assert_eq!(
r.anchor.as_deref(),
Some("top-left"),
"anchor must survive a format → re-parse round-trip"
);
}
other => panic!("expected Rect on re-parse, got {other:?}"),
}
}
#[test]
fn connector_label_roundtrip_preserves_spans_and_text_style() {
let src = r##"zenith version=1 {
project id="proj.clrt" name="CLRT"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.clrt" title="CLRT" {
page id="p.clrt" w=(px)640 h=(px)360 {
rect id="a" x=(px)40 y=(px)40 w=(px)100 h=(px)80
rect id="b" x=(px)300 y=(px)60 w=(px)100 h=(px)80
connector id="c1" from="a" to="b" text-style="s.branch" {
span "Yes"
}
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
let connector = match &doc.body.pages[0].children[2] {
Node::Connector(c) => c,
other => panic!("expected Connector node, got {other:?}"),
};
assert_eq!(connector.spans.len(), 1, "connector must have 1 span");
assert_eq!(connector.spans[0].text, "Yes", "span text must be \"Yes\"");
assert_eq!(
connector.text_style.as_deref(),
Some("s.branch"),
"text-style must parse as \"s.branch\""
);
let formatted = format_document(&doc).expect("format must succeed");
let formatted_str = String::from_utf8(formatted).expect("formatted must be utf8");
assert!(
formatted_str.contains("span \"Yes\""),
"formatted output must contain span \"Yes\"; got:\n{formatted_str}"
);
assert!(
formatted_str.contains("text-style=\"s.branch\""),
"formatted output must contain text-style=\"s.branch\"; got:\n{formatted_str}"
);
let doc2 = adapter
.parse(formatted_str.as_bytes())
.expect("re-parse after format must succeed");
let connector2 = match &doc2.body.pages[0].children[2] {
Node::Connector(c) => c,
other => panic!("expected Connector on re-parse, got {other:?}"),
};
assert_eq!(
connector2.spans.len(),
1,
"connector must still have 1 span after roundtrip"
);
assert_eq!(
connector2.spans[0].text, "Yes",
"span text must survive parse → format → parse"
);
assert_eq!(
connector2.text_style.as_deref(),
Some("s.branch"),
"text-style must survive parse → format → parse"
);
}
#[test]
fn connector_without_label_emits_no_brace_block() {
let src = r##"zenith version=1 {
project id="proj.cnbl" name="CNBL"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.cnbl" title="CNBL" {
page id="p.cnbl" w=(px)640 h=(px)360 {
rect id="a" x=(px)40 y=(px)40 w=(px)100 h=(px)80
rect id="b" x=(px)300 y=(px)60 w=(px)100 h=(px)80
connector id="c1" from="a" to="b"
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
let formatted = format_document(&doc).expect("format must succeed");
let formatted_str = String::from_utf8(formatted).expect("utf8");
let connector_line = formatted_str
.lines()
.find(|l| l.trim_start().starts_with("connector "))
.expect("formatted output must contain a connector line");
assert!(
!connector_line.contains('{'),
"label-less connector must not emit a brace block; line: {connector_line:?}"
);
}