- // ==UserScript==
- // @name JSON formatter
- // @icon http://cn.gravatar.com/avatar/970cf1cfdd1c5bdca24e561425c57f4e?s=80
- // @description Format JSON data in a beautiful way.
- // @description:zh-CN 更加漂亮地显示JSON数据。
- // @version 2.0.7
- // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@1
- // @match *://*/*
- // @match file:///*
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_addStyle
- // @grant GM_registerMenuCommand
- // @grant GM_setClipboard
- // @namespace https://greasyfork.org/users/5185
- // ==/UserScript==
-
- (function () {
- 'use strict';
-
- 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}";
-
- const React = VM;
- const gap = 5;
- const formatter = {
- options: [{
- key: 'show-quotes',
- title: '"',
- def: true
- }, {
- key: 'show-commas',
- title: ',',
- def: true
- }]
- };
- const config = { ...formatter.options.reduce((res, item) => {
- res[item.key] = item.def;
- return res;
- }, {}),
- ...GM_getValue('config')
- };
- if (['application/json', 'text/plain', 'application/javascript', 'text/javascript' // file:///foo/bar.js
- ].includes(document.contentType)) formatJSON();
- GM_registerMenuCommand('Toggle JSON format', formatJSON);
-
- function createQuote() {
- return /*#__PURE__*/React.createElement("span", {
- className: "subtle quote"
- }, "\"");
- }
-
- function createComma() {
- return /*#__PURE__*/React.createElement("span", {
- className: "subtle comma"
- }, ",");
- }
-
- function isColor(str) {
- return /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(str);
- }
-
- function tokenize(raw) {
- const skipWhitespace = index => {
- while (index < raw.length && ' \t\r\n'.includes(raw[index])) index += 1;
-
- return index;
- };
-
- const expectIndex = index => {
- if (index < raw.length) return index;
- throw new Error('Unexpected end of input');
- };
-
- const expectChar = (index, white, black) => {
- const ch = raw[index];
-
- if (white && !white.includes(ch) || black && black.includes(ch)) {
- throw new Error(`Unexpected token "${ch}" at ${index}`);
- }
-
- return ch;
- };
-
- const findWord = (index, words) => {
- for (const word of words) {
- if (raw.slice(index, index + word.length) === word) {
- return word;
- }
- }
- };
-
- const expectSpaceAndCharIndex = (index, white, black) => {
- const i = expectIndex(skipWhitespace(index));
- expectChar(i, white, black);
- return i;
- };
-
- const parseString = start => {
- let j;
-
- for (j = start + 1; true; j = expectIndex(j + 1)) {
- const ch = raw[j];
- if (ch === '"') break;
-
- if (ch === '\\') {
- j = expectIndex(j + 1);
- const ch2 = raw[j];
-
- if (ch2 === 'x') {
- j = expectIndex(j + 2);
- } else if (ch2 === 'u') {
- j = expectIndex(j + 4);
- }
- }
- }
-
- const source = raw.slice(start + 1, j);
- return {
- type: 'string',
- source,
- data: source,
- color: isColor(source),
- start,
- end: j + 1
- };
- };
-
- const parseKeyword = start => {
- const nullWord = findWord(start, ['null']);
-
- if (nullWord) {
- return {
- type: 'null',
- source: 'null',
- data: null,
- start,
- end: start + 4
- };
- }
-
- const bool = findWord(start, ['true', 'false']);
-
- if (bool) {
- return {
- type: 'boolean',
- source: bool,
- data: bool === 'true',
- start,
- end: start + bool.length
- };
- }
-
- expectChar(start, '0');
- };
-
- const DIGITS = '0123456789';
-
- const findDecimal = (start, fractional) => {
- let i = start;
- if ('+-'.includes(raw[i])) i += 1;
- let j;
- let dot = -1;
-
- for (j = i; true; j = expectIndex(j + 1)) {
- const ch = expectChar(j, // there must be at least one digit
- // dot must not be the last character of a number, expecting a digit
- j === i || dot >= 0 && dot === j - 1 ? DIGITS : null, // there can be at most one dot
- !fractional || dot >= 0 ? '.' : null);
- if (ch === '.') dot = j;else if (!DIGITS.includes(ch)) break;
- }
-
- return j;
- };
-
- const parseNumber = start => {
- let i = findDecimal(start, true);
- const ch = raw[i];
-
- if (ch && ch.toLowerCase() === 'e') {
- i = findDecimal(i + 1);
- }
-
- const source = raw.slice(start, i);
- return {
- type: 'number',
- source,
- data: +source,
- start,
- end: i
- };
- };
-
- let parseItem;
-
- const parseArray = start => {
- const result = {
- type: 'array',
- data: [],
- start
- };
- let i = start + 1;
-
- while (true) {
- i = expectIndex(skipWhitespace(i));
- if (raw[i] === ']') break;
- if (result.data.length) i = expectSpaceAndCharIndex(i, ',') + 1;
- const item = parseItem(i);
- result.data.push(item);
- i = item.end;
- }
-
- result.end = i + 1;
- return result;
- };
-
- const parseObject = start => {
- const result = {
- type: 'object',
- data: [],
- start
- };
- let i = start + 1;
-
- while (true) {
- i = expectIndex(skipWhitespace(i));
- if (raw[i] === '}') break;
- if (result.data.length) i = expectSpaceAndCharIndex(i, ',') + 1;
- i = expectSpaceAndCharIndex(i, '"');
- const key = parseString(i);
- i = expectSpaceAndCharIndex(key.end, ':') + 1;
- const value = parseItem(i);
- result.data.push({
- key,
- value
- });
- i = value.end;
- }
-
- result.end = i + 1;
- return result;
- };
-
- parseItem = start => {
- const i = expectIndex(skipWhitespace(start));
- const ch = raw[i];
- if (ch === '"') return parseString(i);
- if (ch === '[') return parseArray(i);
- if (ch === '{') return parseObject(i);
- if ('-0123456789'.includes(ch)) return parseNumber(i);
- return parseKeyword(i);
- };
-
- const result = parseItem(0);
- const end = skipWhitespace(result.end);
- if (end < raw.length) expectChar(end, []);
- return result;
- }
-
- function loadJSON() {
- const raw = document.body.innerText;
-
- try {
- // JSON
- const content = tokenize(raw);
- return {
- raw,
- content
- };
- } catch (e) {
- // not JSON
- console.error('Not JSON', e);
- }
-
- try {
- // JSONP
- const parts = raw.match(/^(.*?\w\s*\()(.+)(\)[;\s]*)$/);
- const content = tokenize(parts[2]);
- return {
- raw,
- content,
- prefix: /*#__PURE__*/React.createElement("span", {
- className: "subtle"
- }, parts[1].trim()),
- suffix: /*#__PURE__*/React.createElement("span", {
- className: "subtle"
- }, parts[3].trim())
- };
- } catch (e) {
- // not JSONP
- console.error('Not JSONP', e);
- }
- }
-
- function formatJSON() {
- if (formatter.formatted) return;
- formatter.formatted = true;
- formatter.data = loadJSON();
- if (!formatter.data) return;
- formatter.style = GM_addStyle(css);
- formatter.root = /*#__PURE__*/React.createElement("div", {
- id: "json-formatter"
- });
- document.body.innerHTML = '';
- document.body.append(formatter.root);
- initTips();
- initMenu();
- bindEvents();
- generateNodes(formatter.data, formatter.root);
- }
-
- function generateNodes(data, container) {
- const rootSpan = /*#__PURE__*/React.createElement("span", null);
- const root = /*#__PURE__*/React.createElement("div", null, rootSpan);
- const pre = /*#__PURE__*/React.createElement("pre", null, root);
- formatter.pre = pre;
- const queue = [{
- el: rootSpan,
- elBlock: root,
- ...data
- }];
-
- while (queue.length) {
- const item = queue.shift();
- const {
- el,
- content,
- prefix,
- suffix
- } = item;
- if (prefix) el.append(prefix);
-
- if (content.type === 'array') {
- queue.push(...generateArray(item));
- } else if (content.type === 'object') {
- queue.push(...generateObject(item));
- } else {
- const {
- type,
- color
- } = content;
- if (type === 'string') el.append(createQuote());
- if (color) el.append( /*#__PURE__*/React.createElement("span", {
- className: "color",
- style: `background-color: ${content.data}`
- }));
- el.append( /*#__PURE__*/React.createElement("span", {
- className: `${type} item`,
- "data-type": type,
- "data-value": toString(content)
- }, toString(content)));
- if (type === 'string') el.append(createQuote());
- }
-
- if (suffix) el.append(suffix);
- }
-
- container.append(pre);
- updateView();
- }
-
- function toString(content) {
- return `${content.source}`;
- }
-
- function setFolder(el, length) {
- if (length) {
- el.classList.add('complex');
- el.append( /*#__PURE__*/React.createElement("div", {
- className: "folder"
- }, '\u25b8'), /*#__PURE__*/React.createElement("span", {
- className: "summary"
- }, `// ${length} items`));
- }
- }
-
- function generateArray({
- el,
- elBlock,
- content
- }) {
- const elContent = content.data.length && /*#__PURE__*/React.createElement("div", {
- className: "content"
- });
- setFolder(elBlock, content.data.length);
- el.append( /*#__PURE__*/React.createElement("span", {
- className: "bracket"
- }, "["), elContent || ' ', /*#__PURE__*/React.createElement("span", {
- className: "bracket"
- }, "]"));
- return content.data.map((item, i) => {
- const elValue = /*#__PURE__*/React.createElement("span", null);
- const elChild = /*#__PURE__*/React.createElement("div", null, elValue);
- elContent.append(elChild);
- if (i < content.data.length - 1) elChild.append(createComma());
- return {
- el: elValue,
- elBlock: elChild,
- content: item
- };
- });
- }
-
- function generateObject({
- el,
- elBlock,
- content
- }) {
- const elContent = content.data.length && /*#__PURE__*/React.createElement("div", {
- className: "content"
- });
- setFolder(elBlock, content.data.length);
- el.append( /*#__PURE__*/React.createElement("span", {
- className: "bracket"
- }, '{'), elContent || ' ', /*#__PURE__*/React.createElement("span", {
- className: "bracket"
- }, '}'));
- return content.data.map(({
- key,
- value
- }, i) => {
- const elValue = /*#__PURE__*/React.createElement("span", null);
- const elChild = /*#__PURE__*/React.createElement("div", null, createQuote(), /*#__PURE__*/React.createElement("span", {
- className: "key item",
- "data-type": key.type
- }, key.data), createQuote(), ': ', elValue);
- if (i < content.data.length - 1) elChild.append(createComma());
- elContent.append(elChild);
- return {
- el: elValue,
- content: value,
- elBlock: elChild
- };
- });
- }
-
- function updateView() {
- formatter.options.forEach(({
- key
- }) => {
- formatter.pre.classList[config[key] ? 'add' : 'remove'](key);
- });
- }
-
- function removeEl(el) {
- el.remove();
- }
-
- function initMenu() {
- const handleCopy = () => {
- GM_setClipboard(formatter.data.raw);
- };
-
- const handleCollapse = () => {
- var list = React.getElementsByXPath("//div[@class='complex']");
- for (var i in list) {
- list[i].classList.toggle('collapse');
- }
- };
-
- const handleExpand = () => {
- var list = React.getElementsByXPath("//div[@class='complex collapse']");
- for (var i in list) {
- list[i].classList.toggle('collapse');
- }
- };
-
- const handleMenuClick = e => {
- const el = e.target;
- const {
- key
- } = el.dataset;
-
- if (key) {
- config[key] = !config[key];
- GM_setValue('config', config);
- el.classList.toggle('active');
- updateView();
- }
- };
-
- formatter.root.append( /*#__PURE__*/React.createElement("div", {
- className: "menu",
- onClick: handleMenuClick
- }, /*#__PURE__*/React.createElement("span", {
- onClick: handleCopy
- }, "Copy"), React.createElement("span", {
- onClick: handleExpand
- }, "Expand All"), React.createElement("span", {
- onClick: handleCollapse
- }, "Collapse All"), formatter.options.map(item => /*#__PURE__*/React.createElement("span", {
- className: `toggle${config[item.key] ? ' active' : ''}`,
- dangerouslySetInnerHTML: {
- __html: item.title
- },
- "data-key": item.key
- }))));
- }
-
- function initTips() {
- const tips = /*#__PURE__*/React.createElement("div", {
- className: "tips",
- onClick: e => {
- e.stopPropagation();
- }
- });
-
- const hide = () => removeEl(tips);
-
- document.addEventListener('click', hide, false);
- formatter.tips = {
- node: tips,
- hide,
-
- show(range) {
- const {
- scrollTop
- } = document.body;
- const rects = range.getClientRects();
- let rect;
-
- if (rects[0].top < 100) {
- rect = rects[rects.length - 1];
- tips.style.top = `${rect.bottom + scrollTop + gap}px`;
- tips.style.bottom = '';
- } else {
- [rect] = rects;
- tips.style.top = '';
- tips.style.bottom = `${formatter.root.offsetHeight - rect.top - scrollTop + gap}px`;
- }
-
- tips.style.left = `${rect.left}px`;
- const {
- type,
- value
- } = range.startContainer.dataset;
- tips.innerHTML = '';
- tips.append( /*#__PURE__*/React.createElement("span", {
- className: "tips-key"
- }, "type"), ': ', /*#__PURE__*/React.createElement("span", {
- className: "tips-val",
- dangerouslySetInnerHTML: {
- __html: type
- }
- }));
-
- if (type === 'string' && /^(https?|ftps?):\/\/\S+/.test(value)) {
- tips.append( /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("a", {
- className: "tips-link",
- href: value,
- target: "_blank",
- rel: "noopener noreferrer"
- }, "Open link"));
- }
-
- formatter.root.append(tips);
- }
-
- };
- }
-
- function selectNode(node) {
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStartBefore(node.firstChild);
- range.setEndAfter(node.firstChild);
- selection.addRange(range);
- return range;
- }
-
- function bindEvents() {
- formatter.root.addEventListener('click', e => {
- e.stopPropagation();
- const {
- target
- } = e;
-
- if (target.classList.contains('item')) {
- formatter.tips.show(selectNode(target));
- } else {
- formatter.tips.hide();
- }
-
- if (target.classList.contains('folder')) {
- target.parentNode.classList.toggle('collapse');
- }
- }, false);
- }
-
- }());