279 lines
6.8 KiB
Plaintext
279 lines
6.8 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
|
|
var r = replace(s, '\\"', '"')
|
|
r = replace(r, '\\\\', '\\')
|
|
return r
|
|
}
|
|
|
|
function toml_escape(s) {
|
|
if (!is_text(s)) return null
|
|
var r = replace(s, '\\', '\\\\')
|
|
r = replace(r, '"', '\\"')
|
|
return r
|
|
}
|
|
|
|
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 = ''
|
|
|
|
var i = 0
|
|
var line = null
|
|
var inner = null
|
|
var section_path = null
|
|
var j = 0
|
|
var key = null
|
|
var eq_index = null
|
|
var key_part = null
|
|
var value = null
|
|
var unquoted = null
|
|
for (i = 0; i < length(lines); i++) {
|
|
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, ']')) {
|
|
inner = text(line, 1, -1)
|
|
section_path = parse_key_path(inner)
|
|
if (section_path == null) return null
|
|
|
|
current_section = result
|
|
current_section_name = text(section_path, '.')
|
|
|
|
for (j = 0; j < length(section_path); j++) {
|
|
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
|
|
eq_index = search(line, '=')
|
|
if (eq_index != null && eq_index > 0) {
|
|
key_part = trim(text(line, 0, eq_index))
|
|
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))
|
|
|
|
key = parse_key(key_part)
|
|
if (key == null) return null
|
|
|
|
if (starts_with(value, '"') && ends_with(value, '"')) {
|
|
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 (number(value) != null) {
|
|
current_section[key] = number(value)
|
|
} else {
|
|
// Unquoted string
|
|
current_section[key] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
function parse_key(str) {
|
|
if (!is_text(str)) return null
|
|
var inner = null
|
|
if (starts_with(str, '"') && ends_with(str, '"')) {
|
|
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
|
|
|
|
var i = 0
|
|
var c = null
|
|
var piece = null
|
|
for (i = 0; i < length(str); i++) {
|
|
c = str[i]
|
|
if (c == '"' && (i == 0 || str[i - 1] != '\\')) {
|
|
in_quote = !in_quote
|
|
} else if (c == '.' && !in_quote) {
|
|
piece = trim(current)
|
|
if (piece == null) piece = trim(current)
|
|
push(parts, parse_key(piece))
|
|
current = ''
|
|
continue
|
|
}
|
|
current = 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
|
|
var s = text(str, 1, -1)
|
|
s = trim(s)
|
|
if (!s) return []
|
|
|
|
var items = []
|
|
var current = ''
|
|
var in_quotes = false
|
|
|
|
var i = 0
|
|
var ch = null
|
|
var piece = null
|
|
for (i = 0; i < length(s); i++) {
|
|
ch = s[i]
|
|
|
|
if (ch == '"' && (i == 0 || s[i - 1] != '\\')) {
|
|
in_quotes = !in_quotes
|
|
current = current + ch
|
|
} else if (ch == ',' && !in_quotes) {
|
|
piece = trim(current)
|
|
if (piece == null) piece = trim(current)
|
|
push(items, parse_value(piece))
|
|
current = ''
|
|
} else {
|
|
current = 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'
|
|
|
|
var n = number(str)
|
|
if (n != null) return n
|
|
|
|
return str
|
|
}
|
|
|
|
function encode_toml(obj) {
|
|
var result = []
|
|
|
|
function encode_value(value) {
|
|
var items = null
|
|
var i = 0
|
|
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)) {
|
|
items = []
|
|
for (i = 0; i < length(value); i++) push(items, encode_value(value[i]))
|
|
return '[' + text(items, ', ') + ']'
|
|
}
|
|
return text(value)
|
|
}
|
|
|
|
function quote_key(k) {
|
|
var i = 0
|
|
var c = null
|
|
for (i = 0; i < length(k); i++) {
|
|
c = k[i]
|
|
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-')) {
|
|
return '"' + toml_escape(k) + '"'
|
|
}
|
|
}
|
|
return k
|
|
}
|
|
|
|
// First pass: encode top-level simple values
|
|
var keys = array(obj)
|
|
var i = 0
|
|
var key = null
|
|
var value = null
|
|
for (i = 0; i < length(keys); i++) {
|
|
key = keys[i]
|
|
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)
|
|
var i = 0
|
|
var key = null
|
|
var value = null
|
|
var quoted = null
|
|
var section_path = null
|
|
var section_keys = null
|
|
var j = 0
|
|
var sk = null
|
|
var sv = null
|
|
for (i = 0; i < length(keys); i++) {
|
|
key = keys[i]
|
|
value = o[key]
|
|
|
|
if (is_object(value)) {
|
|
quoted = quote_key(key)
|
|
section_path = path ? path + '.' + quoted : quoted
|
|
push(result, '[' + section_path + ']')
|
|
|
|
// Direct properties
|
|
section_keys = array(value)
|
|
for (j = 0; j < length(section_keys); j++) {
|
|
sk = section_keys[j]
|
|
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
|
|
}
|