///|
/// Serializes a `Table` back into TOML source text.
///
/// The output preserves insertion order, quotes keys that are not valid bare
/// keys, and emits nested tables as `[table]` headers and arrays of tables as
/// `[[table]]` headers. Round-tripping `parse` -> `serialize` -> `parse`
/// preserves the value structure.
///|
/// Serializes a table into TOML text.
pub fn serialize(table : Table) -> String {
let lines : Array[String] = []
write_table(table, "", lines)
join_lines(lines)
}
///|
fn join_lines(lines : Array[String]) -> String {
join(lines, "\n")
}
///|
fn join(strings : Array[String], sep : String) -> String {
let mut result = ""
let mut first = true
for s in strings {
if !first {
result = result + sep
}
result = result + s
first = false
}
result
}
///|
/// Writes the contents of `table` as key-value lines plus nested-table
/// headers, using `prefix` as the dotted key path to this table.
fn write_table(table : Table, prefix : String, out : Array[String]) -> Unit {
for key, value in table {
let qkey = serialize_key_segment(key)
let child = if prefix == "" { qkey } else { prefix + "." + qkey }
match value {
Table(t) => {
out.push("")
out.push("[" + child + "]")
write_table(t, child, out)
}
Array(arr) if all_tables(arr) =>
for elem in arr {
match elem {
Table(t) => {
out.push("")
out.push("[[" + child + "]]")
write_table(t, child, out)
}
_ => out.push(qkey + " = " + serialize_value(elem))
}
}
_ => out.push(qkey + " = " + serialize_value(value))
}
}
}
///|
fn all_tables(arr : Array[Value]) -> Bool {
if arr.length() == 0 {
return false
}
for v in arr {
match v {
Table(_) => continue
_ => return false
}
}
true
}
///|
/// Serializes a single value to TOML source.
fn serialize_value(value : Value) -> String {
match value {
String(s) => "\"" + escape_basic_string(s) + "\""
Integer(i) => i.to_string()
Float(f) => format_float(f)
Boolean(b) => if b { "true" } else { "false" }
OffsetDateTime(dt) =>
format_date(dt.date) +
"T" +
format_time(dt.time) +
format_offset(dt.offset_minutes)
LocalDateTime(dt) => format_date(dt.date) + "T" + format_time(dt.time)
LocalDate(d) => format_date(d)
LocalTime(t) => format_time(t)
Array(arr) => "[" + join(arr.map(serialize_value), ", ") + "]"
Table(t) => "{ " + serialize_inline(t) + " }"
}
}
///|
fn serialize_inline(table : Table) -> String {
let parts : Array[String] = []
for key, value in table {
parts.push(serialize_key_segment(key) + " = " + serialize_value(value))
}
join(parts, ", ")
}
///|
/// Quotes a key if it is not a valid bare key.
fn serialize_key_segment(key : String) -> String {
if is_bare_key(key) {
key
} else {
"\"" + escape_basic_string(key) + "\""
}
}
///|
fn is_bare_key(key : String) -> Bool {
if key.length() == 0 {
return false
}
for c in key.iter() {
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '-') {
return false
}
}
true
}
///|
fn escape_basic_string(s : String) -> String {
let chars : Array[Char] = []
for c in s.iter() {
match c {
'"' => {
chars.push('\\')
chars.push('"')
}
'\\' => {
chars.push('\\')
chars.push('\\')
}
'\n' => {
chars.push('\\')
chars.push('n')
}
'\r' => {
chars.push('\\')
chars.push('r')
}
'\t' => {
chars.push('\\')
chars.push('t')
}
c if c.to_int() < 0x20 => {
chars.push('\\')
chars.push('u')
for ch in u_escape(c.to_int()).iter() {
chars.push(ch)
}
}
_ => chars.push(c)
}
}
String::from_iter(chars.iter())
}
///|
/// Formats a code point as four lowercase hex digits (for `\uXXXX`).
fn u_escape(code : Int) -> String {
let digits = "0123456789abcdef"
let d0 = (code >> 12) & 0xF
let d1 = (code >> 8) & 0xF
let d2 = (code >> 4) & 0xF
let d3 = code & 0xF
digits.substring(start=d0, end=d0 + 1) +
digits.substring(start=d1, end=d1 + 1) +
digits.substring(start=d2, end=d2 + 1) +
digits.substring(start=d3, end=d3 + 1)
}
///|
fn format_float(f : Double) -> String {
if f.is_inf() {
if f > 0.0 {
"inf"
} else {
"-inf"
}
} else if f.is_nan() {
"nan"
} else {
let s = f.to_string()
if s.contains(".") || s.contains("e") || s.contains("E") {
s
} else {
s + ".0"
}
}
}
///|
fn pad(n : Int, width : Int) -> String {
let s = n.to_string()
let mut result = s
while result.length() < width {
result = "0" + result
}
result
}
///|
fn format_date(d : LocalDate) -> String {
pad(d.year, 4) + "-" + pad(d.month, 2) + "-" + pad(d.day, 2)
}
///|
fn format_time(t : LocalTime) -> String {
let base = pad(t.hour, 2) + ":" + pad(t.minute, 2) + ":" + pad(t.second, 2)
if t.nanosecond == 0 {
base
} else {
base + "." + format_frac(t.nanosecond)
}
}
///|
fn format_frac(nanos : Int) -> String {
let s = pad(nanos, 9)
let mut i = s.length()
while i > 1 && s.substring(start=i - 1, end=i) == "0" {
i -= 1
}
s.substring(start=0, end=i)
}
///|
fn format_offset(minutes : Int) -> String {
if minutes == 0 {
"Z"
} else {
let sign = if minutes < 0 { "-" } else { "+" }
let abs = if minutes < 0 { -minutes } else { minutes }
sign + pad(abs / 60, 2) + ":" + pad(abs % 60, 2)
}
}