diff --git a/README.md b/README.md index 8354ba7..296e843 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ The `options` object may contain the following: to be used with: when `platform` is `win32`, line terminations are CR+LF, for other platforms line termination is LF. By default the current platform name is used. +* `bracketedArrays` Boolean to specify whether array values are appended + with `[]`. By default this is true but there are some ini parsers + that instead treat duplicate names as arrays. For backwards compatibility reasons, if a `string` options is passed in, then it is assumed to be the `section` value. diff --git a/lib/ini.js b/lib/ini.js index 84be838..bbd8661 100644 --- a/lib/ini.js +++ b/lib/ini.js @@ -8,18 +8,20 @@ const encode = (obj, opt = {}) => { opt.newline = opt.newline === true /* istanbul ignore next */ opt.platform = opt.platform || process?.platform + opt.bracketedArray = opt.bracketedArray !== false /* istanbul ignore next */ const eol = opt.platform === 'win32' ? '\r\n' : '\n' const separator = opt.whitespace ? ' = ' : '=' const children = [] let out = '' + const arraySuffix = opt.bracketedArray ? '[]' : '' for (const k of Object.keys(obj)) { const val = obj[k] if (val && Array.isArray(val)) { for (const item of val) { - out += safe(k + '[]') + separator + safe(item) + eol + out += safe(`${k}${arraySuffix}`) + separator + safe(item) + eol } } else if (val && typeof val === 'object') { children.push(k) @@ -75,13 +77,15 @@ function splitSections (str, separator) { return sections } -const decode = str => { +const decode = (str, opt = {}) => { + opt.bracketedArray = opt.bracketedArray !== false const out = Object.create(null) let p = out let section = null // section |key = value const re = /^\[([^\]]*)\]\s*$|^([^=]+)(=(.*))?$/i const lines = str.split(/[\r\n]+/g) + const duplicates = {} for (const line of lines) { if (!line || line.match(/^\s*[;#]/) || line.match(/^\s*$/)) { @@ -103,7 +107,13 @@ const decode = str => { continue } const keyRaw = unsafe(match[2]) - const isArray = keyRaw.length > 2 && keyRaw.slice(-2) === '[]' + let isArray + if (opt.bracketedArray) { + isArray = keyRaw.length > 2 && keyRaw.slice(-2) === '[]' + } else { + duplicates[keyRaw] = (duplicates?.[keyRaw] || 0) + 1 + isArray = duplicates[keyRaw] > 1 + } const key = isArray ? keyRaw.slice(0, -2) : keyRaw if (key === '__proto__') { continue diff --git a/tap-snapshots/test/duplicate-properties.js.test.cjs b/tap-snapshots/test/duplicate-properties.js.test.cjs new file mode 100644 index 0000000..4a88d10 --- /dev/null +++ b/tap-snapshots/test/duplicate-properties.js.test.cjs @@ -0,0 +1,57 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/duplicate-properties.js TAP decode duplicate properties with bracketedArray=false > must match snapshot 1`] = ` +Null Object { + "ar": Array [ + "three", + ], + "ar[]": "one", + "b": Array [ + "2", + "3", + "3", + ], + "brr": "1", + "str": "3", + "zr": "123", + "zr[]": "deedee", +} +` + +exports[`test/duplicate-properties.js TAP decode with duplicate properties > must match snapshot 1`] = ` +Null Object { + "ar": Array [ + "one", + "three", + ], + "brr": "3", + "str": "3", + "zr": Array [ + "deedee", + "123", + ], +} +` + +exports[`test/duplicate-properties.js TAP encode duplicate properties with bracketedArray=false > must match snapshot 1`] = ` +ar=1 +ar=2 +ar=3 +br=1 +br=2 + +` + +exports[`test/duplicate-properties.js TAP encode with duplicate properties > must match snapshot 1`] = ` +ar[]=1 +ar[]=2 +ar[]=3 +br[]=1 +br[]=2 + +` diff --git a/test/duplicate-properties.js b/test/duplicate-properties.js new file mode 100644 index 0000000..6f47c91 --- /dev/null +++ b/test/duplicate-properties.js @@ -0,0 +1,40 @@ +const i = require('../') +const tap = require('tap') +const test = tap.test +const fs = require('fs') +const path = require('path') + +const fixture = path.resolve(__dirname, './fixtures/duplicate.ini') +const data = fs.readFileSync(fixture, 'utf8') + +tap.cleanSnapshot = s => s.replace(/\r\n/g, '\n') + +test('decode with duplicate properties', function (t) { + const d = i.decode(data) + t.matchSnapshot(d) + t.end() +}) + +test('encode with duplicate properties', function (t) { + const e = i.encode({ + ar: ['1', '2', '3'], + br: ['1', '2'], + }) + t.matchSnapshot(e) + t.end() +}) + +test('decode duplicate properties with bracketedArray=false', function (t) { + const d = i.decode(data, { bracketedArray: false }) + t.matchSnapshot(d) + t.end() +}) + +test('encode duplicate properties with bracketedArray=false', function (t) { + const e = i.encode({ + ar: ['1', '2', '3'], + br: ['1', '2'], + }, { bracketedArray: false }) + t.matchSnapshot(e) + t.end() +}) diff --git a/test/fixtures/duplicate.ini b/test/fixtures/duplicate.ini new file mode 100644 index 0000000..7bce09a --- /dev/null +++ b/test/fixtures/duplicate.ini @@ -0,0 +1,9 @@ +zr[] = deedee +zr=123 +ar[] = one +ar[] = three +str = 3 +brr = 1 +brr = 2 +brr = 3 +brr = 3