diff --git a/README.md b/README.md index 296e843..d775352 100644 --- a/README.md +++ b/README.md @@ -76,19 +76,25 @@ prepended to all sub-sections, see the usage example above. The `options` object may contain the following: -* `section` A string which will be the first `section` in the encoded +* `align` Boolean to specify whether to align the `=` characters for + each section. This option will automatically enable `whitespace`. + Defaults to `false`. +* `section` String which will be the first `section` in the encoded ini data. Defaults to none. +* `sort` Boolean to specify if all keys in each section, as well as + all sections, will be alphabetically sorted. Defaults to `false`. * `whitespace` Boolean to specify whether to put whitespace around the `=` character. By default, whitespace is omitted, to be friendly to some persnickety old parsers that don't tolerate it well. But some find that it's more human-readable and pretty with the whitespace. + Defaults to `false`. * `newline` Boolean to specify whether to put an additional newline after a section header. Some INI file parsers (for example the TOSHIBA FlashAir one) need this to parse the file successfully. By default, the additional newline is omitted. * `platform` String to define which platform this INI file is expected to be used with: when `platform` is `win32`, line terminations are - CR+LF, for other platforms line termination is LF. By default the + 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 diff --git a/lib/ini.js b/lib/ini.js index bbd8661..763c829 100644 --- a/lib/ini.js +++ b/lib/ini.js @@ -4,8 +4,10 @@ const encode = (obj, opt = {}) => { if (typeof opt === 'string') { opt = { section: opt } } - opt.whitespace = opt.whitespace === true + opt.align = opt.align === true opt.newline = opt.newline === true + opt.sort = opt.sort === true + opt.whitespace = opt.whitespace === true || opt.align === true /* istanbul ignore next */ opt.platform = opt.platform || process?.platform opt.bracketedArray = opt.bracketedArray !== false @@ -14,19 +16,42 @@ const encode = (obj, opt = {}) => { const eol = opt.platform === 'win32' ? '\r\n' : '\n' const separator = opt.whitespace ? ' = ' : '=' const children = [] + + const keys = opt.sort ? Object.keys(obj).sort() : Object.keys(obj) + + let padToChars = 0 + // If aligning on the separator, then padToChars is determined as follows: + // 1. Get the keys + // 2. Exclude keys pointing to objects unless the value is null or an array + // 3. Add `[]` to array keys + // 4. Ensure non empty set of keys + // 5. Reduce the set to the longest `safe` key + // 6. Get the `safe` length + if (opt.align) { + padToChars = safe( + ( + keys + .filter(k => obj[k] === null || Array.isArray(obj[k]) || typeof obj[k] !== 'object') + .map(k => Array.isArray(obj[k]) ? `${k}[]` : k) + ) + .concat(['']) + .reduce((a, b) => safe(a).length >= safe(b).length ? a : b) + ).length + } + let out = '' const arraySuffix = opt.bracketedArray ? '[]' : '' - for (const k of Object.keys(obj)) { + for (const k of keys) { const val = obj[k] if (val && Array.isArray(val)) { for (const item of val) { - out += safe(`${k}${arraySuffix}`) + separator + safe(item) + eol + out += safe(`${k}${arraySuffix}`).padEnd(padToChars, ' ') + separator + safe(item) + eol } } else if (val && typeof val === 'object') { children.push(k) } else { - out += safe(k) + separator + safe(val) + eol + out += safe(k).padEnd(padToChars, ' ') + separator + safe(val) + eol } } diff --git a/tap-snapshots/test/foo.js.test.cjs b/tap-snapshots/test/foo.js.test.cjs index 8bb048f..26b81ce 100644 --- a/tap-snapshots/test/foo.js.test.cjs +++ b/tap-snapshots/test/foo.js.test.cjs @@ -111,6 +111,98 @@ noHashComment=this\\# this is not a comment ` +exports[`test/foo.js TAP encode with align > must match snapshot 1`] = ` +o = p +a with spaces = b c +" xa n p " = "\\"\\r\\nyoyoyo\\r\\r\\n" +"[disturbing]" = hey you never know +s = something +s1 = "something' +s2 = something else +s3 = +s4 = +s5 = " " +s6 = " a " +s7 = true +true = true +false = false +null = null +undefined = undefined +zr[] = deedee +ar[] = one +ar[] = three +ar[] = this is included +br = warm +eq = "eq=eq" + +[a] +av = a val +e = { o: p, a: { av: a val, b: { c: { e: "this [value]" } } } } +j = "\\"{ o: \\"p\\", a: { av: \\"a val\\", b: { c: { e: \\"this [value]\\" } } } }\\"" +"[]" = a square? +cr[] = four +cr[] = eight + +[a.b.c] +e = 1 +j = 2 + +[x\\.y\\.z] +x.y.z = xyz + +[x\\.y\\.z.a\\.b\\.c] +a.b.c = abc +nocomment = this\\; this is not a comment +noHashComment = this\\# this is not a comment + +` + +exports[`test/foo.js TAP encode with align and sort > must match snapshot 1`] = ` +" xa n p " = "\\"\\r\\nyoyoyo\\r\\r\\n" +"[disturbing]" = hey you never know +a with spaces = b c +ar[] = one +ar[] = three +ar[] = this is included +br = warm +eq = "eq=eq" +false = false +null = null +o = p +s = something +s1 = "something' +s2 = something else +s3 = +s4 = +s5 = " " +s6 = " a " +s7 = true +true = true +undefined = undefined +zr[] = deedee + +[a] +"[]" = a square? +av = a val +cr[] = four +cr[] = eight +e = { o: p, a: { av: a val, b: { c: { e: "this [value]" } } } } +j = "\\"{ o: \\"p\\", a: { av: \\"a val\\", b: { c: { e: \\"this [value]\\" } } } }\\"" + +[a.b.c] +e = 1 +j = 2 + +[x\\.y\\.z] +x.y.z = xyz + +[x\\.y\\.z.a\\.b\\.c] +a.b.c = abc +noHashComment = this\\# this is not a comment +nocomment = this\\; this is not a comment + +` + exports[`test/foo.js TAP encode with newline > must match snapshot 1`] = ` [log] @@ -145,6 +237,52 @@ Array [ ] ` +exports[`test/foo.js TAP encode with sort > must match snapshot 1`] = ` +" xa n p "="\\"\\r\\nyoyoyo\\r\\r\\n" +"[disturbing]"=hey you never know +a with spaces=b c +ar[]=one +ar[]=three +ar[]=this is included +br=warm +eq="eq=eq" +false=false +null=null +o=p +s=something +s1="something' +s2=something else +s3= +s4= +s5=" " +s6=" a " +s7=true +true=true +undefined=undefined +zr[]=deedee + +[a] +"[]"=a square? +av=a val +cr[]=four +cr[]=eight +e={ o: p, a: { av: a val, b: { c: { e: "this [value]" } } } } +j="\\"{ o: \\"p\\", a: { av: \\"a val\\", b: { c: { e: \\"this [value]\\" } } } }\\"" + +[a.b.c] +e=1 +j=2 + +[x\\.y\\.z] +x.y.z=xyz + +[x\\.y\\.z.a\\.b\\.c] +a.b.c=abc +noHashComment=this\\# this is not a comment +nocomment=this\\; this is not a comment + +` + exports[`test/foo.js TAP encode with whitespace > must match snapshot 1`] = ` [log] type = file diff --git a/test/foo.js b/test/foo.js index d65c60b..4d380e3 100644 --- a/test/foo.js +++ b/test/foo.js @@ -60,3 +60,27 @@ test('encode with platform=win32', function (t) { t.matchSnapshot(e.split('\r\n')) t.end() }) + +test('encode with align', function (t) { + const d = i.decode(data) + const e = i.encode(d, { align: true }) + + t.matchSnapshot(e) + t.end() +}) + +test('encode with sort', function (t) { + const d = i.decode(data) + const e = i.encode(d, { sort: true }) + + t.matchSnapshot(e) + t.end() +}) + +test('encode with align and sort', function (t) { + const d = i.decode(data) + const e = i.encode(d, { align: true, sort: true }) + + t.matchSnapshot(e) + t.end() +})