您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds [replace], recursive BBCode, lists, entity decoding, etc.
// ==UserScript== // @name F-List / Ascendant // @namespace http://tampermonkey.net/ // @version 1.1 // @description Adds [replace], recursive BBCode, lists, entity decoding, etc. // @author Your Name // @match https://www.f-list.net/c/* // @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; function decodeHTMLEntitiesRecursive(text, maxDepth = 5) { if (typeof text !== 'string' || maxDepth <= 0) return text; const textarea = document.createElement('textarea'); textarea.innerHTML = text; let decoded = textarea.value; if (decoded !== text) { return decodeHTMLEntitiesRecursive(decoded, maxDepth - 1); } return decoded; } function convertCustomBBCodeToHTML(bbcodeString) { let processedHtml = String(bbcodeString); // Ensure it's a string // Order of processing is important: simpler/content tags first, then containers. // Decode entities from the entire incoming string once, as it might be serialized HTML. // This is a bit broad. A more targeted decode happens when extracting content from regex. // processedHtml = decodeHTMLEntitiesRecursive(processedHtml); // --- Process Tables --- processedHtml = processedHtml.replace(/\[table class="([^"]+)"\]/gi, '<table class="$1">'); processedHtml = processedHtml.replace(/\[table\]/gi, '<table>'); processedHtml = processedHtml.replace(/\[\/table\]/gi, '</table>'); processedHtml = processedHtml.replace(/\[tr\]/gi, '<tr>'); processedHtml = processedHtml.replace(/\[\/tr\]/gi, '</tr>'); processedHtml = processedHtml.replace(/\[th\]/gi, '<th>'); processedHtml = processedHtml.replace(/\[\/th\]/gi, '</th>'); processedHtml = processedHtml.replace(/\[td\]/gi, '<td>'); processedHtml = processedHtml.replace(/\[\/td\]/gi, '</td>'); // --- Process Lists (Basic) --- processedHtml = processedHtml.replace(/\[list\]/gi, '<ul>'); processedHtml = processedHtml.replace(/\[\/list\]/gi, '</ul>'); processedHtml = processedHtml.replace(/\[\*\]\s*([\s\S]*?)(?=\r?\n\s*\[\*\]|\r?\n\s*\[\/list\]|\[\/list\]|$)/gi, (match, itemContent) => { // Recursively process content of list items let decodedItemContent = decodeHTMLEntitiesRecursive(itemContent.trim()); let processedItemHtml = convertCustomBBCodeToHTML(decodedItemContent); return `<li>${processedItemHtml}</li>`; }); // --- Process Columns --- processedHtml = processedHtml.replace(/\[columns\]([\s\S]*?)\[\/columns\]/gi, (match, columnsContent) => { let innerColsHtml = columnsContent.replace(/\[col\]([\s\S]*?)\[\/col\]/gi, (colMatch, colBbcodeContent) => { let decodedColBbcodeContent = decodeHTMLEntitiesRecursive(colBbcodeContent.trim()); let processedColHtml = convertCustomBBCodeToHTML(decodedColBbcodeContent); return `<div style="flex: 1; padding: 10px;">${processedColHtml}</div>`; }); return `<div style="display: flex; width: 100%; gap: 15px;">${innerColsHtml}</div>`; }); // --- Process Custom Collapses (BBCode to HTML) --- processedHtml = processedHtml.replace(/\[collapse(?:=([^\]]*))?\]([\s\S]*?)\[\/collapse\]/gi, (match, titleFromRegex, bbcodeContentWithinCollapse) => { let rawTitleAttribute = titleFromRegex; let displayTitle; if (typeof rawTitleAttribute === 'string') { displayTitle = decodeHTMLEntitiesRecursive(rawTitleAttribute); } else { displayTitle = 'Details'; } const tempTitleDiv = document.createElement('div'); tempTitleDiv.textContent = displayTitle; const sanitizedTitleForHeader = tempTitleDiv.innerHTML; let decodedBbcodeContent = decodeHTMLEntitiesRecursive(bbcodeContentWithinCollapse.trim()); let processedContentHtml = convertCustomBBCodeToHTML(decodedBbcodeContent); return `<div class="tm-custom-collapse"> <div class="tm-custom-collapse-header">${sanitizedTitleForHeader}</div> <div class="tm-custom-collapse-content" style="display: none;">${processedContentHtml}</div> </div>`; }); return processedHtml; } function attachCustomCollapseEventListeners(rootElement = document) { // (No changes) rootElement.querySelectorAll('.tm-custom-collapse-header:not(.tm-listener-attached)').forEach(header => { header.classList.add('tm-listener-attached'); header.style.cursor = 'pointer'; header.addEventListener('click', function(event) { event.stopPropagation(); event.preventDefault(); const contentElement = this.nextElementSibling; if (contentElement && contentElement.classList.contains('tm-custom-collapse-content')) { const isVisible = contentElement.style.display !== 'none'; contentElement.style.display = isVisible ? 'none' : 'block'; this.classList.toggle('tm-collapse-active', !isVisible); } }); }); } function processProfileContent() { const contentAreas = document.querySelectorAll('#tabs-1 .FormattedBlock, #tabs-2 .FormattedBlock'); contentAreas.forEach(area => { // Clear old listeners before reprocessing an area area.querySelectorAll('.tm-listener-attached').forEach(el => { // A more robust way to remove listeners if we re-create elements often, // but for now, just removing the flag is part of the strategy. // If elements are fully replaced by area.innerHTML, listeners are gone anyway. el.classList.remove('tm-listener-attached'); }); let currentHtmlContent = area.innerHTML; // STEP 0: Handle [replace] functionality FIRST const replaceRegex = /\[replace\]([\s\S]*?)\[\/replace\]/i; // Case-insensitive const replaceMatch = currentHtmlContent.match(replaceRegex); if (replaceMatch && replaceMatch[1]) { // If [replace] is found, its content becomes the new base HTML for this area. // All other original content of the area is discarded. currentHtmlContent = replaceMatch[1].trim(); // The [replace] tags themselves are removed by this assignment. // Note: The [replace] block itself is consumed and doesn't appear in the output. } // From now on, all operations use 'currentHtmlContent' which is either original or replaced. // --- Convert F-List Native Collapses to BBCode Text Nodes (within currentHtmlContent) --- // This step needs to operate on a DOM structure if we are to reliably get innerHTML of F-List blocks. // So, we'll parse currentHtmlContent, do the conversion, then serialize back. const tempDiv = document.createElement('div'); tempDiv.innerHTML = currentHtmlContent; const convertFListCollapseToBBCodeNode = (fListHeader) => { // (No changes to this helper from v0.9) const fListBlock = fListHeader.querySelector(':scope > .CollapseBlock'); const fListTitleSpan = fListHeader.querySelector(':scope > .CollapseHeaderText > span'); if (fListBlock && fListTitleSpan) { let rawTitle = fListTitleSpan.textContent || ""; let titleForBBCode; if (rawTitle === " ") { titleForBBCode = " "; } else if (rawTitle.trim() === "") { titleForBBCode = ""; } else { titleForBBCode = rawTitle.trim(); } const content = fListBlock.innerHTML; let bbCodeString; if (titleForBBCode === " ") { bbCodeString = `[collapse=${titleForBBCode}]`; } else if (titleForBBCode) { bbCodeString = `[collapse=${titleForBBCode}]`; } else { bbCodeString = `[collapse]`; } bbCodeString += content + '[/collapse]'; return document.createTextNode(bbCodeString); } return null; }; tempDiv.querySelectorAll('.CollapseHeader:not(.tm-custom-collapse-header)').forEach(fListHeader => { if (fListHeader.closest('.tm-custom-collapse')) return; // Avoid re-processing tm-custom-collapse internals const bbNode = convertFListCollapseToBBCodeNode(fListHeader); if (bbNode) fListHeader.replaceWith(bbNode); }); currentHtmlContent = tempDiv.innerHTML; // Get back the string with F-List collapses as BBCode // --- Main BBCode string to Final HTML conversion --- const processedAreaHtml = convertCustomBBCodeToHTML(currentHtmlContent); // Set the fully processed HTML to the area. // This single assignment replaces the entire content of 'area'. if (area.innerHTML !== processedAreaHtml) { area.innerHTML = processedAreaHtml; } // --- Iterative DOM pass for F-List collapses newly rendered INSIDE tm-custom-collapse content --- // This part *must* run after area.innerHTML is set with processedAreaHtml, // because it operates on the live DOM structure created by the browser from processedAreaHtml. let iterations = 0; const maxIterations = 15; while (iterations < maxIterations) { // (No changes from v0.9 for this F-List native nested collapse handling, but it operates on 'area' now) iterations++; let workDoneThisIteration = false; const nestedFListHeaders = area.querySelectorAll('.tm-custom-collapse-content .CollapseHeader:not(.tm-custom-collapse-header)'); if (nestedFListHeaders.length === 0) break; nestedFListHeaders.forEach(fListHeader => { const fListBlock = fListHeader.querySelector(':scope > .CollapseBlock'); const fListTitleSpan = fListHeader.querySelector(':scope > .CollapseHeaderText > span'); if (fListBlock && fListTitleSpan) { let rawTitle = fListTitleSpan.textContent || ""; let actualTitleText; if (rawTitle === " ") { actualTitleText = " "; } else if (rawTitle.trim() === "") { actualTitleText = 'Details'; } else { actualTitleText = rawTitle.trim(); } const content = fListBlock.innerHTML; const tempTitleDivForHeader = document.createElement('div'); tempTitleDivForHeader.textContent = actualTitleText; const sanitizedTitle = tempTitleDivForHeader.innerHTML; const newTmCollapseHTML = ` <div class="tm-custom-collapse"> <div class="tm-custom-collapse-header">${sanitizedTitle}</div> <div class="tm-custom-collapse-content" style="display: none;">${content}</div> </div>`; const tempNewCollapseContainer = document.createElement('div'); tempNewCollapseContainer.innerHTML = newTmCollapseHTML; const newTmCollapseElement = tempNewCollapseContainer.firstElementChild; if (newTmCollapseElement) { fListHeader.replaceWith(newTmCollapseElement); workDoneThisIteration = true; } else { fListHeader.remove(); } } else { fListHeader.remove(); } }); if (!workDoneThisIteration) break; } if (iterations === maxIterations && area.querySelectorAll('.tm-custom-collapse-content .CollapseHeader:not(.tm-custom-collapse-header)').length > 0) { console.warn("F-List Enhanced BBCode: Max iterations for nested F-List collapses."); } // --- Remove "Quote:" text --- area.querySelectorAll('.QuoteHeader').forEach(quoteHeader => { if (quoteHeader.textContent.trim() === "Quote:") { quoteHeader.textContent = ''; } }); // --- Attach event listeners to all custom collapses in the area --- attachCustomCollapseEventListeners(area); }); } // --- Script Execution & Observer --- (No changes from v0.9) if (document.readyState === 'interactive' || document.readyState === 'complete') { processProfileContent(); } else { document.addEventListener('DOMContentLoaded', processProfileContent); } const profileContentContainer = document.getElementById('Content'); if (profileContentContainer) { const observer = new MutationObserver((mutationsList) => { let needsProcessing = false; for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && (node.matches('.FormattedBlock, .ui-tabs-panel') || node.querySelector('.FormattedBlock'))) { needsProcessing = true; } }); } else if (mutation.type === 'attributes' && (mutation.target.matches('.ui-tabs-panel') || mutation.target.matches('.FormattedBlock'))) { needsProcessing = true; } if (needsProcessing) break; } if (needsProcessing) { processProfileContent(); } }); observer.observe(profileContentContainer, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); } // --- Stylesheet --- (No changes from v0.9) const styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = ` .FormattedBlock table { border-collapse: collapse; width: 90%; margin: 15px auto; border: 1px solid #555; } .FormattedBlock th, .FormattedBlock td { border: 1px solid #444; padding: 8px; text-align: left; } .FormattedBlock th { background-color: #333; color: #eee; } .tm-custom-collapse { margin: 10px 0; border: 1px solid #4a4a4a; border-radius: 4px; overflow: hidden; background-color: #2c2c2c; } .tm-custom-collapse-header { background-color: #3a3a3a; color: #eee; padding: 10px 15px; cursor: pointer; user-select: none; transition: background-color 0.2s ease; } .tm-custom-collapse-header:hover { background-color: #454545; } .tm-custom-collapse-header.tm-collapse-active { background-color: #505050; } .tm-custom-collapse-content { padding: 15px; border-top: 1px solid #4a4a4a; color: #ddd; background-color: #2f2f2f; } `; document.head.appendChild(styleSheet); })();