WME PLN Core - XML Handler

Módulo para importar y exportar datos en formato XML para WME Place Normalizer. No funciona por sí solo.

As of 2025-09-08. See the latest version.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyforks.org/scripts/548745/1656811/WME%20PLN%20Core%20-%20XML%20Handler.js

// ==UserScript==
// @name         WME PLN Core - XML Handler
// @namespace    https://greasyforks.org/en/users/mincho77
// @version      9.0.0
// @description  Módulo para importar y exportar datos en formato XML para WME Place Normalizer. No funciona por sí solo.
// @author       mincho77
// @license      MIT
// @grant        none
// ==/UserScript==
function exportSharedDataToXml(type = "full") {
    try {
        const _excludedWords = (window.excludedWords instanceof Set) ? window.excludedWords : new Set(Array.isArray(window.excludedWords) ? window.excludedWords : []);
        const _replacementWords = (window.replacementWords && typeof window.replacementWords === 'object') ? window.replacementWords : {};
        const _swapWords = Array.isArray(window.swapWords) ? window.swapWords : [];
        const _editorStats = (window.editorStats && typeof window.editorStats === 'object') ? window.editorStats : {};
        const _excludedPlaces = (window.excludedPlaces instanceof Map) ? window.excludedPlaces : new Map();
        const _processedPlaces = Array.isArray(window.processedPlaces) ? window.processedPlaces : [];

        let xmlParts = [];
        const rootTagName = "WME_PLN_Backup";
        const fileName = "wme_pln_full_backup.xml";

        if (_excludedWords.size === 0 && Object.keys(_replacementWords).length === 0 && _swapWords.length === 0 && Object.keys(_editorStats).length === 0 && _excludedPlaces.size === 0 && _processedPlaces.length === 0) {
            alert("No hay datos para exportar.");
            return;
        }

        if (_excludedWords.size > 0) {
            xmlParts.push("    <words>");
            Array.from(_excludedWords).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).forEach(w => xmlParts.push(`        <word>${xmlEscape(w)}</word>`));
            xmlParts.push("    </words>");
        }

        if (Object.keys(_replacementWords).length > 0) {
            xmlParts.push("    <replacements>");
            Object.entries(_replacementWords).sort((a, b) => a[0].toLowerCase().localeCompare(b[0].toLowerCase())).forEach(([from, to]) => {
                xmlParts.push(`        <replacement from="${xmlEscape(from)}">${xmlEscape(to)}</replacement>`);
            });
            xmlParts.push("    </replacements>");
        }

        if (_swapWords.length > 0) {
            xmlParts.push("    <swapWords>");
            _swapWords.forEach(item => {
                xmlParts.push(`        <swap value="${xmlEscape(item.word || '')}" direction="${xmlEscape(item.direction || 'start')}"/>`);
            });
            xmlParts.push("    </swapWords>");
        }

        if (Object.keys(_editorStats).length > 0) {
            xmlParts.push("    <statistics>");
            Object.entries(_editorStats).forEach(([userId, data]) => {
                xmlParts.push(`        <editor id="${xmlEscape(userId)}" name="${xmlEscape(data?.userName || '')}" total_count="${data?.total_count || 0}" monthly_count="${data?.monthly_count || 0}" monthly_period="${xmlEscape(data?.monthly_period || '')}" weekly_count="${data?.weekly_count || 0}" weekly_period="${xmlEscape(data?.weekly_period || '')}" daily_count="${data?.daily_count || 0}" daily_period="${xmlEscape(data?.daily_period || '')}" last_update="${data?.last_update || 0}" />`);
            });
            xmlParts.push("    </statistics>");
        }

        if (_excludedPlaces.size > 0) {
            xmlParts.push("    <excludedPlaces>");
            Array.from(_excludedPlaces.entries()).sort((a,b) => String(a[0]).localeCompare(String(b[0]))).forEach(([id, name]) => {
                xmlParts.push(`        <place id="${xmlEscape(String(id))}" name="${xmlEscape(String(name || ''))}"/>`);
            });
            xmlParts.push("    </excludedPlaces>");
        }

        if (typeof exportProcessedPlacesSectionXML === 'function' && _processedPlaces.length > 0) {
             xmlParts.push(exportProcessedPlacesSectionXML());
        }

        const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<${rootTagName}>\n${xmlParts.join("\n")}\n</${rootTagName}>`;
        const blob = new Blob([xmlContent], { type: "application/xml;charset=utf-8" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = fileName;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

    } catch (err) {
        console.error('[WME PLN] exportSharedDataToXml error:', err);
        alert('No fue posible exportar el XML. Revisa la consola para más detalles.');
    }
}

function handleXmlFileDrop(file, type = "words") {
    if (!file || !(file.type === "text/xml" || (file.name || '').endsWith(".xml"))) {
        alert("Por favor, arrastra un archivo XML válido.");
        return;
    }
    const reader = new FileReader();
    reader.onload = function(evt) {
        try {
            let newExcludedAdded = 0, newReplacementsAdded = 0, replacementsOverwritten = 0, newSwapWordsAdded = 0, newPlacesAdded = 0;

            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(evt.target.result, "application/xml");
            const parserError = xmlDoc.querySelector("parsererror");
            if (parserError) {
                alert("Error al parsear el archivo XML: " + parserError.textContent);
                return;
            }
            const rootTag = (xmlDoc.documentElement.tagName || '').toLowerCase();

            if (type === "words") {
                if (rootTag !== "excludedwords") {
                    alert("El XML no es válido para Palabras Especiales. Raíz esperada: <ExcludedWords>.");
                    return;
                }
                const wordsNode = xmlDoc.querySelector("excludedwords > words");
                if (wordsNode) {
                    const items = Array.from(wordsNode.querySelectorAll("word")).map(n => (n.textContent || '').trim()).filter(Boolean);
                    if (!window.excludedWords || !(window.excludedWords instanceof Set)) window.excludedWords = new Set();
                    items.forEach(w => { if (!window.excludedWords.has(w)) { window.excludedWords.add(w); newExcludedAdded++; } });
                }
                const replNode = xmlDoc.querySelector("excludedwords > replacements");
                if (replNode) {
                    if (!window.replacementWords || typeof window.replacementWords !== 'object') window.replacementWords = {};
                    Array.from(replNode.querySelectorAll("replacement")).forEach(n => {
                        const from = n.getAttribute("from") || "";
                        const to = (n.textContent || "").trim();
                        if (!from) return;
                        if (from in window.replacementWords) replacementsOverwritten++;
                        else newReplacementsAdded++;
                        window.replacementWords[from] = to;
                    });
                }
                const swapNode = xmlDoc.querySelector("excludedwords > swapwords");
                if (swapNode) {
                    if (!Array.isArray(window.swapWords)) window.swapWords = [];
                    Array.from(swapNode.querySelectorAll("swap")).forEach(n => {
                        const word = n.getAttribute("value") || "";
                        const direction = n.getAttribute("direction") || "start";
                        if (word) {
                            window.swapWords.push({ word, direction });
                            newSwapWordsAdded++;
                        }
                    });
                }
                const statsNode = xmlDoc.querySelector("excludedwords > statistics");
                if (statsNode && window.editorStats && typeof window.editorStats === 'object') {
                    const editorNode = statsNode.querySelector("editor");
                    if (editorNode) {
                        const editorId = editorNode.getAttribute("id");
                        if (editorId && window.editorStats[editorId]) {
                            window.editorStats[editorId] = {
                                userName: editorNode.getAttribute("name") || window.editorStats[editorId].userName,
                                total_count: parseInt(editorNode.getAttribute("total_count"), 10) || 0,
                                monthly_count: parseInt(editorNode.getAttribute("monthly_count"), 10) || 0,
                                monthly_period: editorNode.getAttribute("monthly_period") || '',
                                weekly_count: parseInt(editorNode.getAttribute("weekly_count"), 10) || 0,
                                weekly_period: editorNode.getAttribute("weekly_period") || '',
                                daily_count: parseInt(editorNode.getAttribute("daily_count"), 10) || 0,
                                daily_period: editorNode.getAttribute("daily_period") || '',
                                last_update: parseInt(editorNode.getAttribute("last_update"), 10) || 0
                            };
                        }
                    }
                }

                try { saveExcludedWordsToLocalStorage?.(); } catch (_) { }
                try { saveReplacementWordsToStorage?.(); } catch (_) { }
                try { saveSwapWordsToStorage?.(); } catch (_) { }
                try { saveEditorStats?.(); } catch (_) { }
                try { renderExcludedWordsList?.(document.getElementById("excludedWordsList")); } catch (_) { }
                try { renderReplacementsList?.(document.getElementById("replacementsListElementID")); } catch (_) { }
                try { updateStatsDisplay?.(); } catch (_) { }

                alert(`Importación completada.\n- Palabras nuevas: ${newExcludedAdded}\n- Reemplazos nuevos: ${newReplacementsAdded}\n- Reemplazos sobrescritos: ${replacementsOverwritten}\n- SwapWords importadas: ${newSwapWordsAdded}`);
            } else if (type === "places") {
                if (rootTag !== "excludedplaces") {
                    alert("El XML no es válido para Lugares Excluidos. Raíz esperada: <ExcludedPlaces>.");
                    return;
                }
                if (!(window.excludedPlaces instanceof Map)) window.excludedPlaces = new Map();
                const nodes = xmlDoc.querySelectorAll("excludedplaces > placeids > placeid");
                nodes.forEach(n => {
                    const id = n.getAttribute("id") || "";
                    const name = n.getAttribute("name") || "";
                    if (id && !window.excludedPlaces.has(id)) {
                        window.excludedPlaces.set(id, name);
                        newPlacesAdded++;
                    }
                });
                try { saveExcludedPlacesToLocalStorage?.(); } catch (_) { }
                try { renderExcludedPlacesList?.(document.getElementById("excludedPlacesListUL")); } catch (_) { }
                alert(`Importación completada.\n- Lugares excluidos nuevos: ${newPlacesAdded}`);
            }
        } catch (err) {
            console.error("[WME PLN] Error procesando el XML:", err);
            alert("Ocurrió un error procesando el archivo XML.");
        }
    };
    reader.readAsText(file);
}

function exportProcessedPlacesSectionXML() {
    try {
        if (!Array.isArray(window.processedPlaces) || window.processedPlaces.length === 0) {
            return '';
        }
        let xml = '    <processedPlaces>\n';
        window.processedPlaces.forEach(p => {
            xml += `        <place id="${xmlEscape(p.placeId || '')}"
                       name="${xmlEscape(p.name || '')}"
                       city="${xmlEscape(p.city || '')}"
                       department="${xmlEscape(p.department || '')}"
                       modifiedAt="${xmlEscape(p.modifiedAt || '')}"
                       editorId="${xmlEscape(p.editorId || '')}"
                       editorName="${xmlEscape(p.editorName || '')}" />\n`;
        });
        xml += '    </processedPlaces>';
        return xml;
    } catch (e) {
        console.error('[WME PLN] Error generando XML de lugares procesados:', e);
        return '';
    }
}

function importProcessedPlacesFromXML(xmlString) {
    try {
        if (!xmlString || typeof xmlString !== 'string') return;
        const parser = new DOMParser();
        const xmlDoc = parser.parseFromString(xmlString, "application/xml");
        const placeNodes = xmlDoc.querySelectorAll("processedPlaces > place");
        if (placeNodes.length === 0) return;

        let importedCount = 0;
        placeNodes.forEach(node => {
            const entry = {
                placeId: node.getAttribute('id') || '',
                name: node.getAttribute('name') || '',
                city: node.getAttribute('city') || 'N/A',
                department: node.getAttribute('department') || 'N/A',
                modifiedAt: node.getAttribute('modifiedAt') || new Date().toISOString(),
                editorId: Number(node.getAttribute('editorId')) || 0,
                editorName: node.getAttribute('editorName') || 'Importado'
            };
            if (entry.placeId && entry.editorId) {
                addProcessedPlace(entry);
                importedCount++;
            }
        });
        if (importedCount > 0) {
            plnToast(`✅ Importados ${importedCount} registros de Lugares Procesados.`, 2500);
        }
    } catch (e) {
        console.error('[WME PLN] Error importando XML de lugares procesados:', e);
    }
}

function plnAttachProcessedPlacesToXML(xmlString) {
    try {
        let s = String(xmlString || '');
        if (!s.trim()) return s;
        if (!s.match(/<\w+.*?>/)) s = `<data>${s}</data>`;
        if (typeof exportProcessedPlacesSectionXML === 'function' && !/<processedPlaces[\s>]/i.test(s)) {
            const processedSection = '\n' + exportProcessedPlacesSectionXML() + '\n';
            if (s.includes('</ExcludedWords>')) {
                 s = s.replace(/\n?<\/ExcludedWords>\s*$/i, processedSection + '</ExcludedWords>');
            } else if (s.includes('</data>')) {
                 s = s.replace(/\n?<\/data>\s*$/i, processedSection + '</data>');
            } else {
                 s += processedSection;
            }
        }
        return s;
    } catch (_) {
        return xmlString;
    }
}
长期地址
遇到问题?请前往 GitHub 提 Issues。