您需要先安装一个扩展,例如 篡改猴、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.1.0 // @description Normaliza nombres de lugares en Waze Map Editor (WME) // @author mincho77 // @match https://www.waze.com/*editor* // @match https://beta.waze.com/*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 = 100; let normalizeArticles = false; let placesToNormalize = []; let wordLists = { excludeWords : JSON.parse(localStorage.getItem("excludeWords")) || [], dictionaryWords : JSON.parse(localStorage.getItem("dictionaryWords")) || [] }; // ============================================== // Internacionalización (i18n) // ============================================== const uiStrings = { SP : { activeLangLabel : "Idioma activo:", normalizeArticlesLabel : "Normalizar artículos (el, la, los, ...)", useApiLabel : "Usar API de ortografía", maxPlacesLabel : "Máximo de Places a buscar:", specialWordsLabel : "Palabras Especiales", addWordPlaceholder : "Agregar palabra...", addButton : "Agregar", searchWordPlaceholder : "Buscar palabra...", exportWordsButton : "Exportar Palabras", importListButton : "Importar Lista", replaceListLabel : "Reemplazar lista actual", dropZoneText : "📂 Arrastra aquí tu archivo .txt o .xml para importar palabras especiales", dictionaryLabel : "Diccionario Ortográfico", searchDictionaryPlaceholder : "Buscar palabra...", exportDictionaryButton : "📤 Exportar Diccionario", importDictionaryButton : "📥 Importar Diccionario", clearDictionaryButton : "🧹 Limpiar Diccionario", dictionaryDropZoneText : "📂 Arrastra aquí tu archivo de palabras del diccionario (.xml o .txt)", scanButton : "Scan...", clearSpecialWordsButton : "Eliminar Palabras Especiales", // Floating Panel Headers & Text panelTitle : "Normalizador de Nombres", placesToReview : "lugares para revisar", applyCol : "Aplicar", deleteCol : "Eliminar", typeCol : "Tipo Place", permaCol : "Perma", categoryCol : "Categoría", // Nueva traducción currentNameCol : "Nombre Actual", normalizedNameCol : "Nombre Normalizado", problemCol : "Problema Detectado", actionsCol : "Acciones", applySelectedButton : "Aplicar Cambios Seleccionados", cancelButton : "Cancelar", errorDetailTitle : "error(es) encontrado(s)", // Panel flotante - Tabla normalizationLabel : "Normalización", normalizeButtonLabel : "NrmliZer", // O "Normalizar" si prefieres excludeWordButtonLabel : "ExcludeWrd", // O "Excluir Palabra" errorInLabel : "Error en:", suggestionLabel : "Sugerencia:", useSuggestionButtonLabel : "Usar sugerencia", // Tooltips placeTypeAreaTooltip : "Tipo de Lugar: Área", // Spinner & Progress spinnerProcessingMessage : "Procesando lugares...", // Nuevo spinnerCheckingMessage : "Revisando ortografía...", spinnerProgressMessage : "% completado", placesReadyToNormalize : "lugares marcados para Normalización", // Nuevo contador placeTypePointTooltip : "Tipo de Lugar: Punto" }, EN : { activeLangLabel : "Active Language:", normalizeArticlesLabel : "Do not normalize articles (the, a, an, ...)", // Will be hidden useApiLabel : "Verify Spelling With API", maxPlacesLabel : "Maximum Places to search:", specialWordsLabel : "Special Words", addWordPlaceholder : "Add word...", addButton : "Add", searchWordPlaceholder : "Search word...", exportWordsButton : "Export Words", importListButton : "Import List", replaceListLabel : "Replace current list", dropZoneText : "📂 Drag your .txt or .xml file here to import special words", dictionaryLabel : "Spelling Dictionary", searchDictionaryPlaceholder : "Search word...", exportDictionaryButton : "📤 Export Dictionary", importDictionaryButton : "📥 Import Dictionary", clearDictionaryButton : "🧹 Clear Dictionary", dictionaryDropZoneText : "📂 Drag your dictionary words file (.xml or .txt) here", scanButton : "Scan...", clearSpecialWordsButton : "Delete Special Words", // Floating Panel Headers & Text panelTitle : "Name Normalizer", placesToReview : "places to review", applyCol : "Apply", deleteCol : "Delete", typeCol : "Place Type", permaCol : "Perma", categoryCol : "Category", // Nueva traducción currentNameCol : "Current Name", normalizedNameCol : "Normalized Name", problemCol : "Problem Detected", actionsCol : "Actions", applySelectedButton : "Apply Selected Changes", cancelButton : "Cancel", errorDetailTitle : "error(s) found", // Floating Panel - Table normalizationLabel : "Normalization", normalizeButtonLabel : "NrmliZer", // Or "Normalize" excludeWordButtonLabel : "ExcludeWrd", // Or "Exclude Word" errorInLabel : "Error in:", suggestionLabel : "Suggestion:", useSuggestionButtonLabel : "Use suggestion", // Tooltips placeTypeAreaTooltip : "Place Type: Area", // Spinner & Progress spinnerProcessingMessage : "Processing places...", // New spinnerCheckingMessage : "Checking spelling...", spinnerProgressMessage : "% complete", placesReadyToNormalize : "places checked to normalize", // Nuevo contador placeTypePointTooltip : "Place Type: Point" } }; // Acá se obtienen los textos según el idioma activo function S(key) { return uiStrings[activeDictionaryLang]?.[key] || uiStrings['SP']?.[key] || `[${key}]`; // Fallback a SP y luego a la key } // Helper para obtener código de idioma para la API function getApiLangCode(internalLang) { return internalLang === 'EN' ? 'en' : 'es'; } // ============================================== // 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; // Asegura que normalizeArticles se inicialice correctamente según el // idioma Si es inglés, la normalización de artículos SIEMPRE está // desactivada. if (activeDictionaryLang === 'EN') { normalizeArticles = false; // Forzar desactivación para inglés } else { // Para español, cargar desde localStorage o usar valor por defecto // (true = NO normalizar) normalizeArticles = localStorage.getItem('normalizeArticles_SP') !== null ? JSON.parse(localStorage.getItem('normalizeArticles_SP')) : false; } 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, passive : false }); }); //******************************************************************************************************************************** // Nombre: obtenerListaEsdrujulas // Fecha modificación: 2025-07-29 // Autor: mincho77 // Entradas: Ninguna // Salidas: Array<string> - Una lista de palabras esdrújulas comunes. // Descripción: Devuelve una lista predefinida de palabras esdrújulas en español, // que siempre deben llevar tilde. Útil para validaciones o mejoras // en la detección de acentos. //******************************************************************************************************************************** function obtenerListaEsdrujulas() { return [ "música", "pájaro", "teléfono", "brújula", "matemáticas", "miércoles", "cámara", "rápido", "América", // También nombres propios "clásico", "página", "último", "oxígeno", "plátano", "semáforo", "clínica", "vehículos", "láminas", "sábado", "número", "crédito", "público", "ejército", "océano", "gramática", "fábrica", "máquina", "plástico", "cerámica", "económico", "política", "académico", "simpático", "fantástico", "práctico", "círculo", "triángulo", "rectángulo", "hipótesis", "análisis", "síntesis", "catástrofe", "metáfora", "paréntesis", "helicóptero", "termómetro", "kilómetro", "centímetro", "milímetro", "décimo", "vigésimo", "trigésimo", "cuadragésimo", "quincuagésimo", "sexagésimo", "septuagésimo", "octogésimo", "nonagésimo", "centésimo", "milésimo" // Puedes añadir más palabras aquí si lo necesitas ]; } //******************************************************************************************************************************** // Nombre: generarPluralesEsdrujulas // Fecha modificación: 2025-07-29 // Autor: Chat Asistente (basado en solicitud) // Entradas: listaSingulares (Array<string>) - Lista de palabras esdrújulas // en singular. Salidas: Array<string> - Lista combinada de singulares y sus // plurales correspondientes. Descripción: Genera formas plurales para una // lista de palabras esdrújulas. // Maneja casos comunes como añadir 's' a vocales y palabras // invariables terminadas en 's'. Devuelve una lista única y // ordenada alfabéticamente. //******************************************************************************************************************************** function generarPluralesEsdrujulas(listaSingulares) { if (!Array.isArray(listaSingulares)) { console.error( "generarPluralesEsdrujulas: La entrada debe ser un array."); return []; } const resultadoSet = new Set(); // Usar un Set para evitar duplicados automáticamente listaSingulares.forEach(palabra => { if (typeof palabra !== 'string' || palabra.trim() === '') return; // Ignorar entradas inválidas resultadoSet.add(palabra); // Añadir la palabra original const ultimaLetra = palabra.slice(-1).toLowerCase(); // Regla general para plurales de esdrújulas: if ('sx'.includes(ultimaLetra)) { // Palabras terminadas en 's' o 'x' (no agudas) suelen ser // invariables resultadoSet.add( palabra); // Añadir la misma palabra (Set maneja duplicados) } else if ('aeiouáéíóú'.includes(ultimaLetra)) { // Palabras terminadas en vocal: añadir 's' resultadoSet.add(palabra + 's'); } // Nota: Es raro que esdrújulas terminen en otras consonantes, pero // se podrían añadir más reglas aquí si fuera necesario. }); return Array.from(resultadoSet) .sort((a, b) => a.localeCompare(b)); // Convertir a array y ordenar } // Acá se normaliza una palabra: se pasa a minúsculas y se quitan las tildes function normalizarPalabra(palabra) { return palabra.normalize("NFD") .replace(/[\u0300-\u036f]/g, "") // Quitar tildes .toLowerCase(); } // ======================= // Integración con API LanguageTool y gestión de sugerencias ortográficas // ======================= // Estructura interna: cada error ortográfico sugerido por la API se // almacena así: { word: 'Frayle', suggestion: 'Fraile', origin: 'API', // message: 'Sugerencia de la API', index: ... } // Mapa de sugerencias API por placeId (o por nombre si es necesario) let spellingApiSuggestions = {}; // { [placeId]: [ {word, suggestion, // origin, message, index} ] } //******************************************************************************************************************************** // Nombre: checkSpellingWithAPI (Acá se llama a la API de LanguageTool) // Descripción: Llama a la API de LanguageTool para revisar la ortografía de // un texto. // Acá se guardan las sugerencias encontradas para un 'placeId' // específico, pero NO se aplican automáticamente al nombre. // Acá se ejecuta un 'callback' con las sugerencias obtenidas. //******************************************************************************************************************************** function checkSpellingWithAPI(text, lang, placeId, callback) { // Limpieza y validación inicial if (!placeId || !text || typeof callback !== 'function') { console.warn(`checkSpellingWithAPI: Faltan parámetros (placeId: ${ !!placeId}, text: "${text}", callback: ${ typeof callback}). Llamando callback(null).`); if (typeof callback === 'function') callback(null); // <-- LLAMAR AL CALLBACK AQUÍ return; // <-- Salir temprano } if (!spellingApiSuggestions[placeId]) { spellingApiSuggestions[placeId] = []; } // Llamada a la API console.log( `[checkSpellingWithAPI] Initiating GM_xmlhttpRequest for placeId ${ placeId}`); // <-- Nuevo Log GM_xmlhttpRequest({ method : "POST", url : "https://api.languagetool.org/v2/check", headers : { "Content-Type" : "application/x-www-form-urlencoded" }, data : `text=${encodeURIComponent(text)}&language=${ getApiLangCode(lang)}`, onload : function(response) { console.log( `[checkSpellingWithAPI] onload triggered for placeId ${ placeId}, status: ${response.status}`); // <-- Nuevo Log try { if (response.status === 200) { const result = JSON.parse(response.responseText); const suggestions = result.matches.map( (match) => ({ word : match.context.text.substring( match.context.offset, match.context.offset + match.context.length), suggestion : match.replacements.length > 0 ? match.replacements[0].value : null, // Mantener null si no hay sugerencia message : match.message, rule : match.rule.id, })); // Almacenar sugerencias en el objeto global spellingApiSuggestions[placeId] = suggestions; // Llamar al callback con las sugerencias callback(suggestions); } else { console.error("Error en la API de LanguageTool:", response.status, response.statusText); // safeCallback(null) ya se llama en el catch o al final // del try callback(null); // o callback([]) } } catch (e) { // <--- Añadir catch console.error("Error procesando respuesta de la API:", e); callback(null); // Llamar con null si hay error en el try } }, onerror : function(error) { // <--- Añadir parámetro error console.log( `[checkSpellingWithAPI] onerror triggered for placeId ${ placeId}`); // <-- Nuevo Log console.error("Error al contactar la API de LanguageTool:", error); // Llamar al callback con null o vacío en caso de error de API callback(null); // o callback([]) }, ontimeout : function() { // Añadir handler de timeout console.log( `[checkSpellingWithAPI] ontimeout triggered for placeId ${ placeId}`); // <-- Nuevo Log console.error("Timeout al contactar la API de LanguageTool."); callback(null); // Llamar con null en timeout }, timeout : 30000 // Aumentar timeout a 30 segundos }); } // Acá se revisa si la palabra está en las especiales function esPalabraEspecial(palabra) { const palabraNormalizada = normalizarPalabra(palabra); for (const especial of specialWords) { const especialNormalizada = normalizarPalabra( especial); // Se normaliza la palabra especial para comparar if (palabraNormalizada === especialNormalizada) { return especial; // Retorna la versión oficial con tildes y // formato } } return null; } //******************************************************************************************************************************** // Nombre: debugDictionaries (Acá se depuran los diccionarios) // Fecha de modificación: 2025-04-15 12:17 // Autor: mincho77 // Entradas: Nada // Salidas: Nada. Se 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 que se está usando. Sirve para depurar y ver cómo están // configurados los diccionarios. Permite verificar que el idioma y el // diccionario se hayan cargado correctamente. Se puede usar directamente // desde la consola del navegador para revisar el estado. //********************************************************************************************************************************* unsafeWindow.debugDictionaries = function () { console.log("Idioma activo:", activeDictionaryLang); console.log("Diccionario actual:", spellDictionaries[activeDictionaryLang]); }; // --- Inicio: Adaptación del código Python para silabificación --- const VOWEL_SET = new Set('AEIOUaeiouÀÁÄÈÉËÌÍÏÒÓÖÙÚÜàáäèéëìíïòóöùúü'); const OPEN_PLAIN = new Set('aeo'); const OPEN_ACCENTED = new Set('áàéèóò'); // Usando caracteres explícitos const OPEN_FULL = new Set([...OPEN_PLAIN, ...OPEN_ACCENTED ]); const CLOSED_PLAIN = new Set('iu'); const CLOSED_ACCENTED = new Set('íìúù'); // Usando caracteres explícitos const BEFORE_L_GROUP = new Set('bvckfgpt'); const BEFORE_R_GROUP = new Set('bvcdkfgpt'); const FOREIGN_GROUP = new Set('slrnc'); const CONSONANT_PAIRS = new Set([ 'pt', 'ct', 'cn', 'ps', 'mn', 'gn', 'ft', 'pn', 'cz', 'ts' ]); function isConsonant(character) { return character && !VOWEL_SET.has(character); } //******************************************************************************************************************************** // Nombre: onsetPy (Acá se procesa el ataque de la sílaba) // Descripción: Parte de la adaptación del código Python para // silabificación. Acá se procesa la parte inicial consonántica (ataque) de // una sílaba en una palabra dada, empezando desde la posición 'pos'. //******************************************************************************************************************************** function onsetPy(word, pos) { let last_consonant = 'a'; // Equivalente a u'a' const length = word.length; while (pos < length && isConsonant(word[pos]) && word[pos].toLowerCase() !== 'y') { last_consonant = word[pos]; pos += 1; } if (length <= pos) { return pos; } const c1 = word[pos].toLowerCase(); if (pos < length - 1) { if (c1 === 'u') { if (last_consonant === 'q') { pos += 1; } else if (last_consonant === 'g') { const c2 = word[pos + 1].toLowerCase(); if ('eéií'.includes(c2)) { // Simple string check for these vowels pos += 1; } } } else if (c1 === 'ü' && last_consonant === 'g') { pos += 1; } } return pos; } function nucleusPy(word, pos) { let previous = 0; const length = word.length; let stress_found = false; if (pos >= length) { return { pos : pos, stress_found : stress_found }; } if (word[pos].toLowerCase() === 'y') { pos += 1; } if (pos < length) { const cr = word[pos].toLowerCase(); if (OPEN_ACCENTED.has(cr)) { stress_found = true; previous = 0; pos += 1; } else if (OPEN_PLAIN.has(cr)) { previous = 0; pos += 1; } else if (CLOSED_ACCENTED.has(cr) || cr === 'ü') { stress_found = true; return { pos : pos + 1, stress_found : stress_found }; } else if (CLOSED_PLAIN.has(cr)) { previous = 2; pos += 1; } } let aitch = false; if (pos < length) { const cr = word[pos].toLowerCase(); if (cr === 'h') { pos += 1; aitch = true; } } if (pos < length) { const cr = word[pos].toLowerCase(); if (OPEN_FULL.has(cr)) { if (OPEN_ACCENTED.has(cr)) { stress_found = true; } if (previous === 0) { if (aitch) { pos -= 1; } return { pos : pos, stress_found : stress_found }; } else { pos += 1; } } else if (CLOSED_ACCENTED.has(cr)) { stress_found = true; if (previous !== 0) { pos += 1; } else if (aitch) { pos -= 1; } return { pos : pos, stress_found : stress_found }; } else if (CLOSED_PLAIN.has(cr) || cr === 'ü') { if (pos < length - 1) { const cr_next = word[pos + 1].toLowerCase(); if (!isConsonant(cr_next)) { if (pos > 0 && word[pos - 1].toLowerCase() === 'h') { pos -= 1; } return { pos : pos, stress_found : stress_found }; } } // Check needed: word[pos-1] might be out of bounds if pos=0 if (pos > 0 && word[pos].toLowerCase() !== word[pos - 1].toLowerCase()) { pos += 1; } else if (pos === 0) { // If it's the first char, still might need to advance pos += 1; } return { pos : pos, stress_found : stress_found }; } } if (pos < length) { if (CLOSED_PLAIN.has(word[pos].toLowerCase())) { return { pos : pos + 1, stress_found : stress_found }; } } return { pos : pos, stress_found : stress_found }; } function codaPy(word, pos) { const length = word.length; if (pos >= length || !isConsonant(word[pos])) { return pos; } else if (pos === length - 1) { return pos + 1; } const c1 = word[pos].toLowerCase(); const c2 = word[pos + 1].toLowerCase(); if (!isConsonant(c2)) { // Specific check for 'y' acting as vowel sound after consonant if (c2 === 'y' && pos + 1 === length - 1) return pos; // e.g., rey -> keep c1 with nucleus return pos; // Only one consonant in coda } if (pos < length - 2) { const c3 = word[pos + 2].toLowerCase(); if (!isConsonant(c3)) { // Pattern C-C-V if (c1 === 'l' && c2 === 'l') return pos; if (c1 === 'c' && c2 === 'h') return pos; if (c1 === 'r' && c2 === 'r') return pos; if (c1 !== 's' && c1 !== 'r' && c2 === 'h') return pos; if (c2 === 'y') return FOREIGN_GROUP.has(c1) ? pos : pos + 1; if (BEFORE_L_GROUP.has(c1) && c2 === 'l') return pos; if (BEFORE_R_GROUP.has(c1) && c2 === 'r') return pos; return pos + 1; // Split C1 | C2-V } else { // Pattern C-C-C... if (pos + 3 === length) { // Ends in CCC if (c2 === 'y') return FOREIGN_GROUP.has(c1) ? pos : pos + 1; if (c3 === 'y') return pos + 1; // Ends V-CCy -> VC | Cy return pos + 3; // Ends V-CCC -> VCCC | } // Check next char after CCC const c4 = word[pos + 3].toLowerCase(); if (!isConsonant(c4)) { // Pattern C-C-C-V if (c2 === 'y') return FOREIGN_GROUP.has(c1) ? pos : pos + 1; if (CONSONANT_PAIRS.has(word.substring(pos + 1, pos + 3))) return pos + 1; // V-C | CCV (pair starts syllable) if ((c3 === 'l' || c3 === 'r') || (c2 === 'c' && c3 === 'h') || (c3 === 'y')) return pos + 1; // V-C | CCV (blend/ch/y starts syllable) return pos + 2; // V-CC | CV } else { // Pattern C-C-C-C... very rare, assume split after C2 for // simplicity return pos + 2; } } } else { // Pattern C-C at end of word if (c2 === 'y') return pos; // Treat final 'y' like a vowel sound attached to C1 return pos + 2; // Ends in CC } } function obtenerSilabasAproximadas(word) { if (!word || typeof word !== 'string') return []; word = word.normalize("NFC"); // Keep NFC normalization let pos = 0; const positions = []; const length = word.length; // let stress_found = false; // Ignored for now // let stressed = 0; // Ignored for now while (pos < length) { positions.push(pos); pos = onsetPy(word, pos); let nucleusResult = nucleusPy(word, pos); pos = nucleusResult.pos; // stress_found = nucleusResult.stress_found; // Ignored pos = codaPy(word, pos); // Stress logic from Python ignored for now } positions.push(length); const syllabes = []; for (let i = 0; i < positions.length - 1; i++) { // Avoid creating empty syllables if positions are duplicated (can // happen with complex logic) if (positions[i + 1] > positions[i]) { syllabes.push(word.substring(positions[i], positions[i + 1])); } } // Filter out potential empty strings just in case return syllabes.filter(s => s.length > 0); } // --- Fin: Adaptación del código Python --- //*********************************************************************************************************** // Nombre: detectarTilde // Descripción: Analiza tildes, clasifica y valida acentuación básica // (simplificado). Versión final revisada para asegurar definición de // variables y lógica de validación. //*********************************************************************************************************** function detectarTilde(palabra) { // 1. Normalizar la palabra original primero palabra = palabra.normalize("NFC"); // 2. Limpiar puntuación final común ANTES de cualquier validación const palabraLimpia = palabra.replace(/[.,;:)]+$/, ''); // Añadido ')' a la regex // 3. Validación inicial (AHORA usa palabraLimpia después de declararla) if (!palabraLimpia || typeof palabraLimpia !== "string" || palabraLimpia.trim().length === 0) { // Retornar basado en la entrada original si es necesario return { tieneTilde : false, tipo : null, silabaTildada : null, esValida : true // Considerar palabra vacía o inválida como // "válida" para no marcarla como error }; } // --- Resto de la lógica usando palabraLimpia --- // Definiciones de vocales con y sin tilde const vocalesConTilde = "áéíóúÁÉÍÓÚ"; const vocales = "aeiouáéíóúÁÉÍÓÚ"; // Información sobre la última letra para reglas de acentuación const ultimaLetra = palabraLimpia .slice(-1) // <-- Usar palabraLimpia .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase(); const terminaEnVocalNS = [ 'a', 'e', 'i', 'o', 'u', 'n', 's' ].includes(ultimaLetra); // Obtiene una aproximación de las sílabas const silabas = obtenerSilabasAproximadas(palabraLimpia); // <-- Usar palabraLimpia const totalSilabas = silabas.length; // Detecta si hay tilde gráfica y en qué sílaba let silabaTildadaIndex = -1; // Índice base 0 for (let i = 0; i < silabas.length; i++) { for (let char of silabas[i]) { // Busca si el carácter es una vocal tildada (usando la variable // definida) if (vocalesConTilde.includes(char)) { silabaTildadaIndex = i; break; // Tilde encontrada } } if (silabaTildadaIndex !== -1) break; // Tilde encontrada en alguna sílaba } const tieneTilde = silabaTildadaIndex !== -1; let tipo = null; // Tipo de palabra (aguda, grave, etc.) let esValida = false; // Indica si cumple reglas de acentuación let silabaTildadaOrdinal = null; // Número ordinal (base 1) if (tieneTilde) { // Si tiene tilde, el tipo y la validación se basan en su posición. silabaTildadaOrdinal = silabaTildadaIndex + 1; const posicionDesdeFinal = totalSilabas - silabaTildadaOrdinal; // 0=última, 1=penúltima, etc. // *** NUEVO: Chequeo prioritario para hiato acentual con vocal abierta *** const silabaConTilde = silabas[silabaTildadaIndex]; let caracterTildado = ''; for (let char of silabaConTilde) { if (vocalesConTilde.includes(char)) { caracterTildado = char; break; } } // Verificación de hiato con vocal débil acentuada (í, ú) const hiatoConVocalDebil = () => { if (!tieneTilde || silabaTildadaIndex === -1) return false; const silaba = silabas[silabaTildadaIndex]; const vocalDebilTildada = /[íúÍÚ]/i.test(silaba); // Incluir mayúsculas if (!vocalDebilTildada) return false; const anterior = silabas[silabaTildadaIndex - 1] || ""; const siguiente = silabas[silabaTildadaIndex + 1] || ""; const vocalFuerte = /[aeoáéó]/i; return vocalFuerte.test(anterior.slice(-1)) || vocalFuerte.test(siguiente); }; // *** MODIFICADO: Si la tilde está en vocal abierta (á,é,ó), es válida (hiato) *** if (OPEN_ACCENTED.has(caracterTildado.toLowerCase())) // <-- Chequea si es á, é, ó { tipo = "hiato_abierta_acentuada"; // Nuevo tipo para claridad esValida = true; // Tilde en vocal abierta para hiato es válida } else if (hiatoConVocalDebil()) // <-- Ahora es else if { tipo = "hiato_debil_acentuada"; // Podría ser grave o aguda // dependiendo de la estructura esValida = true; // Hiato con tilde en débil siempre es válido } else if (totalSilabas === 1) { tipo = "monosílaba"; esValida = true; // Monosílabos con tilde (diacrítica) asumidos válidos. } else if (posicionDesdeFinal === 0) { tipo = "aguda"; esValida = terminaEnVocalNS; // Aguda con tilde es válida si // termina en vocal/n/s } else if (posicionDesdeFinal === 1) { tipo = "grave"; esValida = !terminaEnVocalNS; // Grave con tilde es válida si NO // termina en vocal/n/s } else if (posicionDesdeFinal === 2) { tipo = "esdrújula"; esValida = true; // Esdrújulas siempre llevan tilde y son // válidas si las tienen. } else { // posicionDesdeFinal >= 3 tipo = "sobresdrújula"; esValida = true; // Sobresdrújulas siempre llevan tilde y son // válidas si las tienen. } } else // No tiene tilde { silabaTildadaOrdinal = null; // No hay sílaba tildada if (totalSilabas <= 1) { // Considerar palabras de 0 o 1 sílaba como monosílabas tipo = "monosílaba"; esValida = true; // Monosílabos sin tilde son válidos. } else { // Clasificar tipo por acento natural según terminación // *** Lógica corregida para determinar tipo sin tilde *** if (terminaEnVocalNS) { // Si termina en vocal, n, o s -> Naturalmente grave tipo = "grave"; // Acento natural en penúltima } else { tipo = "aguda"; // Acento natural en última } // *** NUEVO: Heurística para palabras terminadas en -ción/-sión *** // Si termina en 'cion' o 'sion' y no tiene tilde, es casi seguro que es aguda. const lowerPalabraLimpia = palabraLimpia.toLowerCase(); if (lowerPalabraLimpia.endsWith('cion') || lowerPalabraLimpia.endsWith('sion')) { tipo = "aguda"; // Corregir tipo a aguda } // Validar si DEBERÍA llevar tilde pero no la tiene if ((tipo === "aguda" && terminaEnVocalNS) || (tipo === "grave" && !terminaEnVocalNS)) // Ya no se asume esdrújula aquí, las esdrújulas SIEMPRE deben tener tilde gráfica { esValida = false; // Debería llevar tilde pero no la tiene } else { esValida = true; // Es válida sin tilde } } } // Validación extra: hiato obligatorio (palabras conocidas) if (!tieneTilde && esHiatoObligatorio(palabraLimpia)) { tipo = "hiato_debil_acentuada_forzada"; esValida = false; } if (!esValida) { // Usar 'palabra' (original con posible puntuación) para el log de // error console.log(`❌ Palabra: ${palabra}, Tipo: ${tipo}, Tilde en: ${ silabaTildadaOrdinal || 'N/A'}, Total Sílabas: ${totalSilabas}, Es válida: ${esValida}`); } return { tieneTilde, tipo, silabaTildada : silabaTildadaOrdinal, esValida }; } window.detectarTilde = detectarTilde; function validarReglasAcentuacion(tipo, ultimaLetra, tieneTilde) { if (tipo === "aguda") { // Agudas llevan tilde si terminan en vocal, "n" o "s" return (tieneTilde && /[nsaeiou]$/.test(ultimaLetra)) || (!tieneTilde && !/[nsaeiou]$/.test(ultimaLetra)); } else if (tipo === "grave") { // Graves llevan tilde si NO terminan en vocal, "n" o "s" return (!tieneTilde && /[nsaeiou]$/.test(ultimaLetra)) || (tieneTilde && !/[nsaeiou]$/.test(ultimaLetra)); } else if (tipo === "esdrújula") { // Esdrújulas siempre llevan tilde return tieneTilde; } return true; // Otros casos (monosílabos, sobresdrújulas, etc.) } // ======================= // Normalización y gestión de sugerencias ortográficas // ======================= // Esta función NO debe aplicar sugerencias API automáticamente function normalizePlaceName(originalName, placeId, callback) { // Aquí va la lógica de normalización local: tildes, diccionario, etc. // ... // Si se desea usar la API, sólo agregar sugerencias, NO reemplazar el // valor const useApi = document.getElementById("useSpellingAPI")?.checked; if (useApi) { checkSpellingWithAPI(originalName, activeDictionaryLang, placeId, function(apiSuggestions) { // Almacenar sugerencias API en el formato // requerido spellingApiSuggestions[placeId] = apiSuggestions.map( s => ({ original : s.word, suggestion : s.suggestion, message : s.message, origin : "API" })); // No se hace ningún reemplazo aquí, sólo // se pasa la sugerencia al callback/panel if (typeof callback === "function") callback({ normalized : originalName, apiSuggestions : apiSuggestions }); }); } else { // Normalización tradicional aquí (si aplica) if (typeof callback === "function") callback({ normalized : originalName, apiSuggestions : [] }); } } // ======================= // Panel flotante: renderizado de errores ortográficos y sugerencias API // ======================= // Renderiza el panel flotante de errores ortográficos, incluyendo // sugerencias de origen API function openFloatingPanel(placesToNormalize) { // Cierra panel previo si existe const existingPanel = document.getElementById("normalizer-floating-panel"); if (existingPanel) { existingPanel.remove(); } 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: 0; 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; vertical-align: top; } .normalize-btn, .apply-btn, .add-exclude-btn { padding: 8px 16px; 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; } .close-btn { position: absolute; top: 15px; right: 15px; background: #e74c3c; color: white; border: none; width: 30px; height: 30px; border-radius: 50%; font-weight: bold; cursor: pointer; z-index: 11; } input[type="checkbox"] { transform: scale(1.3); margin: 0 5px; } input[type="text"] { width: 100%; padding: 5px; box-sizing: border-box; } </style> <div style="padding: 20px;"> <button class="close-btn" id="close-panel-btn">×</button> <h2 style="color: #2c3e50; margin-top: 0;">Normalizador de Nombres</h2> <table id="normalizer-table"> <thead> <tr> <th>Aplicar</th> <th>Eliminar</th> <th>Nombre Actual</th> <th>Nombre Normalizado</th> <th>Problemas Detectados</th> <th>Acciones</th> </tr> </thead> <tbody> `; placesToNormalize.forEach((place, index) => { // Asegura incluir spellingWarnings y hasSpellingWarning desde // spellingApiSuggestions place.spellingWarnings = spellingApiSuggestions[place.placeId] || []; place.hasSpellingWarning = place.spellingWarnings.length > 0; const { originalName, newName, hasSpellingWarning, spellingWarnings = [] } = place; html += ` <tr> <td><input type="checkbox" class="normalize-checkbox" data-index="${ index}"></td> <td><input type="checkbox" class="delete-checkbox" data-index="${ index}"></td> <td>${originalName}</td> <td><input type="text" class="new-name-input" value="${ newName}" data-index="${index}"></td> <td>${ hasSpellingWarning ? `${spellingWarnings.length} errores` : "Ninguno"}</td> <td> <button class="normalize-btn" data-index="${ index}">Normalizar</button> <button class="add-exclude-btn" data-word="${ originalName}" data-index="${index}">Excluir</button> </td> </tr> `; if (spellingWarnings.length > 0) { html += ` <tr> <td colspan="6"> <details> <summary>${ spellingWarnings.length} errores encontrados</summary> <ul> ${ spellingWarnings .map(warning => ` <li> Error en "${warning.original}": ${ warning.message}<br> Sugerencia: <strong>${ warning.suggestion}</strong> <button class="apply-suggestion-btn" data-index="${ index}" data-suggestion="${ warning.suggestion}">Aplicar</button> </li> `) .join('')} </ul> </details> </td> </tr> `; } }); html += ` </tbody> </table> <div style="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</button> <button id="cancel-btn" style="background: #e74c3c; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-weight: bold;">Cancelar</button> </div> </div> `; panel.innerHTML = html; document.body.appendChild(panel); // Cerrar panel document.getElementById("close-panel-btn") .addEventListener("click", () => panel.remove()); // Aplicar todos los cambios document.getElementById("apply-all-btn") .addEventListener("click", () => { const selectedChanges = placesToNormalize.filter((_, index) => { const checkbox = panel.querySelector( `.normalize-checkbox[data-index="${index}"]`); return checkbox && checkbox.checked; }); if (selectedChanges.length === 0) { alert("No se seleccionaron cambios para aplicar."); return; } console.log("Aplicando cambios:", selectedChanges); panel.remove(); }); // Cancelar document.getElementById("cancel-btn") .addEventListener("click", () => panel.remove()); // Aplicar sugerencias panel.querySelectorAll(".apply-suggestion-btn").forEach(btn => { btn.addEventListener("click", function() { const index = this.dataset.index; const suggestion = this.dataset.suggestion; const input = panel.querySelector(`.new-name-input[data-index="${index}"]`); if (input) { input.value = suggestion; const checkbox = panel.querySelector( `.normalize-checkbox[data-index="${index}"]`); if (checkbox) checkbox.checked = true; } }); }); // Normalizar individualmente panel.querySelectorAll(".normalize-btn").forEach(btn => { btn.addEventListener("click", function() { const index = this.dataset.index; const input = panel.querySelector(`.new-name-input[data-index="${index}"]`); if (input) { input.value = input.value.trim() .toUpperCase(); // Ejemplo de normalización const checkbox = panel.querySelector( `.normalize-checkbox[data-index="${index}"]`); if (checkbox) checkbox.checked = true; } }); }); // Excluir palabras panel.querySelectorAll(".add-exclude-btn").forEach(btn => { btn.addEventListener("click", function() { const word = this.dataset.word; console.log(`Palabra excluida: ${word}`); }); }); } //******************************************************************************************************************************** // Nombre: showNoPlacesFoundMessage (Acá se muestra un mensaje si no se // encuentran lugares) Fecha de modificación: 2025-04-10 Autor: mincho77 // Entradas: Nada // Salidas: Nada. Se crea un modal que informa que no se encontraron lugares // con los criterios actuales. Descripción: Muestra un mensaje modal cuando // la búsqueda de lugares no devuelve resultados. // Incluye un botón para cerrar el modal. Sirve para informar // al usuario. //******************************************************************************************************************************** function showNoPlacesFoundMessage() { // Se crea el elemento div para 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> `; // Se agrega el modal al cuerpo del documento document.body.appendChild(modal); // Se maneja el evento de clic en el botón para cerrar el modal 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 (Acá se muestra un modal personalizado) // Fecha de modificación: 2025-04-10 // Autor: mincho77 // Entradas: // - title (string): Título del modal. // - message (string): Mensaje a mostrar. // - confirmText (string): Texto del botón de confirmar. // - cancelText (string): Texto del botón de cancelar. // - onConfirm (function): Qué hacer al confirmar. // - onCancel (function): Qué hacer al cancelar. // - type (string): Tipo de modal (info, error, warning, question, success). // - autoClose (number): Tiempo en ms para que se cierre solo. // - prependText (string): Texto extra antes del mensaje. // Salidas: Nada. Se crea un modal personalizado con título, mensaje y // botones. 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 = "", }) { // Se determina el ícono según el tipo de modal 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); // Se crea el elemento div para 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> `; // Se agrega el modal al cuerpo del documento document.body.appendChild(modal); // Se manejan los eventos de clic en 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 }); } // Se cierra el modal al hacer clic en el botón de cerrar (X) modal.querySelector(".close-modal-btn") .addEventListener("click", () => { modal.remove(); }); // Se cierra automáticamente si se especificó un tiempo en 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 (Acá se abre un popup para editar una palabra) // Fecha de 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: Nada. Se abre un popup para editar una palabra en la lista // especificada. Descripción: Esta función abre un popup que permite al // usuario modificar una palabra de la lista (ya sea de las excluidas o del // diccionario). Se valida que la palabra no esté vacía y que no sea // duplicada. o su diccionario ortográfico personalizado. //******************************************************************************************************************************** function openEditPopup(index, listType = "excludeWords") { const wordList = listType === "dictionaryWords" ? dictionaryWords : excludeWords; const wordToEdit = wordList[index]; if (!wordToEdit) { // Se muestra un error si no se encuentra la palabra en el índice // dado 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 = // Se obtiene el nuevo valor del input document.getElementById("editWordInput").value.trim(); if (!newWord) { showModal({ title : "Error", message : "La palabra no puede estar vacía.", confirmText : "Aceptar", type : "error" }); return; } // Se verifica si la nueva palabra ya existe en la lista (y no // es la misma palabra original) if (wordList.includes(newWord) && wordList[index] !== newWord) { showModal({ title : "Duplicada", message : "Esa palabra ya está en la lista.", confirmText : "Aceptar", type : "warning" }); return; } // Se actualiza la palabra en la lista correspondiente wordList[index] = newWord; if (listType === "dictionaryWords") { // *** Lógica corregida para guardar en el diccionario estructurado *** const originalWord = wordToEdit; // Palabra antes de editar const currentDictLang = spellDictionaries[activeDictionaryLang]; // 1. Eliminar la palabra original de la estructura const originalLetter = originalWord.charAt(0).toLowerCase(); if (currentDictLang && currentDictLang[originalLetter]) { const originalIndexInLetter = currentDictLang[originalLetter].indexOf(originalWord); if (originalIndexInLetter > -1) { currentDictLang[originalLetter].splice(originalIndexInLetter, 1); // Si la letra queda vacía, eliminarla if (currentDictLang[originalLetter].length === 0) { delete currentDictLang[originalLetter]; } } } // 2. Agregar la nueva palabra a la estructura const newLetter = newWord.charAt(0).toLowerCase(); if (!currentDictLang[newLetter]) { currentDictLang[newLetter] = []; } if (!currentDictLang[newLetter].includes(newWord)) { // Evitar duplicados al agregar currentDictLang[newLetter].push(newWord); currentDictLang[newLetter].sort(); // Mantener ordenado } // 3. Guardar la estructura actualizada en localStorage con la clave correcta localStorage.setItem(`spellDictionaries_${activeDictionaryLang}`, JSON.stringify(currentDictLang)); // 4. Actualizar la lista plana global dictionaryWords = Object.values(currentDictLang).flat().sort(); renderDictionaryWordsPanel(); // Re-renderizar el panel } else { wordLists.excludeWords = excludeWords; localStorage.setItem("excludeWords", JSON.stringify(excludeWords)); renderExcludedWordsPanel(); } // Se muestra un mensaje de éxito showModal({ title : "Actualizada", message : "La palabra fue modificada correctamente.", confirmText : "Aceptar", type : "success", autoClose : 2000 }); } }); } //*********************************************************************************************************************************************************** // Nombre: waitForElement (Acá se espera a que un elemento aparezca en el // DOM) Fecha de modificación: 2025-04-10 Autor: mincho77 Entradas: // - selector (string): El selector CSS del elemento que se espera. // - callback (function): Función que se ejecuta cuando se encuentra el // elemento. // - interval (number, opcional): Tiempo en ms entre cada intento (defecto: // 300ms). // - maxAttempts (number, opcional): Número máximo de intentos (defecto: // 20). Salidas: Nada. Se ejecuta el callback con el elemento encontrado o // se muestra una advertencia si no se encuentra. Descripción: Esta función // espera a que un elemento (definido por un selector CSS) aparezca en el // DOM. Se revisa cada cierto tiempo (interval) hasta un número máximo de // veces (maxAttempts). Si se encuentra el elemento, se llama a la función // 'callback' pasándole 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(() => { // Se crea un intervalo para revisar const element = document.querySelector(selector); attempts++; if (element) { // Si se encuentra el elemento clearInterval(checkExist); callback(element); } else if (attempts >= maxAttempts) { // Si se supera el número máximo de intentos clearInterval(checkExist); // Se detiene la búsqueda console.warn(`No se encontró el elemento ${ selector} después de ${maxAttempts} intentos.`); } }, interval); } //******************************************************************************************************************************** // Nombre: handleLanguageChange (Acá se maneja el cambio de idioma) // Fecha de modificación: 2025-05-02 // Hora: 06:45 // Autor: mincho77 // Entradas: Event (opcional) - El evento 'change' del selector. // Descripción: Maneja la lógica cuando el usuario cambia el idioma en el // selector. //******************************************************************************************************************************** function handleLanguageChange() { const selector = document.getElementById("dictionaryLanguageSelect"); if (!selector) return; // Salir si el selector no existe // Se guarda el estado de normalizeArticles específico para español if (activeDictionaryLang === 'SP') { localStorage.setItem('normalizeArticles_SP', JSON.stringify(normalizeArticles)); } // Forzar normalizeArticles a false si el idioma es inglés if (activeDictionaryLang === 'EN') { normalizeArticles = false; } const previousLang = activeDictionaryLang; const newLang = selector.value; if (previousLang && spellDictionaries[previousLang]) // Si había un idioma anterior y // diccionario { // Guardar el diccionario del idioma anterior localStorage.setItem( `spellDictionaries_${previousLang}`, JSON.stringify(spellDictionaries[previousLang])); // Guardar estado de normalizeArticles si era español if (previousLang === 'SP') { localStorage.setItem('normalizeArticles_SP', JSON.stringify(normalizeArticles)); } } activeDictionaryLang = newLang; // Se actualiza el idioma activo localStorage.setItem("activeDictionaryLang", activeDictionaryLang); // Se carga el diccionario del nuevo idioma desde localStorage const storedDictionary = JSON.parse( localStorage.getItem(`spellDictionaries_${activeDictionaryLang}`)); // Si hay diccionario guardado, se usa if (storedDictionary) { spellDictionaries[activeDictionaryLang] = storedDictionary; } else if (!spellDictionaries[activeDictionaryLang] || Object.keys(spellDictionaries[activeDictionaryLang]).length === 0) { // Si no hay diccionario guardado o está vacío, se usan los valores // por defecto if (activeDictionaryLang === "SP") { spellDictionaries.SP = { a : [ "árbol" ], b : [ "barco" ] }; } else if (activeDictionaryLang === "EN") { spellDictionaries.EN = { a : [ "apple" ], b : [ "boat" ] }; } // Se guarda el diccionario por defecto en localStorage localStorage.setItem( `spellDictionaries_${activeDictionaryLang}`, JSON.stringify(spellDictionaries[activeDictionaryLang])); } // Se actualiza la lista plana de palabras del diccionario dictionaryWords = // Se actualiza la lista plana de palabras del // diccionario Object.values(spellDictionaries[activeDictionaryLang]).flat().sort(); // Actualizar estado de normalizeArticles según el nuevo idioma if (activeDictionaryLang === 'EN') { normalizeArticles = false; // Desactivar para inglés } else { // Cargar estado guardado para español o usar default normalizeArticles = localStorage.getItem('normalizeArticles_SP') !== null ? JSON.parse(localStorage.getItem('normalizeArticles_SP')) : true; } // --- INICIO: Se refresca la interfaz sin volver a registrar la pestaña // --- const pane = document.getElementById("normalizer-tab-pane"); // Usar el ID asignado if (pane) { pane.innerHTML = getSidebarHTML(); // Regenerar contenido con nuevo idioma // listeners waitForElement("#normalizeArticles", () => { console.log(`[${ SCRIPT_NAME}] ✅ Refreshing sidebar events after lang change`); attachEvents(); // Re-adjunta listeners generales initSearchSpecialWords(); // Re-inicializa búsqueda palabras // especiales renderDictionaryWordsPanel(); // Re-renderiza panel diccionario attachDictionarySearch(); // Re-adjunta búsqueda diccionario // Se asegura que el dropdown muestre el idioma correcto Y SE // VUELVE A ADJUNTAR EL LISTENER waitForElement("#dictionaryLanguageSelect", (sel) => { sel.value = activeDictionaryLang; attachLanguageChangeListener(); }); attachDetailsToggleListeners(); }); } else { console.error( "No se encontró el panel de la pestaña para refrescar."); } // --- FIN: Se refresca la interfaz --- // console.log("Idioma activo:", activeDictionaryLang); console.log("Datos del diccionario:", spellDictionaries[activeDictionaryLang]); } //*********************************************************************************************************************************************************** // Nombre: attachLanguageChangeListener (Acá se adjunta el listener para el // cambio de idioma) Fecha de modificación: 2025-07-27 Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna. // Descripción: Busca el selector de idioma y le adjunta el listener // 'handleLanguageChange'. //*********************************************************************************************************************************************************** function attachLanguageChangeListener() { const selector = document.getElementById("dictionaryLanguageSelect"); if (selector) { // Remover listener anterior por si acaso (evita duplicados si algo // sale mal) // Se quita el listener anterior por si acaso selector.removeEventListener('change', handleLanguageChange); selector.addEventListener( 'change', handleLanguageChange); // Se adjunta el listener console.log(`[${SCRIPT_NAME}] Language change listener attached.`); } else { console.error( "attachLanguageChangeListener: No se encontró #dictionaryLanguageSelect."); } } //*********************************************************************************************************************************************************** // Nombre: attachDetailsToggleListeners (Acá se conectan los listeners para // desplegar detalles) // Fecha modificación: 2025-07-27 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna. // Descripción: Adjunta los listeners 'toggle' a los elementos <details> // para animar las flechas de despliegue. //*********************************************************************************************************************************************************** function attachDetailsToggleListeners() { waitForElement("#details-special-words", (detailsElem) => { const arrow = document.getElementById("arrow"); if (detailsElem && arrow) { // Remover listener anterior por si acaso detailsElem.removeEventListener("toggle", toggleArrowRotation); // Adjuntar nuevo listener detailsElem.addEventListener("toggle", toggleArrowRotation); } }); waitForElement("#details-dictionary-words", (detailsElem) => { const arrow = document.getElementById("arrow-dic"); if (detailsElem && arrow) { // Remover listener anterior por si acaso detailsElem.removeEventListener("toggle", toggleArrowRotation); // Adjuntar nuevo listener detailsElem.addEventListener("toggle", toggleArrowRotation); } }); console.log(`[${SCRIPT_NAME}] Details toggle listeners attached.`); } //*********************************************************************************************************************************************************** // Nombre: toggleArrowRotation (Acá se rota la flecha de despliegue) // Fecha modificación: 2025-05-01 06:00 // Autor: mincho77 // Entradas: Ninguna // Salidas: Ninguna. Rota la flecha de despliegue según el estado del // elemento <details>. Descripción: Esta función rota la flecha de // despliegue según el estado del elemento <details> al que está asociada. // Si el elemento está abierto, la flecha se rota 90 grados; si está // cerrado, se rota a su posición original. Se utiliza para proporcionar una // indicación visual del estado del elemento <details> al usuario. La // función se adjunta como un event listener al evento 'toggle' del elemento // <details>. //*********************************************************************************************************************************************************** function toggleArrowRotation() { // 'this' se refiere al elemento <details> const arrowId = this.id === 'details-special-words' ? 'arrow' : 'arrow-dic'; const arrow = document.getElementById(arrowId); if (arrow) { arrow.style.transform = this.open ? "rotate(90deg)" : "rotate(0deg)"; } } //*********************************************************************************************************************************************************** // 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> ${ S('dictionaryLabel')} </summary> <!-- Buscar palabra --> <div style="margin-top: 10px;"> <input type="text" id="searchDictionaryWord" placeholder="${ S('searchDictionaryPlaceholder')}" 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">${ S('exportDictionaryButton')}</button> <button id="importDictionaryBtn">${ S('importDictionaryButton')}</button> <button id="clear-dictionary-btn" style="margin-left: 10px;">${ S('clearDictionaryButton')}</button> <input type="file" id="hiddenImportDictionaryInput" accept=".xml" style="display: none;"> </div> <!-- Drag & Drop Diccionario--> <div id="dictionary-drop-zone" style="border: 2px dashed #ccc; padding: 10px; margin: 10px;"> ${S('dictionaryDropZoneText')} </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> <!-- Selector de idioma (Movido aquí) --> <div style="margin-top: 10px;"> <label for="dictionaryLanguageSelect"><b>${ S('activeLangLabel')}</b></label> <select id="dictionaryLanguageSelect" style="width: 100%; margin-top: 5px; padding: 4px;"> <option value="SP">Español 🇪🇸</option> <option value="EN">English 🇬🇧</option> </select> </div> <!-- No Normalizar artículos --> <div style="margin-top: 15px; display: ${ activeDictionaryLang === 'EN' ? 'none' : 'block'};"> <input type="checkbox" id="normalizeArticles" ${ normalizeArticles ? "checked" : ""}> <label for="normalizeArticles">${ S('normalizeArticlesLabel')}</label> </div> <div style="margin-top: 15px;"> <input type="checkbox" id="useSpellingAPI"> <label for="useSpellingAPI">${S('useApiLabel')}</label> </div> <!-- Máximo de Places a buscar --> <div style="margin-top: 15px;"> <label>${S('maxPlacesLabel')} </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> ${ S('specialWordsLabel')} </summary> <div style="margin-top: 10px; display: flex; gap: 5px;"> <input type="text" id="excludeWord" placeholder="${ S('addWordPlaceholder')}" 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;">${ S('addButton')}</button> </div> <div style="margin-top: 10px; display: flex; gap: 5px;"> <input type="text" id="searchWord" placeholder="${ S('searchWordPlaceholder')}" 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;">${ S('exportWordsButton')}</button> <button id="importExcludeWordsUnifiedBtn" style="margin-top: 5px;">${ S('importListButton')}</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">${ S('replaceListLabel')}</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;"> ${S('dictionaryDropZoneText')} </div> ${S('dropZoneText')} </div> </details> <hr> <!-- Sección de Diccionario Ortográfico --> ${renderSpellDictionaryPanel()} <hr> <!-- Botón Scan --> <button id="scanPlaces">${S('scanButton')}</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;"> ${S('clearSpecialWordsButton')} </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; // Guardar preferencia solo si es español if (activeDictionaryLang === 'SP') { localStorage.setItem('normalizeArticles_SP', JSON.stringify(normalizeArticles)); } }); // 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; } let registration; const tabId = "normalizer-tab-pane"; // Usar un ID consistente // para el CONTENEDOR 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) // tabPane es el div contenedor { throw new Error( "Tab registration failed to return required elements"); } // Limpiar el contenido anterior del panel ANTES de añadir el // nuevo NO eliminar el tabPane en sí. tabPane.innerHTML = ''; // Configure tab tabLabel.innerHTML = ` <img src="data:image/jpeg;base64,/9j/4QDKRXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAoCgAwAEAAAAAQAAAqmkBgADAAAAAQAAAAAAAAAAAAD/2wCEAAEBAQEBAQIBAQIDAgICAwQDAwMDBAUEBAQEBAUGBQUFBQUFBgYGBgYGBgYHBwcHBwcICAgICAkJCQkJCQkJCQkBAQEBAgICBAICBAkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCf/dAAQABv/AABEIAGUAXwMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP7+KKKKACiivxO/4Kff8FePDv7Gt23wK+B9jbeKvird26zNBcMf7O0SGUfu7nUTGVd3Ycw2kbK8g5ZokIevQyvKq+MrLD4eN2/6+47MBl9XE1VRoq7P2rlnht4zNOwRFGSxOAB7noK5i18e+CL67+wWWsWMs/Ty0uImb/vkNmv86P42fHz9o/8Aaf1eTW/2lPiBrXitpWZvsAuXsdIiD/8ALOLTbQx2/lr0XzVlkx952PNfNsHwh+FdlOlzY+HNOtpoyGSWG3jjkUjoVdAGBHYg1+uYXwbk4fvq9n5RuvzX5H6Rh/C+q4/vKqT8l/wx/qNZFLX+ej+zZ+3h+2f+x/qMNx8G/Heoato0LBpPDnii4m1bTJlG3KI9w7XVqSowrQTKik7jG/Q/2Mf8E7v+Cjvwo/4KAfD25vdDtz4c8a6AI08QeGriUST2bSZCTwSAL9pspip8mdVXoUkWORWRfiuJ+A8Xlkfav3qfddPVdPyPlc/4QxOAXPLWHdf5dD9FqKK5bxp4v0bwH4Zu/FWvPstrOPccfeY9FRR3ZjgKPWvgMXi6WHpSr1pKMYq7b2SX+R81QoTqTVOmrt6JHU0V8kfAr9pTUfix4uufC+q6SlliB7iF4ZDIAqMqlZMgc/MMEcdsdK+t68DhHjHL88wax+WT5qd2tmtV5NJno51keJy6v9WxUbSt5fof/9D+/iiiigD5K/bq/acsP2Ov2TfG/wC0Tcwpd3Ph7TmbT7R2CC61Gdlt7G2z2865kjT8a/z2ftfifWtUv/GPj7UJNZ8Sa9cyajrGpTf6y7vZzullb0GeEQfKiBUXCqoH9d3/AAcVXl2n7C+haPGStrqHjnQo7nHdYGluYwfbzYkr+RYnnmv6H8I8vpwwM8T9qTt8klY/bPDPBQWGnX6t2+Ssdp8K/hT8Yf2gviVZ/Bn9n7w7N4p8U3qGYW0brDBbW6kK1zeXD/Jb26EjLHLH7qK74U/qF4j/AOCDH/BRrw74N/4SvTr7wTr+oLGHfQrS8vIJxgfMkN5cQLDK/ZQ6QqT1Ze36Wf8ABuH8PPBVn+zz8Rvi9DGj+J9c8YT6bfSHaZYbPTLeFbK3B6rFiV7hV6bpmPev6NK8Xi3xIxmGx0sNhUlGGmq3/wCB6Hj8R8dYuji5UcPZKOm39fgf5lFzba3o+t6l4R8XabdaHruiXL2Wp6Xfx+VdWdzHjdFKnY4IKkZV1KshKMpPoXwU/aD8ZfsgfGzw7+1R8PyxvPCU2/ULZP8Al/0aQqNRsmAxu8yFd8Q6CeOJv4cV+t3/AAcK+BPCXhT9tz4eeOvDsaQar4w8JajHrAT/AJbDR7u1Sxmcf3lW7mj3dWVVByEXH4i3j20dpLJesqQqjGRmIChAPmJJ4AA6+1fqmT42GZ5dCtUhpNar8H+Wnkfo2V4uGY4CNSpHSS2/A/0wfCnifRfGnhfTvGPhyYXOnaraw3lrKvSSGdBJGw9ipFfmh+1J8XX8feKx4K8PuZNM0qXZ+75+0XX3SQB1CfcT1OfavHv2Ovil43+FH/BKf4HeCfFENxp3i+/8GabbtDcqUuLa2SEIsrq3zK5i2BAQCCeR8pFfRv7I/wAGf7Y1Bfih4gi/0SyYpp8bdHlXgy/7sfRf9rn+EV/k/wCO+bYnOc5jwDkstW/30ltGC6fdaT7+7Hq0fHcG5PQymjVzzG6qF4013e11+S7avoj6Z/Zy+DqfC3wiLrVox/bOpBZLo9fLA+5CD6J/Fjq2e2K+iqTGOBS1/QXDHDmFyjAUsuwUbU6asv8AN+b3fmfk+bZpWxuIniq7vKX9W9Fsj//R/v4ooooA/K7/AILRfATWv2gf+CdfjzRfCls95rXhpLXxTp1vEPnmm0OdL0wr/tSxRvGOP4q/hs07UbPV9Pg1XTpFmt7mNZYnXlWRxlSMdiOlf6b80MVxC0E6h0cbWVhkEHggj0r/AD7f+Cgv7Hd3+wd+1dqvwhsIDF4K8Sm413wbLgBFsZJAbnThjgNp00gjVQABbvBjJ3Y/cPCPPIpTy+e/xR+7X8l+J+s+GmbxjzYKXXVfr+SOi/4J7/8ABQLx/wD8E7vinq/iHTtHl8V+BvFvlNr+hWzxxXaXMC+XFqFg0pSIziMCKWKRlWWNUw6GMbv6DPEP/BxT+wTZ+F5NR8JWHjLW9a8smLSE8P3VnK0gHCNc3Yis09NxmK+meK/kNZgME9K+ifgT+yJ+07+0zqMVh8EvBOp6tbybc6jNC1lpcasMiR764VIWTjnyfNf0Q19txFwVlWKqfXMX7vd3SXzv+lj6zO+EMvxFT6zXfL31SXz/AKRX/aH+Pnxo/bq/aTu/jT47sS2ua59m0fQ9A09muVsrQORbWNvkIZZZJZC0km1TJI38KKoT9f8A4F/8EdvDfw28a+H/ABj+2J460eWPS9uq6l4KsbeWWWby0EkNlNfecI5A0oHnRrABKmYwShZm/Qf9gn/gmd8PP2MJ4vif49vLfxj8SyjiK7jjI07SFkG1ksUf5pJinyPcvhiMhFiRih774r/BzxpoOq3nivSXn12xupHnm3fPdws5ydw/5aoOxHIHGMDNfyN9IH6SOOyPBrB8F0ozUdJSteytb3Vvp3WqOzJq2GxFT6lRqeypJWTSV5el17qt10b6W63/AAtoXib9pT4uST3uYYZSJLlk+7a2ifKkadgcfIn+1luxr9ddH0jTtA0u30XSIVgtbWNYoo0GAqKMACvgb9hPXri9i8QaTbQxNaRGCUzhcSea25fLY9wFXIH8PPrX6F1+DfRs4foRyZ55NudfEuTnJrXSTVl5XTfm35JL4DxVzKo8csviuWnSSUUttl+mnoFFFFf0Yflp/9L+/iiiigAr4P8A+CiX7Dfg79vb9na9+FGsTJpfiCwkGpeG9ZKb207VIVIikIGGaCRSYbiMEb4XYZBwR94UV04PF1MPVjWou0o7G2GxE6M1UpuzWx/mfXOleM/hR8ULrwL8V9DWy8U+B9Zt49Z0S5bMTy2U0VwYGcKd1reRBdsgXD28oYLztr+8H4G/tO/DX9rD4NWvxm+EF+8mkKFgv9LbC3Gk3SKN1rcQx8KUBBVhlHQq6EoytXyF/wAFf/8Agl8v7X3hJPj58B7WG2+Lnhe18uFeI49e0+MlzplyxwokUlms5m/1UhKE+XI9fy1/sJ/Gn9qzwH+09oOkfsXWdxcfETWp30u68OXqSQ2s8NpIVvIddhYBre3sWLebMyia1f5Y8ySeRN+t8U5dheL8l/ieyq00/Radf7rtofskcyoZphFiW1GdPo9v+Be2j6H9yWn6hJqLebBHstxwGbqx9h0AFaAuYt7qD/qsbvb2/KvSNc+H2qz6RHc6MILa+Ma+bChJhD4+byiQCAD93IHHYV51b+FNUluYvCsMMiPKf30jKRtT+JyenPav4PzDh3HYSqqMoXvs1s+yR5WGzPD1oc8Xa3TsepfCDQLLTdBn1uG3SGbVZjPIyqFLgfIhbAGeBXrVVrO1gsbSOytl2xxKEUegUYFWa/ecmy2OEwtPDR+yvx6/ifm2PxTr1pVX1/pfgFFFFemcZ//T/v4oopOlAH5M/wDBZL/gpvaf8Esf2Urf43aZo9t4k8Ta5rVpomiaTdSywQzyyB57l5ZII5HjjgtIZpSwU5YKnVgK+Q/+CHX/AAXNuP8AgrD4j8d/Dvx/4X0vwj4j8KWljqllBpV9Lew3mn3Mk1vK4aeOFw8E0ShwE27ZY8HOQPxT/wCC4WvXf/BUb/guB8IP+CXnhWc3PhzwhPbafraLJIiibUlj1PXZMrxuttGt44UdRlZLopuQk1i/tR29p/wRy/4OU/CHx60GIaR8Nfiy1m9zHBHHFbJY655Oi6pFnhQlnfwWF8wG0hWON3SgD+rb/grf+3b4u/4Jx/sX6r+1F4I8PWfii/0/VdJ05NPv55LaBxqV5HaFjLCkjrs8zdwh6YxX8vPwi/4Lm/t1XUWtft1/Bv8AYL0u7sPGEKjV/HGgx6pM2pQac5gPnXVrpUk8q27IUZmQqmz5uF4/Zj/g6IYH/gkb4jYc/wDFT+FP/Txb1/P7/wAEvv8Agu78V/2Iv+CYvhX9nr4ffsy+N/HcvhmHV3t/FUVtdDw5MbnULq7LvNa2dy/lweb5cgQH50YZXqKjNpNJjUmtj+on/gj/AP8ABZn4Pf8ABWXwPrn9g6DceDfGvhKO1l1jRJ51vIDbXm9YLyxvEVBcW0jxSJ80cUqMvzxqGQt82/8ABW3/AIOFvg1/wTn8eH9nX4T+HD8TPikiQte2CXX2bT9JNwEa3ivZoo553upkdWitLeF5MNGZDEJYt/5c/wDBo58KPh/s+MP7Wknjjw/qvjLxHDb20/hLR2K3ekWbXV1qBuLyI4VBdzylbZIPMijhiUedI7MqfEP/AAbR+ENH/bp/4K1fEr9sT47QjU9b0Gz1HxbZW96DJJFq2u6rNAk7q5I8ywtka3i4/dbtq42JiRH163/BzH/wVV+Bk1p8QP2v/wBk/wDsLwLfXEaQ3bW2u6K0iSNgLFdahaPb+a3SKO48jzGwBgHNf1nfsJ/tyfAj/god+znpP7Sn7Pt5LLpN+8lrd2d0qpeadf2523Fldxozqs0TY5RmjdSskbNGysfefjH8H/h58ffhV4g+CnxY0yHWPDfiiwn03UbO4UPHLBOhRhhgQCM5U9VYAjkV8r/sC/8ABN79l7/gm18PtU+Hf7MllqUFvr1zFeapc6rqV1qNxeXUMK26zOZ3McbeWiqRCka4AG3AGAD70ooooA//1P7+K8j+Pnxn8F/s6fBLxZ8efiLcraaF4O0m71i+lY4xBZwtKwHudu0DuSAK9crG8QeHfD/izR5/Dvimxt9S0+5AWa2uokmhkAIIDxuCrDIBwR2oA/zJ/wDgl3/wSl+Lv/Bd/wCI/wAYv2wPGvxOvfh0P7eaebVtJiW+lu9X1gvqF3ZxzC5jKxWNtJbQ5U8rsTaqpivYP+Cr3/Btd40/YP8A2SNS/at0/wCNGtfFC28P3VpZ6rZarZeW1ppmpzLaTXUM5uJ/LETyRtLldmwFmxsBH+jR4S8D+C/AOnPo/gXSLLRbSSQytBYW8dtG0hABcpEqqWIAGcZwB6Vp67oGheKNIuPD/iWyg1Cwu08ue2uY1lhkQ/wvG4KsPYjFAH8SP7a/7a1t+3D/AMGsPh742a/qMNxr+l614X8P+JJfNi2jVNG1m3tbiZmRigS4VFukJI/dSq2BXjv/AASH/wCDkL/gnv8A8E//APgm54H/AGUvi1H4j1Xxt4VGrNNb6Va2z2crXmpXV7Akd5NdRQDMcyBixVVbIPSv7lovgj8GIPDs3hCDwjoqaTczLcy2S6fbC3kmQALI0Qj2M6gABiMjAx0rGh/Zu/Z4t5Vnt/Afh2N0OVZdLswQR6ERcUAfxH/8GyvwY+M3x1/4KPfE3/gpFpnhR/CXwy1i38UJbj5vsj3HiLWIL+HTrGTYqXMVmkDGeSImJHKKh5KR+AfG3wT+0b/wbZ/8FXtV/a28M+F5PEPwZ8bXmpJBKhMFldaRq9wL2bTJ7zaYbTUbC6H+ieftjkjChM+bMYf9E6ysrPTrWOysIkghiAVI41CqoHQBRgAewqlrmgaH4m0ubQ/EdnBf2VwuyW3uY1lidfRkcFSPYigD+Mv9pj/g8H/Zo8Q/BDU/DX7GXhHxBN8StVs3tbJ9dWyhstNuph5ayEW13PLfyRkkxQ2qssrhVMkYYGv2Z/4IT/8ADyjWv2R5viH/AMFJfEF9qeq+ILxJ/DVhrFrbW2q2ejrCio+ofZ7a1YTXMu+RY5k82OHy/M2yM6J+lngf9kf9lb4ZeIT4t+HHw18LaBqpbd9s07R7K2nz6+ZFErfrX0KBigBaKKKAP//V/v4ooooAKKKKACiiigAooooAKKKKACiiigD/2Q==" style="height: 16px; vertical-align: middle; margin-right: 5px;"> NrmliZer `; tabPane.id = tabId; // Asignar ID al CONTENEDOR (tabPane) // Inyectar HTML del panel tabPane.innerHTML = getSidebarHTML(); // Esperar que el DOM esté listo antes de adjuntar eventos waitForElement("#normalizeArticles", () => { console.log( `[${SCRIPT_NAME}] ✅ Sidebar DOM ready, attaching events`); attachEvents(); // Activar búsqueda para palabras especiales initSearchSpecialWords(); // Mover aquí dentro renderDictionaryWordsPanel(); // Renderizar diccionario // inicial attachDictionarySearch(); // Adjuntar búsqueda diccionario // inicial // Mover la configuración del selector aquí, después de que // el HTML se inyecta waitForElement("#dictionaryLanguageSelect", (selectorElem) => { console.log( `[${SCRIPT_NAME}] Attaching language change listener`); // El elemento ya está disponible como 'selectorElem' selectorElem.value = activeDictionaryLang; // Asegura valor inicial attachLanguageChangeListener(); // Llamar a la función // que adjunta el // listener }); }); // waitForElement("#normalizeArticles", ...) // Exponer depuración por consola unsafeWindow.debugDictionaries = function() { // Renderizar el panel del diccionario después de que el HTML // esté listo renderDictionaryWordsPanel(); attachDictionarySearch(); 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" }, // Usar el idioma activo para la API data : `language=${getApiLangCode(activeDictionaryLang)}&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: mostrarErroresOrtograficosEnPanel // Fecha modificación: 2025-05-03 // Hora: 16:33 // Autor: mincho77 // Entradas: // - errores (Array): Lista de errores con palabra, sugerencia, tipo y // severidad Salidas: Ninguna Descripción: Muestra los errores ortográficos // en el panel flotante, incluyendo sugerencias desde API o reglas locales //******************************************************************************************************************************** function mostrarErroresOrtograficosEnPanel(errores) { if (!Array.isArray(errores) || errores.length === 0) return; errores.forEach((error) => { const row = document.querySelector( `.normalizer-row[data-original="${error.palabra}"]`); if (row) { const warningIcon = row.querySelector(".warning-icon"); const suggestionsDropdown = row.querySelector(".suggestions-dropdown"); if (warningIcon) { warningIcon.classList.remove("hidden"); warningIcon.title = `Error (${error.tipo}) detectado: ${error.palabra}`; } if (suggestionsDropdown && error.sugerencia) { const option = document.createElement("option"); option.value = error.sugerencia; option.textContent = `🔁 ${error.sugerencia} (API)`; suggestionsDropdown.appendChild(option); } } else { console.warn( `❗ No se encontró fila para la palabra: ${error.palabra}`); } }); } //******************************************************************************************************************************** // 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 : texto, errores : [], metadata : { totalErrores : 0, apiUsada : false, tiempoProcesamiento : 0 } }; // 1. Normalización básica inicial const normalizado = await normalizePlaceName(texto, true); // const normalizado = await normalizePlaceName(texto, placeId); // <-- // Pasar placeId resultadoBase.normalizado = normalizado; // <-- Usar el resultado de normalizePlaceName // 2. Revisión por palabras especiales antes de hacer cualquier otra // cosa const originalWords = texto.split(/\s+/); const especialesLower = wordLists.excludeWords.map( w => w.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase()); for (let i = 0; i < originalWords.length; i++) { const palabraOriginal = originalWords[i]; const palabraNormalizada = palabraOriginal.normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase(); const index = especialesLower.findIndex(w => w === palabraNormalizada); if (index !== -1) { resultadoBase.normalizado = resultadoBase.normalizado.replace( new RegExp(`\\b${palabraOriginal}\\b`, "g"), wordLists.excludeWords[index]); } } // 3. Detección de errores locales (tilde y ortografía) if (config.reglasLocales !== false) { const erroresLocales = detectarErroresLocales( texto, resultadoBase.normalizado); // <-- Pasar original y normalizado resultadoBase.errores.push(...erroresLocales); const palabrasNoValidas = erroresLocales.filter(e => e.tipo === "tilde" && e.sugerencia && e.sugerencia.length > 0); if (palabrasNoValidas.length > 0 && typeof mostrarErroresOrtograficosEnPanel === "function") { mostrarErroresOrtograficosEnPanel(palabrasNoValidas); } } // 4. Revisión con API de LanguageTool, pero sin aplicar cambios // automáticos // *** Lógica condicional revisada *** let apiResult = { errores : [], apiStatus : 'skipped' }; // Inicializar if (config.usarAPI === true && activeDictionaryLang !== 'EN' && resultadoBase.normalizado.length > 1) { // <-- Usar === true y verificar idioma EN y longitud console.log(`[evaluarOrtografiaCompleta] Llamando API para idioma ${ activeDictionaryLang}`); // Log para confirmar try { // Llamar a la API con el texto YA NORMALIZADO apiResult = await revisarConLanguageTool( resultadoBase.normalizado, config.timeout); resultadoBase.metadata.apiUsada = true; } catch (error) { console.error("Error llamando a revisarConLanguageTool:", error); apiResult = { errores : [], apiStatus : 'error' }; // Marcar como error si la llamada falla } } else { // Log detallado de por qué se omitió la API if (config.usarAPI !== true) { console.log( `[evaluarOrtografiaCompleta] API omitida (checkbox desmarcado).`); } else if (activeDictionaryLang === 'EN') { console.log( `[evaluarOrtografiaCompleta] API omitida (idioma EN).`); } else if (resultadoBase.normalizado.length <= 1) { console.log( `[evaluarOrtografiaCompleta] API omitida (texto normalizado muy corto).`); } } // Añadir errores de API (si los hubo) a la lista principal apiResult.errores.forEach(e => { // Asegura que los errores de API tengan los campos necesarios e.origen = 'API'; e.tipo = e.tipo || 'ortografia'; // Default si no viene de la API e.severidad = e.severidad || 'media'; // Default }); resultadoBase.errores.push(...apiResult.errores); // (Opcional) Mostrar errores combinados en el panel si la función // existe if (typeof mostrarErroresOrtograficosEnPanel === "function") { // mostrarErroresOrtograficosEnPanel(resultadoBase.errores); // } // 5. Filtrado final 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 = []; // Array para almacenar los errores encontrados const palabrasOriginal = original.split(/\s+/); // let palabrasNormalizadas = [...palabrasOriginal]; // Esta variable no // se usa para construir el resultado de errores // Itera sobre cada palabra original palabrasOriginal.forEach((palabra, i) => { const contieneNumeros = /\d/.test(palabra); // Limpiar puntuación final común ANTES de cualquier validación const palabraLimpia = palabra.replace(/[.,;:!?()"']+$/, ''); // Añadir más puntuación const contieneSoloLetras = /^[a-zA-ZáéíóúÁÉÍÓÚüÜñÑ]+$/.test(palabraLimpia); // Usar palabraLimpia aquí también // Aplicar regla solo si no tiene números, son letras puras y no // está vacía if (!contieneNumeros && contieneSoloLetras && palabraLimpia.length > 0) // Añadir check de longitud > 0 { let errorAgregado = false; // Flag para prevenir múltiples errores para la misma palabra // 1. Verificar Exclusiones y Palabras Especiales PRIMERO const lowerPalabraLimpiaNormalizada = normalizarPalabra(palabraLimpia); const isExcluded = excludeWords.some(excluded => normalizarPalabra(excluded) === lowerPalabraLimpiaNormalizada); if (isExcluded) return; // Saltar palabra excluida const palabraEspecialCorrecta = esPalabraEspecial(palabraLimpia); // Devuelve la forma correcta o null if (palabraEspecialCorrecta) // <-- Corregido: usar palabraEspecialCorrecta { if (palabraLimpia !== palabraEspecialCorrecta) { errores.push({ palabraOriginal: palabra, palabraProblematica: palabraLimpia, sugerida: palabraEspecialCorrecta, tipo: "especial_incorrecta", severidad: "media", motivo: `Forma incorrecta de palabra especial. Usar: "${palabraEspecialCorrecta}"`, origen: 'Local (Especial)' }); errorAgregado = true; } return; // Si es especial (correcta o no), no hacer más checks locales } // 2. Verificar Diccionario (Coincidencia EXACTA) const estaEnDiccionarioExacto = dictionaryWords.includes(palabraLimpia); if (estaEnDiccionarioExacto) { console.log(`[DEBUG] "${ palabraLimpia}" está en diccionario. Verificando ambigüedad...`); // LOG AMBIGÜEDAD 1 // *** NUEVO: Lógica de ambigüedad de acento *** const tieneTildeActual = /[áéíóúÁÉÍÓÚ]/.test(palabraLimpia); if (!tieneTildeActual) { // La palabra SIN tilde está en el diccionario. // ¿Existe una versión CON tilde en el diccionario? const palabraConTildePotencial = generarSugerenciaTilde(palabraLimpia); // Intentar generar versión con tilde if (palabraConTildePotencial !== palabraLimpia) { // Si se pudo generar una versión diferente (con tilde) console.log(`[DEBUG] Posible versión con tilde: "${ palabraConTildePotencial}"`); // LOG AMBIGÜEDAD 2 // Verificar si la palabra con tilde está en el diccionario // const tildeNormalizadaDic = normalizarPalabra(palabraConTildePotencial); // <-- Ya no se usa normalizado aquí // const tildeEnDiccionario = dictionaryWords.some(dictWord => normalizarPalabra(dictWord) === tildeNormalizadaDic); // <-- Ya no se usa normalizado aquí const tildeEnDiccionario = dictionaryWords.includes(palabraConTildePotencial); // <-- Check EXACTO if (tildeEnDiccionario) { // ¡Ambigüedad! Ambas formas (con y sin tilde) están en el diccionario. console.log(`[detectarErroresLocales] Ambigüedad detectada para "${palabraLimpia}" / "${palabraConTildePotencial}" (ambas en diccionario).`); console.log( `[DEBUG] Pushing error tilde_ambigua`); // LOG // AMBIGÜEDAD // 3 errores.push({ palabraOriginal: palabra, // <-- Usar palabraOriginal sugerencia: palabraConTildePotencial, // Sugerir la versión con tilde por defecto tipo: "tilde_ambigua", severidad: "baja", // Es una advertencia más que un error claro motivo: `Ambigüedad: "${palabraLimpia}" y "${palabraConTildePotencial}" están en diccionario. Verificar contexto.`, // Motivo más claro palabraOriginal: palabra, palabraProblematica: palabraLimpia, origen: 'Local (Diccionario)' }); errorAgregado = true; // Marcar que se agregó un error (ambigüedad) } console.log(`[DEBUG] Versión con tilde "${ palabraConTildePotencial}" NO encontrada en diccionario.`); // LOG AMBIGÜEDAD 4 } console.log( `[DEBUG] No se pudo generar versión con tilde o es igual a la original.`); // LOG AMBIGÜEDAD 5 } // Si no hay ambigüedad o la palabra ya tenía tilde, simplemente omitir revisión local. console.log(`[detectarErroresLocales] Palabra "${palabraLimpia}" encontrada en diccionario (sin ambigüedad). Omitiendo revisión local.`); console.log(`[DEBUG] Omitiendo revisión local para "${ palabraLimpia}" (en diccionario, sin ambigüedad).`); // LOG // AMBIGÜEDAD // 6 if (!errorAgregado) // Si no se reportó ambigüedad, omitir más revisiones return; // Palabra en diccionario, sin ambigüedad detectada, omitir otras revisiones. } // 3. Si NO está en el diccionario EXACTO, aplicar reglas de Tilde y Ortografía else if (!errorAgregado) { // Asegura que solo se ejecute si no está en diccionario exacto y no hay error previo } const tildeData = detectarTilde(palabraLimpia); // <-- Usar palabraLimpia para detectar tilde console.log( `[DEBUG] "${ palabraLimpia}" NO está en diccionario. Resultado detectarTilde:`, tildeData); // LOG ORTO 1 // 3.1 Caso: `detectarTilde` dice VÁLIDA pero SIN TILDE -> ¿Está la versión CON TILDE en el diccionario? if (!errorAgregado && tildeData.esValida && !tildeData.tieneTilde) { const sugerenciaConTilde = generarSugerenciaTilde(palabraLimpia); if (sugerenciaConTilde !== palabraLimpia) { // Verificar si la versión CON tilde SÍ está en el diccionario const tildeEnDiccionario = dictionaryWords.includes(sugerenciaConTilde); if (tildeEnDiccionario) { // ¡Caso "drogueria" encontrado! console.log(`[DEBUG] Pushing error tilde_faltante_dic para "${palabraLimpia}" -> "${sugerenciaConTilde}"`); errores.push({ palabraOriginal: palabra, palabraProblematica: palabraLimpia, sugerida: sugerenciaConTilde, tipo: "tilde_faltante_dic", // Tipo específico severidad: "alta", motivo: `Posible falta de tilde. Forma correcta "${sugerenciaConTilde}" encontrada en diccionario.`, origen: 'Local (Diccionario/Tilde)' }); errorAgregado = true; // Marcar que se agregó un error } } } // 3.2 Caso: `detectarTilde` dice INVÁLIDA (y no se agregó error antes) // (La condición original `!tildeData.esValida && tildeData.tieneTilde` // podría ser la que causa problemas con // palabras como "Portón" si el 'tipo' fue mal clasificado como // grave pero tenía tilde) else if (!errorAgregado && !tildeData // <-- Cambiado a else if .esValida /*&& tildeData.tieneTilde*/) // Podrías probar // a eliminar la // // <-- Usar // palabraLimpia // segunda parte // de la condición { // <-- Este bloque ahora solo se ejecuta si !tildeData.esValida Y !errorAgregado console.log(`❌ Palabra inválida (detectada localmente): "${ palabra}" - Tipo detectado: ${ tildeData.tipo}, Es válida según regla local: ${ tildeData.esValida}`); // Aquí se decide si añadir el error. // Quizás necesites refinar esta condición: // Añadir el error si es inválida localmente Y NO está en la // lista de excluidas // const lowerExcluidas = // <-- Ya no es necesario, se verifica antes // excludeWords.map(w => w.toLowerCase()); // console.log(`[DEBUG] Verificando exclusión para "${ // palabraLimpia}"...`); // LOG ORTO 2 // if (!lowerExcluidas.includes(palabraLimpia.toLowerCase())) // <-- Ya no es necesario { // Generar sugerencia usando la palabra limpia const sugerida = generarSugerenciaTilde(palabraLimpia); // El bloque comentado anterior sobre corregirTildeLocal fue eliminado por sintaxis incorrecta. // *** NUEVO: Lógica mejorada para buscar corrección // ortográfica *** const sugerenciaTildeNormalizada = normalizarPalabra(sugerida); // <-- Cambiado de sugerenciaTilde a sugerida const sugerenciaTildeEnDiccionario = dictionaryWords.some(dictWord => normalizarPalabra(dictWord) === sugerenciaTildeNormalizada); let posibleCorreccionOrtografica = null; console.log( `[DEBUG] Buscando corrección ortográfica para "${ palabraLimpia}" en diccionario...`); // LOG ORTO 3 // Si la sugerencia de tilde NO está en el diccionario, // buscar una corrección ortográfica más probable. O // incluso si está, podríamos buscar una mejor // corrección ortográfica. Vamos a buscar siempre que // haya un error de tilde detectado. const palabraLimpiaNormalizada = normalizarPalabra(palabraLimpia); // Buscar palabra en diccionario con base similar (ej. // Levenshtein 1 o reemplazo s/z/c/b/v) Simplificación: // Buscar misma longitud y diferencia en s/z/c/b/v for (const dictWord of dictionaryWords) { const dictWordNormalizada = normalizarPalabra(dictWord); if (dictWordNormalizada.length === palabraLimpiaNormalizada.length) { let diffCount = 0; let diffIndex = -1; for (let k = 0; k < dictWordNormalizada.length; k++) { if (dictWordNormalizada[k] !== palabraLimpiaNormalizada[k]) { diffCount++; diffIndex = k; } } // Si difiere en 1 caracter y es una sustitución // común (simplificado) if (diffCount === 1) { const char1 = palabraLimpiaNormalizada[diffIndex]; const char2 = dictWordNormalizada[diffIndex]; console.log(`[DEBUG] Comparando "${ palabraLimpiaNormalizada}" vs "${ dictWordNormalizada}" (diff: ${ diffCount})`); // LOG ORTO 4 const commonSubs = [ [ 's', 'z' ], [ 'z', 's' ], [ 's', 'c' ], [ 'c', 's' ], [ 'b', 'v' ], [ 'v', 'b' ] ]; if (commonSubs.some(pair => pair[0] === char1 && pair[1] === char2)) { console.log( `[DEBUG] Encontrada corrección ortográfica: "${ dictWord}"`); // LOG ORTO 5 posibleCorreccionOrtografica = dictWord; // Encontramos corrección // ortográfica break; } } } } // Decidir qué error reportar console.log(`[DEBUG] Resultado búsqueda ortográfica: ${ posibleCorreccionOrtografica}`); // LOG ORTO 6 // Si encontramos una corrección ortográfica válida, // reportar eso if (posibleCorreccionOrtografica) { console.log( `[DEBUG] Pushing error ortografia_dic`); // LOG // ORTO 7 // Priorizar error ortográfico encontrado en // diccionario errores.push({ palabra : palabra, sugerencia : posibleCorreccionOrtografica, tipo : "ortografia_dic", severidad : "alta", motivo : `Posible error ortográfico. Encontrado en diccionario: "${ posibleCorreccionOrtografica}"`, palabraOriginal : palabra, palabraProblematica : palabraLimpia, origen : 'Local (Diccionario)' }); // <-- Aquí debería ser origen 'Local (Diccionario/Orto)'? errorAgregado = true; // Marcar que se agregó un error } else { console.log( `[DEBUG] Pushing error tilde (detectarTilde inválido)`); // LOG ORTO 8 // Si no, reportar el error de tilde original errores.push({ palabra : palabra, sugerencia : sugerida, // <-- Cambiado de sugerenciaTilde a sugerida tipo : "tilde", severidad : "media", motivo : `Tipo detectado: ${ tildeData.tipo}, Válida localmente: ${ tildeData.esValida}`, palabraOriginal : palabra, palabraProblematica : palabraLimpia, origen : 'Local' }); // <-- Aquí debería ser origen 'Local (Tilde)'? errorAgregado = true; // Marcar que se agregó un error } } // console.log(`[DEBUG] Palabra "${ // <-- Ya no es necesario // palabraLimpia}" está excluida o es especial.`); // LOG ORTO 9 } } }); 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" }, // Usar el idioma activo para la API data : `language=${getApiLangCode(activeDictionaryLang)}&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 palabraProblematica = // <-- Renombrar // para claridad match?.context?.text?.substring( match?.context?.offset || 0, (match?.context?.offset || 0) + (match?.context?.length || 0)) || "(sin contexto)"; // Obtener la palabra original del contexto si // es posible const palabraOriginal = match?.context?.text?.substring( match?.context?.offset || 0, (match?.context?.offset || 0) + (match?.context?.length || 0)) || palabraProblematica; // Fallback a la // problemática 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"; const motivo = match?.message || `Regla API: ${ match?.rule ?.id}`; // <-- Mensaje de la regla return { palabraOriginal, // <-- Palabra original del // contexto palabraProblematica, // <-- Palabra que // disparó la regla sugerencia, tipo, severidad, motivo, // <-- Mensaje de la regla origen : 'API' // <-- Indicar origen }; }); 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 = []; // Array para almacenar errores únicos const vistas = new Set(); errores.forEach((error) => { // Usar palabraProblematica para la clave de unicidad const clave = `${error.palabraProblematica}-${error.sugerencia}-${error.tipo}`; if (!vistas.has(clave)) // Si la clave no está en el Set, es un error único { vistas.add(clave); unicos.push(error); } }); return unicos.sort((a, b) => { if (a.severidad === b.severidad) // Ordenar por severidad (alta primero) 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; } function esGrave(palabra) { if (!palabra || typeof palabra !== "string") { return false; } const silabas = separarSilabas(palabra); const numSilabas = silabas.length; const tieneTilde = /[áéíóú]/.test(palabra); let silabaTonicaIndex = -1; if (tieneTilde) { for (let i = 0; i < silabas.length; i++) { if (/[áéíóú]/.test(silabas[i])) { silabaTonicaIndex = i; break; } } } // Si tiene tilde, verificar si está en la penúltima sílaba if (tieneTilde && silabaTonicaIndex === numSilabas - 2) { return true; // Es grave con tilde correcta } // Si NO tiene tilde, verificar si DEBERÍA ser grave if (!tieneTilde && numSilabas > 1 && !/[ns]$/.test(palabra.slice(-1).toLowerCase()) && /[aeiou]$/.test(palabra.slice(-1).toLowerCase())) { return true; // Debería ser grave (sin tilde) } return false; } function esAguda(palabra) { if (!palabra || typeof palabra !== "string") { return false; } const silabas = separarSilabas(palabra); // **NECESITA IMPLEMENTARSE** const numSilabas = silabas.length; const tieneTilde = /[áéíóú]/.test(palabra); let silabaTonicaIndex = -1; if (tieneTilde) { for (let i = 0; i < silabas.length; i++) { if (/[áéíóú]/.test(silabas[i])) { silabaTonicaIndex = i; break; } } } // Si tiene tilde, verificar si está en la última sílaba if (tieneTilde && silabaTonicaIndex === numSilabas - 1) { return true; // Es aguda con tilde correcta } // Si NO tiene tilde, verificar si DEBERÍA ser aguda if (!tieneTilde && numSilabas > 1 && /[ns]$/.test(palabra.slice(-1).toLowerCase()) && /[aeiou]$/.test(palabra.slice(-1).toLowerCase())) { return true; // Debería ser aguda (sin tilde) } return false; } //--------------------------------------------------- function tipoVocal(vocal) { const debiles = "iu"; const fuertes = "aeoáéó"; if (debiles.includes(vocal.toLowerCase())) { return "débil"; } if (fuertes.includes(vocal.toLowerCase())) { return "fuerte"; } return null; // No es una vocal } function agruparVocales(silaba) { let nuevaSilaba = ""; let i = 0; while (i < silaba.length) { const char = silaba[i]; const siguiente = silaba[i + 1]; const siguiente2 = silaba[i + 2]; const tipo1 = tipoVocal(char); if (tipo1) { if (siguiente && tipoVocal(siguiente)) { const tipo2 = tipoVocal(siguiente); if (tipo1 === "fuerte" && tipo2 === "fuerte") { nuevaSilaba += char; // Hiato: se separan i++; continue; } else if (tipo1 === "fuerte" && tipo2 === "débil") { if (siguiente2 && tipoVocal(siguiente2) === "débil") { nuevaSilaba += char + siguiente + siguiente2; // Triptongo i += 3; continue; } nuevaSilaba += char + siguiente; // Diptongo creciente i += 2; continue; } else if (tipo1 === "débil" && tipo2 === "fuerte") { nuevaSilaba += char + siguiente; // Diptongo decreciente i += 2; continue; } else if (tipo1 === "débil" && tipo2 === "débil") { nuevaSilaba += char + siguiente; // Diptongo i += 2; continue; } } nuevaSilaba += char; i++; } else { nuevaSilaba += char; i++; } } return nuevaSilaba; } function separarSilabas(palabra) { if (!palabra || typeof palabra !== "string") { return []; } palabra = palabra.toLowerCase(); let silabas = []; let buffer = ""; for (let i = 0; i < palabra.length; i++) { buffer += palabra[i]; if (esSeparable(palabra, i)) { silabas.push(agruparVocales(buffer)); buffer = ""; } } silabas.push(agruparVocales(buffer)); // Añadir la última sílaba return silabas; } function esSeparable(palabra, indice) { if (indice === palabra.length - 1) { return false; // No separar al final } const char = palabra[indice]; const siguiente = palabra[indice + 1]; if (!tipoVocal(char) && tipoVocal(siguiente)) { return true; // Consonante seguida de vocal } if (tipoVocal(char) && tipoVocal(siguiente)) { return tipoVocal(char) === "fuerte" && tipoVocal(siguiente) === "fuerte"; // Hiato } if (!tipoVocal(char) && !tipoVocal(siguiente)) { // Dos consonantes seguidas if (indice > 0 && tipoVocal(palabra[indice - 1])) { if ([ "l", "r" ].includes(siguiente) && !["b", "c", "d", "f", "g", "k", "p", "t"].includes(char)) { return false; // No separar "bl", "cl", "dr", etc. } return true; } } return false; } ///----------------------------------------- //******************************************************************************************************************************** // 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: esHiatoObligatorio // Fecha modificación: 2025-05-03 23:12 // Autor: mincho77 // Entradas: palabra (string) // Salidas: booleano (true si se considera que debería llevar tilde por // hiato obligatorio) Descripción: Evalúa si una palabra debería llevar // tilde por formar hiato acentual aunque no la tenga escrita. //******************************************************************************************************************************** function esHiatoObligatorio(palabra) { const hiatosConocidos = [ "vehiculo", "vehiculos", "rio", "tio", "mia", "mio", "frio", "envio", "tardia", "estaria", "baul", "continua", "confia", "rubi", "paul" ]; const palabraSinTilde = palabra.normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase(); return hiatosConocidos.includes(palabraSinTilde); } //******************************************************************************************************************************** //******************************************************************************************************************************** // 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 = S('spinnerCheckingMessage'), progress = null) // Usar S() para mensaje por defecto { let existingSpinner = document.querySelector(".spinner-overlay"); if (existingSpinner) { if (show) { // Actualiza el mensaje y el progreso si el spinner ya existe const spinnerMessage = existingSpinner.querySelector(".spinner-message"); spinnerMessage.innerHTML = ` ${message} ${ progress !== null ? `<br><strong>${progress}${ S('spinnerProgressMessage')}</strong>` // Usar S() para // parte del // progreso : ""} `; } 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}${ S('spinnerProgressMessage')}</strong>` // Usar S() para // parte del progreso : ""} </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: 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, "'"); } //******************************************************************************************************************************** // Nombre: escapeRegExp // Descripción: Escapa caracteres especiales para usar en new RegExp(). //******************************************************************************************************************************** function escapeRegExp(string) { // $& significa la cadena completa coincidente return string.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: 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) { // Cierra panel previo si existe const existingPanel = document.getElementById("normalizer-floating-panel"); if (existingPanel) { existingPanel.remove(); } 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: 0; /* Quitar padding principal para controlar scroll interno */ 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; vertical-align: top; /* Alinear contenido arriba */ } .warning-row { background: #fff8e1; } .normalize-btn, .apply-btn, .add-exclude-btn, .add-special-btn, .view-place-btn { /* Añadido .view-place-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; } /* Estilo modificado para el botón Ver/Link */ .view-place-btn { background: none; /* Sin fondo */ color: #333; /* Color oscuro para el icono */ padding: 2px; /* Padding reducido */ font-size: 16px; /* Ajustar tamaño si es necesario */ } .add-special-btn { background: #9b59b6; color: white; } /* Estilo para el botón de cerrar */ .close-btn { position: absolute; /* Posición absoluta relativa al header sticky */ top: 15px; right: 15px; background: #e74c3c; color: white; border: none; width: 30px; height: 30px; border-radius: 50%; font-weight: bold; cursor: pointer; z-index: 11; /* Encima de todo */ } input[type="checkbox"] { transform: scale(1.3); margin: 0 5px; } input[type="text"] { width: 100%; padding: 5px; box-sizing: border-box; } /* Estilos para sticky header */ .panel-header-sticky { position: sticky; top: 0; background-color: white; padding: 15px 20px 10px 20px; /* Padding para el header */ z-index: 10; border-bottom: 1px solid #ccc; /* Línea divisoria */ } #normalizer-table th { position: sticky; top: 75px; /* Ajusta esto según la altura del header */ z-index: 5; /* Debajo del header principal pero encima del contenido */ } </style> <style> /* Estilos adicionales para <details> */ details > summary { list-style: none; cursor: pointer; } details > summary::-webkit-details-marker { display: none; } /* Ocultar marcador por defecto en Chrome/Safari */ .error-summary-arrow { display: inline-block; transition: transform 0.2s; margin-right: 5px; } details[open] .error-summary-arrow { transform: rotate(90deg); } </style> <!-- Contenedor para header pegajoso --> <div class="panel-header-sticky"> <button class="close-btn" id="close-panel-btn">×</button> <h2 style="color: #2c3e50; margin-top: 0; margin-bottom: 5px;">Normalizador de Nombres</h2> <div style="margin: 0; color: #7f8c8d;"> <span id="places-count">${placesToNormalize.length} ${ S('placesToReview')}</span> | <span id="ready-count-display" style="color: #27ae60; font-weight: bold;">0 ${ S('placesReadyToNormalize')}</span> <!-- Nuevo contador --> </div> </div> <div style="padding: 0 20px 20px 20px;"> <!-- Contenedor para el resto del contenido con padding --> <table id="normalizer-table"> <thead> <!-- Usar helper S() para cabeceras --> <tr> <th width="5%">${S('applyCol')}</th> <!-- 1 --> <th width="5%">${S('deleteCol')}</th> <!-- 2 --> <th width="5%">${S('permaCol')}</th> <!-- 3 - Perma --> <th width="8%">${ S('categoryCol')}</th> <!-- Nueva Columna Categoría --> <th width="5%">${ S('typeCol')}</th> <!-- 4 - Tipo Place --> <th width="20%">${ S('currentNameCol')}</th> <!-- 5 --> <th width="25%">${S('normalizedNameCol')}</th> <!-- 6 --> <th width="15%">${S('problemCol')}</th> <!-- 7 --> <th width="10%">${S('actionsCol')}</th> <!-- 8 --> </tr> </thead> <tbody>`; placesToNormalize.forEach((place, index) => { const { originalName, newName, hasSpellingWarning, spellingWarnings = [], // This now contains objects from // detectarTilde where esValida is false place : venue } = place; const placeId = venue.getID(); // Obtener la primera categoría o un guion si no hay const category = venue.attributes.categories?.[0] || '-'; // Determinar tipo de lugar (Área o Punto) // --- Lógica robusta para obtener tipo de geometría --- const geometry = venue.getOLGeometry(); let geometryType = null; if (geometry) { if (typeof geometry.getType === 'function') { geometryType = geometry.getType(); // Método estándar OL3+ } else if (geometry.CLASS_NAME) { // Fallback para versiones anteriores/diferentes (ej: // "OpenLayers.Geometry.Polygon") geometryType = geometry.CLASS_NAME.split('.').pop(); } } const isArea = geometryType === 'Polygon'; const placeTypeIcon = isArea ? '⭔' : '⊙'; // Usar S() para obtener el tooltip correcto según el idioma y tipo const placeTypeTitle = isArea ? S('placeTypeAreaTooltip') : S('placeTypePointTooltip'); 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> <!-- 3 - Celda para el botón Perma/Ver --> <button class="view-place-btn" data-index="${ index}" title="Ver detalles del lugar">🔗</button> </td> <td>${escapeHtml(category)}</td> <!-- Nueva Celda para Categoría --> </td> <td> <!-- 4 - Celda para el icono de tipo con tooltip --> <span title="${placeTypeTitle}">${placeTypeIcon}</span> </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 ? S('normalizationLabel') : ""}${ spellingWarnings.length > 0 ? ` / ${spellingWarnings.length} Errores` : ""}</td> <td> <button class="normalize-btn" data-index="${ index}">NrmliZer</button> <button class="add-exclude-btn" data-word="${ escapeHtml( originalName)}" data-index="${index}">ExcludeWrd</button> </td> </tr>`; // Si hay errores, añadir la fila desplegable if (spellingWarnings.length > 0) { html += ` <tr> <td colspan="9" style="padding-top: 0; padding-bottom: 5px;"> <!-- Ajustado colspan a 9 --> <details> <summary style="color: red;"> <span class="error-summary-arrow">▶</span> ${ // Usar helper S() spellingWarnings.length} error(es) encontrado(s) </summary> <ul style="margin-top: 5px; padding-left: 20px;"> ${ spellingWarnings .map((warning, warningIndex) => ` <li style="margin-bottom: 5px;"> ${S('errorInLabel')} "${ escapeHtml(warning.palabraProblematica || warning.palabraOriginal)}" (${ escapeHtml(warning.motivo || warning.tipo || 'Ortografía')})<br> ${S('suggestionLabel')} "<strong>${ escapeHtml(warning.sugerida)}</strong>" <button class="btn apply-suggestion-btn" data-index="${index}" data-problem-word="${ escapeHtml(warning.palabraProblematica || warning.palabraOriginal)}" data-suggestion="${ escapeHtml(warning.sugerida)}" style="margin-left: 10px; background-color: #27ae60; color: white; padding: 2px 6px; font-size: 0.9em;"> ${S('useSuggestionButtonLabel')} </button> </li> `) .join('')} </ul> </details> </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;">${ S('applySelectedButton')}</button> <button id="cancel-btn" style="background: #e74c3c; color: white; padding: 10px 20px; border: none; border-radius: 4px; margin-left: 10px; font-weight: bold;">${ S('cancelButton')}</button> </div> </div>`; panel.innerHTML = html; document.body.appendChild(panel); // --- Función para actualizar el contador de listos --- function updateReadyCount() { const readyCount = panel .querySelectorAll( '.normalize-checkbox[data-type="full"]:checked') .length; const countDisplay = panel.querySelector('#ready-count-display'); if (countDisplay) { countDisplay.innerHTML = `${readyCount} ${S('placesReadyToNormalize')}`; } } // --- Fin función contador --- // Llamada inicial para establecer el contador en 0 updateReadyCount(); // --- Añadir listener directo a checkboxes de aplicar --- panel.querySelectorAll('.normalize-checkbox[data-type="full"]') .forEach(checkbox => { checkbox.addEventListener( 'change', updateReadyCount); // Llamar a update en cualquier cambio }); // --- Fin listener checkboxes --- // --- Añadir listener para el botón de cerrar (X) --- const closePanelBtn = panel.querySelector("#close-panel-btn"); if (closePanelBtn) { closePanelBtn.addEventListener("click", () => panel.remove()); } else { console.error("Error: No se encontró el botón #close-panel-btn"); } // --- Fin listener botón cerrar --- //******** // Eventos dinámicos para cada lugar panel.querySelectorAll('.normalize-btn').forEach(btn => { btn.addEventListener("click", function() { const row = this.closest("tr"); const input = row.querySelector(".new-name-input"); const checkbox = row.querySelector(".normalize-checkbox"); if (input && checkbox) { checkbox.checked = true; this.textContent = "Listo"; } }); }); panel.querySelectorAll('.add-special-btn').forEach(btn => { btn.addEventListener("click", function() { const word = this.dataset.word; if (word) { addSpecialWord(word); const row = this.closest("tr"); const checkbox = row.querySelector(".normalize-checkbox"); const normalizeButton = row.querySelector(".normalize-btn"); if (checkbox) checkbox.checked = false; if (normalizeButton) normalizeButton.textContent = "NrmliZer"; } // Actualiza contador después de cambiar estado updateReadyCount(); }); }); panel.querySelectorAll('.new-name-input').forEach(input => { input.addEventListener("input", function() { const row = this.closest("tr"); const checkbox = row.querySelector(".normalize-checkbox"); const normalizeButton = row.querySelector(".normalize-btn"); // const originalNormalized = this.dataset.originalNormalized; const originalNormalized = this.dataset.original; if (checkbox && normalizeButton) { if (this.value.trim() !== originalNormalized.trim()) { checkbox.checked = true; normalizeButton.textContent = "Listo"; } else { checkbox.checked = false; normalizeButton.textContent = "NrmliZer"; } } // Actualizar contador después de cambiar estado por input updateReadyCount(); }); }); //***** // Nuevo: Botones especiales dinámicos panel.querySelectorAll('[id^="special-btn-"]').forEach(btn => { btn.addEventListener("click", (e) => { const placeId = e.target.getAttribute("data-place-id"); console.log("Click en botón especial, placeId:", placeId); //***** // Eventos dinámicos por cada lugar placesToNormalize.forEach((place, index) => { const normalizedInput = document.getElementById(`normalized-name-${index}`); const normalizeButton = document.getElementById(`normalize-btn-${index}`); if (normalizeButton) { normalizeButton.addEventListener("click", () => { if (checkbox) checkbox.checked = true; normalizeButton.textContent = "Listo"; }); } else { console.warn(`No se encontró normalize-btn-${index}`); } // const fixButton = // document.getElementById(`fix-btn-${index}`); const specialButton = document.getElementById(`special-btn-${index}`); if (specialButton) { specialButton.addEventListener("click", () => { if (place.originalName) { addSpecialWord(place.originalName); if (checkbox) checkbox.checked = false; if (normalizeButton) normalizeButton.textContent = "NrmliZer"; } }); } else { console.warn(`No se encontró special-btn-${index}`); } const checkbox = panel.querySelector( `.normalize-checkbox[data-index="${index}"]`); // Cambio manual del texto if (normalizedInput && checkbox && normalizeButton) { normalizedInput.addEventListener("input", () => { if (normalizedInput.value.trim() !== place.normalizedName.trim()) { checkbox.checked = true; normalizeButton.textContent = "Listo"; } else { checkbox.checked = false; normalizeButton.textContent = "NrmliZer"; } }); } // Botón agregar palabra especial if (specialButton) { specialButton.addEventListener("click", () => { if (place.originalName) { addSpecialWord(place.originalName); if (checkbox) checkbox.checked = false; if (normalizeButton) normalizeButton.textContent = "NrmliZer"; } }); } else { console.error(`No se encontró special-btn-${index}`); } }); //***** }); }); // Botón cancelar // const cancelBtn = document.getElementById("cancel-btn"); const cancelBtn = panel.querySelector("#cancel-btn"); if (cancelBtn) { cancelBtn.addEventListener("click", () => panel.remove()); } else { console.error("El botón 'cancel-btn' no se encontró en el DOM."); } // Botón aplicar todos const applyAllBtn = panel.querySelector("#apply-all-btn"); if (applyAllBtn) { applyAllBtn.addEventListener("click", () => { // 1. Filtrar los // lugares marcados // para aplicar const selectedChanges = placesToNormalize .filter((_, index) => { const checkbox = panel.querySelector( // Asegúrate de seleccionar solo los checkboxes // principales o todos si quieres aplicar cambios de // advertencias también `.normalize-checkbox[data-index="${ index}"][data-type="full"]`); return checkbox && checkbox.checked; }) .map( placeData => ({...placeData })); // Crear copias para no mutar el // array original directamente aquí // 2. Actualiza 'newName' con el valor actual del input ANTES // de aplicar selectedChanges.forEach((change) => { const index = placesToNormalize.findIndex( p => p.id === change.id); // Encontrar índice original por ID const inputElement = panel.querySelector(`.new-name-input[data-index="${ index}"][data-type="full"]`); if (inputElement) { change.newName = inputElement.value; // Sobrescribir newName con el // valor del input } }); if (selectedChanges.length === 0) { showModal({ title : "Advertencia", message : "No se seleccionaron lugares para aplicar cambios.", confirmText : "Aceptar", type : "warning" }); return; } // 3. Llamar a applyNormalization con los datos actualizados applyNormalization(selectedChanges); panel.remove(); }); } else { console.error("El botón 'apply-all-btn' no se encontró en el DOM."); } // 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; } // Actualizar contador después de cambiar estado por input updateReadyCount(); } }); }); // 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; // Actualizar contador si se marca eliminar updateReadyCount(); } }); }); // 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; // Actualizar contador después de normalizar updateReadyCount(); } catch (error) { console.error("Error al normalizar:", error); clearInterval(interval); this.textContent = originalText; } }); }); // Evento para los botones "Usar sugerencia" dentro de <details> panel.querySelectorAll(".apply-suggestion-btn").forEach((btn) => { btn.addEventListener("click", function() { const index = parseInt(this.dataset.index, 10); const problemWord = this.dataset.problemWord; const suggestion = this.dataset.suggestion; if (isNaN(index) || !problemWord || !suggestion) { console.error("Datos inválidos en botón de sugerencia:", this.dataset); return; } const placeData = placesToNormalize[index]; // const warning = placeData.spellingWarnings[warningIndex]; const mainInput = panel.querySelector( `.new-name-input[data-index="${index}"][data-type="full"]`); const mainCheckbox = panel.querySelector(`.normalize-checkbox[data-index="${ index}"][data-type="full"]`); // if (!placeData || !warning || !mainInput || !mainCheckbox) if (!placeData || !mainInput || !mainCheckbox) return; // Verificar si la palabra original tiene tildes incorrectas const currentValue = mainInput.value; const regex = new RegExp('\\b' + escapeRegExp(problemWord) + '\\b', 'i'); // Busca la palabra exacta (case-insensitive) const newValue = currentValue.replace(regex, suggestion); mainInput.value = newValue; placeData.newName = newValue; mainCheckbox.checked = true; // Actualizar contador después de aplicar sugerencia updateReadyCount(); this.disabled = true; this.textContent = "Corregido"; }); }); 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 } }); }); // Evento para el botón "Ver Place" (icono del ojo) panel.querySelectorAll(".view-place-btn").forEach((btn) => { btn.addEventListener("click", function() { const index = parseInt(this.dataset.index, 10); if (isNaN(index) || index < 0 || index >= placesToNormalize.length) { console.error("Índice de lugar inválido para 'Ver Place':", this.dataset.index); return; } const placeObject = placesToNormalize[index].place; if (placeObject && W && W.selectionManager) { W.selectionManager.setSelectedModels([ placeObject ]); } else { console.error("No se pudo seleccionar el lugar:", placeObject); } }); }); } //******************************************************************************************************************************** // 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: 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: openDeletePopupForDictionary // Fecha modificación: 2025-07-28 // Autor: mincho77 // Entradas: // - index (number): Índice de la palabra a eliminar en la lista // `dictionaryWords`. Salidas: Ninguna. Muestra un modal de confirmación // para eliminar del diccionario. Descripción: Muestra un modal para // confirmar la eliminación de una palabra del diccionario activo. Si el // usuario confirma, elimina la palabra de `spellDictionaries`, actualiza // `localStorage`, `dictionaryWords` y renderiza el panel. //******************************************************************************************************************************** function openDeletePopupForDictionary(index) { const wordToDelete = dictionaryWords[index]; // Obtener la palabra de la lista plana if (!wordToDelete) { console.error( `[Diccionario] No se encontró la palabra en el índice ${index}`); return; } showModal({ title : "Eliminar palabra del Diccionario", message : `¿Estás seguro de que deseas eliminar la palabra <strong>${ escapeHtml(wordToDelete)}</strong> del diccionario?`, confirmText : "Eliminar", cancelText : "Cancelar", type : "warning", onConfirm : () => { // 1. Encontrar y eliminar la palabra de la estructura principal // spellDictionaries const firstLetter = wordToDelete.charAt(0).toLowerCase(); const currentDictLang = spellDictionaries[activeDictionaryLang]; if (currentDictLang && currentDictLang[firstLetter]) { const letterArray = currentDictLang[firstLetter]; const wordIndexInLetterArray = letterArray.indexOf(wordToDelete); if (wordIndexInLetterArray > -1) { letterArray.splice(wordIndexInLetterArray, 1); // Eliminar del array de la letra // Opcional: Si el array de la letra queda vacío, // eliminar la letra if (letterArray.length === 0) { delete currentDictLang[firstLetter]; } // 2. Actualizar localStorage localStorage.setItem( `spellDictionaries_${activeDictionaryLang}`, JSON.stringify(currentDictLang)); // 3. Actualizar la lista plana global y renderizar dictionaryWords = Object.values(currentDictLang).flat().sort(); renderDictionaryWordsPanel(); showModal({ title : "Éxito", message : "La palabra fue eliminada correctamente del diccionario.", confirmText : "Aceptar", type : "success", autoClose : 2000, }); } else { console.error(`[Diccionario] No se encontró "${ wordToDelete}" en el array de la letra '${ firstLetter}'.`); } } else { console.error(`[Diccionario] No se encontró la letra '${ firstLetter}' o el diccionario para el idioma ${ activeDictionaryLang}.`); } }, }); } //******************************************************************************************************************************** // 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: addTildeToVowel // Fecha modificación: 2025-07-26 // Autor: mincho77 // Entradas: vocal (string) - Una vocal sin tilde (a, e, i, o, u) // Salidas: string - La vocal con tilde (á, é, í, ó, ú) // Descripción: Añade la tilde a una vocal. //******************************************************************************************************************************** function addTildeToVowel(vowel) { const tildes = { 'a' : 'á', 'e' : 'é', 'i' : 'í', 'o' : 'ó', 'u' : 'ú', 'A' : 'Á', 'E' : 'É', 'I' : 'Í', 'O' : 'Ó', 'U' : 'Ú' }; return tildes[vowel] || vowel; } //******************************************************************************************************************************** // Nombre: generarSugerenciaTilde // Fecha modificación: 2025-07-26 // Autor: mincho77 // Entradas: palabra (string) - La palabra con posible error de tilde. // Salidas: string - La palabra con la tilde corregida, o la original si no // se pudo corregir. Descripción: Intenta corregir la tilde de una palabra // basándose en las reglas detectadas por detectarTilde. // Prioriza añadir tildes faltantes. //******************************************************************************************************************************** function generarSugerenciaTilde(palabra) { const tildeData = detectarTilde(palabra); // No retornar si es válida, intentar corregir de todas formas por si // falta tilde const silabas = obtenerSilabasAproximadas(palabra); const totalSilabas = silabas.length; const terminaEnVocalNS = [ 'a', 'e', 'i', 'o', 'u', 'n', 's' ].includes( palabra.slice(-1) .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase()); // Intentar añadir tilde si falta if (!tildeData.tieneTilde) { let silabaIndexTarget = -1; if (tildeData.tipo === 'aguda' && terminaEnVocalNS && totalSilabas > 0) silabaIndexTarget = totalSilabas - 1; else if (tildeData.tipo === 'grave' && !terminaEnVocalNS && totalSilabas > 1) silabaIndexTarget = totalSilabas - 2; else if (tildeData.tipo === 'esdrújula' && totalSilabas > 2) silabaIndexTarget = totalSilabas - 3; else if (tildeData.tipo === 'sobresdrújula' && totalSilabas > 3) silabaIndexTarget = totalSilabas - 4; // Asumiendo la 4ta desde el final if (silabaIndexTarget >= 0 && silabaIndexTarget < silabas.length) { const silabaTarget = silabas[silabaIndexTarget]; let palabraCorregidaArray = palabra.split(''); let offset = silabas.slice(0, silabaIndexTarget).join('').length; for (let i = silabaTarget.length - 1; i >= 0; i--) { // Buscar última vocal en la sílaba const charIndex = offset + i; const char = palabraCorregidaArray[charIndex]; if ("aeiouAEIOU".includes(char)) { palabraCorregidaArray[charIndex] = addTildeToVowel(char); return palabraCorregidaArray.join(''); } } } } return palabra; // Devolver original si no se pudo corregir } //******************************************************************************************************************************** // 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 maxPlacesToScan = // Renombrar para evitar confusión con la // variable global parseInt(document.getElementById("maxPlacesInput")?.value || 100, 10); if (!W?.model?.venues?.objects) { // Verificar si el modelo de WME está disponible 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, maxPlacesToScan); // Usar la variable local if (allPlaces.length === 0) { // Si no se encontraron lugares toggleSpinner(false); showNoPlacesFoundMessage(); // Mostrar el mensaje mejorado return; } let processedCount = 0; const useAPI = document.getElementById("useSpellingAPI") ?.checked; // Verificar si se usará la API const placesToNormalize = []; const processBatch = async (index) => { if (index >= allPlaces.length) { // Si ya se procesaron todos los lugares // Ocultar spinner 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]; const originalName = place.attributes.name || ""; // Asegura que originalName sea string const placeId = place.getID(); // Obtener ID try { // *** MODIFICADO: Verificar si el lugar es editable y si la función existe *** let isEditable = true; // Asumir editable por defecto si la función no existe if (typeof place.isEditable === 'function') { isEditable = place.isEditable(); } if (!isEditable) { console.log(`[scanPlaces] Saltando lugar ${placeId} (no editable por el usuario actual).`); // Opcional: Podrías añadirlo a una lista separada o simplemente saltarlo processedCount++; // Asegúrate de incrementar el contador setTimeout(() => processBatch(index + 1), 10); // Procesar el siguiente rápido return; // Saltar el resto del procesamiento para este lugar } console.log(`[scanPlaces] Processing index ${index}, placeId ${ placeId}, name: "${originalName}"`); // Log inicio // 1. Llamar a la función centralizada de evaluación const evalConfig = { usarAPI : useAPI, // Usar el valor del checkbox reglasLocales : true, // Asumimos que siempre queremos reglas locales timeout : 30000 // Timeout para la API }; // Pasar placeId a evaluarOrtografiaCompleta para que pueda // usarlo normalizePlaceName const evalResult = await evaluarOrtografiaCompleta( originalName, placeId, evalConfig); // 2. Mapear los errores al formato esperado por el panel // flotante Los errores ya deberían tener 'palabraOriginal' y // 'palabraProblematica' const spellingWarningsForPanel = evalResult.errores.map( error => ({ palabraOriginal : error.palabraOriginal || error.palabraProblematica || '?', // Fallback palabraProblematica : error.palabraProblematica || error.palabraOriginal || '?', // Fallback sugerida : error.sugerencia, tipo : error.tipo, motivo : error.motivo || `Severidad: ${error.severidad}`, origen : error.origen || 'Desconocido' // Origen (Local o API) })); // 3. Determinar el nombre final (el 'normalizado' de la // evaluación) const finalCorrectedName = evalResult.normalizado; processedCount++; toggleSpinner( true, `${S('spinnerProcessingMessage')} (${processedCount}/${ // Usar S() aquí`Procesando lugares... (${processedCount}/${ allPlaces.length})`, // Usar allPlaces.length Math.round((processedCount / allPlaces.length) * 100)); console.log( `Lugar: ${originalName}, Errores locales detectados:`, spellingWarningsForPanel); // 4. Añadir al panel si hubo advertencias o si el nombre final // difiere del original console.log( `[scanPlaces] Checking if place ${placeId} needs review`); if (spellingWarningsForPanel.length > 0 || originalName !== finalCorrectedName) { placesToNormalize.push({ id : placeId, // Usar el ID obtenido al inicio originalName, newName : // Usar el nombre normalizado/corregido de la // evaluación finalCorrectedName, // Nombre corregido SOLO // por reglas locales // inicialmente hasSpellingWarning : spellingWarningsForPanel.length > 0, spellingWarnings : spellingWarningsForPanel, // Pasar las advertencias // detalladas place }); console.log(`[scanPlaces] Place ${ placeId} added to normalization list.`); } else { console.log( `[scanPlaces] Place ${placeId} does NOT need review.`); } setTimeout(() => processBatch(index + 1), 50); // Asegura que el contador avance y el spinner se actualice // incluso si hay error } catch (error) { console.error(`Error procesando lugar ${place.getID()} (${ originalName || 'N/A'}):`, error); // Asegura que el contador avance y el spinner se actualice processedCount++; toggleSpinner( true, `Procesando lugares... (${processedCount}/${ allPlaces.length})`, // Usar allPlaces.length Math.round((processedCount / allPlaces.length) * 100)); setTimeout(() => processBatch(index + 1), 50); } }; 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: 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: 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); } //******************************************************************************************************************************** // 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); }); } //******************************************************************************************************************************** // 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 .filter((word) => { // Si es para excluir, no mostrar si ya está excluida // (comparando en minúsculas) return !(listType === "excludeWords" && excludeWords.some((ex) => ex.toLowerCase() === word.toLowerCase())); }) .map((word, index) => ` <li> <label> <input type="checkbox" class="special-word-checkbox" data-word="${ escapeHtml(word)}" id="word-${index}"> ${escapeHtml(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. // placeId (string): ID del lugar (opcional, para caché o logs // futuros). // Salidas: Promise<string>: Promesa que resuelve con el nombre normalizado // básico. Descripción: Aplica normalización básica (mayúsculas, artículos, // exclusiones, especiales). // YA NO llama a la API de ortografía directamente para aplicar // sugerencias. //******************************************************************************************************************************** async function normalizePlaceName(name, placeId = null) // useSpellingAPI = false) { if (!name) { return ""; } // Usar la variable global 'normalizeArticles' que se actualiza con el // listener. Recordar: normalizeArticles = true significa NO normalizar // (checkbox marcado). Por lo tanto, la condición para normalizar (poner // en minúscula) debe ser cuando normalizeArticles es false. const shouldLowercaseArticle = activeDictionaryLang === 'SP' && !normalizeArticles; const articles = [ "el", "la", "los", "las", "de", "del", "al", "y", "e", "en" ]; const words = name.trim().split(/\s+/); 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) => { // 🛑 Verificar si es palabra especial y retornarla intacta const palabraEspecial = esPalabraEspecial(word); if (palabraEspecial) return palabraEspecial; const lowerWord = word.normalize("NFD").toLowerCase(); // Reglas de reemplazo... if (lowerWord === "él" || lowerWord === "el") return word; if (lowerWord === "sa" || lowerWord === "s.a") return "S.A"; if (lowerWord === "sas" || lowerWord === "s.a.s") return "S.A.S"; if (lowerWord === "[p]") return ""; if (/^\d+$/.test(word)) return word; if (isRoman(word)) return word.toUpperCase(); 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()); } // Poner artículo en minúscula solo si está permitido (checkbox // desmarcado en SP) if (shouldLowercaseArticle && articles.includes(lowerWord) && index !== 0) return lowerWord; // --- Exclude Word Check (CRITICAL) --- const matchedExcludeWord = wordLists.excludeWords.find( (w) => w.normalize("NFD").toLowerCase() === lowerWord); if (matchedExcludeWord) { console.log( `"${word}" matched exclude word: "${matchedExcludeWord}"`); return matchedExcludeWord; // Return the *original* form } // --- Main Normalization Logic --- // Ya no usamos spellingSuggestion aquí, solo capitalización // básica let normalizedWord; normalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); return normalizedWord; })); let newName = normalizedWords.join(" ") // Restaurar la cadena original de .replace() .replace(/\s*\|\s*/g, " - ") .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()); newName = newName.replace(/([A-Za-z])'([A-Za-z])/g, (match, before, after) => `${before}'${after.toLowerCase()}`); newName = newName.replace( /-\s*([a-z])/g, (match, letter) => `- ${letter.toUpperCase()}`); // *** NUEVO: Capitalizar después de punto y espacio *** 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(); // Movido arriba 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" }); // Configurar el selector de idioma DESPUÉS de que el HTML esté en // el DOM waitForElement("#dictionaryLanguageSelect", (selector) => { selector.value = activeDictionaryLang; // Establecer valor inicial // configurarCambioIdiomaDiccionario(); // Ya no se llama aquí, // se llama desde attachLanguageChangeListener attachLanguageChangeListener(); // Adjuntar el listener inicial }); attachDetailsToggleListeners(); // Adjuntar listeners de detalles // iniciales 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 // ************************************************************************** // Nombre: NormalizerUtils // Fecha modificación: 2025-04-29 // Hora: 07:50 // Autor: mincho77 // Entradas: Ninguna // Salidas: Objeto global con funciones relacionadas a ortografía y tildes // Prerrequisitos: Las funciones detectarTilde, separarSilabas y // validarReglasAcentuacion deben estar ya definidas Descripción: Define un // objeto global con utilidades de ortografía para facilitar su uso en // consola o desde otros módulos // ************************************************************************** window.NormalizerUtils = { detectarTilde, separarSilabas : obtenerSilabasAproximadas, // Usar la versión adaptada validarReglasAcentuacion // Mantener si existe }; unsafeWindow.NormalizerUtils = window.NormalizerUtils; unsafeWindow.normalizePlaceName = normalizePlaceName; unsafeWindow.applyNormalization = applyNormalization; window.addEventListener("dragover", e => e.preventDefault(), { passive : false }); // <-- Añadir passive: false window.addEventListener("drop", e => e.preventDefault(), { passive : false }); // <-- Añadir passive: false function exposeNormalizerTools() { if (typeof window.NormalizerUtils === "undefined") { window.NormalizerUtils = {}; } window.NormalizerUtils.detectarTilde = detectarTilde; window.NormalizerUtils.separarSilabas = obtenerSilabasAproximadas; // Exponer la versión adaptada window.NormalizerUtils.validarReglasAcentuacion = validarReglasAcentuacion; console.log("✅ NormalizerUtils disponible en window"); } // Siempre exponer NormalizerTools después de un pequeño retardo setTimeout(exposeNormalizerTools, 2000); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址