// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Write a tree as strict JSON, whatever dialect it was read from.
///
/// `indent` of zero, the default, writes the compact form with no space
/// anywhere; anything larger writes one line per member with that many spaces a
/// level, and a space after each colon.
///
/// A number that came in as text goes back out as that text, so a document read
/// and written again is unchanged. Only a number with no such text is formatted,
/// and a number that is not finite โ which JSON5 can produce and JSON cannot
/// write โ becomes `null`, as every JavaScript implementation does.
pub fn dumps(
value : Json,
indent? : Int = 0,
ascii? : Bool = false,
sort? : Bool = false,
) -> String {
let out = StringBuilder()
put(out, value, indent, 0, ascii, sort)
out.to_string()
}
///|
/// The same, as UTF-8 bytes โ the form a body goes onto a wire in.
pub fn dump(
value : Json,
indent? : Int = 0,
ascii? : Bool = false,
sort? : Bool = false,
) -> Bytes {
@utf8.encode(dumps(value, indent~, ascii~, sort~)[:])
}
///|
fn put(
out : StringBuilder,
value : Json,
indent : Int,
level : Int,
ascii : Bool,
sort : Bool,
) -> Unit {
match value {
Null => out.write_string("null")
True => out.write_string("true")
False => out.write_string("false")
Number(n, repr~) =>
match repr {
Some(text) => out.write_string(text)
None =>
if n.is_nan() || n.is_inf() {
out.write_string("null")
} else {
out.write_string(n.to_string())
}
}
String(s) => quote(out, s[:], ascii)
Array(items) => {
if items.length() == 0 {
out.write_string("[]")
return
}
out.write_char('[')
for k, item in items {
if k > 0 {
out.write_char(',')
}
newline(out, indent, level + 1)
put(out, item, indent, level + 1, ascii, sort)
}
newline(out, indent, level)
out.write_char(']')
}
Object(members) => {
if members.length() == 0 {
out.write_string("{}")
return
}
out.write_char('{')
let names : Array[String] = []
for name, _ in members {
names.push(name)
}
if sort {
names.sort()
}
let mut first = true
for name in names {
if !first {
out.write_char(',')
}
first = false
newline(out, indent, level + 1)
quote(out, name[:], ascii)
out.write_char(':')
if indent > 0 {
out.write_char(' ')
}
put(out, members.get(name).unwrap(), indent, level + 1, ascii, sort)
}
newline(out, indent, level)
out.write_char('}')
}
}
}
///|
fn newline(out : StringBuilder, indent : Int, level : Int) -> Unit {
if indent > 0 {
out.write_char('\n')
for _ in 0..<(indent * level) {
out.write_char(' ')
}
}
}
///|
/// A string as RFC 8259 ยง7 writes one.
///
/// Only what must be escaped is: the quote, the backslash, and the control
/// characters. The two-character forms are used where they exist because they
/// are shorter and what a reader expects to see; everything else below U+0020
/// goes out as `\u00XX`.
fn quote(out : StringBuilder, s : StringView, ascii : Bool) -> Unit {
out.write_char('"')
let mut run = 0
for i in 0.. "\\\""
0x5C => "\\\\"
0x08 => "\\b"
0x0C => "\\f"
0x0A => "\\n"
0x0D => "\\r"
0x09 => "\\t"
_ => if c < 0x20 || (ascii && c > 0x7E) { hex_escape(c) } else { "" }
}
if escape != "" {
out.write_view(s[run:i])
out.write_string(escape)
run = i + 1
}
}
out.write_view(s[run:])
out.write_char('"')
}
///|
fn hex_escape(c : Int) -> String {
let digits = "0123456789abcdef"
let out = StringBuilder()
out.write_string("\\u")
for shift in [12, 8, 4, 0] {
out.write_char(digits.get_char((c >> shift) & 0xF).unwrap())
}
out.to_string()
}