WME PLN Core - XML Handler

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

Fra og med 08.09.2025. Se den nyeste version.

Dette script bør ikke installeres direkte. Det er et bibliotek, som andre scripts kan inkludere med metadirektivet // @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。