JSON Viewer

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

目前為 2024-09-14 提交的版本,檢視 最新版本

// ==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
}