Skip to content

Commit

Permalink
Start lines that need folding on a new line (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
eemeli authored Feb 25, 2024
1 parent 0682dd1 commit 845e56d
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 93 deletions.
189 changes: 97 additions & 92 deletions lib/stringify.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
const { Pair, Comment, EmptyLine } = require('./ast')

const escapeNonPrintable = (str, latin1) => {
const re =
latin1 !== false ? /[^\t\n\f\r -~\xa1-\xff]/g : /[\0-\b\v\x0e-\x1f]/g
return String(str).replace(re, ch => {
const esc = ch.charCodeAt(0).toString(16)
return '\\u' + ('0000' + esc).slice(-4)
})
}
const escapeNonPrintable = (str, latin1) =>
String(str).replace(
latin1 !== false ? /[^\t\n\f\r -~\xa1-\xff]/g : /[\0-\b\v\x0e-\x1f]/g,
ch => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')
)

const escape = str =>
String(str)
Expand All @@ -16,81 +13,84 @@ const escape = str =>
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
const escapeKey = key => escape(key).replace(/[ =:]/g, '\\$&')
const escapeValue = value => escape(value).replace(/^ /, '\\ ')

const pairWithSeparator = (key, value, sep) =>
escape(key).replace(/[ =:]/g, '\\$&') +
sep +
escape(value).replace(/^ /, '\\ ')

const commentWithPrefix = (str, prefix) =>
const prefixComment = (str, prefix) =>
str.replace(/^\s*([#!][ \t\f]*)?/g, prefix)

const getFold =
({ indent, latin1, lineWidth, newline }) =>
line => {
if (!lineWidth || lineWidth < 0) return line
line = escapeNonPrintable(line, latin1)
let start = 0
let split = undefined
for (let i = 0, ch = line[0]; ch; ch = line[(i += 1)]) {
let end = i - start >= lineWidth ? split || i : undefined
if (!end) {
switch (ch) {
case '\r':
if (line[i + 1] === '\n') i += 1
// fallthrough
case '\n':
end = i + 1
break
case '\\':
i += 1
switch (line[i]) {
case 'r':
if (line[i + 1] === '\\' && line[i + 2] === 'n') i += 2
// fallthrough
case 'n':
end = i + 1
break
case ' ':
case 'f':
case 't':
split = i + 1
break
}
break
case '\f':
case '\t':
case ' ':
case '.':
split = i + 1
break
}
function fold({ indent, latin1, lineWidth, newline }, key, value) {
const printKey = escapeNonPrintable(key, latin1)
const printValue = escapeNonPrintable(value, latin1)
let line = printKey + printValue
if (!lineWidth || lineWidth < 0 || line.length <= lineWidth) return line
let start = 0
let split = undefined
let i = 0
if (key && printKey.length < lineWidth) {
line = printKey + newline + indent + printValue
start = printKey.length + newline.length
i = start + indent.length
}
for (let ch = line[i]; ch; ch = line[++i]) {
let end = i - start >= lineWidth ? split || i : undefined
if (!end) {
switch (ch) {
case '\r':
if (line[i + 1] === '\n') i += 1
// fallthrough
case '\n':
end = i + 1
break
case '\\':
i += 1
switch (line[i]) {
case 'r':
if (line[i + 1] === '\\' && line[i + 2] === 'n') i += 2
// fallthrough
case 'n':
end = i + 1
break
case ' ':
case 'f':
case 't':
split = i + 1
break
}
break
case '\f':
case '\t':
case ' ':
case '.':
split = i + 1
break
}
if (end) {
let lineEnd = end
let ch = line[lineEnd - 1]
while (ch === '\n' || ch === '\r') {
lineEnd -= 1
ch = line[lineEnd - 1]
}
const next = line[end]
const atWhitespace = next === '\t' || next === '\f' || next === ' '
line =
line.slice(0, lineEnd) +
newline +
indent +
(atWhitespace ? '\\' : '') +
line.slice(end)
start = lineEnd + newline.length
split = undefined
i = start + indent.length - 1
}
if (end) {
let lineEnd = end
let ch = line[lineEnd - 1]
while (ch === '\n' || ch === '\r') {
lineEnd -= 1
ch = line[lineEnd - 1]
}
const next = line[end]
const atWhitespace = next === '\t' || next === '\f' || next === ' '
line =
line.slice(0, lineEnd) +
newline +
indent +
(atWhitespace ? '\\' : '') +
line.slice(end)
start = lineEnd + newline.length
split = undefined
i = start + indent.length - 1
}
return line
}
return line
}

const toLines = (obj, pathSep, defaultKey, prefix = '') => {
return Object.keys(obj).reduce((lines, key) => {
const toLines = (obj, pathSep, defaultKey, prefix = '') =>
Object.keys(obj).reduce((lines, key) => {
const value = obj[key]
if (value && typeof value === 'object') {
return lines.concat(
Expand All @@ -103,7 +103,6 @@ const toLines = (obj, pathSep, defaultKey, prefix = '') => {
return lines
}
}, [])
}

function stringify(
input,
Expand All @@ -120,18 +119,8 @@ function stringify(
) {
if (!input) return ''
if (!Array.isArray(input)) input = toLines(input, pathSep, defaultKey)
const foldLine = getFold({
indent,
latin1,
lineWidth,
newline: '\\' + newline
})
const foldComment = getFold({
indent: commentPrefix,
latin1,
lineWidth,
newline
})
const lineOpt = { indent, latin1, lineWidth, newline: '\\' + newline }
const commentOpt = { indent: commentPrefix, latin1, lineWidth, newline }
return input
.map(line => {
switch (true) {
Expand All @@ -140,14 +129,30 @@ function stringify(
return ''

case Array.isArray(line):
return foldLine(pairWithSeparator(line[0], line[1], keySep))
return fold(
lineOpt,
escapeKey(line[0]) + keySep,
escapeValue(line[1])
)
case line instanceof Pair:
return foldLine(pairWithSeparator(line.key, line.value, keySep))
return fold(
lineOpt,
escapeKey(line.key) + keySep,
escapeValue(line.value)
)

case line instanceof Comment:
return foldComment(commentWithPrefix(line.comment, commentPrefix))
return fold(
commentOpt,
'',
prefixComment(line.comment, commentPrefix)
)
default:
return foldComment(commentWithPrefix(String(line), commentPrefix))
return fold(
commentOpt,
'',
prefixComment(String(line), commentPrefix)
)
}
})
.join(newline)
Expand Down
2 changes: 1 addition & 1 deletion tests/corner-cases.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ occaecat cupidatat non proident, sunt in culpa qui officia\r
deserunt mollit anim id est laborum.`
expect(stringify([lorem])).toBe(lorem.replace(/\r\n/gm, '\n# ').trim())
expect(stringify([['key', lorem]])).toBe(
'key = ' + lorem.replace(/\r\n/g, '\\r\\n\\\n ')
'key = \\\n ' + lorem.replace(/\r\n/g, '\\r\\n\\\n ')
)
})

Expand Down
50 changes: 50 additions & 0 deletions tests/long-folds.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { stringify } = require('../lib/index')

describe('Folding for long lines', () => {
test('long value starts on new line', () => {
const lines = [
['k0', '0 val0'],
['k1', '1 value1'],
['key', 'v0'],
['key1', 'value1'],
['key2', 'value2 continues']
]
expect(stringify(lines, { indent: ' ', lineWidth: 8 })).toBe(
`\
k0 = \\
0 val0
k1 = \\
1 \\
value1
key = v0
key1 = \\
value1
key2 = \\
value2\\
\\ \\
contin\\
ues`
)
})

test('values after long keys do not start on new line', () => {
const lines = [
['longish key', 'v0'],
['longkeyxx', '42'],
['somelongkey', 'withlongvalue']
]
expect(stringify(lines, { indent: ' ', lineWidth: 8 })).toBe(
`\
longish\\ \\
key = \\
v0
longkeyx\\
x = 42
somelong\\
key = \\
withlo\\
ngvalu\\
e`
)
})
})

0 comments on commit 845e56d

Please sign in to comment.