JSON formatter

Format JSON data in a beautiful way.

当前为 2015-12-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name JSON formatter
  3. // @namespace http://gerald.top
  4. // @description Format JSON data in a beautiful way.
  5. // @description:zh-CN 更加漂亮地显示JSON数据。
  6. // @version 1.1.5
  7. // @match *://*/*
  8. // @grant GM_addStyle
  9. // @grant GM_registerMenuCommand
  10. // ==/UserScript==
  11.  
  12. function safeHTML(html) {
  13. return String(html).replace(/[<&"]/g, function (key) {
  14. return {
  15. '<': '&lt;',
  16. '&': '&amp;',
  17. '"': '&quot;',
  18. }[key];
  19. });
  20. }
  21.  
  22. function join(list) {
  23. var html = [];
  24. var open = false;
  25. var last = null;
  26. var close = function () {
  27. html.push('</li>');
  28. open = false;
  29. last = null;
  30. };
  31. list.forEach(function (item) {
  32. if (open && !item.backwards)
  33. close();
  34. if (!open) {
  35. html.push('<li>');
  36. open = true;
  37. }
  38. if (item.backwards && last && last.forwards)
  39. html.push(last.separator);
  40. html.push(item.data);
  41. if (!item.forwards)
  42. close();
  43. else
  44. last = item;
  45. });
  46. if (open) html.push('</li>');
  47. return html.join('');
  48. }
  49.  
  50. function getHtml(data) {
  51. var html = '<span class="' + (data.cls || 'value ' + typeof data.value) + '" ' +
  52. 'data-type="' + safeHTML(data.type || typeof data.value) + '" ' +
  53. 'data-value="' + safeHTML(data.value) + '">' + safeHTML(data.value) + '</span>';
  54. return html;
  55. }
  56.  
  57. function render(data) {
  58. if (Array.isArray(data)) {
  59. var arr = [];
  60. var ret = {
  61. backwards: true,
  62. forwards: true,
  63. separator: getHtml({value: ',', cls: 'separator'}),
  64. };
  65. arr.push(getHtml({value: '[', cls: 'operator'}));
  66. if (data.length) {
  67. arr.push('<ul>');
  68. arr.push(join(data.map(render)));
  69. arr.push('</ul>');
  70. } else {
  71. arr.push(getHtml({value: '', cls: 'separator'}));
  72. ret.forwards = false;
  73. }
  74. arr.push(getHtml({value: ']', cls: 'operator'}));
  75. ret.data = arr.join('');
  76. return ret;
  77. } else if (data === null)
  78. return {data: getHtml({value: data, cls: 'value null'}), backwards: true};
  79. else if (typeof data == 'object') {
  80. var arr = [];
  81. var ret = {
  82. backwards: true,
  83. forwards: true,
  84. separator: getHtml({value: ',', cls: 'separator'}),
  85. };
  86. arr.push(getHtml({value: '{', cls: 'operator'}));
  87. var objdata = [];
  88. for (var key in data) {
  89. objdata.push({
  90. data: getHtml({value: key, cls: 'key'}),
  91. forwards: true,
  92. separator: getHtml({value: ':', cls: 'separator'}),
  93. });
  94. objdata.push(render(data[key]));
  95. }
  96. if (objdata.length) {
  97. arr.push('<ul>');
  98. arr.push(join(objdata));
  99. arr.push('</ul>');
  100. } else {
  101. arr.push(getHtml({value: '', cls: 'separator'}));
  102. ret.forwards = false;
  103. }
  104. arr.push(getHtml({value: '}', cls: 'operator'}));
  105. ret.data = arr.join('');
  106. return ret;
  107. } else
  108. return {
  109. backwards: true,
  110. data: getHtml({value: data}),
  111. };
  112. }
  113.  
  114. function formatJSON() {
  115. if (config.formatted) {
  116. document.body.innerHTML = config.raw;
  117. config.formatted = false;
  118. } else {
  119. if (!('raw' in config)) {
  120. config.raw = document.body.innerHTML;
  121. config.data = JSON.parse(document.body.innerText);
  122. config.style = GM_addStyle(
  123. '*{font-family:Microsoft YaHei,Tahoma;font-size:15px;}' +
  124. 'ul.root{padding-left:0;}' +
  125. 'li{list-style:none;}' +
  126. '.separator{margin-right:.5em;}' +
  127. '.number{color:darkorange;}' +
  128. '.null{color:gray;}' +
  129. '.key{color:brown;}' +
  130. '.string{color:green;}' +
  131. '.operator{color:blue;}' +
  132. '.value{position:relative;cursor:pointer;}' +
  133. '.popup{position:absolute;top:100%;margin-top:.5em;padding:.5em;border-radius:.5em;box-shadow:0 0 1em gray;background:white;z-index:1;white-space:nowrap;color:black;}' +
  134. '.info-key{font-weight:bold;}' +
  135. '.info-val{color:dodgerblue;}' +
  136. '.hide{display:none;}'
  137. );
  138. initPopup();
  139. }
  140. var ret = render(config.data);
  141. document.body.innerHTML = '<ul class="root"><li>' + ret.data + '</li></ul>';
  142. config.formatted = true;
  143. bindEvents(document.body.querySelector('.root'));
  144. }
  145. }
  146.  
  147. function initPopup() {
  148. function hide() {
  149. var parent = popup.parentNode;
  150. if (parent) parent.removeChild(popup);
  151. }
  152. var popup = document.createElement('div');
  153. popup.className = 'popup';
  154. popup.addEventListener('click', function (e) {
  155. e.stopPropagation();
  156. }, false);
  157. document.addEventListener('click', hide, false);
  158. config.popup = {
  159. node: popup,
  160. hide: hide,
  161. show: function (target) {
  162. target.appendChild(popup);
  163. popup.innerHTML = '<span class="info-key">type</span>: <span class="info-val">' + safeHTML(target.dataset.type) + '</span>';
  164. },
  165. };
  166. }
  167.  
  168. function selectNode(node) {
  169. var selection = window.getSelection();
  170. selection.removeAllRanges();
  171. var range = document.createRange();
  172. range.setStartBefore(node.firstChild);
  173. range.setEndAfter(node.firstChild);
  174. selection.addRange(range);
  175. }
  176.  
  177. function bindEvents(root) {
  178. root.addEventListener('click', function (e) {
  179. e.stopPropagation();
  180. var target = e.target;
  181. if (target.classList.contains('value')) {
  182. selectNode(target);
  183. config.popup.show(target);
  184. } else
  185. config.popup.hide();
  186. }, false);
  187. }
  188.  
  189. var config = {};
  190. if (/\/json$/.test(document.contentType))
  191. formatJSON();
  192. GM_registerMenuCommand('Toggle JSON format', formatJSON);