Skip to main content

blockworx/schema/
encode.rs

1//! Hand-rolled KDL emitter for the schema — it walks the [`super::model`] structs
2//! (the same ones the parser/walker in [`super::decode`] produce, keeping the two
3//! in sync). Output is minimal: fields at their default are omitted, enums render
4//! as kebab-case identifiers, and verbatim SVG goes out as a raw string.
5
6use 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    // Assets last: the payloads are bulky and the diagram is what a reader (and a
24    // diff) wants first.
25    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    // The `type` label and the I/O `kind` are distinct fields; see `PinPort`.
105    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    // Anchors are the two positional arguments: `route "b2:p1" "b3:p1"`.
151    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
206/// Emit an image placement (`image`/`icon`) named `tag`: the id of the asset it
207/// draws, then the box it fills.
208fn 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
222/// Emit an image asset: its id, and an `svg`/`png` child holding the content.
223fn 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
238/// Append `head` at `level`; if `body` (already indented one level deeper) is
239/// non-empty, wrap it in `{ … }`, otherwise emit `head` as a childless node.
240fn 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
258/// Format a float so it always carries a decimal point (so it decodes back as a
259/// float, not an integer), while keeping a clean shortest form otherwise.
260fn 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
285/// Emit `s` as a KDL v1 raw string `r#"…"#`, widening the `#` fence so the
286/// payload (verbatim SVG) can contain any `"#…` run without closing early.
287fn 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}