JSON Viewer

格式化显示JSON使数据看起来更加漂亮,支持折叠/展开格式化后的数据,支持JSON脑图让调用层级看着更清晰,支持复制JSON脑图节点路径

Versione datata 14/09/2024. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @license      MIT
// @name         JSON Viewer
// @namespace    http://tampermonkey.net/
// @version      0.4.8
// @note         v0.4.8 代码优化
// @note         v0.4.7 增加对JSONP的判断,代码优化
// @note         v0.4.6 增加复制按钮,JSON脑图CSS样式细节优化,JSON脑图增加收起/展开子节点按钮
// @note         v0.4.5 在json-viewer-updated原基础上进行了一些修改,主要有CSS样式修改,新增折叠/展开全部功能,新增JSON脑图功能,脑图节点点击显示调用路径
// @description   格式化显示JSON使数据看起来更加漂亮,支持折叠/展开格式化后的数据,支持JSON脑图让调用层级看着更清晰,支持复制JSON脑图节点路径
// @author       Feny
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_setClipboard 
// @icon         data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAeAB4AAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAAgACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9wvjF8bLX4ZrHZx+XNqlwnmKjH5YUyRvb6kEAd8H058d1b47XV5Ir3eoN++bagaXYrN6KOBn2FfPPx5/aEutX8a+KNWhIubhruZLSNj8u1WMcIOP4QoXOOwNS/wDBM79lDTfjh8YNe+IXj63TxRdeHfKjsE1KMTxtdSbmMmxvlAiVV2IBsUybgAyKR9ZTy6lhsM69Xovm2+iPmKmOqYjEKjT6v5WXU9e8beKrjxZpp/s/xFrfhjWIwWs9X0q42z2kmPlZo2zDcxjqYZ0eNv7obDDf/YL/AG8L745eJNc+G3xCttP0n4qeEWZZ2sVZNP8AENsu0rd2ysS0ZKPG7RMSQsisCcukWd+3x4RtfhpqGk+JNPjS0g1mV7a8iT5UM4XesgHYsofdjglQepJPxwPE1x4V/au8LePtNkeK40ma0lmdDgyorvHMhPo9uxjP+y1bUcHRxmGbS1adn1TXT0/4fcipiqmExCi3pfVdGn19f+GLni/w7ceGf2wPFXga+Vo5rW/vDbI3WWI/v4HA/wBqBg3tk+lfWX7AXia1+F/iDV9B1CRbWHXjFLayyHannpuUxk+rqy4zxlMdWAPQft7fsL33x91nQfiD4DutP0r4oeDXVrT7cWWx1y3UsTZ3LKCyAh5FEigkLLIpHzBk5vwj4JuvE2kLJfeHdY8N6lGAl5pepwBZrOT+JRIuYp0ByBNCzxPg4bIICqYyljMKot62Sa6prr6P/gBDC1MLiHJLS90+jT6ev/Dnmn/BVP8Aaw0nxv4/8P8Aw18KXK69qmk3MlxqEdiRMwuivlpbrtPLopkMnZNy5IIYLyPwa/Z8vvEmv+HtHu4/Ovr+5iS6KDcqAtukwe6om7nuEJxX0XoH7PK/bJG0vRoY5rr/AFslvbLGZf8AfcAf+PGvafgv8CbX4byNqFyI5tWmXYCoytsh6qvqx7t+A4yWiWYUsLhlRpbr72318kVHA1MTiHVqbP7kl0P/2Q==
// @require      https://code.jquery.com/jquery-3.4.1.min.js
// @require      https://unpkg.com/[email protected]/dist/layer.js
// @require      https://unpkg.com/[email protected]/es6/jsmind.js
// @resource     swalStyle https://unpkg.com/[email protected]/style/jsmind.css
// @resource     layerStyle https://unpkg.com/[email protected]/dist/theme/default/layer.css
// ==/UserScript==


