// ==UserScript== // @name [MT论坛]“借鉴一下” by:青春向上 // @namespace https://github.com/qcxs/mtbbs // @version 2025-11-22 // @description 利用正则表达式将html解析为bbcode,可供论坛发帖时布局参考。通过伪元素无侵入式添加代码复制功能。 // @author 青春向上 // @match *://bbs.binmt.cc/forum.php?*tid=* // @match *://bbs.binmt.cc/*thread-*.html* // @icon https://bbs.binmt.cc/favicon.ico // @grant none // ==/UserScript== (function () { 'use strict'; let isLoading = false; const tid = getTidByUrl(); //不是帖子,停止执行 if (!tid) return; const selectContent = 'div.comiis_messages.comiis_aimg_show.cl' function getTidByUrl() { const url = new URL(window.location.href); const regex = /thread-(\d+)-/; const match = url.pathname.match(regex); return url.searchParams.get('tid') || (match ? match[1] : null); // 匹配成功返回156601,失败返回null } // 复制代码块精简版:伪元素添加前置文字 + 点击复制(自动适配所有 .comiis_blockcode,含动态新增) (function () { // 1. 注入核心样式(伪元素文字 + 点击区域) const style = document.createElement('style'); style.textContent = ` .comiis_blockcode { position: relative !important; pointer-events: auto !important; /* 也允许点击代码块,若要禁止设为none */ } /* 前置文字(伪元素,无真实DOM) */ .comiis_blockcode::before { content: "复制"; /* 前置文字 */ position: absolute !important; right: 10px !important; top: 1em !important; transform: translateY(-50%) !important; color: #42b983 !important; font-size: 14px !important; font-weight: bold !important; pointer-events: auto !important; /* 点击文字触发复制 */ } `; document.head.appendChild(style); // 2. 事件委托:点击代码块(或前置文字)触发复制 document.addEventListener('click', (e) => { const codeBlock = e.target.closest('.comiis_blockcode'); if (!codeBlock) return; // 复制代码块的 textContent(纯文本,不含伪元素内容) const codeText = codeBlock.textContent.trim(); previewDia(codeText) }); })(); // 提取处理单个元素的逻辑为独立函数 function processElement(div) { try { const h2Element = div.querySelector('div.comiis_postli_top.bg_f h2'); const pid = div.id.match(/\d+/)[0]; // 检查是否已经添加过span,避免重复添加 if (!h2Element.querySelector('span[data-action="reference"]')) { // 创建要添加的span元素 const span = document.createElement('span'); span.dataset.action = "reference"; // 添加标识,用于检查重复 span.textContent = ' [借鉴一下]'; span.style.cursor = 'pointer'; span.style.color = '#0066cc'; span.style.marginLeft = '8px'; // 显示在最上层,避免被其余元素遮挡 span.style.position = 'relative'; span.style.zIndex = '1';//无需太大,否则其余元素无法遮住它 // 为span添加点击事件 span.addEventListener('click', async () => { // 查找对应div中的内容元素 const contentElement = div.querySelector(selectContent); if (contentElement) { // 输出innerHTML到控制台 if (!isLoading) { const content = await getLatest(pid, contentElement.innerHTML) previewDia(html2bbcode(content), null, 'ubb代码预览:');; } } else { console.log(`未找到class为"${selectContent}"的元素`); } }); // 将span添加到h2元素的末尾 h2Element.appendChild(span); } const timesElement = div.querySelector('div.comiis_postli_times.bg_f'); //需判断,例如帖子没有此元素 if (timesElement) { const bottomZhanSpan = timesElement.querySelector('span.bottom_zhan.y'); if (bottomZhanSpan) { // 检查是否已经添加过share span,避免重复添加 if (!timesElement.querySelector('span[data-action="share"]')) { // 创建share span标签 const shareSpan = document.createElement('span'); shareSpan.dataset.action = "share"; // 添加标识,用于检查重复 shareSpan.className = 'y'; shareSpan.textContent = 'share'; shareSpan.style.marginLeft = '8px'; // 为share span添加点击事件 shareSpan.addEventListener('click', () => { const link = `https://bbs.binmt.cc/forum.php?mod=redirect&goto=findpost&ptid=${tid}&pid=${pid}`; previewDia(link); }); // 将share span添加到bottomZhanSpan的后面 bottomZhanSpan.parentNode.insertBefore(shareSpan, bottomZhanSpan.nextSibling); } } } } catch (error) { console.error(div, '处理元素时发生错误:', error); } } // 处理初始存在的元素 const targetSelector = 'div.comiis_postli.comiis_list_readimgs.nfqsqi'; document.querySelectorAll(targetSelector).forEach(processElement); // 创建 MutationObserver 实例 const observer = new MutationObserver((mutationsList) => { // 存储已处理的元素(避免重复处理) const processedElements = new WeakSet(); for (const mutation of mutationsList) { // 场景1:childList 变化(innerHTML 替换子节点时触发) // 场景2:characterData 变化(innerHTML 设为纯文本时触发,如 el.innerHTML = '新文本') // 场景3:subtree: true 监听子树变化(目标元素是子元素时也能捕获) if ( (mutation.type === 'childList' && mutation.addedNodes.length > 0) || (mutation.type === 'characterData' && mutation.target.parentElement) ) { // 确定当前变化的关联元素(childList 取当前节点,characterData 取父元素) const currentElement = mutation.type === 'childList' ? mutation.target : mutation.target.parentElement; // 1. 若当前元素就是目标元素,且未处理过 if (currentElement.matches(targetSelector) && !processedElements.has(currentElement)) { processElement(currentElement); processedElements.add(currentElement); } // 2. 若当前元素包含目标子元素,遍历处理(避免重复处理) currentElement.querySelectorAll(targetSelector).forEach(element => { if (!processedElements.has(element)) { processElement(element); processedElements.add(element); } }); } } }); // 开始观察目标节点 observer.observe(document.body, { childList: true, // 监听子节点增减(innerHTML 替换节点时触发) characterData: true, // 监听文本节点变化(innerHTML 设纯文本时触发) subtree: true, // 监听子树所有节点(目标元素是子元素时生效) attributes: false, // 关闭属性监听(innerHTML 不触发属性变化,提升性能) characterDataOldValue: false // 不需要旧值,关闭节省内存 }); // 如果需要停止观察,可以调用: // observer.disconnect(); //核心解析函数 function html2bbcode(html) { let bbcode = html; const codeBlocks = []; let codeIndex = 0; // 提取代码块并替换为占位符(逻辑不变) bbcode = bbcode.replace( /
]*>回复<\/font> ([\s\S]*?)<\/blockquote><\/div>/gi, '[quote]$1[/quote]' ); // 处理隐藏内容 [hide] bbcode = bbcode.replace( /以下内容需要积分高于 ([\d]+) 才可浏览<\/div>/gi, '[hide=,$1][/hide]' ); bbcode = bbcode.replace( /]*>本帖隐藏的内容: <\/h2>([\s\S]*?)<\/div>/gi, '[hide]$1[/hide]' ); // 处理免费内容 [free] bbcode = bbcode.replace( /
([\s\S]*?)<\/blockquote><\/div>/gi, '[free]$1[/free]' ); // 处理表情标签 const smileyMap = new Map(); Object.keys(smilies_type).forEach(typeKey => { const [, dir] = smilies_type[typeKey]; const typeId = typeKey.replace('_', ''); const arrays = smilies_array[typeId]; if (arrays) { Object.keys(arrays).forEach(pageKey => { arrays[pageKey].forEach(item => { const [, tag, filename] = item; const fullPath = `https://cdn-bbs.mt2.cn/static/image/smiley/${dir}/${filename}`; smileyMap.set(fullPath, tag); }); }); } }); bbcode = bbcode.replace( /]*src="([^"]+)"[^>]*>/gi, (match, src) => smileyMap.get(src) || match ); // 处理普通图片 [img] bbcode = bbcode.replace( /
]*src="([^"]+)"[^>]*width="([^"]*)"[^>]*height="([^"]*)"[^>]*>/gi, (_, src, w, h) => `[img=${w || ''},${h || ''}]${src}[/img]` ); bbcode = bbcode.replace( /
]*src="([^"]+)"[^>]*>/gi, '[img]$1[/img]' ); // 处理QQ标签 [qq] bbcode = bbcode.replace( /]*href="http:\/\/wpa\.qq\.com\/msgrd\?[^>]*uin=([^&]+)[^>]*>([\s\S]*?)<\/a>/gi, '[qq]$1[/qq]' ); // 处理电子邮件 [email] bbcode = bbcode.replace( /]*href="mailto:([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (_, email, text) => { const cleanEmail = email.trim(); const cleanText = text.trim(); return cleanEmail === cleanText ? `[email]${cleanEmail}[/email]` : `[email=${cleanEmail}]${cleanText}[/email]`; } ); // 处理链接 [url] bbcode = bbcode.replace( /]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => { const cleanHref = href.trim(); const cleanText = text.trim(); return cleanHref === cleanText ? `[url]${cleanHref}[/url]` : `[url=${cleanHref}]${cleanText}[/url]`; } ); // 处理文本样式标签 bbcode = bbcode.replace(/([\s\S]*?)<\/strong>/gi, '[b]$1[/b]'); bbcode = bbcode.replace(/([\s\S]*?)<\/i>/gi, '[i]$1[/i]'); bbcode = bbcode.replace(/([\s\S]*?)<\/u>/gi, '[u]$1[/u]'); bbcode = bbcode.replace(/
([\s\S]*?)<\/strike>/gi, '[s]$1[/s]'); // 处理颜色和背景色 bbcode = bbcode.replace( /]*color="([^"]+)"[^>]*>([\s\S]*?)<\/font>/gi, '[color=$1]$2[/color]' ); bbcode = bbcode.replace( /]*style="background-color:([^;]+);?"[^>]*>([\s\S]*?)<\/font>/gi, '[backcolor=$1]$2[/backcolor]' ); // 处理字号 [size] bbcode = bbcode.replace( /]*size="([^"]+)"[^>]*>([\s\S]*?)<\/font>/gi, '[size=$1]$2[/size]' ); // 处理对齐 [align] bbcode = bbcode.replace( /]*align="([^"]+)"[^>]*>([\s\S]*?)<\/div>/gi, '[align=$1]$2[/align]' ); // 处理媒体标签 [media] bbcode = bbcode.replace( // 优化后的正则表达式 /[\s\S]*?