JSON formatter

Format JSON data in a beautiful way.

目前為 2017-06-06 提交的版本,檢視 最新版本

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