/** * Json Page Automatic Format Via FeHelper * @author zhaoxianlie */ window.JsonAutoFormat = (() => { // 留100ms时间给静态文件加载,当然,这个代码只是留给未开发过程中用的 let pleaseLetJsLoaded = 0; let __importScript = (filename) => { pleaseLetJsLoaded = 100; let url = filename; if (location.protocol === 'chrome-extension:' || chrome.runtime && chrome.runtime.getURL) { url = chrome.runtime.getURL('json-format/' + filename); } // 使用chrome.runtime.sendMessage向background请求加载脚本 chrome.runtime.sendMessage({ type: 'fh-dynamic-any-thing', thing: 'load-local-script', script: url }, (scriptContent) => { if (!scriptContent) { return; } // 如果有evalCore则使用它 if (window.evalCore && window.evalCore.getEvalInstance) { try { window.evalCore.getEvalInstance(window)(scriptContent); } catch(e) { } } else { // 创建一个函数来执行脚本 try { // 使用Function构造函数创建一个函数,并在当前窗口上下文中执行 // 这比动态创建script元素更安全,因为它不涉及DOM操作 const executeScript = new Function(scriptContent); executeScript.call(window); } catch(e) { } } }); }; // 加载所需脚本 __importScript('json-bigint.js'); __importScript('format-lib.js'); __importScript('json-abc.js'); __importScript('json-decode.js'); const JSON_SORT_TYPE_KEY = 'json_sort_type_key'; // 本地永久存储的key const STORAGE_KEYS = { // 总是开启JSON自动格式化功能 JSON_PAGE_FORMAT: 'JSON_PAGE_FORMAT', // 总是显示顶部工具栏 JSON_TOOL_BAR_ALWAYS_SHOW: 'JSON_TOOL_BAR_ALWAYS_SHOW', // 启用底部状态栏 STATUS_BAR_ALWAYS_SHOW: 'STATUS_BAR_ALWAYS_SHOW', // 自动进行URL、Unicode解码 AUTO_TEXT_DECODE: 'AUTO_TEXT_DECODE', // 修正乱码 FIX_ERROR_ENCODING: 'FIX_ERROR_ENCODING', // 启用JSON key排序功能 ENABLE_JSON_KEY_SORT: 'ENABLE_JSON_KEY_SORT', // 保留键值双引号 KEEP_KEY_VALUE_DBL_QUOTE: 'KEEP_KEY_VALUE_DBL_QUOTE', // 最大json key数量 MAX_JSON_KEYS_NUMBER: 'MAX_JSON_KEYS_NUMBER', // 自定义皮肤 JSON_FORMAT_THEME: 'JSON_FORMAT_THEME' }; // 皮肤定义 const SKIN_THEME = { '0': 'theme-default', '1': 'theme-simple', '2': 'theme-light', '3': 'theme-dark', '4': 'theme-vscode', '5': 'theme-github', '6': 'theme-vegetarian' }; let cssInjected = false; // JSONP形式下的callback name let funcName = null; let fnTry = null; let fnCatch = null; // 工具栏是否显示 let showToolBar = true; // 格式化的配置 let formatOptions = { JSON_FORMAT_THEME: 0, sortType: 0, autoDecode: false, originalSource: '' }; // 获取JSON格式化的配置信息 let _getAllOptions = (success) => { chrome.runtime.sendMessage({ type: 'fh-dynamic-any-thing', thing:'request-jsonformat-options', params: STORAGE_KEYS }, result => success(result)); }; let _getHtmlFragment = () => { // 判断当前地区是否在美国 const isInUSA = () => { // 通过时区判断是否在美国 const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const isUSTimeZone = /^America\/(New_York|Chicago|Denver|Los_Angeles|Anchorage|Honolulu)/.test(timeZone); // 通过语言判断 const language = navigator.language || navigator.userLanguage; const isUSLanguage = language.toLowerCase().indexOf('en-us') > -1; // 如果时区和语言都符合美国特征,则认为在美国 return (isUSTimeZone && isUSLanguage); }; return [ '', '
格式化中...
', '
', '
', '
', '
',
            '
', '
' ].join('') }; let _createSettingPanel = () => { let html = `

基本配置项

自定义皮肤

`; let sPanel = $('#jfSettingPanel'); if (!sPanel.length) { sPanel = $(html).appendTo('#jfToolbar'); // 表单提交时,保存数据 sPanel.find('input[type="submit"]').on('click', function (e) { e.preventDefault(); e.stopPropagation(); let formData = {}; formData.JSON_PAGE_FORMAT = sPanel.find('input[name="alwaysOn"]').prop('checked'); formData.JSON_TOOL_BAR_ALWAYS_SHOW = sPanel.find('input[name="alwaysShowToolbar"]').prop('checked'); formData.STATUS_BAR_ALWAYS_SHOW = sPanel.find('input[name="alwaysShowStatusbar"]').prop('checked'); formData.AUTO_TEXT_DECODE = sPanel.find('input[name="autoDecode"]').prop('checked'); formData.FIX_ERROR_ENCODING = sPanel.find('input[name="errorEncoding"]').prop('checked'); formData.ENABLE_JSON_KEY_SORT = sPanel.find('input[name="enableSort"]').prop('checked'); formData.KEEP_KEY_VALUE_DBL_QUOTE = sPanel.find('input[name="keepQuote"]').prop('checked'); formData.MAX_JSON_KEYS_NUMBER = sPanel.find('input[name="maxlength"]').val(); formData.JSON_FORMAT_THEME = sPanel.find('input[name="skinId"]:checked').val(); // 同步更新当前页面的formatOptions对象 Object.keys(formData).forEach(key => { formatOptions[key] = formData[key]; }); chrome.runtime.sendMessage({ type: 'fh-dynamic-any-thing', thing: 'save-jsonformat-options', params: formData }, result => { sPanel.hide(); // 重新应用格式化以展示最新设置 _didFormat(); }); }); sPanel.find('input[name="alwaysShowToolbar"]').on('click', function (e) { $('.fe-feedback #toggleBtn').trigger('click'); }); sPanel.find('input[name="errorEncoding"]').on('click', function (e) { let el = $('#jfToolbar').find('.x-fix-encoding'); $(this).prop('checked') ? el.show() : el.hide(); }); sPanel.find('input[name="enableSort"]').on('click', function (e) { let el = $('#jfToolbar').find('.x-sort'); $(this).prop('checked') ? el.show() : el.hide(); }); sPanel.find('input[type="reset"]').on('click', (e) => sPanel.hide()); sPanel.find('input[name="skinId"]').on('click', function (e) { formatOptions.JSON_FORMAT_THEME = this.value; _didFormat(); }); sPanel.find('input[name="alwaysShowStatusbar"]').on('click', function (e) { formatOptions.STATUS_BAR_ALWAYS_SHOW = $(this).prop('checked'); let elBody = $('body'); if (formatOptions.STATUS_BAR_ALWAYS_SHOW) { elBody.removeClass('hide-status-bar'); } else { elBody.addClass('hide-status-bar'); } }); sPanel.find('input[name="keepQuote"]').on('click', function (e) { formatOptions.KEEP_KEY_VALUE_DBL_QUOTE = $(this).prop('checked'); let elBody = $('body'); if (formatOptions.KEEP_KEY_VALUE_DBL_QUOTE) { elBody.removeClass('remove-quote'); } else { elBody.addClass('remove-quote'); } }); } else if (sPanel[0].offsetHeight) { return sPanel.hide(); } else { sPanel.show(); } _getAllOptions(result => { result.JSON_PAGE_FORMAT && sPanel.find('input[name="alwaysOn"]').prop('checked', true); result.JSON_TOOL_BAR_ALWAYS_SHOW && sPanel.find('input[name="alwaysShowToolbar"]').prop('checked', true); result.STATUS_BAR_ALWAYS_SHOW && sPanel.find('input[name="alwaysShowStatusbar"]').prop('checked', true); result.AUTO_TEXT_DECODE && sPanel.find('input[name="autoDecode"]').prop('checked', true); result.FIX_ERROR_ENCODING && sPanel.find('input[name="errorEncoding"]').prop('checked', true); result.ENABLE_JSON_KEY_SORT && sPanel.find('input[name="enableSort"]').prop('checked', true); result.KEEP_KEY_VALUE_DBL_QUOTE && sPanel.find('input[name="keepQuote"]').prop('checked', true); sPanel.find('input[name="maxlength"]').attr('value', result.MAX_JSON_KEYS_NUMBER || 10000); sPanel.find(`input[name="skinId"][value="${result.JSON_FORMAT_THEME || 0}"]`).attr('checked', true); }); }; // 检测当前页面的CSP,防止出现这种情况: // DOMException: Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag. let _checkContentSecurityPolicy = () => { try { localStorage.getItem(1); } catch (e) { return false; } return true; }; let _initToolbar = () => { showToolBar = formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW; let cspSafe = _checkContentSecurityPolicy(); if (cspSafe) { // =============================排序:获取上次记录的排序方式 if (formatOptions.ENABLE_JSON_KEY_SORT) { formatOptions.sortType = parseInt(localStorage.getItem(JSON_SORT_TYPE_KEY) || 0); // 排序选项初始化 $('[name=jsonsort][value=' + formatOptions.sortType + ']').attr('checked', 1); } else { formatOptions.sortType = 0; $('#jfToolbar .x-sort').hide(); } // =============================事件初始化 $('[name=jsonsort]').click(function (e) { let sortType = parseInt(this.value); if (sortType !== formatOptions.sortType) { formatOptions.sortType = sortType; _didFormat(); } localStorage.setItem(JSON_SORT_TYPE_KEY, sortType); }); } else { $('#jfToolbar .x-sort').hide(); } // =============================乱码修正 if (!formatOptions.FIX_ERROR_ENCODING) { $('#jfToolbar .x-fix-encoding').hide(); } // =============================工具栏的显示与隐藏控制 let toolBarClassList = document.querySelector('#jfToolbar').classList; let tgBtn = $('.fe-feedback #toggleBtn'); if (formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW) { toolBarClassList.remove('t-collapse'); tgBtn.html('隐藏>>'); } else { toolBarClassList.add('t-collapse'); tgBtn.html('<<'); } tgBtn.click(function (e) { e.preventDefault(); e.stopPropagation(); let toolBarClassList = document.querySelector('#jfToolbar').classList; showToolBar = !showToolBar; if (showToolBar) { toolBarClassList.remove('t-collapse'); tgBtn.html('隐藏>>'); formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW = true; } else { toolBarClassList.add('t-collapse'); tgBtn.html('<<'); formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW = false; } $('#jfToolbar input[name="alwaysShowToolbar"]').prop('checked', showToolBar); }); $('.fe-feedback .x-other-tools').on('click', function (e) { chrome.runtime.sendMessage({ type: 'fh-dynamic-any-thing', thing: 'open-options-page' }); }); $('.fe-feedback .x-settings').click(e => _createSettingPanel()); $('#jsonGetCorrectCnt').click(e => _getCorrectContent()); $('.x-toolbar .x-donate-link').on('click', function (e) { chrome.runtime.sendMessage({ type: 'fh-dynamic-any-thing', thing: 'open-donate-modal', params: { toolName: 'json-format' } }); }); }; let _didFormat = function () { let source = formatOptions.originalSource; if (formatOptions.sortType !== 0) { let jsonObj = JsonABC.sortObj(JSON.parse(formatOptions.originalSource), parseInt(formatOptions.sortType), true); source = JSON.stringify(jsonObj); } let elBody = $('body'); let theme = SKIN_THEME[formatOptions.JSON_FORMAT_THEME || 0]; Object.values(SKIN_THEME).forEach(th => elBody.removeClass(th)); elBody.addClass(theme); // 控制引号 if (formatOptions.KEEP_KEY_VALUE_DBL_QUOTE) { elBody.removeClass('remove-quote'); } else { elBody.addClass('remove-quote'); } // 控制底部状态栏 if (formatOptions.STATUS_BAR_ALWAYS_SHOW) { elBody.removeClass('hide-status-bar'); } else { elBody.addClass('hide-status-bar'); } if (formatOptions.autoDecode) { (async () => { let txt = await JsonEnDecode.urlDecodeByFetch(source); source = JsonEnDecode.uniDecode(txt); // 格式化 try { await Formatter.format(source, theme); } catch (e) { await Formatter.formatSync(source, theme) } $('#jfToolbar').show(); })(); } else { (async () => { // 格式化 try { await Formatter.format(source, theme); } catch (e) { await Formatter.formatSync(source, theme) } $('#jfToolbar').show(); })(); } // 如果是JSONP格式的,需要把方法名也显示出来 if (funcName != null) { if (fnTry && fnCatch) { $('#jfCallbackName_start').html('
' + fnTry + '
' + funcName + '('); $('#jfCallbackName_end').html(')
' + fnCatch + '
'); } else { $('#jfCallbackName_start').html(funcName + '('); $('#jfCallbackName_end').html(')'); } } // 埋点:自动触发json-format-auto chrome.runtime.sendMessage({ type: 'fh-dynamic-any-thing', thing: 'statistics-tool-usage', params: { tool_name: 'json-format', url: location.href } }); }; let _getCorrectContent = function () { fetch(location.href).then(res => res.text()).then(text => { formatOptions.originalSource = text; _didFormat(); }); }; /** * 从一个dom节点去获取json内容,这里面有很多的判断 */ let _getJsonContentFromDOM = function (dom) { let source = dom.textContent.trim(); if (!source && document.body) { source = (document.body.textContent || '').trim() } if (!source) { return false; } // 1、如果body的内容还包含HTML标签,肯定不是合法的json了 // 2、如果是合法的json,也只可能有一个text节点 // 3、但是要兼容一下其他插件对页面的破坏情况 // 4、对于content-type是application/json的页面可以做宽松处理 let nodes = document.body.childNodes; let jsonText = ''; let isJsonContentType = document.contentType === 'application/json'; for (let i = 0, len = nodes.length; i < len; i++) { let elm = nodes[i]; if (elm.nodeType === Node.TEXT_NODE) { jsonText += (elm.textContent || '').trim(); } else if (isJsonContentType) { if ((elm.offsetHeight + elm.offsetWidth !== 0) && elm.textContent.length > jsonText.length) { jsonText = elm.textContent; } } else { if (nodes[i].nodeType === Node.ELEMENT_NODE) { let tagName = elm.tagName.toLowerCase(); let text = (elm.textContent || '').trim(); // 如果包含了script和link标签,需要看标签的src和href属性值,如果不是chrome-extensions注入的,也要跳出 if (['script', 'link'].includes(tagName)) { let url = elm.getAttribute('src') || elm.getAttribute('href'); if (!!url && !/^chrome\-extension:\/\//.test(url)) { return false; } } // 如果不是pre标签,并且还不是隐藏节点,且内容不为空,也要跳出 else if (tagName !== 'pre' && (elm.offsetWidth + elm.offsetHeight !== 0 && !!text)) { return false; } // 如果是pre标签,但当前节点内容与最初body.textContent提取值不一致,都跳出 else if (tagName === 'pre' && text !== source) { return false; } } else { return false; } } } return (jsonText || '').trim() || source; }; /** * 从页面提取JSON文本 * @returns {string} * @private */ let _getJsonText = function () { // 如果是js内容,则不进行json格式化 let isJs = /\.js$/.test(new URL(location.href).pathname); isJs = isJs && document.contentType === 'application/javascript'; if (isJs) { return false; } // 如果是 HTML 页面,也要看一下内容是不是明显就是个JSON,如果不是,则也不进行 json 格式化 if (document.contentType === 'text/html' && document.body) { // 使用 DOMParser 解析 HTML const parser = new DOMParser(); const doc = parser.parseFromString(document.body.outerHTML, "text/html"); // 移除不需要的标签 doc.querySelectorAll('style, script').forEach(el => el.remove()); // 获取清理后的文本 const cleanText = doc.body.textContent; let jsonObj = _getJsonObject(cleanText); if(!jsonObj) { return false; } } let pre = document.querySelectorAll('body>pre')[0] || {textContent: ""}; return _getJsonContentFromDOM(pre); }; /** * 获取一个JSON的所有Key数量 * @param json * @returns {number} * @private */ let _getAllKeysCount = function (json) { let count = 0; if (typeof json === 'object') { let keys = Object.keys(json); count += keys.length; keys.forEach(key => { if (json[key] && typeof json[key] === 'object') { count += _getAllKeysCount(json[key]); } }); } return count; }; // 用新的options来覆盖默认options let _extendsOptions = options => { options = options || {}; Object.keys(options).forEach(opt => formatOptions[opt] = options[opt]); }; /** * 判断字符串参数是否为一个合法的json,如果是则返回json对象 * @param {*} source * @returns */ let _getJsonObject = function (source) { let jsonObj = null; // 下面校验给定字符串是否为一个合法的json try { // 再看看是不是jsonp的格式 let reg = /^([\w\.]+)\(\s*([\s\S]*)\s*\)$/m; // 优化后的 try/catch 包裹处理 fnTry = null; fnCatch = null; // 处理开头 if (source.startsWith('try {')) { fnTry = 'try {'; source = source.slice(5).trimStart(); } // 处理结尾 let catchIdx = source.lastIndexOf('} catch'); if (catchIdx !== -1) { // 找到最后一个 } catch,截取到末尾 fnCatch = source.slice(catchIdx); source = source.slice(0, catchIdx).trimEnd(); } // 只做一次正则匹配 let matches = reg.exec(source); if (matches != null && (fnTry && fnCatch || !fnTry && !fnCatch)) { funcName = matches[1]; source = matches[2]; } else { reg = /^([\{\[])/; if (!reg.test(source)) { return; } } // 这里可能会throw exception jsonObj = JSON.parse(source); } catch (ex) { // new Function的方式,能自动给key补全双引号,但是不支持bigint,所以是下下策,放在try-catch里搞 try { jsonObj = new Function("return " + source)(); } catch (exx) { try { // 再给你一次机会,是不是下面这种情况: "{\"ret\":\"0\", \"msg\":\"ok\"}" jsonObj = new Function("return '" + source + "'")(); if (typeof jsonObj === 'string') { try { // 确保bigint不会失真 jsonObj = JSON.parse(jsonObj); } catch (ie) { // 最后给你一次机会,是个字符串,老夫给你再转一次 jsonObj = new Function("return " + jsonObj)(); } } } catch (exxx) { return; } } } try { // 要尽量保证格式化的东西一定是一个json,所以需要把内容进行JSON.stringify处理 source = JSON.stringify(jsonObj); } catch (ex) { // 通过JSON反解不出来的,一定有问题 return; } return jsonObj; }; /** * 根据最终拿到的json source,对页面进行格式化操作 * @param {*} source * @returns */ let _formatTheSource = function (source) { let jsonObj = _getJsonObject(source); // 是json格式,可以进行JSON自动格式化 if (jsonObj != null && typeof jsonObj === "object") { // 提前注入css if(!cssInjected) { chrome.runtime.sendMessage({ type: 'fh-dynamic-any-thing', thing:'inject-content-css', tool: 'json-format' }); cssInjected = true; } // JSON的所有key不能超过预设的值,比如 10000 个,要不然自动格式化会比较卡 if (formatOptions['MAX_JSON_KEYS_NUMBER']) { let keysCount = _getAllKeysCount(jsonObj); if (keysCount > formatOptions['MAX_JSON_KEYS_NUMBER']) { let msg = '当前JSON共 ' + keysCount + ' 个Key,大于预设值' + formatOptions['MAX_JSON_KEYS_NUMBER'] + ',已取消自动格式化;可到FeHelper设置页调整此配置!'; return toast(msg); } } $('html').addClass('fh-jf'); $('body').prepend(_getHtmlFragment()); let preLength = $('body>pre').remove().length; if (!preLength) { Array.prototype.slice.call(document.body.childNodes).forEach(node => { (node.nodeType === Node.TEXT_NODE) && node.remove(); }); } formatOptions.originalSource = JSON.stringify(jsonObj); // 确保从storage加载最新设置 _getAllOptions(options => { _extendsOptions(options); _initToolbar(); _didFormat(); }); } }; /** * 执行format操作 * @private */ let _format = function () { let source = _getJsonText(); if (source) { _formatTheSource(source); } }; // 页面加载后自动采集 try { if (window.chrome && chrome.runtime && chrome.runtime.sendMessage && window.Awesome && window.Awesome.collectAndSendClientInfo) { window.Awesome.collectAndSendClientInfo(); } else { // fallback: 动态加载Awesome模块 import(chrome.runtime.getURL('background/awesome.js')).then(module => { module.default.collectAndSendClientInfo(); }).catch(() => {}); } } catch(e) {} return { format: () => _getAllOptions(options => { if(options.JSON_PAGE_FORMAT) { let intervalId = setTimeout(() => { if(typeof Formatter !== 'undefined') { clearInterval(intervalId); // 加载所有保存的配置 _extendsOptions(options); // 应用格式化 _format(); } },pleaseLetJsLoaded); } }) }; })(); if(location.protocol !== 'chrome-extension:') { (async () => { await window.JsonAutoFormat.format(); })(); }