Greasy Fork

Wazeopedia Blocks-Library

Biblioteca con la lógica para los bloques de contenido de Wazeopedia.

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/538615/1603508/Wazeopedia%20Blocks-Library.js

// ==UserScript==
// @name         Wazeopedia Blocks Library
// @namespace    http://tampermonkey.net/
// @version      6.1.0
// @description  Biblioteca de lógica para bloques de contenido de Wazeopedia (Título, Bio, FAQ, etc.).
// @author       Annthizze
// @require      https://update.greasyfork.org/scripts/538610/1603505/Wazeopedia%20Core%20UI%20Library.js
// @license      MIT
// ==/UserScript==

'use strict';

(function() {
    // Comprobación robusta: Asegurarse de que la biblioteca UI esté cargada antes de continuar.
    if (typeof window.WazeopediaUI === 'undefined') {
        console.error('FATAL: Wazeopedia Blocks Library cannot initialize because WazeopediaUI is not loaded. Check @require order and URLs.');
        return;
    }

    const WazeopediaBlocks = (function() {
        // ... (TODO EL CÓDIGO DE LA BIBLIOTECA DE BLOQUES SIN CAMBIOS, NO LO MUESTRO POR BREVEDAD) ...
        // Alias para la biblioteca de UI para un uso más limpio dentro de este módulo.
        const UI = window.WazeopediaUI;

        // --- CONSTANTES DE BLOQUES ---
        const TITLE_BLOCK_TOC_MARKER = "<div data-theme-toc=\"true\"> </div>";
        const TITLE_BLOCK_WZBOX_START = "[wzBox]";
        const TITLE_BLOCK_WZBOX_END = "[/wzBox]";
        const TITLE_BLOCK_IMAGE = "[center][wzh=0]![waze64x64|64x64](upload://jRTDuEOGZWkysIHHifIg9ce3nh0.png)[/wzh][/center]";
        const TITLE_STATUS_OPTIONS = {
            aprobado: { label: "Aprobado", text: "> :shield: [color=green]***Artículo aprobado y verificado***[/color]\nEl contenido de este artículo ha sido revisado y aprobado por los ![image|25x25, 100%](upload://vhFGhej3zdZALIqhbHknwTJ1JZk.png) **Champs** de la comunidad. La información aquí presentada es considerada oficial y fiable como guía de referencia." },
            pendiente: { label: "Pendiente de Aprobación", text: "> :hourglass_done: [color=blue]***Artículo pendiente de aprobación***[/color]\nEste artículo ha sido completado y está **pendiente de revisión y aprobación final** por parte de los ![image|25x25, 100%](upload://vhFGhej3zdZALIqhbHknwTJ1JZk.png) <b>Champs</b> de la comunidad. Mientras este mensaje esté visible, el contenido **no debe utilizarse como criterio definitivo**." },
            desarrollo: { label: "En Desarrollo", text: "> :construction: [color=orange]***Artículo en desarrollo***[/color]\nEste artículo está siendo creado o actualizado. La información podría estar incompleta o contener errores. Agradecemos tu paciencia. Si eres ![image|25x25](upload://wsHHONE4FYyBvMShtoQYLmFEJy8.png) **editor wiki**, puedes consultar el [→foro←]({{FORUM_URL}}) para colaborar en su desarrollo.", requiresUrl: true },
            incompleto: { label: "Incompleto", text: "> :puzzle_piece: [color=#FFC300]***Artículo incompleto***[/color]\nA este artículo le falta información relevante o secciones importantes. Si eres ![image|25x25](upload://wsHHONE4FYyBvMShtoQYLmFEJy8.png) <b>editor wiki</b>, por favor, dirígete al [→foro←](https://www.waze.com/discuss/c/editors/spain-usuarios-y-editores/wazeopedia-es/4779) para conocer los detalles y colaborar en su mejora." },
            deficiente: { label: "Deficiente", text: "> :chart_decreasing: [color=orangered]***Información deficiente en el artículo***[/color]\nEl contenido actual de este artículo ha sido señalado como deficiente. Puede contener imprecisiones, estar desactualizado, o carecer de la claridad o fuentes necesarias. Si eres ![image|25x25](upload://wsHHONE4FYyBvMShtoQYLmFEJy8.png) <b>editor wiki</b>, por favor, revisa el [→foro←](https://www.waze.com/discuss/c/editors/spain-usuarios-y-editores/wazeopedia-es/4779) para discutir y aplicar las mejoras necesarias." },
            borrar: { label: "Borrar", text: "> :wastebasket: [color=red]***Artículo pendiente de borrar***[/color]\nEste artículo ha sido marcado para su eliminación por los ![image|25x25, 100%](upload://vhFGhej3zdZALIqhbHknwTJ1JZk.png) **Administradores** de la Wazeopedia Española, generalmente por obsolescencia, contenido incorrecto, duplicidad o incumplimiento de directrices. Para más detalles o alegaciones, consulta el [→foro←](https://www.waze.com/discuss/c/editors/spain-usuarios-y-editores/wazeopedia-es/4779)" }
        };
        const INTRO_BLOCK_HEADER_FULL = "[center][wzh=0]![Info64x64|64x64](upload://1cG8aFsGrCONmfJ4R1Bzb5PP9Ia.png)[/wzh][/center]\n\n# [wzh=1]Introducción[/wzh]";
        const INTRO_NOTE_PREFIX = "> :bookmark: ";
        const INTRO_BLOCK_END_MARKER = "\n\n---";
        const BIO_BLOCK_IMAGE_AND_HEADER = "[center][wzh=0]![image|128x128, 50%](upload://UTuWTJ1XEX6BVzoj1FIhLjAb6i.png)[/wzh][/center]\n\n# [wzh=2]Biografía y Enlaces[/wzh]";
        const MAX_BIO_ENTRIES = 15;
        const FORUM_BLOCK_IDENTIFIER = "# [wzh=1]Foro de discusión:[/wzh]";
        const FORUM_BLOCK_IMAGE = "[center]![image|128x128, 50%](upload://2cmYNNfUCAykbh8vW92usPC9Sf3.png)[/center]";
        const FAQ_BLOCK_HEADER = "# [wzh=1]Preguntas Frecuentes[/wzh]";
        const FAQ_BLOCK_REGEX = /(?:^|\n)---\s*\n+# \[wzh=1\]Preguntas Frecuentes\[\/wzh\]\s*\n+([\s\S]*?)\n+---\s*(?:\n|$)/;
        const FORUM_BLOCK_REGEX_STR = `(?:^|\\n)---` + `\\s*${FORUM_BLOCK_IMAGE.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}` + `\\s*${FORUM_BLOCK_IDENTIFIER.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}` + `[\\s\\S]*?` + `href="https://www\\.waze\\.com/discuss/new-topic\\?category=spain-usuarios-y-editores/wazeopedia-es/4779[^"]*">→aquí←</a>`;

        // --- FUNCIONES PRIVADAS Y HELPERS DE LA BIBLIOTECA ---

        function ensureProperSpacing(currentText, newBlockText, position, relativeBlockData) {
            let before = "", after = "", middle = newBlockText;
            const twoNewlines = "\n\n";
            switch (position) {
                case 'start':
                    before = ""; after = currentText;
                    if (after.trim().length > 0 && !middle.endsWith(twoNewlines) && !after.startsWith("\n")) { middle += (middle.endsWith("\n") ? "\n" : twoNewlines); }
                    else if (after.trim().length > 0 && middle.endsWith("\n") && !middle.endsWith(twoNewlines) && !after.startsWith("\n")){ middle += "\n"; }
                    break;
                case 'end':
                    before = currentText; after = "";
                    if (before.trim().length > 0 && !middle.startsWith(twoNewlines) && !before.endsWith("\n")) { middle = (before.endsWith("\n") ? "\n" : twoNewlines) + middle; }
                    else if (before.trim().length > 0 && !middle.startsWith(twoNewlines) && before.endsWith("\n") && !before.endsWith(twoNewlines) ){ middle = "\n" + middle; }
                    break;
                case 'afterRelative':
                    if (!relativeBlockData) return ensureProperSpacing(currentText, newBlockText, 'start');
                    before = currentText.substring(0, relativeBlockData.endIndex);
                    after = currentText.substring(relativeBlockData.endIndex);
                    if (!before.endsWith(twoNewlines) && !before.endsWith("\n")) middle = twoNewlines + middle;
                    else if (before.endsWith("\n") && !before.endsWith(twoNewlines) && !middle.startsWith("\n")) middle = "\n" + middle;
                    if (after.trim().length > 0 && !middle.endsWith(twoNewlines) && !after.startsWith("\n")) { middle += (middle.endsWith("\n") ? "\n" : twoNewlines); }
                    else if (after.trim().length > 0 && middle.endsWith("\n") && !middle.endsWith(twoNewlines) && !after.startsWith("\n")){ middle += "\n"; }
                    break;
                default: return { textToInsert: newBlockText.trim(), cursorPosition: newBlockText.trim().length };
            }
            return { textToInsert: before + middle + after, cursorPosition: (before + middle).length };
        }

        function parseExistingTitleBlock(editorText) {
            if (!editorText.startsWith(TITLE_BLOCK_TOC_MARKER)) return null;
            const wzBoxStartIndex = editorText.indexOf(TITLE_BLOCK_WZBOX_START);
            if (wzBoxStartIndex === -1) return null;
            const wzBoxEndIndex = editorText.indexOf(TITLE_BLOCK_WZBOX_END, wzBoxStartIndex);
            if (wzBoxEndIndex === -1) return null;
            const content = editorText.substring(wzBoxStartIndex + TITLE_BLOCK_WZBOX_START.length, wzBoxEndIndex);
            const titleMatch = content.match(/\[center\]\[wzh=1\](.*?)\[\/wzh\]\[\/center\]/);
            const title = titleMatch ? titleMatch[1].trim() : "";
            let statusKey = "aprobado", forumUrl = "";
            for (const key in TITLE_STATUS_OPTIONS) {
                if (content.includes(TITLE_STATUS_OPTIONS[key].text.split('***')[1])) {
                    statusKey = key;
                    if (TITLE_STATUS_OPTIONS[key].requiresUrl) {
                        const urlMatch = content.match(/\[→foro←\]\(([^)]+)\)/);
                        forumUrl = urlMatch ? urlMatch[1] : "";
                    }
                    break;
                }
            }
            return { title, statusKey, forumUrl, startIndex: 0, endIndex: wzBoxEndIndex + TITLE_BLOCK_WZBOX_END.length };
        }

        function parseExistingIntroductionBlock(editorText) {
            const fullHeaderSearchIndex = editorText.indexOf(INTRO_BLOCK_HEADER_FULL);
            if (fullHeaderSearchIndex === -1) return null;
            const contentStartAfterFullHeader = fullHeaderSearchIndex + INTRO_BLOCK_HEADER_FULL.length;
            if (!editorText.substring(contentStartAfterFullHeader).startsWith("\n\n")) return null;
            const actualMainTextStartIndex = contentStartAfterFullHeader + 2;
            let endOfBlockIndex = editorText.indexOf(INTRO_BLOCK_END_MARKER, actualMainTextStartIndex);
            if(endOfBlockIndex === -1) return null; // Simple case, no nested blocks
            const blockContentBetween = editorText.substring(actualMainTextStartIndex, endOfBlockIndex);
            let mainText = blockContentBetween, noteText = "", additionalText = "", hasNote = false, hasAdditional = false;
            const noteBlockPattern = "\n\n" + INTRO_NOTE_PREFIX;
            const noteStartIndex = blockContentBetween.indexOf(noteBlockPattern);
            if (noteStartIndex !== -1) {
                hasNote = true;
                mainText = blockContentBetween.substring(0, noteStartIndex).trim();
                const afterNotePrefix = blockContentBetween.substring(noteStartIndex + noteBlockPattern.length);
                const additionalTextSeparator = "\n\n";
                const additionalTextIndex = afterNotePrefix.indexOf(additionalTextSeparator);
                if (additionalTextIndex !== -1) {
                    noteText = afterNotePrefix.substring(0, additionalTextIndex).trim();
                    additionalText = afterNotePrefix.substring(additionalTextIndex + additionalTextSeparator.length).trim();
                    if (additionalText) hasAdditional = true;
                } else {
                    noteText = afterNotePrefix.trim();
                }
            } else {
                mainText = blockContentBetween.trim();
            }
            return { mainText, noteText, additionalText, hasNote, hasAdditional, startIndex: fullHeaderSearchIndex, endIndex: endOfBlockIndex + INTRO_BLOCK_END_MARKER.length };
        }

        function getBioEntryPrefix(dateText) {
            dateText = (dateText || "").trim();
            if (/^\d{1,2} de [a-zA-ZáéíóúÁÉÍÓÚñÑ]+ de \d{4}$/i.test(dateText)) return "* El ";
            if (/^[a-zA-ZáéíóúÁÉÍÓÚñÑ]+ de \d{4}$/i.test(dateText)) return "* En ";
            if (/^\d{4}$/.test(dateText)) return "* En el año ";
            return "* El ";
        }

        function parseExistingBiographyBlock(editorText) {
            const blockStartIndex = editorText.indexOf(BIO_BLOCK_IMAGE_AND_HEADER);
            if (blockStartIndex === -1) return null;
            const contentStartIndex = blockStartIndex + BIO_BLOCK_IMAGE_AND_HEADER.length;
            const nextBlockRegex = /(?:\n\n---|# \[wzh=[12]\]|Foro de discusión:)/;
            const nextBlockMatch = editorText.substring(contentStartIndex).match(nextBlockRegex);
            const endIndex = nextBlockMatch ? contentStartIndex + nextBlockMatch.index : editorText.length;
            const blockContent = editorText.substring(contentStartIndex, endIndex).trim();
            const entries = [];
            if (blockContent) {
                blockContent.split('\n').forEach(line => {
                    if (!line.startsWith('* ')) return;
                    const core = line.substring(2).trim();
                    const linkMatch = core.match(/^(?:El |En el año |En )?\[([^\]]+)\]\(([^)]+)\)\s*(.*)/);
                    if (linkMatch) {
                        entries.push({ dateText: linkMatch[1], url: linkMatch[2], description: linkMatch[3].replace(/\.$/, '') });
                    } else {
                        entries.push({ dateText: '', url: '', description: core.replace(/\.$/, '') });
                    }
                });
            }
            return { entries, startIndex: blockStartIndex, endIndex };
        }
        
        function updateBioEntryPreview(dateInput, urlInput, descInput, previewElement) {
            const dateText = dateInput.value.trim(); const url = urlInput.value.trim(); const description = descInput.value.trim();
            const prefix = getBioEntryPrefix(dateText);
            let descWithPeriod = description; if (descWithPeriod && !/[.!?]$/.test(descWithPeriod)) descWithPeriod += '.';
            previewElement.innerHTML = `* ${prefix.substring(2)}${url ? `[<a href="#" onclick="return false;">${dateText || 'Fecha'}</a>]` : dateText}${description ? ' ' + descWithPeriod : '.'}`;
        }
        
        function createBioEntryElement(entry = { dateText: '', url: '', description: '' }, index, container) {
            const details = document.createElement('details'); details.className = 'wz-bio-entry'; details.name = 'bio-accordion';
            const summary = document.createElement('summary'); summary.appendChild(document.createTextNode(entry.dateText || `Entrada ${index + 1}`));
            const contentDiv = document.createElement('div'); contentDiv.className = 'wz-bio-entry-content';
            contentDiv.innerHTML = `<label>Fecha (texto):</label><input type="text" class="wz-bio-date" placeholder="Ej: 25 de agosto de 2024" value="${entry.dateText}"><label>URL (opcional):</label><input type="text" class="wz-bio-url" placeholder="https://ejemplo.com" value="${entry.url}"><label>Descripción:</label><textarea class="wz-bio-desc">${entry.description}</textarea><div class="wz-bio-preview-label">Previsualización:</div><div class="wz-bio-entry-preview"></div>`;
            const removeBtn = UI.createButton('Eliminar', 'wz-bio-remove-btn', () => { details.remove(); updateBioSummaries(container); });
            summary.appendChild(removeBtn);
            details.append(summary, contentDiv);
            const dateInput = contentDiv.querySelector('.wz-bio-date'), urlInput = contentDiv.querySelector('.wz-bio-url'), descInput = contentDiv.querySelector('.wz-bio-desc'), preview = contentDiv.querySelector('.wz-bio-entry-preview');
            const updateFn = () => {
                updateBioEntryPreview(dateInput, urlInput, descInput, preview);
                summary.firstChild.textContent = dateInput.value.trim() || `Entrada ${Array.from(container.children).indexOf(details) + 1}`;
            };
            [dateInput, urlInput, descInput].forEach(el => el.addEventListener('input', updateFn));
            updateFn();
            return details;
        }

        function updateBioSummaries(container) {
            container.querySelectorAll('details.wz-bio-entry').forEach((details, idx) => {
                const dateInput = details.querySelector('.wz-bio-date');
                details.querySelector('summary').firstChild.textContent = dateInput.value.trim() || `Entrada ${idx + 1}`;
            });
        }
        
        function getForumBlockRegex() { return new RegExp(FORUM_BLOCK_REGEX_STR, 'm'); }
        
        function generateBodyContentAndTitleParams(cleanedPostTitleForDisplay) {
            const isEditing = window.location.href.includes('/t/');
            let linkUrl = isEditing ? window.location.href : `https://www.waze.com/discuss/t/${cleanedPostTitleForDisplay.toLowerCase().replace(/\s+/g, '-').replace(/-+/g, '-').replace(/[^a-z0-9-]/g, '')}`;
            const markdownEscapedPostTitle = cleanedPostTitleForDisplay.replace(/([\[\]\(\)])/g, '\\$1');
            return { bodyContentText: `[${markdownEscapedPostTitle}](${linkUrl})`, urlEncodedTitleForNewTopic: encodeURIComponent(cleanedPostTitleForDisplay) };
        }
        
        function generateFullForumBlock(_, bodyContentTextForTemplate, urlEncodedNewTopicTitle) {
            const bodyParamUnencoded = `Hola editores,\n\nHe leído la información en la Wazeopedia y me gustaría hacer una sugerencia o proponer un cambio relacionado con la información contenida en la pagina de ${bodyContentTextForTemplate}. A continuación detallaré mi idea, error o modificación:\n\n< Pon aquí tu sugerencia, error o cambio >`;
            return `---
${FORUM_BLOCK_IMAGE}
${FORUM_BLOCK_IDENTIFIER}
Si observas cualquier tipo de error en la información aquí contenida, así como si deseas mejorarla o incluso solicitar algún tipo de cambio en los criterios para su uso, puedes informar en el foro correspondiente <a rel="nofollow" class="external text" href="https://www.waze.com/discuss/new-topic?category=spain-usuarios-y-editores/wazeopedia-es/4779&title=WAZO%20-%20${urlEncodedNewTopicTitle}&body=${encodeURIComponent(bodyParamUnencoded)}">→aquí←</a>`;
        }
        
        function showForumUpdateConfirmModal(textarea, existingBlockInfo, newParams, currentParams) {
            UI.closeAllModals();
            const overlay = document.createElement('div'); overlay.className = 'wz-modal-overlay';
            const modalContent = document.createElement('div'); modalContent.className = 'wz-modal-content';
            let htmlContent = `<h3>Estado del Bloque de Discusión</h3><div class="wz-modal-scrollable-content">`;
            let needsUpdate = false;
            if (!existingBlockInfo) {
                htmlContent += `<p>El bloque 'Foro de discusión' no existe en el editor.</p>`;
            } else {
                const bodyContentMatches = currentParams.bodyContent === newParams.bodyContentText;
                htmlContent += `<div class="wz-forum-update-modal-item"><p><span class="status-icon ${bodyContentMatches ? 'wz-status-ok">✔️' : 'wz-status-mismatch">❌'}</span><span class="label">Contenido del enlace (cuerpo):</span></p>${!bodyContentMatches ? `<p><span class="label">Actual:</span> <span class="value">${currentParams.bodyContent || 'No encontrado'}</span></p>` : ''}<p><span class="label">Esperado:</span> <span class="value">${newParams.bodyContentText}</span></p></div>`;
                if (!bodyContentMatches) needsUpdate = true;
                const newTopicTitleMatches = currentParams.urlEncodedTitle === newParams.urlEncodedTitleForNewTopic;
                htmlContent += `<div class="wz-forum-update-modal-item"><p><span class="status-icon ${newTopicTitleMatches ? 'wz-status-ok">✔️' : 'wz-status-mismatch">❌'}</span><span class="label">Título para "Nuevo Tema":</span></p>${!newTopicTitleMatches ? `<p><span class="label">Actual:</span> <span class="value">${decodeURIComponent(currentParams.urlEncodedTitle || 'No encontrado')}</span></p>` : ''}<p><span class="label">Esperado:</span> <span class="value">${decodeURIComponent(newParams.urlEncodedTitleForNewTopic)}</span></p></div>`;
                if (!newTopicTitleMatches) needsUpdate = true;
                if (!needsUpdate) htmlContent += `<p style="text-align:center; color:green; margin-top:15px;">El bloque ya está actualizado.</p>`;
            }
            htmlContent += `</div>`;
            modalContent.innerHTML = htmlContent;
            const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'wz-modal-buttons';
            if (!existingBlockInfo) {
                buttonsDiv.appendChild(UI.createButton('Insertar Bloque', 'wz-confirm', () => {
                    const fullBlock = generateFullForumBlock(newParams.cleanedPostTitleForDisplay, newParams.bodyContentText, newParams.urlEncodedTitleForNewTopic);
                    const { textToInsert: finalContent, cursorPosition } = ensureProperSpacing(textarea.value, fullBlock, 'end');
                    textarea.value = finalContent;
                    textarea.selectionStart = textarea.selectionEnd = cursorPosition;
                    textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
                    UI.closeAllModals();
                }));
            } else if (needsUpdate) {
                buttonsDiv.appendChild(UI.createButton('Actualizar Bloque', 'wz-confirm', () => {
                    const updatedFullBlock = generateFullForumBlock(newParams.cleanedPostTitleForDisplay, newParams.bodyContentText, newParams.urlEncodedTitleForNewTopic);
                    textarea.value = textarea.value.substring(0, existingBlockInfo.startIndex) + updatedFullBlock + textarea.value.substring(existingBlockInfo.endIndex);
                    textarea.selectionStart = textarea.selectionEnd = existingBlockInfo.startIndex + updatedFullBlock.length; textarea.focus();
                    textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); UI.closeAllModals();
                }));
            } else {
                buttonsDiv.appendChild(UI.createButton('Aceptar', 'wz-confirm', UI.closeAllModals));
            }
            buttonsDiv.appendChild(UI.createButton('Cancelar', 'wz-cancel', UI.closeAllModals));
            modalContent.appendChild(buttonsDiv); overlay.appendChild(modalContent); document.body.appendChild(overlay);
        }

        function parseExistingFaqBlock(editorText) {
            const match = editorText.match(FAQ_BLOCK_REGEX);
            if (!match) return null;
            const content = match[1];
            const entries = [];
            const questionRegex = /\*\*🔹 (.*?)\*\*\s*\n(.*?)(?=\n\n\*\*🔹|$(?![\r\n]))/gs;
            let qaMatch;
            while ((qaMatch = questionRegex.exec(content)) !== null) {
                entries.push({ question: qaMatch[1].trim(), answer: qaMatch[2].trim() });
            }
            return {
                entries,
                startIndex: match.index === 0 ? 0 : match.index + 1,
                endIndex: match.index === 0 ? match[0].length : match.index + match[0].length,
            };
        }
        
        function updateFaqEntryPreview(questionInput, answerInput, previewElement) {
            const question = questionInput.value.trim();
            const answer = answerInput.value.trim();
            previewElement.innerHTML = `<strong>🔹 ${question || 'Pregunta'}</strong><br>${answer || 'Respuesta...'}`;
        }
        
        function createFaqEntryElement(entry = { question: '', answer: '' }, index, container) {
            const details = document.createElement('details'); details.className = 'wz-faq-entry'; details.name = 'faq-accordion';
            const summary = document.createElement('summary'); summary.textContent = entry.question || `FAQ #${index + 1}`;
            const contentDiv = document.createElement('div'); contentDiv.className = 'wz-faq-entry-content';
            contentDiv.innerHTML = `<label>Pregunta:</label><input type="text" class="wz-faq-question" placeholder="Escribe la pregunta..." value="${entry.question.replace(/"/g, '"')}"><label>Respuesta:</label><textarea class="wz-faq-answer" placeholder="Escribe la respuesta...">${entry.answer}</textarea><div class="wz-faq-preview-label">Previsualización:</div><div class="wz-faq-entry-preview"></div>`;
            const removeBtn = UI.createButton('Eliminar', 'wz-faq-remove-btn', () => {
                details.remove();
                container.querySelectorAll('.wz-faq-entry summary').forEach((s, i) => {
                    const qInput = s.nextElementSibling.querySelector('.wz-faq-question');
                    if (!s.textContent.startsWith('FAQ #')) return;
                    if (!qInput.value.trim()) s.textContent = `FAQ #${i + 1}`;
                });
            });
            summary.appendChild(removeBtn);
            details.append(summary, contentDiv);
            const questionInput = contentDiv.querySelector('.wz-faq-question');
            const answerInput = contentDiv.querySelector('.wz-faq-answer');
            const previewElement = contentDiv.querySelector('.wz-faq-entry-preview');
            const updateFn = () => {
                updateFaqEntryPreview(questionInput, answerInput, previewElement);
                summary.firstChild.textContent = questionInput.value.trim() || `FAQ #${Array.from(container.children).indexOf(details) + 1}`;
            };
            [questionInput, answerInput].forEach(el => el.addEventListener('input', updateFn));
            updateFn();
            return details;
        }

        // --- API PÚBLICA DE LA BIBLIOTECA ---
        return {
            showTitleConfigModal: function(textarea) {
                UI.closeAllModals();
                const existingData = parseExistingTitleBlock(textarea.value);
                const initial = existingData || { title: "", statusKey: "aprobado", forumUrl: "" };
                const overlay = document.createElement('div'); overlay.className = 'wz-modal-overlay';
                const modalContent = document.createElement('div'); modalContent.className = 'wz-modal-content';
                modalContent.innerHTML = `<h3>Configurar Título y Estado</h3><div class="wz-modal-scrollable-content"><div class="wz-title-modal-error" style="display:none;"></div><label for="wz-title-main">Título Artículo:</label><input type="text" id="wz-title-main" value="${initial.title}"><label for="wz-title-status-select">Estado Artículo:</label><select id="wz-title-status-select">${Object.keys(TITLE_STATUS_OPTIONS).map(k => `<option value="${k}" ${initial.statusKey === k ? 'selected' : ''}>${TITLE_STATUS_OPTIONS[k].label}</option>`).join('')}</select><div id="wz-title-forum-url-section" style="display: ${TITLE_STATUS_OPTIONS[initial.statusKey]?.requiresUrl ? 'block' : 'none'};"><label for="wz-title-forum-url">URL Foro:</label><input type="text" id="wz-title-forum-url" placeholder="https://..." value="${initial.forumUrl}"></div></div>`;
                const errorDiv = modalContent.querySelector('.wz-title-modal-error'), titleInput = modalContent.querySelector('#wz-title-main'), statusSelect = modalContent.querySelector('#wz-title-status-select'), forumUrlSection = modalContent.querySelector('#wz-title-forum-url-section'), forumUrlInput = modalContent.querySelector('#wz-title-forum-url');
                statusSelect.onchange = () => forumUrlSection.style.display = TITLE_STATUS_OPTIONS[statusSelect.value]?.requiresUrl ? 'block' : 'none';
                const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'wz-modal-buttons';
                const saveBtn = UI.createButton(existingData ? 'Actualizar Bloque' : 'Insertar Bloque', 'wz-confirm', () => {
                    const title = titleInput.value.trim(), statusKey = statusSelect.value, forumUrl = forumUrlInput.value.trim();
                    if (!title) { errorDiv.textContent = "Título no puede estar vacío."; errorDiv.style.display = 'block'; return; }
                    if (TITLE_STATUS_OPTIONS[statusKey]?.requiresUrl && !forumUrl) { errorDiv.textContent = 'URL de foro requerida.'; errorDiv.style.display = 'block'; return; }
                    const statusText = TITLE_STATUS_OPTIONS[statusKey].text.replace("{{FORUM_URL}}", forumUrl);
                    const newBlock = `${TITLE_BLOCK_TOC_MARKER}\n\n${TITLE_BLOCK_WZBOX_START}\n${TITLE_BLOCK_IMAGE}\n[center][wzh=1]${title}[/wzh][/center]\n\n${statusText}\n${TITLE_BLOCK_WZBOX_END}`;
                    if (existingData) {
                        textarea.value = newBlock + textarea.value.substring(existingData.endIndex);
                    } else {
                        const { textToInsert, cursorPosition } = ensureProperSpacing(textarea.value, newBlock, 'start');
                        textarea.value = textToInsert;
                        textarea.selectionStart = textarea.selectionEnd = cursorPosition;
                    }
                    UI.closeAllModals();
                });
                buttonsDiv.append(UI.createButton('Cancelar', 'wz-cancel', UI.closeAllModals), saveBtn);
                modalContent.querySelector('.wz-modal-scrollable-content').after(buttonsDiv);
                overlay.appendChild(modalContent); document.body.appendChild(overlay);
                setTimeout(() => titleInput.focus(), 100);
            },

            showIntroductionConfigModal: function(textarea) {
                UI.closeAllModals();
                const existingBlockData = parseExistingIntroductionBlock(textarea.value);
                const initialData = existingBlockData || { mainText: "", noteText: "", additionalText: "", hasNote: false, hasAdditional: false };
                const overlay = document.createElement('div'); overlay.className = 'wz-modal-overlay';
                const content = document.createElement('div'); content.className = 'wz-modal-content';
                content.innerHTML = `<h3>Configurar Bloque de Introducción</h3><div class="wz-modal-scrollable-content"><label for="wz-intro-main">Texto Principal:</label><textarea id="wz-intro-main"></textarea><div class="wz-checkbox-group"><input type="checkbox" id="wz-intro-add-note-check"><label for="wz-intro-add-note-check">Añadir nota</label></div><div id="wz-intro-note-section" class="wz-hidden-section"><label for="wz-intro-note">Texto de Nota (${INTRO_NOTE_PREFIX.trim()} se añadirá):</label><textarea id="wz-intro-note" placeholder="Ej: Edición limitada..."></textarea></div><div class="wz-checkbox-group"><input type="checkbox" id="wz-intro-add-additional-check"><label for="wz-intro-add-additional-check">Añadir texto adicional</label></div><div id="wz-intro-additional-section" class="wz-hidden-section"><label for="wz-intro-additional">Texto Adicional:</label><textarea id="wz-intro-additional"></textarea></div></div>`;
                const mainTextEl = content.querySelector('#wz-intro-main'), addNoteCheckEl = content.querySelector('#wz-intro-add-note-check'), noteSectionEl = content.querySelector('#wz-intro-note-section'), noteTextEl = content.querySelector('#wz-intro-note'), addAdditionalCheckEl = content.querySelector('#wz-intro-add-additional-check'), additionalSectionEl = content.querySelector('#wz-intro-additional-section'), additionalTextEl = content.querySelector('#wz-intro-additional');
                mainTextEl.value = initialData.mainText; noteTextEl.value = initialData.noteText; additionalTextEl.value = initialData.additionalText;
                addNoteCheckEl.checked = initialData.hasNote; if (initialData.hasNote) noteSectionEl.style.display = 'block'; addNoteCheckEl.onchange = () => noteSectionEl.style.display = addNoteCheckEl.checked ? 'block' : 'none';
                addAdditionalCheckEl.checked = initialData.hasAdditional; if (initialData.hasAdditional) additionalSectionEl.style.display = 'block'; addAdditionalCheckEl.onchange = () => additionalSectionEl.style.display = addAdditionalCheckEl.checked ? 'block' : 'none';
                const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'wz-modal-buttons';
                const saveBtn = UI.createButton(existingBlockData ? 'Actualizar Bloque' : 'Insertar Bloque', 'wz-confirm', () => {
                    let blockParts = [INTRO_BLOCK_HEADER_FULL, "\n\n" + mainTextEl.value.trim()];
                    if (addNoteCheckEl.checked) blockParts.push("\n\n" + INTRO_NOTE_PREFIX + noteTextEl.value.trim());
                    if (addAdditionalCheckEl.checked) blockParts.push("\n\n" + additionalTextEl.value.trim());
                    blockParts.push(INTRO_BLOCK_END_MARKER);
                    const finalBlock = blockParts.join('');
                    if (existingBlockData) {
                        const textBefore = textarea.value.substring(0, existingBlockData.startIndex);
                        const textAfter = textarea.value.substring(existingBlockData.endIndex);
                        textarea.value = textBefore + finalBlock + textAfter;
                    } else {
                        const titleBlockData = parseExistingTitleBlock(textarea.value);
                        const { textToInsert, cursorPosition } = ensureProperSpacing(textarea.value, finalBlock, titleBlockData ? 'afterRelative' : 'start', titleBlockData);
                        textarea.value = textToInsert;
                        textarea.selectionStart = textarea.selectionEnd = cursorPosition;
                    }
                    textarea.focus();
                    textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
                    UI.closeAllModals();
                });
                buttonsDiv.appendChild(UI.createButton('Cancelar', 'wz-cancel', UI.closeAllModals)); buttonsDiv.appendChild(saveBtn);
                content.querySelector('.wz-modal-scrollable-content').after(buttonsDiv);
                overlay.appendChild(content); document.body.appendChild(overlay);
                setTimeout(() => mainTextEl.focus(), 100);
            },

            showBiographyConfigModal: function(textarea) {
                UI.closeAllModals();
                const existingBlock = parseExistingBiographyBlock(textarea.value);
                let entries = existingBlock ? existingBlock.entries : [{ dateText: '', url: '', description: '' }];
                const overlay = document.createElement('div'); overlay.className = 'wz-modal-overlay';
                const modalContent = document.createElement('div'); modalContent.className = 'wz-modal-content';
                modalContent.innerHTML = `<h3>Configurar Biografía y Enlaces</h3><div class="wz-bio-modal-error" style="display:none;"></div><div class="wz-modal-scrollable-content"><div id="wz-bio-entry-list"></div></div>`;
                const errorDiv = modalContent.querySelector('.wz-bio-modal-error'), entryList = modalContent.querySelector('#wz-bio-entry-list');
                entries.forEach((entry, i) => entryList.appendChild(createBioEntryElement(entry, i, entryList)));
                const addBtn = UI.createButton('Añadir Entrada', 'wz-bio-add-entry-btn wz-confirm', () => {
                    if (entryList.children.length < MAX_BIO_ENTRIES) {
                        const newEl = createBioEntryElement(undefined, entryList.children.length, entryList);
                        entryList.appendChild(newEl);
                        newEl.open = true;
                        newEl.scrollIntoView({ behavior: 'smooth' });
                    } else { errorDiv.textContent = `Máximo ${MAX_BIO_ENTRIES} entradas.`; errorDiv.style.display = 'block'; }
                });
                modalContent.querySelector('.wz-modal-scrollable-content').appendChild(addBtn);
                const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'wz-modal-buttons';
                const saveButton = UI.createButton(existingBlock ? 'Actualizar Bloque' : 'Insertar Bloque', 'wz-confirm', () => {
                    let bioContent = BIO_BLOCK_IMAGE_AND_HEADER;
                    const currentEntries = Array.from(entryList.querySelectorAll('.wz-bio-entry')).map(el => ({ dateText: el.querySelector('.wz-bio-date').value.trim(), url: el.querySelector('.wz-bio-url').value.trim(), description: el.querySelector('.wz-bio-desc').value.trim() }));
                    if (currentEntries.every(e => !e.dateText && !e.url && !e.description)) {
                         if (existingBlock) {
                            UI.showModal("¿Eliminar bloque de biografía vacío?", "confirm", confirmed => {
                                if (confirmed) { textarea.value = textarea.value.substring(0, existingBlock.startIndex) + textarea.value.substring(existingBlock.endIndex); UI.closeAllModals(); }
                            }, true);
                        }
                        return;
                    }
                    currentEntries.forEach(entry => {
                        if (!entry.dateText && !entry.description) return;
                        const prefix = getBioEntryPrefix(entry.dateText);
                        const link = entry.url ? `[${entry.dateText}](${entry.url})` : entry.dateText;
                        let desc = entry.description; if (desc && !/[.!?]$/.test(desc)) desc += '.';
                        bioContent += `\n${prefix}${link}${desc ? ' ' + desc : '.'}`;
                    });
                    if (existingBlock) {
                        textarea.value = textarea.value.substring(0, existingBlock.startIndex) + bioContent + textarea.value.substring(existingBlock.endIndex);
                    } else {
                        const { textToInsert, cursorPosition } = ensureProperSpacing(textarea.value, bioContent, 'end');
                        textarea.value = textToInsert;
                        textarea.selectionStart = textarea.selectionEnd = cursorPosition;
                    }
                    UI.closeAllModals();
                });
                buttonsDiv.append(UI.createButton('Cancelar', 'wz-cancel', UI.closeAllModals), saveButton);
                modalContent.appendChild(buttonsDiv);
                overlay.appendChild(modalContent);
            },

            applyForumDiscussionFormatting: function(textarea) {
                const titleInputElement = document.getElementById('reply-title');
                if (!titleInputElement) { UI.showModal("Error: Campo de título #reply-title no encontrado.", 'alert'); return; }
                let postTitle = titleInputElement.value.trim();
                if (!postTitle) { UI.showModal("Error: El título del post no puede estar vacío.", 'alert'); return; }
                const cleanedPostTitleForDisplay = postTitle.replace(/:[a-zA-Z0-9\_+-]+:/g, '').trim();
                if (!cleanedPostTitleForDisplay) { UI.showModal("Error: Título (sin emojis) no puede estar vacío.", 'alert'); return; }
                const newGeneratedParams = { ...generateBodyContentAndTitleParams(cleanedPostTitleForDisplay), cleanedPostTitleForDisplay };
                const forumBlockRegex = getForumBlockRegex();
                const existingBlockMatch = textarea.value.match(forumBlockRegex);
                if (!existingBlockMatch) {
                    const fullBlock = generateFullForumBlock(newGeneratedParams.cleanedPostTitleForDisplay, newGeneratedParams.bodyContentText, newGeneratedParams.urlEncodedTitleForNewTopic);
                    const { textToInsert: finalContent, cursorPosition } = ensureProperSpacing(textarea.value, fullBlock, 'end', null);
                    textarea.value = finalContent;
                    textarea.selectionStart = textarea.selectionEnd = cursorPosition;
                    textarea.focus();
                    textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
                } else {
                    const existingBlockText = existingBlockMatch[0];
                    const existingBlockInfo = { text: existingBlockText, startIndex: existingBlockMatch.index, endIndex: existingBlockMatch.index + existingBlockText.length };
                    const bodyMatch = existingBlockText.match(/pagina de (\[.*?\]\(.*?\))/);
                    const newTopicMatch = existingBlockText.match(/title=WAZO%20-%20([^&"]+)/);
                    const currentParams = { bodyContent: (bodyMatch && bodyMatch[1]) || '', urlEncodedTitle: (newTopicMatch && newTopicMatch[1]) || '' };
                    if (currentParams.bodyContent === newGeneratedParams.bodyContentText && currentParams.urlEncodedTitle === newGeneratedParams.urlEncodedTitleForNewTopic) {
                        UI.showModal("El bloque 'Foro de discusión' ya está actualizado.", 'alert');
                    } else {
                        showForumUpdateConfirmModal(textarea, existingBlockInfo, newGeneratedParams, currentParams);
                    }
                }
            },
            
            showFaqConfigModal: function(textarea) {
                UI.closeAllModals();
                const existingBlock = parseExistingFaqBlock(textarea.value);
                const entries = existingBlock ? existingBlock.entries : [{ question: '', answer: '' }];
                const overlay = document.createElement('div');
                overlay.className = 'wz-modal-overlay';
                const modalContent = document.createElement('div');
                modalContent.className = 'wz-modal-content';
                modalContent.innerHTML = `<h3>Configurar Preguntas Frecuentes (FAQs)</h3><div class="wz-faq-modal-error" style="display:none;"></div><div class="wz-modal-scrollable-content"><div id="wz-faq-entry-list"></div></div>`;
                const errorDiv = modalContent.querySelector('.wz-faq-modal-error');
                const entryListContainer = modalContent.querySelector('#wz-faq-entry-list');
                entries.forEach((entry, index) => entryListContainer.appendChild(createFaqEntryElement(entry, index, entryListContainer)));
                const addBtn = UI.createButton('Añadir FAQ', 'wz-faq-add-entry-btn wz-confirm', () => {
                    const newFaq = createFaqEntryElement(undefined, entryListContainer.children.length, entryListContainer);
                    entryListContainer.appendChild(newFaq);
                    newFaq.open = true;
                    newFaq.querySelector('.wz-faq-question').focus();
                });
                modalContent.querySelector('.wz-modal-scrollable-content').appendChild(addBtn);
                const buttonsDiv = document.createElement('div');
                buttonsDiv.className = 'wz-modal-buttons';
                const saveButton = UI.createButton(existingBlock ? 'Actualizar Bloque' : 'Insertar Bloque', 'wz-confirm', () => {
                    const faqEntries = Array.from(entryListContainer.querySelectorAll('.wz-faq-entry')).map(details => ({
                        question: details.querySelector('.wz-faq-question').value.trim(),
                        answer: details.querySelector('.wz-faq-answer').value.trim()
                    })).filter(e => e.question && e.answer);

                    if (faqEntries.length === 0) {
                        if (existingBlock) {
                            UI.showModal("No hay FAQs. ¿Eliminar bloque existente?", "confirm", confirmed => {
                                if (confirmed) {
                                    textarea.value = textarea.value.substring(0, existingBlock.startIndex) + textarea.value.substring(existingBlock.endIndex);
                                    UI.closeAllModals();
                                }
                            }, true);
                        } else {
                            errorDiv.textContent = "No hay entradas para guardar.";
                            errorDiv.style.display = 'block';
                        }
                        return;
                    }

                    let faqContent = faqEntries.map(e => `**🔹 ${e.question}**\n\n${e.answer}`).join('\n\n');
                    let finalBlock = `---\n\n${FAQ_BLOCK_HEADER}\n\n${faqContent}\n\n---`;

                    if (existingBlock) {
                        textarea.value = textarea.value.substring(0, existingBlock.startIndex) + finalBlock + textarea.value.substring(existingBlock.endIndex);
                    } else {
                        const { textToInsert, cursorPosition } = ensureProperSpacing(textarea.value, finalBlock, 'end');
                        textarea.value = textToInsert;
                        textarea.selectionStart = textarea.selectionEnd = cursorPosition;
                    }
                    textarea.focus();
                    UI.closeAllModals();
                });
                buttonsDiv.append(UI.createButton('Cancelar', 'wz-cancel', UI.closeAllModals), saveButton);
                modalContent.appendChild(buttonsDiv);
                overlay.appendChild(modalContent);
            }
        };
    })();

    window.WazeopediaBlocks = WazeopediaBlocks;
    console.log('Wazeopedia Blocks Library 6.1.0 loaded.');

})();