/*随机字符串*/
function randomString(e) {
    var e = e || 32,
    t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
    a = t.length,
    n = "";
    for (i = 0; i < e; i++){
        n += t.charAt(Math.floor(Math.random() * a));
    } 
    return n
}
/*检查是否是图片链接*/
function isImg(pathImg) {
    var regexp = /^(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?\/([\w#!:.?+=&%@!\-\/])*\.(gif|jpg|jpeg|png|GIF|JPG|PNG)([\w#!:.?+=&%@!\-\/])?/;
    return regexp.test(pathImg);
}
/** 检验内容是否是json格式的内容*/
function isJSON(str) {
    if (typeof str == 'string') {
        try {
            var obj = JSON.parse(str);
            if(typeof obj == 'object' && obj ){
                console.log("is json")
                return true;
            }else{
                console.log("is not json")
                return false;
            }

        } catch(e) {
            console.log("is not json", e)
            return false;
        }
    }
}

// jquery.json-viewer 插件 开始
// 解决和原网页jquery版本冲突
var jq = jQuery.noConflict(true);
(function(jq){
    /**
     * 检查 arg 是否为至少包含 1 个元素的数组或至少包含 1 个键的字典
     */
    function isCollapsable(arg) {
        return arg instanceof Object && Object.keys(arg).length > 0;
    }

    /**
     * 检查字符串是否为URL
     */
    function isUrl(string) {
        var regexp = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
        return regexp.test(string);
    }

    /**
     * 将 JSON 对象转换为 HTML 表示形式
     * @return string
     */
    function json2html(json) {
        var html = '';
        if (typeof json === 'string') {
            /* Escape tags */
            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
            if (isUrl(json)){
                html += `<a href="${json}" class="json-string">"${json}"</a>`;
            }
            else{
                html += `<span class="json-string">"${json}"</span>`;
            }
        }
        else if (typeof json === 'number') {
            html += `<span class="json-number ">${json}</span>`;
        }
        else if (typeof json === 'boolean') {
            html += `<span class="json-bool ">${json}</span>`;
        }
        else if (json === null) {
            html += '<span class="json-null">null</span>';
        }
        else if (json instanceof Array) {
            if (json.length > 0) {
                html += '<span class="b">[</span><ol class="json-array">';
                for (var i = 0; i < json.length; ++i) {
                    html += '<li>';
                    /* Add toggle button if item is collapsable */
                    if (isCollapsable(json[i])) {
                        html += '<a href class="json-toggle"></a>';
                    }
                    html += json2html(json[i]);
                    /* Add comma if item is not last */
                    if (i < json.length - 1) {
                        html += ',';
                    }
                    html += '</li>';
                }
                html += '</ol><span class="b">]</span>';
            }
            else {
                html += '[]';
            }
        }
        else if (typeof json === 'object') {
            var key_count = Object.keys(json).length;
            if (key_count > 0) {
                html += '<span class="b">{</span><ul class="json-dict">';
                for (var key in json) {
                    if (json.hasOwnProperty(key)) {
                        html += '<li>';
                        /* Add toggle button if item is collapsable */
                        if (isCollapsable(json[key])) {
                            html += '<a href class="json-toggle"></a>';
                        }
                        html += `<span class="json-key">"${key}"</span>: ${json2html(json[key])}`;
                        /* Add comma if item is not last */
                        if (--key_count > 0){
                            html += ',';
                        }
                        html += '</li>';
                    }
                }
                html += '</ul><span class="b">}</span>';
            }
            else {
                html += '{}';
            }
        }
        return html;
    }

    jq.fn.jsonViewer = function(json, jsonpFunctionName) {
        return this.each(function() {
            /* Transform to HTML */
            var html = json2html(json);
            /** is JSONP */
            if(jsonpFunctionName !== undefined && jsonpFunctionName !== null){
                html = `<div class="jsonp">${jsonpFunctionName}(</div>${html}<div class="jsonp">)</div>`
            }
            /* Insert HTML in target DOM element */
            jq(this).html(html);

            /* Bind click on toggle buttons */
            jq(this).off('click');
            jq(this).on('click', 'a.json-toggle', function() {
                var target = jq(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
                target.toggle();
                if (target.is(':visible')) {
                    target.siblings('.json-placeholder').remove();
                }
                else {
                    var count = target.children('li').length;
                    var placeholder = count + (count > 1 ? ' items' : ' item');
                    target.after('<a href class="json-placeholder">' + placeholder + '</a>');                    
                }
                return false;
            });

            /* Simulate click on toggle button when placeholder is clicked */
            jq(this).on('click', 'a.json-placeholder', function() {
                jq(this).siblings('a.json-toggle').click();
                jq(this).siblings('a.json-placeholder').remove();
                return false;
            });
        });
    };
})(jq);
// jquery.json-viewer 插件 结束

(function() {
    'use strict';

    var source = jq('pre[style="word-wrap: break-word; white-space: pre-wrap;"]').first();
    
    // 根据上面这一点没办法确定是需要添加json格式化工具,再加上对内容进行判断是不是json格式的内容
    let rawText = source.html()
    if(!rawText){
        return
    }

    // 判断是否为jsonp格式
    let tokens = rawText.match(/^([^\s(]*)\s*\(([\s\S]*)\)\s*;?$/),
        jsonpFunctionName = null;
    if (tokens && tokens[1] && tokens[2]) {
        jsonpFunctionName = tokens[1]
		rawText= tokens[2]
    }

    // 如果是直接打开的json接口地址才需要格式化插件
    if(source.length == 0 || !isJSON(rawText)){
        return
    }    

    // 随机rgb颜色
    let rgbaColor = `${Math.random()*256}, ${Math.random()*256}, ${Math.random()*256}`
    // 添加样式
    GM_addStyle(GM_getResourceText('swalStyle'))
    GM_addStyle(GM_getResourceText('layerStyle'))
    GM_addStyle(`
        #json-renderer {
            line-height: 1.5;
            font-size: 14px;
            display: block;
            font-family: monospace;
            margin: 15px 30px;
        }
        .btnGroup, .jmBtnGroup{
            position: fixed;
            top: 30px;
            right: 30px;
        }
        .btn {
            border: 1px solid rgb(218, 220, 224);
            box-sizing: border-box;
            color: rgb(26, 115, 232);
            cursor: pointer;
            line-height: 28px;
            float: left;
            display: inherit;
            padding: 0 10px;
        }
        .btn:hover {
            background-color: rgb(210, 227, 252);
        }
        ul.json-dict,
        ol.json-array {
            list-style-type: none;
            margin: 0 0 0 2px;
            border-left: 1px dotted #5D6D7E;
            padding-left: 24px;
        }
        .b {
            font-weight: 700;
        }
        .jsonp{
            margin-left: -30px;
        }
        .json-key {
            /* color: #A31515;*/
            color: #910F93;
        }
        .json-string {
            /* color: #0b7500;*/
            color: #4B8A4C;
        }
        .json-number  {
            /* color: #164FF0;*/
            color: #1a01cc;
            font-weight: 600;
        }
        .json-bool{
            color: #905;
            font-weight: 600;
        }
        .json-null {
            /* color: #F1592A;*/
            color: #0031BC;
            font-weight: 600;
        }
        a.json-toggle {
            position: relative;
            color: inherit;
            opacity: 0.2;
            text-decoration: none;
        }
        
        a.json-toggle:hover {
            opacity: 0.35;
        }
        a.json-toggle:active {
            opacity: 0.5;
        }
        a.json-toggle:focus {
            outline: none;
        }
        a.json-toggle:before {
            top: 2.5px;
            left: -15px;
            position: absolute;
            content: "";
            display: block;
            width: 0;
            height: 0;
            border-style: solid;
            border-width: 5px 0 5px 8px;
            border-color: transparent transparent transparent currentColor;
            transform: rotate(90deg);
        }
        a.json-toggle.collapsed:before {
            transform: rotate(0deg);
        }
        a.json-placeholder {
            color: #aaa;
            font-size: 13px;
            padding: 0 1em;
            text-decoration: none;
        }
        a.json-placeholder:hover {
            text-decoration: underline;
        }
        #jsmind_container{
            position: fixed;
            z-index: 999;
            top: 0;
            left: 0;
            display: none;
            width: 100vw; 
            height: 100%;
            background:#F7F7F7
        }
        .jmBtnGroup{
            z-index: 9999;
            display: none;
        }

        /**脑图自定义样式*/
        jmnode{
            display: flex;
            align-items: center;
            padding: 0 7px 0 22px;
        }
        jmnode{
            color: #475872 !important;
            box-shadow: none !important;
            background-color: transparent !important;
        }
        jmnode:hover{
            text-shadow: 1px 1px 1px currentColor;
        }
        jmnode.root {
            padding: 0;
            color: transparent !important;
        }
        jmnode:not(.root)::before, jmnode.root::before{
            content: " ";
            top: 50%;
            position: absolute;
            border-radius: 50%;
            transform: translateY(-50%);
        }
        jmnode:not(.root)::before{
            left: 0;
            width: 15px;
            height: 15px;
            background: rgba(${rgbaColor}, 0.5);
        }
        jmnode.root::before{
            left: 50%;
            width: 18px;
            height: 18px;
            transform: translate(-18px, -50%);
            background: rgba(${rgbaColor}, 0.7);
        }
        jmexpander{
            margin-top: 1px;
            line-height: 9px;
        }

        .layui-layer-tips{
            width: auto !important;
        }

        .mind-array{
            opacity: 0.5;
            font-size: 12px;
            padding-left: 5px;
        }
    `)

    source.attr("id", "json-source").hide()
    // 将内容用eval函数处理下
    var jsonObject = eval('(' + rawText + ')');
    // 添加一个格式化显示的per元素
    jq("body").append('<div id="json-renderer"></div>')
    .append(`<div class="btnGroup"><input class="btn" type="button" value="复制" id="copyJson"/>
        <input class="btn" type="button" value="折叠全部" id="collapseJson"/>
        <input class="btn" type="button" value="JSON脑图" id="showMind"/>
        <input class="btn" type="button" value="原文本" id="switchRawText"/></div>`)
    // JSON脑图相关
    .append(`<div id="jsmind_container"></div>
        <div class="jmBtnGroup"><input class="btn" type="button" value="收起节点" id="collapseNode"/>
        <input class="btn" type="button" value="展开节点" id="expandNode"/>
        <input class="btn" type="button" value="返回" id="closeMind"/></div>`);

    // 调用格式化方法
    jq('#json-renderer').jsonViewer(jsonObject, jsonpFunctionName);

    let btnEvent = {
        // 复制JSON文本内容
        copyJson: function(){
            GM_setClipboard(JSON.stringify(jsonObject))
            layer.msg('复制成功', {time: 1500})
        },
        // 折叠全部的JSON结构
        collapseJson: function(e){
            var that = jq(e), v = that.val();
            if(v === "折叠全部"){
                jq('.json-toggle').not('.collapsed').click()
            }else{
                jq('a.json-placeholder').click().remove();
            }

            that.val(v === "折叠全部" ? "展开全部" : "折叠全部")
        },
        // 查看原始/格式化JSON内容
        switchRawText: function(e){
            var that = jq(e), v = that.val();
            that.val(v === '原文本' ? "格式化" : "原文本")
            jq('#json-source, #json-renderer').toggle();
        },
        // 显示JSON脑图
        showMind: function(){
            let isArr = false;
            if(Array.isArray(jsonObject)){
                if(typeof jsonObject[0] !== 'object'){
                    layer.msg('数据结构无法生成脑图', {time: 1000})
                    return
                }
                isArr = true
                jsonObject = jsonObject[0]
            }

            jq('.jmBtnGroup').show()
            jq('#jsmind_container').fadeToggle(200);
            document.documentElement.style.overflow='hidden';
            
            if(!window.jm){
                window.jm = new jsMind({
                    mode :'side', 
                    editable: false,
                    container:'jsmind_container',
                    view: {
                        hmargin: 50, // 思维导图距容器外框的最小水平距离
                        vmargin: 50,  // 思维导图距容器外框的最小垂直距离
                        engine: 'svg', // 思维导图各节点之间线条的绘制引擎
                        draggable: true, // 当容器不能完全容纳思维导图时,是否允许拖动画布代替鼠标滚动
                        support_html : false, 
                        line_color: '#C4C9D0',
                    },
                    layout: {
                        vspace: 7, // 节点之间的垂直间距
                        hspace: 150, // 节点之间的水平空间
                    },
                });
                jm.show({
                    "meta":{
                        "name":"JSON脑图",
                        "author":"[email protected]",
                        "version":"1.0"
                    },
                    "format":"node_tree",
                    /* 数据内容 */
                    "data": {
                        "id": "root",
                        "topic": 'Response',
                        "direction": "left",
                        "children": convertToMind(jsonObject),
                        "chain": isArr ? 'Response[i]' : 'Response'
                    }
                });

                // 脑图节点事件
                jq("jmnode").on('dblclick mouseover mouseout', function(event){
                    let that = jq(this), 
                        node = jm.get_node(that.attr('nodeid'))
                    if(!node.parent){
                        return
                    }

                    switch(event.type){
                        case 'dblclick':
                            GM_setClipboard(mindChain(node))
                            layer.msg('节点路径复制成功', {time: 1500})
                            break;
                        case 'mouseover':
                            let s = `<b>节点路径(双击复制)</b><br/>${mindChain(node)}`
                            layer.tips(s, that, {
                                time: 0,
                                tips: [2, '#1e2732']
                            });
                            break;
                        default:
                            layer.closeAll()
                            break;
                    }
                })
            }
        },
        // 收起节点
        collapseNode: () => jm.collapse_all(),
        // 展开节点
        expandNode: () => jm.expand_all(),
        // 关闭JSON脑图
        closeMind: function(){
            jq('.jmBtnGroup').hide()
            jq('#jsmind_container').fadeToggle(200);
            document.documentElement.style.overflow='';
        },
    }
    // 按钮点击事件
    jq('.btn').click(e => btnEvent[e.target.id](e.target))

    // 所有a标签,看是否是图片,是图片生成预览图  
    jq("a.json-string").hover(function(){
        var that = jq(this), href = that.attr('href');
        if(isImg(href)){
            layer.tips(`<img src="${href}" />`, that, {
                time: 0,
                anim: 5,
                maxWidth: 500,
                tips: [2, '#d9d9d9']
            });
        }
    }, () => layer.closeAll())
})();


/** JSON数据转换为jsMind所需要的数据结构 */
function convertToMind(json){
    let children = []
    if(typeof json === 'object'){
        for (let i = 0, keys = Object.keys(json); i < keys.length; i++){
            let val = json[keys[i]];
            if(val === null || ['string', 'number', 'boolean', 'undefined'].includes(typeof val)){
                children.push({
                    id: keys[i] + '-' + randomString(10),
                    topic: `${keys[i]}`,
                    chain: keys[i]
                })
            } else if(Array.isArray(val)){
                children.push({
                    id: keys[i] + '-' + randomString(10),
                    topic: `${keys[i]}<span class="mind-array">[${val.length}]</span>`,
                    chain: keys[i],
                    isArray: true,
                    children: convertToMind(val[0], keys[i])
                })
            } else if(typeof val === 'object'){
                children.push({
                    id: keys[i] + '-' + randomString(10),
                    topic: `${keys[i]}`,
                    chain: keys[i],
                    children: convertToMind(val, keys[i])
                })
            }
        }
    }
    return children;
}
// 脑图节点调用链
function mindChain(node){
    let s = node.data.chain
    if(!node.parent){
        return s
    }

    let p = node.parent, r = mindChain(p)
    s = p.data.isArray ? `${r}[i].${s}` : `${r}.${s}`
    return s
}