您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Normaliza nombres de lugares en Waze Map Editor (WME)
当前为
// ==UserScript== // @name WME Places Name Normalizer // @namespace https://gf.qytechs.cn/en/users/mincho77 // @version 5.0.5 // @description Normaliza nombres de lugares en Waze Map Editor (WME) // @author mincho77 // @match https://www.waze.com/*editor* // @match https://beta.waze.com/*user/editor* // @grant GM_xmlhttpRequest // @connect api.languagetool.org // @grant unsafeWindow // @license MIT // @run-at document-end // ==/UserScript== /*global W*/ (() => { "use strict"; // Variables globales básicas const SCRIPT_NAME = GM_info.script.name; const VERSION = GM_info.script.version.toString(); // Inicializar la lista de palabras especiales let specialWords = JSON.parse(localStorage.getItem("specialWords")) || []; let maxPlaces = 50; let normalizeArticles = true; let placesToNormalize = []; let wordLists = { excludeWords : JSON.parse(localStorage.getItem("excludeWords")) || [], dictionaryWords : JSON.parse(localStorage.getItem("dictionaryWords")) || [] }; // ============================================== // Inicialización de idioma y diccionarios // ============================================== // Idioma activo: cargado desde memoria o por defecto "SP" let activeDictionaryLang = localStorage.getItem("activeDictionaryLang") || "SP"; // Diccionarios por defecto (solo si no hay nada guardado) const defaultDictionaries = { SP : { a : [ "árbol" ], b : [ "barco" ] }, EN : { a : [ "apple" ], b : [ "boat" ] } }; // Diccionario principal, comenzamos con los valores por defecto const spellDictionaries = { SP : {...defaultDictionaries.SP }, EN : {...defaultDictionaries.EN } }; // Si hay datos guardados en localStorage, los sobreescribimos const savedSP = localStorage.getItem("spellDictionaries_SP"); const savedEN = localStorage.getItem("spellDictionaries_EN"); if (savedSP) { try { spellDictionaries.SP = JSON.parse(savedSP); } catch (e) { console.warn( "❌ Diccionario SP corrupto en memoria, se usará el de ejemplo."); } } if (savedEN) { try { spellDictionaries.EN = JSON.parse(savedEN); } catch (e) { console.warn( "❌ Diccionario EN corrupto en memoria, se usará el de ejemplo."); } } // Crear la lista visible a partir del idioma actual let dictionaryWords = Object.values(spellDictionaries[activeDictionaryLang]).flat().sort(); unsafeWindow.debugLang = activeDictionaryLang; unsafeWindow.debugDict = spellDictionaries; let excludeWords = wordLists.excludeWords || []; // ******************************************************************************************************************************** // Declaración global de placesToNormalize // -------------------------------------------------------------------------------------------------------------------------------- // Prevención global del comportamiento por defecto en drag & drop // (Evita que se abra el archivo en otra ventana) // Se aplican los eventos de arrastre y suelta a todo el documento. // Se previene el comportamiento por defecto para todos los eventos // de arrastre y suelta, excepto en el drop-zone. // Se establece el efecto de arrastre como "none" para evitar // cualquier efecto visual no deseado. // -------------------------------------------------------------------------------------------------------------------------------- ["dragenter", "dragover", "dragleave", "drop"].forEach((evt) => { document.addEventListener(evt, (e) => { // Si el evento ocurre dentro del área de drop-zone, no lo // bloquea if (e.target && e.target.closest && e.target.closest("#drop-zone")) { return; // Permitir que el drop-zone maneje el evento } if (e.target && e.target.closest && e.target.closest("#dictionary-drop-zone")) { return; // Permitir que el dictionary-drop-zone maneje el evento } e.preventDefault(); // Prevenir el comportamiento predeterminado e.stopPropagation(); // Detener la propagación del evento }, { capture : true }); }); // ******************************************************************************************************************************** // Nombre: debugDictionaries // Fecha modificación: 2025-04-15 12:17 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna. Muestra en la consola el idioma activo y el diccionario actual. // Descripción: // Esta función muestra en la consola el idioma activo y el diccionario actual. Se utiliza para depurar // y verificar el estado de los diccionarios. Permite al verificar que el idioma y el // diccionario se han configurado correctamente. La función se puede invocar manualmente desde la // consola del navegador para obtener información sobre el estado actual de los diccionarios. // ********************************************************************************************************************************* unsafeWindow.debugDictionaries = function () { console.log("Idioma activo:", activeDictionaryLang); console.log("Diccionario actual:", spellDictionaries[activeDictionaryLang]); }; // ******************************************************************************************************************************** // Nombre: renderExcludedWordsList // Fecha modificación: 2025-04-15 12:17 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna. Renderiza el panel de palabras excluidas. // Descripción: // Esta función renderiza el panel de palabras excluidas en la interfaz // de usuario. Se encarga de crear la estructura HTML necesaria para // mostrar la lista de palabras excluidas, así como los botones para // agregar, eliminar y editar palabras. La función también maneja la // lógica de búsqueda y filtrado de palabras en la lista. Se utiliza para // permitir al usuario gestionar su lista de palabras excluidas de manera // eficiente. La función se activa al cargar la página y cada vez que se // actualiza la lista de palabras excluidas. Se utiliza para mejorar la // experiencia del usuario al permitirle ver y gestionar fácilmente las // palabras excluidas. La función también se encarga de renderizar la // lista de palabras del diccionario después de agregar una nueva palabra. // ********************************************************************************************************************************* function renderDictionaryWordsList() { const container = document.getElementById("dictionary-words-list"); if (!container) { console.warn(`[${SCRIPT_NAME}] No se encontró el contenedor 'dictionary-words-list'.`); return; } container.innerHTML = ""; // Limpia el contenedor const selectedLang = activeDictionaryLang || "SP"; const dictByLetter = spellDictionaries[selectedLang]; if (!dictByLetter || Object.keys(dictByLetter).length === 0) { console.warn(`[${SCRIPT_NAME}] No hay datos para el idioma activo: ${selectedLang}`); container.innerHTML = "<p>No hay palabras en el diccionario para este idioma.</p>"; return; } const words = Object.values(dictByLetter).flat().sort((a, b) => a.localeCompare(b)); const ul = document.createElement("ul"); ul.style.listStyle = "none"; words.forEach((word) => { const li = document.createElement("li"); li.textContent = word; ul.appendChild(li); }); container.appendChild(ul); } // ******************************************************************************************************************************** // Nombre: showNoPlacesFoundMessage // Fecha modificación: 2025-04-10 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna. Crea un modal que informa al usuario que no se encontraron lugares que cumplan con los criterios actuales. // Descripción: // Muestra un mensaje modal cuando no se encuentran lugares. Este mensaje incluye un botón para cerrar el modal. Se utiliza para // mostrar información al usuario sobre la falta de lugares encontrados. // ******************************************************************************************************************************** function showNoPlacesFoundMessage() { // Crear el modal const modal = document.createElement("div"); modal.className = "no-places-modal-overlay"; modal.innerHTML = ` <div class="no-places-modal"> <div class="no-places-header"> <h3>⚠️ No se encontraron lugares</h3> </div> <div class="no-places-body"> <p>No se encontraron lugares que cumplan con los criterios actuales.</p> <p>Intenta ajustar los filtros o ampliar el área de búsqueda.</p> </div> <div class="no-places-footer"> <button id="close-no-places-btn" class="no-places-btn">Aceptar</button> </div> </div> `; // Agregar el modal al documento document.body.appendChild(modal); // Manejar el evento de cierre document.getElementById("close-no-places-btn") .addEventListener("click", () => { modal.remove(); }); } // Estilos CSS para el mensaje const noPlacesStyles = ` <style> .no-places-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); display: flex; justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s ease-in-out; } .no-places-modal { background: #fff; border-radius: 10px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); width: 90%; max-width: 400px; overflow: hidden; animation: slideIn 0.3s ease-in-out; text-align: center; padding: 20px; } .no-places-header { background: #f39c12; color: white; padding: 15px; font-size: 18px; font-weight: bold; border-radius: 10px 10px 0 0; } .no-places-body { padding: 20px; font-size: 14px; color: #333; } .no-places-footer { padding: 15px; background: #f4f4f4; text-align: center; } .no-places-btn { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; background: #3498db; color: white; transition: background 0.3s, transform 0.2s; } .no-places-btn:hover { background: #2980b9; transform: scale(1.05); } /* Animaciones */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } #dictionary-drop-zone { border: 2px dashed #ccc; padding: 10px; margin: 10px; text-align: center; font-style: italic; color: #555; background-color: #f8f9fa; } @keyframes slideIn { from { transform: translateY(-20px); } to { transform: translateY(0); } } </style> `; // Insertar los estilos en el documento document.head.insertAdjacentHTML("beforeend", noPlacesStyles); // ******************************************************************************************************************************** // Nombre: showModal // Fecha modificación: 2025-04-10 // Autor: mincho77 // Entradas: title (string): Título del modal. // message (string): Mensaje a mostrar en el modal. // confirmText (string): Texto del botón de confirmación. // cancelText (string): Texto del botón de cancelación. // onConfirm (function): Función a ejecutar // al hacer clic en el botón de confirmación. // onCancel (function): Función a ejecutar // al hacer clic en el botón de cancelación. // type (string): Tipo de modal (info, error, warning, question, // success). // autoClose (number): Tiempo en milisegundos para cerrar // automáticamente el modal. // prependText (string): Texto a mostrar antes del mensaje. // Salidas: Ninguna. Crea un modal personalizado con título, mensaje y botones de confirmación y cancelación. Permite al usuario // interactuar // con el modal y ejecutar funciones específicas al hacer clic en los botones. Se utiliza para mostrar mensajes de advertencia, // información o error al usuario. El modal se cierra automáticamente después de un tiempo especificado si se indica. // Descripción: // Esta función crea un modal personalizado que se muestra en la pantalla con un título, un mensaje y botones de confirmación y // cancelación. El modal se puede personalizar con diferentes tipos (info, error, warning, question, success) y se puede cerrar // automáticamente después de un tiempo especificado. Permite al usuario interactuar con el modal y ejecutar funciones específicas // al hacer clic en los botones. Se utiliza para mostrar mensajes de advertencia, información o error al usuario. // El modal se cierra automáticamente después de un tiempo especificado si se indica. // ******************************************************************************************************************************** function showModal({ title, message, confirmText, cancelText, onConfirm, onCancel, type = "info", autoClose = null, prependText = "", }) { // Determinar el ícono según el tipo let icon; switch (type) { case "error": icon = "⛔"; break; case "warning": icon = "⚠️"; break; case "info": icon = "ℹ️"; break; case "question": icon = "❓"; break; case "success": icon = "✅"; break; default: icon = "ℹ️"; break; } const fullMessage = message.replace("{prependText}", prependText); // Crear el modal const modal = document.createElement("div"); modal.className = "custom-modal-overlay"; modal.innerHTML = ` <div class="custom-modal"> <div class="custom-modal-header"> <h3>${icon} ${title}</h3> <button class="close-modal-btn" title="Cerrar">×</button> </div> <div class="custom-modal-body"> <p>${fullMessage}</p> </div> <div class="custom-modal-footer"> ${ cancelText ? `<button id="modal-cancel-btn" class="modal-btn cancel-btn">${ cancelText}</button>` : ""} ${ confirmText ? `<button id="modal-confirm-btn" class="modal-btn confirm-btn">${ confirmText}</button>` : ""} </div> </div> `; // Agregar el modal al documento document.body.appendChild(modal); // Manejar eventos de los botones if (confirmText) { document.getElementById("modal-confirm-btn") .addEventListener("click", () => { if (onConfirm) onConfirm(); // Ejecutar la función de confirmación modal.remove(); // Cerrar el modal }); } if (cancelText) { document.getElementById("modal-cancel-btn") .addEventListener("click", () => { if (onCancel) onCancel(); // Ejecutar la función de cancelación modal.remove(); // Cerrar el modal }); } // Cerrar modal al hacer clic en el botón de cerrar modal.querySelector(".close-modal-btn") .addEventListener("click", () => { modal.remove(); }); // Cerrar automáticamente si se especifica autoClose if (autoClose) { setTimeout(() => { modal.remove(); }, autoClose); } } // Estilos CSS para el modal const modalStyles = ` <style> .custom-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); display: flex; justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s ease-in-out; } .custom-modal { background: #fff; border-radius: 10px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); width: 90%; max-width: 400px; overflow: hidden; animation: slideIn 0.3s ease-in-out; } .custom-modal-header { background: #3498db; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center; } .custom-modal-header h3 { margin: 0; font-size: 18px; } .close-modal-btn { background: none; border: none; color: white; font-size: 20px; cursor: pointer; transition: color 0.3s; } .close-modal-btn:hover { color: #e74c3c; } .custom-modal-body { padding: 20px; font-size: 14px; color: #333; text-align: center; } .custom-modal-footer { display: flex; justify-content: space-between; padding: 15px; background: #f4f4f4; } .modal-btn { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s, transform 0.2s; } .confirm-btn { background: #27ae60; color: white; } .confirm-btn:hover { background: #2ecc71; transform: scale(1.05); } .cancel-btn { background: #e74c3c; color: white; } .cancel-btn:hover { background: #c0392b; transform: scale(1.05); } /* Animaciones */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideIn { from { transform: translateY(-20px); } to { transform: translateY(0); } } </style> `; // Insertar los estilos en el documento document.head.insertAdjacentHTML("beforeend", modalStyles); // ******************************************************************************************************************************** // Nombre: openEditPopup // Fecha modificación: 2025-04-22 06:00 // Autor: mincho77 // Entradas: index (number): Índice de la palabra a editar. // listType (string): Tipo de lista (excludeWords o // dictionaryWords). // Salidas: Ninguna. Abre un popup para editar una palabra en la lista // Descripción: // Esta función abre un popup para editar una palabra en la lista especificada (excludeWords o dictionaryWords). Permite al usuario // modificar la palabra y actualizar la lista correspondiente. Si la palabra ya existe en la lista, se muestra un mensaje de // advertencia. La función también valida que la palabra no esté vacía y que no sea duplicada. Se utiliza para permitir al usuario // gestionar su lista de palabras excluidas o su diccionario ortográfico personalizado. // ******************************************************************************************************************************** function openEditPopup(index, listType = "excludeWords") { const wordList = listType === "dictionaryWords" ? dictionaryWords : excludeWords; const wordToEdit = wordList[index]; if (!wordToEdit) { console.error(`No se encontró la palabra en el índice ${index}`); return; } showModal( { title : "Editar palabra", message : `<input type="text" id="editWordInput" value="${ wordToEdit}" style="width: 95%; padding: 5px; border-radius: 4px; border: 1px solid #ccc;">`, confirmText : "Guardar", cancelText : "Cancelar", type : "question", onConfirm : () => { const newWord = document.getElementById("editWordInput").value.trim(); if (!newWord) { showModal({ title : "Error", message : "La palabra no puede estar vacía.", confirmText : "Aceptar", type : "error" }); return; } if (wordList.includes(newWord) && wordList[index] !== newWord) { showModal({ title : "Duplicada", message : "Esa palabra ya está en la lista.", confirmText : "Aceptar", type : "warning" }); return; } wordList[index] = newWord; if (listType === "dictionaryWords") { wordLists.dictionaryWords = dictionaryWords; localStorage.setItem("dictionaryWords", JSON.stringify(dictionaryWords)); renderDictionaryWordsPanel(); } else { wordLists.excludeWords = excludeWords; localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); } showModal({ title : "Actualizada", message : "La palabra fue modificada correctamente.", confirmText : "Aceptar", type : "success", autoClose : 2000 }); } }); } // *********************************************************************************************************************************************************** // Nombre: waitForElement // Fecha modificación: 2025-04-10 // Autor: mincho77 // Entradas: // - selector (string): El selector CSS del elemento que se desea esperar en el DOM. // - callback (function): Función que se ejecutará una vez que el elemento se encuentre en el DOM. // - interval (number, opcional): Tiempo en milisegundos entre cada intento de búsqueda (por defecto: 300ms). // - maxAttempts (number, opcional): Número máximo de intentos antes de abandonar la búsqueda (por defecto: 20). // Salidas: Ninguna. Ejecuta el callback pasando el elemento encontrado o muestra una advertencia en la // consola si no se encuentra. // Prerrequisitos: - El DOM debe estar cargado. // Descripción: Esta función espera a que un elemento definido por un selector CSS aparezca en el DOM. Utiliza un intervalo de // tiempo (interval) para realizar múltiples comprobaciones, hasta un máximo definido(maxAttempts). Si el elemento se encuentra // dentro de esos intentos, se ejecuta la función callback con el elemento como argumento. Si no se encuentra después de los // intentos máximos, se detiene y se muestra una advertencia en la consola. Esto es útil para asegurarse de que elementos // dinámicos estén disponibles antes de asignarles event listeners o manipularlos. // *********************************************************************************************************************************************************** function waitForElement(selector, callback, interval = 300, maxAttempts = 20) { let attempts = 0; const checkExist = setInterval(() => { const element = document.querySelector(selector); attempts++; if (element) { clearInterval(checkExist); callback(element); } else if (attempts >= maxAttempts) { clearInterval(checkExist); console.warn(`No se encontró el elemento ${ selector} después de ${maxAttempts} intentos.`); } }, interval); } // ******************************************************************************************************************************** // Nombre: configurarCambioIdiomaDiccionario // Fecha modificación: 2025-04-22 // Hora: 07:45 // Autor: mincho77 // Entradas: Ninguna directa (se usa el selector del DOM) // Salidas: Cambia el idioma del diccionario y conserva el contenido anterior // Descripción: Esta función configura el evento de cambio de idioma del diccionario. Guarda el idioma anterior, carga el nuevo // desde localStorage y actualiza el panel. // ******************************************************************************************************************************** function configurarCambioIdiomaDiccionario() { const selector = document.getElementById("dictionaryLanguageSelect"); if (!selector) { setTimeout(configurarCambioIdiomaDiccionario, 200); return; } selector.addEventListener("change", () => { const previousLang = activeDictionaryLang; const newLang = selector.value; if (previousLang && spellDictionaries[previousLang]) { localStorage.setItem( `spellDictionaries_${previousLang}`, JSON.stringify(spellDictionaries[previousLang])); } activeDictionaryLang = newLang; localStorage.setItem("activeDictionaryLang", activeDictionaryLang); const storedDictionary = JSON.parse(localStorage.getItem( `spellDictionaries_${activeDictionaryLang}`)); if (storedDictionary) { spellDictionaries[activeDictionaryLang] = storedDictionary; } else if (!spellDictionaries[activeDictionaryLang] || Object.keys(spellDictionaries[activeDictionaryLang]) .length === 0) { if (activeDictionaryLang === "SP") { spellDictionaries.SP = { a : [ "árbol" ], b : [ "barco" ] }; } else if (activeDictionaryLang === "EN") { spellDictionaries.EN = { a : [ "apple" ], b : [ "boat" ] }; } localStorage.setItem( `spellDictionaries_${activeDictionaryLang}`, JSON.stringify(spellDictionaries[activeDictionaryLang])); } dictionaryWords = Object.values(spellDictionaries[activeDictionaryLang]) .flat() .sort(); renderDictionaryWordsPanel(); }); console.log("Idioma activo:", activeDictionaryLang); console.log("Datos del diccionario:", spellDictionaries[activeDictionaryLang]); } // *********************************************************************************************************************************************************** // Nombre: waitForDictionaryLangSelectAndConfigure // Fecha modificación: 2025-04-22 06:17 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna. Espera a que el selector de idioma del diccionario esté disponible en el DOM y luego configura el evento de // cambio de idioma. // Descripción: // Esta función espera a que el selector de idioma del diccionario esté disponible en el DOM. Una vez que se encuentra, llama a // la función configurarCambioIdiomaDiccionario para configurar el evento de cambio de idioma. Se utiliza para asegurarse de que // el selector esté listo antes de intentar agregarle un event listener. Esto es útil en situaciones donde el DOM se carga de // manera asíncrona o el elementopuede no estar presente en el momento de la ejecución del script. // *********************************************************************************************************************************************************** function waitForDictionaryLangSelectAndConfigure() { const selector = document.getElementById("dictionaryLanguageSelect"); if (selector) { // Asignar el idioma activo al selector selector.value = activeDictionaryLang; // Configurar el evento de cambio de idioma dictionaryWords = Object.values(spellDictionaries[activeDictionaryLang]) .flat() .sort(); // Renderizar el panel del diccionario ortográfico renderDictionaryWordsPanel(); // Configurar el evento de cambio de idioma configurarCambioIdiomaDiccionario(); } else { setTimeout(waitForDictionaryLangSelectAndConfigure, 200); } } // *********************************************************************************************************************************************************** // Nombre: renderSpellDictionaryPanel // Fecha modificación: 2025-04-15 12:17 // Autor: mincho77 // Entradas: Ninguna // Salidas: string: HTML para el panel del diccionario ortográfico. // Descripción: Esta función genera el HTML para el panel del diccionario ortográfico. Incluye un selector para elegir el idioma // del diccionario, un campo de texto para agregar nuevas palabras, un botón para agregar palabras, un campo de búsqueda para // filtrar palabras en la lista, y botones para importar y exportar el diccionario. El panel se puede mostrar u ocultar al hacer // clic en el encabezado. Se utiliza para permitir al usuario gestionar un diccionario ortográfico personalizado // para el normalizador de nombres de lugares. Se incluye un icono representativo para cada idioma (España e Inglaterra) junto a // la opción correspondiente en el selector. El campo de búsqueda permite filtrar las palabras en la lista del diccionario, // facilitando la búsqueda de palabras específicas. Los botones de importar y exportar permiten al usuario gestionar su diccionario // ortográfico, facilitando la importación de palabras desde un archivo XML y la exportación de palabras a un archivo XML. // Se utiliza para mejorar la experiencia del usuario al permitirle personalizar su diccionario ortográfico según sus necesidades. // *********************************************************************************************************************************************************** function renderSpellDictionaryPanel() { return ` <details id="details-dictionary-words" style="margin-top: 15px;"> <summary style="cursor: pointer; font-weight: bold; list-style: none;"> <span id="arrow-dic" style="display: inline-block; transition: transform 0.2s;">▶</span> Diccionario Ortográfico </summary> <!-- Selector de idioma --> <div style="margin-top: 10px;"> <label for="dictionaryLanguageSelect"><b>Idioma activo:</b></label> <select id="dictionaryLanguageSelect" style="width: 100%; margin-top: 5px; padding: 4px;"> <option value="SP">Español 🇪🇸</option> <option value="EN">Inglés 🇬🇧</option> </select> </div> <!-- Buscar palabra --> <div style="margin-top: 10px;"> <input type="text" id="searchDictionaryWord" placeholder="Buscar palabra..." style="width: 100%; padding: 5px; border: 1px solid #ccc; border-radius: 4px;"> </div> <div id="dictionary-words-list" style="margin-top: 10px; max-height: 200px; overflow-y: auto;"> </div> <!-- Botones de archivo --> <div style="margin-top: 10px;"> <button id="exportDictionaryBtn">📤 Exportar Diccionario</button> <button id="importDictionaryBtn">📥 Importar Diccionario</button> <button id="clear-dictionary-btn" style="margin-left: 10px;">🧹 Limpiar Diccionario</button> <input type="file" id="hiddenImportDictionaryInput" accept=".xml" style="display: none;"> </div> <!-- Drag & Drop --> <div id="dictionary-drop-zone" style="border: 2px dashed #ccc; padding: 10px; margin: 10px;"> 📂 Arrastra aquí tu archivo de palabras del diccionario (.xml o .txt) </div> </details> `; } // *********************************************************************************************************************************************************** // Nombre: initializeExcludeWords // Fecha modificación: 2025-03-30 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Prerrequisitos si existen: // - localStorage debe estar disponible. // Descripción: Inicializa la lista de palabras excluidas a partir del localStorage, combinando con las palabras ya cargadas en la // variable global excludeWords y actualizando el almacenamiento local. // *********************************************************************************************************************************************************** function initializeExcludeWords() { const saved = JSON.parse(localStorage.getItem("excludeWords")) || []; wordLists.excludeWords = [...new Set([...saved, ...wordLists.excludeWords ]) ].sort(); excludeWords = wordLists.excludeWords; // Sincronizar localStorage.setItem("excludeWords", JSON.stringify(wordLists.excludeWords)); } // *********************************************************************************************************************************************************** // Nombre: initSearchSpecialWords // Fecha modificación: 2025-04-10 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Descripción: Esta función inicializa la búsqueda de palabras especiales en el panel lateral del normalizador. Agrega un evento de // entrada al campo de búsqueda que filtra los elementos de la lista de palabras especiales según el texto ingresado. Si el campo de // búsqueda no está disponible, espera 200 ms y vuelve a intentar. Esto es útil para permitir al usuario buscar y filtrar palabras // especiales en la lista de manera eficiente. // *********************************************************************************************************************************************************** function initSearchSpecialWords() { const searchInput = document.getElementById("searchWord"); const normalizerSidebar = document.getElementById("normalizer-sidebar"); if (searchInput && normalizerSidebar) { searchInput.addEventListener("input", function() { const query = searchInput.value.toLowerCase().trim(); const items = normalizerSidebar.querySelectorAll("li"); items.forEach(item => { const text = item.querySelector("span")?.textContent.toLowerCase() || ""; item.style.display = text.includes(query) ? "flex" : "none"; }); }); } else { setTimeout(initSearchSpecialWords, 200); } } // *********************************************************************************************************************************************************** // Nombre: getSidebarHTML // Fecha modificación: 2025-04-09 // Autor: mincho77 // Entradas: Ninguna // Salidas: string: HTML para el panel lateral del normalizador. // Descripción: Esta función genera el HTML para el panel lateral del normalizador de nombres de lugares. Incluye opciones para // normalizar artículos, un campo para ingresar el máximo de lugares a buscar, una sección para palabras especiales // con un botón para agregar palabras, un campo de búsqueda, y botones para importar y exportar la lista de palabras especiales. // También incluye un botón para limpiar la lista de palabras especiales. El panel se puede mostrar u ocultar al hacer clic en el // encabezado. Se utiliza para permitir al usuario gestionar su lista de palabras especiales y personalizar el comportamiento del // normalizador de nombres de lugares. El HTML incluye estilos en línea para mejorar la apariencia y la usabilidad del panel. // *********************************************************************************************************************************************************** function getSidebarHTML() { return ` <div id="normalizer-tab"> <h4>Places Name Normalizer <span style="font-size:11px;">${ VERSION}</span></h4> <!-- No Normalizar artículos --> <div style="margin-top: 15px;"> <input type="checkbox" id="normalizeArticles" ${ normalizeArticles ? "checked" : ""}> <label for="normalizeArticles">No Normalizar artículos (el, la, los, ...)</label> </div> <div style="margin-top: 15px;"> <input type="checkbox" id="useSpellingAPI"> <label for="useSpellingAPI">Usar API de ortografía</label> </div> <!-- Máximo de Places a buscar --> <div style="margin-top: 15px;"> <label>Máximo de Places a buscar: </label> <input type="number" id="maxPlacesInput" value="${ maxPlaces}" min="1" max="800" style="width: 60px;"> </div> <!-- Sección de Palabras Especiales --> <details id="details-special-words" style="margin-top: 15px;"> <summary style="cursor: pointer; font-weight: bold; list-style: none;"> <span id="arrow" style="display: inline-block; transition: transform 0.2s;">▶</span> Palabras Especiales </summary> <div style="margin-top: 10px; display: flex; gap: 5px;"> <input type="text" id="excludeWord" placeholder="Agregar palabra..." style="flex: 1; padding: 5px; border: 1px solid #ccc; border-radius: 4px;"> <button id="addExcludeWord" style="background: #3498db; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">Agregar</button> </div> <div style="margin-top: 10px; display: flex; gap: 5px;"> <input type="text" id="searchWord" placeholder="Buscar palabra..." style="flex: 1; padding: 5px; border: 1px solid #ccc; border-radius: 4px;"> </div> <div id="normalizer-sidebar" style="margin-top: 10px; max-height: 200px; overflow-y: auto;"></div> <button id="exportExcludeWords" style="margin-top: 10px;">Exportar Palabras</button> <button id="importExcludeWordsUnifiedBtn" style="margin-top: 5px;">Importar Lista</button> <input type="file" id="hiddenImportInput" accept=".xml,.txt" style="display: none;"> <div style="margin-top: 5px;"> <input type="checkbox" id="replaceExcludeListCheckbox"> <label for="replaceExcludeListCheckbox">Reemplazar lista actual</label> </div> <div id="drop-zone" style="border: 2px dashed #ccc; border-radius: 6px; padding: 15px; margin: 15px 0; text-align: center; font-style: italic; color: #555; background-color: #f8f9fa;"> 📂 Arrastra aquí tu archivo .txt o .xml para importar palabras especiales </div> </details> <hr> <!-- Sección de Diccionario Ortográfico --> ${renderSpellDictionaryPanel()} <hr> <!-- Botón Scan --> <button id="scanPlaces">Scan...</button> </div> <hr> <!-- Botón de limpieza --> <button id="customButton" style="background:rgb(219, 96, 52); color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: bold; margin-top: 10px;"> Eliminar Palabras Especiales </button> `; } // *********************************************************************************************************************************************************** // Nombre: clearExcludeWordsList // Fecha modificación: 2025-04-10 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Prerrequisitos si existen: // - La variable global excludeWords debe estar definida. // - La función renderExcludedWordsPanel debe estar definida. // Descripción: Esta función limpia la lista de palabras excluidas almacenadas en localStorage y actualiza la variable global // excludeWords. // *********************************************************************************************************************************************************** function clearExcludeWordsList() { excludeWords = []; // Limpia la lista de palabras excluidas wordLists.excludeWords = excludeWords; // Sincronizar localStorage.removeItem( "excludeWords"); // Elimina las palabras del almacenamiento local // Limpia manualmente el contenedor antes de renderizar const container = document.getElementById("normalizer-sidebar"); if (container) { container.innerHTML = ""; // Limpia el contenido del contenedor } renderExcludedWordsPanel(); // Refresca la lista en la interfaz showModal({ title : "Lista Limpiada", message : "La lista de palabras excluidas ha sido limpiada.", type : "success", autoClose : 1500 }); } // *********************************************************************************************************************************************************** // Nombre: clearActiveDictionary // Fecha modificación: 2025-04-22 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Prerrequisitos si existen: // - La variable global activeDictionaryLang debe estar definida. // - La variable global spellDictionaries debe estar definida. // - La función renderDictionaryWordsPanel debe estar definida. // Descripción: Esta función limpia el diccionario ortográfico activo, eliminando todas las palabras y actualizando el almacenamiento // local. También muestra un modal de confirmación al usuario. // *********************************************************************************************************************************************************** function clearActiveDictionary() { if (!spellDictionaries[activeDictionaryLang]) { console.warn("⚠️ No se encontró el diccionario del idioma activo."); return; } // Limpiar las letras spellDictionaries[activeDictionaryLang] = {}; // Actualizar localStorage localStorage.setItem( `spellDictionaries_${activeDictionaryLang}`, JSON.stringify(spellDictionaries[activeDictionaryLang])); // Limpiar visualmente dictionaryWords = []; renderDictionaryWordsPanel(); showModal({ title : "Diccionario borrado", message : `Se eliminó todo el contenido del diccionario en idioma ${ activeDictionaryLang}.`, confirmText : "Aceptar", type : "info", }); } // *********************************************************************************************************************************************************** // Nombre: attachEvents // Fecha modificación: 2025-03-30 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Prerrequisitos si existen: // - Deben existir en el DOM los elementos con los siguientes IDs: // "normalizeArticles", "maxPlacesInput", "addExcludeWord", "scanPlaces", // "hiddenImportInput", "importExcludeWordsUnifiedBtn" y // "exportExcludeWords". // - Debe existir la función handleImportList y la función scanPlaces. // - Debe estar definida la variable global excludeWords y la funciónrenderExcludedWordsPanel. Descripción: Esta función adjunta // los event listeners necesarios para gestionar la interacción del usuario con el panel del normalizador de nombres. // Se encargan de: // - Actualizar la opción de normalizar artículos al cambiar el estado del checkbox. // - Modificar el número máximo de lugares a procesar a través de un input. // - Exportar la lista de palabras excluidas a un archivo XML. // - Añadir nuevas palabras a la lista de palabras excluidas, evitando duplicados, y actualizar el panel. // - Activar el botón unificado para la importación de palabras excluidas mediante un input oculto. // - Ejecutar la función de escaneo de lugares al hacer clic en el botón correspondiente. // *********************************************************************************************************************************************************** function attachEvents() { console.log(`[${SCRIPT_NAME}] Adjuntando eventos...`); const normalizeArticlesCheckbox = document.getElementById("normalizeArticles"); const maxPlacesInput = document.getElementById("maxPlacesInput"); const addExcludeWordButton = document.getElementById("addExcludeWord"); const scanPlacesButton = document.getElementById("scanPlaces"); const hiddenInput = document.getElementById("hiddenImportInput"); const importButtonUnified = document.getElementById("importExcludeWordsUnifiedBtn"); // Validación de elementos necesarios if (!normalizeArticlesCheckbox || !maxPlacesInput || !addExcludeWordButton || !scanPlacesButton) { console.error( `[${SCRIPT_NAME}] Error: No se encontraron elementos en el DOM.`); return; } // Evento: cambiar estado de "no normalizar artículos" normalizeArticlesCheckbox.addEventListener( "change", (e) => { normalizeArticles = e.target.checked; }); // Evento: cambiar número máximo de places maxPlacesInput.addEventListener( "input", (e) => { maxPlaces = parseInt(e.target.value, 10); }); // Evento para el botón personalizado const customButton = document.getElementById("customButton"); if (customButton) { customButton.addEventListener("click", () => { showModal({ title : "Confirmación", message : "¿Estás seguro de que deseas limpiar la lista de palabras excluidas?", confirmText : "Sí, limpiar", cancelText : "Cancelar", type : "question", onConfirm: () => { clearExcludeWordsList(); }, // Llama a la función para limpiar la lista}, onCancel : () => { console.log("El usuario canceló la limpieza de la lista."); } }); }); } // Evento: exportar palabras excluidas a XML document.getElementById("exportExcludeWords") .addEventListener("click", () => { const savedWords = JSON.parse(localStorage.getItem("excludeWords")) || []; if (savedWords.length === 0) { showModal({ title : "Error", message : "No hay palabras excluidas para exportar.", confirmText : "Aceptar", onConfirm : () => { console.log("El usuario cerró el modal."); } }); return; } const sortedWords = [...savedWords ].sort((a, b) => a.localeCompare(b)); const xmlContent = `<?xml version="1.0" encoding="UTF-8"?> <ExcludedWords> ${sortedWords.map((word) => ` <word>${word}</word>`).join("\n ")} </ExcludedWords>`; const blob = new Blob([ xmlContent ], { type : "application/xml" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "excluded_words.xml"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); // Evento: añadir palabra excluida sin duplicados addExcludeWordButton.addEventListener("click", () => { const wordInput = document.getElementById("excludeWord") || document.getElementById("excludedWord"); const word = wordInput?.value.trim(); if (!word) return; const lowerWord = word.toLowerCase(); const alreadyExists = excludeWords.some((w) => w.toLowerCase() === lowerWord); if (!alreadyExists) { wordLists.excludeWords.push(word); localStorage.setItem("excludeWords", JSON.stringify(wordLists.excludeWords)); renderExcludedWordsPanel(); // Refresca la lista después de agregar la palabra } wordInput.value = ""; // Limpia el campo de entrada }); // Evento: nuevo botón unificado de importación importButtonUnified.addEventListener("click", () => { hiddenInput.click(); }); hiddenInput.addEventListener("change", () => { handleImportList(); }); // limpiardiccionario waitForElement("#clear-dictionary-btn", (btn) => { btn.addEventListener("click", () => { const confirmClear = confirm( "¿Seguro que deseas borrar TODO el diccionario activo?"); if (confirmClear) clearActiveDictionary(); }); }); // Evento: escanear lugares scanPlacesButton.addEventListener("click", scanPlaces); } // *********************************************************************************************************************************************************** // Nombre: attachDictionarySearch // Fecha modificación: 2025-04-10 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Prerrequisitos si existen: // - Debe existir en el DOM el campo de búsqueda con id "searchDictionaryWord" y el contenedor de palabras del diccionario con id // "dictionary-words-list". Descripción: Esta función adjunta un evento de búsqueda al campo de búsqueda del diccionario ortográfico. // Filtra la lista de palabras mostradas en el contenedor "dictionary-words-list" según la entrada del usuario. Se utiliza para // mejorar la experiencia del usuario al permitirle buscar rápidamente palabras específicas en el diccionario ortográfico. // *********************************************************************************************************************************************************** function attachDictionarySearch() { const dictionarySearchInput = document.getElementById("searchDictionaryWord"); const dictionaryWordsContainer = document.getElementById("dictionary-words-list"); if (!dictionarySearchInput || !dictionaryWordsContainer) { console.error( "[PlacesNameNormalizer] No se encontró el campo 'searchDictionaryWord' o 'dictionary-words-list'."); return; } // Solo modifica .style.display para ocultar/mostrar dictionarySearchInput.addEventListener("input", () => { const query = dictionarySearchInput.value.toLowerCase().trim(); const items = dictionaryWordsContainer.querySelectorAll("li"); items.forEach(item => { const text = item.querySelector("span")?.textContent.toLowerCase() || ""; item.style.display = text.includes(query) ? "flex" : "none"; }); }); } // *********************************************************************************************************************************************************** // Nombre: createSidebarTab // Fecha modificación: 2025-04-22 // Hora: 06:50 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Prerrequisitos si existen: // - Debe existir la función W.userscripts.registerSidebarTab. // Descripción: Esta función crea una pestaña en la barra lateral de WME para el normalizador de nombres de lugares. // Primero, verifica si la pestaña ya existe y la elimina si es necesario. Luego, registra una nueva pestaña utilizando la función // W.userscripts.registerSidebarTab. Si la pestaña se registra correctamente, se configura su contenido y se añaden los eventos necesarios. // Se utiliza para proporcionar una interfaz de usuario para el normalizador de nombres de lugares dentro de WME, permitiendo al usuario // acceder a las funciones del script de manera fácil y rápida. La pestaña incluye opciones para normalizar artículos, un campo // para ingresar el máximo de lugares a buscar, una sección para palabras especiales con un botón para agregar palabras, un campo de búsqueda, // y botones para importar y exportar la lista de palabras especiales. También incluye un botón para limpiar la lista de palabras especiales. // *********************************************************************************************************************************************************** function createSidebarTab() { try { if (!W || !W.userscripts) { console.error( `[${SCRIPT_NAME}] WME not ready for sidebar creation`); return; } const existingTab = document.getElementById("normalizer-tab"); if (existingTab) { console.log(`[${SCRIPT_NAME}] Removing existing tab...`); existingTab.remove(); } let registration; try { registration = W.userscripts.registerSidebarTab("PlacesNormalizer"); } catch (e) { if (e.message.includes("already been registered")) { console.warn(`[${ SCRIPT_NAME}] Tab registration conflict, skipping...`); return; } throw e; } const { tabLabel, tabPane } = registration; if (!tabLabel || !tabPane) { throw new Error( "Tab registration failed to return required elements"); } // Configure tab tabLabel.innerHTML = ` <img src="data:image/jpeg;base64,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" style="height: 16px; vertical-align: middle; margin-right: 5px;"> NrmliZer `; // Inyectar HTML del panel tabPane.innerHTML = getSidebarHTML(); // Esperar que el DOM esté listo antes de adjuntar eventos waitForElement("#normalizeArticles", () => { console.log(`[${ SCRIPT_NAME}] ✅ Elementos del DOM listos, adjuntando eventos`); attachEvents(); }); // Activar búsqueda para palabras especiales initSearchSpecialWords(); // Esperar que el selector de idioma esté en el DOM antes de configurar function waitForDictionaryLangSelectAndConfigure() { const selector = document.getElementById("dictionaryLanguageSelect"); if (selector) { configurarCambioIdiomaDiccionario(); } else { setTimeout(waitForDictionaryLangSelectAndConfigure, 200); } } waitForDictionaryLangSelectAndConfigure(); // Exponer depuración por consola unsafeWindow.debugDictionaries = function() { console.log("Idioma activo:", activeDictionaryLang); console.log("Diccionario actual:", spellDictionaries[activeDictionaryLang]); }; } catch (error) { console.error(`[${SCRIPT_NAME}] Error creating sidebar tab:`, error); } } // ******************************************************************************************************************************** // Nombre: checkSpellingWithAPI // Fecha modificación: 2025-03-30 // Autor: mincho77 // Entradas: text (string) – Texto a evaluar ortográficamente. // Salidas: Promise – Resuelve con lista de errores ortográficos detectados. // Prerrequisitos: Requiere permisos GM_xmlhttpRequest y @connect a // api.languagetool.org Descripción: Consulta la API de LanguageTool para // verificar ortografía del texto. // ******************************************************************************************************************************** function checkSpellingWithAPI(text) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method : "POST", url : "https://api.languagetool.org/v2/check", headers : { "Content-Type" : "application/x-www-form-urlencoded" }, data : `language=es&text=${encodeURIComponent(text)}`, onload : function(response) { if (response.status === 200) { const result = JSON.parse(response.responseText); const errores = result.matches.map( (match) => ({ palabra : match.context.text.substring( match.context.offset, match.context.offset + match.context.length), sugerencia : match.replacements.length > 0 ? match.replacements[0].value : match.context .text // Mantener la palabra original si // no hay sugerencias })); resolve(errores); } else { reject("❌ Error en respuesta de LanguageTool"); } }, onerror : function( err) { reject("❌ Error de red al contactar LanguageTool"); } }); }); } window.checkSpellingWithAPI = checkSpellingWithAPI; // ******************************************************************************************************************************** // Nombre: evaluarOrtografiaCompleta // Fecha modificación: 2025-04-10 22:15 GMT-5 // Autor: mincho77 // Entradas: // - texto (string): Texto a evaluar // - config (opcional): { // usarAPI: true, // Usar LanguageTool // reglasLocales: true, // Aplicar reglas de tildes // timeout: 5000 // Tiempo máximo para API // } // Salidas: // Promise<{ // original: string, // normalizado: string, // errores: Array<{ // palabra: string, // sugerencia: string, // tipo: 'ortografia'|'tilde'|'gramatica', // severidad: 'alta'|'media'|'baja' // }>, // metadata: { // totalErrores: number, // apiUsada: boolean, // tiempoProcesamiento: number // } // }> // Descripción: // Sistema completo que combina normalización y revisión ortográfica real // ******************************************************************************************************************************** async function evaluarOrtografiaCompleta(texto, config = {}) { const inicio = Date.now(); const resultadoBase = { original : texto, normalizado : "", errores : [], metadata : { totalErrores : 0, apiUsada : false, tiempoProcesamiento : 0 } }; // 1. Normalización básica inicial const normalizado = await normalizePlaceName(texto, true); resultadoBase.normalizado = normalizado; // 2. Detección de errores locales (síncrono) if (config.reglasLocales !== false) { const erroresLocales = detectarErroresLocales(texto, normalizado); resultadoBase.errores.push(...erroresLocales); } // 3. Revisión con API LanguageTool (asíncrono) if (config.usarAPI !== false && texto.length > 1) { try { const resultadoAPI = await revisarConLanguageTool(texto, config.timeout); resultadoBase.errores.push(...resultadoAPI.errores); resultadoBase.metadata.apiUsada = true; } catch (error) { console.error("Error en API LanguageTool:", error); } } // 4. Filtrar y clasificar errores resultadoBase.errores = filtrarErrores(resultadoBase.errores); resultadoBase.metadata.totalErrores = resultadoBase.errores.length; resultadoBase.metadata.tiempoProcesamiento = Date.now() - inicio; return resultadoBase; } // ==================== FUNCIONES DE SOPORTE ==================== // ******************************************************************************************************************************** // Nombre: detectarErroresLocales // Descripción: Detecta errores de tildes y mayúsculas // ******************************************************************************************************************************** function detectarErroresLocales(original, normalizado) { const errores = []; const palabrasOriginal = original.split(/\s+/); const palabrasNormalizado = normalizado.split(/\s+/); palabrasOriginal.forEach((palabra, i) => { const palabraNormalizada = palabrasNormalizado[i] || palabra; // 1. Comparación directa para detectar cambios if (palabra !== palabraNormalizada) { errores.push({ palabra, sugerencia : palabraNormalizada, tipo : "ortografia", severidad : "media" }); } // 2. Detección específica de tildes if (tieneTildesIncorrectas(palabra)) { errores.push({ palabra, sugerencia : corregirTildeLocal(palabra), tipo : "tilde", severidad : "alta" }); } }); return errores; } // ******************************************************************************************************************************** // Nombre: revisarConLanguageTool // Fecha modificación: 2025-04-10 22:15 GMT-5 // Autor: mincho77 // Entradas: // - texto (string): Texto a evaluar // - timeout (opcional): Tiempo máximo para la API (en milisegundos) // Salidas: // Promise<{ // errores: Array<{ // palabra: string, // sugerencia: string, // tipo: 'ortografia'|'gramatica', // severidad: 'alta'|'media' // }>, // apiStatus: // 'success'|'timeout'|'parse_error'|'api_error'|'network_error' // }> // Prerrequisitos: Requiere permisos GM_xmlhttpRequest y @connect a // api.languagetool.org Descripción: Consulta la API para errores // ortográficos y gramaticales // ******************************************************************************************************************************** // Descripción: Consulta la API para errores avanzados // ******************************************************************************************************************************** function revisarConLanguageTool(texto, timeout = 5000) { return new Promise((resolve) => { const timer = setTimeout( () => { resolve({ errores : [], apiStatus : "timeout" }); }, timeout); GM_xmlhttpRequest({ method : "POST", url : "https://api.languagetool.org/v2/check", headers : { "Content-Type" : "application/x-www-form-urlencoded" }, data : `language=es&text=${encodeURIComponent(texto)}`, onload : function(response) { clearTimeout(timer); if (response.status === 200) { try { const data = JSON.parse(response.responseText); const errores = data.matches.map((match) => { // Validar que match y sus propiedades existan const palabra = match?.context?.text?.substring( match?.context?.offset || 0, (match?.context?.offset || 0) + (match?.context?.length || 0)) || "(sin contexto)"; const sugerencia = match?.replacements?.[0]?.value || match?.context?.text || "(sin sugerencia)"; const tipo = "ortografia"; // Valor predeterminado ya que // se eliminó la categoría const severidad = match?.rule?.issueType === "misspelling" ? "alta" : "media"; return { palabra, sugerencia, tipo, severidad }; }); resolve({ errores, apiStatus : "success" }); } catch (e) { resolve( { errores : [], apiStatus : "parse_error" }); } } else { resolve({ errores : [], apiStatus : "api_error" }); } }, onerror : function() { clearTimeout(timer); resolve({ errores : [], apiStatus : "network_error" }); } }); }); } // ******************************************************************************************************************************** // Nombre: filtrarErrores // Descripción: Elimina duplicados y errores menores // ******************************************************************************************************************************** function filtrarErrores(errores) { const unicos = []; const vistas = new Set(); errores.forEach((error) => { const clave = `${error.palabra}-${error.sugerencia}-${error.tipo}`; if (!vistas.has(clave)) { vistas.add(clave); unicos.push(error); } }); return unicos.sort((a, b) => { if (a.severidad === b.severidad) return 0; return a.severidad === "alta" ? -1 : 1; }); } // ******************************************************************************************************************************** // Nombre: tieneTildesIncorrectas // Fecha modificación: 2025-04-10 21:30 GMT-5 // Autor: mincho77 // Entradas: // - palabra (string): Palabra a evaluar // - config (opcional): { // ignorarMayusculas: true, // considerarAdverbios: true, // considerarMonosílabos: false // } // Salidas: boolean - true si la palabra requiere corrección de tilde // Descripción: // Evalúa si una palabra en español tiene tildes incorrectas según las // reglas RAE. Incluye casos especiales para adverbios, hiatos, diptongos y // monosílabos. // ******************************************************************************************************************************** function tieneTildesIncorrectas(palabra, config = {}) { if (typeof palabra !== "string" || palabra.length === 0) return false; const settings = { ignorarMayusculas : config.ignorarMayusculas !== false, // No marcar errores en MAYÚSCULAS considerarAdverbios : config.considerarAdverbios !== false, // Evaluar adverbios terminados en -mente considerarMonosílabos : config.considerarMonosílabos || false, // Seguir reglas pre-2010 }; // Normalizar palabra (quitar tildes existentes para evaluación) const palabraNormalizada = palabra.normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase(); const tieneTildeActual = /[áéíóú]/.test(palabra); // 1. Reglas para palabras específicas (excepciones) const reglasEspecificas = { // Adverbios terminados en -mente mente : settings.considerarAdverbios && /mente$/i.test(palabra) ? tieneTildesIncorrectas(palabra.replace(/mente$/i, ""), config) : false, // Monosílabos monosilabos : settings.considerarMonosílabos && [ "fe", "fue", "fui", "vio", "dio", "lia", "lie", "lio", "rion", "ries", "se", "te", "de", "si", "ti" ].includes(palabraNormalizada), // Casos especiales solo : palabraNormalizada === "solo" && !tieneTildeActual, este : /^este(s)?$/i.test(palabraNormalizada) && !tieneTildeActual, aun : palabraNormalizada === "aun" && !tieneTildeActual, guion : palabraNormalizada === "guion" && !tieneTildeActual, hui : palabraNormalizada === "hui" && !tieneTildeActual }; if (Object.values(reglasEspecificas).some((v) => v)) return true; // 2. Reglas generales de acentuación const silabas = separarSilabas(palabraNormalizada); const numSilabas = silabas.length; const ultimaLetra = palabraNormalizada.slice(-1); // Palabras agudas (tildan en última sílaba) if (numSilabas === 1) return false; // Monosílabos ya evaluados const esAguda = numSilabas === 1 || (numSilabas > 1 && silabas[numSilabas - 1].acento); const debeTildarAguda = esAguda && /[nsaeiouáéíóú]$/i.test(palabraNormalizada); const palabraLower = palabra.toLowerCase(); if (correccionesEspecificas[palabraLower]) { return aplicarCapitalizacion(palabra, correccionesEspecificas[palabraLower]); } // Determinar sílaba a tildar if (numSilabas > 2 && esEsdrujula(palabra)) { silabaTildada = numSilabas - 3; } else if (numSilabas > 1 && esGrave(palabra)) { silabaTildada = numSilabas - 2; } else if (esAguda(palabra)) { silabaTildada = numSilabas - 1; } if (silabaTildada >= 0) { return aplicarTildeSilaba(palabra, silabas, silabaTildada); } return palabra; } // ==================== FUNCIONES AUXILIARES ==================== // ******************************************************************************************************************************** // Nombre: separarSilabas // Fecha modificación: 2025-04-10 22:15 GMT-5 // Autor: mincho77 // Entradas: palabra (string) – Palabra a separar en sílabas. // Salidas: Array<{ texto: string, acento: boolean }> – Lista de sílabas // Descripción: Separa la palabra en sílabas y determina si cada sílaba // tiene acento. Implementación simplificada para propósitos de // normalización visual. // ******************************************************************************************************************************** function separarSilabas(palabra) { // Implementación simplificada (usar librería completa en producción) const vocalesFuertes = /[aeoáéó]/; const vocalesDebiles = /[iuü]/; const silabas = []; let silabaActual = ""; let tieneVocalFuerte = false; for (let i = 0; i < palabra.length; i++) { const c = palabra[i]; silabaActual += c; if (vocalesFuertes.test(c)) { tieneVocalFuerte = true; } // Lógica simplificada de separación if (i < palabra.length - 1 && ((vocalesFuertes.test(c) && vocalesFuertes.test(palabra[i + 1])) || (vocalesDebiles.test(c) && vocalesFuertes.test(palabra[i + 1]) && !tieneVocalFuerte))) { silabas.push( { texto : silabaActual, acento : tieneVocalFuerte }); silabaActual = ""; tieneVocalFuerte = false; } } if (silabaActual) { silabas.push({ texto : silabaActual, acento : tieneVocalFuerte }); } return silabas; } // ******************************************************************************************************************************** // Nombre: aplicarCapitalizacion // Fecha modificación: 2025-04-10 22:15 GMT-5 // Autor: mincho77 // Entradas: original (string) – Palabra original // corregida (string) – Palabra corregida // Salidas: string – Palabra corregida con mayúsculas/minúsculas // Descripción: Aplica mayúsculas/minúsculas a la palabra corregida // según la original. Mantiene mayúsculas y minúsculas en la primera letra // y el resto de la palabra. // ******************************************************************************************************************************** function aplicarCapitalizacion(original, corregida) { if (original === original.toUpperCase()) { return corregida.toUpperCase(); } else if (original[0] === original[0].toUpperCase()) { return corregida[0].toUpperCase() + corregida.slice(1); } return corregida; } // ******************************************************************************************************************************** // Nombre: aplicarTildeSilaba // Fecha modificación: 2025-04-10 22:15 GMT-5 // Autor: mincho77 // Entradas: palabra (string) – Palabra original // silabas (Array<{ texto: string, acento: boolean }>) – Lista de // sílabas // indiceSilaba (number) – Índice de la sílaba a tildar // Salidas: string – Palabra con tilde aplicada // Descripción: Aplica tilde a la sílaba especificada // según las reglas de acentuación. La sílaba se identifica por su índice // en la lista de sílabas. La función asume que la palabra ya ha sido // separada en sílabas y que el índice es válido. // ******************************************************************************************************************************** function aplicarTildeSilaba(palabra, silabas, indiceSilaba) { let resultado = ""; let posActual = 0; silabas.forEach((silaba, i) => { if (i === indiceSilaba) { const conTilde = silaba.texto.replace( /([aeiou])([^aeiou]*)$/, (match, vocal, resto) => { return ( vocal.normalize("NFD").replace(/[\u0300-\u036f]/g, "") + "́" + resto); }); resultado += conTilde; } else { resultado += silaba.texto; } }); return resultado; } // ******************************************************************************************************************************** // Nombre: applyNormalization // Fecha modificación: 2025-04-15 // Hora: 13:30:00 // Autor: mincho77 // Entradas: Ninguna directamente (usa el arreglo `changes` ya cargado) // Salidas: Aplica acciones en WME y muestra resultados // Prerrequisitos: `changes` debe contener objetos válidos con `place`, // `newName`, y opcionalmente `delete` // ******************************************************************************************************************************** function applyNormalization(changes) { if (!Array.isArray(changes) || changes.length === 0) { showModal({ title : "Información", message : "No hay cambios seleccionados para aplicar", confirmText : "Aceptar", type : "info" }); return; } let lastAttemptedPlace = null; let cambiosRechazados = 0; try { changes.forEach((change) => { lastAttemptedPlace = { name : change.originalName || change.place.attributes?.name || "Sin nombre", id : change.place.getID?.() || "ID no disponible" }; if (change.delete) { const DeleteObject = require("Waze/Action/DeleteObject"); const action = new DeleteObject(change.place); W.model.actionManager.add(action); } else { const UpdateObject = require("Waze/Action/UpdateObject"); const action = new UpdateObject(change.place, { name : change.newName }); W.model.actionManager.add(action); } }); observarErroresDeWME(changes.length, lastAttemptedPlace); W.controller?.setModified?.(true); showModal({ title : "Éxito", message : `${ changes .length} cambio(s) enviados. Clic en Guardar para aplicar en WME.`, type : "success", autoClose : 2000 }); } catch (error) { console.error("Error aplicando cambios:", error); showModal({ title : "Error", message : "Error al aplicar cambios. Ver consola para detalles.", confirmText : "Aceptar", type : "error" }); } } // ******************************************************************************************************************************** // Nombre: evaluarOrtografiaConTildes // Fecha modificación: 2025-04-02 // Autor: mincho77 // Entradas: name (string) - Nombre del lugar // Salidas: objeto con errores detectados // Descripción: // Evalúa palabra por palabra si falta una tilde en las palabras que lo // requieren, según las reglas del español. Primero normaliza el nombre y // luego verifica si las palabras necesitan una tilde. // ******************************************************************************************************************************** function evaluarOrtografiaConTildes(name) { // Si el nombre está vacío, retornar inmediatamente una promesa resuelta if (!name) { return Promise.resolve( { hasSpellingWarning : false, spellingWarnings : [] }); } const palabras = name.trim().split(/\s+/); const spellingWarnings = []; console.log( `[evaluarOrtografiaConTildes] Verificando ortografía de: ${name}`); palabras.forEach( async ( palabra, index) => { // Normalizar la palabra antes de cualquier verificación let normalizada = await normalizePlaceName(palabra, true); // Ignorar palabras con "&" o que sean emoticonos if (/^[A-Za-z]&[A-Za-z]$/.test(normalizada) || /^[\u263a-\u263c\u2764\u1f600-\u1f64f\u1f680-\u1f6ff]+$/.test( normalizada)) { return; // No verificar ortografía } // Excluir palabras específicas como "y" o "Y" if (normalizada.toLowerCase() === "y" || /^\d+$/.test(normalizada) || normalizada === "-") { return; // Ignorar } // Excluir palabras específicas como "e" o "E" if (normalizada.toLowerCase() === "e" || /^\d+$/.test(normalizada) || normalizada === "-") { return; // Ignorar } // Verificar si la palabra está en la lista de excluidas if (excludeWords.some((w) => w.toLowerCase() === normalizada.toLowerCase())) { return; // Ignorar palabra excluida } // Validar que no tenga más de una tilde const cantidadTildes = (normalizada.match(/[áéíóú]/g) || []).length; if (cantidadTildes > 1) { spellingWarnings.push({ original : palabra, sugerida : null, // No hay sugerencia válida tipo : "Error de tildes", posicion : index }); return; } // Verificar ortografía usando la API de LanguageTool checkSpellingWithAPI(normalizada) .then((errores) => { errores.forEach((error) => { spellingWarnings.push({ original : error.palabra, sugerida : error.sugerencia, tipo : "LanguageTool", posicion : index }); }); }) .catch((err) => { console.error( "Error al verificar ortografía con LanguageTool:", err); }); }); return { hasSpellingWarning : spellingWarnings.length > 0, spellingWarnings }; } // ******************************************************************************************************************************** // Nombre: toggleSpinner // Fecha modificación: 2025-03-31 // Autor: mincho77 // Entradas: // show (boolean) - true para mostrar el spinner, false para ocultarlo // message (string, opcional) - mensaje personalizado a mostrar junto al // spinner Salidas: ninguna (modifica el DOM) Prerrequisitos: debe existir // el estilo CSS del spinner en el documento Descripción: Muestra u oculta // un indicador visual de carga con un mensaje opcional. El spinner usa un // emoji de reloj de arena (⏳) con animación de rotación para indicar que // el proceso está en curso. // ******************************************************************************************************************************** function toggleSpinner( show, message = "Revisando ortografía...", progress = null) { let existingSpinner = document.querySelector(".spinner-overlay"); if (existingSpinner) { if (show) { // Actualizar el mensaje y el progreso si el spinner ya existe const spinnerMessage = existingSpinner.querySelector(".spinner-message"); spinnerMessage.innerHTML = ` ${message} ${ progress !== null ? `<br><strong>${progress}% completado</strong>` : ""} `; } else { existingSpinner.remove(); // Ocultar el spinner } return; } if (show) { const spinner = document.createElement("div"); spinner.className = "spinner-overlay"; spinner.innerHTML = ` <div class="spinner-content"> <div class="spinner-icon">⏳</div> <div class="spinner-message"> ${message} ${ progress !== null ? `<br><strong>${progress}% completado</strong>` : ""} </div> </div> `; document.body.appendChild(spinner); } } // Agregar los estilos CSS necesarios const spinnerStyles = ` <style> .spinner-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; } .spinner-content { background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } .spinner-icon { font-size: 24px; margin-bottom: 10px; animation: spin 1s linear infinite; /* Aseguramos que la animación esté activa */ display: inline-block; } .spinner-message { color: #333; font-size: 14px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>`; // Insertar los estilos al inicio del documento document.head.insertAdjacentHTML("beforeend", spinnerStyles); if (!Array.prototype.flat) { Array.prototype.flat = function(depth = 1) { return this.reduce(function(flat, toFlatten) { return flat.concat(Array.isArray(toFlatten) ? toFlatten.flat(depth - 1) : toFlatten); }, []); }; } // ******************************************************************************************************************************** // Nombre: validarWordSpelling // Fecha modificación: 2025-03-30 // Autor: mincho77 // Entradas: palabra (string) - Palabra en español a validar // ortográficamente Salidas: true si cumple reglas ortográficas básicas, // false si no Descripción: Evalúa si una palabra tiene el uso correcto de // tilde o si le falta una tilde según las reglas del español: esdrújulas // siempre con tilde, agudas con tilde si terminan en n, s o vocal, y llanas // con tildse si NO terminan en n, s o vocal. Se asegura que solo haya una // tilde por palabra. // ******************************************************************************************************************************** function validarWordSpelling(palabra) { if (!palabra) return false; // Ignorar siglas con formato X&X if (/^[A-Za-z]&[A-Za-z]$/.test(palabra)) return true; // Si la palabra es un número, no necesita validación if (/^\d+$/.test(palabra)) return true; const tieneTilde = /[áéíóú]/.test(palabra); const cantidadTildes = (palabra.match(/[áéíóú]/g) || []).length; if (cantidadTildes > 1) return false; // Solo se permite una tilde const silabas = palabra.normalize("NFD") .replace(/[^aeiouAEIOU\u0300-\u036f]/g, "") .match(/[aeiouáéíóú]+/gi); if (!silabas || silabas.length === 0) return false; const totalSilabas = silabas.length; const ultimaLetra = palabra.slice(-1).toLowerCase(); let tipo = ""; if (totalSilabas >= 3 && /[áéíóú]/.test(palabra)) { tipo = "esdrújula"; } else if (totalSilabas >= 2) { const penultimaSilaba = silabas[totalSilabas - 2]; if (/[áéíóú]/.test(penultimaSilaba)) tipo = "grave"; } if (!tipo) tipo = /[áéíóú]/.test(silabas[totalSilabas - 1]) ? "aguda" : "sin tilde"; if (tipo === "esdrújula") return tieneTilde; if (tipo === "aguda") { return ((/[nsáéíóúaeiou]$/.test(ultimaLetra) && tieneTilde) || (!/[nsáéíóúaeiou]$/.test(ultimaLetra) && !tieneTilde)); } if (tipo === "grave") { return ((!/[nsáéíóúaeiou]$/.test(ultimaLetra) && tieneTilde) || (/[nsáéíóúaeiou]$/.test(ultimaLetra) && !tieneTilde)); } return true; } // ******************************************************************************************************************************** // Nombre: escapeHtml // Fecha modificación: 2025-06-20 18:30 GMT-5 // Autor: mincho77 // Entradas: // - unsafe (string|any): Valor a escapar // Salidas: // - string: Texto escapado seguro para usar en HTML // Prerrequisitos: // - Ninguno // Descripción: // Convierte caracteres especiales en entidades HTML para prevenir XSS. // Escapa los siguientes caracteres: // & → & // < → < // > → > // " → " // ' → ' // Si el input no es string, lo convierte a string. // Devuelve string vacío si el input es null/undefined. // ******************************************************************************************************************************** function escapeHtml(unsafe) { if (unsafe === null || unsafe === undefined) return ""; return String(unsafe) .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } let cambiosRechazados = 0; //********************************************************************** // Nombre: observarErroresDeWME // Fecha modificación: 2025-04-15 // Hora: 13:01:25 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Descripción: Observa errores de WME y muestra un modal si se detecta // un mensaje de error relacionado con restricciones de edición. // Prerrequisitos: Ninguno //********************************************************************** function observarErroresDeWME(totalEsperado, lastAttemptedPlace) { const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { for (const node of mutation.addedNodes) { if (node.nodeType === 1 && node.innerText?.includes( "That change isn't allowed at this time")) { observer.disconnect(); const ahora = new Date().toLocaleString("es-CO"); const historico = JSON.parse( localStorage.getItem("rechazosWME") || "[]"); historico.push({ timestamp : ahora, motivo : "Cambio no permitido por WME", lugar : lastAttemptedPlace?.name || "Desconocido", id : lastAttemptedPlace?.id || "N/A" }); localStorage.setItem("rechazosWME", JSON.stringify(historico)); showModal({ title : "Resultado parcial", message : `⚠️ Algunos lugares no pudieron ser modificados por restricciones de WME.\n` + `Verifica el historial o vuelve a intentarlo.`, confirmText : "Aceptar", type : "warning" }); break; } } } }); observer.observe(document.body, { childList : true, subtree : true }); } //********************************************************************** // Nombre: waitForDOM // Fecha modificación: 2025-04-15 // Hora: 13:01:25 // Autor: mincho77 // Entradas: selector (string) - Selector CSS del elemento a esperar // callback (function) - Función a ejecutar cuando se encuentra el // elemento // interval (number) - Intervalo de tiempo entre intentos en ms // maxAttempts (number) - Número máximo de intentos // Salidas: Ninguna // Descripción: Espera a que un elemento del DOM esté disponible y ejecuta // la función de callback. Si no se encuentra el elemento después de un // número máximo de intentos, se muestra un mensaje de advertencia en la // consola. //********************************************************************** function waitForDOM(selector, callback, interval = 300, maxAttempts = 20) { let attempts = 0; const checkExist = setInterval(() => { const element = document.querySelector(selector); attempts++; if (element) { clearInterval(checkExist); callback(element); } else if (attempts >= maxAttempts) { clearInterval(checkExist); console.warn( `[PlacesNameNormalizer] No se encontró el elemento ${ selector} después de ${maxAttempts} intentos.`); } }, interval); } // ****************************************************************************************************************************************************************** // Nombre: openFloatingPanel // Fecha modificación: 2025-04-15 // Hora: 13:01:25 // Autor: mincho77 // Entradas: placesToNormalize (array) - Arreglo de lugares a normalizar // Salidas: Ninguna // Descripción: Abre un panel flotante con una tabla para normalizar nombres // de lugares. Permite aplicar cambios, excluir palabras y agregar // palabras especiales. Incluye un botón para cerrar el panel. // Prerrequisitos: Ninguno // ****************************************************************************************************************************************************************** function openFloatingPanel(placesToNormalize) { const panel = document.createElement("div"); panel.id = "normalizer-floating-panel"; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 1200px; max-height: 80vh; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 25px rgba(0,0,0,0.4); z-index: 10000; overflow-y: auto; font-family: Arial, sans-serif; `; let html = ` <style> #normalizer-table { width: 100%; border-collapse: collapse; margin: 15px 0; } #normalizer-table th { background: #2c3e50; color: white; padding: 10px; text-align: left; } #normalizer-table td { padding: 8px 10px; border-bottom: 1px solid #eee; } .warning-row { background: #fff8e1; } .normalize-btn, .apply-btn, .add-exclude-btn, .add-special-btn { padding: 8px 16px; /* Aumentar el tamaño del botón */ margin: 2px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: all 0.3s; } .normalize-btn { background: #3498db; color: white; } .apply-btn { background: #2ecc71; color: white; } .add-exclude-btn { background: #e67e22; color: white; } .add-special-btn { background: #9b59b6; color: white; } .close-btn { position: absolute; top: 10px; right: 10px; background: #e74c3c; color: white; border: none; width: 30px; height: 30px; border-radius: 50%; font-weight: bold; } input[type="checkbox"] { transform: scale(1.3); margin: 0 5px; } input[type="text"] { width: 100%; padding: 5px; box-sizing: border-box; } </style> <button class="close-btn" id="close-panel-btn">×</button> <h2 style="color: #2c3e50; margin-top: 5px;">Normalizador de Nombres</h2> <div style="margin: 10px 0; color: #7f8c8d;"> <span id="places-count">${ placesToNormalize.length} lugares para revisar</span> </div> <table id="normalizer-table"> <thead> <tr> <th width="5%">Aplicar</th> <th width="5%">Eliminar</th> <th width="25%">Nombre Actual</th> <th width="25%">Nombre Normalizado</th> <th width="15%">Problema Detectado</th> <th width="10%">Acciones</th> </tr> </thead> <tbody>`; placesToNormalize.forEach((place, index) => { const { originalName, newName, hasSpellingWarning, spellingWarnings, place : venue } = place; const placeId = venue.getID(); html += ` <tr> <td> <input type="checkbox" class="normalize-checkbox" data-index="${ index}" data-type="full"> </td> <td> <input type="checkbox" class="delete-checkbox" data-index="${ index}"> </td> <td>${escapeHtml(originalName)}</td> <td> <input type="text" class="new-name-input" value="${ escapeHtml(newName)}" data-index="${index}" data-place-id="${ placeId}" data-type="full" data-original="${escapeHtml(originalName)}"> </td> <td>${ originalName !== newName ? "Normalización necesaria" : "-"}</td> <td> <button class="normalize-btn" data-index="${ index}">NrmliZer</button> <button class="add-special-btn" data-word="${ escapeHtml(originalName)}">AddWrdDic</button> <button class="add-exclude-btn" data-word="${ escapeHtml( originalName)}" data-index="${index}">ExcludeWrd</button> </td> </tr>`; spellingWarnings.forEach((warning, warningIndex) => { html += ` <tr class="warning-row"> <td><input type="checkbox" class="normalize-checkbox" data-index="${index}" data-warning-index="${warningIndex}" data-type="warning"></td> <td></td> <td>${escapeHtml(warning.original)}</td> <td><input type="text" class="new-name-input" value="${escapeHtml(warning.sugerida || newName)}" data-index="${index}" data-place-id="${placeId}" data-warning-index="${warningIndex}" data-type="warning"></td> <td>${escapeHtml(warning.tipo || "Error ortográfico")} <div class="tool-source">${warning.origen || "Reglas locales"}</div> </td> <td> <button class="apply-btn" data-index="${index}" data-warning-index="${warningIndex}">Aplicar</button> <button class="add-exclude-btn" data-word="${escapeHtml(warning.original)}" data-index="${index}">Excluir</button> </td> </tr>`; }); }); html += `</tbody></table> <div style="margin-top: 20px; text-align: right;"> <button id="apply-all-btn" style="background: #27ae60; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-weight: bold;">Aplicar Cambios Seleccionados</button> <button id="cancel-btn" style="background: #e74c3c; color: white; padding: 10px 20px; border: none; border-radius: 4px; margin-left: 10px; font-weight: bold;">Cancelar</button> </div>`; panel.innerHTML = html; document.body.appendChild(panel); document.getElementById("close-panel-btn") .addEventListener("click", () => panel.remove()); document.getElementById("cancel-btn") .addEventListener("click", () => panel.remove()); document.getElementById("apply-all-btn") .addEventListener("click", () => { const selectedPlaces = placesToNormalize.filter((place, index) => { const checkbox = panel.querySelector( `.normalize-checkbox[data-index="${index}"]`); return checkbox && checkbox.checked; }); if (selectedPlaces.length === 0) { showModal({ title : "Advertencia", message : "No se seleccionaron lugares para aplicar cambios.", confirmText : "Aceptar", type : "warning" }); return; } applyNormalization(selectedPlaces); panel.remove(); }); // Evento para marcar el checkbox de "Aplicar" al modificar un texto, // y lógica de exclusión para "Eliminar" panel.querySelectorAll(".new-name-input").forEach((input) => { input.addEventListener("input", function () { const row = this.closest("tr"); const applyCheckbox = row?.querySelector(".normalize-checkbox"); const deleteCheckbox = row?.querySelector(".delete-checkbox"); const original = this.dataset.original || ""; const current = this.value.trim(); if (applyCheckbox && deleteCheckbox) { if (current !== original) { applyCheckbox.checked = true; deleteCheckbox.checked = false; } else { applyCheckbox.checked = false; } } }); }); // Evento para marcar "Aplicar" si se selecciona "Eliminar" (sólo una vez) panel.querySelectorAll(".delete-checkbox").forEach((checkbox) => { checkbox.addEventListener("change", function () { const row = this.closest("tr"); const applyCheckbox = row?.querySelector(".normalize-checkbox"); if (this.checked && applyCheckbox) { applyCheckbox.checked = true; } }); }); // Evento para normalizar el nombre al hacer clic en "NrmliZer" panel.querySelectorAll(".normalize-btn").forEach((btn) => { btn.addEventListener("click", async function() { const row = this.closest("tr"); const input = row.querySelector(".new-name-input[data-type='full']"); const applyCheckbox = row.querySelector("input.normalize-checkbox"); const deleteCheckbox = row.querySelector("input.delete-checkbox"); if (!input) return; // Animación let dots = 0; const originalText = "NrmliZer"; const interval = setInterval(() => { dots = (dots + 1) % 4; this.textContent = originalText + ".".repeat(dots); }, 500); try { input.value = await normalizePlaceName(input.value, true); if (applyCheckbox) applyCheckbox.checked = true; if (deleteCheckbox) deleteCheckbox.checked = false; clearInterval(interval); this.textContent = "✓ Ready"; this.style.backgroundColor = "#95a5a6"; this.disabled = true; } catch (error) { console.error("Error al normalizar:", error); clearInterval(interval); this.textContent = originalText; } }); }); panel.querySelectorAll(".apply-btn").forEach((btn) => { btn.addEventListener("click", function() { const index = this.dataset.index; const warningIndex = this.dataset.warningIndex; const checkbox = panel.querySelector(`.normalize-checkbox[data-index="${ index}"][data-warning-index="${warningIndex}"]`); if (checkbox) { checkbox.checked = true; this.textContent = "✓ Aplicado"; this.style.backgroundColor = "#95a5a6"; this.disabled = true; } }); }); panel.querySelectorAll(".add-special-btn").forEach((btn) => { btn.addEventListener("click", function() { const name = this.dataset.word; openAddSpecialWordPopup( name); // Llamar al modal para seleccionar palabras }); }); panel.querySelectorAll(".add-exclude-btn").forEach((btn) => { btn.addEventListener("click", function() { const word = this.dataset.word; if (word) { openAddSpecialWordPopup( word, "excludeWords"); // Llama al popup para agregar a palabras excluidas } }); }); } // ******************************************************************************************************************************** // Nombre: checkOnlyTildes (4) // Fecha modificación: 2025-06-21 // Autor: mincho77 // Entradas: // - original (string): Palabra original a comparar. // - sugerida (string): Palabra sugerida a comparar. // Salidas: // - boolean: // - true si las palabras son iguales excepto por tildes. // - false si difieren en otros caracteres o si alguna es // undefined/null. // Descripción: // Compara dos palabras ignorando tildes/diacríticos para determinar si la // única diferencia entre ellas es la acentuación. Utiliza normalización // Unicode para una comparación precisa. Optimizada para reducir operaciones // innecesarias. // ******************************************************************************************************************************** function checkOnlyTildes(original, sugerida) { if (typeof original !== "string" || typeof sugerida !== "string") { return false; } if (original === sugerida) { return false; } const normalize = (str) => str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); return normalize(original) === normalize(sugerida); } // ******************************************************************************************************************************** // Nombre: deleteWord // Fecha modificación: 2025-04-14 // Autor: mincho77 // Entradas: // - index (number): Índice de la palabra a eliminar. // Salidas: Ninguna. Muestra un modal de confirmación. // Descripción: // Muestra un modal de confirmación para eliminar una palabra de la lista de // exclusiones. Si el usuario confirma, elimina la palabra de la lista y // actualiza el almacenamiento local. // ******************************************************************************************************************************** function deleteWord(index) { const wordToDelete = excludeWords[index]; if (!wordToDelete) return; showModal({ title : "Eliminar palabra", message : `¿Estás seguro de que deseas eliminar la palabra <strong>${ wordToDelete}</strong>?`, confirmText : "Eliminar", cancelText : "Cancelar", type : "warning", onConfirm : () => { // Eliminar la palabra de la lista excludeWords.splice(index, 1); // Actualizar localStorage localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); // Actualizar la interfaz renderExcludedWordsPanel(); showModal({ title : "Éxito", message : "La palabra fue eliminada correctamente.", confirmText : "Aceptar", type : "success", autoClose : 2000, }); }, }); } // ******************************************************************************************************************************** // Nombre: openDeletePopup // Fecha modificación: 2025-04-14 // Autor: mincho77 // Entradas: // - index (number): Índice de la palabra a eliminar. // Salidas: Ninguna. Muestra un modal de confirmación. // Descripción: // Muestra un modal de confirmación para eliminar una palabra de la lista de exclusiones. Si el usuario confirma, elimina la // palabra de la lista y actualiza el almacenamiento local. // ******************************************************************************************************************************** function openDeletePopup(index) { const wordToDelete = excludeWords[index]; if (!wordToDelete) { console.error(`No se encontró la palabra en el índice ${index}`); return; } showModal({ title : "Eliminar palabra", message : `¿Estás seguro de que deseas eliminar la palabra <strong>${ wordToDelete}</strong>?`, confirmText : "Eliminar", cancelText : "Cancelar", type : "warning", onConfirm : () => { // Eliminar la palabra de la lista excludeWords.splice(index, 1); // Actualizar localStorage localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); // Actualizar la interfaz renderExcludedWordsPanel(); showModal({ title : "Éxito", message : "La palabra fue eliminada correctamente.", confirmText : "Aceptar", type : "success", autoClose : 2000, }); }, }); } // ******************************************************************************************************************************** // Nombre: evaluarOrtografiaNombre // Fecha modificación: 2025-04-10 20:45 GMT-5 // Autor: mincho77 // Entradas: // - name (string): Nombre a evaluar. // - opciones (object): Opciones de configuración. // - timeout (number): Tiempo máximo de espera para la API (ms). // - usarCache (boolean): Si se debe usar caché para resultados // - modoEstricto (boolean): Si se debe aplicar modo estricto. // Salidas: // - Promise: Objeto que contiene el resultado de la evaluación. // Descripción: Evalúa la ortografía de un nombre utilizando reglas locales y la API de LanguageTool. Devuelve un objeto con // advertencias de ortografía y metadatos sobre la evaluación. Incluye un sistema de caché para evitar llamadas duplicadas durante // la sesión. // Prerrequisitos: Funciones auxiliares como tieneTildesIncorrectas y corregirTildeLocal. // ******************************************************************************************************************************** function evaluarOrtografiaNombre(name, opciones = {}) { const config = { timeout : opciones.timeout || 5000, usarCache : opciones.usarCache !== false, modoEstricto : opciones.modoEstricto || false }; // Cache simple (evita llamadas duplicadas durante la sesión) const cache = evaluarOrtografiaNombre.cache || (evaluarOrtografiaNombre.cache = new Map()); const cacheKey = `${config.modoEstricto}-${name}`; if (config.usarCache && cache.has(cacheKey)) { return Promise.resolve(cache.get(cacheKey)); } return new Promise((resolve) => { // 1. Validación de entrada if (typeof name !== "string" || name.trim().length === 0) { const resultado = { hasSpellingWarning : false, spellingWarnings : [], metadata : { apiStatus : "invalid_input" } }; cache.set(cacheKey, resultado); return resolve(resultado); } const inicio = Date.now(); let timeoutExcedido = false; // 2. Timeout de seguridad const timeoutId = setTimeout(() => { timeoutExcedido = true; const resultado = { hasSpellingWarning : false, spellingWarnings : [], metadata : { apiStatus : "timeout", tiempoRespuesta : Date.now() - inicio } }; cache.set(cacheKey, resultado); resolve(resultado); }, config.timeout); // 3. Primero verificar reglas locales (sincrónicas) const problemasLocales = []; const palabras = name.split(/\s+/); palabras.forEach((palabra) => { if (tieneTildesIncorrectas(palabra)) { problemasLocales.push({ original : palabra, sugerida : corregirTildeLocal(palabra), tipo : "Tilde incorrecta", origen : "Reglas locales" }); } }); // 4. Si hay problemas locales y no es modo estricto, devolver // inmediato if (problemasLocales.length > 0 && !config.modoEstricto) { clearTimeout(timeoutId); const resultado = { hasSpellingWarning : true, spellingWarnings : problemasLocales, metadata : { apiStatus : "local_rules_applied" } }; cache.set(cacheKey, resultado); return resolve(resultado); } // 5. Consultar API LanguageTool GM_xmlhttpRequest({ method : "POST", url : "https://api.languagetool.org/v2/check", headers : { "Content-Type" : "application/x-www-form-urlencoded", Accept : "application/json" }, data : `language=es&text=${encodeURIComponent(name)}`, onload : (response) => { if (timeoutExcedido) return; clearTimeout(timeoutId); const tiempoRespuesta = Date.now() - inicio; let resultado; try { if (response.status === 200) { const data = JSON.parse(response.responseText); const problemasAPI = data.matches.map( (match) => ({ original : match.context.text.substring( match.context.offset, match.context.offset + match.context.length), sugerida : match.replacements[0]?.value || match.context.text, tipo : "Ortografía", // Cambiar a "Ortografía" origen : "API", regla : match.rule.id, contexto : match.context.text })); // Combinar resultados locales y de API const todosProblemas = [...problemasLocales, ...problemasAPI ]; resultado = { hasSpellingWarning : todosProblemas.length > 0, spellingWarnings : todosProblemas, metadata : { apiStatus : "success", tiempoRespuesta, totalErrores : todosProblemas.length } }; } else { resultado = { hasSpellingWarning : problemasLocales.length > 0, spellingWarnings : problemasLocales, metadata : { apiStatus : `api_error_${response.status}`, tiempoRespuesta } }; } } catch (error) { resultado = { hasSpellingWarning : problemasLocales.length > 0, spellingWarnings : problemasLocales, metadata : { apiStatus : "parse_error", tiempoRespuesta } }; } cache.set(cacheKey, resultado); resolve(resultado); }, onerror : () => { if (timeoutExcedido) return; clearTimeout(timeoutId); const resultado = { hasSpellingWarning : problemasLocales.length > 0, spellingWarnings : problemasLocales, metadata : { apiStatus : "network_error", tiempoRespuesta : Date.now() - inicio } }; cache.set(cacheKey, resultado); resolve(resultado); } }); }); } // ******************************************************************************************************************************** // Nombre: corregirTildeLocal // Fecha modificación: 2025-04-25 05:45 GMT-5 // Autor: mincho77 // Entradas: // - palabra (string): Palabra a corregir // Salidas: (string): Palabra corregida o la original si no hay corrección. // Descripción: Esta función corrige las tildes de palabras específicas en español. Se basa en un objeto de correcciones // predefinido. Si la palabra no está en el objeto, se devuelve la palabra original. // ******************************************************************************************************************************** function corregirTildeLocal(palabra) { const correcciones = { aun : "aún", // Adverbio de tiempo tu : "tú", // Pronombre personal mi : "mí", // Pronombre personal el : "él", // Pronombre personal si : "sí", // Afirmación o pronombre reflexivo de : "dé", // Verbo dar se : "sé", // Verbo saber o ser mas : "más", // Adverbio de cantidad te : "té", // Sustantivo (bebida) que : "qué", // Interrogativo o exclamativo quien : "quién", // Interrogativo o exclamativo como : "cómo", // Interrogativo o exclamativo cuando : "cuándo", // Interrogativo o exclamativo donde : "dónde", // Interrogativo o exclamativo cual : "cuál", // Interrogativo o exclamativo cuanto : "cuánto", // Interrogativo o exclamativo porque : "porqué", // Sustantivo (la razón) porqué : "por qué", // Interrogativo o exclamativo }; return correcciones[palabra.toLowerCase()] || palabra; } // ******************************************************************************************************************************** // Nombre: scanPlaces // Fecha modificación: 2025-04-10 18:30 GMT-5 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Descripción: Escanea los lugares en el mapa y normaliza sus nombres. Filtra los lugares que no tienen nombre y procesa aquellos // que requieren normalización. Muestra un panel flotante con los lugares a normalizar y // permite aplicar cambios, excluir palabras y agregar palabras especiales. Incluye un botón para cerrar el panel. // Prerrequisitos: Funciones auxiliares como normalizePlaceName y evaluarOrtografiaNombre. // ******************************************************************************************************************************** function scanPlaces() { const maxPlaces = parseInt(document.getElementById("maxPlacesInput")?.value || 100, 10); if (!W?.model?.venues?.objects) { console.error("Modelo WME no disponible"); return; } const allPlaces = Object.values(W.model.venues.objects) .filter((place) => { // Filtrar lugares que no tienen nombre if (!place?.attributes?.name) { return false; } return true; }) .slice(0, maxPlaces); if (allPlaces.length === 0) { toggleSpinner(false); showNoPlacesFoundMessage(); // Mostrar el mensaje mejorado return; } // 6. Procesamiento asíncrono con progreso let processedCount = 0; const placesToNormalize = []; const processBatch = async (index) => { if (index >= allPlaces.length) { toggleSpinner(false); if (placesToNormalize.length > 0) { openFloatingPanel(placesToNormalize); } else { showModal({ title : "Advertencia", message : "No se encontraron lugares que requieran normalización.", confirmText : "Entendido", type : "warning" }); } return; } const place = allPlaces[index]; try { const originalName = place.attributes.name; const normalizedName = await normalizePlaceName(originalName, true); // Actualizar progreso processedCount++; toggleSpinner( true, `Procesando lugares... (${processedCount}/${ allPlaces.length})`, Math.round((processedCount / allPlaces.length) * 100)); // Evaluar ortografía (usando el modo seleccionado) const ortografia = checkOnlyTildes ? await evaluarOrtografiaConTildes(normalizedName) : await evaluarOrtografiaNombre(normalizedName); if (ortografia.hasSpellingWarning || originalName !== normalizedName) { placesToNormalize.push({ id : place.getID(), originalName, newName : normalizedName, hasSpellingWarning : ortografia.hasSpellingWarning, spellingWarnings : ortografia.spellingWarnings, place }); } // Procesar siguiente lugar con pequeño retardo para no bloquear UI setTimeout(() => processBatch(index + 1), 50); } catch (error) { console.error(`Error procesando lugar ${place.getID()}:`, error); // Continuar con el siguiente lugar a pesar del error setTimeout(() => processBatch(index + 1), 50); } }; // Iniciar procesamiento por lotes processBatch(0); } // ******************************************************************************************************************************** // Nombre: renderDictionaryWordsPanel // Fecha modificación: 2025-04-14 // Autor: mincho77 // Entradas: Ninguna (usa la variable global dictionaryWords). // Salidas: Ninguna. // Descripción: // Limpia y renderiza la lista de palabras del diccionario en el panel // lateral. Ordena las palabras alfabéticamente y actualiza el localStorage. // ******************************************************************************************************************************** function renderDictionaryWordsPanel() { const container = document.getElementById("dictionary-words-list"); if (!container) { console.warn( "[PlacesNameNormalizer] No se encontró el contenedor 'dictionary-words-list'."); return; } container.innerHTML = ""; const dict = spellDictionaries[activeDictionaryLang] || {}; const words = Object.values(dict).flat().sort((a, b) => a.localeCompare(b)); dictionaryWords = words; // Actualiza global para búsquedas const ul = document.createElement("ul"); ul.style.listStyle = "none"; words.forEach((word) => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "5px 0"; const wordSpan = document.createElement("span"); wordSpan.textContent = word; li.appendChild(wordSpan); const btnContainer = document.createElement("div"); btnContainer.style.display = "flex"; btnContainer.style.gap = "10px"; const editBtn = document.createElement("button"); editBtn.textContent = "✏️"; editBtn.title = "Editar"; editBtn.style.cursor = "pointer"; editBtn.addEventListener("click", () => { const index = dictionaryWords.indexOf(wordSpan.textContent.trim()); if (index !== -1) { openEditPopup(index, "dictionaryWords"); } }); const deleteBtn = document.createElement("button"); deleteBtn.textContent = "🗑️"; deleteBtn.title = "Eliminar"; deleteBtn.style.cursor = "pointer"; deleteBtn.addEventListener("click", () => { const index = dictionaryWords.indexOf(wordSpan.textContent.trim()); if (index !== -1) { openDeletePopupForDictionary(index); } }); btnContainer.appendChild(editBtn); btnContainer.appendChild(deleteBtn); li.appendChild(btnContainer); ul.appendChild(li); }); container.appendChild(ul); } // ******************************************************************************************************************************** // Nombre: renderExcludedWordsPanel // Fecha modificación: 2025-03-30 // Autor: mincho77 // Entradas: Ninguna (usa la variable global excludeWords). // Salidas: Ninguna. // Descripción: Limpia y renderiza la lista de palabras excluidas en el panel lateral. Ordena las palabras alfabéticamente y // actualiza el localStorage. // ******************************************************************************************************************************** function renderExcludedWordsPanel() { const container = document.getElementById("normalizer-sidebar"); if (!container) { console.warn(`[${ SCRIPT_NAME}] No se encontró el contenedor "normalizer-sidebar".`); return; } // Limpiar el contenedor para evitar acumulaciones container.innerHTML = ""; // Crear un elemento <ul> para la lista const list = document.createElement("ul"); list.style.listStyle = "none"; // Opcional: eliminar viñetas // Iterar sobre cada palabra de la lista excluida excludeWords.forEach((word, index) => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "5px 0"; // Crear un <span> que muestre la palabra const wordSpan = document.createElement("span"); wordSpan.textContent = word; li.appendChild(wordSpan); // Crear un contenedor para los botones const btnContainer = document.createElement("div"); btnContainer.style.display = "flex"; btnContainer.style.gap = "10px"; // Botón de editar const editBtn = document.createElement("button"); editBtn.textContent = "✏️"; editBtn.title = "Editar"; editBtn.style.cursor = "pointer"; // Asigna el event listener de editar, pasando el índice y el tipo de lista editBtn.addEventListener( "click", () => { openEditPopup(index, "excludeWords"); }); btnContainer.appendChild(editBtn); // Botón de borrar const deleteBtn = document.createElement("button"); deleteBtn.textContent = "🗑️"; deleteBtn.title = "Eliminar"; deleteBtn.style.cursor = "pointer"; deleteBtn.addEventListener("click", () => { openDeletePopup(index); // Llama a la función para mostrar el modal de confirmación }); btnContainer.appendChild(deleteBtn); li.appendChild(btnContainer); list.appendChild(li); }); container.appendChild(list); } function renderExcludedWordsPanel2() { const container = document.getElementById("normalizer-sidebar"); if (!container) { console.warn(`[${ SCRIPT_NAME}] No se encontró el contenedor "normalizer-sidebar".`); return; } // Limpiar el contenedor para evitar acumulaciones container.innerHTML = ""; // Crear un elemento <ul> para la lista const list = document.createElement("ul"); list.style.listStyle = "none"; // Opcional: eliminar viñetas // Iterar sobre cada palabra de la lista excluida excludeWords.forEach((word, index) => { const li = document.createElement("li"); li.style.display = "flex"; li.style.justifyContent = "space-between"; li.style.alignItems = "center"; li.style.padding = "5px 0"; // Crear un <span> que muestre la palabra const wordSpan = document.createElement("span"); wordSpan.textContent = word; li.appendChild(wordSpan); // Crear un contenedor para los botones const btnContainer = document.createElement("div"); btnContainer.style.display = "flex"; btnContainer.style.gap = "10px"; // Botón de editar const editBtn = document.createElement("button"); editBtn.textContent = "✏️"; editBtn.title = "Editar"; editBtn.style.cursor = "pointer"; // Asigna el event listener de editar, pasando el índice y el tipo // de lista editBtn.addEventListener( "click", () => { openEditPopup(index, "excludeWords"); }); btnContainer.appendChild(editBtn); // Botón de borrar const deleteBtn = document.createElement("button"); deleteBtn.textContent = "🗑️"; deleteBtn.title = "Eliminar"; deleteBtn.style.cursor = "pointer"; deleteBtn.addEventListener( "click", () => { openDeletePopupForDictionary(index); }); btnContainer.appendChild(deleteBtn); li.appendChild(btnContainer); list.appendChild(li); }); container.appendChild(list); } // ******************************************************************************************************************************** // Nombre: setupDragAndDrop // Fecha modificación: 2025-04-22 // Hora: 22:37 // Autor: mincho77 // Entradas: Ninguna (usa la variable global type). // Salidas: Ninguna. // Descripción: Soporta archivos .txt y .xml para diccionario ortográfico. // ******************************************************************************************************************************** function setupDragAndDrop({ dropZoneId, onFileProcessed, type }) { const dropZone = document.getElementById(dropZoneId); if (!dropZone) { console.warn( `[setupDragAndDrop] No se encontró el elemento con ID '${ dropZoneId}'`); return; } // 🔁 Evitar que el navegador abra el archivo en toda la ventana ["dragenter", "dragover", "drop"].forEach(eventName => { window.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }); }); // 🟩 Efecto visual al arrastrar dropZone.addEventListener("dragover", (e) => { e.preventDefault(); e.stopPropagation(); dropZone.style.borderColor = "#4CAF50"; dropZone.style.backgroundColor = "#eaffea"; }); // 🔙 Restablecer el estilo si sale del área dropZone.addEventListener("dragleave", () => { dropZone.style.borderColor = "#ccc"; dropZone.style.backgroundColor = ""; }); // 📥 Manejar el archivo soltado dropZone.addEventListener("drop", (event) => { event.preventDefault(); event.stopPropagation(); dropZone.style.borderColor = "#ccc"; dropZone.style.backgroundColor = ""; const file = event.dataTransfer.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const content = e.target.result.trim(); let words = []; if (file.name.endsWith(".xml")) { try { const parser = new DOMParser(); const xml = parser.parseFromString(content, "application/xml"); const wordNodes = xml.getElementsByTagName("word"); words = Array.from(wordNodes) .map(n => n.textContent.trim()) .filter(Boolean); } catch (err) { console.error("❌ Error al parsear XML:", err); showModal({ title : "Error", message : "No se pudo leer el archivo XML.", confirmText : "Aceptar", type : "error", }); return; } } else { words = content.split(/\r?\n/) .map(line => line.trim()) .filter(Boolean); } if (typeof onFileProcessed === "function") { onFileProcessed(words); } }; reader.readAsText(file); }); } function setupDragAndDrop2({ dropZoneId, onFileProcessed, type }) { const dropZone = document.getElementById(dropZoneId); if (!dropZone) { console.warn( `[setupDragAndDrop] No se encontró el elemento con ID '${ dropZoneId}'`); return; } // Prevenir comportamiento por defecto en toda la ventana window.addEventListener("dragover", e => e.preventDefault()); window.addEventListener("drop", e => e.preventDefault()); // Prevenir comportamiento por defecto y aplicar estilo en zona ["dragenter", "dragover"].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.style.backgroundColor = "#e6ffe6"; // Verde suave dropZone.style.borderColor = "#28a745"; }); }); ["dragleave", "drop"].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.style.backgroundColor = ""; dropZone.style.borderColor = "#ccc"; }); }); dropZone.addEventListener("drop", (event) => { const file = event.dataTransfer.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const content = e.target.result; let words = []; if (file.name.endsWith(".txt")) { words = content.split(/\r?\n/) .map(line => line.trim()) .filter(Boolean); } else if (file.name.endsWith(".xml")) { try { const parser = new DOMParser(); const xml = parser.parseFromString(content, "text/xml"); const wordNodes = xml.getElementsByTagName("word"); words = Array.from(wordNodes).map(n => n.textContent.trim()); } catch (err) { console.error("❌ Error al parsear XML:", err); return; } } if (typeof onFileProcessed === "function") { onFileProcessed(words); } }; reader.readAsText(file); }); } // ******************************************************************************************************************************** // Nombre: setupDragAndDropImport // Fecha modificación: 2025-03-31 // Autor: mincho77 // Entradas: Ninguna. // Salidas: Ninguna. // Descripción: // Activa la funcionalidad de drag & drop sobre el elemento con id "drop-zone" para importar un archivo con palabras excluidas. // Procesa archivos .xml y .txt. // ******************************************************************************************************************************** function setupDragAndDropImport() { const dropZone = document.getElementById("drop-zone"); if (!dropZone) { console.warn( "setupDragAndDropImport: No se encontró el elemento #drop-zone"); return; } dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.style.borderColor = "#4CAF50"; dropZone.style.backgroundColor = "#f0fff0"; console.log("dragover detectado"); }); dropZone.addEventListener("dragleave", (e) => { dropZone.style.backgroundColor = ""; dropZone.style.borderColor = "#ccc"; console.log("dragleave detectado"); }); dropZone.addEventListener("drop", (e) => { e.preventDefault(); dropZone.style.borderColor = "#ccc"; dropZone.style.backgroundColor = ""; console.log("drop detectado"); const file = e.dataTransfer.files[0]; if (!file) { console.log("No se detectó ningún archivo"); return; } console.log("Archivo soltado:", file.name); const reader = new FileReader(); reader.onload = function(event) { console.log("Contenido del archivo:", event.target.result); let palabras = []; if (file.name.endsWith(".xml")) { const parser = new DOMParser(); const xml = parser.parseFromString(event.target.result, "text/xml"); const nodes = xml.querySelectorAll( "word, palabra, item, excluded, exclude"); palabras = Array.from(nodes) .map((n) => n.textContent.trim()) .filter((p) => p.length > 0); } else { palabras = event.target.result.split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line.length > 0); } if (palabras.length === 0) { // alert("⚠️ No se encontraron palabras válidas."); showModal({ title : "Advertencia", message : "No se encontraron palabras válidas.", type : "warning", autoClose : 2000, // El modal desaparecerá después de 2 segundos }); return; } const replace = document.getElementById("replaceExcludeListCheckbox"); if (replace && replace.checked) { excludeWords = []; localStorage.removeItem("excludeWords"); } else { excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || []; } excludeWords = [...new Set([...excludeWords, ...palabras ]) ] .filter((w) => w.trim().length > 0) .sort((a, b) => a.localeCompare(b)); localStorage.setItem("excludeWords", JSON.stringify(wordLists.excludeWords)); renderExcludedWordsPanel(); showModal({ title : "Información", message : "Se importaron {prependText} palabras desde el archivo.", prependText : palabras.length, confirmText : "Aceptar", type : "info" }); }; reader.readAsText(file); }); } // ******************************************************************************************************************************** // Nombre: handleImportList // Fecha modificación: 2025-03-30 // Autor: mincho77 // Entradas: Ninguna (depende del input file "importListInput" y checkbox "replaceExcludeListCheckbox"). // Salidas: Ninguna. // Descripción: Lee un archivo seleccionado por el usuario, procesa sus líneas para extraer palabras válidas, y actualiza la lista // de palabras excluidas (localStorage y panel). // ******************************************************************************************************************************** function handleImportList() { const fileInput = document.getElementById("importListInput"); const replaceCheckbox = document.getElementById("replaceExcludeListCheckbox"); if (!fileInput || !fileInput.files || fileInput.files.length === 0) { showModal({ title : "Información", message : "No se seleccionó ningún archivo.", confirmText : "Aceptar", type : "info" }); return; } const file = fileInput.files[0]; const reader = new FileReader(); reader.onload = function(event) { const rawLines = event.target.result.split(/\r?\n/); const lines = rawLines .map((line) => line.replace(/[^\p{L}\p{N}().\s-]/gu, "").trim()) .filter((line) => line.length > 0); if (lines.length === 0) { showModal({ title : "Error", message : "El archivo no contiene datos válidos.", confirmText : "Aceptar", type : "error" }); return; } if (replaceCheckbox && replaceCheckbox.checked) { excludeWords = []; } else { excludeWords = JSON.parse(localStorage.getItem("excludeWords")) || []; } excludeWords = [...new Set([...excludeWords, ...lines ]) ] .filter((w) => w.trim().length > 0) .sort((a, b) => a.localeCompare(b)); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); // Refresca la lista después de importar showModal({ title : "Éxito", message : `Se importaron ${ lines.length} palabras a la lista de palabras especiales.`, confirmText : "Aceptar", type : "success" }); fileInput.value = ""; // Reinicia el input de archivo }; reader.onerror = function() { showModal({ title : "Error", message : "Hubo un problema al leer el archivo. Inténtalo nuevamente.", confirmText : "Aceptar", type : "error" }); }; reader.readAsText(file); } // ******************************************************************************************************************************** // Nombre: renderSpecialWordsPanel // Fecha modificación: 2025-04-25 05:45 GMT-5 // Autor: mincho77 // Entradas: Ninguna (usa la variable global specialWords). // Salidas: Ninguna. // Descripción: Limpia y renderiza la lista de palabras especiales en el panel lateral. Ordena las palabras alfabéticamente y // actualiza el localStorage. Se utiliza para mostrar las palabras especiales que se pueden agregar o editar. // ******************************************************************************************************************************** function renderSpecialWordsPanel() { const container = document.getElementById("special-words-list"); if (!container) { console.warn( "[PlacesNameNormalizer] No se encontró el contenedor 'special-words-list'."); return; } container.innerHTML = ""; // Limpia el contenedor // Ordenar las palabras alfabéticamente const sortedWords = specialWords.sort((a, b) => a.localeCompare(b)); // Crear una lista de palabras const ul = document.createElement("ul"); ul.style.listStyle = "none"; sortedWords.forEach((word) => { const li = document.createElement("li"); li.textContent = word; ul.appendChild(li); }); container.appendChild(ul); } // ******************************************************************************************************************************** // Nombre: addWordsToList // Fecha modificación: 2025-04-25 05:45 GMT-5 // Autor: mincho77 // Entradas: // - words (string[]): Palabras a agregar. // - listType (string): Tipo de lista ("specialWords" o "dictionaryWords"). // Salidas: Ninguna. // Descripción: Agrega palabras a la lista correspondiente (especiales o del diccionario). Evita duplicados y actualiza el localStorage. // También renderiza la lista correspondiente en el panel lateral y muestra un mensaje de éxito. // ******************************************************************************************************************************** function addWordsToList(words, listType) { // Determinar la lista correspondiente let targetList; if (listType === "specialWords") { targetList = specialWords; } else if (listType === "dictionaryWords") { targetList = dictionaryWords; } else { console.error(`Tipo de lista desconocido: ${listType}`); return; } // Agregar palabras a la lista, evitando duplicados const newWords = words.filter((word) => !targetList.includes(word)); targetList.push(...newWords); // Guardar en localStorage localStorage.setItem(listType, JSON.stringify(targetList)); // Renderizar la lista correspondiente if (listType === "specialWords") { renderSpecialWordsPanel(); } else if (listType === "dictionaryWords") { renderDictionaryWordsPanel(); } // Mostrar mensaje de éxito showModal({ title: "Éxito", message: `Se agregaron ${newWords.length} palabra(s) a la lista ${listType}.`, confirmText: "Aceptar", type: "success", autoClose: 1500, }); } // ******************************************************************************************************************************** // Nombre: openAddSpecialWordPopup // Fecha modificación: 2025-04-25 04:56 // Autor: mincho77 // Entradas: // - name (string): Nombre de la palabra o frase a agregar. // - listType (string): Tipo de lista ("specialWords" o "excludeWords"). // Salidas: Ninguna. // Descripción: Abre un modal para agregar palabras especiales o excluidas. Permite seleccionar palabras de una frase y // agregarlas a la lista correspondiente. Actualiza el localStorage y renderiza la lista en el panel lateral. Muestra mensajes // de éxito o advertencia según corresponda. // ******************************************************************************************************************************** function openAddSpecialWordPopup(name, listType = "specialWords") { const words = name.split(/\s+/); // Dividir el nombre en palabras const modal = document.createElement("div"); modal.className = "custom-modal-overlay"; modal.innerHTML = ` <div class="custom-modal"> <div class="custom-modal-header"> <h3>Agregar Palabras ${ listType === "excludeWords" ? "Excluidas" : "Especiales"}</h3> <button class="close-modal-btn" title="Cerrar">×</button> </div> <div class="custom-modal-body"> <p>Selecciona las palabras que deseas agregar como ${ listType === "excludeWords" ? "excluidas" : "especiales"}:</p> <ul style="list-style: none; padding: 0;"> ${ words .map((word, index) => ` <li> <label> <input type="checkbox" class="special-word-checkbox" data-word="${ word}" id="word-${index}"> ${word} </label> </li> `) .join("")} </ul> </div> <div class="custom-modal-footer"> <button id="add-selected-words-btn" class="modal-btn confirm-btn">Agregar</button> <button id="cancel-add-words-btn" class="modal-btn cancel-btn">Cancelar</button> </div> </div> `; document.body.appendChild(modal); // Manejar el cierre del modal modal.querySelector(".close-modal-btn").addEventListener("click", () => modal.remove()); modal.querySelector("#cancel-add-words-btn").addEventListener("click", () => modal.remove()); // Manejar la acción de agregar palabras seleccionadas modal.querySelector("#add-selected-words-btn") .addEventListener("click", () => { const selectedWords = Array .from(modal.querySelectorAll( ".special-word-checkbox:checked")) .map((checkbox) => checkbox.dataset.word); if (selectedWords.length > 0) { selectedWords.forEach((word) => { if (listType === "excludeWords") { if (!excludeWords.includes(word)) { excludeWords.push(word); } } else { addWordsToList([ word ], listType); } }); // Guardar en localStorage y actualizar la interfaz if (listType === "excludeWords") { localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); } else { localStorage.setItem("specialWords", JSON.stringify(specialWords)); renderSpecialWordsPanel(); } // Mostrar mensaje de éxito con tiempo reducido showModal({ title : "Éxito", message : `Se agregaron ${selectedWords.length} palabra(s) como ${ listType === "excludeWords" ? "excluidas" : "especiales"}.`, type : "success", autoClose : 1000, // Tiempo reducido a 1 segundos }); } else { // Mostrar mensaje de advertencia si no se seleccionó ninguna // palabra showModal({ title : "Advertencia", message : "No seleccionaste ninguna palabra.", type : "warning", autoClose : 1000, // Tiempo reducido a 1 segundos }); } modal.remove(); }); } // ******************************************************************************************************************************** // Nombre: normalizePlaceName // Fecha modificación: 2025-04-15 // Autor: mincho77 // Entradas: // - name (string): Nombre del lugar a normalizar. // - useSpellingAPI (boolean): Indica si se debe usar la API de ortografía. // Salidas: (string): Nombre normalizado. // Descripción: Normaliza el nombre del lugar aplicando reglas de capitalización, eliminando artículos innecesarios y corrigiendo // errores ortográficos. Utiliza la API de LanguageTool para verificar la ortografía y aplicar sugerencias. También maneja números // romanos y apóstrofes de manera adecuada. La función devuelve el nombre normalizado. // ******************************************************************************************************************************** async function normalizePlaceName(name, useSpellingAPI = false) { if (!name) return ""; // Obtener el estado del checkbox para usar la API const useAPI = document.getElementById("useSpellingAPI")?.checked; const normalizeArticles = !document.getElementById("normalizeArticles")?.checked; // Obtener el estado del checkbox para verificar ortografía const articles = [ "el", "la", "los", "las", "de", "del", "al", "y", "e" ]; const words = name.trim().split(/\s+/); // Filtrar palabras excluidas const isRoman = (word) => /^(i{1,3}|iv|v|vi{0,3}|ix|x|xi{0,3}|xiv|xv|xvi{0,3}|xix|xx|xxi{0,3}|xxiv|xxv|xl)$/i .test(word); const normalizedWords = await Promise.all(words.map(async (word, index) => { const lowerWord = word.normalize("NFD").toLowerCase(); // Si es "Él" o "el", no modificar if (lowerWord === "él" || lowerWord === "el") { return word; // Mantener la palabra tal como está } // Reemplazar "SA" por "S.A" if (lowerWord === "sa" || lowerWord === "s.a") { return "S.A"; } // Reemplazar "SAS" por "S.A.S" if (lowerWord === "sas") { return "S.A.S"; } // Si es un número, se mantiene igual if (/^\d+$/.test(word)) return word; // Si la palabra está en la lista de excluidas, se devuelve tal // cual const match = wordLists.excludeWords.find( (w) => w.normalize("NFD").toLowerCase() === lowerWord); if (match) return match; // Si es un número romano, convertir a mayúsculas if (isRoman(word)) return word.toUpperCase(); // Si contiene un apóstrofo, no capitalizar la letra siguiente if (/^[A-Za-z]+'[A-Za-z]/.test(word)) { return (word.charAt(0).toUpperCase() + word.slice(1, word.indexOf("'") + 1) + word.slice(word.indexOf("'") + 1).toLowerCase()); } // Si no se deben normalizar artículos y es un artículo, mantener en minúsculas if (!normalizeArticles && articles.includes(lowerWord) && index !== 0) return lowerWord; // Si se debe usar la API de ortografía, verificar ortografía if (useAPI) { try { const errors = await checkSpellingWithAPI(word); if (errors.length > 0) { const suggestion = errors[0].sugerencia || word; return suggestion.charAt(0).toUpperCase() + suggestion.slice(1).toLowerCase(); } } catch (error) { console.error("Error al verificar ortografía:", error); } } // Capitalizar la primera letra de la palabra return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); })); let newName = normalizedWords.join(" ") .replace(/\s*\|\s*/g, " - ") .replace(/\s*\[P]\s*/g, "") // Reemplaza [P] por un espacio vacío .replace(/([(["'])\s*([\p{L}])/gu, (match, p1, p2) => p1 + p2.toUpperCase()) .replace(/\s*-\s*/g, " - ") .replace(/\b(\d+)([A-Z])\b/g, (match, num, letter) => num + letter.toUpperCase()) .replace(/\.$/, "") .replace(/&(\s*)([A-Z])/g, (match, space, letter) => "&" + space + letter.toUpperCase()); // Asegurar que las letras después de un apóstrofo estén en minúscula newName = newName.replace(/([A-Za-z])'([A-Za-z])/g, (match, before, after) => `${before}'${after.toLowerCase()}`); // Asegurar que la primera letra después de un guion esté en mayúscula newName = newName.replace( /-\s*([a-z])/g, (match, letter) => `- ${letter.toUpperCase()}`); return newName.replace(/\s{2,}/g, " ").trim(); } // ******************************************************************************************************************************** // Nombre: init // Fecha modificación: 2025-04-09 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna // Prerrequisitos si existen: // - El objeto global W debe estar disponible. // - Deben estar definidas las funciones: initializeExcludeWords, // createSidebarTab, waitForDOM, renderExcludedWordsPanel y setupDragAndDropImport. // Descripción: Esta función espera a que el entorno de edición de Waze (WME) esté completamente cargado, verificando que // existan los objetos necesarios para iniciar el script. Una vez disponible, inicializa la lista de palabras excluidas, crea el // tab lateral personalizado, y espera a que el DOM del tab esté listo para renderizar el panel de palabras excluidas y activar la // funcionalidad de arrastrar y soltar para importar palabras. Finalmente, expone globalmente las funciones applyNormalization y // normalizePlaceName. // ******************************************************************************************************************************** function init() { if (!W || !W.userscripts || !W.model || !W.model.venues) { console.log(`[${SCRIPT_NAME}] Esperando que WME esté listo...`); setTimeout(init, 1000); return; } console.log(`[${SCRIPT_NAME}] Inicializando v${VERSION}`); initializeExcludeWords(); createSidebarTab(); waitForDOM("#normalizer-tab", () => { console.log("[init] Sidebar listo"); renderExcludedWordsPanel(); waitForDOM("#dictionary-words-list", (element) => { console.log("Contenedor del diccionario encontrado:", element); renderDictionaryWordsPanel(); attachDictionarySearch(); // ✅ Ejecuta el buscador sobre los // <li> }); setupDragAndDrop({ dropZoneId : "drop-zone", onFileProcessed : (words) => { excludeWords = [...new Set([...excludeWords, ...words ]) ].sort(); localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); showModal({ title : "Éxito", message : `Se importaron ${ words .length} palabras a la lista de palabras especiales.`, confirmText : "Aceptar", type : "success", }); }, type : "excludeWords", }); setupDragAndDrop({ dropZoneId : "dictionary-drop-zone", onFileProcessed : (words) => { const nuevoDiccionario = {}; for (const palabra of words) { const letra = palabra.charAt(0).toLowerCase(); if (!nuevoDiccionario[letra]) { nuevoDiccionario[letra] = []; } nuevoDiccionario[letra].push(palabra); } for (const letra in nuevoDiccionario) { if (!spellDictionaries[activeDictionaryLang][letra]) { spellDictionaries[activeDictionaryLang][letra] = []; } const conjunto = new Set([ ...spellDictionaries[activeDictionaryLang][letra], ...nuevoDiccionario[letra] ]); spellDictionaries[activeDictionaryLang][letra] = Array.from(conjunto).sort(); } localStorage.setItem( `spellDictionaries_${activeDictionaryLang}`, JSON.stringify(spellDictionaries[activeDictionaryLang])); dictionaryWords = Object.values(spellDictionaries[activeDictionaryLang]) .flat() .sort(); renderDictionaryWordsPanel(); showModal({ title : "Éxito", message : `Se importaron ${ words.length} palabras al diccionario.`, confirmText : "Aceptar", type : "success", }); }, type : "dictionaryWords" }); configurarCambioIdiomaDiccionario(); waitForElement("#details-special-words", (detailsElem) => { const arrow = document.getElementById("arrow"); if (detailsElem && arrow) { detailsElem.addEventListener("toggle", function() { arrow.style.transform = detailsElem.open ? "rotate(90deg)" : "rotate(0deg)"; }); } else { console.error( "No se encontró el elemento #details-special-words o #arrow"); } }); waitForElement("#details-dictionary-words", (detailsElem) => { const arrow = document.getElementById("arrow-dic"); if (detailsElem && arrow) { detailsElem.addEventListener("toggle", function() { arrow.style.transform = detailsElem.open ? "rotate(90deg)" : "rotate(0deg)"; }); } else { console.error( "No se encontró el elemento #details-dictionary-words o #arrow-dic"); } }); window.applyNormalization = applyNormalization; window.normalizePlaceName = normalizePlaceName; if (W && W.model && W.model.venues) { W.model.venues.on("zoomchanged", () => { placesToNormalize = []; const existingPanel = document.getElementById("normalizer-floating-panel"); if (existingPanel) { existingPanel.remove(); } console.log( "Cambio de zoom detectado: Se ha reiniciado la búsqueda de lugares."); }); } }); } // Inicia el script init(); // -------------------------------------------------------------------- // Fin del script principal unsafeWindow.normalizePlaceName = normalizePlaceName; unsafeWindow.applyNormalization = applyNormalization; window.addEventListener("dragover", e => e.preventDefault()); window.addEventListener("drop", e => e.preventDefault()); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址