Current section
Files
Jump to
Current section
Files
src/kdleam/serialize.gleam
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
import kdleam/document.{
type KdlDocument, type KdlEntry, type KdlNode, type KdlValue,
}
import kdleam/scanner
@internal
pub fn serialize(doc: KdlDocument) -> String {
let document.KdlDocument(nodes: nodes) = doc
nodes |> list.map(write_node(_, 0)) |> string.join("")
}
fn write_node(node: KdlNode, depth: Int) -> String {
let document.KdlNode(ty: ty, name: name, entries: entries, children: children) =
node
let head =
scanner.repeat(" ", depth)
<> write_type(ty)
<> write_identifier_or_string(name)
let entry_text =
entries
|> list.map(fn(entry) { " " <> write_entry(entry) })
|> string.join("")
case children {
Some(kids) ->
head
<> entry_text
<> " {\n"
<> { kids |> list.map(write_node(_, depth + 1)) |> string.join("") }
<> scanner.repeat(" ", depth)
<> "}\n"
_ -> head <> entry_text <> "\n"
}
}
fn write_entry(entry: KdlEntry) -> String {
case entry {
document.KdlArgument(ty: ty, value: value) ->
write_type(ty) <> write_value(value)
document.KdlProperty(key: key, ty: ty, value: value) ->
write_identifier_or_string(key)
<> "="
<> write_type(ty)
<> write_value(value)
}
}
fn write_type(ty: Option(String)) -> String {
case ty {
Some(ty) -> "(" <> write_identifier_or_string(ty) <> ")"
None -> ""
}
}
@internal
pub fn write_value(value: KdlValue) -> String {
case value {
document.KdlString(value) -> write_quoted_string(value)
document.KdlNumberValue(number) -> document.number_raw(number)
document.KdlBool(True) -> "#true"
document.KdlBool(False) -> "#false"
document.KdlNull -> "#null"
}
}
@internal
pub fn write_identifier_or_string(value: String) -> String {
case can_be_bare_identifier(value) {
True -> value
False -> write_quoted_string(value)
}
}
@internal
pub fn write_quoted_string(value: String) -> String {
"\""
<> {
value |> string.to_graphemes |> list.map(escape_char) |> string.join("")
}
<> "\""
}
fn escape_char(ch: String) -> String {
case ch {
"\"" -> "\\\""
"\\" -> "\\\\"
"\n" -> "\\n"
"\r" -> "\\r"
"\t" -> "\\t"
"\u{0008}" -> "\\b"
"\u{000C}" -> "\\f"
c ->
case scanner.is_newline(c) || scanner.is_disallowed(c) {
True -> "\\u{" <> { scanner.to_upper_hex(scanner.codepoint(c)) } <> "}"
False -> c
}
}
}
@internal
pub fn can_be_bare_identifier(value: String) -> Bool {
scanner.can_be_bare(value)
}