// 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 qtmaildir.conf, tested by test_ini.js under node. // // The file is walked a line at a time, never cut at the next "[": values // hold brackets ([Gmail]/Bozze), and a section body taken as "everything up // to the next [" once lost three accounts their labels (AGENTS.md). A header // is a line that is only "[name]". Comments and order are kept: a key unset // is commented out in place, a commented key that is set again is activated, // and anything this file does not touch passes through. // // Values follow QSettings' INI reading: an unquoted ; starts a comment, and // a "quoted" value may hold one. const HEADER = /^\s*\[([^\]]+)\]\s*(;.*)?$/; const KEY = /^(\s*)(;\s*)?([^=;\s][^=]*?)\s*=\s*(.*)$/; function value(rest) { if (rest.startsWith('"')) { let out = ""; for (let i = 1; i < rest.length; i++) { const c = rest[i]; if (c === '"') return { v: out, end: i + 1 }; if (c === "\\" && i + 1 < rest.length) out += rest[++i]; else out += c; } return { v: out, end: rest.length }; } const semi = rest.search(/\s*;/); const raw = semi < 0 ? rest : rest.slice(0, semi); return { v: raw.trim(), end: raw.replace(/\s+$/, "").length }; } function quote(v) { return /[;"]|^\s|\s$/.test(v) ? '"' + v.replace(/[\\"]/g, c => "\\" + c) + '"' : v; } // [{name, h, end}]: the header's line and the line after the section. function ranges(lines) { const out = []; lines.forEach((l, i) => { const m = HEADER.exec(l); if (!m) return; if (out.length) out[out.length - 1].end = i; out.push({ name: m[1].trim(), h: i, end: lines.length }); }); return out; } function sections(text) { return ranges(text.split("\n")).map(r => r.name); } // [{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]); if (m && !m[2]) out.push({ key: m[3].trim(), value: value(m[4]).v }); } return out; } function get(text, section, key) { const e = entries(text, section).find(x => x.key === key); return e ? e.value : undefined; } // value null unsets: the key is commented out where it stands. function set(text, section, key, val) { const lines = text.split("\n"); const r = ranges(lines).find(x => x.name === section); if (!r) { if (val === null) return text; return text.replace(/\n*$/, "") + "\n\n[" + section + "]\n" + key + " = " + quote(val) + "\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 (lines[i].trim() !== "") last = i; if (!m || m[3].trim() !== key) continue; const tail = m[4].slice(value(m[4]).end); if (!m[2]) { lines[i] = val === null ? m[1] + "; " + key + " = " + m[4] : m[1] + key + " = " + quote(val) + tail; return lines.join("\n"); } if (commented < 0) commented = i; } if (val === null) return text; if (commented >= 0) { const m = KEY.exec(lines[commented]); lines[commented] = m[1] + key + " = " + quote(val) + m[4].slice(value(m[4]).end); } else lines.splice(last + 1, 0, key + " = " + quote(val)); return lines.join("\n"); } // Deletes an active key's line outright, for the free-form sections // ([tagcolors], [keys]) where a commented-out row would only pile up. 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] && m[3].trim() === key) { lines.splice(i, 1); return lines.join("\n"); } } return text; } // pairs: [[key, value], ...] function addSection(text, section, pairs) { return text.replace(/\n*$/, "") + "\n\n[" + section + "]\n" + pairs.map(p => p[0] + " = " + quote(p[1])).join("\n") + "\n"; } // The header through the line before the next header, which takes the blank // line that separated it from the next section with it. function removeSection(text, section) { const lines = text.split("\n"); const r = ranges(lines).find(x => x.name === section); if (!r) return text; lines.splice(r.h, r.end - r.h); return lines.join("\n"); }