| Leo Repp | 58b9f11 | 2021-11-22 11:57:47 +0100 | [diff] [blame^] | 1 | exports.parse = exports.decode = decode |
| 2 | |
| 3 | exports.stringify = exports.encode = encode |
| 4 | |
| 5 | exports.safe = safe |
| 6 | exports.unsafe = unsafe |
| 7 | |
| 8 | var eol = typeof process !== 'undefined' && |
| 9 | process.platform === 'win32' ? '\r\n' : '\n' |
| 10 | |
| 11 | function encode (obj, opt) { |
| 12 | var children = [] |
| 13 | var out = '' |
| 14 | |
| 15 | if (typeof opt === 'string') { |
| 16 | opt = { |
| 17 | section: opt, |
| 18 | whitespace: false |
| 19 | } |
| 20 | } else { |
| 21 | opt = opt || {} |
| 22 | opt.whitespace = opt.whitespace === true |
| 23 | } |
| 24 | |
| 25 | var separator = opt.whitespace ? ' = ' : '=' |
| 26 | |
| 27 | Object.keys(obj).forEach(function (k, _, __) { |
| 28 | var val = obj[k] |
| 29 | if (val && Array.isArray(val)) { |
| 30 | val.forEach(function (item) { |
| 31 | out += safe(k + '[]') + separator + safe(item) + '\n' |
| 32 | }) |
| 33 | } else if (val && typeof val === 'object') { |
| 34 | children.push(k) |
| 35 | } else { |
| 36 | out += safe(k) + separator + safe(val) + eol |
| 37 | } |
| 38 | }) |
| 39 | |
| 40 | if (opt.section && out.length) { |
| 41 | out = '[' + safe(opt.section) + ']' + eol + out |
| 42 | } |
| 43 | |
| 44 | children.forEach(function (k, _, __) { |
| 45 | var nk = dotSplit(k).join('\\.') |
| 46 | var section = (opt.section ? opt.section + '.' : '') + nk |
| 47 | var child = encode(obj[k], { |
| 48 | section: section, |
| 49 | whitespace: opt.whitespace |
| 50 | }) |
| 51 | if (out.length && child.length) { |
| 52 | out += eol |
| 53 | } |
| 54 | out += child |
| 55 | }) |
| 56 | |
| 57 | return out |
| 58 | } |
| 59 | |
| 60 | function dotSplit (str) { |
| 61 | return str.replace(/\1/g, '\u0002LITERAL\\1LITERAL\u0002') |
| 62 | .replace(/\\\./g, '\u0001') |
| 63 | .split(/\./).map(function (part) { |
| 64 | return part.replace(/\1/g, '\\.') |
| 65 | .replace(/\2LITERAL\\1LITERAL\2/g, '\u0001') |
| 66 | }) |
| 67 | } |
| 68 | |
| 69 | function decode (str) { |
| 70 | var out = {} |
| 71 | var p = out |
| 72 | var section = null |
| 73 | // section |key = value |
| 74 | var re = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i |
| 75 | var lines = str.split(/[\r\n]+/g) |
| 76 | |
| 77 | lines.forEach(function (line, _, __) { |
| 78 | if (!line || line.match(/^\s*[;#]/)) return |
| 79 | var match = line.match(re) |
| 80 | if (!match) return |
| 81 | if (match[1] !== undefined) { |
| 82 | section = unsafe(match[1]) |
| 83 | p = out[section] = out[section] || {} |
| 84 | return |
| 85 | } |
| 86 | var key = unsafe(match[2]) |
| 87 | var value = match[3] ? unsafe(match[4]) : true |
| 88 | switch (value) { |
| 89 | case 'true': |
| 90 | case 'false': |
| 91 | case 'null': value = JSON.parse(value) |
| 92 | } |
| 93 | |
| 94 | // Convert keys with '[]' suffix to an array |
| 95 | if (key.length > 2 && key.slice(-2) === '[]') { |
| 96 | key = key.substring(0, key.length - 2) |
| 97 | if (!p[key]) { |
| 98 | p[key] = [] |
| 99 | } else if (!Array.isArray(p[key])) { |
| 100 | p[key] = [p[key]] |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | // safeguard against resetting a previously defined |
| 105 | // array by accidentally forgetting the brackets |
| 106 | if (Array.isArray(p[key])) { |
| 107 | p[key].push(value) |
| 108 | } else { |
| 109 | p[key] = value |
| 110 | } |
| 111 | }) |
| 112 | |
| 113 | // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}} |
| 114 | // use a filter to return the keys that have to be deleted. |
| 115 | Object.keys(out).filter(function (k, _, __) { |
| 116 | if (!out[k] || |
| 117 | typeof out[k] !== 'object' || |
| 118 | Array.isArray(out[k])) { |
| 119 | return false |
| 120 | } |
| 121 | // see if the parent section is also an object. |
| 122 | // if so, add it to that, and mark this one for deletion |
| 123 | var parts = dotSplit(k) |
| 124 | var p = out |
| 125 | var l = parts.pop() |
| 126 | var nl = l.replace(/\\\./g, '.') |
| 127 | parts.forEach(function (part, _, __) { |
| 128 | if (!p[part] || typeof p[part] !== 'object') p[part] = {} |
| 129 | p = p[part] |
| 130 | }) |
| 131 | if (p === out && nl === l) { |
| 132 | return false |
| 133 | } |
| 134 | p[nl] = out[k] |
| 135 | return true |
| 136 | }).forEach(function (del, _, __) { |
| 137 | delete out[del] |
| 138 | }) |
| 139 | |
| 140 | return out |
| 141 | } |
| 142 | |
| 143 | function isQuoted (val) { |
| 144 | return (val.charAt(0) === '"' && val.slice(-1) === '"') || |
| 145 | (val.charAt(0) === "'" && val.slice(-1) === "'") |
| 146 | } |
| 147 | |
| 148 | function safe (val) { |
| 149 | return (typeof val !== 'string' || |
| 150 | val.match(/[=\r\n]/) || |
| 151 | val.match(/^\[/) || |
| 152 | (val.length > 1 && |
| 153 | isQuoted(val)) || |
| 154 | val !== val.trim()) |
| 155 | ? JSON.stringify(val) |
| 156 | : val.replace(/;/g, '\\;').replace(/#/g, '\\#') |
| 157 | } |
| 158 | |
| 159 | function unsafe (val, doUnesc) { |
| 160 | val = (val || '').trim() |
| 161 | if (isQuoted(val)) { |
| 162 | // remove the single quotes before calling JSON.parse |
| 163 | if (val.charAt(0) === "'") { |
| 164 | val = val.substr(1, val.length - 2) |
| 165 | } |
| 166 | try { val = JSON.parse(val) } catch (_) {} |
| 167 | } else { |
| 168 | // walk the val to find the first not-escaped ; character |
| 169 | var esc = false |
| 170 | var unesc = '' |
| 171 | for (var i = 0, l = val.length; i < l; i++) { |
| 172 | var c = val.charAt(i) |
| 173 | if (esc) { |
| 174 | if ('\\;#'.indexOf(c) !== -1) { |
| 175 | unesc += c |
| 176 | } else { |
| 177 | unesc += '\\' + c |
| 178 | } |
| 179 | esc = false |
| 180 | } else if (';#'.indexOf(c) !== -1) { |
| 181 | break |
| 182 | } else if (c === '\\') { |
| 183 | esc = true |
| 184 | } else { |
| 185 | unesc += c |
| 186 | } |
| 187 | } |
| 188 | if (esc) { |
| 189 | unesc += '\\' |
| 190 | } |
| 191 | return unesc.trim() |
| 192 | } |
| 193 | return val |
| 194 | } |