F-List BBCode Tables Enabler

Adds support for [table], [tr], [th], [td] BBCode on F-List profiles and allows custom classes. Protects [collapse] tags.

Verze ze dne 01. 06. 2025. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         F-List BBCode Tables Enabler
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Adds support for [table], [tr], [th], [td] BBCode on F-List profiles and allows custom classes. Protects [collapse] tags.
// @author       Your Name
// @match        https://www.f-list.net/c/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    const collapsePlaceholderPrefix = 'FL_COLLAPSE_PLACEHOLDER_';
    let placeholderIdCounter = 0;

    // Function to extract specific BBCode blocks (like [collapse]) and replace them with placeholders
    function extractAndPlaceholderBbcodes(htmlContent, bbcodeRegex, placeholderMap) {
        return htmlContent.replace(bbcodeRegex, (match) => {
            const placeholder = `<!-- ${collapsePlaceholderPrefix}${placeholderIdCounter} -->`;
            placeholderMap[placeholder] = match; // Store the original BBCode block
            placeholderIdCounter++;
            return placeholder;
        });
    }

    // Function to restore the original BBCode blocks from placeholders
    function restorePlaceholderedBbcodes(htmlContent, placeholderMap) {
        for (const placeholder in placeholderMap) {
            if (placeholderMap.hasOwnProperty(placeholder)) {
                // Use a loop to replace all occurrences, in case a placeholder was somehow duplicated
                while(htmlContent.includes(placeholder)) {
                    htmlContent = htmlContent.replace(placeholder, placeholderMap[placeholder]);
                }
            }
        }
        return htmlContent;
    }

    // Function to convert custom table BBCode to HTML
    function convertTableBbcode(htmlWithPlaceholders) {
        let html = htmlWithPlaceholders;

        // [table class="custom-class"]
        html = html.replace(/\[table\s+class="([^"]+)"\]/gi, (match, className) => {
            return `<table class="${className} fl-generated-table">`;
        });
        html = html.replace(/\[table\]/gi, '<table class="fl-generated-table">');
        html = html.replace(/\[\/table\]/gi, '</table>');
        html = html.replace(/\[tr\]/gi, '<tr>');
        html = html.replace(/\[\/tr\]/gi, '</tr>');
        html = html.replace(/\[th\]/gi, '<th>');
        html = html.replace(/\[\/th\]/gi, '</th>');
        html = html.replace(/\[td\]/gi, '<td>');
        html = html.replace(/\[\/td\]/gi, '</td>');

        return html;
    }

    // Function to process all relevant blocks on the page
    function processPageForTables() {
        const formattedBlocks = document.querySelectorAll('div.FormattedBlock');
        const collapseBbcodeRegex = /\[collapse(?:=[^\]]*)?\][\s\S]*?\[\/collapse\]/gi; // Regex for [collapse] tags

        formattedBlocks.forEach(block => {
            if (!block.dataset.flTablesProcessed && (block.innerHTML.includes('[table]') || block.innerHTML.includes('[collapse]'))) {
                const originalHtml = block.innerHTML;
                let currentHtml = originalHtml;
                const placeholderMap = {};
                placeholderIdCounter = 0; // Reset for each block

                // 1. Extract [collapse] blocks and replace with placeholders
                currentHtml = extractAndPlaceholderBbcodes(currentHtml, collapseBbcodeRegex, placeholderMap);

                // 2. Convert [table] BBCode
                currentHtml = convertTableBbcode(currentHtml);

                // 3. Restore [collapse] blocks
                currentHtml = restorePlaceholderedBbcodes(currentHtml, placeholderMap);

                if (currentHtml !== originalHtml) {
                    block.innerHTML = currentHtml;
                }
                // Mark as processed to avoid re-processing by MutationObserver if not needed
                // or if the observer is too aggressive. Consider if this is robust enough.
                // block.dataset.flTablesProcessed = 'true';
            }
        });
    }

    // Inject some default CSS for the tables.
    GM_addStyle(`
        table.fl-generated-table {
            border-collapse: collapse;
            width: auto;
            margin: 1em 0;
            font-size: 0.9em;
            border: 1px solid #666;
        }
        table.fl-generated-table th,
        table.fl-generated-table td {
            border: 1px solid #666;
            padding: 6px 8px;
            text-align: left;
        }
        table.fl-generated-table th {
            background-color: #f0f0f0;
            font-weight: bold;
        }
        /* Example styles for user-defined classes */
        table.fl-generated-table.my-stats-table {
            width: 50%;
            border: 2px solid purple !important;
        }
        table.fl-generated-table.my-stats-table th {
            background-color: lightgoldenrodyellow;
            color: black;
        }
    `);

    function init() {
        processPageForTables();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    const observer = new MutationObserver(mutations => {
        let needsProcessing = false;
        mutations.forEach(mutation => {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches('div.FormattedBlock') || node.querySelector('div.FormattedBlock')) {
                            // If a FormattedBlock is added, or a node containing one
                            // Unset the processed flag for children to allow re-processing
                            const blocksToReset = node.matches('div.FormattedBlock') ? [node] : node.querySelectorAll('div.FormattedBlock');
                            blocksToReset.forEach(b => delete b.dataset.flTablesProcessed);
                            needsProcessing = true;
                        }
                    }
                });
            } else if (mutation.type === 'characterData' && mutation.target.parentNode) {
                 // If text content changes within a FormattedBlock
                const parentBlock = mutation.target.parentNode.closest('div.FormattedBlock');
                if (parentBlock) {
                    delete parentBlock.dataset.flTablesProcessed;
                    needsProcessing = true;
                }
            }
        });

        if (needsProcessing) {
            // A small delay can sometimes help ensure other scripts have finished their manipulation
            setTimeout(processPageForTables, 50);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        characterData: true // Observe changes to text nodes as well
    });

})();