Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support parse and stringify with comments #247

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ Attempts to turn the given INI string into a nested data object.

```js
// You can also use `decode`
const object = parse(`<INI Text>`)
const object = parse(`<INI Text>`)

// Preserve the comments when parsing
const objectWithComments = parse(`<INI Text>`, { preserveComments: true })
```

### Stringify
Expand Down Expand Up @@ -152,8 +155,20 @@ stringify(object,{
* Some parsers treat duplicate names by themselves as arrays
*/

bracketedArray : true
bracketedArray : true,

/**
* Use a custom dictionary object key for storing comments. Default value is comments.
* If have a key in your object named comments, you will need to pass in a different value here.
*/

commentsKey,

/**
* Whether or not to save comments during stringify. Default value is false.
*/

preserveComments,
})
```

Expand Down
53 changes: 51 additions & 2 deletions lib/ini.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
/* istanbul ignore next */
opt.platform = opt.platform || (typeof process !== 'undefined' && process.platform)
opt.bracketedArray = opt.bracketedArray !== false
const commentsKey = opt.commentsKey || 'comments'
const commentsKeyRegEx = new RegExp(commentsKey, 'i')
wraithgar marked this conversation as resolved.
Show resolved Hide resolved
const commentsDictionary = hasOwnProperty.call(obj, commentsKey) ? obj[commentsKey] : {}

/* istanbul ignore next */
const eol = opt.platform === 'win32' ? '\r\n' : '\n'
Expand Down Expand Up @@ -46,24 +49,40 @@
for (const k of keys) {
const val = obj[k]
if (val && Array.isArray(val)) {
if (hasOwnProperty.call(commentsDictionary, k)) {
out += commentsDictionary[k]
}
for (const item of val) {
out += safe(`${k}${arraySuffix}`).padEnd(padToChars, ' ') + separator + safe(item) + eol
}
} else if (val && typeof val === 'object') {
children.push(k)
} else {
if (hasOwnProperty.call(commentsDictionary, k)) {
out += commentsDictionary[k]
}
out += safe(k).padEnd(padToChars, ' ') + separator + safe(val) + eol
}
}

if (opt.section && out.length) {
out = '[' + safe(opt.section) + ']' + (opt.newline ? eol + eol : eol) + out
let sectionComments = ''
if (hasOwnProperty.call(commentsDictionary, opt.section)) {
sectionComments = commentsDictionary[opt.section]
}
out = sectionComments + '[' + safe(opt.section) + ']' + (opt.newline ? eol + eol : eol) + out
}

for (const k of children) {
if (commentsKeyRegEx.test(k)) {
continue
}
const nk = splitSections(k, '.').join('\\.')
const section = (opt.section ? opt.section + '.' : '') + nk
const child = encode(obj[k], {
const childObject = hasOwnProperty.call(obj, commentsKey) ?
Object.assign(obj[k], { [commentsKey]: obj[commentsKey] }) :
obj[k]
const child = encode(childObject, {
...opt,
section,
})
Expand Down Expand Up @@ -104,16 +123,30 @@
}

const decode = (str, opt = {}) => {
// The `typeof` check is required because accessing the `process` directly fails on browsers.
/* istanbul ignore next */
opt.platform = opt.platform || (typeof process !== 'undefined' && process.platform)
/* istanbul ignore next */
const eol = opt.platform === 'win32' ? '\r\n' : '\n'
opt.bracketedArray = opt.bracketedArray !== false
const out = Object.create(null)
let p = out
let section = null
const preserveComments = opt.preserveComments || false
const commentsKey = opt.commentsKey || 'comments'
const commentsDictionary = {}
let lineCommentArray = []
const commentsRegEx = /^[#;]{1,}.*$/
// 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(commentsRegEx)) {
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
const commentsMatch = line.match(commentsRegEx)[0]
Fixed Show fixed Hide fixed
lineCommentArray.push(commentsMatch)
}
if (!line || line.match(/^\s*[;#]/) || line.match(/^\s*$/)) {
wraithgar marked this conversation as resolved.
Show resolved Hide resolved
continue
}
Expand All @@ -130,6 +163,10 @@
continue
}
p = out[section] = out[section] || Object.create(null)
if (lineCommentArray.length > 0) {
commentsDictionary[section] = lineCommentArray.join(eol) + eol
lineCommentArray = []
}
continue
}
const keyRaw = unsafe(match[2])
Expand Down Expand Up @@ -163,8 +200,16 @@
// array by accidentally forgetting the brackets
if (Array.isArray(p[key])) {
p[key].push(value)
if (lineCommentArray.length > 0) {
commentsDictionary[key] = lineCommentArray.join(eol) + eol
lineCommentArray = []
}
} else {
p[key] = value
if (lineCommentArray.length > 0) {
commentsDictionary[key] = lineCommentArray.join(eol) + eol
lineCommentArray = []
}
}
}

Expand Down Expand Up @@ -204,6 +249,10 @@
delete out[del]
}

if (preserveComments) {
out[commentsKey] = commentsDictionary
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will work because the comments key on the returned object could have already been set in the ini text being decoded.

Here's an example:

> require('ini').decode('comments = this is in my ini file')
{ comments: 'this is in my ini file' }

> require('ini').decode('comments = this is in my ini file',{preserveComments:true})
{ comments: {} }

I think the best strategy here would be to follow what json-parse-even-better-errors does and store this data as a symbol on the returned object which can then be read back when encoding. Here's a reference to that code: https://github.com/npm/json-parse-even-better-errors/blob/9355df83b4ce4567711823fc1b40fe63dbeebba5/lib/index.js#L106-L108

Another option would be new methods like decodeWithComments which could return a tuple of [data, comments] which could then be passed back to a new encodeWithComments. But I think the symbols would be a better API if that will work.

return out
}

Expand Down
148 changes: 148 additions & 0 deletions tap-snapshots/test/foo.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,151 @@ label=debug
value=10

`

