From 1740851f1fc2edc5c626241e8a737523bcabf8d8 Mon Sep 17 00:00:00 2001 From: Zachary Carter Date: Sat, 13 Mar 2010 14:01:56 -0500 Subject: [PATCH] First --- Jakefile | 32 +++++ README.md | 5 + lib/jsonlint.js | 325 +++++++++++++++++++++++++++++++++++++++++ package.json | 14 ++ src/grammar.jison | 91 ++++++++++++ src/grammar.jisonlex | 22 +++ tests/all-tests.js | 23 +++ web/json2.js | 334 +++++++++++++++++++++++++++++++++++++++++++ web/jsonlint.html | 59 ++++++++ web/jsonlint.js | 22 +++ 10 files changed, 927 insertions(+) create mode 100644 Jakefile create mode 100644 README.md create mode 100644 lib/jsonlint.js create mode 100644 package.json create mode 100644 src/grammar.jison create mode 100644 src/grammar.jisonlex create mode 100755 tests/all-tests.js create mode 100644 web/json2.js create mode 100644 web/jsonlint.html create mode 100644 web/jsonlint.js diff --git a/Jakefile b/Jakefile new file mode 100644 index 0000000..a380241 --- /dev/null +++ b/Jakefile @@ -0,0 +1,32 @@ +#!/usr/bin/env narwhal + +var FILE = require("file"), + ENV = require("system").env, + OS = require("os"), + jake = require("jake"); + +var cwd = FILE.path(FILE.cwd()); + +jake.task("build", ["build:commonjs", "build:web"]); + +jake.task("build:commonjs", function () { + OS.system(['jison', 'src/grammar.jison', 'src/grammar.jisonlex']); + OS.system(['mv', 'grammar.js', 'lib/jsonlint.js']); +}); + +jake.task("build:web", function () { + var lint = cwd.join('lib', 'jsonlint.js'); + + var sourceArray = ["var jsonlint = (function(){var require=true,module=false;var exports={};"]; + sourceArray.push(lint.read({charset: "utf-8"}), + "return exports;})()"); + + var source = require("jsmin").encode(sourceArray.join("\n")); + + var stream = cwd.join('web', 'jsonlint.js').open("w"); + stream.print(source).close(); +}); + +jake.task("test", function () { + OS.system(['narwhal', 'tests/all-tests.js']); +}); diff --git a/README.md b/README.md new file mode 100644 index 0000000..a85ed44 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +JSON Lint +========= + +A pure JavaScript version of the service provided at [jsonlin.com](http://jsonlint.com). + diff --git a/lib/jsonlint.js b/lib/jsonlint.js new file mode 100644 index 0000000..808199a --- /dev/null +++ b/lib/jsonlint.js @@ -0,0 +1,325 @@ +/* Jison generated parser */ +var grammar = (function(){ +var parser = {trace: function trace() { +}, +yy: {}, +symbols_: {"JSONString":2,"STRING":3,"JSONNumber":4,"NUMBER":5,"JSONNullLiteral":6,"NULL":7,"JSONBooleanLiteral":8,"TRUE":9,"FALSE":10,"JSONText":11,"JSONObject":12,"JSONArray":13,"JSONValue":14,"{":15,"}":16,"JSONMemberList":17,"JSONMember":18,":":19,",":20,"[":21,"]":22,"JSONElementList":23,"$accept":0,"$end":1}, +terminals_: {"3":"STRING","5":"NUMBER","7":"NULL","9":"TRUE","10":"FALSE","15":"{","16":"}","19":":","20":",","21":"[","22":"]"}, +productions_: [0,[2,1],[4,1],[6,1],[8,1],[8,1],[11,1],[11,1],[14,1],[14,1],[14,1],[14,1],[14,1],[14,1],[12,2],[12,3],[18,3],[17,1],[17,3],[13,2],[13,3],[23,1],[23,3]], +performAction: function anonymous(yytext, yyleng, yylineno, yy) { + var $$ = arguments[5], $0 = arguments[5].length; + switch (arguments[4]) { + case 1: + this.$ = yytext; + break; + case 2: + this.$ = Number(yytext); + break; + case 3: + this.$ = null; + break; + case 4: + this.$ = true; + break; + case 5: + this.$ = false; + break; + case 6: + return this.$ = $$[$0 - 1 + 1 - 1]; + break; + case 7: + return this.$ = $$[$0 - 1 + 1 - 1]; + break; + case 8: + this.$ = $$[$0 - 1 + 1 - 1]; + break; + case 9: + this.$ = $$[$0 - 1 + 1 - 1]; + break; + case 10: + this.$ = $$[$0 - 1 + 1 - 1]; + break; + case 11: + this.$ = $$[$0 - 1 + 1 - 1]; + break; + case 12: + this.$ = $$[$0 - 1 + 1 - 1]; + break; + case 13: + this.$ = $$[$0 - 1 + 1 - 1]; + break; + case 14: + this.$ = {}; + break; + case 15: + this.$ = $$[$0 - 3 + 2 - 1]; + break; + case 16: + this.$ = [$$[$0 - 3 + 1 - 1], $$[$0 - 3 + 3 - 1]]; + break; + case 17: + this.$ = {}; + this.$[$$[$0 - 1 + 1 - 1][0]] = $$[$0 - 1 + 1 - 1][1]; + break; + case 18: + this.$ = $$[$0 - 3 + 1 - 1]; + $$[$0 - 3 + 1 - 1][$$[$0 - 3 + 3 - 1][0]] = $$[$0 - 3 + 3 - 1][1]; + break; + case 19: + this.$ = []; + break; + case 20: + this.$ = $$[$0 - 3 + 2 - 1]; + break; + case 21: + this.$ = [$$[$0 - 1 + 1 - 1]]; + break; + case 22: + this.$ = $$[$0 - 3 + 1 - 1]; + $$[$0 - 3 + 1 - 1].push($$[$0 - 3 + 3 - 1]); + break; + default:; + } +}, +table: [{"11":1,"12":2,"13":3,"15":[1,4],"21":[1,5]},{"1":[3]},{"1":[2,6]},{"1":[2,7]},{"16":[1,6],"17":7,"18":8,"2":9,"3":[1,10]},{"22":[1,11],"23":12,"14":13,"6":14,"8":15,"2":16,"4":17,"12":18,"13":19,"7":[1,20],"9":[1,21],"10":[1,22],"3":[1,10],"5":[1,23],"15":[1,4],"21":[1,5]},{"1":[2,14],"22":[2,14],"20":[2,14],"16":[2,14]},{"16":[1,24],"20":[1,25]},{"16":[2,17],"20":[2,17]},{"19":[1,26]},{"19":[2,1],"22":[2,1],"20":[2,1],"16":[2,1]},{"1":[2,19],"22":[2,19],"20":[2,19],"16":[2,19]},{"22":[1,27],"20":[1,28]},{"22":[2,21],"20":[2,21]},{"22":[2,8],"20":[2,8],"16":[2,8]},{"22":[2,9],"20":[2,9],"16":[2,9]},{"22":[2,10],"20":[2,10],"16":[2,10]},{"22":[2,11],"20":[2,11],"16":[2,11]},{"22":[2,12],"20":[2,12],"16":[2,12]},{"22":[2,13],"20":[2,13],"16":[2,13]},{"22":[2,3],"20":[2,3],"16":[2,3]},{"22":[2,4],"20":[2,4],"16":[2,4]},{"22":[2,5],"20":[2,5],"16":[2,5]},{"22":[2,2],"20":[2,2],"16":[2,2]},{"1":[2,15],"22":[2,15],"20":[2,15],"16":[2,15]},{"18":29,"2":9,"3":[1,10]},{"14":30,"6":14,"8":15,"2":16,"4":17,"12":18,"13":19,"7":[1,20],"9":[1,21],"10":[1,22],"3":[1,10],"5":[1,23],"15":[1,4],"21":[1,5]},{"1":[2,20],"22":[2,20],"20":[2,20],"16":[2,20]},{"14":31,"6":14,"8":15,"2":16,"4":17,"12":18,"13":19,"7":[1,20],"9":[1,21],"10":[1,22],"3":[1,10],"5":[1,23],"15":[1,4],"21":[1,5]},{"16":[2,18],"20":[2,18]},{"16":[2,16],"20":[2,16]},{"22":[2,22],"20":[2,22]}], +parseError: function parseError(str, hash) { + throw new Error(str); +}, +parse: function parse(input) { + var self = this, stack = [0], vstack = [null], table = this.table, yytext = "", yylineno = 0, yyleng = 0, shifts = 0, reductions = 0; + this.lexer.setInput(input); + this.lexer.yy = this.yy; + var parseError = this.yy.parseError = this.yy.parseError || this.parseError; + + function lex() { + var token; + token = self.lexer.lex() || 1; + if (typeof token !== "number") { + token = self.symbols_[token]; + } + return token; + } + + var symbol, state, action, a, r, yyval = {}, p, len, ip = 0, newState, expected; + symbol = lex(); + while (true) { + state = stack[stack.length - 1]; + action = table[state] && table[state][symbol]; + if (typeof action === "undefined" || !action.length || !action[0]) { + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p != 1) { + expected.push("'" + this.terminals_[p] + "'"); + } + } + if (this.lexer.showPosition) { + parseError("Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", "), {text: this.lexer.match, token: this.terminals_[symbol], line: this.lexer.yylineno, expected: expected}); + } else { + parseError("Parse error on line " + (yylineno + 1) + ": Unexpected '" + this.terminals_[symbol] + "'", {text: this.lexer.match, token: this.terminals_[symbol], line: this.lexer.yylineno, expected: expected}); + } + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); + } + a = action; + switch (a[0]) { + case 1: + shifts++; + stack.push(symbol); + ++ip; + yyleng = this.lexer.yyleng; + yytext = this.lexer.yytext; + yylineno = this.lexer.yylineno; + symbol = lex(); + vstack.push(null); + stack.push(a[1]); + break; + case 2: + reductions++; + len = this.productions_[a[1]][1]; + yyval.$ = vstack[vstack.length - len]; + r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, a[1], vstack); + if (typeof r !== "undefined") { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + } + stack.push(this.productions_[a[1]][0]); + vstack.push(yyval.$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + this.reductionCount = reductions; + this.shiftCount = shifts; + return true; + default:; + } + } + return true; +}};/* Jison generated lexer */ +var lexer = (function(){var lexer = ({EOF:"", +parseError:function parseError(str, hash) { + if (this.yy.parseError) { + this.yy.parseError(str, hash); + } else { + throw new Error(str); + } +}, +setInput:function (input) { + this._input = input; + this._more = this._less = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ""; + return this; +}, +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/\n/); + if (lines) { + this.yylineno++; + } + this._input = this._input.slice(1); + return ch; +}, +unput:function (ch) { + this._input = ch + this._input; + return this; +}, +more:function () { + this._more = true; + return this; +}, +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? "..." : "") + past.substr(-20).replace(/\n/g, ""); +}, +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20 - next.length); + } + return (next.substr(0, 20) + (next.length > 20 ? "..." : "")).replace(/\n/g, ""); +}, +showPosition:function () { + var pre = this.pastInput(); + var c = (new Array(pre.length + 1)).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; +}, +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + var token, match, lines; + if (!this._more) { + this.yytext = ""; + this.match = ""; + } + for (var i = 0; i < this.rules.length; i++) { + match = this._input.match(this.rules[i]); + if (match) { + lines = match[0].match(/\n/g); + if (lines) { + this.yylineno += lines.length; + } + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + this._more = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, i); + if (token) { + return token; + } else { + return; + } + } + } + if (this._input == this.EOF) { + return this.EOF; + } else { + this.parseError("Lexical error on line " + (this.yylineno + 1) + ". Unrecognized text.\n" + this.showPosition(), {text: "", token: null, line: this.yylineno}); + } +}, +lex:function () { + var r = this.next(); + if (typeof r !== "undefined") { + return r; + } else { + return this.lex(); + } +}}); +lexer.performAction = function anonymous(yy, yy_) { + switch (arguments[2]) { + case 0: + break; + case 1: + return 5; + break; + case 2: + yy_.yytext = yy_.yytext.substr(1, yy_.yyleng - 2); + return 3; + break; + case 3: + return 15; + break; + case 4: + return 16; + break; + case 5: + return 21; + break; + case 6: + return 22; + break; + case 7: + return 20; + break; + case 8: + return 19; + break; + case 9: + return 9; + break; + case 10: + return 10; + break; + case 11: + return 7; + break; + case 12: + return "INVALID"; + break; + default:; + } +}; +lexer.rules = [/^\s+/,/^-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][-+]?[0-9]+)?\b\b/,/^"(\\["bfnrt\/\\]|\\u[a-fA-F0-9]{4}|[^\0-\x08\x0a-\x1f"\\])*"/,/^\{/,/^\}/,/^\[/,/^\]/,/^,/,/^:/,/^true\b/,/^false\b/,/^null\b/,/^./];return lexer;})() +parser.lexer = lexer; +return parser; +})(); +if (typeof require !== 'undefined') { +exports.parser = grammar; +exports.parse = function () { return grammar.parse.apply(grammar, arguments); } +exports.main = function commonjsMain(args) { + var cwd = require("file").path(require("file").cwd()); + if (!args[1]) { + throw new Error("Usage: " + args[0] + " FILE"); + } + var source = cwd.join(args[1]).read({charset: "utf-8"}); + this.parse(source); +} +if (require.main === module) { + exports.main(require("system").args); +} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..959445c --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "jsonlint", + "author": "Zach Carter", + "email": "zach@carter.name", + "keywords": [ + "jsonlint", + "json", + "parser", + "lint" + ], + "githubName": "jsonlint", + "type": "zip", + "location": "http://github.com/zaach/jsonlint/zipball/master" +} diff --git a/src/grammar.jison b/src/grammar.jison new file mode 100644 index 0000000..ff4abc5 --- /dev/null +++ b/src/grammar.jison @@ -0,0 +1,91 @@ + +/* + ECMA-262 5th Edition, 15.12.1 The JSON Grammar. + Modified to forbid top level primitives. +*/ + + +/* author: Zach Carter */ + + +%start JSONText + +%% + +JSONString + : STRING + {$$ = yytext;} + ; + +JSONNumber + : NUMBER + {$$ = Number(yytext);} + ; + +JSONNullLiteral + : NULL + {$$ = null;} + ; + +JSONBooleanLiteral + : TRUE + {$$ = true;} + | FALSE + {$$ = false;} + ; + +JSONText + : JSONObject + {return $$ = $1;} + | JSONArray + {return $$ = $1;} + ; + +JSONValue + : JSONNullLiteral + {$$ = $1;} + | JSONBooleanLiteral + {$$ = $1;} + | JSONString + {$$ = $1;} + | JSONNumber + {$$ = $1;} + | JSONObject + {$$ = $1;} + | JSONArray + {$$ = $1;} + ; + +JSONObject + : '{' '}' + {{$$ = {};}} + | '{' JSONMemberList '}' + {$$ = $2;} + ; + +JSONMember + : JSONString ':' JSONValue + {$$ = [$1, $3];} + ; + +JSONMemberList + : JSONMember + {{$$ = {}; $$[$1[0]] = $1[1];}} + | JSONMemberList ',' JSONMember + {$$ = $1; $1[$3[0]] = $3[1];} + ; + +JSONArray + : '[' ']' + {$$ = [];} + | '[' JSONElementList ']' + {$$ = $2;} + ; + +JSONElementList + : JSONValue + {$$ = [$1];} + | JSONElementList ',' JSONValue + {$$ = $1; $1.push($3);} + ; + diff --git a/src/grammar.jisonlex b/src/grammar.jisonlex new file mode 100644 index 0000000..d418334 --- /dev/null +++ b/src/grammar.jisonlex @@ -0,0 +1,22 @@ +esc "\\" +int "-"?([0-9]|[1-9][0-9]+) +exp ([eE][-+]?[0-9]+) +frac ("."[0-9]+) + +%% +\s+ {/* skip whitespace */} +{int}{frac}?{exp}?\b {return 'NUMBER';} +'"'("\\"["bfnrt/{esc}]|"\\u"[a-fA-F0-9]{4}|[^\0-\x08\x0a-\x1f"{esc}])*'"' {yytext = yytext.substr(1,yyleng-2); return 'STRING';} +"{" %{ return '{' %} +"}" %{ return '}' %} +"[" {return '['} +"]" {return ']'} +"," {return ','} +":" {return ':'} +"true" {return 'TRUE'} +"false" {return 'FALSE'} +"null" {return 'NULL'} +. {return 'INVALID'} + +%% + diff --git a/tests/all-tests.js b/tests/all-tests.js new file mode 100755 index 0000000..59a9ba5 --- /dev/null +++ b/tests/all-tests.js @@ -0,0 +1,23 @@ +#!/usr/bin/env narwhal + +var fs = require("file"), + assert = require("assert"), + parser = require("../lib/jsonlint").parser; + +exports["test object"] = function () { + var json = '{"foo": "bar"}'; + assert.deepEqual(parser.parse(json), {"foo": "bar"}); +}; + +exports["test string with escaped line break"] = function () { + var json = '{"foo": "bar\\nbar"}'; + assert.deepEqual(parser.parse(json), {"foo": "bar\\nbar"}); +}; + +exports["test string with line break"] = function () { + var json = '{"foo": "bar\nbar"}'; + assert["throws"](function () {parser.parse(json)}, "should throw error"); +}; + +if (require.main === module) + require("os").exit(require("test").run(exports)); diff --git a/web/json2.js b/web/json2.js new file mode 100644 index 0000000..7044c6d --- /dev/null +++ b/web/json2.js @@ -0,0 +1,334 @@ + +/*jslint evil: true, strict: false */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + this.JSON = {}; +} + +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) ? + this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/. +test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/web/jsonlint.html b/web/jsonlint.html new file mode 100644 index 0000000..b088066 --- /dev/null +++ b/web/jsonlint.html @@ -0,0 +1,59 @@ + + + + + JSON Lint + + + + + + +

