use std::fmt::Write as _;
use crate::ast::{
ActionDef, AssetBlock, AssetDecl, BrandContract, ComponentDef, DiagnosticPolicy, Dimension,
Document, LibraryDef, MasterDef, ObjectPosition, PolicyVerb, Project, PropertyValue,
ProvenanceDef, RecipeDef, RecipeParam, SectionDef, UnknownProperty, UnknownValue, VariantDef,
};
use crate::error::FormatError;
mod nodes;
mod styles;
mod tokens;
#[cfg(test)]
mod tests;
use nodes::{write_component_children, write_document_body};
use styles::write_style_block;
use tokens::write_token_block;
fn fmt_unknown_value(v: &UnknownValue) -> String {
match v {
UnknownValue::String(s) => {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
out.push_str(&escape_kdl_string(s));
out.push('"');
out
}
UnknownValue::Integer(n) => n.to_string(),
UnknownValue::Float(f) => fmt_f64(*f),
UnknownValue::Bool(b) => (if *b { "#true" } else { "#false" }).to_owned(),
UnknownValue::Null => "#null".to_owned(),
}
}
pub(super) fn fmt_unknown_property(p: &UnknownProperty) -> String {
match &p.ty {
Some(ty) => format!("({}){}", ty, fmt_unknown_value(&p.value)),
None => fmt_unknown_value(&p.value),
}
}
pub fn format_document(doc: &Document) -> Result<Vec<u8>, FormatError> {
let mut out = String::new();
write_document(doc, &mut out);
out.push('\n');
Ok(out.into_bytes())
}
pub(super) fn indent(out: &mut String, depth: usize) {
for _ in 0..depth * 2 {
out.push(' ');
}
}
pub(super) fn fmt_f64(v: f64) -> String {
if v.fract() == 0.0 && v.is_finite() {
format!("{}", v as i64)
} else {
format!("{v}")
}
}
pub(super) fn fmt_dimension(d: &Dimension) -> String {
d.to_kdl_string()
}
pub(super) fn fmt_property_value(pv: &PropertyValue) -> String {
match pv {
PropertyValue::TokenRef(id) => format!("(token)\"{id}\""),
PropertyValue::Literal(s) => format!("\"{s}\""),
PropertyValue::Dimension(d) => fmt_dimension(d),
PropertyValue::DataRef(path) => format!("(data)\"{path}\""),
}
}
pub(super) fn write_opt_property_value(out: &mut String, key: &str, opt: &Option<PropertyValue>) {
if let Some(pv) = opt {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_property_value(pv));
}
}
pub(super) fn write_opt_dimension(out: &mut String, key: &str, opt: &Option<Dimension>) {
if let Some(d) = opt {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_dimension(d));
}
}
pub(super) fn write_opt_str(out: &mut String, key: &str, opt: &Option<String>) {
if let Some(s) = opt {
out.push(' ');
out.push_str(key);
out.push_str("=\"");
out.push_str(s);
out.push('"');
}
}
pub(super) fn write_opt_str_escaped(out: &mut String, key: &str, opt: &Option<String>) {
if let Some(s) = opt {
out.push(' ');
out.push_str(key);
out.push_str("=\"");
out.push_str(&escape_kdl_string(s));
out.push('"');
}
}
pub(super) fn write_opt_bool(out: &mut String, key: &str, opt: &Option<bool>) {
if let Some(b) = opt {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(if *b { "#true" } else { "#false" });
}
}
pub(super) fn write_opt_object_position(out: &mut String, key: &str, opt: &Option<ObjectPosition>) {
if let Some(pos) = opt {
out.push(' ');
out.push_str(key);
out.push('=');
match pos {
ObjectPosition::Start => out.push_str("\"start\""),
ObjectPosition::Center => out.push_str("\"center\""),
ObjectPosition::End => out.push_str("\"end\""),
ObjectPosition::Pct(n) => {
out.push_str("(pct)");
out.push_str(&fmt_f64(*n));
}
}
}
}
pub(super) fn write_opt_f64(out: &mut String, key: &str, opt: &Option<f64>) {
if let Some(v) = opt {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_f64(*v));
}
}
pub(super) fn escape_kdl_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
other => out.push(other),
}
}
out
}
fn write_document(doc: &Document, out: &mut String) {
out.push_str("zenith version=");
let _ = write!(out, "{}", doc.version);
write_opt_str(out, "colorspace", &doc.colorspace);
write_opt_str(out, "doc-id", &doc.doc_id);
write_opt_bool(out, "mirror-margins", &doc.mirror_margins);
write_opt_bool(out, "facing-pages", &doc.facing_pages);
write_opt_dimension(out, "spread-gutter", &doc.spread_gutter);
write_opt_dimension(out, "margin-inner", &doc.margin_inner);
write_opt_dimension(out, "margin-outer", &doc.margin_outer);
write_opt_dimension(out, "margin-top", &doc.margin_top);
write_opt_dimension(out, "margin-bottom", &doc.margin_bottom);
write_opt_str(out, "page-progression", &doc.page_progression);
write_opt_str(out, "page-parity-start", &doc.page_parity_start);
out.push_str(" {\n");
write_diagnostics_block(&doc.diagnostic_policy, out, 1);
write_brand_block(&doc.brand_contract, out, 1);
if let Some(proj) = &doc.project {
write_project(proj, out, 1);
}
write_asset_block(&doc.assets, out, 1);
write_library_block(&doc.libraries, out, 1);
write_token_block(&doc.tokens, out, 1);
write_style_block(&doc.styles, out, 1);
write_component_block(&doc.components, out, 1);
write_master_block(&doc.masters, out, 1);
write_section_block(&doc.sections, out, 1);
write_provenance_block(&doc.provenance, out, 1);
write_variants_block(&doc.variants, out, 1);
write_recipes_block(&doc.recipes, out, 1);
write_action_block(&doc.actions, out, 1);
write_document_body(&doc.body, out, 1);
out.push('}');
}
fn write_diagnostics_block(policy: &DiagnosticPolicy, out: &mut String, depth: usize) {
if policy.entries.is_empty() {
return;
}
indent(out, depth);
out.push_str("diagnostics {\n");
for entry in &policy.entries {
indent(out, depth + 1);
let verb = match entry.verb {
PolicyVerb::Allow => "allow",
PolicyVerb::Deny => "deny",
PolicyVerb::Warn => "warn",
};
out.push_str(verb);
out.push_str(" \"");
out.push_str(&escape_kdl_string(&entry.code));
out.push_str("\"\n");
}
indent(out, depth);
out.push_str("}\n");
}
fn write_brand_block(contract: &BrandContract, out: &mut String, depth: usize) {
if contract.is_empty() {
return;
}
indent(out, depth);
out.push_str("brand {\n");
if let Some(colors) = &contract.allowed_colors {
indent(out, depth + 1);
out.push_str("colors");
for color in colors {
out.push_str(" \"");
out.push_str(color);
out.push('"');
}
out.push('\n');
}
if let Some(fonts) = &contract.allowed_fonts {
indent(out, depth + 1);
out.push_str("fonts");
for font in fonts {
out.push_str(" \"");
out.push_str(&escape_kdl_string(font));
out.push('"');
}
out.push('\n');
}
if let Some(weights) = &contract.allowed_weights {
indent(out, depth + 1);
out.push_str("weights");
for weight in weights {
out.push(' ');
let _ = write!(out, "{weight}");
}
out.push('\n');
}
indent(out, depth);
out.push_str("}\n");
}
fn write_master_block(masters: &[MasterDef], out: &mut String, depth: usize) {
if masters.is_empty() {
return;
}
indent(out, depth);
out.push_str("masters {\n");
for def in masters {
indent(out, depth + 1);
out.push_str("master id=\"");
out.push_str(&def.id);
out.push_str("\" {\n");
write_component_children(&def.children, out, depth + 1);
indent(out, depth + 1);
out.push_str("}\n");
}
indent(out, depth);
out.push_str("}\n");
}
fn write_section_block(sections: &[SectionDef], out: &mut String, depth: usize) {
if sections.is_empty() {
return;
}
indent(out, depth);
out.push_str("sections {\n");
for def in sections {
indent(out, depth + 1);
out.push_str("section id=\"");
out.push_str(&def.id);
out.push_str("\" name=\"");
out.push_str(&escape_kdl_string(&def.name));
out.push('"');
if let Some(fs) = def.folio_start {
out.push_str(" folio-start=");
let _ = write!(out, "{fs}");
}
write_opt_str(out, "folio-style", &def.folio_style);
out.push_str(" start-page=\"");
out.push_str(&def.start_page);
out.push_str("\"\n");
}
indent(out, depth);
out.push_str("}\n");
}
fn write_component_block(components: &[ComponentDef], out: &mut String, depth: usize) {
if components.is_empty() {
return;
}
indent(out, depth);
out.push_str("components {\n");
for def in components {
indent(out, depth + 1);
out.push_str("component id=\"");
out.push_str(&def.id);
out.push_str("\" {\n");
write_component_children(&def.children, out, depth + 1);
indent(out, depth + 1);
out.push_str("}\n");
}
indent(out, depth);
out.push_str("}\n");
}
fn write_project(proj: &Project, out: &mut String, depth: usize) {
indent(out, depth);
out.push_str("project");
out.push_str(" id=\"");
out.push_str(&proj.id);
out.push('"');
out.push_str(" name=\"");
out.push_str(&proj.name);
out.push('"');
if let Some(author) = &proj.author {
out.push_str(" {\n");
indent(out, depth + 1);
out.push_str("author \"");
out.push_str(author);
out.push_str("\"\n");
indent(out, depth);
out.push_str("}\n");
} else {
out.push('\n');
}
}
fn write_asset_block(block: &AssetBlock, out: &mut String, depth: usize) {
indent(out, depth);
out.push_str("assets {\n");
for decl in &block.assets {
write_asset_decl(decl, out, depth + 1);
}
indent(out, depth);
out.push_str("}\n");
}
fn write_asset_decl(decl: &AssetDecl, out: &mut String, depth: usize) {
indent(out, depth);
out.push_str("asset");
out.push_str(" id=\"");
out.push_str(&decl.id);
out.push('"');
out.push_str(" kind=\"");
out.push_str(decl.kind.kind_str());
out.push('"');
out.push_str(" src=\"");
out.push_str(&decl.src);
out.push('"');
if let Some(sha256) = &decl.sha256 {
out.push_str(" sha256=\"");
out.push_str(sha256);
out.push('"');
}
write_opt_str_escaped(out, "ai-prompt", &decl.ai_prompt);
write_opt_str_escaped(out, "ai-model", &decl.ai_model);
write_opt_str_escaped(out, "ai-provider", &decl.ai_provider);
if let Some(seed) = decl.ai_seed {
out.push_str(" ai-seed=");
let _ = write!(out, "{seed}");
}
write_opt_str_escaped(out, "ai-generation-date", &decl.ai_generation_date);
write_opt_str_escaped(out, "ai-license", &decl.ai_license);
write_opt_str_escaped(out, "ai-source-rights", &decl.ai_source_rights);
write_opt_str_escaped(out, "ai-safety-status", &decl.ai_safety_status);
write_opt_str_escaped(out, "ai-reuse-policy", &decl.ai_reuse_policy);
for (key, prop) in &decl.unknown_props {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_unknown_property(prop));
}
out.push('\n');
}
fn write_library_block(libraries: &[LibraryDef], out: &mut String, depth: usize) {
if libraries.is_empty() {
return;
}
indent(out, depth);
out.push_str("libraries {\n");
for def in libraries {
indent(out, depth + 1);
out.push_str("library id=\"");
out.push_str(&def.id);
out.push('"');
if let Some(version) = &def.version {
out.push_str(" version=\"");
out.push_str(version);
out.push('"');
}
if let Some(hash) = &def.hash {
out.push_str(" hash=\"");
out.push_str(hash);
out.push('"');
}
for (key, prop) in &def.unknown_props {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_unknown_property(prop));
}
out.push('\n');
}
indent(out, depth);
out.push_str("}\n");
}
fn write_provenance_block(provenance: &[ProvenanceDef], out: &mut String, depth: usize) {
if provenance.is_empty() {
return;
}
indent(out, depth);
out.push_str("provenance {\n");
for def in provenance {
indent(out, depth + 1);
out.push_str("origin id=\"");
out.push_str(&def.id);
out.push_str("\" node=\"");
out.push_str(&def.node);
out.push_str("\" library=\"");
out.push_str(&def.library);
out.push('"');
if let Some(item) = &def.item {
out.push_str(" item=\"");
out.push_str(item);
out.push('"');
}
write_opt_bool(out, "linked", &def.linked);
for (key, prop) in &def.unknown_props {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_unknown_property(prop));
}
out.push('\n');
}
indent(out, depth);
out.push_str("}\n");
}
fn write_variants_block(variants: &[VariantDef], out: &mut String, depth: usize) {
if variants.is_empty() {
return;
}
indent(out, depth);
out.push_str("variants {\n");
for def in variants {
indent(out, depth + 1);
out.push_str("variant id=\"");
out.push_str(&def.id);
out.push_str("\" source=\"");
out.push_str(&def.source);
out.push_str("\" w=");
out.push_str(&fmt_dimension(&def.w));
out.push_str(" h=");
out.push_str(&fmt_dimension(&def.h));
for (key, prop) in &def.unknown_props {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_unknown_property(prop));
}
out.push_str(" {\n");
for ov in &def.overrides {
indent(out, depth + 2);
out.push_str("override node=\"");
out.push_str(&ov.node);
out.push('"');
write_opt_bool(out, "visible", &ov.visible);
write_opt_dimension(out, "x", &ov.x);
write_opt_dimension(out, "y", &ov.y);
write_opt_dimension(out, "w", &ov.w);
write_opt_dimension(out, "h", &ov.h);
write_opt_property_value(out, "fill", &ov.fill);
write_opt_str_escaped(out, "text", &ov.text);
for (key, prop) in &ov.unknown_props {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_unknown_property(prop));
}
out.push('\n');
}
indent(out, depth + 1);
out.push_str("}\n");
}
indent(out, depth);
out.push_str("}\n");
}
fn write_recipes_block(recipes: &[RecipeDef], out: &mut String, depth: usize) {
if recipes.is_empty() {
return;
}
indent(out, depth);
out.push_str("recipes {\n");
for def in recipes {
indent(out, depth + 1);
out.push_str("recipe id=\"");
out.push_str(&def.id);
out.push_str("\" kind=\"");
out.push_str(&escape_kdl_string(&def.kind));
out.push('"');
if let Some(seed) = def.seed {
out.push_str(" seed=");
let _ = write!(out, "{seed}");
}
if let Some(generator) = &def.generator {
out.push_str(" generator=\"");
out.push_str(&escape_kdl_string(generator));
out.push('"');
}
if let Some(bounds) = &def.bounds {
out.push_str(" bounds=\"");
out.push_str(&escape_kdl_string(bounds));
out.push('"');
}
write_opt_bool(out, "detached", &def.detached);
for (key, prop) in &def.unknown_props {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_unknown_property(prop));
}
out.push_str(" {\n");
for param in &def.params {
write_recipe_param(param, out, depth + 2);
}
for token_id in &def.palette {
indent(out, depth + 2);
out.push_str("palette token=\"");
out.push_str(token_id);
out.push_str("\"\n");
}
for node_id in &def.expanded {
indent(out, depth + 2);
out.push_str("expanded node=\"");
out.push_str(node_id);
out.push_str("\"\n");
}
indent(out, depth + 1);
out.push_str("}\n");
}
indent(out, depth);
out.push_str("}\n");
}
fn write_recipe_param(param: &RecipeParam, out: &mut String, depth: usize) {
indent(out, depth);
out.push_str("param name=\"");
out.push_str(¶m.name);
out.push_str("\" value=");
out.push_str(&fmt_property_value(¶m.value));
for (key, prop) in ¶m.unknown_props {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_unknown_property(prop));
}
out.push('\n');
}
fn write_action_block(actions: &[ActionDef], out: &mut String, depth: usize) {
if actions.is_empty() {
return;
}
indent(out, depth);
out.push_str("actions {\n");
for def in actions {
indent(out, depth + 1);
out.push_str("action id=\"");
out.push_str(&def.id);
out.push('"');
if let Some(label) = &def.label {
out.push_str(" label=\"");
out.push_str(&escape_kdl_string(label));
out.push('"');
}
if let Some(version) = &def.version {
out.push_str(" version=\"");
out.push_str(version);
out.push('"');
}
for (key, prop) in &def.unknown_props {
out.push(' ');
out.push_str(key);
out.push('=');
out.push_str(&fmt_unknown_property(prop));
}
out.push_str(" {\n");
indent(out, depth + 2);
out.push_str("tx \"");
out.push_str(&escape_kdl_string(&def.tx_json));
out.push_str("\"\n");
indent(out, depth + 1);
out.push_str("}\n");
}
indent(out, depth);
out.push_str("}\n");
}