Reformats JSON(P) files in the github tree view for readability.
// ==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);