From 7416cef7233386a8cdeeee2977a693d3447eab51 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 21 Oct 2026 10:10:19 +0200 Subject: sbbuilder: a line editor for sb-builder's TOML configs Same shape as qtmaildir's Ini.js, so a file the user annotated keeps its comments: unset comments a key out, set reactivates it. A value spanning lines throws instead of gaining a duplicate key, which TOML rejects. Co-Authored-By: Claude Sonnet 5.5 --- desktop/modules/sbbuilder/Toml.js | 152 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 desktop/modules/sbbuilder/Toml.js (limited to 'desktop/modules/sbbuilder/Toml.js') 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. +// +// 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; +} -- cgit v1.2.3