您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Format JSON data in a beautiful way.
当前为
'use strict'; // ==UserScript== // @name JSON formatter // @namespace http://gerald.top // @author Gerald <[email protected]> // @icon http://cn.gravatar.com/avatar/a0ad718d86d21262ccd6ff271ece08a3?s=80 // @description Format JSON data in a beautiful way. // @description:zh-CN 更加漂亮地显示JSON数据。 // @version 2.0.0 // @match *://*/* // @match file:///* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setClipboard // ==/UserScript== const gap = 5; const formatter = { options: [{ key: 'show-quotes', title: '"', def: true }, { key: 'show-commas', title: ',', def: true }] }; const config = Object.assign({}, formatter.options.reduce((res, item) => { res[item.key] = item.def; return res; }, {}), GM_getValue('config')); if (['application/json', 'text/plain', 'application/javascript', 'text/javascript'].includes(document.contentType)) formatJSON(); GM_registerMenuCommand('Toggle JSON format', formatJSON); function safeHTML(html) { return String(html).replace(/[<&"]/g, key => ({ '<': '<', '&': '&', '"': '"' })[key]); } function createElement(tag, props) { const el = document.createElement(tag); if (props) { Object.keys(props).forEach(key => { el[key] = props[key]; }); } return el; } function createQuote() { return createElement('span', { className: 'subtle quote', textContent: '"' }); } function createComma() { return createElement('span', { className: 'subtle comma', textContent: ',' }); } function loadJSON() { const raw = document.body.innerText; try { // JSON const content = JSON.parse(raw); return { raw, content }; } catch (e) { // not JSON } try { // JSONP const parts = raw.match(/^(.*?\w\s*\()(.+)(\)[;\s]*)$/); const content = JSON.parse(parts[2]); return { raw, content, prefix: createElement('span', { className: 'subtle', textContent: parts[1].trim() }), suffix: createElement('span', { className: 'subtle', textContent: parts[3].trim() }) }; } catch (e) { // not JSONP } } function formatJSON() { if (formatter.formatted) return; formatter.formatted = true; formatter.data = loadJSON(); if (!formatter.data) return; formatter.style = GM_addStyle(".tips-link {\n color: slateblue;\n}.tips-val {\n color: dodgerblue;\n}.complex.collapse::before {\n display: none;\n}* {\n margin: 0;\n padding: 0;\n}\n\nhtml, body {\n font-family: Menlo, \"Microsoft YaHei\", Tahoma;\n}\n\n#json-formatter {\n position: relative;\n margin: 0;\n padding: 2em 1em 1em 2em;\n font-size: 14px;\n line-height: 1.5;\n}\n\n#json-formatter > pre {\n white-space: pre-wrap\n}\n\n#json-formatter > pre:not(.show-quotes) .quote, #json-formatter > pre:not(.show-commas) .comma {\n display: none;\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.content {\n padding-left: 2em;\n}\n.collapse > span > .content {\n display: inline;\n padding-left: 0;\n}\n.collapse > span > .content > * {\n display: none;\n}\n.collapse > span > .content::before {\n content: '...';\n}\n.complex {\n position: relative\n}\n.complex::before {\n content: '';\n position: absolute;\n top: 1.5em;\n left: -.5em;\n bottom: .7em;\n margin-left: -1px;\n border-left: 1px dashed currentColor;\n}\n.folder {\n color: #999;\n position: absolute;\n top: 0;\n left: -1em;\n width: 1em;\n text-align: center;\n transform: rotate(90deg);\n transition: transform .3s;\n}\n.collapse > .folder {\n transform: rotate(0);\n}\n.folder::before {\n content: '\\25B8';\n}\n.summary {\n color: #999;\n margin-left: 1em;\n}\n*:not(.collapse) > .summary {\n display: none;\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 z-index: 10;\n}\n.menu > span {\n display: inline-block;\n padding: 4px 8px;\n margin-right: 5px;\n border-radius: 4px;\n background: #ddd;\n border: 1px solid #ddd;\n cursor: pointer\n}\n.menu > span.toggle:not(.active) {\n background: none;\n}\n"); formatter.root = createElement('div', { id: 'json-formatter' }); document.body.innerHTML = ''; document.body.append(formatter.root); initTips(); initMenu(); bindEvents(); generateNodes(formatter.data, formatter.root); } async function generateNodes(data, container) { const pre = createElement('pre'); formatter.pre = pre; const root = createElement('div'); const rootSpan = createElement('span'); root.append(rootSpan); pre.append(root); const queue = [Object.assign({ el: rootSpan, elBlock: root }, data)]; while (queue.length) { const item = queue.shift(); const { el, content, prefix, suffix } = item; if (prefix) el.append(prefix); if (Array.isArray(content)) { queue.push(...generateArray(item)); } else if (content && typeof content === 'object') { queue.push(...generateObject(item)); } else { const type = content == null ? 'null' : typeof content; if (type === 'string') el.append(createQuote()); const node = createElement('span', { className: `${type} item`, textContent: `${content}` }); node.dataset.type = type; node.dataset.value = content; el.append(node); if (type === 'string') el.append(createQuote()); } if (suffix) el.append(suffix); } container.append(pre); updateView(); } function setFolder(el, length) { if (length) { el.classList.add('complex'); el.append(createElement('div', { className: 'folder' })); el.append(createElement('span', { textContent: `// ${length} items`, className: 'summary' })); } } function generateArray({ el, elBlock, content }) { const elContent = content.length && createElement('div', { className: 'content' }); setFolder(elBlock, content.length); el.append(createElement('span', { textContent: '[', className: 'bracket' }), elContent || ' ', createElement('span', { textContent: ']', className: 'bracket' })); return content.map((item, i) => { const elChild = createElement('div'); elContent.append(elChild); const elValue = createElement('span'); elChild.append(elValue); if (i < content.length - 1) elChild.append(createComma()); return { el: elValue, elBlock: elChild, content: item }; }); } function generateObject({ el, elBlock, content }) { const keys = Object.keys(content); const elContent = keys.length && createElement('div', { className: 'content' }); setFolder(elBlock, keys.length); el.append(createElement('span', { textContent: '{', className: 'bracket' }), elContent || ' ', createElement('span', { textContent: '}', className: 'bracket' })); return keys.map((key, i) => { const elChild = createElement('div'); elContent.append(elChild); const elValue = createElement('span'); const node = createElement('span', { className: 'key item', textContent: key }); node.dataset.type = typeof key; elChild.append(createQuote(), node, createQuote(), ': ', elValue); if (i < keys.length - 1) elChild.append(createComma()); return { el: elValue, content: content[key], elBlock: elChild }; }); } function updateView() { formatter.options.forEach(({ key }) => { formatter.pre.classList[config[key] ? 'add' : 'remove'](key); }); } function removeEl(el) { el.remove(); } function initMenu() { const menu = createElement('div', { className: 'menu' }); const btnCopy = createElement('span', { textContent: 'Copy' }); btnCopy.addEventListener('click', () => { GM_setClipboard(formatter.data.raw); }, false); menu.append(btnCopy); formatter.options.forEach(item => { const span = createElement('span', { className: `toggle${config[item.key] ? ' active' : ''}`, innerHTML: item.title }); span.dataset.key = item.key; menu.append(span); }); menu.addEventListener('click', e => { const el = e.target; const { key } = el.dataset; if (key) { config[key] = !config[key]; GM_setValue('config', config); el.classList.toggle('active'); updateView(); } }, false); formatter.root.append(menu); } function initTips() { const tips = createElement('div', { className: 'tips' }); const hide = () => removeEl(tips); tips.addEventListener('click', e => { e.stopPropagation(); }, false); document.addEventListener('click', hide, false); formatter.tips = { node: tips, hide, show(range) { const scrollTop = document.body.scrollTop; 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[0]; 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; const html = [`<span class="tips-key">type</span>: <span class="tips-val">${safeHTML(type)}</span>`]; if (type === 'string' && /^(https?|ftps?):\/\/\S+/.test(value)) { html.push('<br>', `<a class="tips-link" href="${encodeURI(value)}" target="_blank">Open link</a>`); } tips.innerHTML = html.join(''); 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); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址