Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const config = parse(text)

config.scope = 'local'
config.database.database = 'use_another_database'
config.database.description = 'this is a multiline\n value -- continuation lines\n must be indented'
config.paths.default.tmpdir = '/tmp'
delete config.paths.default.datadir
config.paths.default.array.push('fourth value')
Expand All @@ -71,6 +72,9 @@ scope=local
user=dbuser
password=dbpassword
database=use_another_database
description=this is a multiline
value -- continuation lines
must be indented
[section.paths.default]
tmpdir=/tmp
array[]=first value
Expand All @@ -87,7 +91,23 @@ 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>`, {
/**
* Read indented lines that directly follow a key/value pair as
* continuation lines of that value, the way Python's configparser
* does. Enabled by default; see "Multiline values" below.
*
* Set to `false` to read those indented lines as standalone keys,
* which is how versions before multiline support read them.
*/
multiline: true,

/**
* Whether to treat repeated keys without a `[]` suffix as arrays.
* Enabled by default to match the .ini format used by npm.
*/
bracketedArray: true,
})
```

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

bracketedArray : true
bracketedArray : true,

/**
* A string value containing newlines is written as indented
* continuation lines when `parse()` can read it back unchanged
* (see "Multiline values" below), and JSON-quoted otherwise,
* as before multiline support.
*
* Set to `true` to have `stringify()` throw instead of falling
* back to JSON quoting, for output that must stay readable by
* parsers that do not understand quoted values.
*/
strictMultiline : false

})
```
Expand All @@ -164,6 +196,25 @@ stringify(object,{
stringify(object,'section')
```

### Multiline values

A value continues onto the following lines when each of them starts with
a space or tab. The continuation lines are kept verbatim, indentation
included, joined with `\n`:

```ini
description=first line
second line
third line
```

`parse()` ends the value at a blank line, a section header, or any line
that is not indented, and skips comment lines in between. `stringify()`
writes a value this way only when every line after the first is indented,
is not blank, contains no `=`, and does not start with `;` or `#`, and the
value contains no carriage return. Any other value with newlines is
JSON-quoted, so `parse(stringify(x))` gives back `x` either way.

### Un / Escape

Turn the given string into a safe to
Expand Down
76 changes: 72 additions & 4 deletions lib/ini.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
const { hasOwnProperty } = Object.prototype

// A string value with embedded newlines can be written verbatim as indented
// continuation lines only if the decoder will read every one of those lines
// back as part of the same value. Returns the lines to write, or null when
// the value has to be JSON-quoted instead.
const continuationLines = val => {
// The decoder drops a CR that precedes LF, and on win32 the platform eol
// would add a second one.
if (val.includes('\r')) {
return null
}
const lines = val.split('\n')
for (const ln of lines.slice(1)) {
if (
!/^[ \t]/.test(ln) || // must be indented
!/\S/.test(ln) || // a blank line ends the value
ln.includes('=') || // would parse as a new key
/^\s*[;#]/.test(ln) // would be skipped as a comment
) {
return null
}
}
return lines
}

const encode = (obj, opt = {}) => {
if (typeof opt === 'string') {
opt = { section: opt }
Expand All @@ -8,6 +32,7 @@ const encode = (obj, opt = {}) => {
opt.newline = opt.newline === true
opt.sort = opt.sort === true
opt.whitespace = opt.whitespace === true || opt.align === true
opt.strictMultiline = opt.strictMultiline === true
// 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)
Expand Down Expand Up @@ -43,16 +68,36 @@ const encode = (obj, opt = {}) => {
let out = ''
const arraySuffix = opt.bracketedArray ? '[]' : ''

// Write one key/value line, as indented continuation lines when the value
// allows it.
const entry = (key, val) => {
let text = null
if (typeof val === 'string' && val.includes('\n')) {
const lines = continuationLines(val)
if (lines) {
// The first line is escaped like any other value; the continuation
// lines are read back verbatim.
text = safe(lines[0]) + eol + lines.slice(1).join(eol)
} else if (opt.strictMultiline) {
throw new Error(`Value of "${key}" has an embedded newline but cannot be written as indented continuation lines. Disable strictMultiline to allow such a value.`)
}
}
if (text === null) {
text = safe(val)
}
out += safe(key).padEnd(padToChars, ' ') + separator + text + eol
}

for (const k of keys) {
const val = obj[k]
if (val && Array.isArray(val)) {
for (const item of val) {
out += safe(`${k}${arraySuffix}`).padEnd(padToChars, ' ') + separator + safe(item) + eol
entry(`${k}${arraySuffix}`, item)
}
} else if (val && typeof val === 'object') {
children.push(k)
} else {
out += safe(k).padEnd(padToChars, ' ') + separator + safe(val) + eol
entry(k, val)
}
}

Expand Down Expand Up @@ -105,18 +150,35 @@ function splitSections (str, separator) {

const decode = (str, opt = {}) => {
opt.bracketedArray = opt.bracketedArray !== false
opt.multiline = opt.multiline !== 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 lines = str.split(/\r?\n/g)
const duplicates = {}
// While an indented line may still continue the most recent string value,
// `cont` says where that value lives: [object or array, key or index].
// Anything other than a comment or a continuation line clears it.
let cont = null

for (const line of lines) {
if (!line || line.match(/^\s*[;#]/) || line.match(/^\s*$/)) {
if (line.match(/^\s*[;#]/)) {
// comments do not break continuation, just skip
continue
}
if (line.match(/^\s*$/)) {
// blank lines break continuation
cont = null
continue
}
if (cont && /^[ \t]/.test(line) && line.indexOf('=') === -1) {
const [target, slot] = cont
target[slot] += '\n' + line
continue
}
cont = null
const match = line.match(re)
if (!match) {
continue
Expand Down Expand Up @@ -165,8 +227,14 @@ const decode = (str, opt = {}) => {
// array by accidentally forgetting the brackets
if (Array.isArray(p[key])) {
p[key].push(value)
if (opt.multiline && typeof value === 'string') {
cont = [p[key], p[key].length - 1]
}
} else {
p[key] = value
if (opt.multiline && typeof value === 'string') {
cont = [p, key]
}
}
}

Expand Down
Loading