exports[`test/foo.js TAP stringify with comments > must match snapshot 1`] = `
o=p
a with spaces=b c
; wrap in quotes to JSON-decode and preserve spaces
" xa n p "="\\"\\r\\nyoyoyo\\r\\r\\n"
; wrap in quotes to get a key with a bracket, not a section.
"[disturbing]"=hey you never know
; Test single quotes
s=something
; Test mixing quotes
s1="something'
; Test double quotes
s2=something else
; Test blank value
s3=
; Test value with only spaces
s4=
; Test quoted value with only spaces
s5=" "
; Test quoted value with leading and trailing spaces
s6=" a "
; Test no equal sign
s7=true
; Test bool(true)
true=true
; Test bool(false)
false=false
; Test null
null=null
; Test undefined
undefined=undefined
; Test arrays
zr[]=deedee
; This should be included in the array
ar[]=one
ar[]=three
ar[]=this is included
; Test resetting of a value (and not turn it into an array)
br=warm
eq="eq=eq"

; a section
[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?
; Nested array
cr[]=four
cr[]=eight

; nested child without middle parent
; should create otherwise-empty a.b
[a.b.c]
e=1
j=2

; dots in the section name should be literally interpreted
[x\\.y\\.z]
x.y.z=xyz

[x\\.y\\.z.a\\.b\\.c]
; nested child without middle parent
; should create otherwise-empty a.b
a.b.c=abc
; this next one is not a comment! it's escaped!
nocomment=this\\; this is not a comment
# Support the use of the number sign (#) as an alternative to the semicolon for indicating comments.
# http://en.wikipedia.org/wiki/INI_file#Comments
# this next one is not a comment! it's escaped!
noHashComment=this\\# this is not a comment

`

exports[`test/foo.js TAP stringify with comments custom commentsKey > must match snapshot 1`] = `
o=p
a with spaces=b c
; wrap in quotes to JSON-decode and preserve spaces
" xa n p "="\\"\\r\\nyoyoyo\\r\\r\\n"
; wrap in quotes to get a key with a bracket, not a section.
"[disturbing]"=hey you never know
; Test single quotes
s=something
; Test mixing quotes
s1="something'
; Test double quotes
s2=something else
; Test blank value
s3=
; Test value with only spaces
s4=
; Test quoted value with only spaces
s5=" "
; Test quoted value with leading and trailing spaces
s6=" a "
; Test no equal sign
s7=true
; Test bool(true)
true=true
; Test bool(false)
false=false
; Test null
null=null
; Test undefined
undefined=undefined
; Test arrays
zr[]=deedee
; This should be included in the array
ar[]=one
ar[]=three
ar[]=this is included
; Test resetting of a value (and not turn it into an array)
br=warm
eq="eq=eq"

; a section
[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?
; Nested array
cr[]=four
cr[]=eight

; nested child without middle parent
; should create otherwise-empty a.b
[a.b.c]
e=1
j=2

; dots in the section name should be literally interpreted
[x\\.y\\.z]
x.y.z=xyz

[x\\.y\\.z.a\\.b\\.c]
; nested child without middle parent
; should create otherwise-empty a.b
a.b.c=abc
; this next one is not a comment! it's escaped!
nocomment=this\\; this is not a comment
# Support the use of the number sign (#) as an alternative to the semicolon for indicating comments.
# http://en.wikipedia.org/wiki/INI_file#Comments
# this next one is not a comment! it's escaped!
noHashComment=this\\# this is not a comment

`
13 changes: 13 additions & 0 deletions test/foo.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,16 @@ test('encode within browser context', function (t) {
t.matchSnapshot(e)
t.end()
})
test('stringify with comments', function (t) {
const d = i.parse(data, { preserveComments: true })
const s = i.stringify(d, { preserveComments: true })
t.matchSnapshot(s)
t.end()
})

test('stringify with comments custom commentsKey', function (t) {
const d = i.parse(data, { commentsKey: 'internalComments', preserveComments: true })
const s = i.stringify(d, { commentsKey: 'internalComments', preserveComments: true })
t.matchSnapshot(s)
t.end()
})