JSON formatter

Format JSON data in a beautiful way.

目前为 2020-11-24 提交的版本,查看 最新版本

  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 2.0.8
  9. // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@1
  10. // @match *://*/*
  11. // @match file:///*
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_addStyle
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_setClipboard
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. var css_248z = "*{margin:0;padding:0}body,html{font-family:Menlo,Microsoft YaHei,Tahoma}#json-formatter{position:relative;margin:0;padding:2em 1em 1em 2em;font-size:14px;line-height:1.5}#json-formatter>pre{white-space:pre-wrap}#json-formatter>pre:not(.show-commas) .comma,#json-formatter>pre:not(.show-quotes) .quote{display:none}.subtle{color:#999}.number{color:#ff8c00}.null{color:grey}.key{color:brown}.string{color:green}.boolean{color:#1e90ff}.bracket{color:#00f}.color{display:inline-block;width:.8em;height:.8em;margin:0 .2em;border:1px solid #666;vertical-align:-.1em}.item{cursor:pointer}.content{padding-left:2em}.collapse>span>.content{display:inline;padding-left:0}.collapse>span>.content>*{display:none}.collapse>span>.content:before{content:\"...\"}.complex{position:relative}.complex:before{content:\"\";position:absolute;width:.3em;top:1.5em;left:-.5em;bottom:.7em;margin-left:-1px;border-left:1px dashed #999;border-bottom:1px dashed #999}.complex.collapse:before{display:none}.folder{color:#999;position:absolute;top:0;left:-1em;width:1em;text-align:center;transform:rotate(90deg);transition:transform .3s;cursor:pointer}.collapse>.folder{transform:rotate(0)}.summary{color:#999;margin-left:1em}:not(.collapse)>.summary{display:none}.tips{position:absolute;padding:.5em;border-radius:.5em;box-shadow:0 0 1em grey;background:#fff;z-index:1;white-space:nowrap;color:#000}.tips-key{font-weight:700}.tips-val{color:#1e90ff}.tips-link{color:#6a5acd}.menu{position:fixed;top:0;right:0;background:#fff;padding:5px;user-select:none;z-index:10}.menu>span{display:inline-block;padding:4px 8px;margin-right:5px;border-radius:4px;background:#ddd;border:1px solid #ddd;cursor:pointer}.menu>span.toggle:not(.active){background:none}";
  23.  
  24. const React = VM;
  25. const gap = 5;
  26. const formatter = {
  27. options: [{
  28. key: 'show-quotes',
  29. title: '"',
  30. def: true
  31. }, {
  32. key: 'show-commas',
  33. title: ',',
  34. def: true
  35. }]
  36. };
  37. const config = { ...formatter.options.reduce((res, item) => {
  38. res[item.key] = item.def;
  39. return res;
  40. }, {}),
  41. ...GM_getValue('config')
  42. };
  43. if (testRules([// text/javascript - file:///foo/bar.js
  44. /^(?:text|application)\/(?:.*?\+)?(?:plain|json|javascript)$/], document.contentType)) formatJSON();
  45. GM_registerMenuCommand('Toggle JSON format', formatJSON);
  46.  
  47. function testRules(rules, contentType) {
  48. for (const rule of rules) {
  49. if (typeof rule === 'string') {
  50. if (rule === contentType) return true;
  51. } else if (typeof rule?.test === 'function') {
  52. if (rule.test(contentType)) return true;
  53. }
  54. }
  55.  
  56. return false;
  57. }
  58.  
  59. function createQuote() {
  60. return /*#__PURE__*/React.createElement("span", {
  61. className: "subtle quote"
  62. }, "\"");
  63. }
  64.  
  65. function createComma() {
  66. return /*#__PURE__*/React.createElement("span", {
  67. className: "subtle comma"
  68. }, ",");
  69. }
  70.  
  71. function isColor(str) {
  72. return /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(str);
  73. }
  74.  
  75. function tokenize(raw) {
  76. const skipWhitespace = index => {
  77. while (index < raw.length && ' \t\r\n'.includes(raw[index])) index += 1;
  78.  
  79. return index;
  80. };
  81.  
  82. const expectIndex = index => {
  83. if (index < raw.length) return index;
  84. throw new Error('Unexpected end of input');
  85. };
  86.  
  87. const expectChar = (index, white, black) => {
  88. const ch = raw[index];
  89.  
  90. if (white && !white.includes(ch) || black && black.includes(ch)) {
  91. throw new Error(`Unexpected token "${ch}" at ${index}`);
  92. }
  93.  
  94. return ch;
  95. };
  96.  
  97. const findWord = (index, words) => {
  98. for (const word of words) {
  99. if (raw.slice(index, index + word.length) === word) {
  100. return word;
  101. }
  102. }
  103. };
  104.  
  105. const expectSpaceAndCharIndex = (index, white, black) => {
  106. const i = expectIndex(skipWhitespace(index));
  107. expectChar(i, white, black);
  108. return i;
  109. };
  110.  
  111. const parseString = start => {
  112. let j;
  113.  
  114. for (j = start + 1; true; j = expectIndex(j + 1)) {
  115. const ch = raw[j];
  116. if (ch === '"') break;
  117.  
  118. if (ch === '\\') {
  119. j = expectIndex(j + 1);
  120. const ch2 = raw[j];
  121.  
  122. if (ch2 === 'x') {
  123. j = expectIndex(j + 2);
  124. } else if (ch2 === 'u') {
  125. j = expectIndex(j + 4);
  126. }
  127. }
  128. }
  129.  
  130. const source = raw.slice(start + 1, j);
  131. return {
  132. type: 'string',
  133. source,
  134. data: source,
  135. color: isColor(source),
  136. start,
  137. end: j + 1
  138. };
  139. };
  140.  
  141. const parseKeyword = start => {
  142. const nullWord = findWord(start, ['null']);
  143.  
  144. if (nullWord) {
  145. return {
  146. type: 'null',
  147. source: 'null',
  148. data: null,
  149. start,
  150. end: start + 4
  151. };
  152. }
  153.  
  154. const bool = findWord(start, ['true', 'false']);
  155.  
  156. if (bool) {
  157. return {
  158. type: 'boolean',
  159. source: bool,
  160. data: bool === 'true',
  161. start,
  162. end: start + bool.length
  163. };
  164. }
  165.  
  166. expectChar(start, '0');
  167. };
  168.  
  169. const DIGITS = '0123456789';
  170.  
  171. const findDecimal = (start, fractional) => {
  172. let i = start;
  173. if ('+-'.includes(raw[i])) i += 1;
  174. let j;
  175. let dot = -1;
  176.  
  177. for (j = i; true; j = expectIndex(j + 1)) {
  178. const ch = expectChar(j, // there must be at least one digit
  179. // dot must not be the last character of a number, expecting a digit
  180. j === i || dot >= 0 && dot === j - 1 ? DIGITS : null, // there can be at most one dot
  181. !fractional || dot >= 0 ? '.' : null);
  182. if (ch === '.') dot = j;else if (!DIGITS.includes(ch)) break;
  183. }
  184.  
  185. return j;
  186. };
  187.  
  188. const parseNumber = start => {
  189. let i = findDecimal(start, true);
  190. const ch = raw[i];
  191.  
  192. if (ch && ch.toLowerCase() === 'e') {
  193. i = findDecimal(i + 1);
  194. }
  195.  
  196. const source = raw.slice(start, i);
  197. return {
  198. type: 'number',
  199. source,
  200. data: +source,
  201. start,
  202. end: i
  203. };
  204. };
  205.  
  206. let parseItem;
  207.  
  208. const parseArray = start => {
  209. const result = {
  210. type: 'array',
  211. data: [],
  212. start
  213. };
  214. let i = start + 1;
  215.  
  216. while (true) {
  217. i = expectIndex(skipWhitespace(i));
  218. if (raw[i] === ']') break;
  219. if (result.data.length) i = expectSpaceAndCharIndex(i, ',') + 1;
  220. const item = parseItem(i);
  221. result.data.push(item);
  222. i = item.end;
  223. }
  224.  
  225. result.end = i + 1;
  226. return result;
  227. };
  228.  
  229. const parseObject = start => {
  230. const result = {
  231. type: 'object',
  232. data: [],
  233. start
  234. };
  235. let i = start + 1;
  236.  
  237. while (true) {
  238. i = expectIndex(skipWhitespace(i));
  239. if (raw[i] === '}') break;
  240. if (result.data.length) i = expectSpaceAndCharIndex(i, ',') + 1;
  241. i = expectSpaceAndCharIndex(i, '"');
  242. const key = parseString(i);
  243. i = expectSpaceAndCharIndex(key.end, ':') + 1;
  244. const value = parseItem(i);
  245. result.data.push({
  246. key,
  247. value
  248. });
  249. i = value.end;
  250. }
  251.  
  252. result.end = i + 1;
  253. return result;
  254. };
  255.  
  256. parseItem = start => {
  257. const i = expectIndex(skipWhitespace(start));
  258. const ch = raw[i];
  259. if (ch === '"') return parseString(i);
  260. if (ch === '[') return parseArray(i);
  261. if (ch === '{') return parseObject(i);
  262. if ('-0123456789'.includes(ch)) return parseNumber(i);
  263. return parseKeyword(i);
  264. };
  265.  
  266. const result = parseItem(0);
  267. const end = skipWhitespace(result.end);
  268. if (end < raw.length) expectChar(end, []);
  269. return result;
  270. }
  271.  
  272. function loadJSON() {
  273. const raw = document.body.innerText;
  274.  
  275. try {
  276. // JSON
  277. const content = tokenize(raw);
  278. return {
  279. raw,
  280. content
  281. };
  282. } catch (e) {
  283. // not JSON
  284. console.error('Not JSON', e);
  285. }
  286.  
  287. try {
  288. // JSONP
  289. const parts = raw.match(/^(.*?\w\s*\()(.+)(\)[;\s]*)$/);
  290. const content = tokenize(parts[2]);
  291. return {
  292. raw,
  293. content,
  294. prefix: /*#__PURE__*/React.createElement("span", {
  295. className: "subtle"
  296. }, parts[1].trim()),
  297. suffix: /*#__PURE__*/React.createElement("span", {
  298. className: "subtle"
  299. }, parts[3].trim())
  300. };
  301. } catch (e) {
  302. // not JSONP
  303. console.error('Not JSONP', e);
  304. }
  305. }
  306.  
  307. function formatJSON() {
  308. if (formatter.formatted) return;
  309. formatter.formatted = true;
  310. formatter.data = loadJSON();
  311. if (!formatter.data) return;
  312. formatter.style = GM_addStyle(css_248z);
  313. formatter.root = /*#__PURE__*/React.createElement("div", {
  314. id: "json-formatter"
  315. });
  316. document.body.innerHTML = '';
  317. document.body.append(formatter.root);
  318. initTips();
  319. initMenu();
  320. bindEvents();
  321. generateNodes(formatter.data, formatter.root);
  322. }
  323.  
  324. function generateNodes(data, container) {
  325. const rootSpan = /*#__PURE__*/React.createElement("span", null);
  326. const root = /*#__PURE__*/React.createElement("div", null, rootSpan);
  327. const pre = /*#__PURE__*/React.createElement("pre", null, root);
  328. formatter.pre = pre;
  329. const queue = [{
  330. el: rootSpan,
  331. elBlock: root,
  332. ...data
  333. }];
  334.  
  335. while (queue.length) {
  336. const item = queue.shift();
  337. const {
  338. el,
  339. content,
  340. prefix,
  341. suffix
  342. } = item;
  343. if (prefix) el.append(prefix);
  344.  
  345. if (content.type === 'array') {
  346. queue.push(...generateArray(item));
  347. } else if (content.type === 'object') {
  348. queue.push(...generateObject(item));
  349. } else {
  350. const {
  351. type,
  352. color
  353. } = content;
  354. if (type === 'string') el.append(createQuote());
  355. if (color) el.append( /*#__PURE__*/React.createElement("span", {
  356. className: "color",
  357. style: `background-color: ${content.data}`
  358. }));
  359. el.append( /*#__PURE__*/React.createElement("span", {
  360. className: `${type} item`,
  361. "data-type": type,
  362. "data-value": toString(content)
  363. }, toString(content)));
  364. if (type === 'string') el.append(createQuote());
  365. }
  366.  
  367. if (suffix) el.append(suffix);
  368. }
  369.  
  370. container.append(pre);
  371. updateView();
  372. }
  373.  
  374. function toString(content) {
  375. return `${content.source}`;
  376. }
  377.  
  378. function setFolder(el, length) {
  379. if (length) {
  380. el.classList.add('complex');
  381. el.append( /*#__PURE__*/React.createElement("div", {
  382. className: "folder"
  383. }, '\u25b8'), /*#__PURE__*/React.createElement("span", {
  384. className: "summary"
  385. }, `// ${length} items`));
  386. }
  387. }
  388.  
  389. function generateArray({
  390. el,
  391. elBlock,
  392. content
  393. }) {
  394. const elContent = content.data.length && /*#__PURE__*/React.createElement("div", {
  395. className: "content"
  396. });
  397. setFolder(elBlock, content.data.length);
  398. el.append( /*#__PURE__*/React.createElement("span", {
  399. className: "bracket"
  400. }, "["), elContent || ' ', /*#__PURE__*/React.createElement("span", {
  401. className: "bracket"
  402. }, "]"));
  403. return content.data.map((item, i) => {
  404. const elValue = /*#__PURE__*/React.createElement("span", null);
  405. const elChild = /*#__PURE__*/React.createElement("div", null, elValue);
  406. elContent.append(elChild);
  407. if (i < content.data.length - 1) elChild.append(createComma());
  408. return {
  409. el: elValue,
  410. elBlock: elChild,
  411. content: item
  412. };
  413. });
  414. }
  415.  
  416. function generateObject({
  417. el,
  418. elBlock,
  419. content
  420. }) {
  421. const elContent = content.data.length && /*#__PURE__*/React.createElement("div", {
  422. className: "content"
  423. });
  424. setFolder(elBlock, content.data.length);
  425. el.append( /*#__PURE__*/React.createElement("span", {
  426. className: "bracket"
  427. }, '{'), elContent || ' ', /*#__PURE__*/React.createElement("span", {
  428. className: "bracket"
  429. }, '}'));
  430. return content.data.map(({
  431. key,
  432. value
  433. }, i) => {
  434. const elValue = /*#__PURE__*/React.createElement("span", null);
  435. const elChild = /*#__PURE__*/React.createElement("div", null, createQuote(), /*#__PURE__*/React.createElement("span", {
  436. className: "key item",
  437. "data-type": key.type
  438. }, key.data), createQuote(), ': ', elValue);
  439. if (i < content.data.length - 1) elChild.append(createComma());
  440. elContent.append(elChild);
  441. return {
  442. el: elValue,
  443. content: value,
  444. elBlock: elChild
  445. };
  446. });
  447. }
  448.  
  449. function updateView() {
  450. formatter.options.forEach(({
  451. key
  452. }) => {
  453. formatter.pre.classList[config[key] ? 'add' : 'remove'](key);
  454. });
  455. }
  456.  
  457. function removeEl(el) {
  458. el.remove();
  459. }
  460.  
  461. function initMenu() {
  462. const handleCopy = () => {
  463. GM_setClipboard(formatter.data.raw);
  464. };
  465.  
  466. const handleMenuClick = e => {
  467. const el = e.target;
  468. const {
  469. key
  470. } = el.dataset;
  471.  
  472. if (key) {
  473. config[key] = !config[key];
  474. GM_setValue('config', config);
  475. el.classList.toggle('active');
  476. updateView();
  477. }
  478. };
  479.  
  480. formatter.root.append( /*#__PURE__*/React.createElement("div", {
  481. className: "menu",
  482. onClick: handleMenuClick
  483. }, /*#__PURE__*/React.createElement("span", {
  484. onClick: handleCopy
  485. }, "Copy"), formatter.options.map(item => /*#__PURE__*/React.createElement("span", {
  486. className: `toggle${config[item.key] ? ' active' : ''}`,
  487. dangerouslySetInnerHTML: {
  488. __html: item.title
  489. },
  490. "data-key": item.key
  491. }))));
  492. }
  493.  
  494. function initTips() {
  495. const tips = /*#__PURE__*/React.createElement("div", {
  496. className: "tips",
  497. onClick: e => {
  498. e.stopPropagation();
  499. }
  500. });
  501.  
  502. const hide = () => removeEl(tips);
  503.  
  504. document.addEventListener('click', hide, false);
  505. formatter.tips = {
  506. node: tips,
  507. hide,
  508.  
  509. show(range) {
  510. const {
  511. scrollTop
  512. } = document.body;
  513. const rects = range.getClientRects();
  514. let rect;
  515.  
  516. if (rects[0].top < 100) {
  517. rect = rects[rects.length - 1];
  518. tips.style.top = `${rect.bottom + scrollTop + gap}px`;
  519. tips.style.bottom = '';
  520. } else {
  521. [rect] = rects;
  522. tips.style.top = '';
  523. tips.style.bottom = `${formatter.root.offsetHeight - rect.top - scrollTop + gap}px`;
  524. }
  525.  
  526. tips.style.left = `${rect.left}px`;
  527. const {
  528. type,
  529. value
  530. } = range.startContainer.dataset;
  531. tips.innerHTML = '';
  532. tips.append( /*#__PURE__*/React.createElement("span", {
  533. className: "tips-key"
  534. }, "type"), ': ', /*#__PURE__*/React.createElement("span", {
  535. className: "tips-val",
  536. dangerouslySetInnerHTML: {
  537. __html: type
  538. }
  539. }));
  540.  
  541. if (type === 'string' && /^(https?|ftps?):\/\/\S+/.test(value)) {
  542. tips.append( /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("a", {
  543. className: "tips-link",
  544. href: value,
  545. target: "_blank",
  546. rel: "noopener noreferrer"
  547. }, "Open link"));
  548. }
  549.  
  550. formatter.root.append(tips);
  551. }
  552.  
  553. };
  554. }
  555.  
  556. function selectNode(node) {
  557. const selection = window.getSelection();
  558. selection.removeAllRanges();
  559. const range = document.createRange();
  560. range.setStartBefore(node.firstChild);
  561. range.setEndAfter(node.firstChild);
  562. selection.addRange(range);
  563. return range;
  564. }
  565.  
  566. function bindEvents() {
  567. formatter.root.addEventListener('click', e => {
  568. e.stopPropagation();
  569. const {
  570. target
  571. } = e;
  572.  
  573. if (target.classList.contains('item')) {
  574. formatter.tips.show(selectNode(target));
  575. } else {
  576. formatter.tips.hide();
  577. }
  578.  
  579. if (target.classList.contains('folder')) {
  580. target.parentNode.classList.toggle('collapse');
  581. }
  582. }, false);
  583. }
  584.  
  585. }());