JSON Lint

+

A pure JavaScript version of the service provided at jsonlint.com.

+ +

+ + +

+

Results

+

+  

project on github

+ + diff --git a/web/jsonlint.js b/web/jsonlint.js new file mode 100644 index 0000000..30964ba --- /dev/null +++ b/web/jsonlint.js @@ -0,0 +1,22 @@ + +var jsonlint=(function(){var require=true,module=false;var exports={};var grammar=(function(){var parser={trace:function trace(){},yy:{},symbols_:{"JSONString":2,"STRING":3,"JSONNumber":4,"NUMBER":5,"JSONNullLiteral":6,"NULL":7,"JSONBooleanLiteral":8,"TRUE":9,"FALSE":10,"JSONText":11,"JSONObject":12,"JSONArray":13,"JSONValue":14,"{":15,"}":16,"JSONMemberList":17,"JSONMember":18,":":19,",":20,"[":21,"]":22,"JSONElementList":23,"$accept":0,"$end":1},terminals_:{"3":"STRING","5":"NUMBER","7":"NULL","9":"TRUE","10":"FALSE","15":"{","16":"}","19":":","20":",","21":"[","22":"]"},productions_:[0,[2,1],[4,1],[6,1],[8,1],[8,1],[11,1],[11,1],[14,1],[14,1],[14,1],[14,1],[14,1],[14,1],[12,2],[12,3],[18,3],[17,1],[17,3],[13,2],[13,3],[23,1],[23,3]],performAction:function anonymous(yytext,yyleng,yylineno,yy){var $$=arguments[5],$0=arguments[5].length;switch(arguments[4]){case 1:this.$=yytext;break;case 2:this.$=Number(yytext);break;case 3:this.$=null;break;case 4:this.$=true;break;case 5:this.$=false;break;case 6:return this.$=$$[$0-1+1-1];break;case 7:return this.$=$$[$0-1+1-1];break;case 8:this.$=$$[$0-1+1-1];break;case 9:this.$=$$[$0-1+1-1];break;case 10:this.$=$$[$0-1+1-1];break;case 11:this.$=$$[$0-1+1-1];break;case 12:this.$=$$[$0-1+1-1];break;case 13:this.$=$$[$0-1+1-1];break;case 14:this.$={};break;case 15:this.$=$$[$0-3+2-1];break;case 16:this.$=[$$[$0-3+1-1],$$[$0-3+3-1]];break;case 17:this.$={};this.$[$$[$0-1+1-1][0]]=$$[$0-1+1-1][1];break;case 18:this.$=$$[$0-3+1-1];$$[$0-3+1-1][$$[$0-3+3-1][0]]=$$[$0-3+3-1][1];break;case 19:this.$=[];break;case 20:this.$=$$[$0-3+2-1];break;case 21:this.$=[$$[$0-1+1-1]];break;case 22:this.$=$$[$0-3+1-1];$$[$0-3+1-1].push($$[$0-3+3-1]);break;default:;}},table:[{"11":1,"12":2,"13":3,"15":[1,4],"21":[1,5]},{"1":[3]},{"1":[2,6]},{"1":[2,7]},{"16":[1,6],"17":7,"18":8,"2":9,"3":[1,10]},{"22":[1,11],"23":12,"14":13,"6":14,"8":15,"2":16,"4":17,"12":18,"13":19,"7":[1,20],"9":[1,21],"10":[1,22],"3":[1,10],"5":[1,23],"15":[1,4],"21":[1,5]},{"1":[2,14],"22":[2,14],"20":[2,14],"16":[2,14]},{"16":[1,24],"20":[1,25]},{"16":[2,17],"20":[2,17]},{"19":[1,26]},{"19":[2,1],"22":[2,1],"20":[2,1],"16":[2,1]},{"1":[2,19],"22":[2,19],"20":[2,19],"16":[2,19]},{"22":[1,27],"20":[1,28]},{"22":[2,21],"20":[2,21]},{"22":[2,8],"20":[2,8],"16":[2,8]},{"22":[2,9],"20":[2,9],"16":[2,9]},{"22":[2,10],"20":[2,10],"16":[2,10]},{"22":[2,11],"20":[2,11],"16":[2,11]},{"22":[2,12],"20":[2,12],"16":[2,12]},{"22":[2,13],"20":[2,13],"16":[2,13]},{"22":[2,3],"20":[2,3],"16":[2,3]},{"22":[2,4],"20":[2,4],"16":[2,4]},{"22":[2,5],"20":[2,5],"16":[2,5]},{"22":[2,2],"20":[2,2],"16":[2,2]},{"1":[2,15],"22":[2,15],"20":[2,15],"16":[2,15]},{"18":29,"2":9,"3":[1,10]},{"14":30,"6":14,"8":15,"2":16,"4":17,"12":18,"13":19,"7":[1,20],"9":[1,21],"10":[1,22],"3":[1,10],"5":[1,23],"15":[1,4],"21":[1,5]},{"1":[2,20],"22":[2,20],"20":[2,20],"16":[2,20]},{"14":31,"6":14,"8":15,"2":16,"4":17,"12":18,"13":19,"7":[1,20],"9":[1,21],"10":[1,22],"3":[1,10],"5":[1,23],"15":[1,4],"21":[1,5]},{"16":[2,18],"20":[2,18]},{"16":[2,16],"20":[2,16]},{"22":[2,22],"20":[2,22]}],parseError:function parseError(str,hash){throw new Error(str);},parse:function parse(input){var self=this,stack=[0],vstack=[null],table=this.table,yytext="",yylineno=0,yyleng=0,shifts=0,reductions=0;this.lexer.setInput(input);this.lexer.yy=this.yy;var parseError=this.yy.parseError=this.yy.parseError||this.parseError;function lex(){var token;token=self.lexer.lex()||1;if(typeof token!=="number"){token=self.symbols_[token];} +return token;} +var symbol,state,action,a,r,yyval={},p,len,ip=0,newState,expected;symbol=lex();while(true){state=stack[stack.length-1];action=table[state]&&table[state][symbol];if(typeof action==="undefined"||!action.length||!action[0]){expected=[];for(p in table[state]){if(this.terminals_[p]&&p!=1){expected.push("'"+this.terminals_[p]+"'");}} +if(this.lexer.showPosition){parseError("Parse error on line "+(yylineno+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+expected.join(", "),{text:this.lexer.match,token:this.terminals_[symbol],line:this.lexer.yylineno,expected:expected});}else{parseError("Parse error on line "+(yylineno+1)+": Unexpected '"+this.terminals_[symbol]+"'",{text:this.lexer.match,token:this.terminals_[symbol],line:this.lexer.yylineno,expected:expected});}} +if(action[0]instanceof Array&&action.length>1){throw new Error("Parse Error: multiple actions possible at state: "+state+", token: "+symbol);} +a=action;switch(a[0]){case 1:shifts++;stack.push(symbol);++ip;yyleng=this.lexer.yyleng;yytext=this.lexer.yytext;yylineno=this.lexer.yylineno;symbol=lex();vstack.push(null);stack.push(a[1]);break;case 2:reductions++;len=this.productions_[a[1]][1];yyval.$=vstack[vstack.length-len];r=this.performAction.call(yyval,yytext,yyleng,yylineno,this.yy,a[1],vstack);if(typeof r!=="undefined"){return r;} +if(len){stack=stack.slice(0,-1*len*2);vstack=vstack.slice(0,-1*len);} +stack.push(this.productions_[a[1]][0]);vstack.push(yyval.$);newState=table[stack[stack.length-2]][stack[stack.length-1]];stack.push(newState);break;case 3:this.reductionCount=reductions;this.shiftCount=shifts;return true;default:;}} +return true;}};var lexer=(function(){var lexer=({EOF:"",parseError:function parseError(str,hash){if(this.yy.parseError){this.yy.parseError(str,hash);}else{throw new Error(str);}},setInput:function(input){this._input=input;this._more=this._less=this.done=false;this.yylineno=this.yyleng=0;this.yytext=this.matched=this.match="";return this;},input:function(){var ch=this._input[0];this.yytext+=ch;this.yyleng++;this.match+=ch;this.matched+=ch;var lines=ch.match(/\n/);if(lines){this.yylineno++;} +this._input=this._input.slice(1);return ch;},unput:function(ch){this._input=ch+this._input;return this;},more:function(){this._more=true;return this;},pastInput:function(){var past=this.matched.substr(0,this.matched.length-this.match.length);return(past.length>20?"...":"")+past.substr(-20).replace(/\n/g,"");},upcomingInput:function(){var next=this.match;if(next.length<20){next+=this._input.substr(0,20-next.length);} +return(next.substr(0,20)+(next.length>20?"...":"")).replace(/\n/g,"");},showPosition:function(){var pre=this.pastInput();var c=(new Array(pre.length+1)).join("-");return pre+this.upcomingInput()+"\n"+c+"^";},next:function(){if(this.done){return this.EOF;} +if(!this._input){this.done=true;} +var token,match,lines;if(!this._more){this.yytext="";this.match="";} +for(var i=0;i