PageSourceSearch

https://www.ipvc.pt/ess/wp-content/themes/gsk-site-escola/js/skill/glossary-extended.js?ver=0.0.1

js ipvc.pt collected 2026-10-01 11:27:07 UTC 14,314 bytes, 468 lines download raw bytes

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.