1use crate::emitter::emit_document;
8use crate::parser::parse_document;
9use crate::transform::{dedup_use_styles, sort_nodes};
10
11#[derive(Debug, Clone)]
18pub struct FormatConfig {
19 pub dedup_use: bool,
21
22 pub hoist_styles: bool,
26
27 pub sort_nodes: bool,
30}
31
32impl Default for FormatConfig {
33 fn default() -> Self {
34 Self {
35 dedup_use: true,
36 hoist_styles: false,
37 sort_nodes: true,
38 }
39 }
40}
41
42pub fn format_document(text: &str, config: &FormatConfig) -> Result<String, String> {
51 let mut scene = parse_document(text)?;
52
53 if config.dedup_use {
54 dedup_use_styles(&mut scene);
55 }
56
57 if config.hoist_styles {
58 crate::transform::hoist_styles(&mut scene);
59 }
60
61 if config.sort_nodes {
62 sort_nodes(&mut scene);
63 }
64
65 Ok(emit_document(&scene))
66}
67
68#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn format_document_default_is_idempotent() {
76 let input = r#"
77# FD v1
78
79theme accent {
80 fill: #6C5CE7
81 corner: 10
82}
83
84rect @primary_btn {
85 w: 200 h: 48
86 use: accent
87}
88"#;
89 let config = FormatConfig::default();
90 let first = format_document(input, &config).expect("first format failed");
91 let second = format_document(&first, &config).expect("second format failed");
92 assert_eq!(first, second, "format must be idempotent");
93 }
94
95 #[test]
96 fn format_document_dedupes_use_styles() {
97 let input = r#"
98theme card {
99 fill: #FFF
100}
101rect @box {
102 w: 100 h: 50
103 use: card
104 use: card
105}
106"#;
107 let config = FormatConfig::default();
108 let output = format_document(input, &config).expect("format failed");
109 let use_count = output.matches("use: card").count();
111 assert_eq!(use_count, 1, "duplicate use: should be removed");
112 }
113
114 #[test]
115 fn format_document_preserves_comments() {
116 let input = r#"
117# This is a section header
118rect @box {
119 w: 100 h: 50
120 fill: #FF0000
121}
122"#;
123 let config = FormatConfig::default();
124 let output = format_document(input, &config).expect("format failed");
125 assert!(
126 output.contains("# This is a section header"),
127 "comments must survive formatting"
128 );
129 }
130
131 #[test]
132 fn format_document_sorts_nodes_by_kind() {
133 let input = r#"
134text @label "World" {
135 font: "Inter" regular 14
136}
137rect @box {
138 w: 100 h: 50
139}
140group @wrapper {
141 rect @child {
142 w: 50 h: 50
143 }
144}
145"#;
146 let config = FormatConfig::default();
147 let output = format_document(input, &config).expect("format failed");
148 let group_pos = output.find("group @wrapper").expect("group not found");
150 let rect_pos = output.find("rect @box").expect("rect not found");
151 let text_pos = output.find("text @label").expect("text not found");
152 assert!(
153 group_pos < rect_pos,
154 "group should come before rect in formatted output"
155 );
156 assert!(
157 rect_pos < text_pos,
158 "rect should come before text in formatted output"
159 );
160 }
161
162 #[test]
163 fn format_document_sort_is_idempotent() {
164 let input = r#"
165text @label "Hello" {
166 font: "Inter" regular 14
167}
168ellipse @circle {
169 w: 60 h: 60
170}
171rect @box {
172 w: 100 h: 50
173}
174group @container {
175 rect @inner {
176 w: 50 h: 50
177 }
178}
179"#;
180 let config = FormatConfig::default();
181 let first = format_document(input, &config).expect("first format failed");
182 let second = format_document(&first, &config).expect("second format failed");
183 assert_eq!(first, second, "sort + format must be idempotent");
184 }
185}