Created
June 17, 2015 01:42
-
-
Save 1995eaton/4977e2743a83257648eb to your computer and use it in GitHub Desktop.
JSON parser written in JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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