Comparing version 1.3.0 to 1.4.0
1025
lib/hjson.js
@@ -8,6 +8,6 @@ /* | ||
This file creates a hjson_parse and a hjson_stringify function. | ||
This file creates a Hjson object: | ||
hjson_parse(text, options) | ||
Hjson.parse(text, options) | ||
@@ -27,3 +27,3 @@ options { | ||
hjson_stringify(value, options) | ||
Hjson.stringify(value, options) | ||
obsolete: hjson_stringify(value, replacer, space) | ||
@@ -56,83 +56,51 @@ | ||
This is a reference implementation. You are free to copy, modify, or | ||
redistribute. | ||
Hjson.endOfLine() | ||
Hjson.setEndOfLine(eol) | ||
This code should be minified before deployment. | ||
*/ | ||
Gets or sets the EOL character ('\n' or '\r\n'). | ||
var hjson_EOL = '\n'; | ||
var hjson_bracesSameLine = false; | ||
var hjson_parse = (function () { | ||
"use strict"; | ||
Hjson.bracesSameLine() | ||
Hjson.setBracesSameLine(b) | ||
// This is a function that can parse a Hjson text, producing a JavaScript | ||
// data structure. It is a simple, recursive descent parser. It does not use | ||
// eval or regular expressions, so it can be used as a model for implementing | ||
// a JSON parser in other languages. | ||
Gets or sets if braces should appear on the same line (for stringify). | ||
// We are defining the function inside of another function to avoid creating | ||
// global variables. | ||
var text; | ||
var at; // The index of the current character | ||
var ch; // The current character | ||
var escapee = { | ||
'"': '"', | ||
'\\': '\\', | ||
'/': '/', | ||
b: '\b', | ||
f: '\f', | ||
n: '\n', | ||
r: '\r', | ||
t: '\t' | ||
}; | ||
var keepWsc; // keep whitespace | ||
This is a reference implementation. You are free to copy, modify, or | ||
redistribute. | ||
// Call error when something is wrong. | ||
var error = function (m) { | ||
var i, col=0, line=1; | ||
for (i = at-1; i > 0 && text[i] !== '\n'; i--, col++) {} | ||
for (; i > 0; i--) if (text[i] === '\n') line++; | ||
throw new Error(m + " at line " + line + "," + col + " >>>" + text.substr(at-col, 20) + " ..."); | ||
}; | ||
This code should be minified before deployment. | ||
*/ | ||
var next = function (c) { | ||
// If a c parameter is provided, verify that it matches the current character. | ||
var Hjson = (function () { | ||
if (c && c !== ch) | ||
error("Expected '" + c + "' instead of '" + ch + "'"); | ||
var EOL = '\n'; | ||
var bracesSameLine = false; | ||
// Get the next character. When there are no more characters, | ||
// return the empty string. | ||
ch = text.charAt(at); | ||
at++; | ||
return ch; | ||
}; | ||
var peek = function (offs) { | ||
// range check is not required | ||
return text.charAt(at + offs); | ||
} | ||
var move = function (pos) { | ||
at = pos - 1; | ||
return next(); | ||
}; | ||
var number = function () { | ||
var tryParseNumber = function (text) { | ||
// Parse a number value. | ||
var number, string = ''; | ||
var pos = at; | ||
var number, string = '', leadingZeros = 0, testLeading = true; | ||
var at = 0; | ||
var ch; | ||
function next() { | ||
ch = text.charAt(at); | ||
at++; | ||
return ch; | ||
} | ||
next(); | ||
if (ch === '-') { | ||
string = '-'; | ||
next('-'); | ||
next(); | ||
} | ||
while (ch >= '0' && ch <= '9') { | ||
if (testLeading) { | ||
if (ch == '0') leadingZeros++; | ||
else testLeading = false; | ||
} | ||
string += ch; | ||
next(); | ||
} | ||
if (testLeading) leadingZeros--; // single 0 is allowed | ||
if (ch === '.') { | ||
@@ -157,10 +125,7 @@ string += '.'; | ||
// skip white/to (newline) | ||
while (ch && ch <= ' ' && ch !== '\n') next(); | ||
while (ch && ch <= ' ') next(); | ||
if (ch === '\n' || ch === ',' || ch === '}' || ch ===']') number = +string; | ||
if (!isFinite(number)) { | ||
// treat as a string | ||
move(pos); | ||
return wordOrString(); | ||
number = +string; | ||
if (ch || leadingZeros || !isFinite(number)) { | ||
return undefined; | ||
} | ||
@@ -170,530 +135,600 @@ else return number; | ||
var string = function () { | ||
// Parse a string value. | ||
var hex, i, string = '', uffff; | ||
var hjson_parse = (function () { | ||
"use strict"; | ||
// When parsing for string values, we must look for " and \ characters. | ||
if (ch === '"') { | ||
while (next()) { | ||
if (ch === '"') { | ||
next(); | ||
return string; | ||
} | ||
if (ch === '\\') { | ||
next(); | ||
if (ch === 'u') { | ||
uffff = 0; | ||
for (i = 0; i < 4; i++) { | ||
hex = parseInt(next(), 16); | ||
if (!isFinite(hex)) | ||
break; | ||
uffff = uffff * 16 + hex; | ||
// This is a function that can parse a Hjson text, producing a JavaScript | ||
// data structure. It is a simple, recursive descent parser. It does not use | ||
// eval or regular expressions, so it can be used as a model for implementing | ||
// a JSON parser in other languages. | ||
// We are defining the function inside of another function to avoid creating | ||
// global variables. | ||
var text; | ||
var at; // The index of the current character | ||
var ch; // The current character | ||
var escapee = { | ||
'"': '"', | ||
'\\': '\\', | ||
'/': '/', | ||
b: '\b', | ||
f: '\f', | ||
n: '\n', | ||
r: '\r', | ||
t: '\t' | ||
}; | ||
var keepWsc; // keep whitespace | ||
// Call error when something is wrong. | ||
var error = function (m) { | ||
var i, col=0, line=1; | ||
for (i = at-1; i > 0 && text[i] !== '\n'; i--, col++) {} | ||
for (; i > 0; i--) if (text[i] === '\n') line++; | ||
throw new Error(m + " at line " + line + "," + col + " >>>" + text.substr(at-col, 20) + " ..."); | ||
}; | ||
var next = function (c) { | ||
// If a c parameter is provided, verify that it matches the current character. | ||
if (c && c !== ch) | ||
error("Expected '" + c + "' instead of '" + ch + "'"); | ||
// Get the next character. When there are no more characters, | ||
// return the empty string. | ||
ch = text.charAt(at); | ||
at++; | ||
return ch; | ||
}; | ||
var peek = function (offs) { | ||
// range check is not required | ||
return text.charAt(at + offs); | ||
} | ||
var string = function () { | ||
// Parse a string value. | ||
var hex, i, string = '', uffff; | ||
// When parsing for string values, we must look for " and \ characters. | ||
if (ch === '"') { | ||
while (next()) { | ||
if (ch === '"') { | ||
next(); | ||
return string; | ||
} | ||
if (ch === '\\') { | ||
next(); | ||
if (ch === 'u') { | ||
uffff = 0; | ||
for (i = 0; i < 4; i++) { | ||
hex = parseInt(next(), 16); | ||
if (!isFinite(hex)) | ||
break; | ||
uffff = uffff * 16 + hex; | ||
} | ||
string += String.fromCharCode(uffff); | ||
} | ||
string += String.fromCharCode(uffff); | ||
else if (typeof escapee[ch] === 'string') string += escapee[ch]; | ||
else break; | ||
} | ||
else if (typeof escapee[ch] === 'string') string += escapee[ch]; | ||
else break; | ||
else string += ch; | ||
} | ||
else string += ch; | ||
} | ||
} | ||
error("Bad string"); | ||
}; | ||
error("Bad string"); | ||
}; | ||
var mlString = function () { | ||
// Parse a multiline string value. | ||
var string = '', triple = 0; | ||
var mlString = function () { | ||
// Parse a multiline string value. | ||
var string = '', triple = 0; | ||
// we are at ''' +1 - get indent | ||
var indent = 0; | ||
while (true) { | ||
var c=peek(-indent-5); | ||
if (!c || c=='\n') break; | ||
indent++; | ||
} | ||
// we are at ''' +1 - get indent | ||
var indent = 0; | ||
while (true) { | ||
var c=peek(-indent-5); | ||
if (!c || c=='\n') break; | ||
indent++; | ||
} | ||
var skipIndent = function () { | ||
var skip = indent; | ||
while (ch && ch <= ' ' && ch !== '\n' && skip-- > 0) next(); | ||
} | ||
var skipIndent = function () { | ||
var skip = indent; | ||
while (ch && ch <= ' ' && ch !== '\n' && skip-- > 0) next(); | ||
} | ||
// skip white/to (newline) | ||
while (ch && ch <= ' ' && ch !== '\n') next(); | ||
if (ch === '\n') { next(); skipIndent(); } | ||
// skip white/to (newline) | ||
while (ch && ch <= ' ' && ch !== '\n') next(); | ||
if (ch === '\n') { next(); skipIndent(); } | ||
// When parsing multiline string values, we must look for ' characters. | ||
while (true) { | ||
if (!ch) error("Bad multiline string"); | ||
else if (ch === '\'') { | ||
triple++; | ||
next(); | ||
if (triple === 3) return string; | ||
else continue; | ||
// When parsing multiline string values, we must look for ' characters. | ||
while (true) { | ||
if (!ch) error("Bad multiline string"); | ||
else if (ch === '\'') { | ||
triple++; | ||
next(); | ||
if (triple === 3) return string; | ||
else continue; | ||
} | ||
else while (triple > 0) { | ||
string += '\''; | ||
triple--; | ||
} | ||
if (ch === '\n') { | ||
string += ch; | ||
next(); | ||
skipIndent(); | ||
} | ||
else { | ||
string += ch; | ||
next(); | ||
} | ||
} | ||
else while (triple > 0) { | ||
string += '\''; | ||
triple--; | ||
} | ||
if (ch === '\n') { | ||
string += ch; | ||
next(); | ||
skipIndent(); | ||
} | ||
else { | ||
string += ch; | ||
next(); | ||
} | ||
} | ||
}; | ||
}; | ||
var keyname = function () { | ||
// quotes for keys that consist only of letters and digits are optional in Hjson | ||
var keyname = function () { | ||
// quotes for keys that consist only of letters and digits are optional in Hjson | ||
if (ch === '"') return string(); | ||
if (ch === '"') return string(); | ||
var name = ch; | ||
while (next()) { | ||
if (ch === ':') return name; | ||
else if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9') | ||
name += ch; | ||
else error("Bad name"); | ||
} | ||
error("Bad name"); | ||
}; | ||
var name = ch; | ||
while (next()) { | ||
if (ch === ':') return name; | ||
else if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9') | ||
name += ch; | ||
else error("Bad name"); | ||
} | ||
error("Bad name"); | ||
}; | ||
var white = function () { | ||
while (ch) { | ||
// Skip whitespace. | ||
while (ch && ch <= ' ') next(); | ||
// Hjson allows comments | ||
if (ch === "#" || ch === "/" && peek(0) === "/") { | ||
while (ch && ch !== '\n') next(); | ||
var white = function () { | ||
while (ch) { | ||
// Skip whitespace. | ||
while (ch && ch <= ' ') next(); | ||
// Hjson allows comments | ||
if (ch === '#' || ch === '/' && peek(0) === '/') { | ||
while (ch && ch !== '\n') next(); | ||
} | ||
else if (ch === '/' && peek(0) === '*') | ||
{ | ||
next(); next(); | ||
while (ch && !(ch === '*' && peek(0) === '/')) next(); | ||
if (ch) { next(); next(); } | ||
} | ||
else break; | ||
} | ||
else if (ch === "/" && peek(0) === "*") | ||
{ | ||
next(); next(); | ||
while (ch && !(ch === '*' && peek(0) === "/")) next(); | ||
if (ch) { next(); next(); } | ||
} | ||
else break; | ||
} | ||
}; | ||
}; | ||
var wordOrString = function () { | ||
// Hjson strings can be quoteless | ||
// returns string, true, false, or null. | ||
var value = ch; | ||
while (next()) { | ||
if (value.length === 3 && value === "'''") return mlString(); | ||
if (ch === '\r' || ch === '\n' || ch === ',' || ch === '}' || ch ===']') { | ||
switch (value[0]) { | ||
case 'f': if (value.trim() === "false") return false; break; | ||
case 'n': if (value.trim() === "null") return null; break; | ||
case 't': if (value.trim() === "true") return true; break; | ||
var tfnns = function () { | ||
// Hjson strings can be quoteless | ||
// returns string, true, false, or null. | ||
var value = ch; | ||
while (next()) { | ||
if (value.length === 3 && value === "'''") return mlString(); | ||
var isEol=ch === '\r' || ch === '\n'; | ||
if (isEol || ch === ',' || | ||
ch === '}' || ch === ']' || | ||
ch === '#' || | ||
ch === '/' && (peek(0) === '/' || peek(0) === '*') | ||
) { | ||
var chf = value[0]; | ||
switch (chf) { | ||
case 'f': if (value.trim() === "false") return false; break; | ||
case 'n': if (value.trim() === "null") return null; break; | ||
case 't': if (value.trim() === "true") return true; break; | ||
default: | ||
if (chf === '-' || chf >= '0' && chf <= '9') { | ||
var n = tryParseNumber(value); | ||
if (n !== undefined) return n; | ||
} | ||
} | ||
if (isEol) return value; | ||
} | ||
if (ch === '\r' || ch === '\n') return value; | ||
value += ch; | ||
} | ||
value += ch; | ||
} | ||
error("Bad value"); | ||
}; | ||
error("Bad value"); | ||
}; | ||
var getComment = function (wat) { | ||
wat--; | ||
// remove trailing whitespace | ||
for (var i=at-2; i > wat && text[i] <= ' ' && text[i] !== '\n'; i--); | ||
// but only up to EOL | ||
if (text[i] === '\n') i--; | ||
if (text[i] === '\r') i--; | ||
var res = text.substr(wat, i-wat+1); | ||
for (var i=0; i < res.length; i++) | ||
if (res[i] > ' ') return res; | ||
return ""; | ||
} | ||
var getComment = function (wat) { | ||
wat--; | ||
// remove trailing whitespace | ||
for (var i=at-2; i > wat && text[i] <= ' ' && text[i] !== '\n'; i--); | ||
// but only up to EOL | ||
if (text[i] === '\n') i--; | ||
if (text[i] === '\r') i--; | ||
var res = text.substr(wat, i-wat+1); | ||
for (var i=0; i < res.length; i++) | ||
if (res[i] > ' ') return res; | ||
return ""; | ||
} | ||
var array = function () { | ||
// Parse an array value. | ||
var array = function () { | ||
// Parse an array value. | ||
var array = []; | ||
var kw, wat; | ||
if (keepWsc) { | ||
if (Object.defineProperty) Object.defineProperty(array, "__WSC__", { enumerable: false, writable: true }); | ||
array.__WSC__ = kw = []; | ||
} | ||
var array = []; | ||
var kw, wat; | ||
if (keepWsc) { | ||
if (Object.defineProperty) Object.defineProperty(array, "__WSC__", { enumerable: false, writable: true }); | ||
array.__WSC__ = kw = []; | ||
} | ||
if (ch === '[') { | ||
next('['); | ||
wat = at; | ||
white(); | ||
if (kw) kw.push(getComment(wat)); | ||
if (ch === ']') { | ||
next(']'); | ||
return array; // empty array | ||
} | ||
while (ch) { | ||
array.push(value()); | ||
if (ch === '[') { | ||
next(); | ||
wat = at; | ||
white(); | ||
// in Hjson the comma is optional and trailing commas are allowed | ||
if (ch === ',') { next(); wat = at; white(); } | ||
if (kw) kw.push(getComment(wat)); | ||
if (ch === ']') { | ||
next(']'); | ||
return array; | ||
next(); | ||
return array; // empty array | ||
} | ||
white(); | ||
while (ch) { | ||
array.push(value()); | ||
wat = at; | ||
white(); | ||
// in Hjson the comma is optional and trailing commas are allowed | ||
if (ch === ',') { next(); wat = at; white(); } | ||
if (kw) kw.push(getComment(wat)); | ||
if (ch === ']') { | ||
next(); | ||
return array; | ||
} | ||
white(); | ||
} | ||
} | ||
} | ||
error("Bad array"); | ||
}; | ||
error("Bad array"); | ||
}; | ||
var object = function () { | ||
// Parse an object value. | ||
var object = function () { | ||
// Parse an object value. | ||
var key, object = {}; | ||
var kw, wat; | ||
if (keepWsc) { | ||
if (Object.defineProperty) Object.defineProperty(object, "__WSC__", { enumerable: false, writable: true }); | ||
object.__WSC__ = kw = { c: {}, o: [] }; | ||
} | ||
var key, object = {}; | ||
var kw, wat; | ||
if (keepWsc) { | ||
if (Object.defineProperty) Object.defineProperty(object, "__WSC__", { enumerable: false, writable: true }); | ||
object.__WSC__ = kw = { c: {}, o: [] }; | ||
} | ||
function pushWhite(key) { kw.c[key]=getComment(wat); if (key) kw.o.push(key); } | ||
function pushWhite(key) { kw.c[key]=getComment(wat); if (key) kw.o.push(key); } | ||
if (ch === '{') { | ||
next('{'); | ||
wat = at; | ||
white(); | ||
if (kw) pushWhite(""); | ||
if (ch === '}') { | ||
next('}'); | ||
return object; // empty object | ||
} | ||
while (ch) { | ||
key = keyname(); | ||
white(); | ||
next(':'); | ||
// duplicate keys overwrite the previous value | ||
object[key] = value(); | ||
if (ch === '{') { | ||
next(); | ||
wat = at; | ||
white(); | ||
// in Hjson the comma is optional and trailing commas are allowed | ||
if (ch === ',') { next(); wat = at; white(); } | ||
if (kw) pushWhite(key); | ||
if (kw) pushWhite(""); | ||
if (ch === '}') { | ||
next('}'); | ||
return object; | ||
next(); | ||
return object; // empty object | ||
} | ||
white(); | ||
while (ch) { | ||
key = keyname(); | ||
white(); | ||
next(':'); | ||
// duplicate keys overwrite the previous value | ||
object[key] = value(); | ||
wat = at; | ||
white(); | ||
// in Hjson the comma is optional and trailing commas are allowed | ||
if (ch === ',') { next(); wat = at; white(); } | ||
if (kw) pushWhite(key); | ||
if (ch === '}') { | ||
next(); | ||
return object; | ||
} | ||
white(); | ||
} | ||
} | ||
} | ||
error("Bad object"); | ||
}; | ||
error("Bad object"); | ||
}; | ||
var value = function () { | ||
// Parse a Hjson value. It could be an object, an array, a string, a number or a word. | ||
var value = function () { | ||
// Parse a Hjson value. It could be an object, an array, a string, a number or a word. | ||
white(); | ||
switch (ch) { | ||
case '{': return object(); | ||
case '[': return array(); | ||
case '"': return string(); | ||
case '-': return number(); | ||
default: return ch >= '0' && ch <= '9' ? number() : wordOrString(); | ||
} | ||
}; | ||
white(); | ||
switch (ch) { | ||
case '{': return object(); | ||
case '[': return array(); | ||
case '"': return string(); | ||
default: return tfnns(); | ||
} | ||
}; | ||
// Return the hjson_parse function. It will have access to all of the above | ||
// functions and variables. | ||
// Return the hjson_parse function. It will have access to all of the above | ||
// functions and variables. | ||
return function (source, options) { | ||
var result; | ||
return function (source, options) { | ||
var result; | ||
keepWsc = options && options.keepWsc; | ||
text = source; | ||
at = 0; | ||
ch = ' '; | ||
result = value(); | ||
white(); | ||
if (ch) error("Syntax error"); | ||
keepWsc = options && options.keepWsc; | ||
text = source; | ||
at = 0; | ||
ch = ' '; | ||
result = value(); | ||
white(); | ||
if (ch) error("Syntax error"); | ||
return result; | ||
}; | ||
}()); | ||
return result; | ||
}; | ||
}()); | ||
var hjson_stringify = (function () { | ||
"use strict"; | ||
var hjson_stringify = (function () { | ||
"use strict"; | ||
var needsEscape = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; | ||
var needsQuotes = /[\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // like needsEscape but without \\ and \" | ||
var needsEscapeML = /'''|[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // ''' || (needsQuotes but without \n and \r) | ||
var meta = | ||
{ // table of character substitutions | ||
'\b': '\\b', | ||
'\t': '\\t', | ||
'\n': '\\n', | ||
'\f': '\\f', | ||
'\r': '\\r', | ||
'"' : '\\"', | ||
'\\': '\\\\' | ||
}; | ||
var needsEscapeName = /[^a-zA-Z0-9]/; | ||
var gap = ''; | ||
var indent = ' '; | ||
var keepWsc; | ||
var needsEscape = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; | ||
var needsQuotes = /[\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // like needsEscape but without \\ and \" | ||
var needsEscapeML = /'''|[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // ''' || (needsQuotes but without \n and \r) | ||
var meta = | ||
{ // table of character substitutions | ||
'\b': '\\b', | ||
'\t': '\\t', | ||
'\n': '\\n', | ||
'\f': '\\f', | ||
'\r': '\\r', | ||
'"' : '\\"', | ||
'\\': '\\\\' | ||
}; | ||
var needsEscapeName = /[^a-zA-Z0-9]/; | ||
var gap = ''; | ||
var indent = ' '; | ||
var keepWsc; | ||
function isWhite(c) { return c <= ' '; } | ||
function isDigit(c) { return c >= '0' && c <= '9'; } | ||
function isKeyword(value) { return value == 'true' || value == 'false' || value == 'null'; } | ||
function isWhite(c) { return c <= ' '; } | ||
function isKeyword(value) { return value == 'true' || value == 'false' || value == 'null'; } | ||
function quoteReplace(string) { | ||
return string.replace(needsEscape, function (a) { | ||
var c = meta[a]; | ||
return typeof c === 'string' | ||
? c | ||
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); | ||
}); | ||
} | ||
function quoteReplace(string) { | ||
return string.replace(needsEscape, function (a) { | ||
var c = meta[a]; | ||
return typeof c === 'string' | ||
? c | ||
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); | ||
}); | ||
} | ||
function quote(string, gap, hasComment) { | ||
if (!string) return '""'; | ||
function quote(string, gap, hasComment) { | ||
if (!string) return '""'; | ||
needsQuotes.lastIndex = 0; | ||
var doEscape = hasComment || needsQuotes.test(string); | ||
needsQuotes.lastIndex = 0; | ||
var doEscape = hasComment || needsQuotes.test(string); | ||
// Check if we can insert this string without quotes | ||
// Check if we can insert this string without quotes | ||
// see hjson syntax (must not parse as true, false, null or number) | ||
var first = string[0], last = string[string.length-1]; | ||
if (doEscape || | ||
isWhite(first) || | ||
isDigit(first) || | ||
first === '"' || | ||
first === '#' || | ||
first === '-' || | ||
first === '{' || | ||
first === '[' || | ||
isWhite(last) || | ||
isKeyword(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. | ||
var first = string[0], last = string[string.length-1]; | ||
if (doEscape || | ||
isWhite(first) || | ||
first === '"' || | ||
first === '#' || | ||
first === '/' && (string[1] === '*' || string[1] === '/') || | ||
first === '{' || | ||
first === '[' || | ||
isWhite(last) || | ||
tryParseNumber(string) !== undefined || | ||
isKeyword(string)) { | ||
needsEscape.lastIndex = 0; | ||
needsEscapeML.lastIndex = 0; | ||
if (!needsEscape.test(string)) return '"' + string + '"'; | ||
else if (!needsEscapeML.test(string)) return mlString(string, gap); | ||
else return '"' + quoteReplace(string) + '"'; | ||
} | ||
else { | ||
// return without quotes | ||
return 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. | ||
function mlString(string, gap) { | ||
// wrap the string into the ''' (multiline) format | ||
var i, a = string.replace(/\r/g, "").split('\n'); | ||
gap += indent; | ||
var res; | ||
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). | ||
res = "'''" + a[0]; | ||
needsEscape.lastIndex = 0; | ||
needsEscapeML.lastIndex = 0; | ||
if (!needsEscape.test(string)) return '"' + string + '"'; | ||
else if (!needsEscapeML.test(string)) return mlString(string, gap); | ||
else return '"' + quoteReplace(string) + '"'; | ||
} | ||
else { | ||
// return without quotes | ||
return string; | ||
} | ||
} | ||
else { | ||
res = hjson_EOL + gap + "'''"; | ||
for (i = 0; i < a.length; i++) | ||
res += hjson_EOL + gap + a[i]; | ||
} | ||
return res + "'''"; | ||
} | ||
function mlString(string, gap) { | ||
// wrap the string into the ''' (multiline) format | ||
function quoteName(name) { | ||
if (!name) return '""'; | ||
var i, a = string.replace(/\r/g, "").split('\n'); | ||
gap += indent; | ||
// Check if we can insert this name without quotes | ||
var res; | ||
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). | ||
res = "'''" + a[0]; | ||
} | ||
else { | ||
res = EOL + gap + "'''"; | ||
for (i = 0; i < a.length; i++) | ||
res += EOL + gap + a[i]; | ||
} | ||
if (needsEscapeName.test(name)) { | ||
needsEscape.lastIndex = 0; | ||
return '"' + (needsEscape.test(name) ? quoteReplace(name) : name) + '"'; | ||
return res + "'''"; | ||
} | ||
else { | ||
// return without quotes | ||
return name; | ||
} | ||
} | ||
function str(value, hasComment, rootObject) { | ||
// Produce a string from value. | ||
function quoteName(name) { | ||
if (!name) return '""'; | ||
function startsWithNL(str) { return str && str[str[0] === '\r' ? 1 : 0] === '\n'; } | ||
function testWsc(str) { return str && !startsWithNL(str); } | ||
function wsc(str) { | ||
if (!str) return ""; | ||
for (var i = 0; i < str.length; i++) { | ||
var c = str[i]; | ||
if (c === '\n' || c === '#') break; | ||
if (c > ' ') return ' # ' + str; | ||
// Check if we can insert this name without quotes | ||
if (needsEscapeName.test(name)) { | ||
needsEscape.lastIndex = 0; | ||
return '"' + (needsEscape.test(name) ? quoteReplace(name) : name) + '"'; | ||
} | ||
return str; | ||
else { | ||
// return without quotes | ||
return name; | ||
} | ||
} | ||
// What happens next depends on the value's type. | ||
function str(value, hasComment, rootObject) { | ||
// Produce a string from value. | ||
switch (typeof value) { | ||
case 'string': | ||
return quote(value, gap, hasComment); | ||
function startsWithNL(str) { return str && str[str[0] === '\r' ? 1 : 0] === '\n'; } | ||
function testWsc(str) { return str && !startsWithNL(str); } | ||
function wsc(str) { | ||
if (!str) return ""; | ||
for (var i = 0; i < str.length; i++) { | ||
var c = str[i]; | ||
if (c === '\n' || c === '#') break; | ||
if (c > ' ') return ' # ' + str; | ||
} | ||
return str; | ||
} | ||
case 'number': | ||
// JSON numbers must be finite. Encode non-finite numbers as null. | ||
return isFinite(value) ? String(value) : 'null'; | ||
// What happens next depends on the value's type. | ||
case 'boolean': | ||
case 'null': | ||
// If the value is a boolean or null, convert it to a string. Note: | ||
// typeof null does not produce 'null'. The case is included here in | ||
// the remote chance that this gets fixed someday. | ||
return String(value); | ||
switch (typeof value) { | ||
case 'string': | ||
return quote(value, gap, hasComment); | ||
case 'object': | ||
// If the type is 'object', we might be dealing with an object or an array or | ||
// null. | ||
case 'number': | ||
// JSON numbers must be finite. Encode non-finite numbers as null. | ||
return isFinite(value) ? String(value) : 'null'; | ||
// Due to a specification blunder in ECMAScript, typeof null is 'object', | ||
// so watch out for that case. | ||
case 'boolean': | ||
case 'null': | ||
// If the value is a boolean or null, convert it to a string. Note: | ||
// typeof null does not produce 'null'. The case is included here in | ||
// the remote chance that this gets fixed someday. | ||
return String(value); | ||
if (!value) return 'null'; | ||
case 'object': | ||
// If the type is 'object', we might be dealing with an object or an array or | ||
// null. | ||
// Make an array to hold the partial results of stringifying this object value. | ||
var mind = gap; | ||
gap += indent; | ||
var eolMind = hjson_EOL + mind; | ||
var eolGap = hjson_EOL + gap; | ||
var prefix = rootObject || hjson_bracesSameLine ? '' : eolMind; | ||
var partial = []; | ||
// Due to a specification blunder in ECMAScript, typeof null is 'object', | ||
// so watch out for that case. | ||
var i, length; // loop | ||
var k, v; // key, value | ||
var kw, kwl; // whitespace & comments | ||
if (!value) return 'null'; | ||
// Is the value an 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 = rootObject || bracesSameLine ? '' : eolMind; | ||
var partial = []; | ||
if (Object.prototype.toString.apply(value) === '[object Array]') { | ||
// The value is an array. Stringify every element. Use null as a placeholder | ||
// for non-JSON values. | ||
var i, length; // loop | ||
var k, v; // key, value | ||
var kw, kwl; // whitespace & comments | ||
if (keepWsc) kw = value.__WSC__; | ||
// Is the value an array? | ||
for (i = 0, length = value.length; i < length; i++) { | ||
if (kw) partial.push(wsc(kw[i]) + eolGap); | ||
partial.push(str(value[i], kw ? testWsc(kw[i + 1]) : false) || 'null'); | ||
} | ||
if (kw) partial.push(wsc(kw[i]) + eolMind); | ||
if (Object.prototype.toString.apply(value) === '[object Array]') { | ||
// The value is an array. Stringify every element. Use null as a placeholder | ||
// for non-JSON values. | ||
// Join all of the elements together, separated with newline, and wrap them in | ||
// brackets. | ||
if (keepWsc) kw = value.__WSC__; | ||
if (kw) v = prefix + '[' + partial.join('') + ']'; | ||
else if (partial.length === 0) v = '[]'; | ||
else v = prefix + '[' + eolGap + partial.join(eolGap) + eolMind + ']'; | ||
} | ||
else { | ||
// Otherwise, iterate through all of the keys in the object. | ||
if (keepWsc && value.__WSC__) { | ||
kw = value.__WSC__; | ||
kwl = wsc(kw.c[""]); | ||
var keys=kw.o.slice(); | ||
for (k in value) { | ||
if (Object.prototype.hasOwnProperty.call(value, k) && keys.indexOf(k) < 0) | ||
keys.push(k); | ||
for (i = 0, length = value.length; i < length; i++) { | ||
if (kw) partial.push(wsc(kw[i]) + eolGap); | ||
partial.push(str(value[i], kw ? testWsc(kw[i + 1]) : false) || 'null'); | ||
} | ||
if (kw) partial.push(wsc(kw[i]) + eolMind); | ||
for (i = 0, length = keys.length; i < length; i++) { | ||
k = keys[i]; | ||
partial.push(kwl + eolGap); | ||
kwl = wsc(kw.c[k]); | ||
v = str(value[k], testWsc(kwl)); | ||
if (v) partial.push(quoteName(k) + (startsWithNL(v) ? ':' : ': ') + v); | ||
} | ||
partial.push(kwl + eolMind); | ||
// Join all of the elements together, separated with newline, and wrap them in | ||
// brackets. | ||
if (kw) v = prefix + '[' + partial.join('') + ']'; | ||
else if (partial.length === 0) v = '[]'; | ||
else v = prefix + '[' + eolGap + partial.join(eolGap) + eolMind + ']'; | ||
} | ||
else { | ||
for (k in value) { | ||
if (Object.prototype.hasOwnProperty.call(value, k)) { | ||
v = str(value[k]); | ||
// Otherwise, iterate through all of the keys in the object. | ||
if (keepWsc && value.__WSC__) { | ||
kw = value.__WSC__; | ||
kwl = wsc(kw.c[""]); | ||
var keys=kw.o.slice(); | ||
for (k in value) { | ||
if (Object.prototype.hasOwnProperty.call(value, k) && keys.indexOf(k) < 0) | ||
keys.push(k); | ||
} | ||
for (i = 0, length = keys.length; i < length; i++) { | ||
k = keys[i]; | ||
partial.push(kwl + eolGap); | ||
kwl = wsc(kw.c[k]); | ||
v = str(value[k], testWsc(kwl)); | ||
if (v) partial.push(quoteName(k) + (startsWithNL(v) ? ':' : ': ') + v); | ||
} | ||
partial.push(kwl + eolMind); | ||
} | ||
} | ||
else { | ||
for (k in value) { | ||
if (Object.prototype.hasOwnProperty.call(value, k)) { | ||
v = str(value[k]); | ||
if (v) partial.push(quoteName(k) + (startsWithNL(v) ? ':' : ': ') + v); | ||
} | ||
} | ||
} | ||
// Join all of the member texts together, separated with newlines, | ||
// and wrap them in braces. | ||
// Join all of the member texts together, separated with newlines, | ||
// and wrap them in braces. | ||
if (kw) v = prefix + '{' + partial.join('') + '}'; | ||
else if (partial.length === 0) v = '{}'; | ||
else v = prefix + '{' + eolGap + partial.join(eolGap) + eolMind + '}'; | ||
} | ||
if (kw) v = prefix + '{' + partial.join('') + '}'; | ||
else if (partial.length === 0) v = '{}'; | ||
else v = prefix + '{' + eolGap + partial.join(eolGap) + eolMind + '}'; | ||
} | ||
gap = mind; | ||
return v; | ||
gap = mind; | ||
return v; | ||
} | ||
} | ||
} | ||
// Return the hjson_stringify function. It will have access to all of the above | ||
// functions and variables. | ||
// Return the hjson_stringify function. It will have access to all of the above | ||
// functions and variables. | ||
return function (value, opt, space) { | ||
// The stringify method takes a value and an optional replacer, and an optional | ||
// space parameter, and returns a Hjson text. The replacer can be a function | ||
// that can replace values, or an array of strings that will select the keys. | ||
// A default replacer method can be provided. Use of the space parameter can | ||
// produce text that is more easily readable. | ||
return function (value, opt, space) { | ||
// The stringify method takes a value and an optional replacer, and an optional | ||
// space parameter, and returns a Hjson text. The replacer can be a function | ||
// that can replace values, or an array of strings that will select the keys. | ||
// A default replacer method can be provided. Use of the space parameter can | ||
// produce text that is more easily readable. | ||
var i; | ||
var i; | ||
indent = ' '; | ||
keepWsc = false; | ||
indent = ' '; | ||
keepWsc = false; | ||
if (typeof opt === 'object') { | ||
indent = opt.space || ' '; | ||
keepWsc = opt.keepWsc; | ||
} | ||
if (typeof opt === 'object') { | ||
indent = opt.space || ' '; | ||
keepWsc = opt.keepWsc; | ||
} | ||
// 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 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 space === 'number') { | ||
indent = ''; | ||
for (i = 0; i < space; i++) indent += ' '; | ||
} | ||
else if (typeof space === 'string') | ||
indent = space; | ||
if (typeof space === 'number') { | ||
indent = ''; | ||
for (i = 0; i < space; i++) indent += ' '; | ||
} | ||
else if (typeof space === 'string') | ||
indent = space; | ||
// Return the result of stringifying the value. | ||
return str(value, null, true); | ||
// Return the result of stringifying the value. | ||
return str(value, null, true); | ||
}; | ||
}()); | ||
return { | ||
parse: hjson_parse, | ||
stringify: hjson_stringify, | ||
endOfLine: function() { return EOL; }, | ||
setEndOfLine: function(eol) { | ||
if (eol === '\n' || eol === '\r\n') EOL = eol; | ||
}, | ||
bracesSameLine: function() { return bracesSameLine; }, | ||
setBracesSameLine: function(v) { bracesSameLine = v; }, | ||
}; | ||
}()); | ||
// node.js | ||
if (typeof module!=='undefined' && module.exports) { | ||
var os=require('os'); | ||
hjson_EOL=os.EOL; | ||
module.exports= { | ||
parse: hjson_parse, | ||
stringify: hjson_stringify, | ||
endOfLine: function() { return hjson_EOL; }, | ||
setEndOfLine: function(eol) { hjson_EOL = eol; }, | ||
bracesSameLine: function() { return hjson_bracesSameLine; }, | ||
setBracesSameLine: function(v) { hjson_bracesSameLine = v; }, | ||
}; | ||
if (typeof module === "object") { | ||
if (typeof require === "function") { | ||
var os=require('os'); | ||
Hjson.setEndOfLine(os.EOL); | ||
} | ||
module.exports=Hjson; | ||
} |
@@ -6,3 +6,3 @@ { | ||
"author": "Christian Zangl", | ||
"version": "1.3.0", | ||
"version": "1.4.0", | ||
"tags": [ | ||
@@ -9,0 +9,0 @@ "json", |
@@ -49,5 +49,5 @@ # hjson-js | ||
Also see lib/hjson.js | ||
The API is the same for the browser and node version. | ||
## Hjson.parse(text, options) | ||
### Hjson.parse(text, options) | ||
@@ -60,3 +60,3 @@ This method parses *JSON* or *Hjson* text to produce an object or array. | ||
## Hjson.stringify(value, options) | ||
### Hjson.stringify(value, options) | ||
@@ -70,2 +70,10 @@ This method produces Hjson text from a JavaScript value. | ||
### Hjson.endOfLine(), .setEndOfLine(eol) | ||
Gets or sets the EOL character ('\n' or '\r\n'). | ||
### Hjson.bracesSameLine(), .setBracesSameLine(b) | ||
Gets or sets if braces should appear on the same line (for stringify). | ||
## modify & keep comments | ||
@@ -103,23 +111,38 @@ | ||
## v1.2.0 | ||
- v1.4.0 | ||
- add old fashioned /**/ comments | ||
- fix cli eol | ||
Changed the browser interface to match the node api (which didn't change). | ||
## v1.1.0 | ||
Fixed parse for leading zeros ("00") and trailing comments. | ||
- add // support | ||
Fixed stringify for /**/ and // | ||
## v1.0.2 | ||
Added more test cases. | ||
- stringify bug fixes | ||
- v1.3.0 | ||
## v1.0.0 | ||
Added support for the simplified syntax. | ||
- Switched to v1 for semver | ||
- v1.2.0 | ||
- Adds editing support via the `{ keepWsc: true }` option. | ||
Added old fashioned /**/ comments. | ||
- Removes stringify(value, replacer, space) replacer support | ||
Fixed the missing EOL (cli only). | ||
- v1.1.0 | ||
add // support | ||
- v1.0.2 | ||
stringify bug fixes | ||
- v1.0.0 | ||
Switched to v1 for semver. | ||
Adds editing support via the `{ keepWsc: true }` option. | ||
Removes stringify(value, replacer, space) replacer support | ||
You can still use this syntax but replacer will no longer be called. This was removed in favor of editing support and because replacer provided an incomplete solution. |
@@ -5,37 +5,57 @@ | ||
var path = require("path"); | ||
var rootDir = path.normalize(path.join(__dirname, "asset")); | ||
var rootDir = path.normalize(path.join(__dirname, "assets")); | ||
var argv=process.argv.slice(2) | ||
var filter=argv[0]; | ||
Hjson.setEndOfLine("\n"); | ||
function showErr(cap, s1, s2) { | ||
console.log(cap+" FAILED!"); | ||
console.log("--- actual:"); | ||
console.log(s1); | ||
console.log("--- expected:"); | ||
console.log(s2); | ||
function failErr(name, type, s1, s2) { | ||
console.log(name+" "+type+" FAILED!"); | ||
if (s1 || s2) { | ||
console.log("--- actual:"); | ||
console.log(s1); | ||
console.log("--- expected:"); | ||
console.log(s2); | ||
} | ||
process.exit(1); | ||
} | ||
console.log("running tests..."); | ||
fs.readdirSync(rootDir).forEach(function(file) { | ||
var name = file.split("_test.hjson"); | ||
if (name.length !== 2) return; | ||
var name = file.split("_test."); | ||
if (name.length < 2) return; | ||
var isJson = name[2] === "json"; | ||
name = name[0]; | ||
if (filter && name.indexOf(filter) < 0) return; // ignore | ||
var text = fs.readFileSync(path.join(rootDir, file), "utf8"); | ||
var result = JSON.parse(fs.readFileSync(path.join(rootDir, name+"_result.json"), "utf8")); | ||
var shouldFail = name.substr(0, 4) === "fail"; | ||
try { | ||
var data = Hjson.parse(text); | ||
var data1 = JSON.stringify(data, null, 2), data2 = JSON.stringify(result.data, null, 2); | ||
var data1 = JSON.stringify(data, null, 2); | ||
var hjson1 = Hjson.stringify(data); | ||
var hjson2 = fs.readFileSync(path.join(rootDir, name+"_result.txt"), "utf8"); | ||
if (data1 !== data2) showErr(name+" parse", data1, data2); | ||
else if (hjson1 !== hjson2) showErr(name+" stringify", hjson1, hjson2); | ||
else console.log(name+" SUCCESS"); | ||
if (!shouldFail) { | ||
var result = JSON.parse(fs.readFileSync(path.join(rootDir, name+"_result.json"), "utf8")); | ||
var data2 = JSON.stringify(result, null, 2); | ||
var hjson2 = fs.readFileSync(path.join(rootDir, name+"_result.hjson"), "utf8"); | ||
if (data1 !== data2) failErr(name, "parse", data1, data2); | ||
if (hjson1 !== hjson2) failErr(name, "stringify", hjson1, hjson2); | ||
if (isJson) { | ||
var json1 = JSON.stringify(data), json2 = JSON.stringify(JSON.parse(text)); | ||
if (json1 !== json2) failErr(name, "json chk", json1, json2); | ||
} | ||
} | ||
else failErr(name, "should fail"); | ||
} | ||
catch (err) { | ||
if (result.err) console.log(name+" SUCCESS"); | ||
else showErr(name+" exception", err.toString(), ""); | ||
if (!shouldFail) failErr(name, "exception", err.toString(), ""); | ||
} | ||
console.log("- "+name+" OK"); | ||
}); | ||
console.log("ALL OK!"); |
44598
57
954
145