stringify.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. 'use strict'
  2. module.exports = stringify
  3. module.exports.value = stringifyInline
  4. function stringify (obj) {
  5. if (obj === null) throw typeError('null')
  6. if (obj === void (0)) throw typeError('undefined')
  7. if (typeof obj !== 'object') throw typeError(typeof obj)
  8. if (typeof obj.toJSON === 'function') obj = obj.toJSON()
  9. if (obj == null) return null
  10. const type = tomlType(obj)
  11. if (type !== 'table') throw typeError(type)
  12. return stringifyObject('', '', obj)
  13. }
  14. function typeError (type) {
  15. return new Error('Can only stringify objects, not ' + type)
  16. }
  17. function arrayOneTypeError () {
  18. return new Error("Array values can't have mixed types")
  19. }
  20. function getInlineKeys (obj) {
  21. return Object.keys(obj).filter(key => isInline(obj[key]))
  22. }
  23. function getComplexKeys (obj) {
  24. return Object.keys(obj).filter(key => !isInline(obj[key]))
  25. }
  26. function toJSON (obj) {
  27. let nobj = Array.isArray(obj) ? [] : Object.prototype.hasOwnProperty.call(obj, '__proto__') ? {['__proto__']: undefined} : {}
  28. for (let prop of Object.keys(obj)) {
  29. if (obj[prop] && typeof obj[prop].toJSON === 'function' && !('toISOString' in obj[prop])) {
  30. nobj[prop] = obj[prop].toJSON()
  31. } else {
  32. nobj[prop] = obj[prop]
  33. }
  34. }
  35. return nobj
  36. }
  37. function stringifyObject (prefix, indent, obj) {
  38. obj = toJSON(obj)
  39. var inlineKeys
  40. var complexKeys
  41. inlineKeys = getInlineKeys(obj)
  42. complexKeys = getComplexKeys(obj)
  43. var result = []
  44. var inlineIndent = indent || ''
  45. inlineKeys.forEach(key => {
  46. var type = tomlType(obj[key])
  47. if (type !== 'undefined' && type !== 'null') {
  48. result.push(inlineIndent + stringifyKey(key) + ' = ' + stringifyAnyInline(obj[key], true))
  49. }
  50. })
  51. if (result.length > 0) result.push('')
  52. var complexIndent = prefix && inlineKeys.length > 0 ? indent + ' ' : ''
  53. complexKeys.forEach(key => {
  54. result.push(stringifyComplex(prefix, complexIndent, key, obj[key]))
  55. })
  56. return result.join('\n')
  57. }
  58. function isInline (value) {
  59. switch (tomlType(value)) {
  60. case 'undefined':
  61. case 'null':
  62. case 'integer':
  63. case 'nan':
  64. case 'float':
  65. case 'boolean':
  66. case 'string':
  67. case 'datetime':
  68. return true
  69. case 'array':
  70. return value.length === 0 || tomlType(value[0]) !== 'table'
  71. case 'table':
  72. return Object.keys(value).length === 0
  73. /* istanbul ignore next */
  74. default:
  75. return false
  76. }
  77. }
  78. function tomlType (value) {
  79. if (value === undefined) {
  80. return 'undefined'
  81. } else if (value === null) {
  82. return 'null'
  83. /* eslint-disable valid-typeof */
  84. } else if (typeof value === 'bigint' || (Number.isInteger(value) && !Object.is(value, -0))) {
  85. return 'integer'
  86. } else if (typeof value === 'number') {
  87. return 'float'
  88. } else if (typeof value === 'boolean') {
  89. return 'boolean'
  90. } else if (typeof value === 'string') {
  91. return 'string'
  92. } else if ('toISOString' in value) {
  93. return isNaN(value) ? 'undefined' : 'datetime'
  94. } else if (Array.isArray(value)) {
  95. return 'array'
  96. } else {
  97. return 'table'
  98. }
  99. }
  100. function stringifyKey (key) {
  101. var keyStr = String(key)
  102. if (/^[-A-Za-z0-9_]+$/.test(keyStr)) {
  103. return keyStr
  104. } else {
  105. return stringifyBasicString(keyStr)
  106. }
  107. }
  108. function stringifyBasicString (str) {
  109. return '"' + escapeString(str).replace(/"/g, '\\"') + '"'
  110. }
  111. function stringifyLiteralString (str) {
  112. return "'" + str + "'"
  113. }
  114. function numpad (num, str) {
  115. while (str.length < num) str = '0' + str
  116. return str
  117. }
  118. function escapeString (str) {
  119. return str.replace(/\\/g, '\\\\')
  120. .replace(/[\b]/g, '\\b')
  121. .replace(/\t/g, '\\t')
  122. .replace(/\n/g, '\\n')
  123. .replace(/\f/g, '\\f')
  124. .replace(/\r/g, '\\r')
  125. /* eslint-disable no-control-regex */
  126. .replace(/([\u0000-\u001f\u007f])/, c => '\\u' + numpad(4, c.codePointAt(0).toString(16)))
  127. /* eslint-enable no-control-regex */
  128. }
  129. function stringifyMultilineString (str) {
  130. let escaped = str.split(/\n/).map(str => {
  131. return escapeString(str).replace(/"(?="")/g, '\\"')
  132. }).join('\n')
  133. if (escaped.slice(-1) === '"') escaped += '\\\n'
  134. return '"""\n' + escaped + '"""'
  135. }
  136. function stringifyAnyInline (value, multilineOk) {
  137. let type = tomlType(value)
  138. if (type === 'string') {
  139. if (multilineOk && /\n/.test(value)) {
  140. type = 'string-multiline'
  141. } else if (!/[\b\t\n\f\r']/.test(value) && /"/.test(value)) {
  142. type = 'string-literal'
  143. }
  144. }
  145. return stringifyInline(value, type)
  146. }
  147. function stringifyInline (value, type) {
  148. /* istanbul ignore if */
  149. if (!type) type = tomlType(value)
  150. switch (type) {
  151. case 'string-multiline':
  152. return stringifyMultilineString(value)
  153. case 'string':
  154. return stringifyBasicString(value)
  155. case 'string-literal':
  156. return stringifyLiteralString(value)
  157. case 'integer':
  158. return stringifyInteger(value)
  159. case 'float':
  160. return stringifyFloat(value)
  161. case 'boolean':
  162. return stringifyBoolean(value)
  163. case 'datetime':
  164. return stringifyDatetime(value)
  165. case 'array':
  166. return stringifyInlineArray(value.filter(_ => tomlType(_) !== 'null' && tomlType(_) !== 'undefined' && tomlType(_) !== 'nan'))
  167. case 'table':
  168. return stringifyInlineTable(value)
  169. /* istanbul ignore next */
  170. default:
  171. throw typeError(type)
  172. }
  173. }
  174. function stringifyInteger (value) {
  175. /* eslint-disable security/detect-unsafe-regex */
  176. return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, '_')
  177. }
  178. function stringifyFloat (value) {
  179. if (value === Infinity) {
  180. return 'inf'
  181. } else if (value === -Infinity) {
  182. return '-inf'
  183. } else if (Object.is(value, NaN)) {
  184. return 'nan'
  185. } else if (Object.is(value, -0)) {
  186. return '-0.0'
  187. }
  188. var chunks = String(value).split('.')
  189. var int = chunks[0]
  190. var dec = chunks[1] || 0
  191. return stringifyInteger(int) + '.' + dec
  192. }
  193. function stringifyBoolean (value) {
  194. return String(value)
  195. }
  196. function stringifyDatetime (value) {
  197. return value.toISOString()
  198. }
  199. function isNumber (type) {
  200. return type === 'float' || type === 'integer'
  201. }
  202. function arrayType (values) {
  203. var contentType = tomlType(values[0])
  204. if (values.every(_ => tomlType(_) === contentType)) return contentType
  205. // mixed integer/float, emit as floats
  206. if (values.every(_ => isNumber(tomlType(_)))) return 'float'
  207. return 'mixed'
  208. }
  209. function validateArray (values) {
  210. const type = arrayType(values)
  211. if (type === 'mixed') {
  212. throw arrayOneTypeError()
  213. }
  214. return type
  215. }
  216. function stringifyInlineArray (values) {
  217. values = toJSON(values)
  218. const type = validateArray(values)
  219. var result = '['
  220. var stringified = values.map(_ => stringifyInline(_, type))
  221. if (stringified.join(', ').length > 60 || /\n/.test(stringified)) {
  222. result += '\n ' + stringified.join(',\n ') + '\n'
  223. } else {
  224. result += ' ' + stringified.join(', ') + (stringified.length > 0 ? ' ' : '')
  225. }
  226. return result + ']'
  227. }
  228. function stringifyInlineTable (value) {
  229. value = toJSON(value)
  230. var result = []
  231. Object.keys(value).forEach(key => {
  232. result.push(stringifyKey(key) + ' = ' + stringifyAnyInline(value[key], false))
  233. })
  234. return '{ ' + result.join(', ') + (result.length > 0 ? ' ' : '') + '}'
  235. }
  236. function stringifyComplex (prefix, indent, key, value) {
  237. var valueType = tomlType(value)
  238. /* istanbul ignore else */
  239. if (valueType === 'array') {
  240. return stringifyArrayOfTables(prefix, indent, key, value)
  241. } else if (valueType === 'table') {
  242. return stringifyComplexTable(prefix, indent, key, value)
  243. } else {
  244. throw typeError(valueType)
  245. }
  246. }
  247. function stringifyArrayOfTables (prefix, indent, key, values) {
  248. values = toJSON(values)
  249. validateArray(values)
  250. var firstValueType = tomlType(values[0])
  251. /* istanbul ignore if */
  252. if (firstValueType !== 'table') throw typeError(firstValueType)
  253. var fullKey = prefix + stringifyKey(key)
  254. var result = ''
  255. values.forEach(table => {
  256. if (result.length > 0) result += '\n'
  257. result += indent + '[[' + fullKey + ']]\n'
  258. result += stringifyObject(fullKey + '.', indent, table)
  259. })
  260. return result
  261. }
  262. function stringifyComplexTable (prefix, indent, key, value) {
  263. var fullKey = prefix + stringifyKey(key)
  264. var result = ''
  265. if (getInlineKeys(value).length > 0) {
  266. result += indent + '[' + fullKey + ']\n'
  267. }
  268. return result + stringifyObject(fullKey + '.', indent, value)
  269. }