PageSourceSearch

https://tuiuti.sp.gov.br/wp-content/plugins/tuiuti-letreiro/assets/js/letreiro.js?ver=1.0.0

js tuiuti.sp.gov.br collected 2026-10-02 11:44:04 UTC 20,814 bytes, 845 lines download raw bytes

1/**
2 * Tuiuti — Letreiro Inteligente
3 * Motor de rolagem contínua, relógio ao vivo, atualização assíncrona
4 * e prioridade automática para mensagens emergenciais.
5 *
6 * Sem dependências externas.
7 */
8(function () {
9	'use strict';
10
11	if (typeof window === 'undefined' || !window.TL_DATA) {
12		return;
13	}
14
15	var DATA = window.TL_DATA;
16	var MOBILE_BP = 782;
17
18	/* ------------------------------------------------------------------ */
19	/* Utilidades                                                          */
20	/* ------------------------------------------------------------------ */
21
22	function el(tag, cls) {
23		var n = document.createElement(tag);
24		if (cls) {
25			n.className = cls;
26		}
27		return n;
28	}
29
30	function isMobile() {
31		return window.matchMedia('(max-width: ' + MOBILE_BP + 'px)').matches;
32	}
33
34	function prefersReducedMotion() {
35		return (
36			window.matchMedia &&
37			window.matchMedia('(prefers-reduced-motion: reduce)').matches
38		);
39	}
40
41	function hash(str) {
42		var h = 5381;
43		for (var i = 0; i < str.length; i++) {
44			h = ((h << 5) + h + str.charCodeAt(i)) | 0;
45		}
46		return String(h);
47	}
48
49	/* ------------------------------------------------------------------ */
50	/* Relógio sincronizado com o fuso do servidor                          */
51	/* ------------------------------------------------------------------ */
52
53	var Clock = {
54		delta: 0,
55		tz: null,
56		fmt: null,
57		fmtSec: null,
58
59		sync: function (feed) {
60			if (feed && feed.servidor_iso) {
61				var serverMs = Date.parse(feed.servidor_iso);
62				if (!isNaN(serverMs)) {
63					// gerado_em está em segundos; usa-o como referência de emissão.
64					this.delta = serverMs - Date.now();
65					// Compensa a latência já decorrida se o payload veio embutido.
66					if (Math.abs(this.delta) > 24 * 3600 * 1000) {
67						this.delta = 0;
68					}
69				}
70			}
71			var tz = feed && feed.fuso ? feed.fuso : null;
72			if (tz && /^[A-Za-z]+\/[A-Za-z_\-+0-9]+$/.test(tz)) {
73				this.tz = tz;
74			}
75			this.buildFormatters();
76		},
77
78		buildFormatters: function () {
79			var base = { hour: '2-digit', minute: '2-digit', hour12: false };
80			var withSec = {
81				hour: '2-digit',
82				minute: '2-digit',
83				second: '2-digit',
84				hour12: false
85			};
86			if (this.tz) {
87				base.timeZone = this.tz;
88				withSec.timeZone = this.tz;
89			}
90			try {
91				this.fmt = new Intl.DateTimeFormat('pt-BR', base);
92				this.fmtSec = new Intl.DateTimeFormat('pt-BR', withSec);
93			} catch (e) {
94				this.fmt = null;
95				this.fmtSec = null;
96			}
97		},
98
99		now: function () {
100			return new Date(Date.now() + this.delta);
101		},
102
103		text: function (withSeconds) {
104			var d = this.now();
105			var f = withSeconds ? this.fmtSec : this.fmt;
106			if (f) {
107				return f.format(d).replace(/:/g, withSeconds ? ':' : 'h');
108			}
109			var hh = String(d.getHours()).padStart(2, '0');
110			var mm = String(d.getMinutes()).padStart(2, '0');
111			if (!withSeconds) {
112				return hh + 'h' + mm;
113			}
114			return hh + ':' + mm + ':' + String(d.getSeconds()).padStart(2, '0');
115		},
116
117		iso: function () {
118			return this.now().toISOString();
119		}
120	};
121
122	/* ------------------------------------------------------------------ */
123	/* Construção de itens                                                 */
124	/* ------------------------------------------------------------------ */
125
126	function buildItem(item, cfg) {
127		var wrap = el('span', 'tl-item tl-tipo-' + String(item.tipo || 'mensagem').replace(/[^a-z0-9_-]/gi, ''));
128		wrap.setAttribute('data-tl-id', item.id || '');
129		wrap.setAttribute('data-tl-tipo', item.tipo || '');
130
131		if (item.cor) {
132			wrap.style.setProperty('--tl-item-cor', item.cor);
133		}
134
135		if (cfg.icones && item.icone) {
136			var ic = el('span', 'tl-icone');
137			ic.setAttribute('aria-hidden', 'true');
138			ic.textContent = item.icone;
139			wrap.appendChild(ic);
140		}
141
142		if (cfg.selos && item.selo) {
143			var selo = el('span', 'tl-selo');
144			selo.textContent = item.selo;
145			wrap.appendChild(selo);
146		}
147
148		var texto = String(item.texto || '');
149		var alvo;
150
151		if (item.link) {
152			alvo = el('a', 'tl-texto');
153			alvo.href = item.link;
154		} else {
155			alvo = el('span', 'tl-texto');
156		}
157
158		if (texto.indexOf('{{HORA}}') !== -1) {
159			var partes = texto.split('{{HORA}}');
160			for (var i = 0; i < partes.length; i++) {
161				if (partes[i]) {
162					alvo.appendChild(document.createTextNode(partes[i]));
163				}
164				if (i < partes.length - 1) {
165					var t = el('time', 'tl-relogio');
166					alvo.appendChild(t);
167				}
168			}
169		} else {
170			alvo.textContent = texto;
171		}
172
173		wrap.appendChild(alvo);
174
175		var frag = document.createDocumentFragment();
176		frag.appendChild(wrap);
177		var sep = el('span', 'tl-sep');
178		sep.setAttribute('aria-hidden', 'true');
179		frag.appendChild(sep);
180
181		return frag;
182	}
183
184	function buildSeq(itens, cfg) {
185		var seq = el('div', 'tl-seq');
186		for (var i = 0; i < itens.length; i++) {
187			seq.appendChild(buildItem(itens[i], cfg));
188		}
189		return seq;
190	}
191
192	/* ------------------------------------------------------------------ */
193	/* Motor do letreiro                                                   */
194	/* ------------------------------------------------------------------ */
195
196	function Letreiro(root) {
197		this.root = root;
198		this.viewport = root.querySelector('.tl-viewport');
199		this.track = root.querySelector('.tl-track');
200		this.btn = root.querySelector('.tl-btn-pausa');
201
202		this.feed = DATA.inicial || { itens: [], emergencias: [], config: {} };
203		this.cfg = this.feed.config || {};
204
205		this.offset = 0;
206		this.seqWidth = 0;
207		this.viewWidth = 0;
208		this.running = false;
209		this.pausedByUser = false;
210		this.pausedByHover = false;
211		this.pausedByVisibility = false;
212		this.pausedByViewport = false;
213		this.lastTs = 0;
214		this.laps = 0;
215		this.emergencyLaps = 0;
216		this.pendingFeed = null;
217		this.currentHash = '';
218		this.reduced = false;
219		this.statsQueue = [];
220		this.itemMetrics = [];
221		this.destroyed = false;
222
223		this.init();
224	}
225
226	Letreiro.prototype.init = function () {
227		Clock.sync(this.feed);
228
229		this.reduced =
230			!!this.cfg.reducedMotion && prefersReducedMotion();
231
232		if (this.reduced) {
233			this.root.setAttribute('data-tl-reduzido', '1');
234		}
235
236		this.render(this.feed, true);
237		this.bind();
238		this.startClock();
239		this.scheduleRefresh();
240
241		this.root.setAttribute('data-tl-pronto', '1');
242
243		if (!this.reduced) {
244			this.start();
245		}
246	};
247
248	/**
249	 * Decide quais itens exibir agora (emergência tem prioridade).
250	 */
251	Letreiro.prototype.pickItems = function (feed) {
252		var emerg = feed.emergencias || [];
253		var itens = feed.itens || [];
254
255		if (!emerg.length) {
256			this.root.classList.remove('tl-modo-emergencia');
257			return itens;
258		}
259
260		var exclusiva = !!this.cfg.emergenciaExclusiva;
261		var reps = parseInt(this.cfg.emergenciaRepeticoes, 10);
262		if (isNaN(reps)) {
263			reps = 3;
264		}
265
266		// reps = 0 → sempre exclusiva enquanto houver emergência.
267		if (exclusiva && (reps === 0 || this.emergencyLaps < reps)) {
268			this.root.classList.add('tl-modo-emergencia');
269			return emerg;
270		}
271
272		// Após as repetições, intercala: emergências primeiro, depois o ciclo normal.
273		this.root.classList.remove('tl-modo-emergencia');
274		return emerg.concat(itens);
275	};
276
277	/**
278	 * Monta o conteúdo e mede as dimensões.
279	 */
280	Letreiro.prototype.render = function (feed, first) {
281		var itens = this.pickItems(feed);
282
283		if (!itens.length) {
284			this.root.style.display = 'none';
285			return;
286		}
287		this.root.style.display = '';
288
289		var novoHash = hash(
290			JSON.stringify(
291				itens.map(function (i) {
292					return [i.id, i.texto, i.icone, i.cor, i.selo].join('|');
293				})
294			)
295		);
296
297		// Primeira carga: reaproveita o HTML impresso pelo servidor, mas só se
298		// ele corresponder ao feed. Uma página servida de cache pode trazer
299		// conteúdo antigo — nesse caso remontamos a partir do feed.
300		if (first && this.track.querySelector('.tl-seq')) {
301			if (this.domCombina(itens)) {
302				this.currentHash = novoHash;
303				this.measure();
304				this.fill(itens);
305				return;
306			}
307			// Divergência: cai no caminho de remontagem abaixo.
308			this.currentHash = '';
309		}
310
311		if (novoHash === this.currentHash) {
312			return;
313		}
314		this.currentHash = novoHash;
315
316		while (this.track.firstChild) {
317			this.track.removeChild(this.track.firstChild);
318		}
319		this.track.appendChild(buildSeq(itens, this.cfg));
320
321		this.offset = 0;
322		this.measure();
323		this.fill(itens);
324		this.paint();
325	};
326
327	/**
328	 * O HTML já presente na página corresponde aos itens do feed?
329	 *
330	 * Protege contra HTML servido de cache (plugin de cache, CDN) mais antigo
331	 * que o feed embutido: sem isso o letreiro exibiria conteúdo desatualizado
332	 * até a primeira atualização assíncrona.
333	 */
334	Letreiro.prototype.domCombina = function (itens) {
335		var seq = this.track.querySelector('.tl-seq');
336		if (!seq) {
337			return false;
338		}
339		var nodes = seq.querySelectorAll('.tl-item');
340		if (nodes.length !== itens.length) {
341			return false;
342		}
343		for (var i = 0; i < nodes.length; i++) {
344			if (nodes[i].getAttribute('data-tl-id') !== String(itens[i].id || '')) {
345				return false;
346			}
347		}
348		return true;
349	};
350
351	/**
352	 * Duplica a sequência até cobrir a viewport (loop sem emenda).
353	 */
354	Letreiro.prototype.fill = function (itens) {
355		var base = this.track.querySelector('.tl-seq');
356		if (!base) {
357			return;
358		}
359
360		// Remove clones anteriores.
361		var seqs = this.track.querySelectorAll('.tl-seq');
362		for (var i = seqs.length - 1; i >= 1; i--) {
363			this.track.removeChild(seqs[i]);
364		}
365
366		this.seqWidth = base.getBoundingClientRect().width;
367		this.viewWidth = this.viewport.getBoundingClientRect().width;
368
369		if (this.seqWidth <= 0) {
370			return;
371		}
372
373		// Precisa de largura total >= largura da sequência + viewport.
374		var alvo = this.seqWidth + this.viewWidth;
375		var total = this.seqWidth;
376		var guarda = 0;
377
378		while (total < alvo && guarda < 24) {
379			var clone = base.cloneNode(true);
380			clone.setAttribute('aria-hidden', 'true');
381			this.track.appendChild(clone);
382			total += this.seqWidth;
383			guarda++;
384		}
385
386		this.computeMetrics(itens);
387	};
388
389	/**
390	 * Larguras individuais dos itens, para estatística de tempo em tela.
391	 */
392	Letreiro.prototype.computeMetrics = function (itens) {
393		this.itemMetrics = [];
394		if (!this.cfg.stats) {
395			return;
396		}
397		var base = this.track.querySelector('.tl-seq');
398		if (!base) {
399			return;
400		}
401		var nodes = base.querySelectorAll('.tl-item');
402		for (var i = 0; i < nodes.length && i < itens.length; i++) {
403			this.itemMetrics.push({
404				id: itens[i].id || '',
405				tipo: itens[i].tipo || 'outro',
406				w: nodes[i].getBoundingClientRect().width
407			});
408		}
409	};
410
411	Letreiro.prototype.measure = function () {
412		var base = this.track.querySelector('.tl-seq');
413		this.seqWidth = base ? base.getBoundingClientRect().width : 0;
414		this.viewWidth = this.viewport.getBoundingClientRect().width;
415	};
416
417	Letreiro.prototype.speed = function () {
418		var v = isMobile() ? this.cfg.velocidadeMobile : this.cfg.velocidade;
419		v = parseFloat(v);
420		return isNaN(v) || v <= 0 ? 60 : v;
421	};
422
423	Letreiro.prototype.paint = function () {
424		var x = this.offset;
425		if (this.cfg.direcao === 'direita') {
426			x = this.offset - this.seqWidth;
427		}
428		this.track.style.transform = 'translate3d(' + x.toFixed(2) + 'px,0,0)';
429	};
430
431	/* ---------------- Loop de animação ---------------- */
432
433	Letreiro.prototype.start = function () {
434		if (this.running || this.reduced || this.destroyed) {
435			return;
436		}
437		this.running = true;
438		this.lastTs = 0;
439		var self = this;
440		this.rafId = window.requestAnimationFrame(function (ts) {
441			self.frame(ts);
442		});
443	};
444
445	Letreiro.prototype.stop = function () {
446		this.running = false;
447		if (this.rafId) {
448			window.cancelAnimationFrame(this.rafId);
449			this.rafId = null;
450		}
451	};
452
453	Letreiro.prototype.isPaused = function () {
454		return (
455			this.pausedByUser ||
456			this.pausedByHover ||
457			this.pausedByVisibility ||
458			this.pausedByViewport
459		);
460	};
461
462	Letreiro.prototype.frame = function (ts) {
463		if (!this.running || this.destroyed) {
464			return;
465		}
466
467		var self = this;
468		this.rafId = window.requestAnimationFrame(function (t) {
469			self.frame(t);
470		});
471
472		if (!this.lastTs) {
473			this.lastTs = ts;
474			return;
475		}
476
477		var dt = (ts - this.lastTs) / 1000;
478		this.lastTs = ts;
479
480		// Protege contra saltos grandes (aba em segundo plano).
481		if (dt > 0.25) {
482			dt = 0.25;
483		}
484
485		if (this.isPaused() || this.seqWidth <= 0) {
486			return;
487		}
488
489		var delta = this.speed() * dt;
490
491		if (this.cfg.direcao === 'direita') {
492			this.offset += delta;
493			if (this.offset >= this.seqWidth) {
494				this.offset -= this.seqWidth;
495				this.onLap();
496			}
497		} else {
498			this.offset -= delta;
499			if (this.offset <= -this.seqWidth) {
500				this.offset += this.seqWidth;
501				this.onLap();
502			}
503		}
504
505		this.paint();
506	};
507
508	/**
509	 * Chamado a cada volta completa: ponto seguro para trocar o conteúdo.
510	 */
511	Letreiro.prototype.onLap = function () {
512		this.laps++;
513
514		if (this.root.classList.contains('tl-modo-emergencia')) {
515			this.emergencyLaps++;
516		}
517
518		this.collectStats();
519
520		if (this.pendingFeed) {
521			var f = this.pendingFeed;
522			this.pendingFeed = null;
523			this.feed = f;
524			this.cfg = f.config || this.cfg;
525			Clock.sync(f);
526			this.render(f, false);
527			this.tickClock();
528			return;
529		}
530
531		// Após esgotar as repetições exclusivas, remonta incluindo o ciclo normal.
532		if (
533			this.root.classList.contains('tl-modo-emergencia') &&
534			parseInt(this.cfg.emergenciaRepeticoes, 10) > 0 &&
535			this.emergencyLaps >= parseInt(this.cfg.emergenciaRepeticoes, 10)
536		) {
537			this.render(this.feed, false);
538			this.tickClock();
539		}
540	};
541
542	/* ---------------- Estatísticas ---------------- */
543
544	Letreiro.prototype.collectStats = function () {
545		if (!this.cfg.stats || !this.itemMetrics.length) {
546			return;
547		}
548		var v = this.speed();
549		if (v <= 0) {
550			return;
551		}
552		for (var i = 0; i < this.itemMetrics.length; i++) {
553			var m = this.itemMetrics[i];
554			this.statsQueue.push({
555				id: m.id,
556				tipo: m.tipo,
557				ms: Math.round(((m.w + this.viewWidth) / v) * 1000)
558			});
559		}
560		if (this.statsQueue.length >= 40) {
561			this.flushStats();
562		}
563	};
564
565	Letreiro.prototype.flushStats = function (useBeacon) {
566		if (!this.cfg.stats || !this.statsQueue.length || !DATA.statsUrl) {
567			return;
568		}
569		var payload = JSON.stringify({ eventos: this.statsQueue.slice(0, 200) });
570		this.statsQueue = [];
571
572		try {
573			if (useBeacon && navigator.sendBeacon) {
574				navigator.sendBeacon(
575					DATA.statsUrl,
576					new Blob([payload], { type: 'application/json' })
577				);
578				return;
579			}
580			window.fetch(DATA.statsUrl, {
581				method: 'POST',
582				headers: { 'Content-Type': 'application/json' },
583				body: payload,
584				keepalive: true,
585				credentials: 'omit'
586			})['catch'](function () {});
587		} catch (e) {
588			/* silencioso */
589		}
590	};
591
592	/* ---------------- Relógio ---------------- */
593
594	Letreiro.prototype.startClock = function () {
595		if (!this.cfg.mostrarHora) {
596			return;
597		}
598		var self = this;
599		this.tickClock();
600		this.clockTimer = window.setInterval(
601			function () {
602				self.tickClock();
603			},
604			this.cfg.mostrarSegundos ? 1000 : 15000
605		);
606	};
607
608	Letreiro.prototype.tickClock = function () {
609		var nodes = this.root.querySelectorAll('.tl-relogio');
610		if (!nodes.length) {
611			return;
612		}
613		var txt = Clock.text(!!this.cfg.mostrarSegundos);
614		var iso = Clock.iso();
615		for (var i = 0; i < nodes.length; i++) {
616			if (nodes[i].textContent !== txt) {
617				nodes[i].textContent = txt;
618				nodes[i].setAttribute('datetime', iso);
619			}
620		}
621	};
622
623	/* ---------------- Atualização assíncrona ---------------- */
624
625	Letreiro.prototype.scheduleRefresh = function () {
626		var ms = parseInt(this.cfg.intervalo, 10);
627		if (isNaN(ms) || ms < 30000) {
628			ms = 300000;
629		}
630		var self = this;
631		this.refreshTimer = window.setInterval(function () {
632			self.refresh();
633		}, ms);
634	};
635
636	Letreiro.prototype.refresh = function () {
637		if (this.destroyed || !DATA.feedUrl || document.hidden) {
638			return;
639		}
640		var self = this;
641		window
642			.fetch(DATA.feedUrl, { credentials: 'omit' })
643			.then(function (r) {
644				return r.ok ? r.json() : null;
645			})
646			.then(function (json) {
647				if (!json || self.destroyed) {
648					return;
649				}
650				if (json.ativo === false) {
651					self.root.style.display = 'none';
652					self.stop();
653					return;
654				}
655
656				var temEmergNova =
657					(json.emergencias || []).length > 0 &&
658					(self.feed.emergencias || []).length === 0;
659
660				if (temEmergNova) {
661					// Emergência entra imediatamente, sem esperar a volta.
662					self.feed = json;
663					self.cfg = json.config || self.cfg;
664					self.emergencyLaps = 0;
665					Clock.sync(json);
666					self.render(json, false);
667					self.tickClock();
668					return;
669				}
670
671				self.pendingFeed = json;
672
673				// Se estiver parado (pausado/sem movimento), aplica direto.
674				if (self.reduced || self.isPaused()) {
675					self.feed = json;
676					self.cfg = json.config || self.cfg;
677					self.pendingFeed = null;
678					Clock.sync(json);
679					self.render(json, false);
680					self.tickClock();
681				}
682			})
683			['catch'](function () {});
684	};
685
686	/* ---------------- Eventos ---------------- */
687
688	Letreiro.prototype.bind = function () {
689		var self = this;
690
691		if (this.cfg.pausaHover) {
692			this.root.addEventListener('mouseenter', function () {
693				self.pausedByHover = true;
694			});
695			this.root.addEventListener('mouseleave', function () {
696				self.pausedByHover = false;
697			});
698		}
699
700		if (this.cfg.pausaFoco) {
701			this.root.addEventListener('focusin', function () {
702				self.pausedByHover = true;
703			});
704			this.root.addEventListener('focusout', function () {
705				if (!self.root.contains(document.activeElement)) {
706					self.pausedByHover = false;
707				}
708			});
709		}
710
711		if (this.btn) {
712			this.btn.addEventListener('click', function () {
713				self.pausedByUser = !self.pausedByUser;
714				self.btn.setAttribute('aria-pressed', self.pausedByUser ? 'true' : 'false');
715				self.btn.setAttribute(
716					'aria-label',
717					self.pausedByUser
718						? (DATA.i18n && DATA.i18n.retomar) || 'Retomar letreiro'
719						: (DATA.i18n && DATA.i18n.pausar) || 'Pausar letreiro'
720				);
721			});
722		}
723
724		document.addEventListener('visibilitychange', function () {
725			self.pausedByVisibility = document.hidden;
726			if (!document.hidden) {
727				self.lastTs = 0;
728			}
729		});
730
731		if ('IntersectionObserver' in window) {
732			var io = new IntersectionObserver(
733				function (entries) {
734					for (var i = 0; i < entries.length; i++) {
735						self.pausedByViewport = !entries[i].isIntersecting;
736						if (entries[i].isIntersecting) {
737							self.lastTs = 0;
738						}
739					}
740				},
741				{ threshold: 0 }
742			);
743			io.observe(this.root);
744			this.io = io;
745		}
746
747		var resizeTimer = null;
748		window.addEventListener('resize', function () {
749			window.clearTimeout(resizeTimer);
750			resizeTimer = window.setTimeout(function () {
751				var itens = self.pickItems(self.feed);
752				self.measure();
753				self.fill(itens);
754				if (Math.abs(self.offset) > self.seqWidth) {
755					self.offset = 0;
756				}
757				self.paint();
758			}, 180);
759		});
760
761		// Recalcula quando as fontes terminam de carregar.
762		if (document.fonts && document.fonts.ready) {
763			document.fonts.ready.then(function () {
764				var itens = self.pickItems(self.feed);
765				self.measure();
766				self.fill(itens);
767				self.paint();
768			});
769		}
770
771		window.addEventListener('pagehide', function () {
772			self.flushStats(true);
773		});
774
775		document.addEventListener('visibilitychange', function () {
776			if (document.hidden) {
777				self.flushStats(true);
778			}
779		});
780
781		if (window.matchMedia) {
782			var mq = window.matchMedia('(prefers-reduced-motion: reduce)');
783			var onChange = function () {
784				var novo = !!self.cfg.reducedMotion && mq.matches;
785				if (novo === self.reduced) {
786					return;
787				}
788				self.reduced = novo;
789				self.root.setAttribute('data-tl-reduzido', novo ? '1' : '0');
790				if (novo) {
791					self.stop();
792					self.track.style.transform = '';
793				} else {
794					self.offset = 0;
795					self.paint();
796					self.start();
797				}
798			};
799			if (mq.addEventListener) {
800				mq.addEventListener('change', onChange);
801			} else if (mq.addListener) {
802				mq.addListener(onChange);
803			}
804		}
805	};
806
807	Letreiro.prototype.destroy = function () {
808		this.destroyed = true;
809		this.stop();
810		window.clearInterval(this.clockTimer);
811		window.clearInterval(this.refreshTimer);
812		if (this.io) {
813			this.io.disconnect();
814		}
815	};
816
817	/* ------------------------------------------------------------------ */
818	/* Inicialização                                                       */
819	/* ------------------------------------------------------------------ */
820
821	function boot() {
822		var nodes = document.querySelectorAll('.tl-letreiro');
823		for (var i = 0; i < nodes.length; i++) {
824			if (nodes[i].__tlInstance) {
825				continue;
826			}
827			try {
828				nodes[i].__tlInstance = new Letreiro(nodes[i]);
829			} catch (e) {
830				nodes[i].setAttribute('data-tl-pronto', '1');
831				if (window.console && window.console.warn) {
832					window.console.warn('[Letreiro Tuiuti]', e);
833				}
834			}
835		}
836	}
837
838	if (document.readyState === 'loading') {
839		document.addEventListener('DOMContentLoaded', boot);
840	} else {
841		boot();
842	}
843
844	window.TuiutiLetreiro = { boot: boot };
845})();

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.