1use std::fmt::Write as _;
7
8use super::model::{Asset, Block, Comment, Document, Image, ImageData, Label, Pin, Route, Text};
9
10const INDENT: &str = " ";
11
12pub fn encode(doc: &Document) -> String {
13 let mut s = String::new();
14 let _ = writeln!(s, "version {}", doc.version);
15 if let Some(name) = &doc.name {
16 let _ = writeln!(s, "name {}", quote(name));
17 }
18 let _ = writeln!(s, "top {}", quote(&doc.top));
19 for b in &doc.blocks {
20 s.push('\n');
21 block(&mut s, b);
22 }
23 for a in &doc.assets {
26 s.push('\n');
27 asset(&mut s, a);
28 }
29 s
30}
31
32fn block(s: &mut String, b: &Block) {
33 let mut head = format!(
34 "block {} x={} y={} w={} h={}",
35 quote(&b.id),
36 b.x,
37 b.y,
38 b.w,
39 b.h
40 );
41 if let Some(r) = b.role {
42 let _ = write!(head, " role={r}");
43 }
44 if b.locked {
45 head.push_str(" locked=true");
46 }
47
48 let mut body = String::new();
49 if let Some(l) = &b.title {
50 label(&mut body, 1, "title", l);
51 }
52 if let Some(l) = &b.type_label {
53 label(&mut body, 1, "type", l);
54 }
55 for p in &b.pins {
56 pin(&mut body, 1, p);
57 }
58 for r in &b.routes {
59 route(&mut body, 1, r);
60 }
61 for t in &b.texts {
62 text(&mut body, 1, t);
63 }
64 for c in &b.comments {
65 comment(&mut body, 1, c);
66 }
67 for sym in &b.images {
68 image(&mut body, 1, sym);
69 }
70 if let Some(icon) = &b.icon {
71 image_node(&mut body, 1, "icon", icon);
72 }
73 if !b.children.is_empty() {
74 indent(&mut body, 1);
75 body.push_str("children");
76 for cid in &b.children {
77 let _ = write!(body, " {}", quote(cid));
78 }
79 body.push('\n');
80 }
81
82 wrap(s, 0, &head, &body);
83}
84
85fn label(s: &mut String, level: usize, node: &str, l: &Label) {
86 let mut line = node.to_string();
87 if !l.name.is_empty() {
88 let _ = write!(line, " {}", quote(&l.name));
89 }
90 if let Some(side) = l.side {
91 let _ = write!(line, " side={}", quote(side.as_str()));
92 }
93 if l.offset != 0.0 {
94 let _ = write!(line, " offset={}", fnum(l.offset));
95 }
96 if l.hidden {
97 line.push_str(" hidden=true");
98 }
99 indent(s, level);
100 let _ = writeln!(s, "{line}");
101}
102
103fn pin(s: &mut String, level: usize, p: &Pin) {
104 let mut line = format!("pin {}", quote(&p.id));
106 if !p.name.is_empty() {
107 let _ = write!(line, " {}", quote(&p.name));
108 }
109 if let Some(loc) = &p.loc {
110 let _ = write!(line, " loc={}", quote(loc));
111 }
112 if let Some(x) = p.x {
113 let _ = write!(line, " x={x}");
114 }
115 if let Some(y) = p.y {
116 let _ = write!(line, " y={y}");
117 }
118 if let Some(w) = p.w {
119 let _ = write!(line, " w={w}");
120 }
121 if let Some(dir) = p.dir {
122 let _ = write!(line, " dir={}", quote(dir.as_str()));
123 }
124 if !p.type_label.is_empty() {
125 let _ = write!(line, " type={}", quote(&p.type_label));
126 }
127 if !p.tag.is_empty() {
128 let _ = write!(line, " tag={}", quote(&p.tag));
129 }
130 if p.tag_hidden {
131 line.push_str(" tag-hidden=true");
132 }
133 if let Some(a) = p.pin_accent {
134 let _ = write!(line, " pin-accent={a}");
135 }
136 if let Some(a) = p.port_accent {
137 let _ = write!(line, " port-accent={a}");
138 }
139 if let Some(a) = p.port_pin_accent {
140 let _ = write!(line, " port-pin-accent={a}");
141 }
142 if p.fliplr {
143 line.push_str(" fliplr=true");
144 }
145 indent(s, level);
146 let _ = writeln!(s, "{line}");
147}
148
149fn route(s: &mut String, level: usize, r: &Route) {
150 let mut head = format!("route {} {}", quote(&r.from), quote(&r.to));
152 if !r.name.is_empty() {
153 let _ = write!(head, " name={}", quote(&r.name));
154 }
155 if let Some(role) = r.role {
156 let _ = write!(head, " role={role}");
157 }
158
159 let mut body = String::new();
160 for w in &r.waypoints {
161 indent(&mut body, level + 1);
162 let _ = write!(body, "wp {} {}", w.x, w.y);
163 if w.locked {
164 body.push_str(" locked=true");
165 }
166 body.push('\n');
167 }
168 for at in &r.labels {
169 indent(&mut body, level + 1);
170 let _ = writeln!(body, "label {}", fnum(*at));
171 }
172 wrap(s, level, &head, &body);
173}
174
175fn text(s: &mut String, level: usize, t: &Text) {
176 let mut line = format!("text {}", quote(&t.text));
177 if t.x != 0 {
178 let _ = write!(line, " x={}", t.x);
179 }
180 if t.y != 0 {
181 let _ = write!(line, " y={}", t.y);
182 }
183 if let Some(role) = t.role {
184 let _ = write!(line, " role={role}");
185 }
186 indent(s, level);
187 let _ = writeln!(s, "{line}");
188}
189
190fn comment(s: &mut String, level: usize, c: &Comment) {
191 let mut head = format!("comment x={} y={} w={} h={}", c.x, c.y, c.w, c.h);
192 if let Some(role) = c.role {
193 let _ = write!(head, " role={role}");
194 }
195 let mut body = String::new();
196 if let Some(l) = &c.title {
197 label(&mut body, level + 1, "title", l);
198 }
199 wrap(s, level, &head, &body);
200}
201
202fn image(s: &mut String, level: usize, sym: &Image) {
203 image_node(s, level, "image", sym);
204}
205
206fn image_node(s: &mut String, level: usize, tag: &str, sym: &Image) {
209 indent(s, level);
210 let _ = writeln!(
211 s,
212 "{} {} x={} y={} w={} h={}",
213 tag,
214 quote(&sym.asset),
215 sym.x,
216 sym.y,
217 sym.w,
218 sym.h
219 );
220}
221
222fn asset(s: &mut String, a: &Asset) {
224 let head = format!("asset {}", quote(&a.id));
225 let mut body = String::new();
226 indent(&mut body, 1);
227 match &a.image {
228 ImageData::Svg(svg) => {
229 let _ = writeln!(body, "svg {}", raw_string(svg));
230 }
231 ImageData::Png(png) => {
232 let _ = writeln!(body, "png {}", quote(png));
233 }
234 }
235 wrap(s, 0, &head, &body);
236}
237
238fn wrap(s: &mut String, level: usize, head: &str, body: &str) {
241 indent(s, level);
242 if body.is_empty() {
243 let _ = writeln!(s, "{head}");
244 } else {
245 let _ = writeln!(s, "{head} {{");
246 s.push_str(body);
247 indent(s, level);
248 s.push_str("}\n");
249 }
250}
251
252fn indent(s: &mut String, level: usize) {
253 for _ in 0..level {
254 s.push_str(INDENT);
255 }
256}
257
258fn fnum(v: f32) -> String {
261 if v.is_finite() && v == v.trunc() {
262 format!("{v:.1}")
263 } else {
264 format!("{v}")
265 }
266}
267
268fn quote(s: &str) -> String {
269 let mut o = String::with_capacity(s.len() + 2);
270 o.push('"');
271 for c in s.chars() {
272 match c {
273 '"' => o.push_str("\\\""),
274 '\\' => o.push_str("\\\\"),
275 '\n' => o.push_str("\\n"),
276 '\r' => o.push_str("\\r"),
277 '\t' => o.push_str("\\t"),
278 _ => o.push(c),
279 }
280 }
281 o.push('"');
282 o
283}
284
285fn raw_string(s: &str) -> String {
288 let bytes = s.as_bytes();
289 let mut max_hashes = 0usize;
290 let mut i = 0;
291 while i < bytes.len() {
292 if bytes[i] == b'"' {
293 let mut j = i + 1;
294 while j < bytes.len() && bytes[j] == b'#' {
295 j += 1;
296 }
297 max_hashes = max_hashes.max(j - i - 1);
298 i = j;
299 } else {
300 i += 1;
301 }
302 }
303 let fence = "#".repeat(max_hashes + 1);
304 format!("r{fence}\"{s}\"{fence}")
305}