Greasy Fork

JSON Viewer

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

目前为 2024-10-06 提交的版本。查看 最新版本

// ==UserScript==
// @license      MIT
// @name         JSON Viewer
// @namespace    http://tampermonkey.net/
// @version      0.4.9
// @note         v0.4.9 布局修改,增加保存JSON/脑图为文件,增加JSON过滤,鼠标移入key提示json-path
// @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_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @grant        GM_setClipboard 
// @grant        GM_getResourceText
// @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.min.js
// @require      https://unpkg.com/[email protected]/es6/jsmind.js
// @require      https://unpkg.com/[email protected]/dist/layer.js
// @require      https://unpkg.com/[email protected]/dist/dom-to-image.min.js
// @require      https://unpkg.com/[email protected]/es6/jsmind.screenshot.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() {
    'use strict';

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

        // 检查是否是图片链接
        isImg: function (pathImg) {
            // var regexp = /^(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?\/([\w#!:.?+=&%@!\-\/])*\.(gif|jpg|jpeg|png|GIF|JPG|PNG)([\w#!:.?+=&%@!\-\/])?/;
            let regexp = /\.(ico|bmp|gif|jpg|jpeg|png|svg|webp|GIF|JPG|PNG|WEBP|SVG)([\w#!:.?+=&%@!\-\/])?/i
            return regexp.test(pathImg);
        },
        // 检验内容是否是json格式的内容
        isJSON: function (str) {
            try {
                JSON.parse(str)
                return true
            } catch(e) {
                console.log("is not json", e)
                return false
            }
        },
        // 获取数据类型
        getType: function (value){
            return Object.prototype.toString.call(value).match(/\s(.+)]/)[1].toLowerCase();
        },
        // JSON 过滤
        filterJson: function(json, filter) {
            if(!filter){
                return json
            }

            filter = filter.toLowerCase()
            let newJSON = Utils.getType(json) == 'array' ? [] : {}
            for (let key in json) {
                let val = json[key]
                if (typeof val === 'object') {
                    let subJSON = this.filterJson(val, filter);
                    if (Object.keys(subJSON).length > 0) {
                        newJSON[key] = subJSON;
                    }
                }else{
                    if(key.toLowerCase().includes(filter.toLowerCase())){
                        newJSON[key] = val
                    }

                    if(val !== null && val.toString().toLowerCase().includes(filter.toLowerCase())){
                        newJSON[key] = val
                    }
                }
            }
            return newJSON;
        }
    }

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

        /**
         * 将 JSON 对象转换为 HTML 表示形式
         * @return string
         */
        function json2html(json, parentPath = '') {
            let html = '', type = Utils.getType(json)
            switch(type){
                case 'array':
                case 'object':
                    let len = json.length || Object.keys(json).length;
                    if (len > 0) {
                    html += '<span class="json-brackets">';
                    html += type === 'array' ? '[</span><ol class="json-array">' : '{</span><ul class="json-object">';
                        for (var key in json) {
                            if (json.hasOwnProperty(key)) {
                                let comma  = --len > 0 ? ',' : '',
                                    jsonPath = parentPath + '.' + key,
                                    collapse = isCollapsable(json[key]) ? '<a href class="json-toggle"></a>' : '',
                                    res = json2html(json[key], jsonPath)
                                let toHtml = type === 'array' ? res : `<span class="json-key">"${key}"</span>: ${res}`
                                html += [`<li json-path="${jsonPath}">`, collapse, toHtml, comma, '</li>'].join('')
                            }
                        }

                        if(type === 'array'){
                            html += '</ol><span class="json-brackets">]</span>'
                        }else{
                            html += '</ul><span class="json-brackets">}</span>'
                        }
                    }else{
                        html += '<span class="json-brackets">'
                        html += (type === 'array') ? '[]' : '{}'
                        html += '</span>'
                    }
                    break
                default:
                    /* Escape tags */
                    json = type === 'string' ? json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : json
                    if (Utils.isUrl(json)){
                        html += `<a target="_blank" href="${json}" class="json-string">"${json}"</a>`;
                    }else{
                        json = type === 'string' ? `"${json}"` : json
                        html += `<span class="json-${type}">${json}</span>`;
                    }
                    break
            }
            return html;
        }

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

                /* Bind click on toggle buttons */
                $(this).off('click');
                $(this).on('click', 'a.json-toggle', function() {
                    var target = $(this).toggleClass('collapsed').siblings('ul.json-object, ol.json-array');
                    target.toggle();
                    if (target.is(':visible')) {
                        target.siblings('.json-placeholder').remove();
                    }else {
                        var count = target.children('li:not([class*="hidden"])').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 */
                $(this).on('click', 'a.json-placeholder', function() {
                    $(this).siblings('a.json-toggle').click();
                    $(this).siblings('a.json-placeholder').remove();
                    return false;
                });
            });
        };
    })(_jQuery);
    // jquery.json-viewer 插件 结束

    (function($){
        var source = $('pre[style="word-wrap: break-word; white-space: pre-wrap;"]').first();
        if(source.length === 0){
            return
        }
        
        let rawText = source.text()
        if(!rawText){
            return
        }

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

        if(!Utils.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(GM_getResourceText('treetableStyle'))
        $("head").append(`<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/dist/theme/default/layer.css"/>`)

        GM_addStyle(`
            body, html{
                margin: 0;
                padding: 0;
                font-size: 14px;
            }
            td{
                font-size: 14px;
            }
            li::marker {
                content: '';
            }
            .hidden{
                display: none !important;
            }
            .scroll-top{
                width: 48px;
                height: 48px;
                z-index: 999;
                position: fixed;
                right: 30px;
                bottom: 30px;
                background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAA39JREFUaEPtmV2ITVEUx39rJjT5KPKC5DPKK6UGZZ7Eq0whNUiKFPfcKW9mnmjm7ssLaSLygEx5xItMESnKC4V8FV4IJUkyS+fMHZ07zjl3733PvT6a+3S6Z629/r+99sfa+wgpPw1YDtxLe9/k/1eI4X5STMkSokXWNlloYjgpMZSmIxPgbxBfS8M4QK0eavT78Qw0uodrtd+wDGjAMqBcEVAQw6NaYnzeNwRAi2xBOQlMrYj6jLJTygz6iMzyyR1Ai+xCGUgMqnTmDZErgBYoIJjMXhY2S4mLeWUiNwAt0INwyEqY0iNleq1saxjlAqAB64BrToKEHVLijJNPgnHdALqPaUziOsoKRzGfaKFD+nng6FdlXj+Ay9AZq1Q5L2W2/jEA3c8sWqOSe3aCiK/A3cr/K4G2FKEbxXDZF6KuDKROXKWXFvqlxJdQmBaZzDDdKZP8thhW/RmAgKfA4qrgSq+U6UkSlAostEuJOz4Q3hnQgPXAld+CCh1pB5DogKTcSBAalhpHmw1wGtgxJuhbMczJEqIBbxLmzFUxbGg2gCYE9AVADF6jwctJDzKd73xI7DG/IQQTmCFH+OiaBT+AAyykhWcpBZv7JA4bGmaRHOV5cwBqX7mc5Qe9coyX0TK6n/m0RnVSV4bA1KuTLCi/DBTYhHCpRm+Fe8Bo/b8JmJxp71lqOwNowB7guGuqLe33iuGEpW1k5gTgVDK7qIjbZmyESU1aA1TOuA99dTn5CQulxAsbH3uAeqpOGyXVNrvEcMrGzR4gIGnjsonhYzMkhg4bx3EAm17ysGlIBh4DSzzE+LgMiGG3jaPLEApv2Q7YNFq3jcOmZg9gt/u6aH8PzEx0aGWu9PHapjFrgKimCaIDTHiQqf4pNxHW2AT8ZaMEiZdgSreUKdm25QRQgRi7nIZzI7xZcPmeNkAL5xjmVpVQx1049HUGiCAKFBH6K8GjFUP3MIW2CGJpjd4bFEPnmM54grA761tYWpteAJXgy1A6w6p09Opcu1nED7ZlAcQP/BqwGZjORAblMO9sh03czhsgLZgW6EL4JoYL0b0RTArPBfFnH6G5ZyAVYKTkGBlWQVR2bxDDgvjzvwQQXqGsDQ/sGkTXKdHzOECsB3LtjdjKMjqE/qMMjJwn5olh+98+hM6gvAqXy0aN+4Yuo/HG/weAPmC1GNrzHDbxtn4Coc0pQNdM3UAAAAAASUVORK5CYII=)
            }
            /** 工具栏样式 START **/
            .flex-container{
                height: 100vh;
                display: flex;
                flex-direction: column;
            }
            .tabs, .toolbar{
                display: flex;
                line-height: 28px;
                background: #f3f3f3;
                border-bottom: 1px solid #e0e0e2;
            }

            .toolbar{
                line-height: 23px;
                box-shadow: 0px 3px 5px 0 #ddd;
            }
            .searchbox{
                display: flex;
                flex-grow: 1;
            }
            .toolbar input{
                flex-grow: 1;
                border: none;
                outline: none;
                font-size: 12px;
                padding-left: 23px;
                background-size: 12px;
                background-repeat: no-repeat;
                background-position: 7px center;
                background-image: url(data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIGZpbGw9InJnYmEoMTM1LCAxMzUsIDEzNywgMC45KSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTIgMTIiPgogIDxwYXRoIGZpbGw9ImNvbnRleHQtZmlsbCIgb3BhY2l0eT0iLjQiIGQ9Ik01IDkuMmwyIDEuNlY2LjFMOC41NSA0aC01LjFMNSA2LjF2My4xeiIvPgogIDxwYXRoIGZpbGw9ImNvbnRleHQtZmlsbCIgZD0iTTEuMTggMi42QTEgMSAwIDAgMSAyIDFIMTBhMSAxIDAgMCAxIC44IDEuNkw4IDYuNHY0LjgyYzAgLjYzLS43Mi45OC0xLjIyLjZsLTIuNS0xLjk5QS43NS43NSAwIDAgMSA0IDkuMjVWNi40MUwxLjE4IDIuNnpNMiAyTDUgNi4wOXYzLjA0bDIgMS41OVY2LjA5TDEwLjAxIDJIMnoiLz4KPC9zdmc+Cg==);
            }
            .clear {
                flex: 0 0 auto;
                align-self: center;
                margin: 0 4px;
                padding: 0;
                border: 0;
                width: 16px;
                height: 16px;
                background-color: transparent;
                background-image: url(data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIGZpbGw9ImNvbnRleHQtZmlsbCIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSI+CiAgPHBhdGggZD0iTTYuNTg2IDhsLTIuMjkzIDIuMjkzYTEgMSAwIDAgMCAxLjQxNCAxLjQxNEw4IDkuNDE0bDIuMjkzIDIuMjkzYTEgMSAwIDAgMCAxLjQxNC0xLjQxNEw5LjQxNCA4bDIuMjkzLTIuMjkzYTEgMSAwIDEgMC0xLjQxNC0xLjQxNEw4IDYuNTg2IDUuNzA3IDQuMjkzYTEgMSAwIDAgMC0xLjQxNCAxLjQxNEw2LjU4NiA4ek04IDBhOCA4IDAgMSAxIDAgMTZBOCA4IDAgMCAxIDggMHoiLz4KPC9zdmc+Cg==);
            }
            .tabs-item, .toolbar-item{
                cursor: pointer;
                padding: 0 10px;
                font-size: 12px;
                border-top: 2px solid #f3f3f3;
            }
            .tabs-item.active{
                color: #0060df;
                border-top: 2px solid #0060df !important;
                background: #e9e9e9;
            }
            .tabs-item:hover{
                background: #e9e9e9;
                border-top: 2px solid #c3c3c6;
            }
            
            .toolbar-item:hover{
                background: #e9e9e9;
                border-top: 2px solid #e9e9e9;
            }
            /** 工具栏样式 END **/
            /** JSON 格式化样式 START **/
            ul.json-object,
            ul.json-array {
                list-style-type: none;
                margin: 0 0 0 2px;
                border-left: 1px dotted #5D6D7E;
                padding-left: 24px;
            }
            .json-brackets {
                font-weight: 700;
            }
            .json-key {
                /* color: #A31515;*/
                color: #910F93;
                cursor: pointer;
            }
            .json-string, .json-string a{
                /* color: #0b7500;*/
                color: #4B8A4C;
            }
            .json-number{
                /* color: #164FF0;*/
                color: #1a01cc;
                font-weight: 600;
            }
            .json-boolean{
                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: 12px;
                padding: 0 1em;
                text-decoration: none;
            }
            a.json-placeholder:hover {
                text-decoration: underline;
            }
            /** JSON 格式化样式 END **/
            /** 脑图样式 START **/
            #jmContainer{
                width: 100vw; 
                height: calc(100vh - 57px);
                /* background:#F7F7F7 */
            }
            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;
            }
            /** 脑图样式 END **/

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

            .mind-array{
                opacity: 0.5;
                font-size: 12px;
                padding-left: 5px;
            }
            /** 容器样式 START **/
            .tabs-container{
                overflow: auto;
                line-height: 1.5;
                font-family: monospace;
            }
            .tabs-container > div{
                display: none;
            }
            .tabs-container > div.active{
                display: block;
            }
            .tabs-container #formater{
                padding: 10px;
            }
            .tabs-container #rawText{
                padding: 0 10px;
            }
            .tabs-container #rawText pre{
                display: block !important;
            }
            /** 容器样式 END **/

            table.treetable{
                border: none;
            }
            .treetable tbody tr td {
                line-height: 16px;
            }
            table.treetable .indenter a {
                width: 20px;
                display: inline-block;
                text-decoration: none;
            }
            table.treetable tr:hover{
                background: #f0f9fe;
            }
            table.treetable tr.selected td, table.treetable tr.selected td a{
                color: #fff !important;
                background-color: #3875d7 !important;
            }
            table.treetable tbody tr td:first-child{
                width: 100px;
            }
            table.treetable tr.branch{
                background: none;
            }
            table.treetable span.json-brackets{
                padding: 0;
            }
        `)

        source.hide()
        // 将内容用eval函数处理下
        const jsonObject = eval('(' + rawText + ')');

        $("body").append(`
        <div class="scroll-top"></div>
        <div class="flex-container">
            <div class="panel">
                <div class="tabs">
                    <div class="tabs-item btn active" id="formaterJSON">JSON格式化</div>
                    <div class="tabs-item btn" id="showMind">JSON脑图</div>
                    <div class="tabs-item btn" id="switchRawText">原始数据</div>
                </div>
                <div class="toolbar">
                    <div class="toolbar-item btn" id="saveJson">保存</div>
                    <div class="toolbar-item btn" id="copyJson">复制</div>
                    <div class="toolbar-item btn" id="collapseAll">全部折叠</div>
                    <div class="toolbar-item btn" id="expandAll">全部展开</div>
                    <div class="toolbar-item btn" id="formaterRawText" style="display: none">美化输出</div>
                    <div class="searchbox">
                        <input type="text" placeholder="过滤 JSON "/>
                        <button class="clear" hidden></button>
                    </div>
                </div>
            </div>
            <div class="tabs-container">
                <div class="active" id="formater"></div>
                <div id="jmContainer"></div>
                <div id="rawText"><pre></pre></div>
            </div>
        </div>`) 

        let btnEvent = {
            isFormater: false,
            $rawText: $('#rawText'),
            /**
             * 保存为文件
             */
            download: {
                download: function(content, filename) {
                    const link = document.createElement("a")
                    link.href = content
                    link.download = filename
                    link.click()
                },
                saveJSON: function (text) {
                    // 创建一个 Blob 对象,包含要下载的文本内容
                    const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
                    const url = URL.createObjectURL(blob)
                    let filename = new Date().getTime() + '.json';
                    this.download(url, filename)
                    URL.revokeObjectURL(url);
                },
                savePNG: () => jm.shoot(),
            },
            saveJson:function(){
                if($('#jmContainer').is(':visible')){
                    this.download.savePNG()
                }else{
                    this.download.saveJSON(this.$rawText.text())
                }
            },
            // 复制JSON文本内容
            copyJson: function(){
                GM_setClipboard(this.$rawText.text())
                layer.msg('复制成功', {time: 1500})
            },
            // 全部折叠
            collapseAll: function(){
                if($('#formater').is(':visible')){
                    try{
                        $('.json-toggle').not('.collapsed').click()
                    }catch(e){}
                }else{
                    jm.collapse_all()
                }
            },
            // 全部展开
            expandAll: function(){
                if($('#formater').is(':visible')){
                    try{
                        $('a.json-placeholder').click().remove()
                    }catch(e){}
                }else{
                    jm.expand_all()
                    jm.scroll_node_to_center(jm.get_root())
                }
            },
            formaterJSON: function(){},
            // 显示JSON脑图
            showMind: function(){},
            // 查看原始JSON内容
            switchRawText: function(){
                this.$rawText.html(source.clone())
            },
            // 美化
            formaterRawText: function(){
                this.isFormater = !this.isFormater
                if(this.isFormater){
                    this.$rawText.find('pre').text(JSON.stringify(jsonObject, null, 2))
                }else{
                    this.switchRawText()
                }
            },
            init: function(){
                this.switchRawText()

                // 按钮点击事件
                $('.btn').click(e => {
                    const target = e.target, id = target.id
                    if(target.classList.contains('tabs-item')){
                        let index = $(target).index()
                        $(target).addClass('active').siblings().removeClass("active")
                        $('.tabs-container > div').removeClass("active").eq(index).addClass('active')

                        let rEl = $('#formaterRawText'), 
                            fEl= $('.searchbox'),
                            cEl= $('#copyJson'),
                            aEl = $('#collapseAll, #expandAll')
                        id === 'formaterJSON' ? fEl.show(): fEl.hide()
                        id === 'showMind' ? cEl.hide(): cEl.show()
                        id === 'switchRawText' ? (rEl.show() && aEl.hide()) : (rEl.hide() && aEl.show())
                    }
                    this[id](target)
                })

                return this
            }
        },
        jsonMind = {
            // JSON数据转换为jsMind所需要的数据结构
            convert: function(json){
                let children = []
                if(typeof json === 'object'){
                    for(let key in json){
                        let val = json[key], 
                            isArray = Array.isArray(val)

                        children.push({
                            isArray,
                            chain: key,
                            id: key + '_' + Math.random(),
                            topic: isArray ? `${key}<span class="mind-array">[${val.length}]</span>` : `${key}`,
                            // children: this.convert(val)
                            children: isArray ? this.convert(val[0]) : this.convert(val)
                        })
                    }
                }
                return children;
            },
            // 脑图节点调用链
            mindChain: function (node){
                let chain = node.data.chain
                if(!node.parent){
                    return chain
                }

                let parent = node.parent, parentChain = this.mindChain(parent)
                chain = parent.data.isArray ? `${parentChain}[0].${chain}` : `${parentChain}.${chain}`
                return chain
            },
            //  显示脑图
            show: function(json, isArr){
                jm.show({
                    "meta":{
                        "name":"JSON脑图",
                        "author":"[email protected]",
                        "version":"1.0"
                    },
                    "format":"node_tree",
                    /* 数据内容 */
                    "data": {
                        "id": "root",
                        "topic": 'Response',
                        "direction": "left",
                        "children": this.convert(json),
                        "chain": isArr ? 'Response[0]' : 'Response'
                    }
                })

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

                    switch(event.type){
                        case 'dblclick':
                            GM_setClipboard(jsonMind.mindChain(node))
                            layer.msg('节点路径复制成功', {time: 1500})
                            break;
                        case 'mouseover':
                            let s = `<b>节点路径(双击复制)</b><br/>${jsonMind.mindChain(node)}`
                            layer.tips(s, that, {
                                time: 0,
                                tips: [2, '#1e2732']
                            });
                            break;
                        default:
                            layer.closeAll()
                            break;
                    }
                })
                return this
            },
            init: function(json){
                let isArr = Array.isArray(json);
                if(isArr){
                    if(typeof json[0] !== 'object'){
                        layer.msg('数据结构无法生成脑图', {time: 1000})
                        return
                    }
                    json = json[0]
                }

                if(!window.jm){
                    window.jm = new jsMind({
                        mode :'side', 
                        editable: false,
                        container:'jmContainer',
                        view: {
                            hmargin: 50, // 思维导图距容器外框的最小水平距离
                            vmargin: 50,  // 思维导图距容器外框的最小垂直距离
                            engine: 'svg', // 思维导图各节点之间线条的绘制引擎
                            draggable: true, // 当容器不能完全容纳思维导图时,是否允许拖动画布代替鼠标滚动
                            support_html : false, 
                            line_color: '#C4C9D0',
                        },
                        layout: {
                            vspace: 7, // 节点之间的垂直间距
                            hspace: 150, // 节点之间的水平空间
                        },
                    });
                }

                this.show(json, isArr).event()
            }
        },
        otherOperate = {
            // 过滤 JSON
            filterJSON: function(filter) {
                if(!filter){
                    $('li').removeClass('hidden')
                    return
                }

                let chainSet= new Set()
                /**
                 * JSON key
                 * 假如 filter === i, querySelectorAll得到DOM节点
                 * 得到:['/feedList/0/images/0/user_id', '/feedList/0/images/0', '/feedList/0/images', '/feedList/0', '/feedList']
                 */
                document.querySelectorAll('#formater *[json-path]').forEach(el => {
                    let chain = $(el).attr('json-path')
                    if(!chain){
                        return
                    }
                    let newChain = chain.substr(chain.lastIndexOf('.'))
                    if(!newChain.toLowerCase().includes(filter.toLowerCase())){
                        return
                    }
                    chainSet.add(chain)
                    while(chain = chain.substr(0, chain.lastIndexOf('.'))){
                        chainSet.add(chain)
                    }
                })

                /**
                 * JSON value
                 */
                document.querySelectorAll("#formater *[class*='json-']:not([class*='json-key']):not([class*='json-brackets'])")
                .forEach(el =>{
                    let target = $(el), 
                        chain = target.parent().attr('json-path')
                    if(!chain){
                        return
                    }
                    let text = target.text()
                    if(!text.toLowerCase().includes(filter.toLowerCase())){
                        return
                    }
                    chainSet.add(chain)
                    while(chain = chain.substr(0, chain.lastIndexOf('.'))){
                        chainSet.add(chain)
                    }
                })

                $('li').addClass('hidden')
                chainSet.forEach(chain => {
                    $(`*[json-path="${chain}"]`).removeClass('hidden')
                })
            },
            // JSON 过滤
            input: function(){
                let that = this
                $('input').on('input', function(){
                    let val = $(this).val()
                    val === '' ? $('.clear').attr('hidden', true) : $('.clear').attr('hidden', false)
                    that.filterJSON(val)
                })
                return that
            },
            // 清空输入框内容
            clear: function(){
                let that = this
                $('.clear').click(function(){
                    that.filterJSON()
                    $('input').val('')
                    $(this).attr('hidden', true)
                })
                return this
            },
            // 返回顶部
            scrollTop: function(){
                $('.scroll-top').click(function(){
                    $('.tabs-container').animate({
                        scrollTop: '0'
                    }, 1000);
                })
                return this
            },
            urlHover:function(){
                // 所有a标签,看是否是图片,是图片生成预览图  
                $("a.json-string[href]").hover(function(){
                    var that = $(this), 
                        href = that.attr('href')
                    if(Utils.isImg(href)){
                        layer.tips(`<img src="${href}" />`, that, {
                            time: 0,
                            anim: 5,
                            maxWidth: 500,
                            tips: [3, '#d9d9d9']
                        });
                    }
                }, () => layer.closeAll())
                return this
            },
            jsonKeyHover: function(){
                $(".json-key").hover(function(){
                    var that = $(this), 
                        jsonPath = that.parent().attr('json-path')
                    if(jsonPath){
                        layer.tips(jsonPath, that, {
                            time: 0,
                            anim: 5,
                            maxWidth: 500,
                            tips: [1, '#1e2732']
                        });
                    }
                }, () => layer.closeAll())
                return this
            },
            init:function(){
                this.input().clear().scrollTop().urlHover().jsonKeyHover()
            }
        }

        $('#formater').jsonViewer(jsonObject, jsonpFun)
        btnEvent.init()
        otherOperate.init()
        jsonMind.init(jsonObject)

    })(_jQuery)
})();