1use crate::text::{Spans, write_string};
7use crate::value::{Map, Value};
8
9const ONE_LINE: usize = 72;
11const INDENT: &str = " ";
13const DECIMALS: f64 = 10_000.0;
15
16pub 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.iter().map(|v| format!("{pad}{}", format(v, level + 1))).collect();
28 format!("[\n{}\n{end}]", body.join(",\n"))
29 }
30 Value::Map(map) => {
31 let body: Vec<String> = map.iter().map(|(k, v)| format!("{pad}{}: {}", quoted(k), format(v, level + 1))).collect();
32 format!("{{\n{}\n{end}}}", body.join(",\n"))
33 }
34 other => scalar(other).unwrap_or_default(),
35 }
36}
37
38fn quoted(s: &str) -> String {
39 let mut out = String::with_capacity(s.len() + 2);
40 write_string(s, &mut out);
41 out
42}
43
44fn number(n: f64) -> String {
45 let rounded = (n * DECIMALS).round() / DECIMALS;
46 if rounded.fract() == 0.0 && rounded.abs() < 1e15 { format!("{}", rounded as i64) } else { rounded.to_string() }
47}
48
49fn scalar(v: &Value) -> Option<String> {
50 match v {
51 Value::Null => Some("null".into()),
52 Value::Bool(b) => Some(b.to_string()),
53 Value::Int(n) => Some(n.to_string()),
54 Value::Float(f) if f.is_finite() => Some(number(*f)),
55 Value::Float(_) => Some("null".into()),
56 Value::List(_) | Value::Map(_) => None,
57 Value::Str(s) => Some(quoted(s)),
59 other => Some(crate::text::to_string(other)),
60 }
61}
62
63fn inline(v: &Value) -> Option<String> {
64 if let Some(s) = scalar(v) {
65 return Some(s);
66 }
67 let out = match v {
68 Value::List(items) if items.is_empty() => return Some("[]".into()),
69 Value::List(items) => {
70 if !items.iter().all(|x| matches!(x, Value::Int(_) | Value::Float(_) | Value::Bool(_))) {
71 return None;
72 }
73 format!("[{}]", items.iter().filter_map(scalar).collect::<Vec<_>>().join(", "))
74 }
75 Value::Map(map) if map.is_empty() => return Some("{}".into()),
76 Value::Map(map) => {
77 let mut parts = Vec::with_capacity(map.len());
78 for (k, x) in map.iter() {
79 if matches!(x, Value::Map(_)) {
80 return None;
81 }
82 parts.push(format!("{}: {}", quoted(k), inline(x)?));
83 }
84 format!("{{ {} }}", parts.join(", "))
85 }
86 _ => return None,
87 };
88 (out.len() <= ONE_LINE).then_some(out)
89}
90
91pub fn to_json(value: &Value) -> String {
94 let mut out = String::new();
95 json_into(value, &mut out);
96 out
97}
98
99fn json_into(value: &Value, out: &mut String) {
100 match value {
101 Value::List(items) => {
102 out.push('[');
103 for (i, v) in items.iter().enumerate() {
104 if i > 0 {
105 out.push(',');
106 }
107 json_into(v, out);
108 }
109 out.push(']');
110 }
111 Value::Map(map) => {
112 out.push('{');
113 for (i, (k, v)) in map.iter().enumerate() {
114 if i > 0 {
115 out.push(',');
116 }
117 write_string(k, out);
118 out.push(':');
119 json_into(v, out);
120 }
121 out.push('}');
122 }
123 Value::Str(s) => write_string(s, out),
124 Value::Float(f) if f.is_finite() => out.push_str(&f.to_string()),
125 Value::Float(_) => out.push_str("null"),
126 other => out.push_str(&crate::text::to_string(other)),
127 }
128}
129
130pub fn same(a: &Value, b: &Value) -> bool {
132 match (a, b) {
133 (Value::Int(_) | Value::Float(_), Value::Int(_) | Value::Float(_)) => a.as_f64() == b.as_f64(),
134 (Value::List(x), Value::List(y)) => x.len() == y.len() && x.iter().zip(y).all(|(a, b)| same(a, b)),
135 (Value::Map(x), Value::Map(y)) => x.len() == y.len() && x.iter().zip(y.iter()).all(|((ka, a), (kb, b))| ka == kb && same(a, b)),
136 _ => a == b,
137 }
138}
139
140pub fn patch(text: &str, next: &Value) -> String {
144 let Ok(doc) = crate::text::parse(text) else {
145 return format!("{}\n", format(next, 0));
146 };
147 let (start, end) = range(&doc.spans);
148 format!("{}{}{}", &text[..start], rewrite(text, &doc.value, &doc.spans, next), &text[end..])
149}
150
151fn range(s: &Spans) -> (usize, usize) {
152 (s.span.start as usize, s.span.end as usize)
153}
154
155fn line_indent(text: &str, at: usize) -> &str {
157 let start = text[..at].rfind('\n').map_or(0, |i| i + 1);
158 let line = &text[start..at];
159 &line[..line.len() - line.trim_start_matches([' ', '\t']).len()]
160}
161
162fn level_of(indent: &str) -> usize {
163 indent.chars().map(|c| if c == '\t' { INDENT.len() } else { 1 }).sum::<usize>().div_ceil(INDENT.len())
164}
165
166fn skip_blank(text: &str, mut i: usize) -> usize {
168 let b = text.as_bytes();
169 loop {
170 match (b.get(i), b.get(i + 1)) {
171 (Some(b' ' | b'\t' | b'\n' | b'\r'), _) => i += 1,
172 (Some(b'/'), Some(b'/')) => i = text[i..].find('\n').map_or(text.len(), |n| i + n),
173 (Some(b'/'), Some(b'*')) => i = text[i + 2..].find("*/").map_or(text.len(), |n| i + 2 + n + 2),
174 _ => return i,
175 }
176 }
177}
178
179struct Member {
181 body: String,
182 trail: String,
183}
184
185fn join(out: &[Member], trailing_comma: bool) -> String {
186 let mut s = String::new();
187 for (i, m) in out.iter().enumerate() {
188 s.push_str(&m.body);
189 if i + 1 < out.len() || trailing_comma {
190 s.push(',');
191 }
192 s.push_str(&m.trail);
193 }
194 s
195}
196
197struct Members {
201 segments: Vec<(String, String)>,
202 comma: bool,
203 tail: String,
204}
205
206fn members(text: &str, node: (usize, usize), spans: &[(usize, usize)]) -> Members {
207 let mut segments = Vec::with_capacity(spans.len());
208 let mut at = node.0 + 1;
209 let mut comma = false;
210 for &(start, end) in spans {
211 let lead = text[at..start].to_string();
212 let after = skip_blank(text, end);
213 comma = text.as_bytes().get(after) == Some(&b',');
214 let eol = text[end..].find('\n').map(|n| end + n);
216 let stop = if comma { after } else { eol.map_or(after, |e| after.min(e)) };
217 let mut trail = text[end..stop].to_string();
218 at = if comma { after + 1 } else { stop };
219 if comma {
220 let eol = text[at..].find('\n').map_or(text.len(), |n| at + n);
221 let rest = &text[at..eol];
222 let t = rest.trim();
223 let comment = t.starts_with("//") || (t.starts_with("/*") && t.ends_with("*/") && t.matches("*/").count() == 1);
224 if comment && eol < node.1 {
225 trail.push_str(rest);
226 at = eol;
227 }
228 }
229 segments.push((lead, trail));
230 }
231 Members { segments, comma, tail: text[at..node.1 - 1].to_string() }
232}
233
234fn rewrite(text: &str, old: &Value, spans: &Spans, next: &Value) -> String {
236 let node = range(spans);
237 if same(old, next) {
238 return text[node.0..node.1].to_string();
239 }
240 let level = level_of(line_indent(text, node.0));
241 let fresh = || format(next, level);
242 if !text[node.0..node.1].contains('\n') {
243 return fresh();
244 }
245 match (old, next) {
246 (Value::Map(old), Value::Map(next)) if !old.is_empty() && spans.children.len() == old.len() => rewrite_map(text, node, old, spans, next),
247 (Value::List(old), Value::List(next)) if !old.is_empty() && spans.children.len() == old.len() => rewrite_list(text, node, old, spans, next),
248 _ => fresh(),
249 }
250}
251
252fn rewrite_map(text: &str, node: (usize, usize), old: &Map, spans: &Spans, next: &Map) -> String {
253 let keys: Vec<&str> = old.keys().collect();
254 let key_start = |c: &Spans| c.key.map_or(c.span.start, |k| k.start) as usize;
255 let ranges: Vec<(usize, usize)> = spans.children.iter().map(|c| (key_start(c), c.span.end as usize)).collect();
256 let parts = members(text, node, &ranges);
257 let pad = line_indent(text, ranges[0].0);
258 let gone = |i: usize| i < keys.len() && next.get(keys[i]).is_none();
259 let mut out = Vec::with_capacity(next.len());
260 let mut last: Option<usize> = None;
262 for (k, v) in next.iter() {
263 let after = last.map_or(0, |l| l + 1);
264 let found = keys.iter().position(|o| *o == k);
265 let renamed = found.is_none() && gone(after);
266 let Some(i) = found.or(renamed.then_some(after)) else {
267 out.push(Member { body: format!("\n{pad}{}: {}", quoted(k), format(v, level_of(pad))), trail: String::new() });
268 continue;
269 };
270 last = Some(i);
271 let child = &spans.children[i];
272 let value_start = child.span.start as usize;
273 let key = if renamed {
274 let key_end = child.key.map_or(value_start, |s| s.end as usize);
275 format!("{}{}", quoted(k), &text[key_end..value_start])
276 } else {
277 text[ranges[i].0..value_start].to_string()
278 };
279 let old_value = old.value_at(i).unwrap_or(&Value::Null);
280 let (lead, trail) = &parts.segments[i];
281 out.push(Member { body: format!("{lead}{key}{}", rewrite(text, old_value, child, v)), trail: trail.clone() });
282 }
283 format!("{{{}{}}}", join(&out, parts.comma), parts.tail)
284}
285
286fn rewrite_list(text: &str, node: (usize, usize), old: &[Value], spans: &Spans, next: &[Value]) -> String {
287 let ranges: Vec<(usize, usize)> = spans.children.iter().map(range).collect();
288 let parts = members(text, node, &ranges);
289 let pad = line_indent(text, ranges[0].0);
290 let item = |i: usize, v: &Value| {
291 let (lead, trail) = &parts.segments[i];
292 Member { body: format!("{lead}{}", rewrite(text, &old[i], &spans.children[i], v)), trail: trail.clone() }
293 };
294 let out: Vec<Member> = if next.len() == old.len() {
295 next.iter().enumerate().map(|(i, v)| item(i, v)).collect()
296 } else {
297 let mut from = 0;
299 next.iter()
300 .map(|v| match (from..old.len()).find(|&k| same(&old[k], v)) {
301 Some(i) => {
302 from = i + 1;
303 item(i, v)
304 }
305 None => Member { body: format!("\n{pad}{}", format(v, level_of(pad))), trail: String::new() },
306 })
307 .collect()
308 };
309 format!("[{}{}]", join(&out, parts.comma), parts.tail)
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::text::parse_value;
316
317 fn edit(text: &str, change: impl FnOnce(&mut Map)) -> String {
318 let Value::Map(mut v) = parse_value(text).unwrap() else { panic!("a map") };
319 change(&mut v);
320 let next = Value::Map(v);
321 let out = patch(text, &next);
322 assert!(same(&parse_value(&out).unwrap(), &next), "{out}");
323 out
324 }
325
326 const FILE: &str = r#"// A block.
327{
328 "id": "lamp", // its id
329 /* What it is. */
330 "block": {
331 "hardness": 4.0,
332 "light": 15
333 },
334 "states": {
335 "properties": {
336 // On or off.
337 "lit": ["false", "true"],
338 },
339 },
340}
341"#;
342
343 fn map_at<'a>(m: &'a mut Map, key: &str) -> &'a mut Map {
344 match m.get_mut(key) {
345 Some(Value::Map(inner)) => inner,
346 _ => panic!("no map `{key}`"),
347 }
348 }
349
350 #[test]
351 fn what_didnt_change_keeps_its_text() {
352 assert_eq!(edit(FILE, |_| {}), FILE);
353 let out = edit(FILE, |m| {
354 map_at(m, "block").insert("light", Value::Int(7));
355 });
356 assert_eq!(out, FILE.replace("\"light\": 15", "\"light\": 7"));
357 }
358
359 #[test]
360 fn a_new_key_goes_after_the_others() {
361 let out = edit(FILE, |m| {
362 map_at(m, "block").insert("sound", Value::Str("stone".into()));
363 });
364 assert!(out.contains("\"light\": 15,\n \"sound\": \"stone\"\n },"), "{out}");
365 assert!(out.contains("// its id"));
366 }
367
368 #[test]
369 fn a_removed_key_takes_its_comments() {
370 let out = edit(FILE, |m| {
371 m.remove("block");
372 });
373 assert!(!out.contains("What it is."), "{out}");
374 assert!(out.contains("\"id\": \"lamp\", // its id") && out.contains("// On or off."), "{out}");
375 assert_eq!(edit("{\n \"a\": 1, // one\n \"b\": 2\n}", |m| {
376 m.remove("b");
377 }), "{\n \"a\": 1 // one\n}");
378 assert_eq!(edit("{\n \"a\": 1 // one\n}", |m| {
379 m.insert("b", Value::Int(2));
380 }), "{\n \"a\": 1, // one\n \"b\": 2\n}");
381 }
382
383 #[test]
384 fn a_renamed_key_keeps_its_place_and_comments() {
385 let out = edit(FILE, |m| {
386 let props = map_at(map_at(m, "states"), "properties");
387 let lit = props.remove("lit").unwrap_or(Value::Null);
388 props.insert("on", lit);
389 });
390 assert!(out.contains("// On or off.\n \"on\": [\"false\", \"true\"],"), "{out}");
391 let text = "{\n a: 1,\n b: 2,\n c: 3\n}";
392 let mut next = Map::new();
393 for (k, v) in [("a", 1), ("x", 2), ("c", 3)] {
394 next.insert(k, Value::Int(v));
395 }
396 assert_eq!(patch(text, &Value::Map(next)), "{\n a: 1,\n \"x\": 2,\n c: 3\n}");
397 }
398
399 #[test]
400 fn list_items_added_and_removed_keep_the_others() {
401 let text = "{\n \"list\": [\n // first\n { \"a\": 1 },\n { \"b\": 2 }\n ]\n}";
402 let list = |m: &mut Map| match m.get_mut("list") {
403 Some(Value::List(l)) => std::mem::take(l),
404 _ => Vec::new(),
405 };
406 let out = edit(text, |m| {
407 let mut l = list(m);
408 l.push(parse_value("{ c: 3 }").unwrap());
409 m.insert("list", Value::List(l));
410 });
411 assert!(out.contains("// first"), "{out}");
412 let out = edit(text, |m| {
413 let mut l = list(m);
414 l.remove(0);
415 m.insert("list", Value::List(l));
416 });
417 assert!(!out.contains("first") && out.contains("{ \"b\": 2 }"), "{out}");
418 }
419
420 #[test]
421 fn number_keys_keep_their_order() {
422 let text = "{\n \"rotation\": {\n \"0\": [0, 0, 0], // rest\n \"0.5\": [45, 0, 0],\n \"1\": [0, 0, 0]\n }\n}";
423 let out = edit(text, |m| {
424 map_at(m, "rotation").insert("0.5", parse_value("[90, 0, 0]").unwrap());
425 });
426 assert_eq!(out, text.replace("[45, 0, 0]", "[90, 0, 0]"));
427 }
428
429 #[test]
430 fn json_keeps_order_and_source_strings() {
431 let v = parse_value(r#"{ "1": "@item:minecraft:stick", "0.5": [1.5, null, true] }"#).unwrap();
432 assert_eq!(to_json(&v), r#"{"1":"@item:minecraft:stick","0.5":[1.5,null,true]}"#);
433 }
434
435 #[test]
436 fn short_values_go_on_one_line() {
437 let v = parse_value(r#"{ "uv": [0, 0, 16, 16.5], "faces": { "north": { "texture": "x" } } }"#).unwrap();
438 assert_eq!(format(&v, 0), "{\n \"uv\": [0, 0, 16, 16.5],\n \"faces\": {\n \"north\": { \"texture\": \"x\" }\n }\n}");
439 assert_eq!(patch("{ oops", &Value::Int(1)), "1\n");
440 }
441}