From 135c2269c707c7d5e74dcccbd923ebbc4b156c1a Mon Sep 17 00:00:00 2001 From: nanhaoluo <3075912108@qq.com> Date: Tue, 27 Jan 2026 00:28:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Mermaid=20=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启用代码块转换功能(移除 convertMermaidCodeblocks 中的 return 语句) - 添加完整的 Mermaid 代码块检测选择器 - 修复首页预览中显示原始 Mermaid 代码的问题 - 添加 argon_remove_mermaid_from_preview 函数过滤预览内容 - 更新三个文章预览模板,在预览中用 [Mermaid 图表] 替代原始代码 --- argontheme.js | 766 ++++++++++++--------------- functions.php | 287 +++++++++- template-parts/content-preview-1.php | 8 +- template-parts/content-preview-2.php | 8 +- template-parts/content-preview-3.php | 8 +- 5 files changed, 635 insertions(+), 442 deletions(-) diff --git a/argontheme.js b/argontheme.js index 7020ba5..6f7d5e4 100644 --- a/argontheme.js +++ b/argontheme.js @@ -1,4 +1,4 @@ -/*! +/*! * Argon 主题核心 JavaScript */ @@ -3034,6 +3034,23 @@ function resetGT4Captcha() { } } +function handleHashNavigation() { + if (!location.hash) { + return; + } + let target = null; + try { + target = document.getElementById(decodeURIComponent(location.hash.slice(1))); + } catch (e) { + target = document.querySelector(location.hash); + } + if (!target) { + return; + } + try { highlightJsRender(); } catch (err) { ArgonDebug.error('highlightJsRender failed:', err); } + try { lazyloadInit(); } catch (err) { ArgonDebug.error('lazyloadInit failed:', err); } +} + // ========================================================================== // 内联脚本执行器 (Inline Script Executor) // ========================================================================== @@ -3246,12 +3263,16 @@ $(document).pjax("a[href]:not([no-pjax]):not(.no-pjax):not([target='_blank']):no // Mermaid 图表渲染(需求 3.6: 页面切换时重新渲染) try { + if (typeof MermaidRenderer !== 'undefined' && MermaidRenderer.init && !MermaidRenderer.initialized) { + MermaidRenderer.init(); + } if (typeof MermaidRenderer !== 'undefined' && MermaidRenderer.renderAllCharts) { MermaidRenderer.renderAllCharts(); } } catch (err) { ArgonDebug.error('MermaidRenderer.renderAllCharts failed:', err); } + try { handleHashNavigation(); } catch (err) { ArgonDebug.error('handleHashNavigation failed:', err); } $("html").trigger("resize"); @@ -3287,6 +3308,17 @@ $(document).pjax("a[href]:not([no-pjax]):not(.no-pjax):not([target='_blank']):no resetGT4Captcha(); }); +window.addEventListener('hashchange', function() { + handleHashNavigation(); +}); +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + handleHashNavigation(); + }); +} else { + handleHashNavigation(); +} + /*Reference 跳转*/ $(document).on("click", ".reference-link , .reference-list-backlink" , function(e){ e.preventDefault(); @@ -4365,7 +4397,7 @@ function convertMermaidCodeblocks(){ // 创建容器 const container = document.createElement('div'); - container.className = 'mermaid-from-codeblock'; + container.className = 'mermaid mermaid-from-codeblock'; container.textContent = code; container.dataset.processed = 'true'; @@ -4797,16 +4829,17 @@ void 0; initialized: false, rendered: new Set(), // 已渲染的图表 ID 集合 config: null, + compatibilityFallbackAttempted: false, /** * 等待 Mermaid 库加载 * 需求 2.6: 清除缓存后首次加载时等待 Mermaid 库完全加载 * 需求 4.1: 开始渲染前检查 Mermaid 库是否已加载 * 需求 4.2: Mermaid 库未加载时等待库加载或显示错误提示 - * @param {number} timeout - 超时时间(毫秒),默认 5000ms + * @param {number} timeout - 超时时间(毫秒),默认 10000ms (增加超时时间以应对慢速网络) * @returns {Promise} 是否加载成功 */ - waitForMermaid(timeout = 5000) { + waitForMermaid(timeout = 10000) { return new Promise((resolve) => { // 如果已经加载,直接返回成功 if (typeof window.mermaid !== 'undefined') { @@ -4830,7 +4863,22 @@ void 0; else if (Date.now() - startTime > timeout) { clearInterval(checkInterval); this.logError(`Mermaid 库加载超时(${timeout}ms)`); - resolve(false); + + // 尝试手动触发降级加载 + if (typeof window.argonMermaidLoadFallback === 'function') { + this.logDebug('尝试手动触发降级加载'); + window.argonMermaidLoadFallback(); + // 给降级加载一点时间 + setTimeout(() => { + if (typeof window.mermaid !== 'undefined') { + resolve(true); + } else { + resolve(false); + } + }, 2000); + } else { + resolve(false); + } } }, 100); // 每 100ms 检查一次 }); @@ -4871,56 +4919,61 @@ void 0; // 配置 Mermaid // 需求 2.1-2.4: 支持多种图表类型,设置正确的主题和安全级别 try { - window.mermaid.initialize({ - startOnLoad: false, // 手动控制渲染时机 - theme: theme, // 根据网站主题自动切换 - securityLevel: 'loose', // 允许 HTML 标签 - logLevel: this.config.debugMode ? 'debug' : 'error', - // 流程图配置 (需求 2.2) - flowchart: { - useMaxWidth: true, - htmlLabels: true, - curve: 'basis' - }, - // 时序图配置 - sequence: { - useMaxWidth: true, - wrap: true - }, - // 甘特图配置 - gantt: { - useMaxWidth: true - }, - // ER 图配置 (需求 2.3) - er: { - useMaxWidth: true, - layoutDirection: 'TB' - }, - // 状态图配置 (需求 2.4) - stateDiagram: { - useMaxWidth: true - }, - // 类图配置 - class: { - useMaxWidth: true - }, - // 饼图配置 - pie: { - useMaxWidth: true - }, - // Git 图配置 - gitGraph: { - useMaxWidth: true - }, - // 用户旅程图配置 - journey: { - useMaxWidth: true - } - }); - - this.initialized = true; - this.logDebug('Mermaid 配置初始化成功', { theme }); - return true; + // 检查 initialize 是否存在(兼容旧版本) + if (typeof window.mermaid.initialize === 'function') { + window.mermaid.initialize({ + startOnLoad: false, // 手动控制渲染时机 + theme: theme, // 根据网站主题自动切换 + securityLevel: 'loose', // 允许 HTML 标签 + logLevel: this.config.debugMode ? 'debug' : 'error', + // 流程图配置 (需求 2.2) + flowchart: { + useMaxWidth: true, + htmlLabels: true, + curve: 'basis' + }, + // 时序图配置 + sequence: { + useMaxWidth: true, + wrap: true + }, + // 甘特图配置 + gantt: { + useMaxWidth: true + }, + // ER 图配置 (需求 2.3) + er: { + useMaxWidth: true, + layoutDirection: 'TB' + }, + // 状态图配置 (需求 2.4) + stateDiagram: { + useMaxWidth: true + }, + // 类图配置 + class: { + useMaxWidth: true + }, + // 饼图配置 + pie: { + useMaxWidth: true + }, + // Git 图配置 + gitGraph: { + useMaxWidth: true + }, + // 用户旅程图配置 + journey: { + useMaxWidth: true + } + }); + this.initialized = true; + this.logDebug('Mermaid 配置初始化成功', { theme }); + return true; + } else { + this.logError('Mermaid.initialize 方法不存在'); + return false; + } } catch (error) { this.logError('Mermaid 配置初始化失败', error); return false; @@ -4951,22 +5004,27 @@ void 0; */ detectMermaidBlocks() { const blocks = []; + const seen = new Set(); + const selectors = [ + 'div.mermaid-shortcode', // Shortcode 格式(推荐) + 'div.mermaid-from-codeblock', // 代码块魔改格式 + 'div.mermaid', // 标准格式 + 'pre code.language-mermaid', // Markdown 格式(降级) + 'pre[data-lang="mermaid"]', // 自定义属性格式 + 'code.mermaid' // 简化格式 + ]; - // 需求 3.1: 扫描所有
 元素
