aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/sbbuilder/Toml.js
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/sbbuilder/Toml.js')
-rw-r--r--desktop/modules/sbbuilder/Toml.js152
1 files changed, 152 insertions, 0 deletions
diff --git a/desktop/modules/sbbuilder/Toml.js b/desktop/modules/sbbuilder/Toml.js
new file mode 100644
index 0000000..7fd940a
--- /dev/null
+++ b/desktop/modules/sbbuilder/Toml.js
@@ -0,0 +1,152 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+.pragma library
+
+// Pure helpers for sb-builder's two config.toml files, tested by
+// test_toml.js under node. Walked a line at a time like Ini.js, so comments
+// and order survive: a key unset is commented out with #, a commented key
+// set again is activated. Top-level keys are section "".
+//
+// ponytail: the TOML these files use, one key per line: strings, integers,
+// booleans, one-line arrays. A value spanning lines makes set() throw rather
+// than write a duplicate key; such a file is edited by hand.
+
+const HEADER = /^\s*\[([A-Za-z0-9_.-]+)\]\s*(#.*)?$/;
+const KEY = /^(\s*)(#\s*)?("(?:[^"\\]|\\.)*"|[A-Za-z0-9_-]+)\s*=\s*(.*)$/;
+
+function unkey(k) {
+ if (!k.startsWith('"')) return k;
+ try { return JSON.parse(k); } catch (e) { return k; }
+}
+function enkey(k) { return /^[A-Za-z0-9_-]+$/.test(k) ? k : enc(k); }
+
+// JSON's escapes are a subset of TOML basic strings', bar a raw DEL.
+function enc(v) {
+ if (Array.isArray(v)) return "[" + v.map(enc).join(", ") + "]";
+ if (typeof v === "string") return JSON.stringify(v).replace(/\x7f/g, "\\u007f");
+ return String(v);
+}
+
+function skip(s, i) { while (s[i] === " " || s[i] === "\t") i++; return i; }
+
+// {v, i}: the value at s[i] and the index after it, or null.
+function val(s, i) {
+ const c = s[i];
+ if (c === '"') {
+ let j = i + 1;
+ while (j < s.length && s[j] !== '"') j += s[j] === "\\" ? 2 : 1;
+ if (j >= s.length) return null;
+ try { return { v: JSON.parse(s.slice(i, j + 1)), i: j + 1 }; } catch (e) { return null; }
+ }
+ if (c === "'") {
+ const j = s.indexOf("'", i + 1);
+ return j < 0 ? null : { v: s.slice(i + 1, j), i: j + 1 };
+ }
+ if (c === "[") {
+ const out = [];
+ i = skip(s, i + 1);
+ while (s[i] !== "]") {
+ const e = val(s, i);
+ if (!e) return null;
+ out.push(e.v);
+ i = skip(s, e.i);
+ if (s[i] === ",") i = skip(s, i + 1);
+ else if (s[i] !== "]") return null;
+ }
+ return { v: out, i: i + 1 };
+ }
+ const m = /^(true|false|[+-]?[0-9][0-9_]*)(?=[\s#,\]]|$)/.exec(s.slice(i));
+ if (!m) return null;
+ const v = m[1] === "true" ? true : m[1] === "false" ? false : Number(m[1].replace(/_/g, ""));
+ return { v: v, i: i + m[1].length };
+}
+
+// [{name, h, end}]: the header's line and the line after the section. The
+// top-level keys are a section "" with its header at -1.
+function ranges(lines) {
+ const out = [{ name: "", h: -1, end: lines.length }];
+ lines.forEach((l, i) => {
+ const m = HEADER.exec(l);
+ if (!m) return;
+ out[out.length - 1].end = i;
+ out.push({ name: m[1], h: i, end: lines.length });
+ });
+ return out;
+}
+
+// [{key, value}] for the active keys of a section, in file order.
+function entries(text, section) {
+ const lines = text.split("\n");
+ const r = ranges(lines).find(x => x.name === section);
+ if (!r) return [];
+ const out = [];
+ for (let i = r.h + 1; i < r.end; i++) {
+ const m = KEY.exec(lines[i]);
+ const p = m && !m[2] ? val(m[4], 0) : null;
+ if (p) out.push({ key: unkey(m[3]), value: p.v });
+ }
+ return out;
+}
+
+function get(text, section, key) {
+ const e = entries(text, section).find(x => x.key === key);
+ return e ? e.value : undefined;
+}
+
+// v null unsets: the key is commented out where it stands.
+function set(text, section, key, v) {
+ const lines = text.split("\n");
+ const r = ranges(lines).find(x => x.name === section);
+ const line = enkey(key) + " = " + (v === null ? "" : enc(v));
+ if (!r) {
+ if (v === null) return text;
+ return text.replace(/\n*$/, "") + (text.trim() ? "\n\n" : "") + "[" + section + "]\n" + line + "\n";
+ }
+ let commented = -1;
+ let last = r.h;
+ for (let i = r.h + 1; i < r.end; i++) {
+ const m = KEY.exec(lines[i]);
+ if (!m) continue;
+ if (!m[2]) last = i;
+ if (unkey(m[3]) !== key) continue;
+ const p = val(m[4], 0);
+ if (!m[2]) {
+ if (!p) throw new Error(key + ": a value this page cannot edit; change it in the file");
+ lines[i] = v === null ? m[1] + "# " + lines[i].slice(m[1].length)
+ : m[1] + line + m[4].slice(p.i);
+ return lines.join("\n");
+ }
+ if (p && commented < 0) commented = i;
+ }
+ if (v === null) return text;
+ if (commented >= 0) {
+ const m = KEY.exec(lines[commented]);
+ lines[commented] = m[1] + line + m[4].slice(val(m[4], 0).i);
+ return lines.join("\n");
+ }
+ // No key yet at the top: after the leading comments, not above them.
+ if (last < 0) while (last + 1 < r.end && lines[last + 1].trim().startsWith("#")) last++;
+ lines.splice(last + 1, 0, line);
+ return lines.join("\n");
+}
+
+// Deletes an active key's line outright, for [manifests] rows.
+function remove(text, section, key) {
+ const lines = text.split("\n");
+ const r = ranges(lines).find(x => x.name === section);
+ if (!r) return text;
+ for (let i = r.h + 1; i < r.end; i++) {
+ const m = KEY.exec(lines[i]);
+ if (m && !m[2] && unkey(m[3]) === key) { lines.splice(i, 1); return lines.join("\n"); }
+ }
+ return text;
+}