PageSourceSearch

https://extremos.com.br/painel/publi/gerenciador.js?v=20260812-1

js extremos.com.br collected 2026-10-02 11:37:27 UTC 29,821 bytes, 777 lines download raw bytes

1(function () {
2  "use strict";
3
4  if (window.ExtremosAdsManagerStarted) return;
5  window.ExtremosAdsManagerStarted = true;
6
7  const CONFIG_URL = "/painel/publi/config.js";
8  const AD_ITEM_SELECTOR = "[data-billboard], [data-trip-card], [data-book-ad], [data-book], [data-cta]";
9  const LEGACY_SLOT_SELECTORS = {
10    billboard: "[data-billboard-fragment]",
11    travel: "[data-trip-fragment]",
12    book: "[data-book-ad-fragment]",
13    books: "[data-store-fragment]",
14    cta: "[data-cta-fragment]"
15  };
16  const DEFAULT_BODY_SELECTORS = [
17    ".news-article",
18    ".news-article-body",
19    ".news-content",
20    "#texto-noticia",
21    ".page-content"
22  ];
23  const claimedLegacySlots = new WeakSet();
24  const trackedImpressions = new WeakSet();
25  const registeredFrequencySlots = new WeakSet();
26  const placementByElement = new WeakMap();
27
28  function loadManagerConfig() {
29    if (window.ExtremosAdsConfig) return Promise.resolve(window.ExtremosAdsConfig);
30
31    return new Promise((resolve) => {
32      const script = document.createElement("script");
33      script.src = `${CONFIG_URL}?t=${Date.now()}`;
34      script.async = true;
35      script.onload = () => resolve(window.ExtremosAdsConfig || null);
36      script.onerror = () => resolve(null);
37      document.head.appendChild(script);
38    });
39  }
40
41  function normalizeToken(value) {
42    return String(value || "")
43      .normalize("NFD")
44      .replace(/[\u0300-\u036f]/g, "")
45      .toLowerCase()
46      .trim()
47      .replace(/[^a-z0-9]+/g, "-")
48      .replace(/^-+|-+$/g, "");
49  }
50
51  function splitTokens(value) {
52    if (Array.isArray(value)) return value.map(normalizeToken).filter(Boolean);
53    return String(value || "")
54      .split(",")
55      .map(normalizeToken)
56      .filter(Boolean);
57  }
58
59  function getPageCategories(settings) {
60    const categories = new Set();
61    const selectors = settings.categorySelectors || [];
62
63    selectors.forEach((selector) => {
64      document.querySelectorAll(selector).forEach((element) => {
65        const value = element.tagName === "META"
66          ? element.getAttribute("content")
67          : element.textContent;
68        splitTokens(value).forEach((category) => categories.add(category));
69      });
70    });
71
72    splitTokens(document.body.getAttribute("data-category")).forEach((category) => {
73      categories.add(category);
74    });
75
76    return Array.from(categories);
77  }
78
79  function matchesCategories(requiredCategories, pageCategories) {
80    const required = splitTokens(requiredCategories);
81    if (!required.length || required.includes("*") || required.includes("all")) return true;
82    return required.some((category) => pageCategories.includes(category));
83  }
84
85  function getPageChannel() {
86    const path = window.location.pathname.toLowerCase();
87    if (path === "/" || path === "/index.html") return "home";
88    if (path.startsWith("/blog/")) return "blog";
89    if (path.startsWith("/podcast/")) return "podcast";
90    if (path.startsWith("/online/")) return "online";
91    if (path.startsWith("/cicloviagem/")) return "cicloviagem";
92    if (path.startsWith("/expedicao/")) return "expedicao";
93    if (path.startsWith("/noticia/") && document.body.classList.contains("news-page")) {
94      return "modernNews";
95    }
96    if (path.startsWith("/noticia/")) return "legacyNews";
97    return null;
98  }
99
100  function isCoverageOpen(config) {
101    const channel = getPageChannel();
102    if (!channel) return false;
103    const coverage = config?.coverage?.[channel];
104    return !coverage || coverage.status === "open";
105  }
106
107  function isInsideDateRange(start, end, now = Date.now()) {
108    const startTime = start ? Date.parse(start) : Number.NaN;
109    const endTime = end ? Date.parse(end) : Number.NaN;
110    if (!Number.isNaN(startTime) && now < startTime) return false;
111    if (!Number.isNaN(endTime) && now > endTime) return false;
112    return true;
113  }
114
115  function getFrequencyStorage(frequency) {
116    if (!frequency || !Number.isFinite(Number(frequency.limit))) return null;
117    try {
118      return frequency.period === "day" ? window.localStorage : window.sessionStorage;
119    } catch (error) {
120      return null;
121    }
122  }
123
124  function getFrequencyKey(placement) {
125    const frequency = placement.frequency;
126    const day = frequency && frequency.period === "day"
127      ? `:${new Date().toISOString().slice(0, 10)}`
128      : "";
129    return `extremos-ad-frequency:${placement.id}${day}`;
130  }
131
132  function isFrequencyAllowed(placement) {
133    const frequency = placement.frequency;
134    if (!frequency || Number(frequency.limit) <= 0) return true;
135    const storage = getFrequencyStorage(frequency);
136    if (!storage) return true;
137    const count = Number(storage.getItem(getFrequencyKey(placement))) || 0;
138    return count < Number(frequency.limit);
139  }
140
141  function registerFrequencyImpression(placement) {
142    const frequency = placement.frequency;
143    if (!frequency || Number(frequency.limit) <= 0) return;
144    const storage = getFrequencyStorage(frequency);
145    if (!storage) return;
146    const key = getFrequencyKey(placement);
147    const count = Number(storage.getItem(key)) || 0;
148    storage.setItem(key, String(count + 1));
149  }
150
151  function findArticleBody(settings) {
152    if (getPageChannel() === "home") {
153      return document.getElementById("home-publicidade");
154    }
155    const selectors = settings.bodySelectors || DEFAULT_BODY_SELECTORS;
156    for (const selector of selectors) {
157      const body = document.querySelector(selector);
158      if (body) return body;
159    }
160    return document.querySelector("main article, article, main");
161  }
162
163  function isEditorialParagraph(paragraph, body, settings) {
164    if (!paragraph || !body.contains(paragraph)) return false;
165    if ((paragraph.textContent || "").trim().length < (settings.minimumParagraphCharacters || 40)) {
166      return false;
167    }
168
169    const exclusions = settings.excludedParagraphSelectors || [];
170    if (exclusions.some((selector) => paragraph.matches(selector) || paragraph.closest(selector))) {
171      return false;
172    }
173
174    if (body.classList.contains("page-content")) {
175      return paragraph.classList.contains("texto2") || !paragraph.hasAttribute("class");
176    }
177
178    return true;
179  }
180
181  function getEditorialParagraphs(body, settings) {
182    if (!body) return [];
183
184    let candidates;
185    if (body.classList.contains("news-article")) {
186      candidates = body.querySelectorAll(settings.modernParagraphSelector || ".news-copy p");
187    } else if (body.classList.contains("page-content")) {
188      candidates = body.querySelectorAll(settings.legacyParagraphSelector || "p.texto2, p:not([class])");
189    } else {
190      candidates = body.querySelectorAll("p");
191    }
192
193    return Array.from(candidates).filter((paragraph) => {
194      return isEditorialParagraph(paragraph, body, settings);
195    });
196  }
197
198  function getEditorialSections(body, settings) {
199    if (!body || !body.classList.contains("news-article")) return [];
200    const selector = settings.modernSectionSelector || ".news-copy > .news-section";
201    return Array.from(body.querySelectorAll(selector)).filter((section) => {
202      if (!body.contains(section)) return false;
203      return !section.closest("[data-extremos-ad-slot], .news-comments, [data-extremos-comments]");
204    });
205  }
206
207  function findTopAnchor(body, placement) {
208    if (placement.selector) {
209      const configured = document.querySelector(placement.selector);
210      if (configured) return configured;
211    }
212    return document.querySelector(".news-hero, .home-full.main-slide") || body;
213  }
214
215  function insertAfter(anchor, element) {
216    if (!anchor || !anchor.parentNode) return false;
217    anchor.parentNode.insertBefore(element, anchor.nextSibling);
218    return true;
219  }
220
221  function insertBefore(anchor, element) {
222    if (!anchor || !anchor.parentNode) return false;
223    anchor.parentNode.insertBefore(element, anchor);
224    return true;
225  }
226
227  function getParagraphAnchor(paragraph, body, paragraphs, direction) {
228    if (!paragraph || !body.classList.contains("news-article")) return paragraph;
229    const copyBlock = paragraph.closest(".news-copy");
230    if (!copyBlock) return paragraph;
231    const blockParagraphs = paragraphs.filter((candidate) => copyBlock.contains(candidate));
232    if (direction === "after" && paragraph === blockParagraphs[blockParagraphs.length - 1]) {
233      return copyBlock;
234    }
235    if (direction === "before" && paragraph === blockParagraphs[0]) return copyBlock;
236    return paragraph;
237  }
238
239  function getSectionAnchor(section, body, direction) {
240    if (!section || !body.classList.contains("news-article")) return section;
241    const copyBlock = section.closest(".news-copy");
242    if (!copyBlock) return section;
243    if (direction === "after") return copyBlock;
244    return copyBlock;
245  }
246
247  function findCommentsAnchor(body, settings) {
248    const selector = settings.commentsSelector
249      || ".news-comments, [data-extremos-comments], #disqus_thread";
250    const anchor = body.querySelector(selector);
251    if (!anchor) return null;
252    return anchor.matches("#disqus_thread")
253      ? anchor.closest(".news-comments, [data-extremos-comments], section") || anchor
254      : anchor;
255  }
256
257  function placeAtArticleEnd(slot, body, settings) {
258    const comments = findCommentsAnchor(body, settings);
259    if (comments) return insertBefore(comments, slot);
260    body.appendChild(slot);
261    return true;
262  }
263
264  function placeSlot(slot, placement, context) {
265    if (slot.hasAttribute("data-ad-home-slot")) return true;
266    const { body, paragraphs, sections, settings } = context;
267    const paragraphNumber = Math.max(1, Number.parseInt(placement.paragraph, 10) || 1);
268    const sectionNumber = Math.max(1, Number.parseInt(placement.section, 10) || 1);
269    const requestedParagraph = paragraphs[paragraphNumber - 1];
270    const finalParagraph = paragraphs[paragraphs.length - 1];
271    const requestedSection = sections[sectionNumber - 1];
272    const finalSection = sections[sections.length - 1];
273    let anchor;
274
275    switch (placement.position) {
276      case "after-intro":
277        anchor = document.querySelector(".news-intro")
278          || document.querySelector(".news-intro__lead");
279        return anchor ? insertAfter(anchor, slot) : insertBefore(body, slot);
280
281      case "top":
282        anchor = findTopAnchor(body, placement);
283        return anchor === body ? insertBefore(body, slot) : insertAfter(anchor, slot);
284
285      case "middle":
286        anchor = paragraphs[Math.floor((paragraphs.length - 1) / 2)] || body;
287        anchor = getParagraphAnchor(anchor, body, paragraphs, "after");
288        return anchor === body ? insertBefore(body, slot) : insertAfter(anchor, slot);
289
290      case "end":
291      case "before-comments":
292        return placeAtArticleEnd(slot, body, settings);
293
294      case "after-section":
295        anchor = requestedSection || finalSection;
296        anchor = getSectionAnchor(anchor, body, "after");
297        return anchor ? insertAfter(anchor, slot) : placeAtArticleEnd(slot, body, settings);
298
299      case "before-section":
300        anchor = requestedSection || finalSection;
301        anchor = getSectionAnchor(anchor, body, "before");
302        return anchor ? insertBefore(anchor, slot) : insertBefore(body, slot);
303
304      case "after-paragraph":
305        anchor = requestedParagraph || finalParagraph;
306        anchor = getParagraphAnchor(anchor, body, paragraphs, "after");
307        return anchor ? insertAfter(anchor, slot) : insertAfter(body, slot);
308
309      case "before-paragraph":
310        anchor = requestedParagraph || finalParagraph;
311        anchor = getParagraphAnchor(anchor, body, paragraphs, "before");
312        return anchor ? insertBefore(anchor, slot) : insertBefore(body, slot);
313
314      case "after-selector":
315        anchor = placement.selector ? document.querySelector(placement.selector) : null;
316        return anchor ? insertAfter(anchor, slot) : (finalParagraph ? insertAfter(finalParagraph, slot) : false);
317
318      case "before-selector":
319        anchor = placement.selector ? document.querySelector(placement.selector) : null;
320        return anchor ? insertBefore(anchor, slot) : (finalParagraph ? insertBefore(finalParagraph, slot) : false);
321
322      case "sidebar-right":
323        slot.classList.add("news-store-rail--right");
324        return insertBefore(body, slot);
325
326      case "sidebar-left":
327        slot.classList.add("news-store-rail--left");
328        return insertBefore(body, slot);
329
330      default:
331        return false;
332    }
333  }
334
335  function findLegacySlot(type) {
336    const selector = LEGACY_SLOT_SELECTORS[type];
337    if (!selector) return null;
338    return Array.from(document.querySelectorAll(selector)).find((slot) => {
339      return !claimedLegacySlots.has(slot);
340    }) || null;
341  }
342
343  function createOrReuseSlot(placement) {
344    let slot = document.querySelector(`[data-extremos-ad-slot="${placement.id}"]`);
345    if (!slot) slot = findLegacySlot(placement.type);
346    if (!slot) slot = document.createElement(placement.type === "cta" ? "div" : "aside");
347
348    claimedLegacySlots.add(slot);
349    slot.replaceChildren();
350    slot.className = placement.className || "";
351    slot.setAttribute("data-extremos-ad-slot", placement.id);
352    slot.setAttribute("data-ad-type", placement.type);
353    slot.setAttribute("data-priority", String(placement.priority || 0));
354    slot.setAttribute("aria-label", placement.ariaLabel || "Publicidade");
355    slot.setAttribute("aria-busy", "true");
356    slot.hidden = true;
357    placementByElement.set(slot, placement);
358    return slot;
359  }
360
361  function removeUnusedLegacySlots(activeSlots) {
362    Object.values(LEGACY_SLOT_SELECTORS).forEach((selector) => {
363      document.querySelectorAll(selector).forEach((slot) => {
364        if (!activeSlots.has(slot)) slot.remove();
365      });
366    });
367  }
368
369  function getItemId(item, type) {
370    return item.getAttribute("data-ad-id")
371      || `${type}:${item.getAttribute("data-billboard")
372        || item.getAttribute("data-trip-card")
373        || item.getAttribute("data-book-ad")
374        || item.getAttribute("data-book")
375        || item.getAttribute("data-cta")
376        || "unknown"}`;
377  }
378
379  function getCreativeStatus(item, placement) {
380    const id = item.getAttribute("data-ad-id");
381    const override = Array.isArray(placement?.creatives)
382      ? placement.creatives.find((creative) => creative.id === id)
383      : null;
384    return override ? override.status : item.getAttribute("data-status");
385  }
386
387  function isCampaignEligible(item, pageCategories, placement) {
388    if (getCreativeStatus(item, placement) !== "on") return false;
389    if (!matchesCategories(item.getAttribute("data-categories") || ["*"], pageCategories)) return false;
390    return isInsideDateRange(
391      item.getAttribute("data-start"),
392      item.getAttribute("data-end")
393    );
394  }
395
396  function randomNumber() {
397    if (window.crypto && window.crypto.getRandomValues) {
398      return window.crypto.getRandomValues(new Uint32Array(1))[0] / 4294967296;
399    }
400    return Math.random();
401  }
402
403  function weightedPick(items) {
404    if (!items.length) return null;
405    const weights = items.map((item) => Math.max(0, Number(item.getAttribute("data-weight")) || 1));
406    const total = weights.reduce((sum, weight) => sum + weight, 0);
407    if (total <= 0) return items[Math.floor(randomNumber() * items.length)];
408
409    let cursor = randomNumber() * total;
410    for (let index = 0; index < items.length; index += 1) {
411      cursor -= weights[index];
412      if (cursor <= 0) return items[index];
413    }
414    return items[items.length - 1];
415  }
416
417  function weightedSelection(items, count) {
418    const pool = items.slice();
419    const selected = [];
420    while (pool.length && selected.length < count) {
421      const item = weightedPick(pool);
422      selected.push(item);
423      pool.splice(pool.indexOf(item), 1);
424    }
425    return selected;
426  }
427
428  function selectFixedItems(items, fixedValue, count, attribute) {
429    const fixedIds = String(fixedValue || "")
430      .split(",")
431      .map((id) => id.trim())
432      .filter(Boolean);
433    const selected = [];
434
435    fixedIds.forEach((id) => {
436      const item = items.find((candidate) => candidate.getAttribute(attribute) === id);
437      if (item && !selected.includes(item) && selected.length < count) selected.push(item);
438    });
439
440    items.forEach((item) => {
441      if (!selected.includes(item) && selected.length < count) selected.push(item);
442    });
443    return selected;
444  }
445
446  function parseFragment(html) {
447    const template = document.createElement("template");
448    template.innerHTML = html.trim();
449    return template.content;
450  }
451
452  function renderStoreFragment(fragment, placement, pageCategories) {
453    const config = fragment.querySelector("[data-store-config]");
454    if (!config) return null;
455
456    const inner = config.querySelector(".news-store-rail__inner");
457    const list = config.querySelector(".news-store-rail__list");
458    const items = Array.from(config.querySelectorAll("[data-book]")).filter((item) => {
459      return isCampaignEligible(item, pageCategories, placement);
460    });
461    const count = Math.max(1, Math.min(Number.parseInt(config.getAttribute("data-count"), 10) || 3, items.length));
462    if (!inner || !list || !items.length) return null;
463
464    const selected = config.getAttribute("data-mode") === "fixed"
465      ? selectFixedItems(items, config.getAttribute("data-fixed"), count, "data-book")
466      : weightedSelection(items, count);
467
468    const rendered = inner.cloneNode(true);
469    const renderedList = rendered.querySelector(".news-store-rail__list");
470    renderedList.replaceChildren(...selected.map((item) => item.cloneNode(true)));
471    return rendered;
472  }
473
474  function renderSingleFragment(fragment, placement, pageCategories) {
475    const definitions = {
476      billboard: {
477        config: "[data-billboard-config]",
478        item: "[data-billboard]",
479        attribute: "data-billboard"
480      },
481      travel: {
482        config: "[data-trip-config]",
483        item: "[data-trip-card]",
484        attribute: "data-trip-card"
485      },
486      book: {
487        config: "[data-book-ad-config]",
488        item: "[data-book-ad]",
489        attribute: "data-book-ad"
490      },
491      cta: {
492        config: "[data-cta-config]",
493        item: "[data-cta]",
494        attribute: "data-cta"
495      }
496    };
497    const definition = definitions[placement.type];
498    if (!definition) return null;
499
500    const config = fragment.querySelector(definition.config);
501    if (!config) return null;
502
503    let items;
504    if (placement.type === "cta") {
505      const activeItems = Array.from(config.querySelectorAll(definition.item)).filter((item) => {
506        if (getCreativeStatus(item, placement) !== "on") return false;
507        return isInsideDateRange(item.getAttribute("data-start"), item.getAttribute("data-end"));
508      });
509      const segmentItems = activeItems.filter((item) => {
510        const categories = splitTokens(item.getAttribute("data-categories"));
511        return categories.some((category) => {
512          return category !== "all" && category !== "*" && pageCategories.includes(category);
513        });
514      });
515      const allItems = activeItems.filter((item) => {
516        const categories = splitTokens(item.getAttribute("data-categories"));
517        return categories.includes("all") || categories.includes("*") || !categories.length;
518      });
519      items = segmentItems.length ? segmentItems : allItems;
520    } else {
521      items = Array.from(config.querySelectorAll(definition.item)).filter((item) => {
522        return isCampaignEligible(item, pageCategories, placement);
523      });
524    }
525    if (!items.length) return null;
526
527    let selected;
528    if (config.getAttribute("data-mode") === "fixed") {
529      selected = items.find((item) => {
530        return item.getAttribute(definition.attribute) === config.getAttribute("data-fixed");
531      }) || items[0];
532    } else {
533      selected = weightedPick(items);
534    }
535    return selected.cloneNode(true);
536  }
537
538  async function loadAndRenderPlacement(slot, placement, pageCategories, ga4Settings) {
539    try {
540      const response = await window.fetch(placement.fragment, {
541        credentials: "same-origin",
542        cache: "no-store"
543      });
544      if (!response.ok) throw new Error(`Falha ao carregar ${placement.id}`);
545
546      const fragment = parseFragment(await response.text());
547      const content = placement.type === "books"
548        ? renderStoreFragment(fragment, placement, pageCategories)
549        : renderSingleFragment(fragment, placement, pageCategories);
550
551      if (!content) {
552        slot.remove();
553        return;
554      }
555
556      slot.replaceChildren(content);
557      slot.hidden = false;
558      slot.removeAttribute("aria-busy");
559      activateAdTracking(slot, placement, pageCategories, ga4Settings);
560    } catch (error) {
561      slot.remove();
562    }
563  }
564
565  function getAdMetadata(advertisement, placement, pageCategories) {
566    const type = placement?.type || advertisement.closest("[data-ad-type]")?.getAttribute("data-ad-type") || "ad";
567    const slot = advertisement.closest("[data-extremos-ad-slot]");
568    const placementId = placement?.id || slot?.getAttribute("data-extremos-ad-slot") || "unknown-placement";
569    const title = advertisement.querySelector("h3, strong");
570    const image = advertisement.querySelector("img");
571    const name = title?.textContent.trim()
572      || advertisement.getAttribute("aria-label")
573      || getItemId(advertisement, type);
574
575    return {
576      id: getItemId(advertisement, type),
577      name,
578      type,
579      placementId,
580      position: placement?.position || "legacy",
581      priority: Number(placement?.priority || 0),
582      variant: advertisement.getAttribute("data-variant") || "default",
583      creative: image?.getAttribute("alt") || name,
584      categories: pageCategories || []
585    };
586  }
587
588  function sendGa4Promotion(eventName, advertisement, placement, pageCategories) {
589    if (typeof window.gtag !== "function") return;
590    const metadata = getAdMetadata(advertisement, placement, pageCategories);
591
592    window.gtag("event", eventName, {
593      promotion_id: metadata.id,
594      promotion_name: metadata.name,
595      creative_name: metadata.creative,
596      creative_slot: metadata.placementId,
597      placement_id: metadata.placementId,
598      placement_position: metadata.position,
599      ad_type: metadata.type,
600      ad_variant: metadata.variant,
601      ad_priority: metadata.priority,
602      page_categories: metadata.categories.join(","),
603      page_path: window.location.pathname,
604      items: [{
605        item_id: metadata.id,
606        item_name: metadata.name,
607        item_category: metadata.type,
608        item_category2: metadata.variant,
609        item_list_id: metadata.placementId,
610        item_list_name: metadata.position
611      }]
612    });
613  }
614
615  function addExternalCampaignParameters(advertisement, placement, pageCategories) {
616    const metadata = getAdMetadata(advertisement, placement, pageCategories);
617    advertisement.querySelectorAll("a[href]").forEach((link) => {
618      let destination;
619      try {
620        destination = new URL(link.href, window.location.href);
621      } catch (error) {
622        return;
623      }
624
625      if (destination.origin === window.location.origin || !/^https?:$/.test(destination.protocol)) return;
626      if (!destination.searchParams.has("utm_source")) destination.searchParams.set("utm_source", "extremos");
627      if (!destination.searchParams.has("utm_medium")) destination.searchParams.set("utm_medium", "display");
628      if (!destination.searchParams.has("utm_campaign")) destination.searchParams.set("utm_campaign", metadata.id);
629      if (!destination.searchParams.has("utm_content")) destination.searchParams.set("utm_content", metadata.placementId);
630      link.href = destination.toString();
631    });
632  }
633
634  function observeAdImpression(advertisement, placement, pageCategories, ga4Settings) {
635    if (advertisement.getAttribute("data-ga4-ready") === "true") return;
636    advertisement.setAttribute("data-ga4-ready", "true");
637    addExternalCampaignParameters(advertisement, placement, pageCategories);
638
639    const register = () => {
640      if (trackedImpressions.has(advertisement)) return;
641      trackedImpressions.add(advertisement);
642      const slot = advertisement.closest("[data-extremos-ad-slot]") || advertisement;
643      if (!registeredFrequencySlots.has(slot)) {
644        registeredFrequencySlots.add(slot);
645        registerFrequencyImpression(placement);
646      }
647      if (ga4Settings.enabled !== false) {
648        sendGa4Promotion("view_promotion", advertisement, placement, pageCategories);
649      }
650    };
651
652    if (!("IntersectionObserver" in window)) {
653      register();
654      return;
655    }
656
657    const ratio = Number(ga4Settings.visibleRatio) || 0.5;
658    const duration = Number(ga4Settings.visibleMilliseconds) || 1000;
659    let visibleTimer = 0;
660    const observer = new IntersectionObserver((entries) => {
661      entries.forEach((entry) => {
662        if (entry.intersectionRatio >= ratio) {
663          if (visibleTimer) return;
664          visibleTimer = window.setTimeout(() => {
665            visibleTimer = 0;
666            register();
667            observer.unobserve(advertisement);
668          }, duration);
669        } else if (visibleTimer) {
670          window.clearTimeout(visibleTimer);
671          visibleTimer = 0;
672        }
673      });
674    }, { threshold: [0, ratio, 1] });
675
676    observer.observe(advertisement);
677  }
678
679  function activateAdTracking(container, placement, pageCategories, ga4Settings = {}) {
680    const advertisements = [];
681    if (container.matches(AD_ITEM_SELECTOR)) advertisements.push(container);
682    advertisements.push(...container.querySelectorAll(AD_ITEM_SELECTOR));
683    advertisements.forEach((advertisement) => {
684      placementByElement.set(advertisement, placement);
685      observeAdImpression(advertisement, placement, pageCategories, ga4Settings);
686    });
687  }
688
689  function initializeAdClickTracking(pageCategories, ga4Settings) {
690    document.addEventListener("click", (event) => {
691      const link = event.target.closest?.("a[href]");
692      const advertisement = link?.closest?.(AD_ITEM_SELECTOR);
693      if (!advertisement) return;
694      const placement = placementByElement.get(advertisement)
695        || placementByElement.get(advertisement.closest("[data-extremos-ad-slot]"));
696      if (!placement || ga4Settings.enabled === false) return;
697      sendGa4Promotion("select_promotion", advertisement, placement, pageCategories);
698    });
699  }
700
701  function isPlacementActive(placement, pageCategories, channel) {
702    if (!placement?.id || !placement.fragment || placement.status !== "open") return false;
703    if (Array.isArray(placement.channels) && !placement.channels.includes(channel)) return false;
704    if (!matchesCategories(placement.categories, pageCategories)) return false;
705    if (!isInsideDateRange(placement.start, placement.end)) return false;
706    return isFrequencyAllowed(placement);
707  }
708
709  function resolvePlacementForViewport(placement) {
710    const viewport = window.matchMedia("(max-width: 1279px)").matches ? "mobile" : "desktop";
711    const responsiveRule = placement && typeof placement[viewport] === "object"
712      ? placement[viewport]
713      : null;
714    return {
715      ...placement,
716      ...(responsiveRule || {}),
717      viewport
718    };
719  }
720
721  async function initializeAdManager(config) {
722    if (!config || !Array.isArray(config.placements)) {
723      removeUnusedLegacySlots(new Set());
724      return;
725    }
726
727    if (!isCoverageOpen(config)) {
728      removeUnusedLegacySlots(new Set());
729      document.dispatchEvent(new CustomEvent("extremos:ads-disabled", {
730        detail: { channel: getPageChannel(), version: config.version }
731      }));
732      return;
733    }
734
735    const settings = config.settings || {};
736    const body = findArticleBody(settings);
737    if (!body) return;
738
739    const pageCategories = getPageCategories(settings);
740    const ga4Settings = settings.ga4 || {};
741    const paragraphs = getEditorialParagraphs(body, settings);
742    const sections = getEditorialSections(body, settings);
743    const activeSlots = new Set();
744    const activePlacements = config.placements
745      .map(resolvePlacementForViewport)
746      .filter((placement) => isPlacementActive(placement, pageCategories, getPageChannel()))
747      .sort((first, second) => Number(first.priority || 0) - Number(second.priority || 0));
748
749    const jobs = [];
750    activePlacements.forEach((placement) => {
751      const slot = createOrReuseSlot(placement);
752      placementByElement.set(slot, placement);
753      if (!placeSlot(slot, placement, { body, paragraphs, sections, settings })) {
754        slot.remove();
755        return;
756      }
757      activeSlots.add(slot);
758      jobs.push(loadAndRenderPlacement(slot, placement, pageCategories, ga4Settings));
759    });
760
761    removeUnusedLegacySlots(activeSlots);
762    initializeAdClickTracking(pageCategories, ga4Settings);
763    await Promise.all(jobs);
764
765    document.dispatchEvent(new CustomEvent("extremos:ads-ready", {
766      detail: {
767        version: config.version,
768        categories: pageCategories,
769        paragraphs: paragraphs.length,
770        sections: sections.length,
771        placements: activePlacements.map((placement) => placement.id)
772      }
773    }));
774  }
775
776  loadManagerConfig().then(initializeAdManager);
777}());

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.