1use super::{LayoutNode, LayoutTreeIR};
28use crate::types::Rect;
29use std::fmt::Write;
30
31const INDENT: &str = " ";
32
33pub fn write_tree(out: &mut String, tree: &LayoutTreeIR) {
35 out.push_str("{\n");
36 write_indented(out, 1, "\"root\": ");
37 write_node(out, &tree.root, 1);
38 out.push_str(",\n");
39 write_indented(out, 1, "\"schema_version\": ");
40 let _ = writeln!(out, "{}", tree.schema_version);
41 out.push_str("}\n");
42}
43
44fn write_node(out: &mut String, node: &LayoutNode, depth: usize) {
45 out.push_str("{\n");
46
47 write_kv_array(out, depth + 1, "children", &node.children, |o, n, d| {
51 write_node(o, n, d)
52 });
53 write_kv_optional_str(
54 out,
55 depth + 1,
56 "field_kind",
57 node.field_kind.map(|f| f.tag()),
58 );
59 write_kv_optional_u64(out, depth + 1, "form_node_id", node.form_node_id);
60 write_kv_str(out, depth + 1, "id", node.id.as_str());
61 write_kv_str(out, depth + 1, "kind", node.kind.tag());
62 write_kv_str(out, depth + 1, "overflow", node.overflow.tag());
63 write_kv_optional_u32(out, depth + 1, "page_index", node.page_index);
64 write_kv_str(out, depth + 1, "presence", node.presence.tag());
65 write_kv_optional_rect(out, depth + 1, "rect", node.rect);
66 write_kv_optional_str(out, depth + 1, "som", node.som.as_deref());
67 write_kv_optional_str_last(out, depth + 1, "value_hash", node.value_hash.as_deref());
68
69 write_indent(out, depth);
70 out.push('}');
71}
72
73fn write_indent(out: &mut String, depth: usize) {
76 for _ in 0..depth {
77 out.push_str(INDENT);
78 }
79}
80
81fn write_indented(out: &mut String, depth: usize, s: &str) {
82 write_indent(out, depth);
83 out.push_str(s);
84}
85
86fn write_kv_str(out: &mut String, depth: usize, key: &str, value: &str) {
87 write_indent(out, depth);
88 out.push('"');
89 out.push_str(key);
90 out.push_str("\": ");
91 write_json_string(out, value);
92 out.push_str(",\n");
93}
94
95fn write_kv_optional_str(out: &mut String, depth: usize, key: &str, value: Option<&str>) {
96 write_indent(out, depth);
97 out.push('"');
98 out.push_str(key);
99 out.push_str("\": ");
100 match value {
101 Some(s) => write_json_string(out, s),
102 None => out.push_str("null"),
103 }
104 out.push_str(",\n");
105}
106
107fn write_kv_optional_str_last(out: &mut String, depth: usize, key: &str, value: Option<&str>) {
108 write_indent(out, depth);
109 out.push('"');
110 out.push_str(key);
111 out.push_str("\": ");
112 match value {
113 Some(s) => write_json_string(out, s),
114 None => out.push_str("null"),
115 }
116 out.push('\n');
117}
118
119fn write_kv_optional_u32(out: &mut String, depth: usize, key: &str, value: Option<u32>) {
120 write_indent(out, depth);
121 out.push('"');
122 out.push_str(key);
123 out.push_str("\": ");
124 match value {
125 Some(v) => {
126 let _ = write!(out, "{}", v);
127 }
128 None => out.push_str("null"),
129 }
130 out.push_str(",\n");
131}
132
133fn write_kv_optional_u64(out: &mut String, depth: usize, key: &str, value: Option<u64>) {
134 write_indent(out, depth);
135 out.push('"');
136 out.push_str(key);
137 out.push_str("\": ");
138 match value {
139 Some(v) => {
140 let _ = write!(out, "{}", v);
141 }
142 None => out.push_str("null"),
143 }
144 out.push_str(",\n");
145}
146
147fn write_kv_optional_rect(out: &mut String, depth: usize, key: &str, value: Option<Rect>) {
148 write_indent(out, depth);
149 out.push('"');
150 out.push_str(key);
151 out.push_str("\": ");
152 match value {
153 Some(r) => write_rect(out, r, depth),
154 None => out.push_str("null"),
155 }
156 out.push_str(",\n");
157}
158
159fn write_rect(out: &mut String, r: Rect, depth: usize) {
160 out.push_str("{\n");
161 write_indent(out, depth + 1);
163 out.push_str("\"height\": ");
164 write_float(out, r.height);
165 out.push_str(",\n");
166 write_indent(out, depth + 1);
167 out.push_str("\"width\": ");
168 write_float(out, r.width);
169 out.push_str(",\n");
170 write_indent(out, depth + 1);
171 out.push_str("\"x\": ");
172 write_float(out, r.x);
173 out.push_str(",\n");
174 write_indent(out, depth + 1);
175 out.push_str("\"y\": ");
176 write_float(out, r.y);
177 out.push('\n');
178 write_indent(out, depth);
179 out.push('}');
180}
181
182fn write_kv_array<T>(
183 out: &mut String,
184 depth: usize,
185 key: &str,
186 items: &[T],
187 write_item: impl Fn(&mut String, &T, usize),
188) {
189 write_indent(out, depth);
190 out.push('"');
191 out.push_str(key);
192 out.push_str("\": ");
193 if items.is_empty() {
194 out.push_str("[],\n");
195 return;
196 }
197 out.push_str("[\n");
198 let last = items.len() - 1;
199 for (i, item) in items.iter().enumerate() {
200 write_indent(out, depth + 1);
201 write_item(out, item, depth + 1);
202 if i != last {
203 out.push(',');
204 }
205 out.push('\n');
206 }
207 write_indent(out, depth);
208 out.push_str("],\n");
209}
210
211fn write_float(out: &mut String, v: f64) {
217 let n = if v == 0.0 { 0.0 } else { v };
218 let n = if n.is_finite() { n } else { 0.0 };
219 let _ = write!(out, "{:.4}", n);
220}
221
222fn write_json_string(out: &mut String, s: &str) {
224 out.push('"');
225 for c in s.chars() {
226 match c {
227 '"' => out.push_str("\\\""),
228 '\\' => out.push_str("\\\\"),
229 '\n' => out.push_str("\\n"),
230 '\r' => out.push_str("\\r"),
231 '\t' => out.push_str("\\t"),
232 '\u{08}' => out.push_str("\\b"),
233 '\u{0C}' => out.push_str("\\f"),
234 c if (c as u32) < 0x20 => {
235 let _ = write!(out, "\\u{:04x}", c as u32);
236 }
237 c => out.push(c),
238 }
239 }
240 out.push('"');
241}
242
243#[cfg(test)]
244mod tests {
245 use super::super::{
246 FieldKindIR, LayoutNode, LayoutNodeId, LayoutTreeIR, NodeKind, OverflowState, PresenceIR,
247 SCHEMA_VERSION,
248 };
249 use crate::types::Rect;
250
251 fn fixture() -> LayoutTreeIR {
252 let mut root = LayoutNode::new(LayoutNodeId::root(), NodeKind::Root);
253 let mut page = LayoutNode::new(root.id.child(0), NodeKind::PageArea);
254 page.page_index = Some(0);
255 page.rect = Some(Rect::new(0.0, 0.0, 612.0, 792.0));
256 let mut field = LayoutNode::new(page.id.child(0), NodeKind::Field);
257 field.field_kind = Some(FieldKindIR::Text);
258 field.presence = PresenceIR::Visible;
259 field.rect = Some(Rect::new(72.0, 100.0, 200.0, 14.4));
260 field.som = Some("form1.subform.field1".into());
261 field.value_hash = Some("abcd1234".into());
262 field.overflow = OverflowState::None;
263 field.form_node_id = Some(42);
264 page.push_child(field);
265 root.push_child(page);
266 LayoutTreeIR {
267 schema_version: SCHEMA_VERSION,
268 root,
269 }
270 }
271
272 #[test]
273 fn output_is_byte_stable() {
274 let tree = fixture();
275 let a = tree.to_canonical_json();
276 let b = tree.to_canonical_json();
277 assert_eq!(a, b);
278 }
279
280 #[test]
281 fn output_starts_and_ends_predictably() {
282 let tree = fixture();
283 let s = tree.to_canonical_json();
284 assert!(s.starts_with("{\n"));
285 assert!(s.ends_with("}\n"));
286 assert!(s.contains("\"schema_version\": 1"));
287 }
288
289 #[test]
290 fn keys_are_alphabetical_at_each_level() {
291 let tree = fixture();
292 let s = tree.to_canonical_json();
293 let ri = s.find("\"root\":").unwrap();
295 let si = s.find("\"schema_version\":").unwrap();
296 assert!(ri < si);
297 for (a, b) in [
300 ("\"children\":", "\"field_kind\":"),
301 ("\"field_kind\":", "\"form_node_id\":"),
302 ("\"form_node_id\":", "\"id\":"),
303 ("\"id\":", "\"kind\":"),
304 ("\"kind\":", "\"overflow\":"),
305 ("\"overflow\":", "\"page_index\":"),
306 ("\"page_index\":", "\"presence\":"),
307 ("\"presence\":", "\"rect\":"),
308 ("\"rect\":", "\"som\":"),
309 ("\"som\":", "\"value_hash\":"),
310 ] {
311 let ai = s.find(a).expect(a);
312 let bi = s.find(b).expect(b);
313 assert!(ai < bi, "expected {a} before {b}");
314 }
315 }
316
317 #[test]
318 fn floats_have_four_decimal_places() {
319 let tree = fixture();
320 let s = tree.to_canonical_json();
321 assert!(s.contains("\"height\": 792.0000"));
322 assert!(s.contains("\"width\": 612.0000"));
323 assert!(s.contains("\"x\": 0.0000"));
324 assert!(s.contains("\"y\": 0.0000"));
325 assert!(s.contains("\"height\": 14.4000"));
326 assert!(s.contains("\"x\": 72.0000"));
327 }
328
329 #[test]
330 fn empty_children_render_as_empty_array() {
331 let tree = LayoutTreeIR::new();
332 let s = tree.to_canonical_json();
333 assert!(s.contains("\"children\": [],"));
334 }
335
336 #[test]
337 fn missing_optional_fields_render_as_null() {
338 let tree = LayoutTreeIR::new();
339 let s = tree.to_canonical_json();
340 assert!(s.contains("\"rect\": null,"));
341 assert!(s.contains("\"som\": null,"));
342 assert!(s.contains("\"value_hash\": null"));
343 assert!(s.contains("\"page_index\": null,"));
344 assert!(s.contains("\"form_node_id\": null,"));
345 assert!(s.contains("\"field_kind\": null,"));
346 }
347
348 #[test]
349 fn negative_zero_is_normalised() {
350 let mut tree = LayoutTreeIR::new();
351 tree.root.rect = Some(Rect::new(-0.0, -0.0, 1.0, 2.0));
352 let s = tree.to_canonical_json();
353 assert!(s.contains("\"x\": 0.0000"));
354 assert!(s.contains("\"y\": 0.0000"));
355 assert!(!s.contains("-0.0000"));
356 }
357
358 #[test]
359 fn strings_are_json_escaped() {
360 let mut tree = LayoutTreeIR::new();
361 tree.root.som = Some("a\"b\\c\nd".into());
362 let s = tree.to_canonical_json();
363 assert!(s.contains("\"som\": \"a\\\"b\\\\c\\nd\""));
364 }
365}