Skip to content

Commit 1fc2422

Browse files
committed
A completely rewritten JSON formatter inspiret by Ben Hollis' Firefox extension JSONView. MIT License, of course.
1 parent 55bfb2f commit 1fc2422

File tree

4 files changed

+260
-0
lines changed

4 files changed

+260
-0
lines changed

json-viewer/formatter.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
var JSONFormatter = (function() {
2+
var toString = Object.prototype.toString, re =
3+
// This regex attempts to match a JSONP structure ("ws" includes Unicode ws)
4+
// * optional leading ws
5+
// * callback name (any valid function name as per ECMA-262 Edition 3 specs)
6+
// * optional ws
7+
// * open parenthesis
8+
// * optional ws
9+
// * either { or [, the only two valid characters to start a JSON string
10+
// * any character, any number of times
11+
// * either } or ], the only two valid closing characters of a JSON string
12+
// * optional trailing ws and semicolon
13+
// (this of course misses anything that has comments, more than one callback
14+
// -- or otherwise requires modification before use by a proper JSON parser)
15+
/^[\s\u200B\uFEFF]*([\w$\[\]\.]+)[\s\u200B\uFEFF]*\([\s\u200B\uFEFF]*([\[{][\s\S]*[\]}])[\s\u200B\uFEFF]*\)([\s\u200B\uFEFF;]*)$/m;
16+
17+
function detectJSONP(s, url) {
18+
var js = s, cb = '', se = '', match;
19+
if ((match = re.exec(s)) && 4 === match.length) {
20+
cb = match[1];
21+
js = match[2];
22+
se = match[3].replace(/[^;]+/g, '');
23+
}
24+
25+
try {
26+
return wrapJSONP(JSON.parse(js), cb, se, url);
27+
}
28+
catch (e) {
29+
return error(e, s, se, url);
30+
}
31+
}
32+
33+
// Convert a JSON value / JSONP response into a formatted HTML document
34+
function wrapJSONP(val, callback, semicolon, url) {
35+
var output = '<span id="json">' +
36+
value(val, callback ? '' : null, callback && '<br\n/>') +
37+
'</span>';
38+
if (callback)
39+
output = '<span class="callback">'+ callback +'(</span>'+ output +
40+
'<span class="callback">)'+ semicolon +'</span>';
41+
return doc(output, url);
42+
}
43+
44+
// utility functions
45+
46+
function isArray(obj) {
47+
return '[object Array]' === toString.call(obj);
48+
}
49+
50+
// Wrap a fragment in a span of class className
51+
function span(value, className) {
52+
return '<span class="'+ className +'">'+ html(value) +'</span>';
53+
}
54+
55+
// Wrap a fragment of HTML in a document
56+
function doc(fragment, title) {
57+
return '<!DOCTYPE html>\n' +
58+
'<html><head><title>'+ html(title) +'</title>\n' +
59+
'<link rel="stylesheet" type="text/css" href="json.css">\n' +
60+
'<script type="text/javascript" src="live.js"></script>\n' +
61+
'</head><body>\n'+ fragment +'<br\n/></body></html>';
62+
}
63+
64+
// Produce an error document for when parsing fails
65+
function error(e, data, url) {
66+
var output = '<div id="error">Error parsing JSON: '+ e +'</div>';
67+
output += '<h1>Document contents:</h1>';
68+
output += '<span id="json">'+ html(data) +'</span>';
69+
return doc(output, (url ? url +' - ' : '') +'Error');
70+
}
71+
72+
// escaping functions
73+
74+
function html(s, isAttribute) {
75+
if (s == null) return '';
76+
s = (s+'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
77+
return isAttribute ? s.replace(/"/g, '&quot;') : s;
78+
}
79+
80+
var js = JSON.stringify('\b\f\n\r\t') === '"\\b\\f\\n\\r\\t"' ?
81+
function saneJSEscaper(s, noQuotes) {
82+
s = html(JSON.stringify(s).slice(1, -1));
83+
return noQuotes ? s : '"'+ s +'"';
84+
}
85+
: function insaneEscaper(s, noQuotes) {
86+
// undo all damage of an \uXXXX-tastic Mozilla JSON serializer
87+
var had = { '\b': 'b' // return
88+
, '\f': 'f' // these
89+
, '\r': 'r' // to the
90+
, '\n': 'n' // tidy
91+
, '\t': 't' // form
92+
}, ws; // below
93+
for (ws in had)
94+
if (-1 === s.indexOf(ws))
95+
delete had[ws];
96+
97+
s = JSON.stringify(s).slice(1, -1);
98+
99+
for (ws in had)
100+
s = s.replace(new RegExp('\\\\u000'+(ws.charCodeAt().toString(16)), 'ig'),
101+
'\\'+ had[ws]);
102+
103+
s = html(s);
104+
return noQuotes ? s : '"'+ s +'"';
105+
};
106+
107+
// conversion functions
108+
109+
// Convert JSON value (Boolean, Number, String, Array, Object, null)
110+
// into an HTML fragment
111+
function value(v, indent, nl) {
112+
var output;
113+
switch (typeof v) {
114+
case 'boolean':
115+
output = span(v, 'bool');
116+
break;
117+
118+
case 'number':
119+
output = span(v, 'num');
120+
break;
121+
122+
case 'string':
123+
if (/^(\w+):\/\/[^\s]+$/i.test(v)) {
124+
output = '"<a href="'+ html(v, !!'attribute') +'">' +
125+
js(v, '!"') +
126+
'</a>"';
127+
} else {
128+
output = '<span class="string">'+ js(v) +'</span>';
129+
}
130+
break;
131+
132+
case 'object':
133+
if (null === v) {
134+
output = span('null', 'null');
135+
} else {
136+
indent = indent == null ? '' : indent +'&nbsp; ';
137+
if (isArray(v)) {
138+
output = array(v, indent, nl);
139+
} else {
140+
output = object(v, indent, nl);
141+
}
142+
}
143+
break;
144+
}
145+
return output;
146+
}
147+
148+
// Convert an Object to an HTML fragment
149+
function object(obj, indent, nl) {
150+
var output = '';
151+
for (var key in obj) {
152+
if (output) output += '<br\n/>'+ indent +', ';
153+
output += '<span class="prop">'+ js(key) +'</span>: ' +
154+
value(obj[key], indent, '<br\n/>');
155+
}
156+
if (!output) return '{}';
157+
return '<span class="unfolded obj"><span class="content">' +
158+
(nl ? nl + indent : '') +'{ '+ output +'<br\n/>' +
159+
indent +'}</span></span>';
160+
}
161+
162+
// Convert an Array into an HTML fragment
163+
function array(a, indent, nl) {
164+
for (var i = 0, output = ''; i < a.length; i++) {
165+
if (output) output += '<br\n/>'+ indent +', ';
166+
output += value(a[i], indent, '');
167+
}
168+
if (!output) return '[]';
169+
return '<span class="unfolded array"><span class="content">' +
170+
(nl ? nl + indent : '') +'[ '+ output +'<br\n/>' +
171+
indent +']</span></span>';
172+
}
173+
174+
return function JSONFormatter(s, url) {
175+
return detectJSONP(s, url);
176+
};
177+
})();

json-viewer/json.css

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
body {
2+
font-family: sans-serif;
3+
}
4+
5+
.prop {
6+
font-weight: bold;
7+
}
8+
9+
.null {
10+
color: red;
11+
}
12+
13+
.bool {
14+
color: blue;
15+
}
16+
17+
.num {
18+
color: blue;
19+
}
20+
21+
.string {
22+
color: green;
23+
white-space: pre-wrap;
24+
}
25+
26+
#error {
27+
-moz-border-radius: 8px;
28+
border: 1px solid #970000;
29+
background-color: #F7E8E8;
30+
margin: .5em;
31+
padding: .5em;
32+
}
33+
34+
.errormessage {
35+
font-family: monospace;
36+
}
37+
38+
#json {
39+
white-space: pre-wrap;
40+
font-family: monospace;
41+
font-size: 1.1em;
42+
}
43+
44+
h1 {
45+
font-size: 1.2em;
46+
}
47+
48+
.callback {
49+
font-family: monospace;
50+
color: #A52A2A;
51+
}
52+
.folded > .content {
53+
display: none;
54+
}
55+
56+
.folded.array:after {
57+
content: "[\002026 ]"; /* [...] */
58+
}
59+
60+
.folded.obj:after {
61+
content: "{\002026 }"; /* {...} */
62+
}
63+
64+
.folded {
65+
cursor: se-resize;
66+
}
67+
68+
.unfolded {
69+
cursor: nw-resize;
70+
}

