///|
pub fn render_value_as_json(value : Value) -> String {
render_value_as_json_with_indent(value, " ")
}
///|
pub fn render_value_as_json_with_indent(
value : Value,
indent_text : String,
) -> String {
let buf = StringBuilder::new()
render_json_value(value, 0, indent_text, buf)
buf.to_string()
}
///|
pub fn render_value_as_json_document(value : Value) -> String {
render_value_as_json_document_with_indent(value, " ")
}
///|
pub fn render_value_as_json_document_with_indent(
value : Value,
indent_text : String,
) -> String {
render_value_as_json_with_indent(value, indent_text) + "\n"
}
///|
fn render_json_value(
value : Value,
indent : Int,
indent_text : String,
buf : StringBuilder,
) -> Unit {
match render_directive_text(value) {
Some(text) => {
buf.write_string(text)
return
}
None => ()
}
match value {
ThunkValue(_) =>
render_json_value(force_eval_thunk(value), indent, indent_text, buf)
IntValue(n) => buf.write_string("\{n}")
FloatValue(d) => buf.write_string(render_float_text(d))
BoolValue(true) => buf.write_string("true")
BoolValue(false) => buf.write_string("false")
NullValue => buf.write_string("null")
DeferredImportValue(_) => buf.write_string("null")
StringValue(s) => render_json_string(s, buf)
ObjectValue(members) =>
match dynamic_listing_elements_from_members(members) {
Some(elements) => render_json_array(elements, indent, indent_text, buf)
None =>
match dynamic_mapping_entries_from_members(members) {
Some(entries) =>
render_json_object_from_entries(entries, indent, indent_text, buf)
None =>
render_json_object_from_members(members, indent, indent_text, buf)
}
}
MappingValue(entries) | DefaultedMappingValue(_, entries, _) =>
render_json_object_from_entries(entries, indent, indent_text, buf)
ListingValue(elements)
| DefaultedListingValue(_, elements, _)
| ListValue(elements) =>
render_json_array(elements, indent, indent_text, buf)
FunctionValue(_, _, _, _, _) => buf.write_string("null")
// Upstream Pkl projects Duration / DataSize to a literal-style string
// (e.g. `"5.min"`) in JSON / YAML / Properties. Match that shape until
// the dedicated projection table from `pkl:base` lands.
DurationValue(n, unit) => render_json_string("\{n}.\{unit}", buf)
DataSizeValue(n, unit) => render_json_string("\{n}.\{unit}", buf)
// PKL-081: Regex projects as the pattern string in JSON / YAML /
// Properties, mirroring upstream Apple Pkl behavior for opaque
// stdlib values.
RegexValue(pattern) => render_json_string(pattern, buf)
// PKL-083: Bytes projects as its base64 encoding in non-PCF
// formats, since none of these formats have a native bytes type
// and base64 is the upstream-compatible representation.
BytesValue(bytes) => render_json_string(@base64.encode(bytes[:]), buf)
// PKL-119a: Pair renders as a 2-element JSON array. Matches
// Apple Pkl's `Pair`-to-JSON projection.
PairValue(first, second) =>
render_json_array([first, second], indent, indent_text, buf)
// PKL-119b: IntSeq materializes to an Int array first, then flows
// through the standard JSON array projection.
IntSeqValue(start, end_v, step) =>
render_json_array(
intseq_materialize(start, end_v, step),
indent,
indent_text,
buf,
)
// PKL-119c: Set renders as a JSON array. Uniqueness is already
// enforced at construction, so the array element order matches
// the upstream insertion-order semantics.
SetValue(elements) => render_json_array(elements, indent, indent_text, buf)
// PKL-119d: Map projects as a JSON object, matching the
// existing MappingValue projection. The entry-walker coerces
// non-string keys the same way `json_coerce_key` already does.
MapValue(entries) =>
render_json_object_from_entries(entries, indent, indent_text, buf)
}
}
///|
fn dynamic_listing_elements_from_members(
members : Array[ValueMember],
) -> Array[Value]? {
let elements : Array[Value] = []
for field in members {
if is_invisible_member_name(field.name) {
continue
}
if !field.name.has_prefix("@element$") {
return None
}
elements.push(force_eval_thunk(field.value))
}
if elements.length() > 0 {
Some(elements)
} else {
None
}
}
///|
fn render_json_string(value : String, buf : StringBuilder) -> Unit {
buf.write_char('"')
for c in value {
match c {
'"' => buf.write_string("\\\"")
'\\' => buf.write_string("\\\\")
'\n' => buf.write_string("\\n")
'\r' => buf.write_string("\\r")
'\t' => buf.write_string("\\t")
'\u{08}' => buf.write_string("\\b")
'\u{0C}' => buf.write_string("\\f")
_ =>
if c < ' ' {
let code = c.to_int()
let hex = json_hex4(code)
buf.write_string("\\u")
buf.write_string(hex)
} else {
buf.write_char(c)
}
}
}
buf.write_char('"')
}
///|
fn json_hex4(code : Int) -> String {
let digits = "0123456789abcdef"
let buf = StringBuilder::new()
let mut i = 12
while i >= 0 {
let nibble = (code >> i) & 0xF
buf.write_char(digits.unsafe_get(nibble).to_int().unsafe_to_char())
i = i - 4
}
buf.to_string()
}
///|
fn json_write_key(value : Value, buf : StringBuilder) -> Unit {
match render_directive_text(value) {
Some(text) => buf.write_string(text)
None => render_json_string(json_coerce_key(value), buf)
}
}
///|
fn json_coerce_key(value : Value) -> String {
match value {
StringValue(s) => s
IntValue(n) => "\{n}"
BoolValue(true) => "true"
BoolValue(false) => "false"
NullValue => "null"
_ => {
let buf = StringBuilder::new()
render_pcf_inline(value, 0, false, buf)
buf.to_string()
}
}
}
///|
fn write_json_indent(
buf : StringBuilder,
indent : Int,
indent_text : String,
) -> Unit {
for i = 0; i < indent; i = i + 1 {
buf.write_string(indent_text)
}
}
///|
fn render_json_object_from_members(
members : Array[ValueMember],
indent : Int,
indent_text : String,
buf : StringBuilder,
) -> Unit {
let visible = visible_members(members)
if visible.length() == 0 {
buf.write_string("{}")
return
}
buf.write_string("{\n")
for i = 0; i < visible.length(); i = i + 1 {
if i > 0 {
buf.write_string(",\n")
}
write_json_indent(buf, indent + 1, indent_text)
render_json_string(visible[i].name, buf)
buf.write_string(": ")
render_json_value(visible[i].value, indent + 1, indent_text, buf)
}
buf.write_char('\n')
write_json_indent(buf, indent, indent_text)
buf.write_char('}')
}
///|
fn render_json_object_from_entries(
entries : Array[ValueEntry],
indent : Int,
indent_text : String,
buf : StringBuilder,
) -> Unit {
if entries.length() == 0 {
buf.write_string("{}")
return
}
buf.write_string("{\n")
for i = 0; i < entries.length(); i = i + 1 {
if i > 0 {
buf.write_string(",\n")
}
write_json_indent(buf, indent + 1, indent_text)
json_write_key(entries[i].key, buf)
buf.write_string(": ")
render_json_value(entries[i].value, indent + 1, indent_text, buf)
}
buf.write_char('\n')
write_json_indent(buf, indent, indent_text)
buf.write_char('}')
}
///|
fn render_json_array(
elements : Array[Value],
indent : Int,
indent_text : String,
buf : StringBuilder,
) -> Unit {
if elements.length() == 0 {
buf.write_string("[]")
return
}
buf.write_string("[\n")
for i = 0; i < elements.length(); i = i + 1 {
if i > 0 {
buf.write_string(",\n")
}
write_json_indent(buf, indent + 1, indent_text)
render_json_value(elements[i], indent + 1, indent_text, buf)
}
buf.write_char('\n')
write_json_indent(buf, indent, indent_text)
buf.write_char(']')
}
///|
/// Render a Value as YAML, mirroring the basic shape of `pkl eval -f yaml`.
///
/// Block style is used throughout: mappings emit `key: value` per line,
/// listings emit `- value` per line at the same column as the parent key, and
/// empty composites use flow form (`[]` / `{}`). String scalars are bare when
/// they parse as plain YAML, single-quoted when they collide with reserved
/// shapes (numeric / keyword / leading indicator / inline `: ` / ` #`), and
/// double-quoted with `\`-escapes when they contain control characters or a
/// backslash. Block scalars (`|`, `|-`, `|+`) for multiline strings remain
/// follow-up work.
///
/// Indentation is fixed at two spaces; Apple Pkl's `indentWidth` knob lives
/// inside the `YamlRenderer` stdlib class (PKL-079 expansion), which our
/// pipeline does not consume yet.