Packages

KDL v2 parser, serializer, normalizer, and document builder for Gleam

Current section

Files

Jump to
kdleam src kdleam normalize.gleam
Raw

src/kdleam/normalize.gleam

import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/order.{type Order}
import gleam/string
import kdleam/bignum
import kdleam/document.{
type KdlDocument, type KdlEntry, type KdlNode, type KdlValue,
}
import kdleam/scanner
import kdleam/serialize
@internal
pub fn normalize(doc: KdlDocument) -> String {
let document.KdlDocument(nodes: nodes) = doc
case nodes {
[] -> "\n"
_ -> 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 #(args, props) = split_entries(entries, [], [])
let sorted_props = props |> dedupe_props([]) |> list.sort(by: compare_prop)
let entry_text =
list.append(args, sorted_props)
|> list.map(fn(entry) { " " <> write_entry(entry) })
|> string.join("")
let head =
scanner.repeat(" ", depth)
<> write_type(ty)
<> serialize.write_identifier_or_string(name)
<> entry_text
case children {
Some(kids) if kids != [] ->
head
<> " {\n"
<> { kids |> list.map(write_node(_, depth + 1)) |> string.join("") }
<> scanner.repeat(" ", depth)
<> "}\n"
_ -> head <> "\n"
}
}
fn split_entries(
entries: List(KdlEntry),
args: List(KdlEntry),
props: List(KdlEntry),
) -> #(List(KdlEntry), List(KdlEntry)) {
case entries {
[] -> #(list.reverse(args), list.reverse(props))
[entry, ..rest] ->
case entry {
document.KdlArgument(..) -> split_entries(rest, [entry, ..args], props)
document.KdlProperty(..) -> split_entries(rest, args, [entry, ..props])
}
}
}
fn dedupe_props(props: List(KdlEntry), acc: List(KdlEntry)) -> List(KdlEntry) {
case props {
[] -> acc
[prop, ..rest] -> {
let key = prop_key(prop)
let without = list.filter(acc, fn(p) { prop_key(p) != key })
dedupe_props(rest, [prop, ..without])
}
}
}
fn prop_key(entry: KdlEntry) -> String {
case entry {
document.KdlProperty(key: key, ..) -> key
_ -> ""
}
}
fn compare_prop(a: KdlEntry, b: KdlEntry) -> Order {
string.compare(prop_key(a), prop_key(b))
}
fn write_type(ty: Option(String)) -> String {
case ty {
Some(ty) -> "(" <> serialize.write_identifier_or_string(ty) <> ")"
None -> ""
}
}
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) ->
serialize.write_identifier_or_string(key)
<> "="
<> write_type(ty)
<> write_value(value)
}
}
fn write_value(value: KdlValue) -> String {
case value {
document.KdlString(value) -> serialize.write_identifier_or_string(value)
document.KdlNumberValue(number) -> normalize_number(number)
document.KdlBool(True) -> "#true"
document.KdlBool(False) -> "#false"
document.KdlNull -> "#null"
}
}
fn normalize_number(number: document.KdlNumber) -> String {
let raw = document.number_raw(number)
case string.starts_with(raw, "#") {
True -> raw
False -> normalize_raw_number(raw)
}
}
fn normalize_raw_number(raw: String) -> String {
let #(sign, rest) = remove_sign(raw)
case prefix(rest) {
#(Some(base), body) ->
case bignum.base_to_decimal(scanner.clean_underscores(body), base) {
Ok("0") -> "0"
Ok(v) -> sign <> v
Error(_) -> sign <> scanner.clean_underscores(body)
}
#(None, _) ->
apply_sign(sign, scanner.clean_underscores(rest) |> normalize_exponent)
}
}
fn apply_sign(sign: String, body: String) -> String {
case body {
"0" -> "0"
_ -> sign <> body
}
}
fn normalize_exponent(clean: String) -> String {
normalize_exponent_chars(string.to_graphemes(clean), "")
}
fn normalize_exponent_chars(chars: List(String), acc: String) -> String {
case chars {
[] -> strip_decimal_leading_zeros(acc)
[ch, ..rest] ->
case ch == "e" || ch == "E" {
True ->
strip_decimal_leading_zeros(acc)
<> "E"
<> normalize_exponent_tail(rest)
False -> normalize_exponent_chars(rest, acc <> ch)
}
}
}
fn strip_decimal_leading_zeros(value: String) -> String {
strip_decimal_leading_zeros_chars(string.to_graphemes(value))
}
fn strip_decimal_leading_zeros_chars(chars: List(String)) -> String {
case chars {
["0", next, ..rest] ->
case scanner.is_ascii_digit(next) {
True -> strip_decimal_leading_zeros_chars([next, ..rest])
False -> scanner.join(chars)
}
_ -> scanner.join(chars)
}
}
fn normalize_exponent_tail(chars: List(String)) -> String {
case chars {
[] -> "+"
["+", ..] -> scanner.join(chars)
["-", ..] -> scanner.join(chars)
_ -> "+" <> scanner.join(chars)
}
}
fn remove_sign(raw: String) -> #(String, String) {
let minus = string.remove_prefix(raw, "-")
case minus != raw {
True -> #("-", minus)
False -> {
let plus = string.remove_prefix(raw, "+")
case plus != raw {
True -> #("", plus)
False -> #("", raw)
}
}
}
}
fn prefix(rest: String) -> #(Option(Int), String) {
try_prefix(rest, scanner.radix_prefixes())
}
fn try_prefix(
rest: String,
prefixes: List(#(String, Int)),
) -> #(Option(Int), String) {
case prefixes {
[] -> #(None, rest)
[#(prefix, base), ..others] -> {
let body = string.remove_prefix(rest, prefix)
case body != rest {
True -> #(Some(base), body)
False -> try_prefix(rest, others)
}
}
}
}