Greasy Fork is available in English.
类名添加miw_前缀 + 样式作用域隔离 + 修复Chromium主题 + 避免普通网页误解析JSON
// ==UserScript== // @name JSON Formatter // @namespace https://github.com/askmiw // @author [email protected] // @description 类名添加miw_前缀 + 样式作用域隔离 + 修复Chromium主题 + 避免普通网页误解析JSON // @version 1.0.0 // @match *://*/* // @match file:///* // @grant GM_addStyle // @grant GM_registerMenuCommand // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 配置项 let CONFIG = { AUTO_FORMAT: true, DARK_MODE: true, COLLAPSE_LEVEL: Infinity, SHOW_LINE_NUMBERS: false, ENABLE_COPY: true, MIN_JSON_LENGTH: 10, MAX_RENDER_SIZE: 50000, SHOW_NODE_COUNT: true }; // 动态样式管理(所有类名带miw_前缀) let styleElement = null; let globalFormatter = null; let mutationObserver = null; // ========== 核心修改:所有样式类名添加miw_前缀 ========== function updateStyles() { // 移除旧样式 if (styleElement && styleElement.parentNode) { styleElement.parentNode.removeChild(styleElement); } styleElement = document.createElement('style'); const lineNumberStyles = CONFIG.SHOW_LINE_NUMBERS ? ` .miw_json-formatter-wrapper .miw_line-number { display: inline-block !important; min-width: 30px !important; width: 30px !important; text-align: right !important; margin-right: 10px !important; color: ${CONFIG.DARK_MODE ? '#6a9955' : '#999'} !important; user-select: none !important; padding: 0 !important; } ` : ` .miw_json-formatter-wrapper .miw_line-number { display: none !important; min-width: 0px !important; width: 0px !important; margin-right: 0px !important; padding: 0 !important; visibility: hidden !important; opacity: 0 !important; } `; // 主题样式(类名带miw_前缀) const themeStyles = CONFIG.DARK_MODE ? ` .miw_json-formatter-wrapper { background-color: #1e1e1e !important; color: #e0e0e0 !important; } .miw_json-formatter-wrapper .miw_json-toolbar { background: rgba(30, 30, 30, 0.95) !important; border-color: #444 !important; } .miw_json-formatter-wrapper .miw_toolbar-button { background: #2d2d2d !important; border-color: #555 !important; color: #e0e0e0 !important; } .miw_json-formatter-wrapper .miw_search-box { background: #2d2d2d !important; border-color: #555 !important; color: #e0e0e0 !important; } .miw_json-formatter-wrapper .miw_search-nav-btn { background: #2d2d2d !important; border-color: #555 !important; color: #e0e0e0 !important; } .miw_json-formatter-wrapper .miw_json-raw-view { background: #2d2d2d !important; border-color: #444 !important; color: #e0e0e0 !important; } ` : ` .miw_json-formatter-wrapper { background-color: #ffffff !important; color: #333333 !important; } .miw_json-formatter-wrapper .miw_json-toolbar { background: rgba(255, 255, 255, 0.95) !important; border-color: #ddd !important; } .miw_json-formatter-wrapper .miw_toolbar-button { background: #f5f5f5 !important; border-color: #ddd !important; color: #333 !important; } .miw_json-formatter-wrapper .miw_search-box { background: #fff !important; border-color: #ddd !important; color: #333 !important; } .miw_json-formatter-wrapper .miw_search-nav-btn { background: #f5f5f5 !important; border-color: #ddd !important; color: #333 !important; } .miw_json-formatter-wrapper .miw_json-raw-view { background: #f9f9f9 !important; border-color: #ddd !important; color: #333 !important; } `; // 所有样式类名均添加miw_前缀,且通过后代选择器限定作用域 styleElement.textContent = ` /* 核心容器(带miw_前缀) */ .miw_json-formatter-wrapper { font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important; min-height: 100vh !important; padding: 20px !important; line-height: 1.4 !important; box-sizing: border-box !important; position: relative !important; margin: 0 !important; width: 100% !important; } ${themeStyles} .miw_json-formatter-wrapper .miw_json-toolbar { position: fixed !important; top: 10px !important; right: 10px !important; border-radius: 6px !important; padding: 8px 12px !important; z-index: 99999 !important; display: flex !important; gap: 6px !important; align-items: center !important; backdrop-filter: blur(10px) !important; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2) !important; flex-wrap: wrap !important; } .miw_json-formatter-wrapper .miw_toolbar-button { padding: 4px 8px !important; border-radius: 4px !important; cursor: pointer !important; font-size: 12px !important; transition: all 0.2s !important; } .miw_json-formatter-wrapper .miw_toolbar-button:hover { background: ${CONFIG.DARK_MODE ? '#3d3d3d' : '#e8e8e8'} !important; border-color: #007acc !important; } .miw_json-formatter-wrapper .miw_toolbar-button.active { background: #007acc !important; color: white !important; border-color: #007acc !important; } /* 搜索区域(带miw_前缀) */ .miw_json-formatter-wrapper .miw_search-container { display: flex !important; align-items: center !important; gap: 4px !important; } .miw_json-formatter-wrapper .miw_search-box { padding: 4px 8px !important; border-radius: 4px !important; font-size: 12px !important; width: 180px !important; outline: none !important; } .miw_json-formatter-wrapper .miw_search-nav-btn { padding: 4px 6px !important; border-radius: 4px !important; cursor: pointer !important; font-size: 11px !important; width: 24px !important; height: 24px !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.2s !important; } .miw_json-formatter-wrapper .miw_search-nav-btn:hover { background: ${CONFIG.DARK_MODE ? '#3d3d3d' : '#e8e8e8'} !important; border-color: #007acc !important; } .miw_json-formatter-wrapper .miw_search-count { font-size: 11px !important; color: ${CONFIG.DARK_MODE ? '#aaa' : '#666'} !important; min-width: 40px !important; text-align: center !important; } .miw_json-formatter-wrapper .miw_json-notification { position: fixed !important; bottom: 20px !important; left: 50% !important; transform: translateX(-50%) !important; background: #007acc !important; color: white !important; padding: 8px 16px !important; border-radius: 4px !important; opacity: 0 !important; transition: opacity 0.3s !important; pointer-events: none !important; z-index: 99999 !important; } .miw_json-formatter-wrapper .miw_json-notification.show { opacity: 1 !important; } /* JSON布局样式(带miw_前缀) */ .miw_json-formatter-wrapper .miw_json-item { margin: 2px 0 !important; } .miw_json-formatter-wrapper .miw_json-line { display: flex !important; margin: 1px 0 !important; position: relative !important; align-items: flex-start !important; } /* 动态行号样式(带miw_前缀) */ ${lineNumberStyles} .miw_json-formatter-wrapper .miw_line-content { flex: 1 !important; width: 100% !important; } .miw_json-formatter-wrapper .miw_json-collapsible { cursor: pointer !important; position: relative !important; padding-left: 16px !important; user-select: none !important; display: inline-block !important; } .miw_json-formatter-wrapper .miw_json-collapsible::before { content: '▶' !important; position: absolute !important; left: 0 !important; font-size: 10px !important; transition: transform 0.2s !important; } .miw_json-formatter-wrapper .miw_json-collapsible.expanded::before { content: '▼' !important; } /* 节点数样式(带miw_前缀) */ .miw_json-formatter-wrapper .miw_json-node-count { color: ${CONFIG.DARK_MODE ? '#888' : '#999'} !important; font-style: italic !important; margin-left: 5px !important; font-size: 11px !important; } .miw_json-formatter-wrapper .miw_json-children { margin-left: 20px !important; border-left: 1px dashed ${CONFIG.DARK_MODE ? '#444' : '#ddd'} !important; padding-left: 10px !important; } .miw_json-formatter-wrapper .miw_json-key { color: ${CONFIG.DARK_MODE ? '#9cdcfe' : '#881391'} !important; font-weight: bold !important; } .miw_json-formatter-wrapper .miw_json-string { color: ${CONFIG.DARK_MODE ? '#ce9178' : '#c41a16'} !important; } .miw_json-formatter-wrapper .miw_json-number { color: ${CONFIG.DARK_MODE ? '#b5cea8' : '#1c00cf'} !important; } .miw_json-formatter-wrapper .miw_json-boolean { color: ${CONFIG.DARK_MODE ? '#569cd6' : '#0b22b9'} !important; } .miw_json-formatter-wrapper .miw_json-null { color: ${CONFIG.DARK_MODE ? '#569cd6' : '#0b22b9'} !important; } .miw_json-formatter-wrapper .miw_json-punctuation { color: ${CONFIG.DARK_MODE ? '#d4d4d4' : '#333'} !important; margin: 0 2px !important; } .miw_json-formatter-wrapper .miw_json-ellipsis { color: ${CONFIG.DARK_MODE ? '#888' : '#999'} !important; font-style: italic !important; margin-left: 5px !important; } /* 搜索高亮(带miw_前缀) */ .miw_json-formatter-wrapper .miw_search-highlight { background-color: rgba(255, 255, 0, 0.4) !important; border-radius: 2px !important; padding: 1px 0 !important; } .miw_json-formatter-wrapper .miw_search-highlight.current { background-color: rgba(255, 165, 0, 0.6) !important; color: #000 !important; font-weight: bold !important; } .miw_json-formatter-wrapper .miw_error-container { background: ${CONFIG.DARK_MODE ? 'rgba(255, 0, 0, 0.1)' : 'rgba(255, 255, 255, 0.05)'} !important; border: 1px solid #ff4444 !important; border-radius: 6px !important; padding: 20px !important; margin: 20px !important; font-family: monospace !important; } .miw_json-formatter-wrapper .miw_error-title { color: #ff4444 !important; font-size: 16px !important; margin-bottom: 10px !important; font-weight: bold !important; } /* 原文显示样式(带miw_前缀) */ .miw_json-formatter-wrapper .miw_json-raw-view { border-radius: 4px !important; padding: 20px !important; margin: 20px 0 !important; font-family: 'Courier New', monospace !important; white-space: pre-wrap !important; word-break: break-all !important; max-height: calc(100vh - 80px) !important; overflow-y: auto !important; font-size: 13px !important; line-height: 1.5 !important; } /* 视图切换容器(带miw_前缀) */ .miw_json-formatter-wrapper .miw_view-container { width: 100% !important; height: 100% !important; } `; // 仅在脚本激活时添加样式 if (globalFormatter?.isActive) { document.head.appendChild(styleElement); } } // JSON提取类(逻辑不变) class JSONExtractor { static extractJSON(content) { if (!content || content.length < CONFIG.MIN_JSON_LENGTH) return null; try { const parsed = JSON.parse(content); if (parsed && (typeof parsed === 'object' || Array.isArray(parsed))) { return content.trim(); } } catch (e) {} const bestMatch = this.findCompleteJson(content); if (bestMatch) { try { const parsed = JSON.parse(bestMatch); if (parsed && (typeof parsed === 'object' || Array.isArray(parsed))) { return bestMatch; } } catch (e) {} } try { const repaired = this.safeRepair(content); const parsed = JSON.parse(repaired); if (parsed && (typeof parsed === 'object' || Array.isArray(parsed))) { return repaired; } } catch (e) {} return null; } static findCompleteJson(content) { const jsonStartIndices = []; for (let i = 0; i < content.length; i++) { if (content[i] === '{' || content[i] === '[') jsonStartIndices.push(i); } for (const start of jsonStartIndices) { let stack = [], inString = false, escaped = false, end = start; for (let i = start; i < content.length; i++) { const char = content[i]; if (escaped) { escaped = false; continue; } if (char === '\\') { escaped = true; continue; } if (char === '"') { inString = !inString; continue; } if (!inString) { if (char === '{' || char === '[') stack.push(char); else if (char === '}' || char === ']') { if (stack.length === 0) break; const last = stack.pop(); if ((last === '{' && char !== '}') || (last === '[' && char !== ']')) { stack.length = 0; break; } } } if (stack.length === 0 && !inString) { end = i; const candidate = content.substring(start, end + 1).trim(); try { const parsed = JSON.parse(candidate); if (parsed && (typeof parsed === 'object' || Array.isArray(parsed))) { return candidate; } } catch (e) { continue; } } } } return null; } static safeRepair(content) { let repaired = content.trim(); repaired = repaired.replace(/^\ufeff/, '') .replace(/<!--[\s\S]*?-->/g, '') .replace(/\/\/[^\r\n]*/g, '') .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/,\s*([}\]])/g, '$1') .replace(/[\x00-\x1F\x7F]/g, '') .replace(/^[a-zA-Z_$][a-zA-Z0-9_$]*\(\s*([\s\S]*?)\s*\);?$/, '$1'); return repaired; } static isJsonResponse() { const contentType = (document.contentType || '').toLowerCase(); if (contentType.includes('text/html')) return false; const validTypes = ['application/json', 'text/json', 'application/javascript', 'text/javascript', 'application/ld+json', 'text/plain']; const hasValidType = validTypes.some(type => contentType.includes(type)); const bodyTrimmed = document.body.textContent?.trim() || ''; const hasJsonStructure = (bodyTrimmed.startsWith('{') || bodyTrimmed.startsWith('[')) && !bodyTrimmed.startsWith('<html') && !bodyTrimmed.startsWith('<head') && !bodyTrimmed.startsWith('<body'); if (!hasValidType && !hasJsonStructure) return false; const extracted = this.extractJSON(bodyTrimmed); return !!extracted; } static isNonJsonFile() { const url = window.location.href.toLowerCase(); const nonJsonExts = ['.js', '.ts', '.vue', '.jsx', '.tsx', '.php', '.py']; return nonJsonExts.some(ext => url.endsWith(ext)); } } // JSON格式化核心类(所有元素类名添加miw_前缀) class LocalJSONFormatter { constructor() { this.container = null; this.jsonData = null; this.originalContent = null; this.searchTerm = ''; this.lineCounter = 1; this.isActive = false; this.isRendering = false; this.matchNodes = []; this.currentMatchIndex = -1; this.contentWrapper = null; this.isRawView = false; } isJsonContent() { if (this.isActive || document.querySelector('.miw_json-formatter-wrapper')) return false; return JSONExtractor.isJsonResponse(); } getPageContent() { if (document.querySelector('.miw_json-formatter-wrapper')) return null; let content = ''; const preCodeTags = document.querySelectorAll('pre, code'); for (const el of preCodeTags) { const text = el.textContent?.trim() || ''; if (text.length >= CONFIG.MIN_JSON_LENGTH) { content = text; break; } } if (!content) { content = document.body.textContent?.trim() || ''; } this.originalContent = content; const jsonContent = JSONExtractor.extractJSON(content); return jsonContent || content; } parseJsonSafely(content) { if (!content) throw new Error('待解析内容为空'); try { const parsed = JSON.parse(content); if (parsed && (typeof parsed === 'object' || Array.isArray(parsed))) { return parsed; } return null; } catch (e) { return null; } } getLineNumber() { const num = this.lineCounter++; if (!CONFIG.SHOW_LINE_NUMBERS) { return ''; } return `<span class="miw_line-number">${num}</span>`; } formatJson(data, depth = 0) { let html = ''; if (this.isRendering && this.lineCounter > CONFIG.MAX_RENDER_SIZE) { return `<span class="miw_json-ellipsis">数据过大,已截断显示</span>`; } switch (true) { case data === null: html = `<span class="miw_json-null">null</span>`; break; case typeof data === 'boolean': html = `<span class="miw_json-boolean">${data}</span>`; break; case typeof data === 'number': html = `<span class="miw_json-number">${data}</span>`; break; case typeof data === 'string': { const escaped = this.escapeHtml(data); html = `<span class="miw_json-string">"${escaped}"</span>`; break; } case Array.isArray(data): html = this.formatArray(data, depth); break; case typeof data === 'object': html = this.formatObject(data, depth); break; default: html = `<span class="miw_json-string">${this.escapeHtml(String(data))}</span>`; } return html; } formatArray(data, depth) { if (data.length === 0) return `<span class="miw_json-punctuation">[</span><span class="miw_json-punctuation">]</span>`; const isCollapsed = depth >= CONFIG.COLLAPSE_LEVEL; const nodeCount = CONFIG.SHOW_NODE_COUNT ? `<span class="miw_json-node-count">${data.length} 项</span>` : ''; let html = `<div class="miw_json-line">${this.getLineNumber()}<div class="miw_line-content">`; html += `<div class="miw_json-collapsible ${isCollapsed ? '' : 'expanded'}" data-depth="${depth}" data-type="array">`; html += `<span class="miw_json-punctuation">[</span>${nodeCount}`; html += `</div></div></div>`; html += `<div class="miw_json-children" ${isCollapsed ? 'style="display:none;"' : ''}>`; data.forEach((item, idx) => { html += `<div class="miw_json-line">${this.getLineNumber()}<div class="miw_line-content">`; html += this.formatJson(item, depth + 1); if (idx < data.length - 1) html += `<span class="miw_json-punctuation">,</span>`; html += `</div></div>`; }); html += `</div>`; html += `<div class="miw_json-line">${this.getLineNumber()}<div class="miw_line-content">`; html += `<span class="miw_json-punctuation">]</span>`; html += `</div></div>`; return html; } formatObject(data, depth) { const keys = Object.keys(data); if (keys.length === 0) return `<span class="miw_json-punctuation">{</span><span class="miw_json-punctuation">}</span>`; const isCollapsed = depth >= CONFIG.COLLAPSE_LEVEL; const nodeCount = CONFIG.SHOW_NODE_COUNT ? `<span class="miw_json-node-count">${keys.length} 个属性</span>` : ''; let html = `<div class="miw_json-line">${this.getLineNumber()}<div class="miw_line-content">`; html += `<div class="miw_json-collapsible ${isCollapsed ? '' : 'expanded'}" data-depth="${depth}" data-type="object">`; html += `<span class="miw_json-punctuation">{</span>${nodeCount}`; html += `</div></div></div>`; html += `<div class="miw_json-children" ${isCollapsed ? 'style="display:none;"' : ''}>`; keys.forEach((key, idx) => { html += `<div class="miw_json-line">${this.getLineNumber()}<div class="miw_line-content">`; html += `<span class="miw_json-key">"${this.escapeHtml(key)}"</span><span class="miw_json-punctuation">:</span> `; html += this.formatJson(data[key], depth + 1); if (idx < keys.length - 1) html += `<span class="miw_json-punctuation">,</span>`; html += `</div></div>`; }); html += `</div>`; html += `<div class="miw_json-line">${this.getLineNumber()}<div class="miw_line-content">`; html += `<span class="miw_json-punctuation">}</span>`; html += `</div></div>`; return html; } escapeHtml(str) { return str.replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // ========== 核心修改:工具栏类名添加miw_前缀 ========== createToolbar() { const toolbar = document.createElement('div'); toolbar.className = 'miw_json-toolbar'; // 添加miw_前缀 const lineBtnActive = CONFIG.SHOW_LINE_NUMBERS ? 'active' : ''; const lineBtnText = CONFIG.SHOW_LINE_NUMBERS ? '行号:开' : '行号:关'; const rawBtnText = this.isRawView ? '显示格式化' : '显示原文'; const searchContainer = ` <div class="miw_search-container"> <!-- 添加miw_前缀 --> <input type="text" class="miw_search-box" placeholder="搜索关键词..." /> <!-- 添加miw_前缀 --> <button class="miw_search-nav-btn prev-match-btn">↑</button> <!-- 添加miw_前缀 --> <button class="miw_search-nav-btn next-match-btn">↓</button> <!-- 添加miw_前缀 --> <span class="miw_search-count">0/0</span> <!-- 添加miw_前缀 --> </div> `; toolbar.innerHTML = ` ${searchContainer} <button class="miw_toolbar-button expand-btn">展开全部</button> <!-- 添加miw_前缀 --> <button class="miw_toolbar-button collapse-btn">折叠全部</button> <!-- 添加miw_前缀 --> <button class="miw_toolbar-button copy-btn">复制内容</button> <!-- 添加miw_前缀 --> <button class="miw_toolbar-button theme-btn">切换主题</button> <!-- 添加miw_前缀 --> <button class="miw_toolbar-button line-numbers-btn ${lineBtnActive}">${lineBtnText}</button> <!-- 添加miw_前缀 --> <button class="miw_toolbar-button raw-view-btn">${rawBtnText}</button> <!-- 添加miw_前缀 --> <button class="miw_toolbar-button close-btn">关闭</button> <!-- 添加miw_前缀 --> `; return toolbar; } // ========== 核心修改:通知元素类名添加miw_前缀 ========== createNotification() { const note = document.createElement('div'); note.className = 'miw_json-notification'; // 添加miw_前缀 return note; } showNotification(message, duration = 2000) { // 选择器添加miw_前缀 const note = document.querySelector('.miw_json-formatter-wrapper .miw_json-notification'); if (!note) return; note.textContent = message; note.classList.add('show'); setTimeout(() => note.classList.remove('show'), duration); } async copyToClipboard(text, msg = '已复制到剪贴板') { try { await navigator.clipboard.writeText(text); this.showNotification(msg); } catch (err) { this.showNotification('复制失败,请手动复制', 3000); } } escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } expandAllCollapsibles() { // 选择器添加miw_前缀 const allCollapsibles = this.contentWrapper.querySelectorAll('.miw_json-collapsible'); allCollapsibles.forEach(el => { el.classList.add('expanded'); const childContainer = el.closest('.miw_json-line')?.nextElementSibling; // 添加miw_前缀 if (childContainer?.classList.contains('miw_json-children')) { // 添加miw_前缀 childContainer.style.display = 'block'; } }); } collectAllTextNodes(rootNode) { const textNodes = []; const traverse = (node) => { if (!node) return; const childNodes = node.childNodes; for (let i = 0; i < childNodes.length; i++) { const child = childNodes[i]; if (child.nodeType === Node.TEXT_NODE) { textNodes.push(child); } else if (child.nodeType === Node.ELEMENT_NODE && !child.classList.contains('miw_json-ellipsis')) { // 添加miw_前缀 traverse(child); } } }; traverse(rootNode); return textNodes; } searchJSON(term) { if (this.isRawView) return; this.searchTerm = term.trim(); this.matchNodes = []; this.currentMatchIndex = -1; // 选择器添加miw_前缀 const highlights = this.contentWrapper.querySelectorAll('.miw_search-highlight'); highlights.forEach(hl => { const parent = hl.parentNode; while (hl.firstChild) parent.insertBefore(hl.firstChild, hl); parent.removeChild(hl); parent.normalize(); }); this.updateSearchCount(); if (!this.searchTerm) return; this.expandAllCollapsibles(); const escapedTerm = this.escapeRegExp(this.searchTerm); const regex = new RegExp(escapedTerm, 'gi'); const allTextNodes = this.collectAllTextNodes(this.contentWrapper); allTextNodes.forEach(textNode => { const originalText = textNode.textContent; if (!originalText) return; regex.lastIndex = 0; const matches = []; let match; while ((match = regex.exec(originalText)) !== null) { matches.push({ start: match.index, end: match.index + match[0].length, value: match[0] }); } if (matches.length === 0) return; const fragment = document.createDocumentFragment(); let lastPos = 0; matches.forEach(m => { if (m.start > lastPos) { fragment.appendChild(document.createTextNode(originalText.slice(lastPos, m.start))); } const hlSpan = document.createElement('span'); hlSpan.className = 'miw_search-highlight'; // 添加miw_前缀 hlSpan.textContent = m.value; fragment.appendChild(hlSpan); this.matchNodes.push(hlSpan); lastPos = m.end; }); if (lastPos < originalText.length) { fragment.appendChild(document.createTextNode(originalText.slice(lastPos))); } textNode.parentNode.replaceChild(fragment, textNode); }); this.updateSearchCount(); if (this.matchNodes.length > 0) { this.currentMatchIndex = 0; this.highlightCurrentMatch(); this.scrollToCurrentMatch(); this.showNotification(`找到 ${this.matchNodes.length} 个匹配项`, 2000); } else { this.showNotification('未找到匹配内容', 2000); } } updateSearchCount() { // 选择器添加miw_前缀 const countEl = this.container.querySelector('.miw_search-count'); if (!countEl) return; const total = this.matchNodes.length; const current = this.currentMatchIndex >= 0 ? this.currentMatchIndex + 1 : 0; countEl.textContent = `${current}/${total}`; } highlightCurrentMatch() { // 类名添加miw_前缀 this.matchNodes.forEach(node => node.classList.remove('current')); if (this.currentMatchIndex >= 0 && this.currentMatchIndex < this.matchNodes.length) { this.matchNodes[this.currentMatchIndex].classList.add('current'); } } scrollToCurrentMatch() { if (this.currentMatchIndex < 0 || this.currentMatchIndex >= this.matchNodes.length) return; const target = this.matchNodes[this.currentMatchIndex]; target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); } prevMatch() { if (this.isRawView) { this.showNotification('原文视图不支持搜索', 1500); return; } if (this.matchNodes.length === 0) { this.showNotification('无匹配内容', 1500); return; } this.currentMatchIndex = this.currentMatchIndex <= 0 ? this.matchNodes.length - 1 : this.currentMatchIndex - 1; this.highlightCurrentMatch(); this.scrollToCurrentMatch(); this.updateSearchCount(); } nextMatch() { if (this.isRawView) { this.showNotification('原文视图不支持搜索', 1500); return; } if (this.matchNodes.length === 0) { this.showNotification('无匹配内容', 1500); return; } this.currentMatchIndex = this.currentMatchIndex >= this.matchNodes.length - 1 ? 0 : this.currentMatchIndex + 1; this.highlightCurrentMatch(); this.scrollToCurrentMatch(); this.updateSearchCount(); } toggleRawView() { this.isRawView = !this.isRawView; // 选择器添加miw_前缀 const viewContainer = this.container.querySelector('.miw_view-container'); if (this.isRawView) { // 类名添加miw_前缀 viewContainer.innerHTML = `<div class="miw_json-raw-view">${this.escapeHtml(this.originalContent)}</div>`; this.showNotification('已切换至原文视图', 1500); } else { this.lineCounter = 1; const formattedContent = this.jsonData ? this.formatJson(this.jsonData) : this.escapeHtml(this.originalContent); viewContainer.innerHTML = formattedContent; this.contentWrapper = viewContainer; if (this.searchTerm) this.searchJSON(this.searchTerm); this.showNotification('已切换至格式化视图', 1500); } // 选择器添加miw_前缀 const rawBtn = this.container.querySelector('.miw_raw-view-btn'); rawBtn.textContent = this.isRawView ? '显示格式化' : '显示原文'; } bindEvents() { // 选择器添加miw_前缀 const toolbar = this.container.querySelector('.miw_json-toolbar'); if (!toolbar) return; // 选择器添加miw_前缀 const searchBox = toolbar.querySelector('.miw_search-box'); searchBox.addEventListener('input', (e) => this.searchJSON(e.target.value)); searchBox.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.shiftKey ? this.prevMatch() : this.nextMatch(); } }); // 选择器添加miw_前缀 toolbar.querySelector('.miw_search-nav-btn.prev-match-btn').addEventListener('click', () => this.prevMatch()); toolbar.querySelector('.miw_search-nav-btn.next-match-btn').addEventListener('click', () => this.nextMatch()); // 选择器添加miw_前缀 toolbar.querySelector('.miw_toolbar-button.expand-btn').addEventListener('click', () => { if (this.isRawView) { this.showNotification('原文视图不支持展开', 1500); return; } this.expandAllCollapsibles(); this.showNotification('已展开全部节点'); }); // 选择器添加miw_前缀 toolbar.querySelector('.miw_toolbar-button.collapse-btn').addEventListener('click', () => { if (this.isRawView) { this.showNotification('原文视图不支持折叠', 1500); return; } // 选择器添加miw_前缀 this.container.querySelectorAll('.miw_json-collapsible').forEach(el => { el.classList.remove('expanded'); // 选择器添加miw_前缀 const child = el.closest('.miw_json-line')?.nextElementSibling; if (child?.classList.contains('miw_json-children')) child.style.display = 'none'; // 添加miw_前缀 }); this.showNotification('已折叠全部节点'); }); // 选择器添加miw_前缀 toolbar.querySelector('.miw_toolbar-button.copy-btn').addEventListener('click', async () => { const copyContent = this.isRawView || !this.jsonData ? this.originalContent : JSON.stringify(this.jsonData, null, 2); await this.copyToClipboard(copyContent, '已复制内容到剪贴板'); }); // 选择器添加miw_前缀 toolbar.querySelector('.miw_toolbar-button.theme-btn').addEventListener('click', () => { CONFIG.DARK_MODE = !CONFIG.DARK_MODE; updateStyles(); this.reRender(); this.showNotification(`已切换至${CONFIG.DARK_MODE ? '暗色' : '亮色'}主题`); }); // 选择器添加miw_前缀 toolbar.querySelector('.miw_toolbar-button.line-numbers-btn').addEventListener('click', () => { if (this.isRawView) { this.showNotification('原文视图不支持行号切换', 1500); return; } CONFIG.SHOW_LINE_NUMBERS = !CONFIG.SHOW_LINE_NUMBERS; updateStyles(); this.lineCounter = 1; // 选择器添加miw_前缀 const lineBtn = toolbar.querySelector('.miw_toolbar-button.line-numbers-btn'); lineBtn.textContent = CONFIG.SHOW_LINE_NUMBERS ? '行号:开' : '行号:关'; lineBtn.classList.toggle('active', CONFIG.SHOW_LINE_NUMBERS); this.reRender(); this.showNotification(`行号${CONFIG.SHOW_LINE_NUMBERS ? '开启' : '关闭'}`); }); // 选择器添加miw_前缀 toolbar.querySelector('.miw_toolbar-button.raw-view-btn').addEventListener('click', () => this.toggleRawView()); // 选择器添加miw_前缀 toolbar.querySelector('.miw_toolbar-button.close-btn').addEventListener('click', () => this.closeFormatter()); this.container.addEventListener('click', e => { if (this.isRawView) return; // 选择器添加miw_前缀 const target = e.target.closest('.miw_json-collapsible'); if (!target) return; const isExpanded = target.classList.contains('expanded'); // 选择器添加miw_前缀 const childContainer = target.closest('.miw_json-line')?.nextElementSibling; if (!childContainer?.classList.contains('miw_json-children')) return; // 添加miw_前缀 if (isExpanded) { target.classList.remove('expanded'); childContainer.style.display = 'none'; } else { target.classList.add('expanded'); childContainer.style.display = 'block'; } }); } closeFormatter() { this.isActive = false; // 移除脚本样式 if (styleElement && styleElement.parentNode) { styleElement.parentNode.removeChild(styleElement); } window.history.length > 1 ? window.history.back() : window.location.reload(); } showError(error) { // 所有类名添加miw_前缀 this.container.innerHTML = ` <div class="miw_error-container"> <div class="miw_error-title">内容处理失败</div> <div class="miw_error-message">${this.escapeHtml(error.message)}</div> <div style="margin-top:15px;"> <button id="retry-btn" class="miw_toolbar-button" style="margin-right:8px;">重试</button> <button id="raw-btn" class="miw_toolbar-button" style="margin-right:8px;">查看原文</button> <button id="back-btn" class="miw_toolbar-button">返回页面</button> </div> ${this.originalContent ? `<div class="miw_json-raw-view" style="margin-top:15px;">${this.escapeHtml(this.originalContent)}</div>` : ''} </div> `; setTimeout(() => { document.getElementById('retry-btn')?.addEventListener('click', () => this.apply()); document.getElementById('raw-btn')?.addEventListener('click', () => { this.container.innerHTML = `<div class="miw_json-raw-view">${this.escapeHtml(this.originalContent)}</div>`; }); document.getElementById('back-btn')?.addEventListener('click', () => this.closeFormatter()); }, 0); } reRender() { if (!this.isActive || !this.contentWrapper) return; if (this.isRawView) { updateStyles(); return; } this.lineCounter = 1; const formattedContent = this.jsonData ? this.formatJson(this.jsonData) : this.escapeHtml(this.originalContent); this.contentWrapper.innerHTML = formattedContent; if (this.searchTerm) this.searchJSON(this.searchTerm); } apply() { if (this.isActive || this.isRendering) return; this.isRendering = true; try { if (!this.isJsonContent()) { this.isRendering = false; return; } const rawContent = this.getPageContent(); if (!rawContent) { this.isRendering = false; return; } this.jsonData = this.parseJsonSafely(rawContent); // 核心修改:容器类名添加miw_前缀 if (!this.container) { this.container = document.createElement('div'); this.container.className = 'miw_json-formatter-wrapper'; // 添加miw_前缀 } document.body.replaceChildren(this.container); this.isActive = true; // 激活后加载样式 updateStyles(); // 核心修改:内容容器类名添加miw_前缀 this.contentWrapper = document.createElement('div'); this.contentWrapper.className = 'miw_view-container'; // 添加miw_前缀 this.isRawView = !this.jsonData || JSONExtractor.isNonJsonFile(); if (this.isRawView) { // 类名添加miw_前缀 this.contentWrapper.innerHTML = `<div class="miw_json-raw-view">${this.escapeHtml(this.originalContent)}</div>`; } else { this.lineCounter = 1; const jsonContent = this.formatJson(this.jsonData); this.contentWrapper.innerHTML = jsonContent; } this.container.innerHTML = ''; this.container.appendChild(this.createToolbar()); this.container.appendChild(this.contentWrapper); this.container.appendChild(this.createNotification()); this.bindEvents(); if (this.isRawView) { this.showNotification('非JSON文件,默认显示原文', 2000); } else { this.showNotification('JSON 格式化完成', 1500); } } catch (err) { console.error('内容处理错误:', err); if (!this.container) { this.container = document.createElement('div'); this.container.className = 'miw_json-formatter-wrapper'; // 添加miw_前缀 document.body.replaceChildren(this.container); this.isActive = true; updateStyles(); } this.showError(err); } finally { this.isRendering = false; } } } // 监听页面动态内容 function setupContentObserver() { if (mutationObserver) return; mutationObserver = new MutationObserver(mutations => { if (globalFormatter?.isActive) return; for (const mut of mutations) { if (mut.addedNodes.length && JSONExtractor.isJsonResponse()) { setTimeout(() => { globalFormatter?.apply(); }, 300); break; } } }); mutationObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); } // 注册脚本菜单 function registerMenuCommands() { if (typeof GM_registerMenuCommand !== 'function') return; GM_registerMenuCommand('🔍 格式化/查看原文', () => { if (!globalFormatter) globalFormatter = new LocalJSONFormatter(); globalFormatter.apply(); }); GM_registerMenuCommand('📋 复制当前内容', () => { if (globalFormatter?.isActive) { const copyContent = globalFormatter.isRawView || !globalFormatter.jsonData ? globalFormatter.originalContent : JSON.stringify(globalFormatter.jsonData, null, 2); globalFormatter.copyToClipboard(copyContent); } }); GM_registerMenuCommand('🌓 切换明暗主题', () => { if (!globalFormatter?.isActive) return; CONFIG.DARK_MODE = !CONFIG.DARK_MODE; updateStyles(); if (globalFormatter?.isActive) globalFormatter.reRender(); }); GM_registerMenuCommand('🔢 切换行号显示', () => { if (!globalFormatter?.isActive || globalFormatter?.isRawView) return; CONFIG.SHOW_LINE_NUMBERS = !CONFIG.SHOW_LINE_NUMBERS; updateStyles(); if (globalFormatter?.isActive) { globalFormatter.lineCounter = 1; globalFormatter.reRender(); } }); } // 初始化 function init() { globalFormatter = new LocalJSONFormatter(); registerMenuCommands(); setupContentObserver(); if (CONFIG.AUTO_FORMAT) { setTimeout(() => { globalFormatter.apply(); }, 500); } } init(); })();