| 1 |
|
| 2 |
/* global console, jsonView */ |
| 3 |
/* |
| 4 |
* ViewJSON |
| 5 |
* Version 1.0 |
| 6 |
* A Google Chrome extension to display JSON in a user-friendly format |
| 7 |
* |
| 8 |
* This is a chromeified version of the JSONView Firefox extension by Ben Hollis: |
| 9 |
* http://jsonview.com |
| 10 |
* http://code.google.com/p/jsonview |
| 11 |
* |
| 12 |
* Also based on the XMLTree Chrome extension by Moonty & alan.stroop |
| 13 |
* https://chrome.google.com/extensions/detail/gbammbheopgpmaagmckhpjbfgdfkpadb |
| 14 |
* |
| 15 |
* port by Jamie Wilkinson (@jamiew) | http://jamiedubs.com | http://github.com/jamiew |
| 16 |
* MIT license / copyfree (f) F.A.T. Lab http://fffff.at |
| 17 |
* Speed Project Approved: 2h |
| 18 |
*/ |
| 19 |
|
| 20 |
function collapse(evt) { |
| 21 |
var collapser = evt.target; |
| 22 |
var target = collapser.parentNode.getElementsByClassName('collapsible'); |
| 23 |
if (!target.length) { |
| 24 |
return; |
| 25 |
} |
| 26 |
target = target[0]; |
| 27 |
if (target.style.display === 'none') { |
| 28 |
var ellipsis = target.parentNode.getElementsByClassName('ellipsis')[0]; |
| 29 |
target.parentNode.removeChild(ellipsis); |
| 30 |
target.style.display = ''; |
| 31 |
} else { |
| 32 |
target.style.display = 'none'; |
| 33 |
var ellipsis = document.createElement('span'); |
| 34 |
ellipsis.className = 'ellipsis'; |
| 35 |
ellipsis.innerHTML = ' … '; |
| 36 |
target.parentNode.insertBefore(ellipsis, target); |
| 37 |
} |
| 38 |
collapser.innerHTML = (collapser.innerHTML === '-') ? '+' : '-'; |
| 39 |
} |
| 40 |
|
| 41 |
function addCollapser(item) { |
| 42 |
// This mainly filters out the root object (which shouldn't be collapsible) |
| 43 |
if (item.nodeName !== 'LI') { |
| 44 |
return; |
| 45 |
} |
| 46 |
var collapser = document.createElement('div'); |
| 47 |
collapser.className = 'collapser'; |
| 48 |
collapser.innerHTML = '-'; |
| 49 |
collapser.addEventListener('click', collapse, false); |
| 50 |
item.insertBefore(collapser, item.firstChild); |
| 51 |
} |
| 52 |
|
| 53 |
function jsonView(id, target) { |
| 54 |
this.debug = false; |
| 55 |
if (id.indexOf("#") !== -1) { |
| 56 |
this.idType = "id"; |
| 57 |
this.id = id.replace('#', ''); |
| 58 |
} else if (id.indexOf(".") !== -1) { |
| 59 |
this.idType = "class"; |
| 60 |
this.id = id.replace('.', ''); |
| 61 |
} else { |
| 62 |
if (this.debug) { console.log("Can't find that element"); } |
| 63 |
return; |
| 64 |
} |
| 65 |
|
| 66 |
this.data = document.getElementById(this.id).innerHTML; |
| 67 |
if (typeof(target) !== undefined) { |
| 68 |
if (target.indexOf("#") !== -1) { |
| 69 |
this.targetType = "id"; |
| 70 |
this.target = target.replace('#', ''); |
| 71 |
} else if (id.indexOf(".") !== -1) { |
| 72 |
this.targetType = "class"; |
| 73 |
this.target = target.replace('.', ''); |
| 74 |
} else { |
| 75 |
if (this.debug) { console.log("Can't find the target element"); } |
| 76 |
return; |
| 77 |
} |
| 78 |
} |
| 79 |
// Note: now using "*.json*" URI matching rather than these page regexes -- save CPU cycles! |
| 80 |
// var is_json = /^\s*(\{.*\})\s*$/.test(this.data); |
| 81 |
// var is_jsonp = /^.*\(\s*(\{.*\})\s*\)$/.test(this.data); |
| 82 |
// if(is_json || is_jsonp){ |
| 83 |
// Our manifest specifies that we only do URLs matching '.json', so attempt to sanitize any HTML |
| 84 |
// added by Chrome's "text/plain" or "text/html" handlers |
| 85 |
if (/^\<pre.*\>(.*)\<\/pre\>$/.test(this.data)) { |
| 86 |
if (this.debug) { console.log("JSONView: data is wrapped in <pre>...</pre>, stripping HTML..."); } |
| 87 |
this.data = this.data.replace(/<(?:.|\s)*?>/g, ''); //Aggressively strip HTML. |
| 88 |
} |
| 89 |
// Test if what remains is JSON or JSONp |
| 90 |
var json_regex = /^\s*([\[\{].*[\}\]])\s*$/; // Ghetto, but it works |
| 91 |
var jsonp_regex = /^[\s\u200B\uFEFF]*([\w$\[\]\.]+)[\s\u200B\uFEFF]*\([\s\u200B\uFEFF]*([\[{][\s\S]*[\]}])[\s\u200B\uFEFF]*\);?[\s\u200B\uFEFF]*$/; |
| 92 |
var jsonp_regex2 = /([\[\{][\s\S]*[\]\}])\)/; // more liberal support... this allows us to pass the jsonp.json & jsonp2.json tests |
| 93 |
var is_json = json_regex.test(this.data); |
| 94 |
var is_jsonp = jsonp_regex.test(this.data); |
| 95 |
if (this.debug) { console.log("JSONView: is_json=" + is_json + " is_jsonp=" + is_jsonp); } |
| 96 |
if (is_json || is_jsonp) { |
| 97 |
if (this.debug) { console.log("JSONView: sexytime!"); } |
| 98 |
// JSONFormatter json->HTML prototype straight from Firefox JSONView |
| 99 |
// For reference: http://code.google.com/p/jsonview |
| 100 |
|
| 101 |
function JSONFormatter() { |
| 102 |
// No magic required. |
| 103 |
} |
| 104 |
JSONFormatter.prototype = { |
| 105 |
htmlEncode: function(t) { |
| 106 |
return t != null ? t.toString().replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">") : ''; |
| 107 |
}, |
| 108 |
decorateWithSpan: function(value, className) { |
| 109 |
return '<span class="' + className + '">' + this.htmlEncode(value) + '</span>'; |
| 110 |
}, |
| 111 |
// Convert a basic JSON datatype (number, string, boolean, null, object, array) into an HTML fragment. |
| 112 |
valueToHTML: function(value) { |
| 113 |
var valueType = typeof value; |
| 114 |
var output = ""; |
| 115 |
if (value === null) { |
| 116 |
output += this.decorateWithSpan('null', 'null'); |
| 117 |
} else if (value && value.constructor === Array) { |
| 118 |
output += this.arrayToHTML(value); |
| 119 |
} else if (valueType === 'object') { |
| 120 |
output += this.objectToHTML(value); |
| 121 |
} else if (valueType === 'number') { |
| 122 |
output += this.decorateWithSpan(value, 'num'); |
| 123 |
} else if (valueType === 'string') { |
| 124 |
if (/^(http|https):\/\/[^\s]+$/.test(value)) { |
| 125 |
output += '<a href="' + value + '">' + this.htmlEncode(value) + '</a>'; |
| 126 |
} else { |
| 127 |
output += this.decorateWithSpan('"' + value + '"', 'string'); |
| 128 |
} |
| 129 |
} else if (valueType === 'boolean') { |
| 130 |
output += this.decorateWithSpan(value, 'bool'); |
| 131 |
} |
| 132 |
return output; |
| 133 |
}, |
| 134 |
// Convert an array into an HTML fragment |
| 135 |
arrayToHTML: function(json) { |
| 136 |
var output = '[<ul class="array collapsible">'; |
| 137 |
var hasContents = false; |
| 138 |
for (var prop in json) { |
| 139 |
hasContents = true; |
| 140 |
output += '<li>'; |
| 141 |
output += this.valueToHTML(json[prop]); |
| 142 |
output += '</li>'; |
| 143 |
} |
| 144 |
output += '</ul>]'; |
| 145 |
if (!hasContents) { |
| 146 |
output = "[ ]"; |
| 147 |
} |
| 148 |
return output; |
| 149 |
}, |
| 150 |
// Convert a JSON object to an HTML fragment |
| 151 |
objectToHTML: function(json) { |
| 152 |
var output = '{<ul class="obj collapsible">'; |
| 153 |
var hasContents = false; |
| 154 |
for (var prop in json) { |
| 155 |
hasContents = true; |
| 156 |
output += '<li>'; |
| 157 |
output += '<span class="prop">' + this.htmlEncode(prop) + '</span>: '; |
| 158 |
output += this.valueToHTML(json[prop]); |
| 159 |
output += '</li>'; |
| 160 |
} |
| 161 |
output += '</ul>}'; |
| 162 |
if (!hasContents) { |
| 163 |
output = "{ }"; |
| 164 |
} |
| 165 |
return output; |
| 166 |
}, |
| 167 |
// Convert a whole JSON object into a formatted HTML document. |
| 168 |
jsonToHTML: function(json, callback, uri) { |
| 169 |
var output = ''; |
| 170 |
if (callback) { |
| 171 |
output += '<div class="callback">' + callback + ' (</div>'; |
| 172 |
output += '<div id="json">'; |
| 173 |
} else { |
| 174 |
output += '<div id="json">'; |
| 175 |
} |
| 176 |
output += this.valueToHTML(json); |
| 177 |
output += '</div>'; |
| 178 |
if (callback) { |
| 179 |
output += '<div class="callback">)</div>'; |
| 180 |
} |
| 181 |
return this.toHTML(output, uri); |
| 182 |
}, |
| 183 |
// Produce an error document for when parsing fails. |
| 184 |
errorPage: function(error, data, uri) { |
| 185 |
// var output = '<div id="error">' + this.stringbundle.GetStringFromName('errorParsing') + '</div>'; |
| 186 |
// output += '<h1>' + this.stringbundle.GetStringFromName('docContents') + ':</h1>'; |
| 187 |
var output = '<div id="error">Error parsing JSON: ' + error.message + '</div>'; |
| 188 |
output += '<h1>' + error.stack + ':</h1>'; |
| 189 |
output += '<div id="json">' + this.htmlEncode(data) + '</div>'; |
| 190 |
return this.toHTML(output, uri + ' - Error'); |
| 191 |
}, |
| 192 |
// Wrap the HTML fragment in a full document. Used by jsonToHTML and errorPage. |
| 193 |
toHTML: function(content) { |
| 194 |
return content; |
| 195 |
} |
| 196 |
}; |
| 197 |
// Sanitize & output -- all magic from JSONView Firefox |
| 198 |
this.jsonFormatter = new JSONFormatter(); |
| 199 |
// This regex attempts to match a JSONP structure: |
| 200 |
// * Any amount of whitespace (including unicode nonbreaking spaces) between the start of the file and the callback name |
| 201 |
// * Callback name (any valid JavaScript function name according to ECMA-262 Edition 3 spec) |
| 202 |
// * Any amount of whitespace (including unicode nonbreaking spaces) |
| 203 |
// * Open parentheses |
| 204 |
// * Any amount of whitespace (including unicode nonbreaking spaces) |
| 205 |
// * Either { or [, the only two valid characters to start a JSON string. |
| 206 |
// * Any character, any number of times |
| 207 |
// * Either } or ], the only two valid closing characters of a JSON string. |
| 208 |
// * Any amount of whitespace (including unicode nonbreaking spaces) |
| 209 |
// * A closing parenthesis, an optional semicolon, and any amount of whitespace (including unicode nonbreaking spaces) until the end of the file. |
| 210 |
// This will miss anything that has comments, or more than one callback, or requires modification before use. |
| 211 |
var outputDoc = ''; |
| 212 |
// text = text.match(jsonp_regex)[1]; |
| 213 |
var cleanData = '', |
| 214 |
callback = ''; |
| 215 |
var callback_results = jsonp_regex.exec(this.data); |
| 216 |
if (callback_results && callback_results.length === 3) { |
| 217 |
if (this.debug) { console.log("THIS IS JSONp"); } |
| 218 |
callback = callback_results[1]; |
| 219 |
cleanData = callback_results[2]; |
| 220 |
} else { |
| 221 |
if (this.debug) { console.log("Vanilla JSON"); } |
| 222 |
cleanData = this.data; |
| 223 |
} |
| 224 |
if (this.debug) { console.log(cleanData); } |
| 225 |
// Covert, and catch exceptions on failure |
| 226 |
try { |
| 227 |
// var jsonObj = this.nativeJSON.decode(cleanData); |
| 228 |
var jsonObj = JSON.parse(cleanData); |
| 229 |
if (jsonObj) { |
| 230 |
outputDoc = this.jsonFormatter.jsonToHTML(jsonObj, callback); |
| 231 |
} else { |
| 232 |
throw "There was no object!"; |
| 233 |
} |
| 234 |
} catch (e) { |
| 235 |
if (this.debug) { console.log(e); } |
| 236 |
outputDoc = this.jsonFormatter.errorPage(e, this.data); |
| 237 |
} |
| 238 |
var links = '<style type="text/css">.jsonViewOutput .prop{font-weight:700;}.jsonViewOutput .null{color:red;}.jsonViewOutput .string{color:green;}.jsonViewOutput .collapser{position:absolute;left:-1em;cursor:pointer;}.jsonViewOutput li{position:relative;}.jsonViewOutput li:after{content:\',\';}.jsonViewOutput li:last-child:after{content:\'\';}.jsonViewOutput #error{-moz-border-radius:8px;border:1px solid #970000;background-color:#F7E8E8;margin:.5em;padding:.5em;}.jsonViewOutput .errormessage{font-family:monospace;}.jsonViewOutput #json{font-family:monospace;font-size:1.1em;}.jsonViewOutput ul{list-style:none;margin:0 0 0 2em;padding:0;}.jsonViewOutput h1{font-size:1.2em;}.jsonViewOutput .callback + #json{padding-left:1em;}.jsonViewOutput .callback{font-family:monospace;color:#A52A2A;}.jsonViewOutput .bool,.jsonViewOutput .num{color:blue;}</style>'; |
| 239 |
if (this.targetType !== undefined) { |
| 240 |
this.idType = this.targetType; |
| 241 |
this.id = this.target; |
| 242 |
} |
| 243 |
var el; |
| 244 |
if (this.idType === "class") { |
| 245 |
el = document.getElementsByClassName(this.id); |
| 246 |
if (el) { |
| 247 |
el.className += el.className ? ' jsonViewOutput' : 'jsonViewOutput'; |
| 248 |
el.innerHTML = links + outputDoc; |
| 249 |
} |
| 250 |
} else if (this.idType === "id") { |
| 251 |
el = document.getElementById(this.id); |
| 252 |
if (el) { |
| 253 |
el.className += el.className ? ' jsonViewOutput' : 'jsonViewOutput'; |
| 254 |
el.innerHTML = links + outputDoc; |
| 255 |
} |
| 256 |
el.innerHTML = links + outputDoc; |
| 257 |
} |
| 258 |
var items = document.getElementsByClassName('collapsible'); |
| 259 |
for (var i = 0; i < items.length; i++) { |
| 260 |
addCollapser(items[i].parentNode); |
| 261 |
} |
| 262 |
} else { |
| 263 |
// console.log("JSONView: this is not json, not formatting."); |
| 264 |
} |
| 265 |
} |