use crate::emitter::emit_document;
use crate::parser::parse_document;
use crate::transform::{dedup_use_styles, sort_nodes};
#[derive(Debug, Clone)]
pub struct FormatConfig {
pub dedup_use: bool,
pub hoist_styles: bool,
pub sort_nodes: bool,
}
impl Default for FormatConfig {
fn default() -> Self {
Self {
dedup_use: true,
hoist_styles: false,
sort_nodes: true,
}
}
}
pub fn format_document(text: &str, config: &FormatConfig) -> Result<String, String> {
let mut scene = parse_document(text)?;
if config.dedup_use {
dedup_use_styles(&mut scene);
}
if config.hoist_styles {
crate::transform::hoist_styles(&mut scene);
}
if config.sort_nodes {
sort_nodes(&mut scene);
}
Ok(emit_document(&scene))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_document_default_is_idempotent() {
let input = r#"
# FD v1
style accent {
fill: #6C5CE7
corner: 10
}
rect @primary_btn {
w: 200 h: 48
use: accent
}
"#;
let config = FormatConfig::default();
let first = format_document(input, &config).expect("first format failed");
let second = format_document(&first, &config).expect("second format failed");
assert_eq!(first, second, "format must be idempotent");
}
#[test]
fn format_document_dedupes_use_styles() {
let input = r#"
style card {
fill: #FFF
}
rect @box {
w: 100 h: 50
use: card
use: card
}
"#;
let config = FormatConfig::default();
let output = format_document(input, &config).expect("format failed");
let use_count = output.matches("use: card").count();
assert_eq!(use_count, 1, "duplicate use: should be removed");
}
#[test]
fn format_document_preserves_comments() {
let input = r#"
# This is a section header
rect @box {
w: 100 h: 50
fill: #FF0000
}
"#;
let config = FormatConfig::default();
let output = format_document(input, &config).expect("format failed");
assert!(
output.contains("# This is a section header"),
"comments must survive formatting"
);
}
#[test]
fn format_document_sorts_nodes_by_kind() {
let input = r#"
text @label "World" {
font: "Inter" regular 14
}
rect @box {
w: 100 h: 50
}
group @wrapper {
rect @child {
w: 50 h: 50
}
}
"#;
let config = FormatConfig::default();
let output = format_document(input, &config).expect("format failed");
let group_pos = output.find("group @wrapper").expect("group not found");
let rect_pos = output.find("rect @box").expect("rect not found");
let text_pos = output.find("text @label").expect("text not found");
assert!(
group_pos < rect_pos,
"group should come before rect in formatted output"
);
assert!(
rect_pos < text_pos,
"rect should come before text in formatted output"
);
}
#[test]
fn format_document_sort_is_idempotent() {
let input = r#"
text @label "Hello" {
font: "Inter" regular 14
}
ellipse @circle {
w: 60 h: 60
}
rect @box {
w: 100 h: 50
}
group @container {
rect @inner {
w: 50 h: 50
}
}
"#;
let config = FormatConfig::default();
let first = format_document(input, &config).expect("first format failed");
let second = format_document(&first, &config).expect("second format failed");
assert_eq!(first, second, "sort + format must be idempotent");
}
}