///|
priv struct ValueWriter {
output : StringBuilder
work : Work
json : Bool
formatted : Bool
}
///|
fn ValueWriter::emit(
self : ValueWriter,
text : String,
) -> Unit raise ParseError {
self.work.spend(text.length())
self.output.write_string(text)
}
///|
fn ValueWriter::indent(
self : ValueWriter,
level : Int,
) -> Unit raise ParseError {
if self.formatted {
for _ in 0.. String {
if !json &&
!text.is_empty() &&
text[0] != 45 &&
numeric_digit(text[0].to_int()) < 0 &&
!["include", "true", "false", "null"]
.iter()
.any(prefix => text.has_prefix(prefix)) &&
text.iter().all(c => c != '_' && path_character(c)) {
text
} else {
text.to_json().stringify()
}
}
///|
// Decimal indices precede ordinary keys. Equal numeric spellings keep native
// ordinary hash-bucket order; their lexicographic spelling is not a tie-breaker.
fn render_numeric_key(text : String) -> String? {
if text.is_empty() {
return None
}
let out = StringBuilder()
let mut nonzero = false
for c in text {
let digit = numeric_digit(c.to_int())
if digit < 0 {
return None
}
if digit != 0 || nonzero {
nonzero = true
out.write_char((48 + digit).unsafe_to_char())
}
}
Some(if nonzero { out.to_string() } else { "0" })
}
///|
fn render_key_compare(a : String, b : String) -> Int {
match (render_numeric_key(a), render_numeric_key(b)) {
(Some(x), Some(y)) =>
if x.length() == y.length() {
java_key_compare(x, y)
} else {
x.length() - y.length()
}
(Some(_), None) => -1
(None, Some(_)) => 1
_ => java_key_compare(a, b)
}
}
///|
fn rendered_keys(
fields : Map[String, Value],
work : Work,
) -> Array[String] raise ParseError {
work.spend(fields.length())
for key in fields.keys() {
work.spend(key.length())
}
let ordered = object_iteration_keys(fields).mapi((i, key) => (key, i))
ordered.sort_by((a, b) => {
let comparison = render_key_compare(a.0, b.0)
if comparison == 0 {
a.1 - b.1
} else {
comparison
}
})
ordered.map(item => item.0)
}
///|
fn render_reference(
parts : Array[String],
optional : Bool,
expansion : Bool,
) -> String raise ParseError {
let out = StringBuilder()
out.write_string("${")
if optional {
out.write_string("?")
}
out.write_string(join_path(parts))
if expansion {
out.write_string("[]")
}
out.write_string("}")
out.to_string()
}
///|
fn write_value(
writer : ValueWriter,
value : Value,
depth : Int,
level : Int,
root : Bool,
key : String?,
) -> Unit raise ParseError {
tree_step(value, depth, writer.work)
if value is Bound(_, _, body) {
write_value(writer, body, depth + 1, level, root, key)
return
}
if value is DelayedMerge(_, _) {
let stack = []
merge_stack(value, stack, depth, writer.work)
for i = stack.length() - 1; i >= 0; i = i - 1 {
writer.indent(level)
if key is Some(name) {
writer.emit(render_string(name, true))
writer.emit(if writer.formatted { " : " } else { ":" })
}
write_value(writer, stack[i], depth + 1, level, root, None)
if i > 0 {
writer.emit(",")
}
if writer.formatted {
writer.emit("\n")
}
}
return
}
if key is Some(name) {
writer.emit(render_string(name, writer.json))
writer.emit(
if writer.json {
if writer.formatted {
" : "
} else {
":"
}
} else if value is (Object(_) | SealedObject(_)) {
if writer.formatted {
" "
} else {
""
}
} else {
"="
},
)
}
match value {
Text(text) => writer.emit(render_string(text, writer.json))
Bare(text) =>
writer.emit(if writer.json { render_string(text, true) } else { text })
Number(_) => writer.emit(render_number(value))
Null => writer.emit("null")
Boolean(value) => writer.emit(if value { "true" } else { "false" })
Reference(path) =>
writer.emit(render_reference(split_path(path), false, false))
PathReference(parts, optional) =>
writer.emit(render_reference(parts, optional, false))
Substitution(paths, optional, expansion, _) => {
guard paths.get(0) is Some(parts) else {
raise Invalid("empty substitution paths")
}
writer.emit(render_reference(parts, optional, expansion))
}
Concat(parts) =>
for (gap, child) in parts {
if !gap.is_empty() {
write_value(writer, Bare(gap), depth + 1, level, root, None)
}
write_value(writer, child, depth + 1, level, root, None)
}
List(items) => {
writer.emit("[")
if !items.is_empty() {
if writer.formatted {
writer.emit("\n")
}
for i, child in items {
if i > 0 {
writer.emit(if writer.formatted { ",\n" } else { "," })
}
writer.indent(level + 1)
write_value(writer, child, depth + 1, level + 1, false, None)
}
if writer.formatted {
writer.emit("\n")
writer.indent(level)
}
}
writer.emit("]")
}
Object(fields) | SealedObject(fields) => {
if fields.is_empty() {
writer.emit("{}")
} else {
let braces = writer.json || !root
let inner = if braces { level + 1 } else { level }
if braces {
writer.emit(if writer.formatted { "{\n" } else { "{" })
}
for i, name in rendered_keys(fields, writer.work) {
if i > 0 {
writer.emit(
if !writer.formatted {
","
} else if writer.json {
",\n"
} else {
"\n"
},
)
}
writer.indent(inner)
write_value(writer, fields[name], depth + 1, inner, false, Some(name))
}
if braces {
if writer.formatted {
writer.emit("\n")
writer.indent(level)
}
writer.emit("}")
}
}
if root && writer.formatted {
writer.emit("\n")
}
}
Bound(_, _, _) | DelayedMerge(_, _) => ()
}
}
///|
/// Render resolved or unresolved values with comments and origin annotations
/// omitted. JSON mode can contain substitutions or repeated unresolved fields;
/// it is valid JSON only for resolved finite values.
pub fn render_value(
value : Value,
json? : Bool = true,
formatted? : Bool = false,
) -> String raise ParseError {
let writer = { output: StringBuilder(), work: tree_work(), json, formatted, }
write_value(writer, value, 0, 0, true, None)
writer.output.to_string()
}