Skip to main content

yog_data/
patch.rs

1//! Editing Yog Data text in place: [`patch`] changes a document to hold a
2//! new value and keeps everything that didn't change as it was — comments,
3//! key order, number spelling, layout. What changed is written by
4//! [`format`], the way Yog's editors write a value.
5
6use crate::text::{Spans, write_string};
7use crate::value::{Map, Value};
8
9/// A line no longer than this is written on one line by [`format`].
10const ONE_LINE: usize = 72;
11/// One indent step.
12const INDENT: &str = "  ";
13/// Numbers are written to this many decimals.
14const DECIMALS: f64 = 10_000.0;
15
16/// `value` as Yog's editors write it at indent `level` (two spaces each):
17/// short lists of numbers and short objects of scalars on one line
18/// (`"uv": [0, 0, 16, 16]`), the rest one entry a line.
19pub fn format(value: &Value, level: usize) -> String {
20    if let Some(one) = inline(value) {
21        return one;
22    }
23    let pad = INDENT.repeat(level + 1);
24    let end = INDENT.repeat(level);
25    match value {
26        Value::List(items) => {
27            let body: Vec<String> = items
28                .iter()
29                .map(|v| format!("{pad}{}", format(v, level + 1)))
30                .collect();
31            format!("[\n{}\n{end}]", body.join(",\n"))
32        }
33        Value::Map(map) => {
34            let body: Vec<String> = map
35                .iter()
36                .map(|(k, v)| format!("{pad}{}: {}", quoted(k), format(v, level + 1)))
37                .collect();
38            format!("{{\n{}\n{end}}}", body.join(",\n"))
39        }
40        other => scalar(other).unwrap_or_default(),
41    }
42}
43
44fn quoted(s: &str) -> String {
45    let mut out = String::with_capacity(s.len() + 2);
46    write_string(s, &mut out);
47    out
48}
49
50fn number(n: f64) -> String {
51    let rounded = (n * DECIMALS).round() / DECIMALS;
52    if rounded.fract() == 0.0 && rounded.abs() < 1e15 {
53        format!("{}", rounded as i64)
54    } else {
55        rounded.to_string()
56    }
57}
58
59fn scalar(v: &Value) -> Option<String> {
60    match v {
61        Value::Null => Some("null".into()),
62        Value::Bool(b) => Some(b.to_string()),
63        Value::Int(n) => Some(n.to_string()),
64        Value::Float(f) if f.is_finite() => Some(number(*f)),
65        Value::Float(_) => Some("null".into()),
66        Value::List(_) | Value::Map(_) => None,
67        // As the source has it: a typed value (`@item:…`) or an escaped `@@`, unread.
68        Value::Str(s) => Some(quoted(s)),
69        other => Some(crate::text::to_string(other)),
70    }
71}
72
73fn inline(v: &Value) -> Option<String> {
74    if let Some(s) = scalar(v) {
75        return Some(s);
76    }
77    let out = match v {
78        Value::List(items) if items.is_empty() => return Some("[]".into()),
79        Value::List(items) => {
80            if !items
81                .iter()
82                .all(|x| matches!(x, Value::Int(_) | Value::Float(_) | Value::Bool(_)))
83            {
84                return None;
85            }
86            format!(
87                "[{}]",
88                items
89                    .iter()
90                    .filter_map(scalar)
91                    .collect::<Vec<_>>()
92                    .join(", ")
93            )
94        }
95        Value::Map(map) if map.is_empty() => return Some("{}".into()),
96        Value::Map(map) => {
97            let mut parts = Vec::with_capacity(map.len());
98            for (k, x) in map.iter() {
99                if matches!(x, Value::Map(_)) {
100                    return None;
101                }
102                parts.push(format!("{}: {}", quoted(k), inline(x)?));
103            }
104            format!("{{ {} }}", parts.join(", "))
105        }
106        _ => return None,
107    };
108    (out.len() <= ONE_LINE).then_some(out)
109}
110
111/// `value` as compact JSON for a tool: maps in their order, strings as the
112/// source has them (a typed value's text unread).
113pub fn to_json(value: &Value) -> String {
114    let mut out = String::new();
115    json_into(value, &mut out);
116    out
117}
118
119fn json_into(value: &Value, out: &mut String) {
120    match value {
121        Value::List(items) => {
122            out.push('[');
123            for (i, v) in items.iter().enumerate() {
124                if i > 0 {
125                    out.push(',');
126                }
127                json_into(v, out);
128            }
129            out.push(']');
130        }
131        Value::Map(map) => {
132            out.push('{');
133            for (i, (k, v)) in map.iter().enumerate() {
134                if i > 0 {
135                    out.push(',');
136                }
137                write_string(k, out);
138                out.push(':');
139                json_into(v, out);
140            }
141            out.push('}');
142        }
143        Value::Str(s) => write_string(s, out),
144        Value::Float(f) if f.is_finite() => out.push_str(&f.to_string()),
145        Value::Float(_) => out.push_str("null"),
146        other => out.push_str(&crate::text::to_string(other)),
147    }
148}
149
150/// Whether two values are the same, a number however it's spelled (`1` = `1.0`).
151pub fn same(a: &Value, b: &Value) -> bool {
152    match (a, b) {
153        (Value::Int(_) | Value::Float(_), Value::Int(_) | Value::Float(_)) => {
154            a.as_f64() == b.as_f64()
155        }
156        (Value::List(x), Value::List(y)) => {
157            x.len() == y.len() && x.iter().zip(y).all(|(a, b)| same(a, b))
158        }
159        (Value::Map(x), Value::Map(y)) => {
160            x.len() == y.len()
161                && x.iter()
162                    .zip(y.iter())
163                    .all(|((ka, a), (kb, b))| ka == kb && same(a, b))
164        }
165        _ => a == b,
166    }
167}
168
169/// `text` changed to hold `next`, the rest as it was. Maps and lists are
170/// edited entry by entry (a kept entry keeps the comments before it; a
171/// removed one takes them along). Text that doesn't parse is written anew.
172pub fn patch(text: &str, next: &Value) -> String {
173    let Ok(doc) = crate::text::parse(text) else {
174        return format!("{}\n", format(next, 0));
175    };
176    let (start, end) = range(&doc.spans);
177    format!(
178        "{}{}{}",
179        &text[..start],
180        rewrite(text, &doc.value, &doc.spans, next),
181        &text[end..]
182    )
183}
184
185fn range(s: &Spans) -> (usize, usize) {
186    (s.span.start as usize, s.span.end as usize)
187}
188
189/// The indent of the line `at` is on.
190fn line_indent(text: &str, at: usize) -> &str {
191    let start = text[..at].rfind('\n').map_or(0, |i| i + 1);
192    let line = &text[start..at];
193    &line[..line.len() - line.trim_start_matches([' ', '\t']).len()]
194}
195
196fn level_of(indent: &str) -> usize {
197    indent
198        .chars()
199        .map(|c| if c == '\t' { INDENT.len() } else { 1 })
200        .sum::<usize>()
201        .div_ceil(INDENT.len())
202}
203
204/// Past whitespace and comments from `i`.
205fn skip_blank(text: &str, mut i: usize) -> usize {
206    let b = text.as_bytes();
207    loop {
208        match (b.get(i), b.get(i + 1)) {
209            (Some(b' ' | b'\t' | b'\n' | b'\r'), _) => i += 1,
210            (Some(b'/'), Some(b'/')) => i = text[i..].find('\n').map_or(text.len(), |n| i + n),
211            (Some(b'/'), Some(b'*')) => {
212                i = text[i + 2..]
213                    .find("*/")
214                    .map_or(text.len(), |n| i + 2 + n + 2)
215            }
216            _ => return i,
217        }
218    }
219}
220
221/// A member as rewritten: its text, then what followed it (a comment) — the comma goes between.
222struct Member {
223    body: String,
224    trail: String,
225}
226
227fn join(out: &[Member], trailing_comma: bool) -> String {
228    let mut s = String::new();
229    for (i, m) in out.iter().enumerate() {
230        s.push_str(&m.body);
231        if i + 1 < out.len() || trailing_comma {
232            s.push(',');
233        }
234        s.push_str(&m.trail);
235    }
236    s
237}
238
239/// A container's members as text: each one's lead (the comments above it)
240/// and trail (after it, up to its comma); whether the last has a comma;
241/// what's left before the bracket.
242struct Members {
243    segments: Vec<(String, String)>,
244    comma: bool,
245    tail: String,
246}
247
248fn members(text: &str, node: (usize, usize), spans: &[(usize, usize)]) -> Members {
249    let mut segments = Vec::with_capacity(spans.len());
250    let mut at = node.0 + 1;
251    let mut comma = false;
252    for &(start, end) in spans {
253        let lead = text[at..start].to_string();
254        let after = skip_blank(text, end);
255        comma = text.as_bytes().get(after) == Some(&b',');
256        // A comment on the member's own line stays with it; the rest goes to the next one.
257        let eol = text[end..].find('\n').map(|n| end + n);
258        let stop = if comma {
259            after
260        } else {
261            eol.map_or(after, |e| after.min(e))
262        };
263        let mut trail = text[end..stop].to_string();
264        at = if comma { after + 1 } else { stop };
265        if comma {
266            let eol = text[at..].find('\n').map_or(text.len(), |n| at + n);
267            let rest = &text[at..eol];
268            let t = rest.trim();
269            let comment = t.starts_with("//")
270                || (t.starts_with("/*") && t.ends_with("*/") && t.matches("*/").count() == 1);
271            if comment && eol < node.1 {
272                trail.push_str(rest);
273                at = eol;
274            }
275        }
276        segments.push((lead, trail));
277    }
278    Members {
279        segments,
280        comma,
281        tail: text[at..node.1 - 1].to_string(),
282    }
283}
284
285/// The new text of the value `old` (at `spans`) holding `next`.
286fn rewrite(text: &str, old: &Value, spans: &Spans, next: &Value) -> String {
287    let node = range(spans);
288    if same(old, next) {
289        return text[node.0..node.1].to_string();
290    }
291    let level = level_of(line_indent(text, node.0));
292    let fresh = || format(next, level);
293    if !text[node.0..node.1].contains('\n') {
294        return fresh();
295    }
296    match (old, next) {
297        (Value::Map(old), Value::Map(next))
298            if !old.is_empty() && spans.children.len() == old.len() =>
299        {
300            rewrite_map(text, node, old, spans, next)
301        }
302        (Value::List(old), Value::List(next))
303            if !old.is_empty() && spans.children.len() == old.len() =>
304        {
305            rewrite_list(text, node, old, spans, next)
306        }
307        _ => fresh(),
308    }
309}
310
311fn rewrite_map(text: &str, node: (usize, usize), old: &Map, spans: &Spans, next: &Map) -> String {
312    let keys: Vec<&str> = old.keys().collect();
313    let key_start = |c: &Spans| c.key.map_or(c.span.start, |k| k.start) as usize;
314    let ranges: Vec<(usize, usize)> = spans
315        .children
316        .iter()
317        .map(|c| (key_start(c), c.span.end as usize))
318        .collect();
319    let parts = members(text, node, &ranges);
320    let pad = line_indent(text, ranges[0].0);
321    let gone = |i: usize| i < keys.len() && next.get(keys[i]).is_none();
322    let mut out = Vec::with_capacity(next.len());
323    // The old entry last written: a new key right after it, in the place of one that's gone, is that one renamed.
324    let mut last: Option<usize> = None;
325    for (k, v) in next.iter() {
326        let after = last.map_or(0, |l| l + 1);
327        let found = keys.iter().position(|o| *o == k);
328        let renamed = found.is_none() && gone(after);
329        let Some(i) = found.or(renamed.then_some(after)) else {
330            out.push(Member {
331                body: format!("\n{pad}{}: {}", quoted(k), format(v, level_of(pad))),
332                trail: String::new(),
333            });
334            continue;
335        };
336        last = Some(i);
337        let child = &spans.children[i];
338        let value_start = child.span.start as usize;
339        let key = if renamed {
340            let key_end = child.key.map_or(value_start, |s| s.end as usize);
341            format!("{}{}", quoted(k), &text[key_end..value_start])
342        } else {
343            text[ranges[i].0..value_start].to_string()
344        };
345        let old_value = old.value_at(i).unwrap_or(&Value::Null);
346        let (lead, trail) = &parts.segments[i];
347        out.push(Member {
348            body: format!("{lead}{key}{}", rewrite(text, old_value, child, v)),
349            trail: trail.clone(),
350        });
351    }
352    format!("{{{}{}}}", join(&out, parts.comma), parts.tail)
353}
354
355fn rewrite_list(
356    text: &str,
357    node: (usize, usize),
358    old: &[Value],
359    spans: &Spans,
360    next: &[Value],
361) -> String {
362    let ranges: Vec<(usize, usize)> = spans.children.iter().map(range).collect();
363    let parts = members(text, node, &ranges);
364    let pad = line_indent(text, ranges[0].0);
365    let item = |i: usize, v: &Value| {
366        let (lead, trail) = &parts.segments[i];
367        Member {
368            body: format!("{lead}{}", rewrite(text, &old[i], &spans.children[i], v)),
369            trail: trail.clone(),
370        }
371    };
372    let out: Vec<Member> = if next.len() == old.len() {
373        next.iter().enumerate().map(|(i, v)| item(i, v)).collect()
374    } else {
375        // Added or removed items: the others keep their text.
376        let mut from = 0;
377        next.iter()
378            .map(|v| match (from..old.len()).find(|&k| same(&old[k], v)) {
379                Some(i) => {
380                    from = i + 1;
381                    item(i, v)
382                }
383                None => Member {
384                    body: format!("\n{pad}{}", format(v, level_of(pad))),
385                    trail: String::new(),
386                },
387            })
388            .collect()
389    };
390    format!("[{}{}]", join(&out, parts.comma), parts.tail)
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use crate::text::parse_value;
397
398    fn edit(text: &str, change: impl FnOnce(&mut Map)) -> String {
399        let Value::Map(mut v) = parse_value(text).unwrap() else {
400            panic!("a map")
401        };
402        change(&mut v);
403        let next = Value::Map(v);
404        let out = patch(text, &next);
405        assert!(same(&parse_value(&out).unwrap(), &next), "{out}");
406        out
407    }
408
409    const FILE: &str = r#"// A block.
410{
411  "id": "lamp", // its id
412  /* What it is. */
413  "block": {
414    "hardness": 4.0,
415    "light": 15
416  },
417  "states": {
418    "properties": {
419      // On or off.
420      "lit": ["false", "true"],
421    },
422  },
423}
424"#;
425
426    fn map_at<'a>(m: &'a mut Map, key: &str) -> &'a mut Map {
427        match m.get_mut(key) {
428            Some(Value::Map(inner)) => inner,
429            _ => panic!("no map `{key}`"),
430        }
431    }
432
433    #[test]
434    fn what_didnt_change_keeps_its_text() {
435        assert_eq!(edit(FILE, |_| {}), FILE);
436        let out = edit(FILE, |m| {
437            map_at(m, "block").insert("light", Value::Int(7));
438        });
439        assert_eq!(out, FILE.replace("\"light\": 15", "\"light\": 7"));
440    }
441
442    #[test]
443    fn a_new_key_goes_after_the_others() {
444        let out = edit(FILE, |m| {
445            map_at(m, "block").insert("sound", Value::Str("stone".into()));
446        });
447        assert!(
448            out.contains("\"light\": 15,\n    \"sound\": \"stone\"\n  },"),
449            "{out}"
450        );
451        assert!(out.contains("// its id"));
452    }
453
454    #[test]
455    fn a_removed_key_takes_its_comments() {
456        let out = edit(FILE, |m| {
457            m.remove("block");
458        });
459        assert!(!out.contains("What it is."), "{out}");
460        assert!(
461            out.contains("\"id\": \"lamp\", // its id") && out.contains("// On or off."),
462            "{out}"
463        );
464        assert_eq!(
465            edit("{\n  \"a\": 1, // one\n  \"b\": 2\n}", |m| {
466                m.remove("b");
467            }),
468            "{\n  \"a\": 1 // one\n}"
469        );
470        assert_eq!(
471            edit("{\n  \"a\": 1 // one\n}", |m| {
472                m.insert("b", Value::Int(2));
473            }),
474            "{\n  \"a\": 1, // one\n  \"b\": 2\n}"
475        );
476    }
477
478    #[test]
479    fn a_renamed_key_keeps_its_place_and_comments() {
480        let out = edit(FILE, |m| {
481            let props = map_at(map_at(m, "states"), "properties");
482            let lit = props.remove("lit").unwrap_or(Value::Null);
483            props.insert("on", lit);
484        });
485        assert!(
486            out.contains("// On or off.\n      \"on\": [\"false\", \"true\"],"),
487            "{out}"
488        );
489        let text = "{\n  a: 1,\n  b: 2,\n  c: 3\n}";
490        let mut next = Map::new();
491        for (k, v) in [("a", 1), ("x", 2), ("c", 3)] {
492            next.insert(k, Value::Int(v));
493        }
494        assert_eq!(
495            patch(text, &Value::Map(next)),
496            "{\n  a: 1,\n  \"x\": 2,\n  c: 3\n}"
497        );
498    }
499
500    #[test]
501    fn list_items_added_and_removed_keep_the_others() {
502        let text = "{\n  \"list\": [\n    // first\n    { \"a\": 1 },\n    { \"b\": 2 }\n  ]\n}";
503        let list = |m: &mut Map| match m.get_mut("list") {
504            Some(Value::List(l)) => std::mem::take(l),
505            _ => Vec::new(),
506        };
507        let out = edit(text, |m| {
508            let mut l = list(m);
509            l.push(parse_value("{ c: 3 }").unwrap());
510            m.insert("list", Value::List(l));
511        });
512        assert!(out.contains("// first"), "{out}");
513        let out = edit(text, |m| {
514            let mut l = list(m);
515            l.remove(0);
516            m.insert("list", Value::List(l));
517        });
518        assert!(
519            !out.contains("first") && out.contains("{ \"b\": 2 }"),
520            "{out}"
521        );
522    }
523
524    #[test]
525    fn number_keys_keep_their_order() {
526        let text = "{\n  \"rotation\": {\n    \"0\": [0, 0, 0], // rest\n    \"0.5\": [45, 0, 0],\n    \"1\": [0, 0, 0]\n  }\n}";
527        let out = edit(text, |m| {
528            map_at(m, "rotation").insert("0.5", parse_value("[90, 0, 0]").unwrap());
529        });
530        assert_eq!(out, text.replace("[45, 0, 0]", "[90, 0, 0]"));
531    }
532
533    #[test]
534    fn json_keeps_order_and_source_strings() {
535        let v =
536            parse_value(r#"{ "1": "@item:minecraft:stick", "0.5": [1.5, null, true] }"#).unwrap();
537        assert_eq!(
538            to_json(&v),
539            r#"{"1":"@item:minecraft:stick","0.5":[1.5,null,true]}"#
540        );
541    }
542
543    #[test]
544    fn short_values_go_on_one_line() {
545        let v =
546            parse_value(r#"{ "uv": [0, 0, 16, 16.5], "faces": { "north": { "texture": "x" } } }"#)
547                .unwrap();
548        assert_eq!(
549            format(&v, 0),
550            "{\n  \"uv\": [0, 0, 16, 16.5],\n  \"faces\": {\n    \"north\": { \"texture\": \"x\" }\n  }\n}"
551        );
552        assert_eq!(patch("{ oops", &Value::Int(1)), "1\n");
553    }
554}