JSON formatter

Format JSON data in a beautiful way.

当前为 2016-05-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name JSON formatter
  3. // @namespace http://gerald.top
  4. // @author Gerald <i@gerald.top>
  5. // @icon http://cn.gravatar.com/avatar/a0ad718d86d21262ccd6ff271ece08a3?s=80
  6. // @description Format JSON data in a beautiful way.
  7. // @description:zh-CN 更加漂亮地显示JSON数据。
  8. // @version 1.3
  9. // @match *://*/*
  10. // @match file:///*
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_addStyle
  14. // @grant GM_registerMenuCommand
  15. // ==/UserScript==
  16.  
  17. function safeHTML(html) {
  18. return String(html).replace(/[<&"]/g, function (key) {
  19. return {
  20. '<': '&lt;',
  21. '&': '&amp;',
  22. '"': '&quot;',
  23. }[key];
  24. });
  25. }
  26.  
  27. function getSpace(level) {
  28. if (!level) return;
  29. var span = document.createElement('span');
  30. span.className = 'space';
  31. span.innerHTML = '&nbsp;'.repeat(2 * level);
  32. return span;
  33. }
  34.  
  35. function initSpaces(root, level) {
  36. level = level || 0;
  37. var space = getSpace(level);
  38. space && root.insertBefore(space, root.firstChild);
  39. var children = root.children;
  40. for (var i = children.length; i --; ) {
  41. var child = children[i];
  42. if (child.tagName.toLowerCase() === 'ul') {
  43. space && root.insertBefore(space.cloneNode(true), child.nextElementSibling);
  44. [].forEach.call(child.children, function (li) {
  45. initSpaces(li, level + 1);
  46. });
  47. }
  48. }
  49. }
  50.  
  51. function join(list) {
  52. function open() {
  53. if (!isOpen) {
  54. html.push('<li>');
  55. isOpen = true;
  56. }
  57. }
  58. function close() {
  59. if (isOpen) {
  60. html.push('</li>');
  61. isOpen = false;
  62. }
  63. }
  64. var html = [];
  65. var isOpen = false;
  66. list.forEach(function (item, i) {
  67. var next = list[i + 1];
  68. open();
  69. item.data && html.push(item.data);
  70. next && item.separator && html.push(item.separator);
  71. if (
  72. !next
  73. || next.type === KEY
  74. || item.type !== KEY && (
  75. item.type === SINGLELINE || next.type === SINGLELINE
  76. )
  77. ) close();
  78. });
  79. return html.join('');
  80. }
  81.  
  82. function getHtml(data) {
  83. var type = typeof data.value;
  84. var html = '<span class="' + (data.cls || 'value ' + type) + '" ' +
  85. 'data-type="' + safeHTML(data.type || type) + '" ' +
  86. 'data-value="' + safeHTML(data.value) + '">' + safeHTML(data.value) + '</span>';
  87. if (data.cls === 'key' || !data.cls && type === 'string')
  88. html = QUOTE + html + QUOTE;
  89. return html;
  90. }
  91.  
  92. function render(data) {
  93. var ret, arr;
  94. if (Array.isArray(data)) {
  95. arr = [];
  96. ret = {
  97. type: MULTILINE,
  98. separator: COMMA,
  99. };
  100. arr.push(getHtml({value: '[', cls: 'operator'}));
  101. if (data.length) {
  102. arr.push(
  103. '<ul>',
  104. join(data.map(render)),
  105. '</ul>'
  106. );
  107. } else {
  108. arr.push(getHtml({value: '', cls: 'separator'}));
  109. ret.type = SINGLELINE;
  110. }
  111. arr.push(getHtml({value: ']', cls: 'operator'}));
  112. ret.data = arr.join('');
  113. } else if (data === null) {
  114. ret = {
  115. type: SINGLELINE,
  116. separator: COMMA,
  117. data: getHtml({value: data, cls: 'value null'}),
  118. };
  119. } else if (typeof data == 'object') {
  120. arr = [];
  121. ret = {
  122. type: MULTILINE,
  123. separator: COMMA,
  124. };
  125. arr.push(getHtml({value: '{', cls: 'operator'}));
  126. var objdata = [];
  127. for (var key in data) {
  128. objdata.push({
  129. type: KEY,
  130. data: getHtml({value: key, cls: 'key'}),
  131. separator: getHtml({value: ':', cls: 'separator'}),
  132. }, render(data[key]));
  133. }
  134. if (objdata.length) {
  135. arr.push(
  136. '<ul>',
  137. join(objdata),
  138. '</ul>'
  139. );
  140. } else {
  141. arr.push(getHtml({value: '', cls: 'separator'}));
  142. ret.type = SINGLELINE;
  143. }
  144. arr.push(getHtml({value: '}', cls: 'operator'}));
  145. ret.data = arr.join('');
  146. } else {
  147. ret = {
  148. type: SINGLELINE,
  149. separator: COMMA,
  150. data: getHtml({value: data}),
  151. };
  152. }
  153. return ret;
  154. }
  155.  
  156. function formatJSON() {
  157. if (formatter.formatted) {
  158. formatter.tips.hide();
  159. formatter.menu.detach();
  160. document.body.innerHTML = formatter.raw;
  161. formatter.formatted = false;
  162. } else {
  163. if (!('raw' in formatter)) {
  164. formatter.raw = document.body.innerHTML;
  165. formatter.data = JSON.parse(document.body.innerText);
  166. formatter.style = GM_addStyle([
  167. '*{font-family:Microsoft YaHei,Tahoma;font-size:14px;}',
  168. 'body,ul{margin:0;padding:0;}',
  169. '#root{position:relative;margin:0;padding:1rem;}',
  170. '#root>ul ul{padding-left:2rem;}',
  171. '.quote,.comma,.separator{color:#999;}',
  172. '.space,.hide-quotes .quote,.hide-separators .comma{font-size:0;}',
  173. 'li{list-style:none;}',
  174. '.comma,.separator{margin-right:.5rem;}',
  175. '.number{color:darkorange;}',
  176. '.null{color:gray;}',
  177. '.key{color:brown;}',
  178. '.string{color:green;}',
  179. '.boolean{color:dodgerblue;}',
  180. '.operator{color:blue;}',
  181. '.value{cursor:pointer;}',
  182. '.tips{position:absolute;padding:.5em;border-radius:.5em;box-shadow:0 0 1em gray;background:white;z-index:1;white-space:nowrap;color:black;}',
  183. '.tips-key{font-weight:bold;}',
  184. '.tips-val{color:dodgerblue;}',
  185. '.menu{position:fixed;top:0;right:0;background:white;padding:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}',
  186. '.menu>span{margin-right:5px;}',
  187. '.menu .btn{display:inline-block;width:18px;height:18px;line-height:18px;text-align:center;background:#ddd;border-radius:4px;cursor:pointer;}',
  188. '.menu .btn.active{color:white;background:#444;}',
  189. '.hide{display:none;}',
  190. ].join(''));
  191. initTips();
  192. initMenu();
  193. formatter.render = function () {
  194. var root = formatter.root.querySelector('li');
  195. root.innerHTML = render(formatter.data).data;
  196. formatter.update();
  197. initSpaces(root);
  198. };
  199. formatter.update = function () {
  200. var ul = formatter.root.querySelector('ul');
  201. formatter.options.forEach(function (item) {
  202. ul.classList[config[item.key] ? 'add' : 'remove'](item.key);
  203. });
  204. };
  205. }
  206. document.body.innerHTML = '<div id="root"><ul><li></li></ul></div>';
  207. formatter.formatted = true;
  208. formatter.root = document.querySelector('#root');
  209. formatter.menu.attach();
  210. bindEvents();
  211. formatter.render();
  212. }
  213. }
  214.  
  215. function removeEl(el) {
  216. el && el.parentNode && el.parentNode.removeChild(el);
  217. }
  218.  
  219. function initMenu() {
  220. var menu = document.createElement('div');
  221. menu.className = 'menu';
  222. formatter.options.forEach(function (item) {
  223. var span = document.createElement('span');
  224. span.className = 'btn';
  225. if (config[item.key]) span.className += ' active';
  226. span.dataset.key = item.key;
  227. span.innerHTML = item.title;
  228. menu.appendChild(span);
  229. });
  230. menu.addEventListener('click', function (e) {
  231. var el = e.target;
  232. var key = el.dataset.key;
  233. if (key) {
  234. config[key] = !config[key];
  235. GM_setValue('config', config);
  236. el.classList.toggle('active');
  237. formatter.update();
  238. }
  239. }, false);
  240. formatter.menu = {
  241. node: menu,
  242. attach: function () {
  243. formatter.root.appendChild(menu);
  244. },
  245. detach: function () {
  246. removeEl(menu);
  247. },
  248. };
  249. }
  250.  
  251. function initTips() {
  252. function hide() {
  253. removeEl(tips);
  254. }
  255. var tips = document.createElement('div');
  256. tips.className = 'tips';
  257. tips.addEventListener('click', function (e) {
  258. e.stopPropagation();
  259. }, false);
  260. document.addEventListener('click', hide, false);
  261. formatter.tips = {
  262. node: tips,
  263. hide: hide,
  264. show: function (range) {
  265. var gap = 5;
  266. var scrollTop = document.body.scrollTop;
  267. var rects = range.getClientRects(), rect;
  268. if (rects[0].top < 100) {
  269. rect = rects[rects.length - 1];
  270. tips.style.top = rect.bottom + scrollTop + gap + 'px';
  271. tips.style.bottom = '';
  272. } else {
  273. rect = rects[0];
  274. tips.style.top = '';
  275. tips.style.bottom = formatter.root.offsetHeight - rect.top - scrollTop + gap + 'px';
  276. }
  277. tips.style.left = rect.left + 'px';
  278. tips.innerHTML = '<span class="tips-key">type</span>: <span class="tips-val">' + safeHTML(range.startContainer.dataset.type) + '</span>';
  279. formatter.root.appendChild(tips);
  280. },
  281. };
  282. }
  283.  
  284. function selectNode(node) {
  285. var selection = window.getSelection();
  286. selection.removeAllRanges();
  287. var range = document.createRange();
  288. range.setStartBefore(node.firstChild);
  289. range.setEndAfter(node.firstChild);
  290. selection.addRange(range);
  291. return range;
  292. }
  293.  
  294. function bindEvents() {
  295. formatter.root.addEventListener('click', function (e) {
  296. e.stopPropagation();
  297. var target = e.target;
  298. if (target.classList.contains('value')) {
  299. formatter.tips.show(selectNode(target));
  300. } else {
  301. formatter.tips.hide();
  302. }
  303. }, false);
  304. }
  305.  
  306. var getId = function () {
  307. var id = 0;
  308. return function () {
  309. return ++ id;
  310. };
  311. }();
  312. var SINGLELINE = getId();
  313. var MULTILINE = getId();
  314. var KEY = getId();
  315. var QUOTE = '<span class="quote">"</span>';
  316. var COMMA = '<span class="comma">,</span>';
  317.  
  318. var formatter = {
  319. options: [{
  320. key: 'hide-quotes',
  321. title: '"',
  322. def: false,
  323. }, {
  324. key: 'hide-separators',
  325. title: ',',
  326. def: false,
  327. }],
  328. };
  329. var config = GM_getValue('config', formatter.options.reduce(function (res, item) {
  330. res[item.key] = item.def;
  331. return res;
  332. }, {}));
  333.  
  334. ~[
  335. 'application/json',
  336. 'text/plain',
  337. ].indexOf(document.contentType) && formatJSON();
  338. GM_registerMenuCommand('Toggle JSON format', formatJSON);