///|
priv enum WriteItem {
Text(String)
Item(Value, Int32)
}
///|
fn key_compare(a : String, b : String) -> Int32 {
let n = if a.length() < b.length() { a.length() } else { b.length() }
for i in 0.. String {
let digits = "0123456789abcdef"
let out = StringBuilder()
for shift in [12, 8, 4, 0] {
out.write_char(digits[(unit >> shift) & 15].to_int().unsafe_to_char())
}
out.to_string()
}
///|
/// Minimal JSON string escaping, or None for an unpaired UTF-16 surrogate,
/// which the reader rejects and which is not Unicode text.
fn quote(text : String) -> String? {
let out = StringBuilder()
out.write_char('"')
let n = text.length()
let mut i = 0
while i < n {
let unit = text.code_unit_at(i).to_int()
if unit >= 0xD800 && unit <= 0xDFFF {
guard unit <= 0xDBFF && i + 1 < n else { return None }
let low = text.code_unit_at(i + 1).to_int()
guard low >= 0xDC00 && low <= 0xDFFF else { return None }
let point = 0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00)
out.write_char(point.unsafe_to_char())
i += 2
continue
}
match unit {
0x22 => out.write_string("\\\"")
0x5C => out.write_string("\\\\")
0x08 => out.write_string("\\b")
0x0C => out.write_string("\\f")
0x0A => out.write_string("\\n")
0x0D => out.write_string("\\r")
0x09 => out.write_string("\\t")
_ =>
if unit < 0x20 {
out.write_string("\\u" + hex4(unit))
} else {
out.write_char(unit.unsafe_to_char())
}
}
i += 1
}
out.write_char('"')
Some(out.to_string())
}
///|
/// Deterministic JSON text: object keys by UTF-16 code units, exact number
/// tokens, array order kept, no final newline. `Pretty` only adds layout.
pub fn write(
value : Value,
format~ : Format = Compact,
limits~ : Limits = Limits::new(),
) -> Result[String, Diagnostic] {
let pretty = format == Pretty
let unpaired = () => {
Diagnostic::new(
InvalidValue,
"String contains an unpaired UTF-16 surrogate and is not Unicode text",
)
}
// One shared line break per depth, built once and reused for every child.
let breaks : Array[WriteItem] = []
let newline = (depth : Int) => {
while breaks.length() <= depth {
let out = StringBuilder()
out.write_char('\n')
for _ in 0.. {
if text.length() > limits.max_source - length {
return Err(
Diagnostic::new(LimitExceeded, "JSON output size limit exceeded"),
)
}
length += text.length()
out.write_string(text)
}
Item(value, depth) => {
count += 1
if count > limits.max_nodes {
return Err(
Diagnostic::new(LimitExceeded, "JSON output node limit exceeded"),
)
}
match value.kind {
Null => stack.push(Text("null"))
Boolean(v) => stack.push(Text(if v { "true" } else { "false" }))
String(v) =>
match quote(v) {
Some(text) => stack.push(Text(text))
None => return Err(unpaired())
}
Numeric(n) => {
if n.token.length() > limits.max_number {
return Err(
Diagnostic::new(
LimitExceeded,
"JSON number token limit exceeded",
),
)
}
stack.push(Text(n.token))
}
Array(items) => {
if items.length() > limits.max_nodes - reserved {
return Err(
Diagnostic::new(
LimitExceeded,
"JSON output node limit exceeded",
),
)
}
reserved += items.length()
if depth >= limits.max_depth {
return Err(
Diagnostic::new(
LimitExceeded,
"JSON output nesting limit exceeded",
),
)
}
stack.push(Text("]"))
if pretty && !items.is_empty() {
stack.push(newline(depth))
}
for i = items.length() - 1; i >= 0; i = i - 1 {
stack.push(Item(items[i], depth + 1))
if pretty {
stack.push(newline(depth + 1))
}
if i > 0 {
stack.push(Text(","))
}
}
stack.push(Text("["))
}
Object(entries) => {
if entries.length() > limits.max_nodes - reserved {
return Err(
Diagnostic::new(
LimitExceeded,
"JSON output node limit exceeded",
),
)
}
reserved += entries.length()
if depth >= limits.max_depth {
return Err(
Diagnostic::new(
LimitExceeded,
"JSON output nesting limit exceeded",
),
)
}
let fields = entries.copy()
fields.sort_by((a, b) => key_compare(a.0, b.0))
stack.push(Text("}"))
if pretty && !fields.is_empty() {
stack.push(newline(depth))
}
for i = fields.length() - 1; i >= 0; i = i - 1 {
stack.push(Item(fields[i].1, depth + 1))
stack.push(Text(if pretty { ": " } else { ":" }))
match quote(fields[i].0) {
Some(text) => stack.push(Text(text))
None => return Err(unpaired())
}
if pretty {
stack.push(newline(depth + 1))
}
if i > 0 {
stack.push(Text(","))
}
}
stack.push(Text("{"))
}
}
}
}
}
Ok(out.to_string())
}