Greasy Fork 支持简体中文。

JSON formatter

Format JSON data in a beautiful way.

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