UNPKG

@template-tools/template-sync

Version:
229 lines (212 loc) 6.11 kB
import { EOL } from "node:os"; export function encode(obj, opt) { const children = []; let out = ""; // opt.section is passed in recursively. If passed in on top-level, it'll affect both the top-level ini keys, and any children if (typeof opt === "string") { opt = { section: opt, whitespace: false, inlineArrays: false }; } else { opt = opt || {}; opt.whitespace = opt.whitespace === true; } const separator = opt.whitespace ? " = " : "="; for (const [key, val] of Object.entries(obj)) { if (val && Array.isArray(val)) { val.forEach(function (item) { out += safe(opt.inlineArrays ? key : key + "[]") + separator + safe(item) + EOL; }); } else if (val && typeof val === "object") { children.push(key); } else { out += safe(key) + separator + safe(val) + EOL; } } if (opt.section && out.length) { out = "[" + safe(opt.section) + "]" + EOL + out; } children.forEach(function (key) { const parsedSection = dotSplit(key).join("\\."); const section = (opt.section ? opt.section + "." : "") + parsedSection; const child = encode(obj[key], { section: section, whitespace: opt.whitespace, inlineArrays: opt.inlineArrays }); if (out.length && child.length) { out += EOL; } out += child; }); return out; } function dotSplit(str) { return str .replace(/\1/g, "\u0002LITERAL\\1LITERAL\u0002") .replace(/\\\./g, "\u0001") .split(/\./) .map(function (part) { return part .replace(/\1/g, "\\.") .replace(/\2LITERAL\\1LITERAL\2/g, "\u0001"); }); } export function decode(str, opt = {}) { const out = {}; let ref = out; let section = null; // section |key = value const re = /^\[([^\]]*)\]$|^([^=]+)(?:=(.*))?$/i; const lines = str.split(/[\r\n]+/g); const commentMatch = /^\s*[;#]/; lines.forEach(function (line) { if (!line || line.match(commentMatch)) { return; } const match = line.match(re); if (!match) { return; } if (match[1] !== undefined) { section = unsafe(match[1]); ref = out[section] = out[section] || {}; return; } let key = unsafe(match[2]); let value = match[3] ? unsafe(match[3]) : true; switch (value) { case "true": case "false": case "null": value = JSON.parse(value); } // Convert keys with '[]' suffix to an array if (key.length > 2 && key.slice(-2) === "[]") { key = key.slice(0, key.length - 2); if (!ref[key]) { ref[key] = []; } else if (!Array.isArray(ref[key])) { ref[key] = [ref[key]]; } } else if ( opt.inlineArrays && typeof ref[key] !== "undefined" && !Array.isArray(ref[key]) ) { ref[key] = [ref[key]]; } // safeguard against resetting a previously defined // array by accidentally forgetting the brackets if (Array.isArray(ref[key])) { ref[key].push(value); } else { ref[key] = value; } }); // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}} // use a filter to return the keys that have to be deleted. Object.keys(out) .filter(function (key) { if ( !out[key] || typeof out[key] !== "object" || Array.isArray(out[key]) ) { return false; } // see if the parent section is also an object. // if so, add it to that, and mark this one for deletion const parts = dotSplit(key); let p = out; const lastKey = parts.pop(); const unescapedLastKey = lastKey.replace(/\\\./g, "."); parts.forEach(function (part) { if (!p[part] || typeof p[part] !== "object") { p[part] = {}; } p = p[part]; }); if (p === out && unescapedLastKey === lastKey) { return false; } p[unescapedLastKey] = out[key]; return true; }) .forEach(function (del) { delete out[del]; }); return out; } // determines if string is encased in quotes function isQuoted(val) { return ( (val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")) ); } // escapes the string val such that it is safe to be used as a key or value in an ini-file. Basically escapes quotes function safe(val) { // all kinds of values and keys if ( typeof val !== "string" || val.match(/[\r\n]/) || val.match(/^\[/) || (val.length > 1 && isQuoted(val)) || val !== val.trim() ) { return JSON.stringify(val); } // comments return val.replace(/;/g, "\\;").replace(/#/g, "\\#"); } // unescapes the string val function unsafe(val) { const escapableChars = "\\;#", commentChars = ";#"; val = (val || "").trim(); if (isQuoted(val)) { // remove the single quotes before calling JSON.parse if (val.charAt(0) === "'") { val = val.substr(1, val.length - 2); } try { val = JSON.parse(val); } catch (e) { // we tried :( } return val; } // walk the val to find the first not-escaped ; character let isEscaping = false; let escapedVal = ""; for (let i = 0, l = val.length; i < l; i++) { const c = val.charAt(i); if (isEscaping) { // check if this character is an escapable character like \ or ; or # if (escapableChars.indexOf(c) !== -1) { escapedVal += c; } else { escapedVal += "\\" + c; } isEscaping = false; } else if (commentChars.indexOf(c) !== -1) { break; } else if (c === "\\") { isEscaping = true; } else { escapedVal += c; } } // we're still escaping - something isn't right. Close out with an escaped escape char if (isEscaping) { escapedVal += "\\"; } return escapedVal.trim(); }