JSON formatter

Format JSON data in a beautiful way.

当前为 2017-11-13 提交的版本,查看 最新版本

  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.5.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. className: 'space',
  46. textContent: ' '.repeat(n)
  47. });
  48. };
  49. var createIndent = function createIndent() {
  50. var n = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
  51. return createSpace(2 * n);
  52. };
  53. var createBr = function createBr() {
  54. return createElement('br');
  55. };
  56.  
  57. var formatter = {
  58. options: [{
  59. key: 'hide-quotes',
  60. title: '"',
  61. def: false
  62. }, {
  63. key: 'hide-commas',
  64. title: ',',
  65. def: false
  66. }]
  67. };
  68.  
  69. var config = GM_getValue('config', formatter.options.reduce(function (res, item) {
  70. res[item.key] = item.def;
  71. return res;
  72. }, {}));
  73.  
  74. if (['application/json', 'text/plain', 'application/javascript', 'text/javascript'].includes(document.contentType)) formatJSON();
  75. GM_registerMenuCommand('Toggle JSON format', formatJSON);
  76.  
  77. function safeHTML(html) {
  78. return String(html).replace(/[<&"]/g, function (key) {
  79. return {
  80. '<': '&lt;',
  81. '&': '&amp;',
  82. '"': '&quot;'
  83. }[key];
  84. });
  85. }
  86.  
  87. function createElement(tag, props) {
  88. var el = document.createElement(tag);
  89. if (props) {
  90. Object.keys(props).forEach(function (key) {
  91. el[key] = props[key];
  92. });
  93. }
  94. return el;
  95. }
  96.  
  97. function join(rendered) {
  98. var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
  99.  
  100. var arr = [];
  101. for (var i = 0; i < rendered.length; i += 1) {
  102. var item = rendered[i];
  103. var next = rendered[i + 1];
  104. if (item.data) arr.push.apply(arr, _toConsumableArray(item.data));
  105. if (next) {
  106. if (item.separator) arr.push.apply(arr, _toConsumableArray(item.separator));
  107. if (next.type === KEY || item.type !== KEY && (item.type === SINGLELINE || next.type === SINGLELINE)) {
  108. arr.push(createBr(), createIndent(level));
  109. } else {
  110. arr.push(createSpace(1));
  111. }
  112. }
  113. }
  114. return arr;
  115. }
  116.  
  117. function createNodes(data) {
  118. var valueType = typeof data.value;
  119. var type = data.type || valueType;
  120. var el = createElement('span', {
  121. className: data.cls || `item ${type}`,
  122. textContent: `${data.value}`
  123. });
  124. el.dataset.type = valueType;
  125. el.dataset.value = data.value;
  126. var els = [el];
  127. if (data.type === 'key' || !data.cls && type === 'string') {
  128. els.unshift(createQuote());
  129. els.push(createQuote());
  130. }
  131. return els;
  132. }
  133.  
  134. function render(data) {
  135. var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
  136.  
  137. if (Array.isArray(data)) {
  138. var arr = [];
  139. var ret = {
  140. type: MULTILINE,
  141. separator: [createComma()]
  142. };
  143. arr.push.apply(arr, _toConsumableArray(createNodes({ value: '[', cls: 'bracket' })));
  144. if (data.length) {
  145. var rendered = data.reduce(function (res, item) {
  146. return [].concat(_toConsumableArray(res), [render(item, level + 1)]);
  147. }, []);
  148. arr.push.apply(arr, [createBr(), createIndent(level + 1)].concat(_toConsumableArray(join(rendered, level + 1)), [createBr(), createIndent(level)]));
  149. } else {
  150. arr.push.apply(arr, _toConsumableArray(createNodes({ value: '', cls: 'subtle' })));
  151. ret.type = SINGLELINE;
  152. }
  153. arr.push.apply(arr, _toConsumableArray(createNodes({ value: ']', cls: 'bracket' })));
  154. ret.data = arr;
  155. return ret;
  156. }
  157. if (data === null) {
  158. return {
  159. type: SINGLELINE,
  160. separator: [createComma()],
  161. data: createNodes({ value: data, type: 'null' })
  162. };
  163. }
  164. if (typeof data === 'object') {
  165. var _arr = [];
  166. var _ret = {
  167. type: MULTILINE,
  168. separator: [createComma()]
  169. };
  170. _arr.push.apply(_arr, _toConsumableArray(createNodes({ value: '{', cls: 'bracket' })));
  171. var _rendered = Object.keys(data).reduce(function (res, key) {
  172. return res.concat([{
  173. type: KEY,
  174. data: createNodes({ value: key, type: 'key' }),
  175. separator: createNodes({ value: ':', cls: 'subtle' })
  176. }, render(data[key], level + 1)]);
  177. }, []);
  178. if (_rendered.length) {
  179. _arr.push.apply(_arr, [createBr(), createIndent(level + 1)].concat(_toConsumableArray(join(_rendered, level + 1)), [createBr(), createIndent(level)]));
  180. } else {
  181. _arr.push.apply(_arr, _toConsumableArray(createNodes({ value: '', cls: 'subtle' })));
  182. _ret.type = SINGLELINE;
  183. }
  184. _arr.push.apply(_arr, _toConsumableArray(createNodes({ value: '}', cls: 'bracket' })));
  185. _ret.data = _arr;
  186. return _ret;
  187. }
  188. return {
  189. type: SINGLELINE,
  190. separator: [createComma()],
  191. data: createNodes({ value: data })
  192. };
  193. }
  194.  
  195. function loadJSON() {
  196. var text = document.body.innerText;
  197. try {
  198. // JSON
  199. var content = JSON.parse(text);
  200. return { prefix: '', suffix: '', content };
  201. } catch (e) {
  202. // not JSON
  203. }
  204. try {
  205. // JSONP
  206. var parts = text.match(/^(.*?\w\s*\()(.+)(\)[;\s]*)$/);
  207. var _content = JSON.parse(parts[2]);
  208. var prefix = parts[1];
  209. var suffix = parts[3];
  210. return { prefix, content: _content, suffix };
  211. } catch (e) {
  212. // not JSONP
  213. }
  214. }
  215.  
  216. function formatJSON() {
  217. if (formatter.formatted) {
  218. formatter.tips.hide();
  219. formatter.menu.detach();
  220. document.body.innerHTML = formatter.raw;
  221. formatter.formatted = false;
  222. } else {
  223. if (!('raw' in formatter)) {
  224. formatter.raw = document.body.innerHTML;
  225. formatter.data = loadJSON();
  226. if (!formatter.data) return;
  227. // formatter.style = GM_addStyle(".tips-link {\n color: slateblue;\n}.tips-val {\n color: dodgerblue;\n}* {\n margin: 0;\n padding: 0;\n}\n\n#root {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n margin: 0;\n padding: 16px;\n font-family: Menlo, \"Microsoft YaHei\", Tahoma;\n font-size: 14px;\n overflow: auto;\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\n.space {\n letter-spacing: 8px;\n}\n");
  228. initTips();
  229. initMenu();
  230. formatter.render = function () {
  231. var pre = formatter.pre;
  232. var _formatter$data = formatter.data,
  233. prefix = _formatter$data.prefix,
  234. content = _formatter$data.content,
  235. suffix = _formatter$data.suffix;
  236.  
  237. pre.innerHTML = '';
  238. [createElement('span', {
  239. className: 'subtle',
  240. textContent: prefix
  241. })].concat(_toConsumableArray(render(content).data), [createElement('span', {
  242. className: 'subtle',
  243. textContent: suffix
  244. })]).forEach(function (el) {
  245. pre.appendChild(el);
  246. });
  247. formatter.update();
  248. };
  249. formatter.update = function () {
  250. formatter.options.forEach(function (_ref) {
  251. var key = _ref.key;
  252.  
  253. formatter.pre.classList[config[key] ? 'add' : 'remove'](key);
  254. });
  255. };
  256. }
  257. formatter.formatted = true;
  258. var hostRoot = createElement('div');
  259. document.body.innerHTML = '';
  260. document.body.appendChild(hostRoot);
  261. var shadow = hostRoot.attachShadow({ mode: 'open' });
  262. formatter.style = createElement('style', {
  263. textContent: ".tips-link {\n color: slateblue;\n}.tips-val {\n color: dodgerblue;\n}* {\n margin: 0;\n padding: 0;\n}\n\n#root {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n margin: 0;\n padding: 16px;\n font-family: Menlo, \"Microsoft YaHei\", Tahoma;\n font-size: 14px;\n overflow: auto;\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\n.space {\n letter-spacing: 8px;\n}\n"
  264. });
  265. shadow.appendChild(formatter.style);
  266. formatter.root = createElement('div', { id: 'root' });
  267. shadow.appendChild(formatter.root);
  268. formatter.pre = createElement('pre');
  269. formatter.root.appendChild(formatter.pre);
  270. formatter.menu.attach();
  271. bindEvents();
  272. formatter.render();
  273. }
  274. }
  275.  
  276. function removeEl(el) {
  277. if (el && el.parentNode) el.parentNode.removeChild(el);
  278. }
  279.  
  280. function initMenu() {
  281. var menu = createElement('div', {
  282. className: 'menu'
  283. });
  284. formatter.options.forEach(function (item) {
  285. var span = createElement('span', {
  286. className: `btn${config[item.key] ? ' active' : ''}`,
  287. innerHTML: item.title
  288. });
  289. span.dataset.key = item.key;
  290. menu.appendChild(span);
  291. });
  292. menu.addEventListener('click', function (e) {
  293. var el = e.target;
  294. var key = el.dataset.key;
  295. if (key) {
  296. config[key] = !config[key];
  297. GM_setValue('config', config);
  298. el.classList.toggle('active');
  299. formatter.update();
  300. }
  301. }, false);
  302. formatter.menu = {
  303. node: menu,
  304. attach() {
  305. formatter.root.appendChild(menu);
  306. },
  307. detach() {
  308. removeEl(menu);
  309. }
  310. };
  311. }
  312.  
  313. function initTips() {
  314. var tips = createElement('div', {
  315. className: 'tips'
  316. });
  317. var hide = function hide() {
  318. return removeEl(tips);
  319. };
  320. tips.addEventListener('click', function (e) {
  321. e.stopPropagation();
  322. }, false);
  323. document.addEventListener('click', hide, false);
  324. formatter.tips = {
  325. node: tips,
  326. hide,
  327. show(range) {
  328. var scrollTop = document.body.scrollTop;
  329. var rects = range.getClientRects();
  330. var rect = void 0;
  331. if (rects[0].top < 100) {
  332. rect = rects[rects.length - 1];
  333. tips.style.top = `${rect.bottom + scrollTop + gap}px`;
  334. tips.style.bottom = '';
  335. } else {
  336. rect = rects[0];
  337. tips.style.top = '';
  338. tips.style.bottom = `${formatter.root.offsetHeight - rect.top - scrollTop + gap}px`;
  339. }
  340. tips.style.left = `${rect.left}px`;
  341. var _range$startContainer = range.startContainer.dataset,
  342. type = _range$startContainer.type,
  343. value = _range$startContainer.value;
  344.  
  345. var html = [`<span class="tips-key">type</span>: <span class="tips-val">${safeHTML(type)}</span>`];
  346. if (type === 'string' && /^(https?|ftps?):\/\/\S+/.test(value)) {
  347. html.push('<br>', `<a class="tips-link" href="${encodeURI(value)}" target="_blank">Open link</a>`);
  348. }
  349. tips.innerHTML = html.join('');
  350. formatter.root.appendChild(tips);
  351. }
  352. };
  353. }
  354.  
  355. function selectNode(node) {
  356. var selection = window.getSelection();
  357. selection.removeAllRanges();
  358. var range = document.createRange();
  359. range.setStartBefore(node.firstChild);
  360. range.setEndAfter(node.firstChild);
  361. selection.addRange(range);
  362. return range;
  363. }
  364.  
  365. function bindEvents() {
  366. formatter.root.addEventListener('click', function (e) {
  367. e.stopPropagation();
  368. var target = e.target;
  369.  
  370. if (target.classList.contains('item')) {
  371. formatter.tips.show(selectNode(target));
  372. } else {
  373. formatter.tips.hide();
  374. }
  375. }, false);
  376. }