mod common;
use common::*;
use zenith_core::format::format_document;
const MINIMAL: &str = r##"zenith version=1 {
project id="proj.test" name="Test Project"
tokens format="zenith-token-v1" {
token id="color.bg" type="color" value="#f8fafc"
token id="size.title" type="dimension" value=(pt)48
token id="font.weight.bold" type="fontWeight" value=700
token id="lh.body" type="number" value=1.45
}
styles {
}
document id="doc.test" title="Test Doc" {
page id="page.one" name="One" w=(px)640 h=(px)360 background=(token)"color.bg" {
rect id="bg.rect" x=(px)0 y=(px)0 w=(px)640 h=(px)360 fill=(token)"color.bg"
text id="label" x=(px)10 y=(px)10 w=(px)200 h=(px)50 align="center" fill=(token)"color.text" {
span "Hello Zenith"
}
}
}
}
"##;
const CODE_DOC: &str = r##"zenith version=1 {
project id="proj.code" name="Code Project"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.code" title="Code Doc" {
page id="page.one" w=(px)640 h=(px)360 {
code id="snippet" x=(px)96 y=(px)320 w=(px)560 h=(px)180 overflow="clip" language="rust" line-numbers=#false tab-width=4 {
content "fn main() {\n let path = \"c:\\\\tmp\";\n\n\tprintln!(\"hi\");\n}"
}
}
}
}
"##;
#[test]
fn test_idempotency() {
let adapter = KdlAdapter;
let doc1 = adapter
.parse(MINIMAL.as_bytes())
.expect("parse 1 must succeed");
let s1 = format_document(&doc1).expect("format 1 must succeed");
let doc2 = adapter.parse(&s1).expect("parse 2 must succeed");
let s2 = format_document(&doc2).expect("format 2 must succeed");
assert_eq!(
String::from_utf8(s1.clone()).unwrap(),
String::from_utf8(s2).unwrap(),
"format must be idempotent"
);
}
#[test]
fn test_round_trip_ast_equality() {
let adapter = KdlAdapter;
let doc_orig = adapter.parse(MINIMAL.as_bytes()).expect("original parse");
let formatted = format_document(&doc_orig).expect("format");
let doc_reparsed = adapter.parse(&formatted).expect("re-parse after format");
let orig_stripped = strip_spans(doc_orig);
let reparsed_stripped = strip_spans(doc_reparsed);
assert_eq!(
orig_stripped, reparsed_stripped,
"re-parsed AST must equal original (spans excluded)"
);
}
#[test]
fn test_baseline_grid_round_trips() {
let src = r##"zenith version=1 {
project id="proj.bg" name="BG"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.bg" title="BG" {
page id="page.one" w=(px)640 h=(px)360 baseline-grid=(px)14 {
rect id="r" x=(px)0 y=(px)0 w=(px)10 h=(px)10 fill=(token)"c"
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse");
let page = &doc.body.pages[0];
assert!(
page.baseline_grid.is_some(),
"baseline-grid must parse onto the page"
);
let formatted = format_document(&doc).expect("format");
let formatted_str = String::from_utf8(formatted.clone()).expect("utf8");
assert!(
formatted_str.contains("baseline-grid=(px)14"),
"formatted output must contain baseline-grid; got:\n{formatted_str}"
);
let reparsed = adapter.parse(&formatted).expect("re-parse");
assert_eq!(
strip_spans(doc).body.pages[0].baseline_grid,
strip_spans(reparsed).body.pages[0].baseline_grid,
"baseline-grid must round-trip identically"
);
}
#[test]
fn test_font_size_min_round_trips() {
let src = r##"zenith version=1 {
project id="proj.fsm" name="FSM"
tokens format="zenith-token-v1" {
token id="size.title.min" type="dimension" value=(px)12
}
styles {
}
document id="doc.fsm" title="FSM" {
page id="page.one" w=(px)640 h=(px)360 {
text id="t" x=(px)0 y=(px)0 w=(px)200 h=(px)40 overflow="autofit" font-size-min=(token)"size.title.min" {
span "Hi"
}
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse");
let formatted = format_document(&doc).expect("format");
let formatted_str = String::from_utf8(formatted.clone()).expect("utf8");
assert!(
formatted_str.contains(r#"font-size-min=(token)"size.title.min""#),
"formatted output must contain font-size-min; got:\n{formatted_str}"
);
let reparsed = adapter.parse(&formatted).expect("re-parse");
assert_eq!(
strip_spans(doc).body.pages[0].children,
strip_spans(reparsed).body.pages[0].children,
"font-size-min must round-trip identically"
);
}
#[test]
fn test_code_content_verbatim_round_trip() {
let adapter = KdlAdapter;
let doc1 = adapter.parse(CODE_DOC.as_bytes()).expect("parse 1");
let original = match &doc1.body.pages[0].children[0] {
Node::Code(c) => c.content.clone(),
other => panic!("expected Code node, got {other:?}"),
};
assert!(original.contains('\n') && original.contains('\t'));
assert!(original.contains('"') && original.contains('\\'));
let s1 = format_document(&doc1).expect("format 1");
let doc2 = adapter.parse(&s1).expect("parse 2");
let reparsed = match &doc2.body.pages[0].children[0] {
Node::Code(c) => c.content.clone(),
other => panic!("expected Code node, got {other:?}"),
};
assert_eq!(
original, reparsed,
"code content must round-trip byte-identically"
);
let s2 = format_document(&doc2).expect("format 2");
assert_eq!(
String::from_utf8(s1).unwrap(),
String::from_utf8(s2).unwrap(),
"code formatting must be idempotent"
);
}
#[test]
fn test_pt_48_round_trips() {
let src = r##"zenith version=1 {
project id="proj.t" name="T"
tokens format="zenith-token-v1" {
token id="size.title" type="dimension" value=(pt)48
}
styles {
}
document id="doc.t" title="T" {
page id="p" w=(px)100 h=(px)100 {
}
}
}
"##;
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("value=(pt)48"),
"expected `value=(pt)48` in output, got:\n{text}"
);
assert!(
!text.contains("(pt)48.0"),
"must not contain (pt)48.0 in output"
);
}
#[test]
fn test_literal_dimension_round_trips() {
use zenith_core::{Dimension, Node, PropertyValue, Unit};
let src = r##"zenith version=1 {
project id="proj.ld" name="LD"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.ld" title="LD" {
page id="p" w=(px)100 h=(px)100 {
rect id="r" x=(px)0 y=(px)0 w=(px)10 h=(px)10 stroke-width=(px)2
}
}
}
"##;
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("stroke-width=(px)2"),
"expected `stroke-width=(px)2`, got:\n{text}"
);
assert!(
!text.contains("(px)2.0"),
"must not emit (px)2.0; got:\n{text}"
);
assert!(
!text.contains("stroke-width=\"2\""),
"must not emit a quoted literal; got:\n{text}"
);
let doc2 = adapter.parse(text.as_bytes()).expect("re-parse");
match &doc2.body.pages[0].children[0] {
Node::Rect(r) => assert_eq!(
r.stroke_width,
Some(PropertyValue::Dimension(Dimension {
value: 2.0,
unit: Unit::Px,
}))
),
other => panic!("expected Rect, got {other:?}"),
}
}
#[test]
fn test_canonical_property_order_rect() {
let src = r##"zenith version=1 {
project id="proj.order" name="Order"
tokens format="zenith-token-v1" {
token id="color.bg" type="color" value="#ffffff"
}
styles {
}
document id="doc.order" title="Order" {
page id="p" w=(px)100 h=(px)100 {
rect id="r" fill=(token)"color.bg" x=(px)10 y=(px)20 w=(px)50 h=(px)50
}
}
}
"##;
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 rect_line = text
.lines()
.find(|l| l.trim_start().starts_with("rect"))
.expect("must find rect line");
let pos_x = rect_line.find(" x=").expect("must find x= on rect line");
let pos_fill = rect_line
.find(" fill=")
.expect("must find fill= on rect line");
assert!(
pos_x < pos_fill,
"x= must appear before fill= in canonical output; rect line: {rect_line:?}"
);
}
#[test]
fn test_boolean_format() {
let src = r##"zenith version=1 {
project id="proj.bool" name="Bool"
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.bool" title="Bool" {
page id="p" w=(px)100 h=(px)100 {
rect id="r" x=(px)0 y=(px)0 w=(px)10 h=(px)10 visible=#false
}
}
}
"##;
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("visible=#false"),
"expected `visible=#false`, got:\n{text}"
);
}
#[test]
fn test_doc_id_round_trips() {
let src = r##"zenith version=1 doc-id="my-doc-123" {
project id="proj.did" name="DocId"
tokens format="zenith-token-v1" {
token id="color.bg" type="color" value="#ffffff"
}
styles {
}
document id="doc.did" title="DocId" {
page id="page.one" w=(px)640 h=(px)360 {
rect id="r" x=(px)0 y=(px)0 w=(px)640 h=(px)360 fill=(token)"color.bg"
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
assert_eq!(
doc.doc_id.as_deref(),
Some("my-doc-123"),
"doc-id must parse onto doc.doc_id"
);
let formatted = format_document(&doc).expect("format must succeed");
let formatted_str = String::from_utf8(formatted.clone()).expect("utf8");
assert!(
formatted_str.contains("doc-id=\"my-doc-123\""),
"formatted output must contain doc-id=\"my-doc-123\"; got:\n{formatted_str}"
);
let reparsed = adapter.parse(&formatted).expect("re-parse after format");
assert_eq!(
doc.doc_id, reparsed.doc_id,
"doc_id must round-trip identically"
);
let formatted2 = format_document(&reparsed).expect("format 2 must succeed");
assert_eq!(
formatted, formatted2,
"doc-id formatting must be idempotent"
);
}
#[test]
fn test_doc_id_absent_is_none() {
let src = r##"zenith version=1 {
project id="proj.nodid" name="NoDid"
tokens format="zenith-token-v1" {
token id="color.bg" type="color" value="#ffffff"
}
styles {
}
document id="doc.nodid" title="NoDid" {
page id="page.one" w=(px)640 h=(px)360 {
rect id="r" x=(px)0 y=(px)0 w=(px)640 h=(px)360 fill=(token)"color.bg"
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
assert!(
doc.doc_id.is_none(),
"doc_id must be None when doc-id is absent from the zenith node"
);
}
#[test]
fn test_absent_assets_block_is_empty() {
let adapter = KdlAdapter;
let doc = adapter
.parse(MINIMAL.as_bytes())
.expect("parse must succeed");
assert!(
doc.assets.assets.is_empty(),
"absent assets block must yield an empty AssetBlock"
);
}