TouchDevelop/lib/JsonParser.ts

269 строки
9.4 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev.RT {
export class JsonParser
{
/// JSON parser trying to be more lenient than real JSON, mimicking what .NET does. This means we accept
/// - single quoted or double quoted keys
/// - unquoted keys, as long as they are letter, digit, _, -, ., +, -
/// NOTE this does not match .NET exactly, as .NET uses Unicode characterization of IsNumberOrDigit.
///
static parse(s: string, log? : (msg:string) => void) : JsonObject
{
var index = 0;
var current = s[0];
var onerror = function (m:string) :void {
throw { message: m }
};
var cnext = function (expected:string) {
if (expected != current) {
onerror(lf("got `{0}`, but expected `{1}`", current, expected));
}
current = s[++index];
return current;
};
var parseNumber = function () : number {
var value: number;
var sofar = "";
if (current === "-") {
sofar = "-";
cnext("-");
}
while (current >= "0" && current <= "9") {
sofar += current;
cnext(current);
}
if (current === ".") {
sofar += current;
while (cnext(current) && current >= "0" && current <= "9") {
sofar += current;
}
}
if (current === "E" || current === "e") {
sofar += current;
cnext(current);
if (current === "-" || current === "+") {
sofar += current;
cnext(current);
}
while (current >= "0" && current <= "9") {
sofar += current;
cnext(current);
}
}
value = +sofar;
if (isNaN(value)) {
onerror(lf("not a number"));
}
return value;
}
var getQuoteChar = function() : string {
if (current == '"') return current;
if (current == "'") return current;
onerror(lf("missing quote for string"));
}
var parseString = function () {
var quote = getQuoteChar();
var escapedChar=false;
var sofar = "";
while (cnext(current)) {
if (current === "\\") {
if (escapedChar) {
sofar += '\\';
escapedChar = false;
}
else {
escapedChar = true;
}
continue;
}
if (escapedChar) {
escapedChar = false;
if (current === '"' || current === "'" || current === "/") {
sofar += current;
}
else if (current === "b") {
sofar += "\b";
}
else if (current === "f") {
sofar += "\f";
}
else if (current === "n") {
sofar += "\n";
}
else if (current === "r") {
sofar += "\r";
}
else if (current === "t") {
sofar += "\t";
}
else if (current === "u") {
var value = 0;
for (var i = 0; i < 4; i++) {
var digit = parseInt(cnext(current), 16);
if (!isFinite(digit)) {
break;
}
value = value * 16 + digit;
}
sofar += String.fromCharCode(value);
}
else {
onerror(lf("bad escaped char: {0}", current));
}
}
else {
if (current === quote) {
cnext(quote);
return sofar;
}
sofar += current;
}
}
onerror(lf("non-terminated string"));
}
var skipWhiteSpace = function () {
while (current && current <= " ") {
cnext(current);
}
}
var parsePrimitiveToken = function() {
var sofar = "";
while (current) {
if (current >= "0" && current <= "9" || current >= "a" && current <= "z" || current >= "A" && current <= "Z" || current === "." || current === "-" || current === "_" || current === "+") {
sofar += current;
cnext(current);
}
else {
break;
}
}
return sofar;
}
var parsePrimitive = function () : any {
switch (current) {
case 't':
cnext("t");
cnext("r");
cnext("u");
cnext("e");
return true;
case 'f':
cnext("f");
cnext("a");
cnext("l");
cnext("s");
cnext("e");
return false;
case 'n':
cnext("n");
cnext("u");
cnext("l");
cnext("l");
return null;
}
onerror("expected true, false, or null at \n"
+" " + s.slice(Math.max(0, index - 20), index) + "\n"
+" ---->" + s.slice(index + 1, Math.min(s.length, index + 20)));
}
var parseArray : () => any[] = function() {
var result = [];
cnext('[');
skipWhiteSpace();
if (current === ']') {
cnext(']');
return result;
}
while (current) {
result.push(parseValue());
skipWhiteSpace();
if (current === ']') {
cnext(']');
return result;
}
cnext(",");
skipWhiteSpace();
}
onerror(lf("incomplete array"));
return result;
}
var parseValue : () => any = function (){
skipWhiteSpace();
switch (current) {
case '{': return parseObject();
case '[': return parseArray();
case '"':
case "'":
return parseString();
case '-':
return parseNumber();
default:
if (current >= '0' && current <= '9') return parseNumber();
else return parsePrimitive();
}
}
var parseObject = function (): any {
var result = {};
cnext("{");
skipWhiteSpace();
if (current === "}") {
cnext("}");
return result;
}
var key: string;
while (current) {
if (current === "'" || current === '"') {
key = parseString();
}
else {
key = parsePrimitiveToken();
}
if (key === null || key === undefined) {
onerror(lf("bad key"));
}
skipWhiteSpace();
cnext(":");
result[key] = parseValue();
skipWhiteSpace();
if (current === "}") {
cnext("}");
return result;
}
cnext(",");
skipWhiteSpace();
}
onerror(lf("incomplete object"));
}
try {
var value = parseValue();
skipWhiteSpace();
if (current) {
onerror(lf("illegal json value"));
}
var js = JsonObject.wrap(value);
return js;
}
catch(e)
{
if (log)
log(lf("error parsing json {0} {1}", s, e.message));
return undefined;
}
}
}
}