Files
cell/toml.cm
2026-01-21 00:52:18 -06:00

249 lines
6.6 KiB
Plaintext

// Simple TOML parser for cell modules
// Supports basic TOML features needed for the module system
function toml_unescape(s) {
if (!is_text(s)) return null
// Order matters:
// "\\\"" (backslash + quote) should become "\"", not just '"'
// So: unescape \" first, then unescape \\.
s = replace(s, '\\"', '"')
s = replace(s, '\\\\', '\\')
return s
}
function toml_escape(s) {
if (!is_text(s)) return null
// Order matters:
// escape backslashes first, otherwise escaping quotes introduces new backslashes that would get double-escaped.
s = replace(s, '\\', '\\\\')
s = replace(s, '"', '\\"')
return s
}
function parse_toml(toml_text) {
if (!is_text(toml_text)) return null
// Prefer Misty split if present; fall back to JS split.
var lines = array(toml_text, '\n')
if (lines == null) lines = array(toml_text, '\n')
var result = {}
var current_section = result
var current_section_name = ''
for (var i = 0; i < length(lines); i++) {
var line = trim(lines[i])
if (line == null) line = lines[i]
// Skip empty lines and comments
if (!line || starts_with(line, '#')) continue
// Section header
if (starts_with(line, '[') && ends_with(line, ']')) {
var inner = text(line, 1, -1)
var section_path = parse_key_path(inner)
if (section_path == null) return null
current_section = result
current_section_name = text(section_path, '.')
for (var j = 0; j < length(section_path); j++) {
var key = section_path[j]
// Only treat null as "missing"; do not clobber false/0/""
if (current_section[key] == null) {
current_section[key] = {}
} else if (!is_object(current_section[key])) {
// Scalar/table collision like: a = 1 then [a.b]
return null
}
current_section = current_section[key]
}
continue
}
// Key-value pair
var eq_index = search(line, '=')
if (eq_index != null && eq_index > 0) {
var key_part = trim(text(line, 0, eq_index))
var value = trim(text(line, eq_index + 1))
if (key_part == null) key_part = trim(text(line, 0, eq_index))
if (value == null) value = trim(text(line, eq_index + 1))
var key = parse_key(key_part)
if (key == null) return null
if (starts_with(value, '"') && ends_with(value, '"')) {
var unquoted = text(value, 1, -1)
current_section[key] = toml_unescape(unquoted)
if (current_section[key] == null) return null
} else if (starts_with(value, '[') && ends_with(value, ']')) {
current_section[key] = parse_array(value)
if (current_section[key] == null) return null
} else if (value == 'true' || value == 'false') {
current_section[key] = value == 'true'
} else if (is_number(value)) {
current_section[key] = Number(value)
} else {
// Unquoted string
current_section[key] = value
}
}
}
return result
}
function parse_key(str) {
if (!is_text(str)) return null
if (starts_with(str, '"') && ends_with(str, '"')) {
var inner = text(str, 1, -1)
return toml_unescape(inner)
}
return str
}
// Split a key path by dots, respecting quotes
function parse_key_path(str) {
if (!is_text(str)) return null
var parts = []
var current = ''
var in_quote = false
for (var i = 0; i < length(str); i++) {
var c = str[i]
if (c == '"' && (i == 0 || str[i - 1] != '\\')) {
in_quote = !in_quote
} else if (c == '.' && !in_quote) {
var piece = trim(current)
if (piece == null) piece = trim(current)
push(parts, parse_key(piece))
current = ''
continue
}
current += c
}
var tail = trim(current)
if (tail == null) tail = trim(current)
if (length(tail) > 0) push(parts, parse_key(tail))
return parts
}
function parse_array(str) {
if (!is_text(str)) return null
// Remove brackets and trim
str = text(str, 1, -1)
str = trim(str)
if (!str) return []
var items = []
var current = ''
var in_quotes = false
for (var i = 0; i < length(str); i++) {
var ch = str[i]
if (ch == '"' && (i == 0 || str[i - 1] != '\\')) {
in_quotes = !in_quotes
current += ch
} else if (ch == ',' && !in_quotes) {
var piece = trim(current)
if (piece == null) piece = trim(current)
push(items, parse_value(piece))
current = ''
} else {
current += ch
}
}
var last = trim(current)
if (last == null) last = trim(current)
if (last) push(items, parse_value(last))
return items
}
function parse_value(str) {
if (!is_text(str)) return null
if (starts_with(str, '"') && ends_with(str, '"')) {
return toml_unescape(text(str, 1, -1))
}
if (str == 'true' || str == 'false') return str == 'true'
// Use your existing numeric test; TOML numeric formats are richer, but this keeps your "module TOML" scope.
if (!isNaN(Number(str))) return Number(str)
return str
}
function encode_toml(obj) {
var result = []
function encode_value(value) {
if (is_text(value)) return '"' + toml_escape(value) + '"'
if (is_logical(value)) return value ? 'true' : 'false'
if (is_number(value)) return text(value)
if (is_array(value)) {
var items = []
for (var i = 0; i < length(value); i++) push(items, encode_value(value[i]))
return '[' + text(items, ', ') + ']'
}
return text(value)
}
function quote_key(k) {
if (search(k, '.') != null || search(k, '"') != null || search(k, ' ') != null) {
return '"' + toml_escape(k) + '"'
}
return k
}
// First pass: encode top-level simple values
var keys = array(obj)
for (var i = 0; i < length(keys); i++) {
var key = keys[i]
var value = obj[key]
if (!is_object(value)) push(result, quote_key(key) + ' = ' + encode_value(value))
}
// Second pass: encode nested objects
function encode_section(o, path) {
var keys = array(o)
for (var i = 0; i < length(keys); i++) {
var key = keys[i]
var value = o[key]
if (is_object(value)) {
var quoted = quote_key(key)
var section_path = path ? path + '.' + quoted : quoted
push(result, '[' + section_path + ']')
// Direct properties
var section_keys = array(value)
for (var j = 0; j < length(section_keys); j++) {
var sk = section_keys[j]
var sv = value[sk]
if (!is_object(sv)) push(result, quote_key(sk) + ' = ' + encode_value(sv))
}
// Nested sections
encode_section(value, section_path)
}
}
}
encode_section(obj, '')
return text(result, '\n')
}
return {
decode: parse_toml,
encode: encode_toml
}