Skip to content

Instantly share code, notes, and snippets.

@1995eaton
Created June 17, 2015 01:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1995eaton/4977e2743a83257648eb to your computer and use it in GitHub Desktop.
Save 1995eaton/4977e2743a83257648eb to your computer and use it in GitHub Desktop.
JSON parser written in JavaScript
module.exports = parse = (function() {
function isDigit(c) {
return c >= '0' && c <= '9';
}
function Parser(source) {
this.cursor = 0;
this.lineno = 1;
this.tokens = [];
this.lookahead = 0;
this.tokindex = 0;
this.source = String(source);
}
Parser.prototype = {
skip: function() {
for (;;) {
var c = this.source[this.cursor];
if (c === ' ' || c === '\t') {
this.cursor++;
} else if (c === '\n') {
this.cursor++;
this.lineno++;
} else {
break;
}
}
},
get: function(checkEOF) {
this.assertEOF();
this.skip();
if (this.cursor === this.source.length) {
return;
}
var c = this.source[this.cursor], r;
switch (c) {
case '{':
r = this.scanObject(); break;
case '"':
r = this.scanString(); break;
case '[':
r = this.scanArray(); break;
case '-': case '0': case '1': case '2': case '3': case '4': case '5':
case '6': case '7': case '8': case '9':
r = this.scanNumber(); break;
case 't':
this.getc();
this.assertToken('r');
this.assertToken('u');
this.assertToken('e');
r = true; break;
case 'f':
this.getc();
this.assertToken('a');
this.assertToken('l');
this.assertToken('s');
this.assertToken('e');
r = false; break;
case 'n':
this.getc();
this.assertToken('u');
this.assertToken('l');
this.assertToken('l');
r = null; break;
default:
throw Error('Unexpected character: ' + c);
}
this.skip();
if (checkEOF && !this.eof()) {
throw Error('Unexpected character: ' + this.source[this.cursor]);
}
return r;
},
getc: function() {
this.assertEOF();
return this.source[this.cursor++];
},
peekc: function() {
this.assertEOF();
return this.source[this.cursor];
},
eof: function() {
return this.cursor >= this.source.length;
},
assertToken: function(token) {
this.assertEOF();
var c = this.source[this.cursor];
if (c !== token) {
throw Error('Unexpected character: ' + c);
}
this.cursor++;
},
assertEOF: function() {
if (this.eof()) {
throw Error('Unexpected end of input');
}
},
scanObject: function() {
var obj = {}, c;
this.assertToken('{');
for (;;) {
this.skip();
c = this.peekc();
if (c === '}') {
this.cursor++;
return obj;
}
var key = this.scanString();
this.skip();
this.assertToken(':');
var val = this.get();
obj[key] = val;
this.skip();
c = this.source[this.cursor];
if (c === '}') {
this.cursor++;
return obj;
}
this.assertToken(',');
this.skip();
c = this.source[this.cursor];
if (c === '}') {
throw Error('Unexpected character: }');
}
}
},
scanArray: function() {
var arr = [], c;
this.assertToken('[');
for (;;) {
this.skip();
c = this.source[this.cursor];
if (c === ']') {
this.cursor++;
return arr;
}
arr.push(this.get());
this.skip();
c = this.source[this.cursor];
if (c === ']') {
this.cursor++;
return arr;
}
this.assertToken(',');
this.skip();
c = this.source[this.cursor];
if (c === ']') {
throw Error('Unexpected character: ]');
}
}
},
scanNumber: function() {
var num = '', c;
c = this.getc();
if (c === '-') {
num += c;
c = this.getc();
}
if (!isDigit(c)) {
throw Error('Unexpected character: ' + c);
}
num += c;
if (c === '0') {
if (isDigit(this.peekc())) {
throw Error('Unexpected character: ' + this.peekc());
}
} else {
while (isDigit(this.peekc())) {
num += this.getc();
}
}
if (this.peekc() === '.') {
num += '.';
this.getc();
while (isDigit(this.peekc())) {
num += this.getc();
}
if (num.charAt(num.length - 1) === '.') {
throw Error('Unexpected end of number');
}
}
c = this.peekc();
if (c === 'e' || c === 'E') {
num += 'e';
this.getc();
c = this.peekc();
if (c === '-' || c === '+') {
num += c;
this.getc();
}
while (isDigit(this.peekc())) {
num += this.getc();
}
if (num.charAt(num.length - 1) === 'e') {
throw Error('Unexpected end of number');
}
}
return +num;
},
scanString: function() {
this.assertToken('"');
var str = '';
var esc = false;
for (;;) {
this.assertEOF();
var c = this.source[this.cursor++];
if (esc) {
esc = false;
switch (c) {
case '"': str += '"'; break;
case '\\': str += '\\'; break;
case '\/': str += '/'; break;
case 'b': str += '\b'; break;
case 'f': str += '\f'; break;
case 'n': str += '\n'; break;
case 'r': str += '\r'; break;
case 't': str += '\t'; break;
case 'u':
var n = this.getc() + this.getc() + this.getc() + this.getc();
if (!/^[0-9abcdef]{4}$/i.test(n)) {
throw Error('Invalid unicode character');
}
str += String.fromCharCode(parseInt(n, 16));
break;
default:
throw Error('Invalid escape character: ' + c);
}
} else if (c === '"') {
return str;
} else if (!esc && c === '\\') {
esc = true;
} else {
str += c;
}
}
}
};
return function(source) {
return new Parser(source).get(true);
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment