| /* Hjson https://hjson.github.io */ |
| "use strict"; |
| |
| module.exports = function(data, opt) { |
| |
| var common = require("./hjson-common"); |
| var dsf = require("./hjson-dsf"); |
| |
| var plainToken = { |
| obj: [ '{', '}' ], |
| arr: [ '[', ']' ], |
| key: [ '', '' ], |
| qkey: [ '"', '"' ], |
| col: [ ':', '' ], |
| com: [ ',', '' ], |
| str: [ '', '' ], |
| qstr: [ '"', '"' ], |
| mstr: [ "'''", "'''" ], |
| num: [ '', '' ], |
| lit: [ '', '' ], |
| dsf: [ '', '' ], |
| esc: [ '\\', '' ], |
| uni: [ '\\u', '' ], |
| rem: [ '', '' ], |
| }; |
| |
| // options |
| var eol = common.EOL; |
| var indent = ' '; |
| var keepComments = false; |
| var bracesSameLine = false; |
| var quoteKeys = false; |
| var quoteStrings = false; |
| var condense = 0; |
| var multiline = 1; // std=1, no-tabs=2, off=0 |
| var separator = ''; // comma separator |
| var dsfDef = null; |
| var sortProps = false; |
| var token = plainToken; |
| |
| if (opt && typeof opt === 'object') { |
| opt.quotes = opt.quotes === 'always' ? 'strings' : opt.quotes; // legacy |
| |
| if (opt.eol === '\n' || opt.eol === '\r\n') eol = opt.eol; |
| keepComments = opt.keepWsc; |
| condense = opt.condense || 0; |
| bracesSameLine = opt.bracesSameLine; |
| quoteKeys = opt.quotes === 'all' || opt.quotes === 'keys'; |
| quoteStrings = opt.quotes === 'all' || opt.quotes === 'strings' || opt.separator === true; |
| if (quoteStrings || opt.multiline == 'off') multiline = 0; |
| else multiline = opt.multiline == 'no-tabs' ? 2 : 1; |
| separator = opt.separator === true ? token.com[0] : ''; |
| dsfDef = opt.dsf; |
| sortProps = opt.sortProps; |
| |
| // If the space parameter is a number, make an indent string containing that |
| // many spaces. If it is a string, it will be used as the indent string. |
| |
| if (typeof opt.space === 'number') { |
| indent = new Array(opt.space + 1).join(' '); |
| } else if (typeof opt.space === 'string') { |
| indent = opt.space; |
| } |
| |
| if (opt.colors === true) { |
| token = { |
| obj: [ '\x1b[37m{\x1b[0m', '\x1b[37m}\x1b[0m' ], |
| arr: [ '\x1b[37m[\x1b[0m', '\x1b[37m]\x1b[0m' ], |
| key: [ '\x1b[33m', '\x1b[0m' ], |
| qkey: [ '\x1b[33m"', '"\x1b[0m' ], |
| col: [ '\x1b[37m:\x1b[0m', '' ], |
| com: [ '\x1b[37m,\x1b[0m', '' ], |
| str: [ '\x1b[37;1m', '\x1b[0m' ], |
| qstr: [ '\x1b[37;1m"', '"\x1b[0m' ], |
| mstr: [ "\x1b[37;1m'''", "'''\x1b[0m" ], |
| num: [ '\x1b[36;1m', '\x1b[0m' ], |
| lit: [ '\x1b[36m', '\x1b[0m' ], |
| dsf: [ '\x1b[37m', '\x1b[0m' ], |
| esc: [ '\x1b[31m\\', '\x1b[0m' ], |
| uni: [ '\x1b[31m\\u', '\x1b[0m' ], |
| rem: [ '\x1b[35m', '\x1b[0m' ], |
| }; |
| } |
| |
| var i, ckeys=Object.keys(plainToken); |
| for (i = ckeys.length - 1; i >= 0; i--) { |
| var k = ckeys[i]; |
| token[k].push(plainToken[k][0].length, plainToken[k][1].length); |
| } |
| } |
| |
| // |
| var runDsf; // domain specific formats |
| |
| var commonRange='\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff'; |
| // needsEscape tests if the string can be written without escapes |
| var needsEscape = new RegExp('[\\\\\\"\x00-\x1f'+commonRange+']', 'g'); |
| // needsQuotes tests if the string can be written as a quoteless string (like needsEscape but without \\ and \") |
| var needsQuotes = new RegExp('^\\s|^"|^\'|^#|^\\/\\*|^\\/\\/|^\\{|^\\}|^\\[|^\\]|^:|^,|\\s$|[\x00-\x1f'+commonRange+']', 'g'); |
| // needsEscapeML tests if the string can be written as a multiline string (like needsEscape but without \n, \r, \\, \", \t unless multines is 'std') |
| var needsEscapeML = new RegExp('\'\'\'|^[\\s]+$|[\x00-'+(multiline === 2 ? '\x09' : '\x08')+'\x0b\x0c\x0e-\x1f'+commonRange+']', 'g'); |
| // starts with a keyword and optionally is followed by a comment |
| var startsWithKeyword = new RegExp('^(true|false|null)\\s*((,|\\]|\\}|#|//|/\\*).*)?$'); |
| var meta = { |
| // table of character substitutions |
| '\b': 'b', |
| '\t': 't', |
| '\n': 'n', |
| '\f': 'f', |
| '\r': 'r', |
| '"' : '"', |
| '\\': '\\' |
| }; |
| var needsEscapeName = /[,\{\[\}\]\s:#"']|\/\/|\/\*/; |
| var gap = ''; |
| // |
| var wrapLen = 0; |
| |
| function wrap(tk, v) { |
| wrapLen += tk[0].length + tk[1].length - tk[2] - tk[3]; |
| return tk[0] + v + tk[1]; |
| } |
| |
| function quoteReplace(string) { |
| return string.replace(needsEscape, function (a) { |
| var c = meta[a]; |
| if (typeof c === 'string') return wrap(token.esc, c); |
| else return wrap(token.uni, ('0000' + a.charCodeAt(0).toString(16)).slice(-4)); |
| }); |
| } |
| |
| function quote(string, gap, hasComment, isRootObject) { |
| if (!string) return wrap(token.qstr, ''); |
| |
| needsQuotes.lastIndex = 0; |
| startsWithKeyword.lastIndex = 0; |
| |
| // Check if we can insert this string without quotes |
| // see hjson syntax (must not parse as true, false, null or number) |
| |
| if (quoteStrings || hasComment || |
| needsQuotes.test(string) || |
| common.tryParseNumber(string, true) !== undefined || |
| startsWithKeyword.test(string)) { |
| |
| // If the string contains no control characters, no quote characters, and no |
| // backslash characters, then we can safely slap some quotes around it. |
| // Otherwise we first check if the string can be expressed in multiline |
| // format or we must replace the offending characters with safe escape |
| // sequences. |
| |
| needsEscape.lastIndex = 0; |
| needsEscapeML.lastIndex = 0; |
| if (!needsEscape.test(string)) return wrap(token.qstr, string); |
| else if (!needsEscapeML.test(string) && !isRootObject && multiline) return mlString(string, gap); |
| else return wrap(token.qstr, quoteReplace(string)); |
| } else { |
| // return without quotes |
| return wrap(token.str, string); |
| } |
| } |
| |
| function mlString(string, gap) { |
| // wrap the string into the ''' (multiline) format |
| |
| var i, a = string.replace(/\r/g, "").split('\n'); |
| gap += indent; |
| |
| if (a.length === 1) { |
| // The string contains only a single line. We still use the multiline |
| // format as it avoids escaping the \ character (e.g. when used in a |
| // regex). |
| return wrap(token.mstr, a[0]); |
| } else { |
| var res = eol + gap + token.mstr[0]; |
| for (i = 0; i < a.length; i++) { |
| res += eol; |
| if (a[i]) res += gap + a[i]; |
| } |
| return res + eol + gap + token.mstr[1]; |
| } |
| } |
| |
| function quoteKey(name) { |
| if (!name) return '""'; |
| |
| // Check if we can insert this key without quotes |
| |
| if (quoteKeys || needsEscapeName.test(name)) { |
| needsEscape.lastIndex = 0; |
| return wrap(token.qkey, needsEscape.test(name) ? quoteReplace(name) : name); |
| } else { |
| // return without quotes |
| return wrap(token.key, name); |
| } |
| } |
| |
| function str(value, hasComment, noIndent, isRootObject) { |
| // Produce a string from value. |
| |
| function startsWithNL(str) { return str && str[str[0] === '\r' ? 1 : 0] === '\n'; } |
| function commentOnThisLine(str) { return str && !startsWithNL(str); } |
| function makeComment(str, prefix, trim) { |
| if (!str) return ""; |
| str = common.forceComment(str); |
| var i, len = str.length; |
| for (i = 0; i < len && str[i] <= ' '; i++) {} |
| if (trim && i > 0) str = str.substr(i); |
| if (i < len) return prefix + wrap(token.rem, str); |
| else return str; |
| } |
| |
| // What happens next depends on the value's type. |
| |
| // check for DSF |
| var dsfValue = runDsf(value); |
| if (dsfValue !== undefined) return wrap(token.dsf, dsfValue); |
| |
| switch (typeof value) { |
| case 'string': |
| return quote(value, gap, hasComment, isRootObject); |
| |
| case 'number': |
| // JSON numbers must be finite. Encode non-finite numbers as null. |
| return isFinite(value) ? wrap(token.num, String(value)) : wrap(token.lit, 'null'); |
| |
| case 'boolean': |
| return wrap(token.lit, String(value)); |
| |
| case 'object': |
| // If the type is 'object', we might be dealing with an object or an array or |
| // null. |
| |
| // Due to a specification blunder in ECMAScript, typeof null is 'object', |
| // so watch out for that case. |
| |
| if (!value) return wrap(token.lit, 'null'); |
| |
| var comments; // whitespace & comments |
| if (keepComments) comments = common.getComment(value); |
| |
| var isArray = Object.prototype.toString.apply(value) === '[object Array]'; |
| |
| // Make an array to hold the partial results of stringifying this object value. |
| var mind = gap; |
| gap += indent; |
| var eolMind = eol + mind; |
| var eolGap = eol + gap; |
| var prefix = noIndent || bracesSameLine ? '' : eolMind; |
| var partial = []; |
| var setsep; |
| // condense helpers: |
| var cpartial = condense ? [] : null; |
| var saveQuoteStrings = quoteStrings, saveMultiline = multiline; |
| var iseparator = separator ? '' : token.com[0]; |
| var cwrapLen = 0; |
| |
| var i, length; // loop |
| var k, v, vs; // key, value |
| var c, ca; |
| var res, cres; |
| |
| if (isArray) { |
| // The value is an array. Stringify every element. Use null as a placeholder |
| // for non-JSON values. |
| |
| for (i = 0, length = value.length; i < length; i++) { |
| setsep = i < length -1; |
| if (comments) { |
| c = comments.a[i]||[]; |
| ca = commentOnThisLine(c[1]); |
| partial.push(makeComment(c[0], "\n") + eolGap); |
| if (cpartial && (c[0] || c[1] || ca)) cpartial = null; |
| } |
| else partial.push(eolGap); |
| wrapLen = 0; |
| v = value[i]; |
| partial.push(str(v, comments ? ca : false, true) + (setsep ? separator : '')); |
| if (cpartial) { |
| // prepare the condensed version |
| switch (typeof v) { |
| case 'string': |
| wrapLen = 0; |
| quoteStrings = true; multiline = 0; |
| cpartial.push(str(v, false, true) + (setsep ? token.com[0] : '')); |
| quoteStrings = saveQuoteStrings; multiline = saveMultiline; |
| break; |
| case 'object': if (v) { cpartial = null; break; } // falls through |
| default: cpartial.push(partial[partial.length - 1] + (setsep ? iseparator : '')); break; |
| } |
| if (setsep) wrapLen += token.com[0].length - token.com[2]; |
| cwrapLen += wrapLen; |
| } |
| if (comments && c[1]) partial.push(makeComment(c[1], ca ? " " : "\n", ca)); |
| } |
| |
| if (length === 0) { |
| // when empty |
| if (comments && comments.e) partial.push(makeComment(comments.e[0], "\n") + eolMind); |
| } |
| else partial.push(eolMind); |
| |
| // Join all of the elements together, separated with newline, and wrap them in |
| // brackets. |
| |
| if (partial.length === 0) res = wrap(token.arr, ''); |
| else { |
| res = prefix + wrap(token.arr, partial.join('')); |
| // try if the condensed version can fit (parent key name is not included) |
| if (cpartial) { |
| cres = cpartial.join(' '); |
| if (cres.length - cwrapLen <= condense) res = wrap(token.arr, cres); |
| } |
| } |
| } else { |
| // Otherwise, iterate through all of the keys in the object. |
| var commentKeys = comments ? comments.o.slice() : []; |
| var objectKeys = []; |
| for (k in value) { |
| if (Object.prototype.hasOwnProperty.call(value, k) && commentKeys.indexOf(k) < 0) |
| objectKeys.push(k); |
| } |
| if(sortProps) { |
| objectKeys.sort(); |
| } |
| var keys = commentKeys.concat(objectKeys); |
| |
| for (i = 0, length = keys.length; i < length; i++) { |
| setsep = i < length - 1; |
| k = keys[i]; |
| if (comments) { |
| c = comments.c[k]||[]; |
| ca = commentOnThisLine(c[1]); |
| partial.push(makeComment(c[0], "\n") + eolGap); |
| if (cpartial && (c[0] || c[1] || ca)) cpartial = null; |
| } |
| else partial.push(eolGap); |
| |
| wrapLen = 0; |
| v = value[k]; |
| vs = str(v, comments && ca); |
| partial.push(quoteKey(k) + token.col[0] + (startsWithNL(vs) ? '' : ' ') + vs + (setsep ? separator : '')); |
| if (comments && c[1]) partial.push(makeComment(c[1], ca ? " " : "\n", ca)); |
| if (cpartial) { |
| // prepare the condensed version |
| switch (typeof v) { |
| case 'string': |
| wrapLen = 0; |
| quoteStrings = true; multiline = 0; |
| vs = str(v, false); |
| quoteStrings = saveQuoteStrings; multiline = saveMultiline; |
| cpartial.push(quoteKey(k) + token.col[0] + ' ' + vs + (setsep ? token.com[0] : '')); |
| break; |
| case 'object': if (v) { cpartial = null; break; } // falls through |
| default: cpartial.push(partial[partial.length - 1] + (setsep ? iseparator : '')); break; |
| } |
| wrapLen += token.col[0].length - token.col[2]; |
| if (setsep) wrapLen += token.com[0].length - token.com[2]; |
| cwrapLen += wrapLen; |
| } |
| } |
| if (length === 0) { |
| // when empty |
| if (comments && comments.e) partial.push(makeComment(comments.e[0], "\n") + eolMind); |
| } |
| else partial.push(eolMind); |
| |
| // Join all of the member texts together, separated with newlines |
| if (partial.length === 0) { |
| res = wrap(token.obj, ''); |
| } else { |
| // and wrap them in braces |
| res = prefix + wrap(token.obj, partial.join('')); |
| // try if the condensed version can fit |
| if (cpartial) { |
| cres = cpartial.join(' '); |
| if (cres.length - cwrapLen <= condense) res = wrap(token.obj, cres); |
| } |
| } |
| } |
| |
| gap = mind; |
| return res; |
| } |
| } |
| |
| |
| runDsf = dsf.loadDsf(dsfDef, 'stringify'); |
| |
| var res = ""; |
| var comments = keepComments ? comments = (common.getComment(data) || {}).r : null; |
| if (comments && comments[0]) res = comments[0] + '\n'; |
| |
| // get the result of stringifying the data. |
| res += str(data, null, true, true); |
| |
| if (comments) res += comments[1]||""; |
| |
| return res; |
| }; |