/* Hjson http://hjson.org */ "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; };