Skip to content
Closed
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
33 changes: 32 additions & 1 deletion lib/ini.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
const { hasOwnProperty } = Object.prototype

// Remember which keys were written with single quotes so stringify can
// restore them. Parse still unquotes the JS value (see PR #16).
const singleQuotedKeys = new WeakMap()

const markSingleQuoted = (obj, key) => {
let keys = singleQuotedKeys.get(obj)
if (!keys) {
keys = new Set()
singleQuotedKeys.set(obj, keys)
}
keys.add(key)
}

const emitValue = (obj, key, val) => {
const keys = singleQuotedKeys.get(obj)
if (!keys || !keys.has(key)) {
return safe(val)
}
const inner = typeof val === 'string' ? val : String(val)
// A raw `'` inside the value would break single-quote wrapping.
if (inner.includes("'")) {
return safe(val)
}
return "'" + inner + "'"
}

const encode = (obj, opt = {}) => {
if (typeof opt === 'string') {
opt = { section: opt }
Expand Down Expand Up @@ -52,7 +78,7 @@ const encode = (obj, opt = {}) => {
} else if (val && typeof val === 'object') {
children.push(k)
} else {
out += safe(k).padEnd(padToChars, ' ') + separator + safe(val) + eol
out += safe(k).padEnd(padToChars, ' ') + separator + emitValue(obj, k, val) + eol
}
}

Expand Down Expand Up @@ -146,6 +172,8 @@ const decode = (str, opt = {}) => {
if (key === '__proto__') {
continue
}
const trimmedValue = match[3] ? (match[4] || '').trim() : ''
const wasSingleQuoted = isQuoted(trimmedValue) && trimmedValue.startsWith("'")
const valueRaw = match[3] ? unsafe(match[4]) : true
const value = valueRaw === 'true' ||
valueRaw === 'false' ||
Expand All @@ -167,6 +195,9 @@ const decode = (str, opt = {}) => {
p[key].push(value)
} else {
p[key] = value
if (wasSingleQuoted) {
markSingleQuoted(p, key)
}
}
}

Expand Down
24 changes: 12 additions & 12 deletions tap-snapshots/test/foo.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ o=p
a with spaces=b c
" xa n p "="\\"\\r\\nyoyoyo\\r\\r\\n"
"[disturbing]"=hey you never know
s=something
s='something'
s1="something'
s2=something else
s3=
s4=
s5=" "
s6=" a "
s5=' '
s6=' a '
s7=true
true=true
false=false
Expand Down Expand Up @@ -116,13 +116,13 @@ o = p
a with spaces = b c
" xa n p " = "\\"\\r\\nyoyoyo\\r\\r\\n"
"[disturbing]" = hey you never know
s = something
s = 'something'
s1 = "something'
s2 = something else
s3 =
s4 =
s5 = " "
s6 = " a "
s5 = ' '
s6 = ' a '
s7 = true
true = true
false = false
Expand Down Expand Up @@ -169,13 +169,13 @@ eq = "eq=eq"
false = false
null = null
o = p
s = something
s = 'something'
s1 = "something'
s2 = something else
s3 =
s4 =
s5 = " "
s6 = " a "
s5 = ' '
s6 = ' a '
s7 = true
true = true
undefined = undefined
Expand Down Expand Up @@ -249,13 +249,13 @@ eq="eq=eq"
false=false
null=null
o=p
s=something
s='something'
s1="something'
s2=something else
s3=
s4=
s5=" "
s6=" a "
s5=' '
s6=' a '
s7=true
true=true
undefined=undefined
Expand Down
82 changes: 82 additions & 0 deletions test/single-quotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const ini = require('../')
const t = require('tap')

const issue242 = `ActiveIndex = '0'
MaximizedFlag = '1911'
ProductVersion = '14.0.25793.400'
Version = '1'
WindowCount = '1'`

t.test('decode then encode preserves single-quoted values (#242)', t => {
const parsed = ini.parse(issue242)

// Parse still unquotes for JS consumers (PR #16)
t.equal(parsed.ActiveIndex, 0)
t.equal(parsed.MaximizedFlag, 1911)
t.equal(parsed.ProductVersion, '14.0.25793.400')
t.equal(parsed.Version, 1)
t.equal(parsed.WindowCount, 1)

const encoded = ini.stringify(parsed)
t.equal(encoded, [
"ActiveIndex='0'",
"MaximizedFlag='1911'",
"ProductVersion='14.0.25793.400'",
"Version='1'",
"WindowCount='1'",
'',
].join('\n'))
t.end()
})

t.test('unquoted safe values stay unquoted', t => {
t.equal(ini.stringify(ini.parse('ActiveIndex = 0')), 'ActiveIndex=0\n')
t.equal(ini.stringify(ini.parse('name = something')), 'name=something\n')
t.equal(ini.stringify({ ActiveIndex: 0 }), 'ActiveIndex=0\n')
t.equal(ini.stringify({ ActiveIndex: '0' }), 'ActiveIndex=0\n')
t.end()
})

t.test('double-quoted safe values are still unquoted on encode', t => {
t.equal(ini.stringify(ini.parse('name = "something"')), 'name=something\n')
t.equal(ini.stringify(ini.parse('s2 = "something else"')), 's2=something else\n')
t.end()
})

t.test('double quotes still used for special characters', t => {
t.equal(ini.stringify({ eq: 'eq=eq' }), 'eq="eq=eq"\n')
t.equal(ini.stringify(ini.parse('eq = "eq=eq"')), 'eq="eq=eq"\n')
t.end()
})

t.test('whitespace option keeps restored single quotes', t => {
const encoded = ini.stringify(ini.parse("ActiveIndex = '0'"), { whitespace: true })
t.equal(encoded, "ActiveIndex = '0'\n")
t.end()
})

t.test('mixed quoted and unquoted keys in one object', t => {
const parsed = ini.parse("quoted = '1'\nplain = 2\n")
t.equal(ini.stringify(parsed), "quoted='1'\nplain=2\n")
t.end()
})

t.test('single-quoted value containing a quote falls back to safe()', t => {
const parsed = ini.parse("odd = 'a'b'\n")
t.equal(parsed.odd, "a'b")
t.equal(ini.stringify(parsed), "odd=a'b\n")
t.end()
})

t.test('single quotes are preserved inside sections', t => {
const parsed = ini.parse("[window]\nActiveIndex = '0'\nTitle = unquoted\n")
t.equal(parsed.window.ActiveIndex, 0)
t.equal(parsed.window.Title, 'unquoted')
t.equal(ini.stringify(parsed), "[window]\nActiveIndex='0'\nTitle=unquoted\n")
t.end()
})

t.test('empty single-quoted value round-trips', t => {
t.equal(ini.stringify(ini.parse("x = ''")), "x=''\n")
t.end()
})