1(function () { 2 'use strict'; 3 4 const GlossaryExtendedFixed = { 5 termsMap: new Map(), 6 7 // Flag para pausar o observer durante o nosso próprio processamento 8 isProcessing: false, 9 10 // Referência ao observer para podermos pausar 11 observer: null, 12 13 config: { 14 debug: false, 15 delay: 500, 16 apiEndpoint: '/wp-json/wp/v2/glossary', 17 perPage: 100, 18 cacheMinutes: 60 19 }, 20 21 init: function () { 22 this.log('ð Iniciando Glossary Extended...'); 23 24 if (document.readyState === 'loading') { 25 document.addEventListener('DOMContentLoaded', () => this.start()); 26 } else { 27 this.start(); 28 } 29 }, 30 31 start: function () { 32 this.log('ð¡ Tentando obter termos via REST API...'); 33 34 this.fetchTermsFromAPI() 35 .then(() => { 36 if (this.termsMap.size > 0) { 37 this.log(`â ${this.termsMap.size} termos obtidos da REST API`); 38 this.processPage(); 39 } else { 40 this.log('â ï¸ API retornou vazio, tentando extrair dos tooltips na página...'); 41 this.fallbackToPageTooltips(); 42 } 43 }) 44 .catch((err) => { 45 this.log(`â ï¸ REST API falhou: ${err.message}. Usando fallback para tooltips na página...`, true); 46 this.fallbackToPageTooltips(); 47 }); 48 49 this.observeDOM(); 50 }, 51 52 fetchTermsFromAPI: async function () { 53 const cached = this.getCachedTerms(); 54 if (cached) { 55 this.log(`ð¦ ${cached.length} termos carregados do cache (sessionStorage)`); 56 this.loadTermsFromArray(cached); 57 return; 58 } 59 60 this.log(`ð A fazer fetch para: ${this.config.apiEndpoint}`); 61 62 const allTerms = []; 63 let page = 1; 64 let hasMore = true; 65 66 while (hasMore && page <= 10) { 67 const url = `${this.config.apiEndpoint}?per_page=${this.config.perPage}&page=${page}&_fields=id,title,excerpt,link`; 68 69 this.log(` Página ${page}: ${url}`); 70 71 const response = await fetch(url, { 72 credentials: 'same-origin', 73 headers: { 'Accept': 'application/json' } 74 }); 75 76 if (!response.ok) { 77 if (response.status === 400 && page > 1) break; 78 throw new Error(`REST API retornou ${response.status}`); 79 } 80 81 const data = await response.json(); 82 83 if (!Array.isArray(data) || data.length === 0) { 84 hasMore = false; 85 break; 86 } 87 88 this.log(` â ${data.length} termos recebidos da página ${page}`); 89 allTerms.push(...data); 90 91 const totalPages = parseInt(response.headers.get('X-WP-TotalPages') || '1'); 92 if (page >= totalPages) hasMore = false; 93 94 page++; 95 } 96 97 this.log(`ð Total: ${allTerms.length} termos obtidos da REST API`); 98 this.setCachedTerms(allTerms); 99 this.loadTermsFromArray(allTerms); 100 }, 101 102 loadTermsFromArray: function (terms) { 103 if (!Array.isArray(terms)) return; 104 105 terms.forEach(term => { 106 const titleRendered = term && term.title && term.title.rendered ? term.title.rendered : ''; 107 const excerptRendered = term && term.excerpt && term.excerpt.rendered ? term.excerpt.rendered : ''; 108 const link = term && term.link ? term.link : '#'; 109 110 const termText = this.stripHTML(titleRendered).trim(); 111 const description = this.stripHTML(excerptRendered).trim(); 112 113 if (termText && termText.length >= 2 && !this.termsMap.has(termText)) { 114 this.termsMap.set(termText, { 115 term: termText, 116 description: description || termText, 117 link: link, 118 id: term.id, 119 fromAPI: true 120 }); 121 } 122 }); 123 }, 124 125 getCachedTerms: function () { 126 try { 127 const cached = sessionStorage.getItem('glossary_extended_terms'); 128 if (!cached) return null; 129 130 const data = JSON.parse(cached); 131 132 if (!Array.isArray(data.terms) || data.terms.length === 0) { 133 sessionStorage.removeItem('glossary_extended_terms'); 134 return null; 135 } 136 137 const expires = data.timestamp + (this.config.cacheMinutes * 60 * 1000); 138 139 if (Date.now() > expires) { 140 sessionStorage.removeItem('glossary_extended_terms'); 141 return null; 142 } 143 144 return data.terms; 145 } catch (e) { 146 return null; 147 } 148 }, 149 150 setCachedTerms: function (terms) { 151 try { 152 if (!Array.isArray(terms) || terms.length === 0) return; 153 sessionStorage.setItem('glossary_extended_terms', JSON.stringify({ 154 timestamp: Date.now(), 155 terms: terms 156 })); 157 } catch (e) { 158 this.log('Aviso: não foi possÃvel guardar no sessionStorage', true); 159 } 160 }, 161 162 clearCache: function () { 163 sessionStorage.removeItem('glossary_extended_terms'); 164 this.log('ðï¸ Cache limpo'); 165 }, 166 167 stripHTML: function (html) { 168 if (typeof html !== 'string') return ''; 169 170 const tmp = document.createElement('div'); 171 tmp.innerHTML = html; 172 return tmp.textContent || tmp.innerText || ''; 173 }, 174 175 fallbackToPageTooltips: function () { 176 setTimeout(() => { 177 this.extractTermsFromTooltips(); 178 179 if (this.termsMap.size > 0) { 180 this.log(`â ${this.termsMap.size} termos extraÃdos dos tooltips da página`); 181 this.processPage(); 182 } else { 183 this.log('â Nenhum termo encontrado (nem na API nem nos tooltips)', true); 184 } 185 }, this.config.delay); 186 }, 187 188 extractTermsFromTooltips: function () { 189 // Importante: só procurar os tooltips ORIGINAIS do plugin, não os nossos 190 const tooltips = document.querySelectorAll('.glossary-tooltip:not(.glossary-extended-added)'); 191 this.log(`ð Total de .glossary-tooltip originais na página: ${tooltips.length}`); 192 193 tooltips.forEach((tooltip) => { 194 try { 195 let fullText = tooltip.textContent.trim(); 196 if (!fullText || fullText.length < 2) return; 197 198 let termText = ''; 199 let description = ''; 200 201 const termSpan = tooltip.querySelector('.glossary-only-text, .glossary-link'); 202 const descSpan = tooltip.querySelector('.glossary-tooltip-text'); 203 204 if (termSpan) termText = termSpan.textContent.trim(); 205 if (descSpan) description = descSpan.textContent.trim(); 206 207 if (!termText) { 208 const walker = document.createTreeWalker(tooltip, NodeFilter.SHOW_TEXT, null, false); 209 const firstTextNode = walker.nextNode(); 210 if (firstTextNode) termText = firstTextNode.textContent.trim(); 211 } 212 213 if (!termText) { 214 fullText = fullText.replace(/\s*More\s*$/i, '').trim(); 215 const parts = fullText.split(/\s{2,}|(?=[A-Z])/); 216 termText = parts[0].trim(); 217 } 218 219 if (description) { 220 description = description.replace(/\s*More\s*$/i, '').trim(); 221 } 222 223 if (!description && fullText !== termText) { 224 description = fullText.replace(termText, '').trim(); 225 description = description.replace(/\s*More\s*$/i, '').trim(); 226 } 227 228 if (termText && termText.length >= 2 && !this.termsMap.has(termText)) { 229 this.termsMap.set(termText, { 230 term: termText, 231 description: description || termText, 232 element: tooltip, 233 classes: tooltip.className,
234 fromAPI: false 235 }); 236 } 237 238 } catch (e) { 239 this.log(`Error: ${e.message}`, true); 240 } 241 }); 242 }, 243 244 processPage: function () { 245 // Pausar observer enquanto processamos para evitar loop 246 this.isProcessing = true; 247 248 this.log('ð¨ A processar página...'); 249 // this.processHeadings(); 250 // this.processLinks(); 251 this.log('â Processamento concluÃdo!'); 252 253 // Aguardar um pouco antes de reativar o observer 254 setTimeout(() => { 255 this.isProcessing = false; 256 }, 100); 257 }, 258 259 processHeadings: function () { 260 const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); 261 let processed = 0; 262 let skipped = 0; 263 264 headings.forEach(heading => { 265 const result = this.processElement(heading); 266 if (result === true) processed++; 267 else if (result === 'skipped') skipped++; 268 }); 269 270 this.log(` Headings: ${processed}/${headings.length} processados (${skipped} ignorados)`); 271 }, 272 273 processLinks: function () { 274 const links = document.querySelectorAll('a'); 275 let processed = 0; 276 let skipped = 0; 277 278 links.forEach(link => { 279 const result = this.processElement(link); 280 if (result === true) processed++; 281 else if (result === 'skipped') skipped++; 282 }); 283 284 this.log(` Links: ${processed}/${links.length} processados (${skipped} ignorados)`); 285 }, 286 287 /** 288 * CORRIGIDO: Verificar várias condições para evitar loop infinito 289 */ 290 processElement: function (element) { 291 // 1. Se já contém um tooltip (do plugin ou nosso), não processar 292 if (element.querySelector('.glossary-tooltip, .glossary-tooltip-text')) { 293 return 'skipped'; 294 } 295 296 // 2. CRÃTICO: Se o elemento ESTà DENTRO de um tooltip (do plugin ou nosso) 297 // Isto evita o aninhamento infinito! 298 if (element.closest('.glossary-tooltip, .glossary-link, .glossary-tooltip-text, .glossary-extended-added')) { 299 return 'skipped'; 300 } 301 302 // 3. Ignorar elementos especiais 303 if (element.closest('script, style, .glossary-archive, .glossary-widget')) { 304 return 'skipped'; 305 } 306 307 const elementText = element.textContent.trim(); 308 if (!elementText || elementText.length < 2) { 309 return false; 310 } 311 312 const sortedTerms = Array.from(this.termsMap.entries()) 313 .sort((a, b) => b[0].length - a[0].length); 314 315 for (const [termText, termData] of sortedTerms) { 316 const regex = new RegExp('\\b' + this.escapeRegex(termText) + '\\b', 'gi'); 317 318 if (regex.test(elementText)) { 319 this.replaceTermInElement(element, termText, termData); 320 return true; 321 } 322 } 323 324 return false; 325 }, 326 327 replaceTermInElement: function (element, termText, termData) { 328 const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); 329 const nodesToProcess = []; 330 let node; 331 332 while (node = walker.nextNode()) { 333 // CRÃTICO: Não processar text nodes que já estão dentro de tooltips 334 if (node.parentElement.closest('.glossary-tooltip, .glossary-link, .glossary-extended-added')) { 335 continue; 336 } 337 338 if (node.textContent.trim().length > 0) { 339 nodesToProcess.push(node); 340 } 341 } 342 343 nodesToProcess.forEach(textNode => { 344 const text = textNode.textContent; 345 const regex = new RegExp('\\b' + this.escapeRegex(termText) + '\\b', 'g'); 346 347 if (regex.test(text)) { 348 const span = document.createElement('span'); 349 const newHTML = text.replace( 350 new RegExp('\\b' + this.escapeRegex(termText) + '\\b', 'g'), 351 () => this.createTooltipHTML(termData) 352 ); 353 354 span.innerHTML = newHTML; 355 textNode.parentNode.replaceChild(span, textNode); 356 } 357 }); 358 }, 359 360 createTooltipHTML: function (termData) { 361 if (termData.fromAPI) { 362 const escapedDesc = this.escapeHTML(termData.description); 363 const escapedTerm = this.escapeHTML(termData.term); 364 const link = termData.link || '#'; 365 366 return `<span class="glossary-tooltip glossary-extended-added" tabindex="0">` + 367 `<span class="glossary-link"><a href="${link}" class="glossary-only-link"><abbr title="${escapedDesc}">${escapedTerm}</abbr></a></span>` + 368 `<span class="hidden glossary-tooltip-content clearfix">` + 369 `<span class="glossary-tooltip-text">${escapedDesc} <a href="${link}">More</a></span>` + 370 `</span>` + 371 `</span>`; 372 } else { 373 const clone = termData.element.cloneNode(true); 374 clone.className = termData.classes; 375 clone.classList.add('glossary-extended-added'); 376 return clone.outerHTML; 377 } 378 }, 379 380 escapeHTML: function (str) { 381 const div = document.createElement('div'); 382 div.textContent = str; 383 return div.innerHTML; 384 }, 385 386 /** 387 * CORRIGIDO: Observer que ignora mutations nossas 388 */ 389 observeDOM: function () { 390 this.observer = new MutationObserver((mutations) => { 391 // Se estamos a processar, ignorar 392 if (this.isProcessing) return; 393 394 // Verificar se as mutations são todas dentro de tooltips (nossas mudanças) 395 const hasRelevantChanges = mutations.some(mutation => { 396 // Ignorar mutations dentro de tooltips ou elementos adicionados por nós 397 if (mutation.target.closest && mutation.target.closest('.glossary-tooltip, .glossary-extended-added')) { 398 return false; 399 } 400 401 // Verificar nodes adicionados 402 const addedNodes = Array.from(mutation.addedNodes); 403 const allInTooltip = addedNodes.every(node => { 404 if (node.nodeType !== 1) return true; // não é element 405 return node.classList && ( 406 node.classList.contains('glossary-tooltip') || 407 node.classList.contains('glossary-extended-added') || 408 node.closest('.glossary-tooltip, .glossary-extended-added') 409 ); 410 });
411 412 return !allInTooltip; 413 }); 414 415 if (!hasRelevantChanges) return; 416 417 clearTimeout(this.observeTimer); 418 this.observeTimer = setTimeout(() => { 419 this.log('ð Mudança no DOM detectada, reprocessando...'); 420 this.processPage(); 421 }, 1000); 422 }); 423 424 this.observer.observe(document.body, { 425 childList: true, 426 subtree: true 427 }); 428 }, 429 430 escapeRegex: function (str) { 431 return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 432 }, 433 434 log: function (message, isError = false) { 435 const style = isError ? 'color: #ff6b6b; font-weight: bold;' : 'color: #667eea;'; 436 if (this.config.debug || isError) { 437 console.log(`%c[Glossary Extended] ${message}`, style); 438 } 439 }, 440 441 setDebug: function (enabled) { 442 this.config.debug = enabled; 443 }, 444 445 reprocess: function () { 446 this.termsMap.clear(); 447 this.start(); 448 }, 449 450 showTerms: function () { 451 console.log('=== TERMOS MAPEADOS ==='); 452 console.log(`Total: ${this.termsMap.size}`); 453 this.termsMap.forEach((data, term) => { 454 const source = data.fromAPI ? '[API]' : '[DOM]'; 455 console.log(`${source} "${term}" -> "${(data.description || '').substring(0, 50)}..."`); 456 }); 457 } 458 }; 459 460 GlossaryExtendedFixed.init(); 461 462 window.GlossaryExtendedFixed = GlossaryExtendedFixed; 463 window.glossaryReprocess = () => GlossaryExtendedFixed.reprocess(); 464 window.glossaryDebug = (enabled) => GlossaryExtendedFixed.setDebug(enabled); 465 window.glossaryShowTerms = () => GlossaryExtendedFixed.showTerms(); 466 window.glossaryClearCache = () => GlossaryExtendedFixed.clearCache(); 467 468})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.