// ==UserScript==
// @name Github: JSON reformatter
// @namespace http://github.com/johan/
// @description Reformats JSON(P) files in the github tree view for readability.
// @include https://github.com/*/blob/*/*.json*
// @match https://github.com/*/blob/*/*.json*
// @version 0.0.1.20140419225639
// ==/UserScript==
var $json, $ln, $o_js, $o_ln // json and line numbers jQuery objects + originals
, spc = ' '
, js_css = // all custom formatting for our node.js-style-indented foldable json
'.json{white-space:pre-wrap;font-family:monospace;}' +
'.json .callback{color:Blue;}' +
'.json .prop{color:DarkGoldenRod;}' +
'.json .str{color:RosyBrown;}' +
'.json .null,.json .bool{color:CadetBlue;}' +
'.json .num{color:#000;}' +
// let :before rules remain visible but make this essentially "display: none":
'.json .folded *{height:0;width:0;top:-999cm;left:-999cm;white-space:normal;'+
'position:absolute;color:transparent;}' +
'.json .folded.arr:before{color:#666;content:"[\\002026 ]'+ spc +'";}' +// […]
'.json .folded.obj:before{color:#666;content:"{\\002026 }'+ spc +'";}' +// {…}
'.json .folded{background:#FFF;}' +
'.json .folded:hover{font-weight:700;color:#000;}' +
'.json .folded{cursor:se-resize;}' +
'.json .unfolded.hovered{background:rgba(255,192,203,0.5);}' +
'.json .unfolded{cursor:nw-resize;}';
var JSONFormatter = (function() {
var toString = Object.prototype.toString, BR = '<br\n/>', re =
// This regex attempts to match a JSONP structure (ws includes Unicode ws)
// * optional leading ws
// * callback name (any valid function name as per ECMA-262 Edition 3 specs)
// * optional ws
// * open parenthesis
// * optional ws
// * either { or [, the only two valid characters to start a JSON string
// * any character, any number of times
// * either } or ], the only two valid closing characters of a JSON string
// * optional trailing ws and semicolon
// (this of course misses anything that has comments, more than one callback
// -- or otherwise requires modification before use by a proper JSON parser)
/^[\s\u200B\uFEFF]*([\w$\[\]\.]+)[\s\u200B\uFEFF]*\([\s\u200B\uFEFF]*([\[{][\s\S]*[\]}])[\s\u200B\uFEFF]*\)([\s\u200B\uFEFF;]*)$/m;
function detectJSONP(s) {
var js = s, cb = '', se = '', match;
if ('string' !== typeof s) return wrapJSONP(s, cb, se);
if ((match = re.exec(s)) && 4 === match.length) {
cb = match[1];
js = match[2];
se = match[3].replace(/[^;]+/g, '');
}
try {
return wrapJSONP(JSON.parse(js), cb, se);
}
catch (e) {
return error(e, s);
}
}
// Convert a JSON value / JSONP response into a formatted HTML document
function wrapJSONP(val, callback, semicolon) {
var output = span(value(val, callback ? '' : null, callback && BR),
'json');
if (callback)
output = span(callback +'(', 'callback') + output +
span(')'+ semicolon, 'callback');
return output;
}
// utility functions
function isArray(obj) {
return '[object Array]' === toString.call(obj);
}
// Wrap a fragment in a span of class className
function span(html, className) {
return '<span class=\''+ className +'\'>'+ html +'</span>';
}
// Produce an error document for when parsing fails
function error(e, data) {
return span('Error parsing JSON: '+ e, 'error') +'<h1>Content:</h1>'+
span(html(data), 'json');
}
// escaping functions
function html(s, isAttribute) {
if (s == null) return '';
s = (s+'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
return isAttribute ? s.replace(/'/g, ''') : s;
}
var js = JSON.stringify('\b\f\n\r\t').length === 12 ?
function saneJSEscaper(s, noQuotes) {
s = html(JSON.stringify(s).slice(1, -1));
return noQuotes ? s : '"'+ s +'"';
}
: function insaneEscaper(s, noQuotes) {
// undo all damage of an \uXXXX-tastic Mozilla JSON serializer
var had = { '\b': 'b' // return
, '\f': 'f' // these
, '\r': 'r' // to the
, '\n': 'n' // tidy
, '\t': 't' // form
}, ws; // below
for (ws in had)
if (-1 === s.indexOf(ws))
delete had[ws];
s = JSON.stringify(s).slice(1, -1);
for (ws in had)
s = s.replace(new RegExp('\\\\u000'+(ws.charCodeAt().toString(16)), 'ig'),
'\\'+ had[ws]);
s = html(s);
return noQuotes ? s : '"'+ s +'"';
};
// conversion functions
// Convert JSON value (Boolean, Number, String, Array, Object, null)
// into an HTML fragment
function value(v, indent, nl) {
var output;
switch (typeof v) {
case 'boolean':
output = span(html(v), 'bool');
break;
case 'number':
output = span(html(v), 'num');
break;
case 'string':
if (/^(\w+):\/\/[^\s]+$/i.test(v)) {
output = '"<a href=\''+ html(v, !!'attribute') +'\'>' +
js(v, 1) +
'</a>"';
} else {
output = span(js(v), 'str');
}
break;
case 'object':
if (null === v) {
output = span('null', 'null');
} else {
indent = indent == null ? '' : indent +' ';
if (isArray(v)) {
output = array(v, indent, nl);
} else {
output = object(v, indent, nl);
}
}
break;
}
return output;
}
// Convert an Object to an HTML fragment
function object(obj, indent, nl) {
var output = '';
for (var key in obj) {
if (output) output += BR + indent +', ';
output += span(js(key), 'prop') +': ' +
value(obj[key], indent, BR);
}
if (!output) return '{}';
return '<span class=\'unfolded obj\'><span class=content>' +
(nl ? nl + indent : '') + '{ '+ output + BR + indent + '}' +
'</span></span>';
}
// Convert an Array into an HTML fragment
function array(a, indent, nl) {
for (var i = 0, output = ''; i < a.length; i++) {
if (output) output += BR + indent +', ';
output += value(a[i], indent, '');
}
if (!output) return '[]';
return '<span class=\'unfolded arr\'><span class=content>' +
(nl ? nl + indent : '') +'[ '+ output + BR +
indent +']</span></span>';
}
// Takes a string of JSON and returns a string of HTML.
// Be sure to call JSONFormatter.init(document) once, too (for styling / UX).
function JSONFormatter(s) {
return detectJSONP(s);
}
// Pass the document that you render the HTML into, to set up css and events.
JSONFormatter.init = function init(doc, css) {
doc = doc || document;
var head = doc.getElementsByTagName('head')[0] || doc.documentElement
, node = doc.getElementById('json-format') || doc.createElement('style');
if (node.id) return; else node.id = 'json-format';
node.textContent = css || js_css;
head.appendChild(node);
doc.addEventListener('click', function folding(e) {
var elem = e.target, is, is_json = elem;
while (is_json && is_json.className != 'json')
is_json = is_json.parentNode;
if (!is_json) return; // only do folding/unfolding on json nodes
do {
if (/^a$/i.test(elem.nodeName)) return;
is = elem.className || '';
} while (!/\b(un)?folded /.test(is) && (elem = elem.parentNode));
if (elem) {
elem.className = /unfolded /.test(is)
? is.replace('unfolded ', 'folded ')
: is.replace('folded ', 'unfolded ');
}
}, false);
};
return JSONFormatter;
})();
function mode_switch() {
$o_ln.toggle(); $ln.toggle();
$o_js.toggle(); $json.toggle();
}
function mode_pick(to) {
var json = 'orig' === to ? 'hide' : 'show'
, orig = 'orig' === to ? 'show' : 'hide';
return function(e) {
$json[json](); $ln[json]();
$o_js[orig](); $o_ln[orig]();
e.preventDefault();
};
}
function init() {
$o_ln = $('#files .file .data .line_numbers');
$o_js = $('#files .file .data .highlight pre');
var el_ln = $o_ln.get(0).cloneNode(false)
, el_js = $o_js.get(0).cloneNode(false)
, json = $o_js.text().replace(/\u00A0+/g,'')
, html;
if (1 === $o_js.length) try {
html = JSONFormatter(json);
$ln = $(el_ln).hide(); $o_ln.before($ln);
$json = $(el_js).hide(); $o_js.before($json);
$json.css('padding-left', '1em'); // this looks much nicer
$json.closest('td').css('vertical-align', 'top'); // ditto – not "middle"
$json.html(html);
for (var ln = '', lines = 1+$json.find('br').length, n = 1; n <= lines; n++)
ln += '<span id="L'+ n +'" rel="#L'+ n +'">'+ n +'</span>\n';
$ln.html(ln);
JSONFormatter.init(document);
mode_switch();
var $actions = $('#files .file .meta .actions');
$actions.prepend('<li><a id="orig" href="#orig">source</a></li>');
$actions.prepend('<li><a id="json" href="#json">json</a></li>');
$actions.find('#orig').click(mode_pick('orig'));
$actions.find('#json').click(mode_pick('json'));
} catch(e) { console.error(e); }
}
// This block of code injects our source in the content scope and then calls the
// passed callback there. The whole script runs in both GM and page content, but
// since we have no other code that does anything, the Greasemonkey sandbox does
// nothing at all when it has spawned the page script, which gets to use jQuery.
// (jQuery unfortunately degrades much when run in Mozilla's javascript sandbox)
if ('object' === typeof opera && opera.extension) {
this.__proto__ = window; // bleed the web page's js into our execution scope
document.addEventListener('DOMContentLoaded', init, false); // GM-style init
} else (function(run_me_in_page_scope) { // for Chrome or Firefox+Greasemonkey
if ('undefined' == typeof __RUNS_IN_PAGE_SCOPE__) { // unsandbox, please!
var src = arguments.callee.caller.toString(),
script = document.createElement('script');
script.setAttribute("type", "application/javascript");
script.innerHTML = "const __RUNS_IN_PAGE_SCOPE__ = true;\n("+ src +')();';
document.documentElement.appendChild(script);
document.documentElement.removeChild(script);
} else { // unsandboxed -- here we go!
run_me_in_page_scope();
}
})(init);