456 lines
13 KiB
JavaScript
456 lines
13 KiB
JavaScript
/* Hjson http://hjson.org */
|
|
"use strict";
|
|
|
|
module.exports = function(source, opt) {
|
|
|
|
var common = require("./hjson-common");
|
|
var dsf = require("./hjson-dsf");
|
|
|
|
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 keepComments;
|
|
var runDsf; // domain specific formats
|
|
|
|
function resetAt() {
|
|
at = 0;
|
|
ch = ' ';
|
|
}
|
|
|
|
function isPunctuatorChar(c) {
|
|
return c === '{' || c === '}' || c === '[' || c === ']' || c === ',' || c === ':';
|
|
}
|
|
|
|
// Call error when something is wrong.
|
|
function error(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) + " ...");
|
|
}
|
|
|
|
function next() {
|
|
// get the next character.
|
|
ch = text.charAt(at);
|
|
at++;
|
|
return ch;
|
|
}
|
|
|
|
function peek(offs) {
|
|
// range check is not required
|
|
return text.charAt(at + offs);
|
|
}
|
|
|
|
function string(allowML) {
|
|
// Parse a string value.
|
|
// callers make sure that (ch === '"' || ch === "'")
|
|
var string = '';
|
|
|
|
// When parsing for string values, we must look for "/' and \ characters.
|
|
var exitCh = ch;
|
|
while (next()) {
|
|
if (ch === exitCh) {
|
|
next();
|
|
if (allowML && exitCh === "'" && ch === "'" && string.length === 0) {
|
|
// ''' indicates a multiline string
|
|
next();
|
|
return mlString();
|
|
} else return string;
|
|
}
|
|
if (ch === '\\') {
|
|
next();
|
|
if (ch === 'u') {
|
|
var uffff = 0;
|
|
for (var i = 0; i < 4; i++) {
|
|
next();
|
|
var c = ch.charCodeAt(0), hex;
|
|
if (ch >= '0' && ch <= '9') hex = c - 48;
|
|
else if (ch >= 'a' && ch <= 'f') hex = c - 97 + 0xa;
|
|
else if (ch >= 'A' && ch <= 'F') hex = c - 65 + 0xa;
|
|
else error("Bad \\u char " + ch);
|
|
uffff = uffff * 16 + hex;
|
|
}
|
|
string += String.fromCharCode(uffff);
|
|
} else if (typeof escapee[ch] === 'string') {
|
|
string += escapee[ch];
|
|
} else break;
|
|
} else if (ch === '\n' || ch === '\r') {
|
|
error("Bad string containing newline");
|
|
} else {
|
|
string += ch;
|
|
}
|
|
}
|
|
error("Bad string");
|
|
}
|
|
|
|
function mlString() {
|
|
// Parse a multiline string value.
|
|
var string = '', triple = 0;
|
|
|
|
// we are at ''' +1 - get indent
|
|
var indent = 0;
|
|
for (;;) {
|
|
var c=peek(-indent-5);
|
|
if (!c || c === '\n') break;
|
|
indent++;
|
|
}
|
|
|
|
function skipIndent() {
|
|
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(); }
|
|
|
|
// When parsing multiline string values, we must look for ' characters.
|
|
for (;;) {
|
|
if (!ch) {
|
|
error("Bad multiline string");
|
|
} else if (ch === '\'') {
|
|
triple++;
|
|
next();
|
|
if (triple === 3) {
|
|
if (string.slice(-1) === '\n') string=string.slice(0, -1); // remove last EOL
|
|
return string;
|
|
} else continue;
|
|
} else {
|
|
while (triple > 0) {
|
|
string += '\'';
|
|
triple--;
|
|
}
|
|
}
|
|
if (ch === '\n') {
|
|
string += '\n';
|
|
next();
|
|
skipIndent();
|
|
} else {
|
|
if (ch !== '\r') string += ch;
|
|
next();
|
|
}
|
|
}
|
|
}
|
|
|
|
function keyname() {
|
|
// quotes for keys are optional in Hjson
|
|
// unless they include {}[],: or whitespace.
|
|
|
|
if (ch === '"' || ch === "'") return string(false);
|
|
|
|
var name = "", start = at, space = -1;
|
|
for (;;) {
|
|
if (ch === ':') {
|
|
if (!name) error("Found ':' but no key name (for an empty key name use quotes)");
|
|
else if (space >=0 && space !== name.length) { at = start + space; error("Found whitespace in your key name (use quotes to include)"); }
|
|
return name;
|
|
} else if (ch <= ' ') {
|
|
if (!ch) error("Found EOF while looking for a key name (check your syntax)");
|
|
else if (space < 0) space = name.length;
|
|
} else if (isPunctuatorChar(ch)) {
|
|
error("Found '" + ch + "' where a key name was expected (check your syntax or use quotes if the key name includes {}[],: or whitespace)");
|
|
} else {
|
|
name += ch;
|
|
}
|
|
next();
|
|
}
|
|
}
|
|
|
|
function white() {
|
|
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;
|
|
}
|
|
}
|
|
|
|
function tfnns() {
|
|
// Hjson strings can be quoteless
|
|
// returns string, true, false, or null.
|
|
var value = ch;
|
|
if (isPunctuatorChar(ch))
|
|
error("Found a punctuator character '" + ch + "' when expecting a quoteless string (check your syntax)");
|
|
|
|
for(;;) {
|
|
next();
|
|
// (detection of ml strings was moved to string())
|
|
var isEol = ch === '\r' || ch === '\n' || ch === '';
|
|
if (isEol ||
|
|
ch === ',' || ch === '}' || ch === ']' ||
|
|
ch === '#' ||
|
|
ch === '/' && (peek(0) === '/' || peek(0) === '*')
|
|
) {
|
|
// this tests for the case of {true|false|null|num}
|
|
// followed by { ',' | '}' | ']' | '#' | '//' | '/*' }
|
|
// which needs to be parsed as the specified value
|
|
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 = common.tryParseNumber(value);
|
|
if (n !== undefined) return n;
|
|
}
|
|
}
|
|
if (isEol) {
|
|
// remove any whitespace at the end (ignored in quoteless strings)
|
|
value = value.trim();
|
|
var dsfValue = runDsf(value);
|
|
return dsfValue !== undefined ? dsfValue : value;
|
|
}
|
|
}
|
|
value += ch;
|
|
}
|
|
}
|
|
|
|
function getComment(cAt, first) {
|
|
var i;
|
|
cAt--;
|
|
// remove trailing whitespace
|
|
// but only up to EOL
|
|
for (i = at - 2; i > cAt && text[i] <= ' ' && text[i] !== '\n'; i--);
|
|
if (text[i] === '\n') i--;
|
|
if (text[i] === '\r') i--;
|
|
var res = text.substr(cAt, i-cAt+1);
|
|
// return if we find anything other than whitespace
|
|
for (i = 0; i < res.length; i++) {
|
|
if (res[i] > ' ') {
|
|
var j = res.indexOf('\n');
|
|
if (j >= 0) {
|
|
var c = [res.substr(0, j), res.substr(j+1)];
|
|
if (first && c[0].trim().length === 0) c.shift();
|
|
return c;
|
|
} else return [res];
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function errorClosingHint(value) {
|
|
function search(value, ch) {
|
|
var i, k, length, res;
|
|
switch (typeof value) {
|
|
case 'string':
|
|
if (value.indexOf(ch) >= 0) res = value;
|
|
break;
|
|
case 'object':
|
|
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
|
for (i = 0, length = value.length; i < length; i++) {
|
|
res=search(value[i], ch) || res;
|
|
}
|
|
} else {
|
|
for (k in value) {
|
|
if (!Object.prototype.hasOwnProperty.call(value, k)) continue;
|
|
res=search(value[k], ch) || res;
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function report(ch) {
|
|
var possibleErr=search(value, ch);
|
|
if (possibleErr) {
|
|
return "found '"+ch+"' in a string value, your mistake could be with:\n"+
|
|
" > "+possibleErr+"\n"+
|
|
" (unquoted strings contain everything up to the next line!)";
|
|
} else return "";
|
|
}
|
|
|
|
return report('}') || report(']');
|
|
}
|
|
|
|
function array() {
|
|
// Parse an array value.
|
|
// assuming ch === '['
|
|
|
|
var array = [];
|
|
var comments, cAt, nextComment;
|
|
try {
|
|
if (keepComments) comments = common.createComment(array, { a: [] });
|
|
|
|
next();
|
|
cAt = at;
|
|
white();
|
|
if (comments) nextComment = getComment(cAt, true).join('\n');
|
|
if (ch === ']') {
|
|
next();
|
|
if (comments) comments.e = [nextComment];
|
|
return array; // empty array
|
|
}
|
|
|
|
while (ch) {
|
|
array.push(value());
|
|
cAt = at;
|
|
white();
|
|
// in Hjson the comma is optional and trailing commas are allowed
|
|
// note that we do not keep comments before the , if there are any
|
|
if (ch === ',') { next(); cAt = at; white(); }
|
|
if (comments) {
|
|
var c = getComment(cAt);
|
|
comments.a.push([nextComment||"", c[0]||""]);
|
|
nextComment = c[1];
|
|
}
|
|
if (ch === ']') {
|
|
next();
|
|
if (comments) comments.a[comments.a.length-1][1] += nextComment||"";
|
|
return array;
|
|
}
|
|
white();
|
|
}
|
|
|
|
error("End of input while parsing an array (missing ']')");
|
|
} catch (e) {
|
|
e.hint=e.hint||errorClosingHint(array);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function object(withoutBraces) {
|
|
// Parse an object value.
|
|
|
|
var key = "", object = {};
|
|
var comments, cAt, nextComment;
|
|
|
|
try {
|
|
if (keepComments) comments = common.createComment(object, { c: {}, o: [] });
|
|
|
|
if (!withoutBraces) {
|
|
// assuming ch === '{'
|
|
next();
|
|
cAt = at;
|
|
} else cAt = 1;
|
|
|
|
white();
|
|
if (comments) nextComment = getComment(cAt, true).join('\n');
|
|
if (ch === '}' && !withoutBraces) {
|
|
if (comments) comments.e = [nextComment];
|
|
next();
|
|
return object; // empty object
|
|
}
|
|
while (ch) {
|
|
key = keyname();
|
|
white();
|
|
if (ch !== ':') error("Expected ':' instead of '" + ch + "'");
|
|
next();
|
|
// duplicate keys overwrite the previous value
|
|
object[key] = value();
|
|
cAt = at;
|
|
white();
|
|
// in Hjson the comma is optional and trailing commas are allowed
|
|
// note that we do not keep comments before the , if there are any
|
|
if (ch === ',') { next(); cAt = at; white(); }
|
|
if (comments) {
|
|
var c = getComment(cAt);
|
|
comments.c[key] = [nextComment||"", c[0]||""];
|
|
nextComment = c[1];
|
|
comments.o.push(key);
|
|
}
|
|
if (ch === '}' && !withoutBraces) {
|
|
next();
|
|
if (comments) comments.c[key][1] += nextComment||"";
|
|
return object;
|
|
}
|
|
white();
|
|
}
|
|
|
|
if (withoutBraces) return object;
|
|
else error("End of input while parsing an object (missing '}')");
|
|
} catch (e) {
|
|
e.hint=e.hint||errorClosingHint(object);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function value() {
|
|
// 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 "'":
|
|
case '"': return string(true);
|
|
default: return tfnns();
|
|
}
|
|
}
|
|
|
|
function checkTrailing(v, c) {
|
|
var cAt = at;
|
|
white();
|
|
if (ch) error("Syntax error, found trailing characters");
|
|
if (keepComments) {
|
|
var b = c.join('\n'), a = getComment(cAt).join('\n');
|
|
if (a || b) {
|
|
var comments = common.createComment(v, common.getComment(v));
|
|
comments.r = [b, a];
|
|
}
|
|
}
|
|
return v;
|
|
}
|
|
|
|
function rootValue() {
|
|
white();
|
|
var c = keepComments ? getComment(1) : null;
|
|
switch (ch) {
|
|
case '{': return checkTrailing(object(), c);
|
|
case '[': return checkTrailing(array(), c);
|
|
default: return checkTrailing(value(), c);
|
|
}
|
|
}
|
|
|
|
function legacyRootValue() {
|
|
// Braces for the root object are optional
|
|
white();
|
|
var c = keepComments ? getComment(1) : null;
|
|
switch (ch) {
|
|
case '{': return checkTrailing(object(), c);
|
|
case '[': return checkTrailing(array(), c);
|
|
}
|
|
|
|
try {
|
|
// assume we have a root object without braces
|
|
return checkTrailing(object(true), c);
|
|
} catch (e) {
|
|
// test if we are dealing with a single JSON value instead (true/false/null/num/"")
|
|
resetAt();
|
|
try { return checkTrailing(value(), c); }
|
|
catch (e2) { throw e; } // throw original error
|
|
}
|
|
}
|
|
|
|
if (typeof source!=="string") throw new Error("source is not a string");
|
|
var dsfDef = null;
|
|
var legacyRoot = true;
|
|
if (opt && typeof opt === 'object') {
|
|
keepComments = opt.keepWsc;
|
|
dsfDef = opt.dsf;
|
|
legacyRoot = opt.legacyRoot !== false; // default true
|
|
}
|
|
runDsf = dsf.loadDsf(dsfDef, "parse");
|
|
text = source;
|
|
resetAt();
|
|
return legacyRoot ? legacyRootValue() : rootValue();
|
|
};
|