-			// 需求 3.2: 识别 language-mermaid 类名
-			document.querySelectorAll('pre code.language-mermaid').forEach(code => {
-				// 需求 3.5: 过滤已渲染的代码块
-				if (!this.isRendered(code)) {
-					blocks.push(code);
-				}
-			});
-			
-			// 需求 3.3: 识别 mermaid 类名
-			document.querySelectorAll('pre code.mermaid').forEach(code => {
-				// 需求 3.5: 过滤已渲染的代码块
-				if (!this.isRendered(code)) {
-					blocks.push(code);
-				}
+			selectors.forEach(selector => {
+				document.querySelectorAll(selector).forEach(element => {
+					if (this.isRendered(element)) {
+						return;
+					}
+					if (seen.has(element)) {
+						return;
+					}
+					blocks.push(element);
+					seen.add(element);
+				});
 			});
 			
 			this.logDebug(`检测到 ${blocks.length} 个未渲染的 Mermaid 代码块`);
@@ -4985,6 +5043,22 @@ void 0;
 				return true;
 			}
 			
+			if (element.classList && element.classList.contains('mermaid-error-container')) {
+				return true;
+			}
+			
+			if (element.classList && element.classList.contains('mermaid-loading')) {
+				return true;
+			}
+			
+			if (element.classList && element.classList.contains('mermaid-container')) {
+				return true;
+			}
+			
+			if (element.tagName === 'DIV' && element.classList.contains('mermaid') && element.querySelector('svg')) {
+				return true;
+			}
+			
 			// 检查元素是否在 mermaid-container 容器中
 			if (element.closest('.mermaid-container') !== null) {
 				return true;
@@ -4992,233 +5066,6 @@ void 0;
 			
 			return false;
 		},
-
-		/**
-		 * 检查元素是否在 HTML 注释中
-		 * @param {HTMLElement} element - 要检查的元素
-		 * @returns {boolean} 是否在注释中
-		 */
-		isInComment(element) {
-			let node = element.parentNode;
-			while (node) {
-				if (node.nodeType === Node.COMMENT_NODE) {
-					return true;
-				}
-				node = node.parentNode;
-			}
-			return false;
-		},
-
-		/**
-		 * 检测 Markdown 容器语法的 Mermaid 代码块
-		 * 格式: ::: mermaid ... :::
-		 * @param {Array} blocks - 代码块数组
-		 */
-		detectContainerBlocks(blocks) {
-			// 查找所有包含 ::: mermaid 的元素
-			const allElements = document.querySelectorAll('p, pre, code, div');
-			const processedElements = new Set();
-			
-			allElements.forEach(element => {
-				// 跳过已处理的元素
-				if (processedElements.has(element)) {
-					return;
-				}
-				
-				const text = element.textContent.trim();
-				
-				// 检查是否是开始标记
-				if (text.startsWith('::: mermaid') || text === '::: mermaid') {
-					this.logDebug('找到容器语法开始标记');
-					
-					// 收集所有内容直到结束标记
-					const container = this.extractContainerContent(element, processedElements);
-					if (container && !blocks.includes(container)) {
-						blocks.push(container);
-						this.logDebug('检测到 Markdown 容器语法的 Mermaid 代码块');
-					}
-				}
-			});
-		},
-
-		/**
-		 * 提取 Markdown 容器语法的内容
-		 * @param {HTMLElement} startElement - 包含开始标记的元素
-		 * @param {Set} processedElements - 已处理的元素集合
-		 * @returns {HTMLElement|null} 包含代码的容器元素
-		 */
-		extractContainerContent(startElement, processedElements) {
-			let codeLines = [];
-			let currentElement = startElement;
-			let foundStart = false;
-			let foundEnd = false;
-			
-			// 标记开始元素为已处理
-			processedElements.add(startElement);
-			
-			// 处理开始元素
-			// 使用 innerHTML 来获取原始内容,包括 
标签 - let startHTML = startElement.innerHTML; - let startText = startElement.textContent.trim(); - - this.logDebug('检查元素,textContent: ' + startText.substring(0, 50)); - this.logDebug('innerHTML: ' + startHTML.substring(0, 100)); - - if (startText.startsWith('::: mermaid')) { - foundStart = true; - this.logDebug('找到容器语法开始标记'); - - // 检查是否整个内容都在一个元素中 - if (startText.includes(':::') && startText.lastIndexOf(':::') > 10) { - // 整个容器语法在一个元素中 - this.logDebug('检测到单元素容器语法'); - - // 使用 htmlToText 转换整个 HTML - let fullText = this.htmlToText(startHTML); - this.logDebug('转换后的完整文本: ' + fullText.substring(0, 200)); - - // 移除开始和结束标记 - fullText = fullText.replace(/^:::\s*mermaid\s*/i, '').trim(); - fullText = fullText.replace(/:::\s*$/, '').trim(); - - this.logDebug('移除标记后的代码: ' + fullText.substring(0, 200)); - - if (fullText) { - // 创建容器 - const container = document.createElement('div'); - container.className = 'mermaid-container-block'; - container.textContent = fullText; - container.dataset.containerBlock = 'true'; - - // 替换开始元素 - startElement.parentNode.replaceChild(container, startElement); - - return container; - } - return null; - } - - // 多元素容器语法(原有逻辑) - // 移除开始标记,保留同一元素中的其他内容 - startText = startText.replace(/^:::\s*mermaid\s*/i, '').trim(); - if (startText && !startText.startsWith(':::')) { - // 如果开始标记后有内容,需要从 HTML 中提取 - // 将
转换为换行符 - let contentHTML = startHTML.replace(/^:::\s*mermaid\s*/i, ''); - let contentText = this.htmlToText(contentHTML); - if (contentText.trim()) { - codeLines.push(contentText); - } - } - // 检查是否在同一元素中就有结束标记 - if (startText.endsWith(':::')) { - foundEnd = true; - // 移除结束标记 - if (codeLines.length > 0) { - let lastLine = codeLines[codeLines.length - 1]; - codeLines[codeLines.length - 1] = lastLine.replace(/:::\s*$/, '').trim(); - } - } - } - - // 如果还没找到结束标记,继续查找后续兄弟元素 - if (foundStart && !foundEnd) { - currentElement = startElement.nextElementSibling; - - while (currentElement) { - processedElements.add(currentElement); - let text = currentElement.textContent.trim(); - - // 检查是否是结束标记 - if (text === ':::' || text.endsWith(':::')) { - foundEnd = true; - // 如果结束标记前还有内容,保留它 - if (text !== ':::') { - text = text.replace(/:::\s*$/, '').trim(); - if (text) { - // 将 HTML 转换为文本,保留换行符 - let contentText = this.htmlToText(currentElement.innerHTML); - codeLines.push(contentText); - } - } - break; - } - - // 添加当前元素的内容 - // 将 HTML 转换为文本,保留换行符 - let contentText = this.htmlToText(currentElement.innerHTML); - if (contentText.trim()) { - codeLines.push(contentText); - } else { - // 空元素,添加空行 - codeLines.push(''); - } - - currentElement = currentElement.nextElementSibling; - } - } - - // 如果没有找到完整的容器语法,返回 null - if (!foundStart || !foundEnd) { - this.logDebug('容器语法不完整,跳过'); - return null; - } - - // 合并所有行 - let code = codeLines.join('\n').trim(); - - this.logDebug('提取的完整代码: ' + code.substring(0, 200)); - - if (!code) { - return null; - } - - // 创建一个新的容器来存储代码 - const container = document.createElement('div'); - container.className = 'mermaid-container-block'; - container.textContent = code; - container.dataset.containerBlock = 'true'; - - // 替换开始元素 - startElement.parentNode.replaceChild(container, startElement); - - return container; - }, - - /** - * 将 HTML 转换为纯文本,保留换行符 - * @param {string} html - HTML 字符串 - * @returns {string} 纯文本 - */ - htmlToText(html) { - // 将

转换为换行符 - let text = html.replace(//gi, '\n'); - // 移除其他 HTML 标签 - text = text.replace(/<[^>]+>/g, ''); - // 解码 HTML 实体 - text = text - .replace(/ /g, ' ') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/–/g, '-') // EN DASH - .replace(/—/g, '--') // EM DASH - .replace(/→/g, '->') // RIGHTWARDS ARROW - .replace(/–/g, '-') // EN DASH (named entity) - .replace(/—/g, '--') // EM DASH (named entity) - .replace(/→/g, '->'); // RIGHTWARDS ARROW (named entity) - - // 转换 Unicode 字符(WordPress 可能直接输出 Unicode) - text = text - .replace(/–/g, '-') // U+2013 EN DASH - .replace(/—/g, '--') // U+2014 EM DASH - .replace(/→/g, '->'); // U+2192 RIGHTWARDS ARROW - - return text; - }, - /** * 提取代码块内容 * @param {HTMLElement} element - 代码块元素 @@ -5274,7 +5121,7 @@ void 0; const clonedElement = element.cloneNode(true); const scripts = clonedElement.querySelectorAll('script'); scripts.forEach(script => script.remove()); - code = clonedElement.textContent; + code = clonedElement.innerText || clonedElement.textContent; this.logDebug('使用降级方案提取代码'); } } @@ -5283,22 +5130,32 @@ void 0; // WP-Markdown 会在 script 后面再输出一次代码 scriptTag.remove(); } else { - code = element.textContent; - this.logDebug('从纯文本提取代码'); + // 检查是否包含块级元素或换行标签 + if (element.querySelector('br') || element.querySelector('p') || element.querySelector('div')) { + const clonedElement = element.cloneNode(true); + clonedElement.querySelectorAll('script').forEach(script => script.remove()); + clonedElement.querySelectorAll('br').forEach(br => br.replaceWith('\n')); + clonedElement.querySelectorAll('p,div').forEach(node => { + node.appendChild(document.createTextNode('\n')); + }); + code = clonedElement.textContent || ''; + this.logDebug('检测到 HTML 结构,从 innerText 提取代码'); + } else { + code = element.textContent || ''; + this.logDebug('未检测到 HTML 结构,从 textContent 提取代码'); + } } } else if (element.tagName === 'CODE') { - // 修复:使用 innerText 而不是 textContent,保留换行符 - // textContent 可能会丢失某些格式化信息 - code = element.innerText || element.textContent; - this.logDebug('从 CODE 标签提取,使用 innerText'); + code = element.textContent || ''; + this.logDebug('从 CODE 标签提取'); } else if (element.tagName === 'PRE') { const codeElement = element.querySelector('code'); if (codeElement) { - code = codeElement.innerText || codeElement.textContent; + code = codeElement.textContent || ''; } else { - code = element.innerText || element.textContent; + code = element.textContent || ''; } - this.logDebug('从 PRE 标签提取,使用 innerText'); + this.logDebug('从 PRE 标签提取'); } // 记录原始提取的代码(调试用) @@ -5335,6 +5192,63 @@ void 0; return code; }, + + convertLegacySyntax(code) { + let updated = code; + if (/^\s*flowchart\b/i.test(updated)) { + updated = updated.replace(/^\s*flowchart\b/i, 'graph'); + } + if (/^\s*stateDiagram-v2\b/i.test(updated)) { + updated = updated.replace(/^\s*stateDiagram-v2\b/i, 'stateDiagram'); + } + return updated; + }, + + shouldRetryWithGraphSyntax(error, code, forceLegacy) { + if (!/^\s*(flowchart|stateDiagram-v2)\b/i.test(code || '')) { + return false; + } + if (forceLegacy) { + return true; + } + if (error && error.hash && Array.isArray(error.hash.expected)) { + const expected = error.hash.expected.join(' '); + const tokenText = String(error.hash.text || '').toLowerCase(); + if (expected.includes('GRAPH') && tokenText === 'flowchart') { + return true; + } + if ((expected.includes('STATE') || expected.includes('SD')) && tokenText === 'statediagram-v2') { + return true; + } + } + const message = String(error && error.message ? error.message : '').toLowerCase(); + return message.includes('graph') || message.includes('state'); + }, + + needsModernMermaid(code) { + return /^\s*(flowchart|erDiagram|stateDiagram|stateDiagram-v2)\b/i.test(code || ''); + }, + + tryUpgradeMermaidLibrary(element, code) { + if (this.compatibilityFallbackAttempted) { + return false; + } + if (!this.needsModernMermaid(code)) { + return false; + } + if (typeof window.argonMermaidLoadFallback !== 'function') { + return false; + } + this.compatibilityFallbackAttempted = true; + const container = document.createElement('div'); + container.className = 'mermaid'; + container.textContent = code; + if (element && element.parentNode) { + element.parentNode.replaceChild(container, element); + } + window.argonMermaidLoadFallback(); + return true; + }, // ---------- 渲染引擎 ---------- @@ -5358,88 +5272,90 @@ void 0; return; } - // 检测所有代码块(一次 DOM 遍历) - const blocks = this.detectMermaidBlocks(); + const startRender = (blocks) => { + this.logDebug(`准备渲染 ${blocks.length} 个图表 (Lazy Load)`); + if (this.observer) { + this.observer.disconnect(); + } + this.observer = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const block = entry.target; + const index = blocks.indexOf(block); + requestAnimationFrame(() => { + this.renderChart(block, index); + }); + observer.unobserve(block); + } + }); + }, { + rootMargin: '200px 0px', + threshold: 0.01 + }); + blocks.forEach(block => { + this.observer.observe(block); + }); + }; + if (this.domObserver) { + this.domObserver.disconnect(); + this.domObserver = null; + } + + const blocks = this.detectMermaidBlocks(); if (blocks.length === 0) { this.logDebug('未找到 Mermaid 代码块'); + if (typeof MutationObserver === 'undefined') { + return; + } + let finished = false; + const waitStart = Date.now(); + const stopWaiting = (message) => { + if (finished) { + return; + } + finished = true; + if (this.domObserver) { + this.domObserver.disconnect(); + this.domObserver = null; + } + if (message) { + this.logDebug(message); + } + }; + const tryRender = () => { + const pending = this.detectMermaidBlocks(); + if (pending.length > 0) { + stopWaiting(); + startRender(pending); + return true; + } + return false; + }; + const maxWait = 4000; + this.domObserver = new MutationObserver(() => { + if (tryRender()) { + return; + } + if (Date.now() - waitStart > maxWait) { + stopWaiting('等待 Mermaid 代码块超时'); + } + }); + this.domObserver.observe(document.body, { + childList: true, + subtree: true + }); + setTimeout(() => { + if (!finished) { + if (!tryRender()) { + stopWaiting('等待 Mermaid 代码块超时'); + } + } + }, maxWait); return; } - this.logDebug(`准备批量渲染 ${blocks.length} 个图表`); - - // 需求 18.4: 将图表分为视口内和视口外两组 - const visibleBlocks = []; - const invisibleBlocks = []; - - blocks.forEach(block => { - if (this.isInViewport(block)) { - visibleBlocks.push(block); - } else { - invisibleBlocks.push(block); - } - }); - - this.logDebug(`视口内图表: ${visibleBlocks.length}, 视口外图表: ${invisibleBlocks.length}`); - - // 优先渲染视口内的图表 - let currentIndex = 0; - const batchSize = 3; // 每批渲染 3 个图表 - - const renderBatch = (blockList, isVisible) => { - if (currentIndex >= blockList.length) { - // 当前列表渲染完成 - if (isVisible && invisibleBlocks.length > 0) { - // 视口内图表渲染完成,开始渲染视口外图表 - this.logDebug('视口内图表渲染完成,开始渲染视口外图表'); - currentIndex = 0; - requestAnimationFrame(() => renderBatch(invisibleBlocks, false)); - } else { - this.logDebug(`完成渲染 ${blocks.length} 个图表`); - } - return; - } - - const endIndex = Math.min(currentIndex + batchSize, blockList.length); - - // 渲染当前批次 - for (let i = currentIndex; i < endIndex; i++) { - const globalIndex = blocks.indexOf(blockList[i]); - this.renderChart(blockList[i], globalIndex); - } - - currentIndex = endIndex; - - // 继续下一批 - requestAnimationFrame(() => renderBatch(blockList, isVisible)); - }; - - // 开始渲染(优先渲染视口内图表) - if (visibleBlocks.length > 0) { - requestAnimationFrame(() => renderBatch(visibleBlocks, true)); - } else if (invisibleBlocks.length > 0) { - // 如果没有视口内图表,直接渲染视口外图表 - requestAnimationFrame(() => renderBatch(invisibleBlocks, false)); - } - }, - - /** - * 检查元素是否在视口内 - * @param {HTMLElement} element - 要检查的元素 - * @returns {boolean} 是否在视口内 - */ - isInViewport(element) { - const rect = element.getBoundingClientRect(); - const windowHeight = window.innerHeight || document.documentElement.clientHeight; - const windowWidth = window.innerWidth || document.documentElement.clientWidth; - - // 元素至少部分可见 - return ( - rect.top < windowHeight && - rect.bottom > 0 && - rect.left < windowWidth && - rect.right > 0 - ); + startRender(blocks); }, /** @@ -5472,7 +5388,8 @@ void 0; } // 提取代码 - const code = this.extractMermaidCode(element); + const rawCode = this.extractMermaidCode(element); + const code = rawCode; if (!code) { this.logDebug(`代码块为空,跳过: ${chartId}`); @@ -5513,45 +5430,65 @@ void 0; return; } - // 渲染图表 - const renderPromise = window.mermaid.render(`mermaid-svg-${chartId}`, code); - - // 检查返回值是否是 Promise - if (!renderPromise || typeof renderPromise.then !== 'function') { - this.logError('Mermaid.render 没有返回 Promise,可能是版本不兼容'); - // 尝试使用旧版 API - this.renderChartLegacy(loadingContainer, code, container, chartId); - return; - } - - renderPromise.then(result => { - // 渲染成功 - container.innerHTML = result.svg; - - // 保存原始代码(用于主题切换时重新渲染) + const onRenderSuccess = (svg, bindFunctions) => { + container.innerHTML = svg; container.dataset.mermaidCode = code; container.dataset.currentTheme = this.getMermaidTheme(); - - // 需求 3.5: 添加渲染状态标记,避免重复渲染 container.setAttribute('data-mermaid-rendered', 'true'); - - // 替换加载动画为渲染结果 if (loadingContainer.parentNode) { loadingContainer.parentNode.replaceChild(container, loadingContainer); } - - // 标记为已渲染 this.rendered.add(container); - - // 应用样式增强(包含淡入动画) this.applyStyles(container); - + if (typeof bindFunctions === 'function') { + try { + bindFunctions(container); + } catch (bindError) { + this.logError('Mermaid bindFunctions 执行失败', bindError); + } + } this.logDebug(`图表渲染成功: ${chartId}`); - }).catch(error => { + }; + + const renderResult = window.mermaid.render(`mermaid-svg-${chartId}`, code); + + if (renderResult && typeof renderResult.then === 'function') { + renderResult.then(result => { + onRenderSuccess(result.svg, result.bindFunctions); + }).catch(error => { // 需求 18.5: 渲染失败时记录错误但不抛出异常 - this.logError(`图表渲染失败: ${chartId}`, error); - this.handleRenderError(loadingContainer, error, code); - }); + const retryCode = this.convertLegacySyntax(code); + if (this.shouldRetryWithGraphSyntax(error, code) && retryCode !== code) { + this.logDebug('语法降级后重试渲染'); + this.renderChartLegacy(loadingContainer, retryCode, container, chartId); + return; + } + if (this.tryUpgradeMermaidLibrary(loadingContainer, code)) { + return; + } + this.logError(`图表渲染失败: ${chartId}`, error); + this.handleRenderError(loadingContainer, error, rawCode); + }); + return; + } + + if (renderResult && typeof renderResult === 'object' && typeof renderResult.svg === 'string') { + onRenderSuccess(renderResult.svg, renderResult.bindFunctions); + return; + } + + if (typeof renderResult === 'string') { + onRenderSuccess(renderResult); + return; + } + + this.logError('Mermaid.render 没有返回 Promise,可能是版本不兼容'); + if (this.tryUpgradeMermaidLibrary(loadingContainer, code)) { + return; + } + const legacyCode = this.convertLegacySyntax(code); + this.renderChartLegacy(loadingContainer, legacyCode, container, chartId); + return; } catch (error) { // 需求 18.5: 捕获所有异常,确保不影响其他图表 @@ -5613,6 +5550,9 @@ void 0; } } catch (error) { this.logError('旧版 API 渲染失败', error); + if (this.tryUpgradeMermaidLibrary(element, code)) { + return; + } this.handleRenderError(element, error, code); } }, diff --git a/functions.php b/functions.php index 6aa783a..911d220 100644 --- a/functions.php +++ b/functions.php @@ -2824,7 +2824,6 @@ function argon_comment_text_render($text){ return argon_apply_comment_macros($text); } add_filter('comment_text', 'argon_comment_text_render', 9); -add_filter('the_content', 'argon_comment_text_render', 9); //评论发送处理 function post_comment_preprocessing($comment){ @@ -4612,6 +4611,19 @@ function shortcode_mermaid($attr,$content=""){ return $out; } + +/** + * 从内容中移除 Mermaid shortcode,用于文章预览 + * 避免在预览中显示原始 Mermaid 代码 + * + * @param string $content 文章内容 + * @return string 移除 Mermaid shortcode 后的内容 + */ +function argon_remove_mermaid_from_preview($content) { + // 移除 [mermaid]...[/mermaid] shortcode + $content = preg_replace('/\[mermaid[^\]]*\].*?\[\/mermaid\]/is', '[Mermaid 图表]', $content); + return $content; +} add_shortcode('hide_reading_time','shortcode_hide_reading_time'); function shortcode_hide_reading_time($attr,$content=""){ return ""; @@ -6422,7 +6434,33 @@ function argon_create_ai_query_log_table() { require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } -add_action('after_switch_theme', 'argon_create_ai_query_log_table'); + +function argon_ai_query_log_table_exists() { + global $wpdb; + $table_name = $wpdb->prefix . 'argon_ai_query_log'; + $found = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)); + return $found === $table_name; +} + +function argon_maybe_create_ai_query_log_table() { + static $ran = false; + if ($ran) return; + $ran = true; + + $option_key = 'argon_ai_query_log_table_version'; + $current_version = 1; + $saved_version = intval(get_option($option_key, 0)); + + if ($saved_version === $current_version && argon_ai_query_log_table_exists()) { + return; + } + + argon_create_ai_query_log_table(); + update_option($option_key, $current_version); +} + +add_action('after_switch_theme', 'argon_maybe_create_ai_query_log_table'); +add_action('init', 'argon_maybe_create_ai_query_log_table', 5); /** * 记录 AI 查询 @@ -6442,6 +6480,8 @@ function argon_log_ai_query($provider, $model, $scenario, $prompt_length, $conte global $wpdb; $table_name = $wpdb->prefix . 'argon_ai_query_log'; + argon_maybe_create_ai_query_log_table(); + $wpdb->insert( $table_name, [ @@ -6490,6 +6530,10 @@ function argon_ai_query($scenario, $prompt, $content, $context = []) { // 优先使用场景化的 API 配置(新系统) $config = null; $provider = ''; + $config_scenario = $scenario; + if ($scenario === 'spam_detection' || $scenario === 'keyword_extraction') { + $config_scenario = 'spam'; + } // 如果 context 中指定了 provider,使用指定的 provider if (isset($context['provider'])) { @@ -6497,19 +6541,19 @@ function argon_ai_query($scenario, $prompt, $content, $context = []) { $config = argon_get_ai_provider_config($provider); } else { // 否则根据场景获取活动的 API 配置 - $config = argon_get_active_api_config($scenario); - if ($config && isset($config['provider'])) { + $config = argon_get_active_api_config($config_scenario); + if ($config && !empty($config['provider'])) { $provider = $config['provider']; } } // 如果新系统没有配置,回退到旧系统 - if (!$config || !isset($config['api_key'])) { + if (!$config || empty($provider) || empty($config['api_key'])) { $provider = get_option('argon_ai_summary_provider', 'openai'); $config = argon_get_ai_provider_config($provider); } - if (!$config || !isset($config['api_key'])) { + if (!$config || empty($provider) || empty($config['api_key'])) { error_log("Argon AI Query Error: Provider config not found for {$provider}"); return false; } @@ -6530,6 +6574,31 @@ function argon_ai_query($scenario, $prompt, $content, $context = []) { // 获取 API 端点 $endpoint = isset($config['api_endpoint']) ? $config['api_endpoint'] : ''; + if (empty($model) || $provider === 'xiaomi') { + $provider_defaults = [ + 'openai' => 'gpt-4o-mini', + 'anthropic' => 'claude-3-5-haiku-20241022', + 'deepseek' => 'deepseek-chat', + 'qianwen' => 'qwen-turbo', + 'wenxin' => 'ernie-4.0-turbo-8k', + 'doubao' => 'doubao-pro-32k', + 'kimi' => 'moonshot-v1-8k', + 'zhipu' => 'glm-4-flash', + 'siliconflow' => 'Qwen/Qwen2.5-7B-Instruct', + 'xiaomi' => 'MiMo-V2-Flash' + ]; + if (empty($model) && isset($provider_defaults[$provider])) { + $model = $provider_defaults[$provider]; + } + if ($provider === 'xiaomi' && !empty($endpoint) && !empty($model)) { + if (strpos($endpoint, 'xiaomimimo.com') !== false && strcasecmp($model, 'MiMo-V2-Flash') === 0) { + $model = 'mimo-v2-flash'; + } elseif (strpos($endpoint, 'api.mimo.xiaomi.com') !== false && strcasecmp($model, 'mimo-v2-flash') === 0) { + $model = 'MiMo-V2-Flash'; + } + } + } + try { switch ($provider) { case 'openai': @@ -6619,6 +6688,91 @@ function argon_ai_query($scenario, $prompt, $content, $context = []) { return $result; } +function argon_resolve_ai_provider_model($scenario, $context = []) { + $config = null; + $provider = ''; + $config_scenario = $scenario; + if ($scenario === 'spam_detection' || $scenario === 'keyword_extraction') { + $config_scenario = 'spam'; + } + + if (is_array($context) && isset($context['provider'])) { + $provider = $context['provider']; + $config = argon_get_ai_provider_config($provider); + } else { + $config = argon_get_active_api_config($config_scenario); + if ($config && !empty($config['provider'])) { + $provider = $config['provider']; + } + } + + if (!$config || empty($provider) || empty($config['api_key'])) { + $provider = get_option('argon_ai_summary_provider', 'openai'); + $config = argon_get_ai_provider_config($provider); + } + + $endpoint = is_array($config) && isset($config['api_endpoint']) ? $config['api_endpoint'] : ''; + $model = ''; + if (is_array($context) && isset($context['model'])) { + $model = $context['model']; + } elseif (is_array($config) && isset($config['model'])) { + $model = $config['model']; + } + + $provider_defaults = [ + 'openai' => 'gpt-4o-mini', + 'anthropic' => 'claude-3-5-haiku-20241022', + 'deepseek' => 'deepseek-chat', + 'qianwen' => 'qwen-turbo', + 'wenxin' => 'ernie-4.0-turbo-8k', + 'doubao' => 'doubao-pro-32k', + 'kimi' => 'moonshot-v1-8k', + 'zhipu' => 'glm-4-flash', + 'siliconflow' => 'Qwen/Qwen2.5-7B-Instruct', + 'xiaomi' => 'MiMo-V2-Flash' + ]; + if (empty($model) && isset($provider_defaults[$provider])) { + $model = $provider_defaults[$provider]; + } + if ($provider === 'xiaomi' && !empty($endpoint) && !empty($model)) { + if (strpos($endpoint, 'xiaomimimo.com') !== false && strcasecmp($model, 'MiMo-V2-Flash') === 0) { + $model = 'mimo-v2-flash'; + } elseif (strpos($endpoint, 'api.mimo.xiaomi.com') !== false && strcasecmp($model, 'mimo-v2-flash') === 0) { + $model = 'MiMo-V2-Flash'; + } + } + + return [ + 'provider' => $provider, + 'model' => $model + ]; +} + +function argon_get_latest_ai_query_provider_model($scenario, $post_id = 0, $comment_id = 0) { + global $wpdb; + $table_name = $wpdb->prefix . 'argon_ai_query_log'; + + $where = ['scenario = %s', "status = 'success'"]; + $params = [$scenario]; + + if (!empty($post_id)) { + $where[] = 'post_id = %d'; + $params[] = intval($post_id); + } + if (!empty($comment_id)) { + $where[] = 'comment_id = %d'; + $params[] = intval($comment_id); + } + + $sql = "SELECT provider, model FROM {$table_name} WHERE " . implode(' AND ', $where) . " ORDER BY id DESC LIMIT 1"; + $row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A); + + if (is_array($row) && !empty($row['provider']) && isset($row['model'])) { + return $row; + } + return null; +} + /** * 获取 AI 查询统计信息 * @@ -6960,6 +7114,25 @@ function argon_get_ai_summary($post_id) { // 如果缓存存在且内容未变化,返回缓存 if (!empty($cached_summary) && $cached_hash === $current_hash) { + $sync_key = 'argon_ai_summary_provider_model_synced_' . $post_id; + if (get_transient($sync_key) === false) { + $latest = argon_get_latest_ai_query_provider_model('summary', $post_id, 0); + if ($latest) { + $current_provider = get_post_meta($post_id, '_argon_ai_summary_provider', true); + $current_model = get_post_meta($post_id, '_argon_ai_summary_model', true); + + if (!empty($latest['provider']) && $latest['provider'] !== $current_provider) { + update_post_meta($post_id, '_argon_ai_summary_provider', $latest['provider']); + } + if (isset($latest['model']) && $latest['model'] !== $current_model) { + update_post_meta($post_id, '_argon_ai_summary_model', $latest['model']); + } + + set_transient($sync_key, 1, DAY_IN_SECONDS); + } else { + set_transient($sync_key, 1, 10 * MINUTE_IN_SECONDS); + } + } return $cached_summary; } @@ -7245,7 +7418,7 @@ function argon_log_ai_error($provider, $error_type, $error_message, $post_id = 0 * @param WP_Post $post 文章对象 * @return string|false 摘要内容或 false */ -function argon_generate_ai_summary($post) { +function argon_generate_ai_summary($post, $ai_context = []) { // 准备文章内容 $content = wp_strip_all_tags($post->post_content); $content = preg_replace('/\s+/', ' ', $content); @@ -7265,10 +7438,10 @@ function argon_generate_ai_summary($post) { $prompt = get_option('argon_ai_summary_prompt', '你是一个专业的内容摘要助手。请仔细阅读以下文章内容,用简洁、准确的语言总结文章的核心观点和主要内容。要求:1) 控制在 100-150 字以内;2) 突出文章的关键信息和亮点;3) 使用通俗易懂的语言;4) 保持客观中立的语气。'); // 使用统一的 AI 查询接口 - $result = argon_ai_query('summary', $prompt, $content, [ + $result = argon_ai_query('summary', $prompt, $content, array_merge([ 'post_id' => $post->ID, 'user_id' => get_current_user_id() - ]); + ], is_array($ai_context) ? $ai_context : [])); // 检查结果 if ($result === false) { @@ -7892,6 +8065,17 @@ function argon_check_ai_summary() { delete_transient('argon_ai_summary_generating_' . $post_id); $model = get_post_meta($post_id, '_argon_ai_summary_model', true); $provider = get_post_meta($post_id, '_argon_ai_summary_provider', true); + $latest = argon_get_latest_ai_query_provider_model('summary', $post_id, 0); + if ($latest) { + if (!empty($latest['provider']) && $latest['provider'] !== $provider) { + $provider = $latest['provider']; + update_post_meta($post_id, '_argon_ai_summary_provider', $provider); + } + if (isset($latest['model']) && $latest['model'] !== $model) { + $model = $latest['model']; + update_post_meta($post_id, '_argon_ai_summary_model', $model); + } + } $code = get_post_meta($post_id, '_argon_ai_summary_code', true); // 如果没有识别码,生成一个 @@ -7918,16 +8102,34 @@ function argon_check_ai_summary() { // 触发生成 $post = get_post($post_id); if ($post) { - $summary = argon_generate_ai_summary($post); + $resolved = argon_resolve_ai_provider_model('summary', [ + 'post_id' => $post_id, + 'user_id' => get_current_user_id() + ]); + $provider = isset($resolved['provider']) ? $resolved['provider'] : ''; + $model = isset($resolved['model']) ? $resolved['model'] : ''; + + $summary = argon_generate_ai_summary($post, [ + 'provider' => $provider, + 'model' => $model + ]); if ($summary !== false) { $current_hash = md5($post->post_content . $post->post_title); - $provider = get_option('argon_ai_summary_provider', 'openai'); - $model = get_option('argon_ai_summary_model', ''); // 生成唯一识别码 $summary_code = argon_generate_summary_code(); + $latest = argon_get_latest_ai_query_provider_model('summary', $post_id, 0); + if ($latest) { + if (!empty($latest['provider'])) { + $provider = $latest['provider']; + } + if (isset($latest['model'])) { + $model = $latest['model']; + } + } + // 保存摘要和模型信息 update_post_meta($post_id, '_argon_ai_summary', $summary); update_post_meta($post_id, '_argon_ai_summary_hash', $current_hash); @@ -9789,11 +9991,21 @@ function argon_detect_spam_comment_sync($comment) { // 构建评论上下文信息 $comment_text = argon_build_comment_context($comment); + $resolved = argon_resolve_ai_provider_model('spam_detection', [ + 'comment_id' => $comment->comment_ID, + 'post_id' => $comment->comment_post_ID, + 'user_id' => $comment->user_id + ]); + $provider = isset($resolved['provider']) ? $resolved['provider'] : ''; + $model = isset($resolved['model']) ? $resolved['model'] : ''; + // 使用统一的 AI 查询接口 $result_text = argon_ai_query('spam_detection', $prompt, $comment_text, [ 'comment_id' => $comment->comment_ID, 'post_id' => $comment->comment_post_ID, - 'user_id' => $comment->user_id + 'user_id' => $comment->user_id, + 'provider' => $provider, + 'model' => $model ]); if ($result_text === false) { @@ -9804,6 +10016,15 @@ function argon_detect_spam_comment_sync($comment) { $result = json_decode($result_text, true); if ($result && isset($result['content_spam'])) { + $latest = argon_get_latest_ai_query_provider_model('spam_detection', 0, $comment->comment_ID); + if ($latest) { + if (!empty($latest['provider'])) { + $provider = $latest['provider']; + } + if (isset($latest['model'])) { + $model = $latest['model']; + } + } // 转换为统一格式 $unified_result = [ 'is_spam' => $result['content_spam'], @@ -9818,6 +10039,8 @@ function argon_detect_spam_comment_sync($comment) { // 保存检测结果 update_comment_meta($comment->comment_ID, '_argon_spam_detection_result', $unified_result); update_comment_meta($comment->comment_ID, '_argon_spam_detection_time', time()); + update_comment_meta($comment->comment_ID, '_argon_spam_detection_provider', $provider); + update_comment_meta($comment->comment_ID, '_argon_spam_detection_model', $model); return $unified_result; } @@ -10246,6 +10469,17 @@ function argon_async_spam_detection_handler($comment_id) { $detection_code = argon_generate_detection_code($comment_id); update_comment_meta($comment_id, '_argon_spam_detection_code', $detection_code); + $config = argon_get_active_api_config('spam'); + if (!empty($config) && !empty($config['api_key']) && !empty($config['provider'])) { + update_comment_meta($comment_id, '_argon_spam_detection_provider', $config['provider']); + update_comment_meta($comment_id, '_argon_spam_detection_model', isset($config['model']) ? $config['model'] : ''); + } else { + $provider = get_option('argon_ai_summary_provider', 'openai'); + $provider_config = argon_get_ai_provider_config($provider); + update_comment_meta($comment_id, '_argon_spam_detection_provider', $provider); + update_comment_meta($comment_id, '_argon_spam_detection_model', !empty($provider_config['model']) ? $provider_config['model'] : get_option('argon_ai_summary_model', '')); + } + if ($result && isset($result['is_spam'])) { $content_spam = $result['is_spam']; $username_invalid = isset($result['username_invalid']) ? $result['username_invalid'] : false; @@ -10501,12 +10735,24 @@ function argon_spam_detection_scan() { $spam_results = []; $checked_ids = []; + $config = argon_get_active_api_config('spam'); + if (!empty($config) && !empty($config['api_key']) && !empty($config['provider'])) { + $provider = $config['provider']; + $model = isset($config['model']) ? $config['model'] : ''; + } else { + $provider = get_option('argon_ai_summary_provider', 'openai'); + $provider_config = argon_get_ai_provider_config($provider); + $model = !empty($provider_config['model']) ? $provider_config['model'] : get_option('argon_ai_summary_model', ''); + } + foreach ($result as $item) { $comment_id = $item['id']; $checked_ids[] = $comment_id; // 记录检测时间 update_comment_meta($comment_id, '_argon_spam_detection_time', time()); + update_comment_meta($comment_id, '_argon_spam_detection_provider', $provider); + update_comment_meta($comment_id, '_argon_spam_detection_model', $model); // 生成识别码 $detection_code = argon_generate_detection_code($comment_id); @@ -10555,6 +10801,8 @@ function argon_spam_detection_scan() { foreach ($comments_data as $comment_data) { if (!in_array($comment_data['id'], $checked_ids)) { update_comment_meta($comment_data['id'], '_argon_spam_detection_time', time()); + update_comment_meta($comment_data['id'], '_argon_spam_detection_provider', $provider); + update_comment_meta($comment_data['id'], '_argon_spam_detection_model', $model); $detection_code = argon_generate_detection_code($comment_data['id']); update_comment_meta($comment_data['id'], '_argon_spam_detection_code', $detection_code); } @@ -10970,11 +11218,8 @@ function argon_update_mermaid_settings($settings) { /** * 检测页面内容是否包含 Mermaid 代码块 * - * 支持多种格式: - * -
- * -

- * - 
- * - 
+ * 支持 Shortcode 格式:
+ * - [mermaid]...[/mermaid]
  * 
  * @param string $content 页面内容
  * @return bool 是否包含 Mermaid 代码块
@@ -10986,11 +11231,7 @@ function argon_has_mermaid_content($content) {
 	
 	// 检测多种 Mermaid 代码块格式
 	$patterns = [
-		'/]*class=["\']([^"\']*\s)?mermaid(\s[^"\']*)?["\'][^>]*>/i',  // 
- '/]*class=["\']([^"\']*\s)?language-mermaid(\s[^"\']*)?["\'][^>]*>/i', // - '/]*data-lang=["\']mermaid["\'][^>]*>/i', //
-		'/]*class=["\']([^"\']*\s)?mermaid(\s[^"\']*)?["\'][^>]*>/i',  // 
-		'/:::\s*mermaid/i'  // ::: mermaid (Markdown 容器语法)
+		'/\[mermaid[^\]]*\]/i'
 	];
 	
 	foreach ($patterns as $pattern) {
diff --git a/template-parts/content-preview-1.php b/template-parts/content-preview-1.php
index f2bd85d..601a65f 100644
--- a/template-parts/content-preview-1.php
+++ b/template-parts/content-preview-1.php
@@ -92,13 +92,17 @@