/* 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(); };