297 lines
8.1 KiB
JavaScript
297 lines
8.1 KiB
JavaScript
|
'use strict'
|
||
|
module.exports = stringify
|
||
|
module.exports.value = stringifyInline
|
||
|
|
||
|
function stringify (obj) {
|
||
|
if (obj === null) throw typeError('null')
|
||
|
if (obj === void (0)) throw typeError('undefined')
|
||
|
if (typeof obj !== 'object') throw typeError(typeof obj)
|
||
|
|
||
|
if (typeof obj.toJSON === 'function') obj = obj.toJSON()
|
||
|
if (obj == null) return null
|
||
|
const type = tomlType(obj)
|
||
|
if (type !== 'table') throw typeError(type)
|
||
|
return stringifyObject('', '', obj)
|
||
|
}
|
||
|
|
||
|
function typeError (type) {
|
||
|
return new Error('Can only stringify objects, not ' + type)
|
||
|
}
|
||
|
|
||
|
function arrayOneTypeError () {
|
||
|
return new Error("Array values can't have mixed types")
|
||
|
}
|
||
|
|
||
|
function getInlineKeys (obj) {
|
||
|
return Object.keys(obj).filter(key => isInline(obj[key]))
|
||
|
}
|
||
|
function getComplexKeys (obj) {
|
||
|
return Object.keys(obj).filter(key => !isInline(obj[key]))
|
||
|
}
|
||
|
|
||
|
function toJSON (obj) {
|
||
|
let nobj = Array.isArray(obj) ? [] : Object.prototype.hasOwnProperty.call(obj, '__proto__') ? {['__proto__']: undefined} : {}
|
||
|
for (let prop of Object.keys(obj)) {
|
||
|
if (obj[prop] && typeof obj[prop].toJSON === 'function' && !('toISOString' in obj[prop])) {
|
||
|
nobj[prop] = obj[prop].toJSON()
|
||
|
} else {
|
||
|
nobj[prop] = obj[prop]
|
||
|
}
|
||
|
}
|
||
|
return nobj
|
||
|
}
|
||
|
|
||
|
function stringifyObject (prefix, indent, obj) {
|
||
|
obj = toJSON(obj)
|
||
|
var inlineKeys
|
||
|
var complexKeys
|
||
|
inlineKeys = getInlineKeys(obj)
|
||
|
complexKeys = getComplexKeys(obj)
|
||
|
var result = []
|
||
|
var inlineIndent = indent || ''
|
||
|
inlineKeys.forEach(key => {
|
||
|
var type = tomlType(obj[key])
|
||
|
if (type !== 'undefined' && type !== 'null') {
|
||
|
result.push(inlineIndent + stringifyKey(key) + ' = ' + stringifyAnyInline(obj[key], true))
|
||
|
}
|
||
|
})
|
||
|
if (result.length > 0) result.push('')
|
||
|
var complexIndent = prefix && inlineKeys.length > 0 ? indent + ' ' : ''
|
||
|
complexKeys.forEach(key => {
|
||
|
result.push(stringifyComplex(prefix, complexIndent, key, obj[key]))
|
||
|
})
|
||
|
return result.join('\n')
|
||
|
}
|
||
|
|
||
|
function isInline (value) {
|
||
|
switch (tomlType(value)) {
|
||
|
case 'undefined':
|
||
|
case 'null':
|
||
|
case 'integer':
|
||
|
case 'nan':
|
||
|
case 'float':
|
||
|
case 'boolean':
|
||
|
case 'string':
|
||
|
case 'datetime':
|
||
|
return true
|
||
|
case 'array':
|
||
|
return value.length === 0 || tomlType(value[0]) !== 'table'
|
||
|
case 'table':
|
||
|
return Object.keys(value).length === 0
|
||
|
/* istanbul ignore next */
|
||
|
default:
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function tomlType (value) {
|
||
|
if (value === undefined) {
|
||
|
return 'undefined'
|
||
|
} else if (value === null) {
|
||
|
return 'null'
|
||
|
/* eslint-disable valid-typeof */
|
||
|
} else if (typeof value === 'bigint' || (Number.isInteger(value) && !Object.is(value, -0))) {
|
||
|
return 'integer'
|
||
|
} else if (typeof value === 'number') {
|
||
|
return 'float'
|
||
|
} else if (typeof value === 'boolean') {
|
||
|
return 'boolean'
|
||
|
} else if (typeof value === 'string') {
|
||
|
return 'string'
|
||
|
} else if ('toISOString' in value) {
|
||
|
return isNaN(value) ? 'undefined' : 'datetime'
|
||
|
} else if (Array.isArray(value)) {
|
||
|
return 'array'
|
||
|
} else {
|
||
|
return 'table'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function stringifyKey (key) {
|
||
|
var keyStr = String(key)
|
||
|
if (/^[-A-Za-z0-9_]+$/.test(keyStr)) {
|
||
|
return keyStr
|
||
|
} else {
|
||
|
return stringifyBasicString(keyStr)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function stringifyBasicString (str) {
|
||
|
return '"' + escapeString(str).replace(/"/g, '\\"') + '"'
|
||
|
}
|
||
|
|
||
|
function stringifyLiteralString (str) {
|
||
|
return "'" + str + "'"
|
||
|
}
|
||
|
|
||
|
function numpad (num, str) {
|
||
|
while (str.length < num) str = '0' + str
|
||
|
return str
|
||
|
}
|
||
|
|
||
|
function escapeString (str) {
|
||
|
return str.replace(/\\/g, '\\\\')
|
||
|
.replace(/[\b]/g, '\\b')
|
||
|
.replace(/\t/g, '\\t')
|
||
|
.replace(/\n/g, '\\n')
|
||
|
.replace(/\f/g, '\\f')
|
||
|
.replace(/\r/g, '\\r')
|
||
|
/* eslint-disable no-control-regex */
|
||
|
.replace(/([\u0000-\u001f\u007f])/, c => '\\u' + numpad(4, c.codePointAt(0).toString(16)))
|
||
|
/* eslint-enable no-control-regex */
|
||
|
}
|
||
|
|
||
|
function stringifyMultilineString (str) {
|
||
|
let escaped = str.split(/\n/).map(str => {
|
||
|
return escapeString(str).replace(/"(?="")/g, '\\"')
|
||
|
}).join('\n')
|
||
|
if (escaped.slice(-1) === '"') escaped += '\\\n'
|
||
|
return '"""\n' + escaped + '"""'
|
||
|
}
|
||
|
|
||
|
function stringifyAnyInline (value, multilineOk) {
|
||
|
let type = tomlType(value)
|
||
|
if (type === 'string') {
|
||
|
if (multilineOk && /\n/.test(value)) {
|
||
|
type = 'string-multiline'
|
||
|
} else if (!/[\b\t\n\f\r']/.test(value) && /"/.test(value)) {
|
||
|
type = 'string-literal'
|
||
|
}
|
||
|
}
|
||
|
return stringifyInline(value, type)
|
||
|
}
|
||
|
|
||
|
function stringifyInline (value, type) {
|
||
|
/* istanbul ignore if */
|
||
|
if (!type) type = tomlType(value)
|
||
|
switch (type) {
|
||
|
case 'string-multiline':
|
||
|
return stringifyMultilineString(value)
|
||
|
case 'string':
|
||
|
return stringifyBasicString(value)
|
||
|
case 'string-literal':
|
||
|
return stringifyLiteralString(value)
|
||
|
case 'integer':
|
||
|
return stringifyInteger(value)
|
||
|
case 'float':
|
||
|
return stringifyFloat(value)
|
||
|
case 'boolean':
|
||
|
return stringifyBoolean(value)
|
||
|
case 'datetime':
|
||
|
return stringifyDatetime(value)
|
||
|
case 'array':
|
||
|
return stringifyInlineArray(value.filter(_ => tomlType(_) !== 'null' && tomlType(_) !== 'undefined' && tomlType(_) !== 'nan'))
|
||
|
case 'table':
|
||
|
return stringifyInlineTable(value)
|
||
|
/* istanbul ignore next */
|
||
|
default:
|
||
|
throw typeError(type)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function stringifyInteger (value) {
|
||
|
/* eslint-disable security/detect-unsafe-regex */
|
||
|
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, '_')
|
||
|
}
|
||
|
|
||
|
function stringifyFloat (value) {
|
||
|
if (value === Infinity) {
|
||
|
return 'inf'
|
||
|
} else if (value === -Infinity) {
|
||
|
return '-inf'
|
||
|
} else if (Object.is(value, NaN)) {
|
||
|
return 'nan'
|
||
|
} else if (Object.is(value, -0)) {
|
||
|
return '-0.0'
|
||
|
}
|
||
|
var chunks = String(value).split('.')
|
||
|
var int = chunks[0]
|
||
|
var dec = chunks[1] || 0
|
||
|
return stringifyInteger(int) + '.' + dec
|
||
|
}
|
||
|
|
||
|
function stringifyBoolean (value) {
|
||
|
return String(value)
|
||
|
}
|
||
|
|
||
|
function stringifyDatetime (value) {
|
||
|
return value.toISOString()
|
||
|
}
|
||
|
|
||
|
function isNumber (type) {
|
||
|
return type === 'float' || type === 'integer'
|
||
|
}
|
||
|
function arrayType (values) {
|
||
|
var contentType = tomlType(values[0])
|
||
|
if (values.every(_ => tomlType(_) === contentType)) return contentType
|
||
|
// mixed integer/float, emit as floats
|
||
|
if (values.every(_ => isNumber(tomlType(_)))) return 'float'
|
||
|
return 'mixed'
|
||
|
}
|
||
|
function validateArray (values) {
|
||
|
const type = arrayType(values)
|
||
|
if (type === 'mixed') {
|
||
|
throw arrayOneTypeError()
|
||
|
}
|
||
|
return type
|
||
|
}
|
||
|
|
||
|
function stringifyInlineArray (values) {
|
||
|
values = toJSON(values)
|
||
|
const type = validateArray(values)
|
||
|
var result = '['
|
||
|
var stringified = values.map(_ => stringifyInline(_, type))
|
||
|
if (stringified.join(', ').length > 60 || /\n/.test(stringified)) {
|
||
|
result += '\n ' + stringified.join(',\n ') + '\n'
|
||
|
} else {
|
||
|
result += ' ' + stringified.join(', ') + (stringified.length > 0 ? ' ' : '')
|
||
|
}
|
||
|
return result + ']'
|
||
|
}
|
||
|
|
||
|
function stringifyInlineTable (value) {
|
||
|
value = toJSON(value)
|
||
|
var result = []
|
||
|
Object.keys(value).forEach(key => {
|
||
|
result.push(stringifyKey(key) + ' = ' + stringifyAnyInline(value[key], false))
|
||
|
})
|
||
|
return '{ ' + result.join(', ') + (result.length > 0 ? ' ' : '') + '}'
|
||
|
}
|
||
|
|
||
|
function stringifyComplex (prefix, indent, key, value) {
|
||
|
var valueType = tomlType(value)
|
||
|
/* istanbul ignore else */
|
||
|
if (valueType === 'array') {
|
||
|
return stringifyArrayOfTables(prefix, indent, key, value)
|
||
|
} else if (valueType === 'table') {
|
||
|
return stringifyComplexTable(prefix, indent, key, value)
|
||
|
} else {
|
||
|
throw typeError(valueType)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function stringifyArrayOfTables (prefix, indent, key, values) {
|
||
|
values = toJSON(values)
|
||
|
validateArray(values)
|
||
|
var firstValueType = tomlType(values[0])
|
||
|
/* istanbul ignore if */
|
||
|
if (firstValueType !== 'table') throw typeError(firstValueType)
|
||
|
var fullKey = prefix + stringifyKey(key)
|
||
|
var result = ''
|
||
|
values.forEach(table => {
|
||
|
if (result.length > 0) result += '\n'
|
||
|
result += indent + '[[' + fullKey + ']]\n'
|
||
|
result += stringifyObject(fullKey + '.', indent, table)
|
||
|
})
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
function stringifyComplexTable (prefix, indent, key, value) {
|
||
|
var fullKey = prefix + stringifyKey(key)
|
||
|
var result = ''
|
||
|
if (getInlineKeys(value).length > 0) {
|
||
|
result += indent + '[' + fullKey + ']\n'
|
||
|
}
|
||
|
return result + stringifyObject(fullKey + '.', indent, value)
|
||
|
}
|