json-viewer/live.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
document.addEventListener('click', function folding(e) {
2+
var elem = e.target, is;
3+
do {
4+
if (/^a$/i.test(elem.nodeName)) return;
5+
is = elem.className || '';
6+
} while (!/\b(un)?folded /.test(is) && (elem = elem.parentNode));
7+
if (elem)
8+
elem.className = /unfolded /.test(is)
9+
? is.replace('unfolded ', 'folded ')
10+
: is.replace('folded ', 'unfolded ');
11+
}, false);

json-viewer/test.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
2+
<script src="formatter.js"></script><script src="live.js"></script><script>window.onload = function() { document.body.innerHTML = JSONFormatter('jsonp(\n{ "hey": "guy"\n, "anumber": 243\n, "anobject":\n { "whoa": "nuts"\n , "anarray":\n [ 1\n , 2\n , [ 1\n , 2\n , { "click anywhere": "to fold!"\n }\n ]\n , "thr<h1>ee"\n , "Best of all? You can copy and paste this pretty-printed layout from any JSON file, whatever their formatting."\n ]\n , "more\\u0000challenging": "stuff \\t with \\b misc \\f control \\u2620 codes\\r\\n"\n }\n, "awesome": true\n, "bogus": false\n, "meaning": null\n, "japanese": "明日がある。"\n, "link": "https://github.com/johan/QuickJSON"\n, "notLink": "JSONFormatter and http://github.com/ are great!"\n, "aZero": 0\n, "aFloat": 47.11\n, "emptyString":""\n})\n'); }</script>

0 commit comments

Comments
 (0)