PageSourceSearch

https://www.itnetwork.rs/wp-content/plugins/clsop/assets/js/wpr-beacon.js

js itnetwork.rs collected 2026-10-02 10:11:43 UTC 64,458 bytes, 1,588 lines download raw bytes

vendor: 18 bytes, lines 1-2
1(() => {
2  // src/
2Utils.js
3  var BeaconUtils = class {
4    static getScreenWidth() {
5      return window.innerWidth || document.documentElement.clientWidth;
6    }
7    static getScreenHeight() {
8      return window.innerHeight || document.documentElement.clientHeight;
9    }
10    static isNotValidScreensize(is_mobile, threshold) {
11      const screenWidth = this.getScreenWidth();
12      const screenHeight = this.getScreenHeight();
13      const isNotValidForMobile = is_mobile && (screenWidth > threshold.width || screenHeight > threshold.height);
14      const isNotValidForDesktop = !is_mobile && (screenWidth < threshold.width || screenHeight < threshold.height);
15      return isNotValidForMobile || isNotValidForDesktop;
16    }
17    static isPageCached() {
18      const signature = document.documentElement.nextSibling && document.documentElement.nextSibling.data ? document.documentElement.nextSibling.data : "";
19      return signature && signature.includes("Debug: cached");
20    }
21    static isIntersecting(rect) {
22      return rect.bottom >= 0 && rect.right >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) && rect.left <= (window.innerWidth || document.documentElement.clientWidth);
23    }
24    static isPageScrolled() {
25      return window.pageYOffset > 0 || document.documentElement.scrollTop > 0;
26    }
27    /**
28     * Checks if an element is visible in the viewport.
29     *
30     * This method checks if the provided element is visible in the viewport by
31     * considering its display, visibility, opacity, width, and height properties.
32     * It also excludes elements with transparent text properties.
33     * It returns true if the element is visible, and false otherwise.
34     *
35     * @param {Element} element - The element to check for visibility.
36     * @returns {boolean} True if the element is visible, false otherwise.
37     */
38    static isElementVisible(element) {
39      const style = window.getComputedStyle(element);
40      const rect = element.getBoundingClientRect();
41      if (!style) {
42        return false;
43      }
44      if (this.hasTransparentText(element)) {
45        return false;
46      }
47      return !(style.display === "none" || style.visibility === "hidden" || style.opacity === "0" || rect.width === 0 || rect.height === 0);
48    }
49    /**
50     * Checks if an element has transparent text properties.
51     *
52     * This method checks for specific CSS properties that make text invisible,
53     * such as `color: transparent`, `color: rgba(..., 0)`, `color: hsla(..., 0)`,
54     * `color: #...00` (8-digit hex with alpha = 0), and `filter: opacity(0)`.
55     *
56     * @param {Element} element - The element to check.
57     * @returns {boolean} True if the element has transparent text properties, false otherwise.
58     */
59    static hasTransparentText(element) {
60      const style = window.getComputedStyle(element);
61      if (!style) {
62        return false;
63      }
64      const color = style.color || "";
65      const filter = style.filter || "";
66      if (color === "transparent") {
67        return true;
68      }
69      const rgbaMatch = color.match(/rgba\(\d+,\s*\d+,\s*\d+,\s*0\)/);
70      if (rgbaMatch) {
71        return true;
72      }
73      const hslaMatch = color.match(/hsla\(\d+,\s*\d+%,\s*\d+%,\s*0\)/);
74      if (hslaMatch) {
75        return true;
76      }
77      const hexMatch = color.match(/#[0-9a-fA-F]{6}00/);
78      if (hexMatch) {
79        return true;
80      }
81      if (filter.includes("opacity(0)")) {
82        return true;
83      }
84      return false;
85    }
86  };
87  var Utils_default = BeaconUtils;
88
89  
89// src/BeaconLcp.js
90  var BeaconLcp = class {
91    constructor(config, logger) {
92      this.config = config;
93      this.performanceImages = [];
94      this.logger = logger;
95    }
96    async run() {
97      try {
98        const above_the_fold_images = this._generateLcpCandidates(Infinity);
99        if (above_the_fold_images) {
100          this._initWithFirstElementWithInfo(above_the_fold_images);
101          this._fillATFWithoutDuplications(above_the_fold_images);
102        }
103      } catch (err) {
104        this.errorCode = "script_error";
105        this.logger.logMessage("Script Error: " + err);
106      }
107    }
108    _generateLcpCandidates(count) {
109      const lcpElements = document.querySelectorAll(this.config.elements);
110      if (lcpElements.length <= 0) {
111        return [];
112      }
113      const potentialCandidates = Array.from(lcpElements);
114      const topCandidates = potentialCandidates.map((element) => {
115        if ("img" === element.nodeName.toLowerCase() && "picture" === element.parentElement.nodeName.toLowerCase()) {
116          return null;
117        }
118        let rect;
119        if ("picture" === element.nodeName.toLowerCase()) {
120          const imgElement = element.querySelector("img");
121          if (imgElement) {
122            rect = imgElement.getBoundingClientRect();
123          } else {
124            return null;
125          }
126        } else {
127          rect = element.getBoundingClientRect();
128        }
129        return {
130          element,
131          rect
132        };
133      }).filter((item) => item !== null).filter((item) => {
134        return item.rect.width > 0 && item.rect.height > 0 && Utils_default.isIntersecting(item.rect) && Utils_default.isElementVisible(item.element);
135      }).map((item) => ({
136        item,
137        area: this._getElementArea(item.rect),
138        elementInfo: this._getElementInfo(item.element)
139      })).sort((a, b) => b.area - a.area).slice(0, count);
140      return topCandidates.map((candidate) => ({
141        element: candidate.item.element,
142        elementInfo: candidate.elementInfo
143      }));
144    }
145    _getElementArea(rect) {
146      const visibleWidth = Math.min(rect.width, (window.innerWidth || document.documentElement.clientWidth) - rect.left);
147      const visibleHeight = Math.min(rect.height, (window.innerHeight || document.documentElement.clientHeight) - rect.top);
148      return visibleWidth * visibleHeight;
149    }
150    _getElementInfo(element) {
151      const nodeName = element.nodeName.toLowerCase();
152      const element_info = {
153        type: "",
154        src: "",
155        srcset: "",
156        sizes: "",
157        sources: [],
158        bg_set: [],
159        current_src: ""
160      };
161      const css_bg_url_rgx = /url\(\s*?['"]?\s*?(.+?)\s*?["']?\s*?\)/ig;
162      if (nodeName === "img" && element.srcset) {
163        element_info.type = "img-srcset";
164        element_info.src = element.src;
165        element_info.srcset = element.srcset;
166        element_info.sizes = element.sizes;
167        element_info.current_src = element.currentSrc;
168      } else if (nodeName === "img") {
169        element_info.type = "img";
170        element_info.src = element.src;
171        element_info.current_src = element.currentSrc;
172      } else if (nodeName === "video") {
173        element_info.type = "img";
174        const source = element.querySelector("source");
175        element_info.src = element.poster || (source ? source.src : "");
176        element_info.current_src = element_info.src;
177      } else if (nodeName === "svg") {
178        const imageElement = element.querySelector("image");
179        if (imageElement) {
180          element_info.type = "img";
181          element_info.src = imageElement.getAttribute("href") || "";
182          element_info.current_src = element_info.src;
183        }
184      } else if (nodeName === "picture") {
185        element_info.type = "picture";
186        const img = element.querySelector("img");
187        element_info.src = img ? img.src : "";
188        element_info.sources = Array.from(element.querySelectorAll("source")).map((source) => ({
189          srcset: source.srcset || "",
190          media: source.media || "",
191          type: source.type || "",
192          sizes: source.sizes || ""
193        }));
194      } else {
195        const computed_style = window.getComputedStyle(element, null);
196        const bg_props = [
197          computed_style.getPropertyValue("background-image"),
198          getComputedStyle(element, ":after").getPropertyValue("background-image"),
199          getComputedStyle(element, ":before").getPropertyValue("background-image")
200        ].filter((prop) => prop !== "none");
201        if (bg_props.length === 0) {
202          return null;
203        }
204        const full_bg_prop = bg_props[0];
205        element_info.type = "bg-img";
206        if (full_bg_prop.includes("image-set(")) {
207          element_info.type = "bg-img-set";
208        }
209        if (!full_bg_prop || full_bg_prop === "" || full_bg_prop.includes("data:image")) {
210          return null;
211        }
212        const matches = [...full_bg_prop.matchAll(css_bg_url_rgx)];
213        element_info.bg_set = matches.map((m) => m[1] ? { src: m[1].trim() + (m[2] ? " " + m[2].trim() : "") } : {});
214        if (element_info.bg_set.every((item) => item.src === "")) {
215          element_info.bg_set = matches.map((m) => m[1] ? { src: m[1].trim() } : {});
216        }
217        if (element_info.bg_set.length <= 0) {
218          return null;
219        }
220        if (element_info.bg_set.length > 0) {
221          element_info.src = element_info.bg_set[0].src;
222          if (element_info.type === "bg-img-set") {
223            element_info.src = element_info.bg_set;
224          }
225        }
226      }
227      return element_info;
228    }
229    _initWithFirstElementWithInfo(elements) {
230      const firstElementWithInfo = elements.find((item) => {
231        return item.elementInfo !== null && (item.elementInfo.src || item.elementInfo.srcset);
232      });
233      if (!firstElementWithInfo) {
234        this.logger.logMessage("No LCP candidate found.");
235        this.performanceImages = [];
236        return;
237      }
238      this.performanceImages = [{
239        ...firstElementWithInfo.elementInfo,
240        label: "lcp"
241      }];
242    }
243    _fillATFWithoutDuplications(elements) {
244      elements.forEach(({ element, elementInfo }) => {
245        if (this._isDuplicateImage(element) || !elementInfo) {
246          return;
247        }
248        this.performanceImages.push({ ...elementInfo, label: "above-the-fold" });
249      });
250    }
251    _isDuplicateImage(image) {
252      const elementInfo = this._getElementInfo(image);
253      if (elementInfo === null) {
254        return false;
255      }
256      const isImageOrVideo = elementInfo.type === "img" || elementInfo.type === "img-srcset" || elementInfo.type === "video";
257      const isBgImageOrPicture = elementInfo.type === "bg-img" || elementInfo.type === "bg-img-set" || elementInfo.type === "picture";
258      return (isImageOrVideo || isBgImageOrPicture) && this.performanceImages.some((item) => item.src === elementInfo.src);
259    }
260    getResults() {
261      return this.performanceImages;
262    }
263  };
264  var BeaconLcp_default = BeaconLcp;
265
266  
266// src/BeaconLrc.js
267  var BeaconLrc = class {
268    constructor(config, logger) {
269      this.config = config;
270      this.logger = logger;
271      this.lazyRenderElements = [];
272    }
273    async run() {
274      try {
275        const elementsInView = this._getLazyRenderElements();
276        if (elementsInView) {
277          this._processElements(elementsInView);
278        }
279      } catch (err) {
280        this.errorCode = "script_error";
281        this.logger.logMessage("Script Error: " + err);
282      }
283    }
284    _getLazyRenderElements() {
285      const elements = document.querySelectorAll("[data-rocket-location-hash]");
286      const svgUseTargets = this._getSvgUseTargets();
287      if (elements.length <= 0) {
288        return [];
289      }
290      const validElements = Array.from(elements).filter((element) => {
291        if (this._skipElement(element)) {
292          return false;
293        }
294        if (svgUseTargets.includes(element)) {
295          this.logger.logColoredMessage(`Element skipped because of SVG: ${element.tagName}`, "orange");
296          return false;
297        }
298        return true;
299      });
300      return validElements.map((element) => ({
301        element,
302        depth: this._getElementDepth(element),
303        distance: this._getElementDistance(element),
304        hash: this._getLocationHash(element)
305      }));
306    }
307    _getElementDepth(element) {
308      let depth = 0;
309      let parent = element.parentElement;
310      while (parent) {
311        depth++;
312        parent = parent.parentElement;
313      }
314      return depth;
315    }
316    _getElementDistance(element) {
317      const rect = element.getBoundingClientRect();
318      const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
319      return Math.max(0, rect.top + scrollTop - Utils_default.getScreenHeight());
320    }
321    _skipElement(element) {
322      const skipStrings = this.config.skipStrings || ["memex"];
323      if (!element || !element.id) return false;
324      return skipStrings.some((str) => element.id.toLowerCase().includes(str.toLowerCase()));
325    }
326    _shouldSkipElement(element, exclusions) {
327      if (!element) return false;
328      for (let i = 0; i < exclusions.length; i++) {
329        const [attribute, pattern] = exclusions[i];
330        const attributeValue = element.getAttribute(attribute);
331        if (attributeValue && new RegExp(pattern, "i").test(attributeValue)) {
332          return true;
333        }
334      }
335      return false;
336    }
337    _checkLcrConflict(element) {
338      const conflictingElements = [];
339      const computedStyle = window.getComputedStyle(element);
340      const validMargins = ["marginTop", "marginRight", "marginBottom", "marginLeft"];
341      const negativeMargins = validMargins.some((margin) => parseFloat(computedStyle[margin]) < 0);
342      const currentElementConflicts = negativeMargins || computedStyle.contentVisibility === "auto" || computedStyle.contentVisibility === "hidden";
343      if (currentElementConflicts) {
344        conflictingElements.push({
345          element,
346          conflicts: [
347            negativeMargins && "negative margin",
348            computedStyle.contentVisibility === "auto" && "content-visibility:auto",
349            computedStyle.contentVisibility === "hidden" && "content-visibility:hidden"
350          ].filter(Boolean)
351        });
352      }
353      Array.from(element.children).forEach((child) => {
354        const childStyle = window.getComputedStyle(child);
355        const validMargins2 = ["marginTop", "marginRight", "marginBottom", "marginLeft"];
356        const childNegativeMargins = validMargins2.some((margin) => parseFloat(childStyle[margin]) < 0);
357        const childConflicts = childNegativeMargins || childStyle.position === "absolute" || childStyle.position === "fixed";
358        if (childConflicts) {
359          conflictingElements.push({
360            element: child,
361            conflicts: [
362              childNegativeMargins && "negative margin",
363              childStyle.position === "absolute" && "position:absolute",
364              childStyle.position === "fixed" && "position:fixed"
365            ].filter(Boolean)
366          });
367        }
368      });
369      return conflictingElements;
370    }
371    _processElements(elements) {
372      elements.forEach(({ element, depth, distance, hash }) => {
373        if (this._shouldSkipElement(element, this.config.exclusions || [])) {
374          return;
375        }
376        if ("No hash detected" === hash) {
377          return;
378        }
379        const conflicts = this._checkLcrConflict(element);
380        if (conflicts.length > 0) {
381          this.logger.logMessage("Skipping element due to conflicts:", conflicts);
382          return;
383        }
384        const can_push_hash = element.parentElement && this._getElementDistance(element.parentElement) < this.config.lrc_threshold && distance >= this.config.lrc_threshold;
385        const color = can_push_hash ? "green" : distance === 0 ? "red" : "";
386        this.logger.logColoredMessage(`${"	".repeat(depth)}${element.tagName} (Depth: ${depth}, Distance from viewport bottom: ${distance}px)`, color);
387        this.logger.logColoredMessage(`${"	".repeat(depth)}Location hash: ${hash}`, color);
388        this.logger.logColoredMessage(`${"	".repeat(depth)}Dimensions Client Height: ${element.clientHeight}`, color);
389        if (can_push_hash) {
390          this.lazyRenderElements.push(hash);
391          this.logger.logMessage(`Element pushed with hash: ${hash}`);
392        }
393      });
394    }
395    _getXPath(element) {
396      if (element && element.id !== "") {
397        return `//*[@id="${element.id}"]`;
398      }
399      return this._getElementXPath(element);
400    }
401    _getElementXPath(element) {
402      if (element === document.body) {
403        return "/html/body";
404      }
405      const position = this._getElementPosition(element);
406      return `${this._getElementXPath(element.parentNode)}/${element.nodeName.toLowerCase()}[${position}]`;
407    }
408    _getElementPosition(element) {
409      let pos = 1;
410      let sibling = element.previousElementSibling;
411      while (sibling) {
412        if (sibling.nodeName === element.nodeName) {
413          pos++;
414        }
415        sibling = sibling.previousElementSibling;
416      }
417      return pos;
418    }
419    _getLocationHash(element) {
420      return element.hasAttribute("data-rocket-location-hash") ? element.getAttribute("data-rocket-location-hash") : "No hash detected";
421    }
422    _getSvgUseTargets() {
423      const useElements = document.querySelectorAll("use");
424      const targets = /* @__PURE__ */ new Set();
425      useElements.forEach((use) => {
426        let parent = use.parentElement;
427        while (parent && parent !== document.body) {
428          targets.add(parent);
429          parent = parent.parentElement;
430        }
431      });
432      return Array.from(targets);
433    }
434    getResults() {
435      return this.lazyRenderElements;
436    }
437  };
438  var BeaconLrc_default = BeaconLrc;
439
440  
440// src/BeaconPreloadFonts.js
441  var BeaconPreloadFonts = class {
442    constructor(config, logger) {
443      this.config = config;
444      this.logger = logger;
445      this.aboveTheFoldFonts = [];
446      const extensions = (Array.isArray(this.config.processed_extensions) && this.config.processed_extensions.length > 0 ? this.config.processed_extensions : ["woff", "woff2", "ttf"]).map((ext) => ext.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
447      this.FONT_FILE_REGEX = new RegExp(`\\.(${extensions})(\\?.*)?$`, "i");
448      this.EXCLUDED_TAG_NAMES = /* @__PURE__ */ new Set([
449        // Metadata/document head
450        "BASE",
451        "HEAD",
452        "LINK",
453        "META",
454        "STYLE",
455        "TITLE",
456        "SCRIPT",
457        // Media
458        "IMG",
459        "VIDEO",
460        "AUDIO",
461        "EMBED",
462        "OBJECT",
463        "IFRAME",
464        // Templating, wrappers, components, fallback
465        "NOSCRIPT",
466        "TEMPLATE",
467        "SLOT",
468        "CANVAS",
469        // Resources
470        "SOURCE",
471        "TRACK",
472        "PARAM",
473        // SVG references
474        "USE",
475        "SYMBOL",
476        // Layout work
477        "BR",
478        "HR",
479        "WBR",
480        // Obsolete/deprecated
481        "APPLET",
482        "ACRONYM",
483        "BGSOUND",
484        "BIG",
485        "BLINK",
486        "CENTER",
487        "FONT",
488        "FRAME",
489        "FRAMESET",
490        "MARQUEE",
491        "NOFRAMES",
492        "STRIKE",
493        "TT",
494        "U",
495        "XMP"
496      ]);
497    }
498    /**
499     * Checks if a URL should be excluded from external font processing based on domain exclusions.
500     *
501     * @param {string} url - The URL to check.
502     * @returns {boolean} True if the URL should be excluded, false otherwise.
503     */
504    isUrlExcludedFromExternalProcessing(url) {
505      if (!url) return false;
506      const externalFontExclusions = this.config.external_font_exclusions || [];
507      const preloadFontsExclusions = this.config.preload_fonts_exclusions || [];
508      const allExclusions = [...externalFontExclusions, ...preloadFontsExclusions];
509      return allExclusions.some((exclusion) => url.includes(exclusion));
510    }
511    /**
512     * Checks if a font family or URL should be excluded from preloading.
513     *
514     * This method determines if the provided font family or any of its URLs
515     * match any exclusion patterns defined in the configuration. It checks for
516     * exact matches and substring matches for both the font family and URLs.
517     *
518     * @param {string} fontFamily - The font family to check.
519     * @param {string[]} urls - Array of font file URLs to check.
520     * @returns {boolean} True if the font should be excluded, false otherwise.
521     */
522    isExcluded(fontFamily, urls) {
523      const exclusions = this.config.preload_fonts_exclusions;
524      const exclusionsSet = new Set(exclusions);
525      if (exclusionsSet.has(fontFamily)) {
526        return true;
527      }
528      if (exclusions.some((exclusion) => fontFamily.includes(exclusion))) {
529        return true;
530      }
531      if (Array.isArray(urls) && urls.length > 0) {
532        if (urls.some((url) => exclusionsSet.has(url))) {
533          return true;
534        }
535        if (urls.some(
536          (url) => exclusions.some((exclusion) => url.includes(exclusion))
537        )) {
538          return true;
539        }
540      }
541      return false;
542    }
543    /**
544     * Checks if an element can be styled with font-family.
545     *
546     * This method determines if the provided element's tag name is not in the list
547     * of excluded tag names that cannot be styled with font-family CSS property.
548     *
549     * @param {Element} element - The element to check.
550     * @returns {boolean} True if the element can be styled with font-family, false otherwise.
551     */
552    canElementBeStyledWithFontFamily(element) {
553      return !this.EXCLUDED_TAG_NAMES.has(element.tagName);
554    }
555    /**
556     * Checks if an element is visible in the viewport.
557     *
558     * This method delegates to BeaconUtils.isElementVisible() for consistent
559     * visibility checking across all beacons.
560     *
561     * @param {Element} element - The element to check for visibility.
562     * @returns {boolean} True if the element is visible, false otherwise.
563     */
564    isElementVisible(element) {
565      return Utils_default.isElementVisible(element);
566    }
567    /**
568     * Cleans a URL by removing query parameters and fragments.
569     *
570     * This method takes a URL as input, removes any query parameters and fragments,
571     * and returns the cleaned URL.
572     *
573     * @param {string} url - The URL to clean.
574     * @returns {string} The cleaned URL.
575     */
576    cleanUrl(url) {
577      try {
578        url = url.split("?")[0].split("#")[0];
579        return new URL(url, window.location.href).href;
580      } catch (e) {
581        return url;
582      }
583    }
584    /**
585     * Fetches external stylesheet links from known font providers, retrieves their CSS,
586     * parses them into in-memory CSSStyleSheet objects, and extracts font-family/font-face
587     * information into a structured object.
588     *
589     * @async
590     * @function externalStylesheetsDoc
591     * @returns {Promise<{styleSheets: CSSStyleSheet[], fontPairs: Object}>} An object containing:
592     *   - styleSheets: Array of parsed CSSStyleSheet objects (not attached to the DOM).
593     *   - fontPairs: An object mapping font URLs to arrays of font variation objects
594     *     ({family, weight, style}).
595     *
596     * @example
597     * const { styleSheets, fontPairs } = await externalStylesheetsDoc();
598     * this.logger.logMessage(fontPairs);
599     */
600    async externalStylesheetsDoc() {
601      function generateFontPairsFromStyleSheets(styleSheetsArray) {
602        const fontPairs = {};
603        function _extractFirstUrlFromSrc(srcValue) {
604          if (!srcValue) return null;
605          const urlMatch = srcValue.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/);
606          return urlMatch ? urlMatch[2] : null;
607        }
608        function _cleanFontFamilyName(fontFamilyValue) {
609          if (!fontFamilyValue) return "";
610          return fontFamilyValue.replace(/^['"]+|['"]+$/g, "").trim();
611        }
612        if (!styleSheetsArray || !Array.isArray(styleSheetsArray)) {
613          console.warn(
614            "generateFontPairsFromStyleSheets: Input is not a valid array. Received:",
615            styleSheetsArray
616          );
617          return fontPairs;
618        }
619        if (styleSheetsArray.length === 0) {
620          return fontPairs;
621        }
622        styleSheetsArray.forEach((sheet) => {
623          if (sheet && sheet.cssRules) {
624            try {
625              for (const rule of sheet.cssRules) {
626                if (rule.type === CSSRule.FONT_FACE_RULE) {
627                  const cssFontFaceRule = rule;
628                  const fontFamily = _cleanFontFamilyName(
629                    cssFontFaceRule.style.getPropertyValue("font-family")
630                  );
631                  const fontWeight = cssFontFaceRule.style.getPropertyValue("font-weight") || "normal";
632                  const fontStyle = cssFontFaceRule.style.getPropertyValue("font-style") || "normal";
633                  const src = cssFontFaceRule.style.getPropertyValue("src");
634                  const fontUrl = _extractFirstUrlFromSrc(src);
635                  if (fontFamily && fontUrl) {
636                    const variation = {
637                      family: fontFamily,
638                      weight: fontWeight,
639                      style: fontStyle
640                    };
641                    if (!fontPairs[fontUrl]) fontPairs[fontUrl] = [];
642                    const variationExists = fontPairs[fontUrl].some(
643                      (v) => v.family === variation.family && v.weight === variation.weight && v.style === variation.style
644                    );
645                    if (!variationExists) fontPairs[fontUrl].push(variation);
646                  }
647                }
648              }
649            } catch (e) {
650              console.warn(
651                "Error processing CSS rules from a stylesheet:",
652                e,
653                sheet
654              );
655            }
656          } else if (sheet && !sheet.cssRules) {
657            console.warn(
658              "Skipping a stylesheet as its cssRules are not accessible or it is empty:",
659              sheet
660            );
661          }
662        });
663        return fontPairs;
664      }
665      const links = [
666        ...document.querySelectorAll('link[rel="stylesheet"]')
667      ].filter((link) => {
668        try {
669          const linkUrl = new URL(link.href);
670          const currentUrl = new URL(window.location.href);
671          if (linkUrl.origin === currentUrl.origin) {
672            return false;
673          }
674          return !this.isUrlExcludedFromExternalProcessing(link.href);
675        } catch (e) {
676          return false;
677        }
678      });
679      if (links.length === 0) {
680        this.logger.logMessage("No external CSS links found to process.");
681        return {
682          // Consistent return structure
683          styleSheets: [],
684          // The retrievable CSSStyleSheet objects
685          fontPairs: {}
686          // Processed data from these sheets
687        };
688      }
689      const fetchedCssPromises = links.map(
690        (linkElement) => fetch(linkElement.href, { mode: "cors" }).then((response) => {
691          if (response.ok) {
692            return response.text();
693          }
694          console.warn(
695            `Failed to fetch external CSS from ${linkElement.href}: ${response.status} ${response.statusText}`
696          );
697          return null;
698        }).catch((error) => {
699          console.error(
700            `Network error fetching external CSS from ${linkElement.href}:`,
701            error
702          );
703          return null;
704        })
705      );
706      const cssTexts = await Promise.all(fetchedCssPromises);
707      const temporaryStyleSheets = [];
708      cssTexts.forEach((txt) => {
709        if (txt && txt.trim() !== "") {
710          try {
711            const sheet = new CSSStyleSheet();
712            sheet.replaceSync(txt);
713            temporaryStyleSheets.push(sheet);
714          } catch (error) {
715            console.error(
716              "Could not parse fetched CSS into a stylesheet:",
717              error,
718              `
719CSS (first 200 chars): ${txt.substring(0, 200)}...`
720            );
721          }
722        }
723      });
724      if (temporaryStyleSheets.length > 0) {
725        this.logger.logMessage(
726          `[Beacon] ${temporaryStyleSheets.length} stylesheet(s) fetched and parsed into CSSStyleSheet objects.`
727        );
728      } else {
729        this.logger.logMessage(
730          "[Beacon] No stylesheets were successfully parsed from the fetched CSS."
731        );
732      }
733      const processedFontPairs = generateFontPairsFromStyleSheets(temporaryStyleSheets);
734      return {
735        styleSheets: temporaryStyleSheets,
736        fontPairs: processedFontPairs
737      };
738    }
739    /**
740     * Asynchronously initializes and parses external font stylesheets.
741     *
742     * Fetches external font stylesheets and font pairs using `externalStylesheetsDoc`,
743     * then stores the parsed results in `externalParsedSheets` and `externalParsedPairs`.
744     * Logs the process and handles errors by resetting `externalParsedSheets` to an empty array.
745     *
746     * @async
747     * @returns {Promise<void>} Resolves when external font stylesheets have been initialized.
748     */
749    async _initializeExternalFontSheets() {
750      this.logger.logMessage("Initializing external font stylesheets...");
751      try {
752        const result = await this.externalStylesheetsDoc();
753        this.externalParsedSheets = result.styleSheets || [];
754        this.externalParsedPairs = result.fontPairs || [];
755        this.logger.logMessage(
756          `Successfully parsed ${this.externalParsedSheets.length} external font stylesheets.`
757        );
758      } catch (error) {
759        this.logger.logMessage(
760          "Error initializing external font stylesheets:",
761          error
762        );
763        this.externalParsedSheets = [];
764      }
765    }
766    /**
767     * Retrieves a map of network-loaded fonts.
768     *
769     * This method uses the Performance API to get all resource entries, filters out
770     * the ones that match the font file regex, and maps them to their cleaned URLs.
771     *
772     * @returns {Map} A map where each key is a cleaned URL of a font file and
773     *                each value is the original URL of the font file.
774     */
775    getNetworkLoadedFonts() {
776      return new Map(
777        window.performance.getEntriesByType("resource").filter((resource) => this.FONT_FILE_REGEX.test(resource.name)).map((resource) => [this.cleanUrl(resource.name), resource.name])
778      );
779    }
780    /**
781     * Retrieves font-face rules from stylesheets.
782     *
783     * This method scans all stylesheets loaded on the page and collects
784     * font-face rules, including their source URLs, font families, weights,
785     * and styles. It returns an object containing the collected font data.
786     *
787     * @returns {Promise<Object>} An object mapping font families to their respective
788     *                  URLs and variations.
789     */
790    async getFontFaceRules() {
791      const stylesheetFonts = {};
792      const processedUrls = /* @__PURE__ */ new Set();
793      const processFontFaceRule = (rule, baseHref = null) => {
794        const src = rule.style.getPropertyValue("src");
795        const fontFamily = rule.style.getPropertyValue("font-family").replace(/['"]/g, "").trim();
796        const weight = rule.style.getPropertyValue("font-weight") || "400";
797        const style = rule.style.getPropertyValue("font-style") || "normal";
798        if (!stylesheetFonts[fontFamily]) {
799          stylesheetFonts[fontFamily] = { urls: [], variations: /* @__PURE__ */ new Set() };
800        }
801        const extractFirstUrlFromSrc = (srcValue) => {
802          if (!srcValue) return null;
803          const urlMatch = srcValue.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/);
804          return urlMatch ? urlMatch[2] : null;
805        };
806        const firstUrl = extractFirstUrlFromSrc(src);
807        if (firstUrl) {
808          let rawUrl = firstUrl;
809          if (baseHref) {
810            rawUrl = new URL(rawUrl, baseHref).href;
811          }
812          const normalized = this.cleanUrl(rawUrl);
813          if (!stylesheetFonts[fontFamily].urls.includes(normalized)) {
814            stylesheetFonts[fontFamily].urls.push(normalized);
815            stylesheetFonts[fontFamily].variations.add(
816              JSON.stringify({ weight, style })
817            );
818          }
819        }
820      };
821      const processImportRule = async (rule) => {
822        try {
823          const importUrl = rule.href;
824          if (this.isUrlExcludedFromExternalProcessing(importUrl)) {
825            return;
826          }
827          if (processedUrls.has(importUrl)) {
828            return;
829          }
830          processedUrls.add(importUrl);
831          const response = await fetch(importUrl, { mode: "cors" });
832          if (!response.ok) {
833            this.logger.logMessage(`Failed to fetch @import CSS: ${response.status}`);
834            return;
835          }
836          const cssText = await response.text();
837          const tempSheet = new CSSStyleSheet();
838          tempSheet.replaceSync(cssText);
839          Array.from(tempSheet.cssRules || []).forEach((importedRule) => {
840            if (importedRule instanceof CSSFontFaceRule) {
841              processFontFaceRule(importedRule, importUrl);
842            }
843          });
844        } catch (error) {
845          this.logger.logMessage(`Error processing @import rule: ${error.message}`);
846        }
847      };
848      const processSheet = async (sheet) => {
849        try {
850          const rules = Array.from(sheet.cssRules || []);
851          for (const rule of rules) {
852            if (rule instanceof CSSFontFaceRule) {
853              processFontFaceRule(rule, sheet.href);
854            } else if (rule instanceof CSSImportRule) {
855              if (rule.styleSheet) {
856                await processSheet(rule.styleSheet);
857              } else {
858                await processImportRule(rule);
859              }
860            } else if (rule.styleSheet) {
861              await processSheet(rule.styleSheet);
862            }
863          }
864        } catch (e) {
865          if (e.name === "SecurityError" && sheet.href) {
866            if (this.isUrlExcludedFromExternalProcessing(sheet.href)) {
867              return;
868            }
869            if (processedUrls.has(sheet.href)) {
870              return;
871            }
872            processedUrls.add(sheet.href);
873            try {
874              const response = await fetch(sheet.href, { mode: "cors" });
875              if (response.ok) {
876                const cssText = await response.text();
877                const tempSheet = new CSSStyleSheet();
878                tempSheet.replaceSync(cssText);
879                Array.from(tempSheet.cssRules || []).forEach((rule) => {
880                  if (rule instanceof CSSFontFaceRule) {
881                    processFontFaceRule(rule, sheet.href);
882                  }
883                });
884                const importRegex = /@import\s+url\(['"]?([^'")]+)['"]?\);?/g;
885                let importMatch;
886                while ((importMatch = importRegex.exec(cssText)) !== null) {
887                  const importUrl = new URL(importMatch[1], sheet.href).href;
888                  if (this.isUrlExcludedFromExternalProcessing(importUrl)) {
889                    continue;
890                  }
891                  if (processedUrls.has(importUrl)) {
892                    continue;
893                  }
894                  processedUrls.add(importUrl);
895                  try {
896                    const importResponse = await fetch(importUrl, { mode: "cors" });
897                    if (importResponse.ok) {
898                      const importCssText = await importResponse.text();
899                      const tempImportSheet = new CSSStyleSheet();
900                      tempImportSheet.replaceSync(importCssText);
901                      Array.from(tempImportSheet.cssRules || []).forEac
901h((importedRule) => {
902                        if (importedRule instanceof CSSFontFaceRule) {
903                          processFontFaceRule(importedRule, importUrl);
904                        }
905                      });
906                    }
907                  } catch (importError) {
908                    this.logger.logMessage(`Error fetching @import ${importUrl}: ${importError.message}`);
909                  }
910                }
911              }
912            } catch (fetchError) {
913              this.logger.logMessage(`Error fetching stylesheet ${sheet.href}: ${fetchError.message}`);
914            }
915          } else {
916            this.logger.logMessage(`Error processing stylesheet: ${e.message}`);
917          }
918        }
919      };
920      const sheets = Array.from(document.styleSheets);
921      for (const sheet of sheets) {
922        await processSheet(sheet);
923      }
924      const inlineStyleElements = document.querySelectorAll("style");
925      for (const styleElement of inlineStyleElements) {
926        const cssText = styleElement.textContent || styleElement.innerHTML || "";
927        const importRegex = /@import\s+url\s*\(\s*['"]?([^'")]+)['"]?\s*\)\s*;?/g;
928        let importMatch;
929        while ((importMatch = importRegex.exec(cssText)) !== null) {
930          const importUrl = importMatch[1];
931          if (this.isUrlExcludedFromExternalProcessing(importUrl)) {
932            continue;
933          }
934          if (processedUrls.has(importUrl)) {
935            continue;
936          }
937          processedUrls.add(importUrl);
938          try {
939            const response = await fetch(importUrl, { mode: "cors" });
940            if (response.ok) {
941              const importCssText = await response.text();
942              const tempSheet = new CSSStyleSheet();
943              tempSheet.replaceSync(importCssText);
944              Array.from(tempSheet.cssRules || []).forEach((importedRule) => {
945                if (importedRule instanceof CSSFontFaceRule) {
946                  processFontFaceRule(importedRule, importUrl);
947                }
948              });
949            }
950          } catch (importError) {
951            this.logger.logMessage(`Error fetching inline @import ${importUrl}: ${importError.message}`);
952          }
953        }
954      }
955      Object.values(stylesheetFonts).forEach((fontData) => {
956        fontData.variations = Array.from(fontData.variations).map((v) => JSON.parse(v));
957      });
958      return stylesheetFonts;
959    }
960    /**
961     * Checks if an element is above the fold (visible in the viewport without scrolling).
962     *
963     * @param {Element} element - The element to check.
964     * @returns {boolean} True if the element is above the fold, false otherwise.
965     */
966    isElementAboveFold(element) {
967      if (!this.isElementVisible(element)) return false;
968      const rect = element.getBoundingClientRect();
969      const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
970      const elementTop = rect.top + scrollTop;
971      const foldPosition = window.innerHeight || document.documentElement.clientHeight;
972      return elementTop <= foldPosition;
973    }
974    /**
975     * Checks if an element can be processed for font analysis.
976     *
977     * This method combines checks for whether an element can be styled with font-family
978     * and whether it is above the fold, providing a single method to determine if an
979     * element should be processed during font analysis.
980     *
981     * @param {Element} element - The element to check.
982     * @returns {boolean} True if the element can be processed, false otherwise.
983     */
984    canElementBeProcessed(element) {
985      return this.canElementBeStyledWithFontFamily(element) && this.isElementAboveFold(element);
986    }
987    /**
988     * Initiates the process of analyzing and summarizing font usage on the page.
989     * This method fetches network-loaded fonts, stylesheet fonts, and external font pairs.
990     * It then processes each element on the page to determine which fonts are used above the fold.
991     * The results are summarized and logged.
992     *
993     * @returns {Promise<void>} A promise that resolves when the analysis is complete.
994     */
995    async run() {
996      await document.fonts.ready;
997      await this._initializeExternalFontSheets();
998      const networkLoadedFonts = this.getNetworkLoadedFonts();
999      const stylesheetFonts = await this.getFontFaceRules();
1000      const hostedFonts = /* @__PURE__ */ new Map();
1001      const externalFontsResults = await this.processExternalFonts(this.externalParsedPairs);
1002      const elements = Array.from(document.getElementsByTagName("*")).filter((el) => this.canElementBeProcessed(el));
1003      elements.forEach((element) => {
1004        const processElementFont = (style, pseudoElement = null) => {
1005          if (!style || !this.isElementVisible(element)) return;
1006          const fontFamily = style.fontFamily.split(",")[0].replace(/['"]+/g, "").trim();
1007          const hasContent = pseudoElement ? style.content !== "none" && style.content !== '""' : element.textContent.trim();
1008          if (hasContent && stylesheetFonts[fontFamily]) {
1009            let urls = stylesheetFonts[fontFamily].urls;
1010            if (!this.isExcluded(fontFamily, urls) && !hostedFonts.has(fontFamily)) {
1011              hostedFonts.set(fontFamily, {
1012                elements: /* @__PURE__ */ new Set(),
1013                urls,
1014                variations: stylesheetFonts[fontFamily].variations
1015              });
1016              hostedFonts.get(fontFamily).elements.add(element);
1017            }
1018          }
1019        };
1020        try {
1021          processElementFont(window.getComputedStyle(element));
1022          ["::before", "::after"].forEach((pseudo) => {
1023            processElementFont(window.getComputedStyle(element, pseudo), pseudo);
1024          });
1025        } catch (e) {
1026          this.logger.logMessage("Error processing element:", e);
1027        }
1028      });
1029      const aboveTheFoldFonts = this.summarizeMatches(externalFontsResults, hostedFonts, networkLoadedFonts);
1030      if (!Object.keys(aboveTheFoldFonts.allFonts).length && !Object.keys(aboveTheFoldFonts.externalFonts).length && !Object.keys(aboveTheFoldFonts.hostedFonts).length) {
1031        this.logger.logMessage("No fonts found above the fold.");
1032        return;
1033      }
1034      this.logger.logMessage("Above the fold fonts:", aboveTheFoldFonts);
1035      this.aboveTheFoldFonts = [...new Set(Object.values(aboveTheFoldFonts.allFonts).flatMap((font) => font.variations.map((variation) => variation.url)))];
1036    }
1037    /**
1038     * Summarizes all font matches found on the page
1039     * Creates a comprehensive object containing font usage data
1040     *
1041     * @param {Object} externalFontsResults - Results from External Fonts analysis
1042     * @param {Map} hostedFonts - Map of hosted (non-External) fonts found
1043     * @param {Map} networkLoadedFonts - Map of all font files loaded via network
1044     * @returns {Object} Complete analysis of font usage including locations and counts
1045     */
1046    summarizeMatches(externalFontsResults, hostedFonts, networkLoadedFonts) {
1047      const allFonts = {};
1048      const hostedFontsResults = {};
1049      if (hostedFonts.size > 0) {
1050        hostedFonts.forEach((data, fontFamily) => {
1051          if (data.variations) {
1052            const elements = Array.from(data.elements);
1053            const aboveElements = elements.filter((el) => this.isElementAboveFold(el));
1054            const belowElements = elements.filter((el) => !this.isElementAboveFold(el));
1055            data.variations.forEach((variation) => {
1056              let matchingUrl = null;
1057              for (const styleUrl of data.urls) {
1058                const normalizedStyleUrl = this.cleanUrl(styleUrl);
1059                if (networkLoadedFonts.has(normalizedStyleUrl)) {
1060                  matchingUrl = networkLoadedFonts.get(normalizedStyleUrl);
1061                  break;
1062                }
1063              }
1064              if (matchingUrl) {
1065                if (!allFonts[fontFamily]) {
1066                  allFonts[fontFamily] = {
1067                    type: "hosted",
1068                    variations: [],
1069                    elementCount: {
1070                      aboveFold: aboveElements.length,
1071                      belowFold: belowElements.length,
1072                      total: elements.length
1073                    },
1074                    urlCount: {
1075                      aboveFold: /* @__PURE__ */ new Set(),
1076                      belowFold: /* @__PURE__ */ new Set()
1077                    }
1078                  };
1079                }
1080                allFonts[fontFamily].variations.push({
1081                  weight: variation.weight,
1082                  style: variation.style,
1083                  url: matchingUrl,
1084                  elementCount: {
1085                    aboveFold: aboveElements.length,
1086                    belowFold: belowElements.length,
1087                    total: elements.length
1088                  }
1089                });
1090                if (aboveElements.length > 0) {
1091                  allFonts[fontFamily].urlCount.aboveFold.add(matchingUrl);
1092                }
1093                if (belowElements.length > 0) {
1094                  allFonts[fontFamily].urlCount.belowFold.add(matchingUrl);
1095                }
1096              }
1097            });
1098            if (allFonts[fontFamily]) {
1099              hostedFontsResults[fontFamily] = {
1100                variations: allFonts[fontFamily].variations,
1101                elementCount: { ...allFonts[fontFamily].elementCount },
1102                urlCount: { ...allFonts[fontFamily].urlCount }
1103              };
1104            }
1105          }
1106        });
1107      }
1108      if (Object.keys(externalFontsResults).length > 0) {
1109        Object.entries(externalFontsResults).forEach(([url, data]) => {
1110          const aboveElements = Array.from(data.elements).filter((el) => this.isElementAboveFold(el));
1111          const belowElements = Array.from(data.elements).filter((el) => !this.isElementAboveFold(el));
1112          if (data.elementCount.aboveFold > 0 || aboveElements.length > 0) {
1113            data.variations.forEach((variation) => {
1114              if (!allFonts[variation.family]) {
1115                allFonts[variation.family] = {
1116                  type: "external",
1117                  variations: [],
1118                  // Track element counts at font family level
1119                  elementCount: {
1120                    aboveFold: 0,
1121                    belowFold: 0,
1122                    total: 0
1123                  },
1124                  // Track unique URLs used in each fold location
1125                  urlCount: {
1126                    aboveFold: /* @__PURE__ */ new Set(),
1127                    belowFold: /* @__PURE__ */ new Set()
1128                  }
1129                };
1130              }
1131              allFonts[variation.family].variations.push({
1132                weight: variation.weight,
1133                style: variation.style,
1134                url,
1135                elementCount: {
1136                  aboveFold: aboveElements.length,
1137                  belowFold: belowElements.length,
1138                  total: data.elements.length
1139                }
1140              });
1141              allFonts[variation.family].elementCount.aboveFold += aboveElements.length;
1142              allFonts[variation.family].elementCount.belowFold += belowElements.length;
1143              allFonts[variation.family].elementCount.total += data.elements.length;
1144              if (aboveElements.length > 0) {
1145                allFonts[variation.family].urlCount.aboveFold.add(url);
1146              }
1147              if (belowElements.length > 0) {
1148                allFonts[variation.family].urlCount.belowFold.add(url);
1149              }
1150            });
1151          }
1152        });
1153      }
1154      Object.values(allFonts).forEach((font) => {
1155        font.urlCount = {
1156          aboveFold: font.urlCount.aboveFold.size,
1157          belowFold: font.urlCount.belowFold.size,
1158          total: (/* @__PURE__ */ new Set([...font.urlCount.aboveFold, ...font.urlCount.belowFold])).size
1159        };
1160      });
1161      Object.values(hostedFontsResults).forEach((font) => {
1162        if (font.urlCount.aboveFold instanceof Set) {
1163          font.urlCount = {
1164            aboveFold: font.urlCount.aboveFold.size,
1165            belowFold: font.urlCount.belowFold.size,
1166            total: (/* @__PURE__ */ new Set([...font.urlCount.aboveFold, ...font.urlCount.belowFold])).size
1167          };
1168        }
1169      });
1170      return {
1171        externalFonts: Object.fromEntries(
1172          Object.entries(externalFontsResults).filter(
1173            (entry) => entry[1].elementCount.aboveFold > 0
1174          )
1175        ),
1176        hostedFonts: hostedFontsResults,
1177        allFonts
1178      };
1179    }
1180    /**
1181     * Processes external font pairs to identify their usage on the page.
1182     *
1183     * This method iterates through all elements on the page, checks if they are above the fold,
1184     * and determines the font information for each element. It then matches the font information
1185     * with the provided external font pairs to identify which fonts are used and where.
1186     *
1187     * @param {Object} fontPairs - An object where each key is a URL and the value is an array of font variations.
1188     * @returns {Promise<Object>} A promise that resolves to an object where each key is a URL and the value is an object c
1188ontaining information about the elements using that font.
1189     */
1190    async processExternalFonts(fontPairs) {
1191      const matches = /* @__PURE__ */ new Map();
1192      const elements = Array.from(document.getElementsByTagName("*")).filter((el) => this.canElementBeProcessed(el));
1193      const fontMap = /* @__PURE__ */ new Map();
1194      Object.entries(fontPairs).forEach(([url, variations]) => {
1195        variations.forEach((variation) => {
1196          const key = `${variation.family}|${variation.weight}|${variation.style}`;
1197          fontMap.set(key, { url, ...variation });
1198        });
1199      });
1200      const getFontInfoForElement = (style) => {
1201        const family = style.fontFamily.split(",")[0].replace(/['"]+/g, "").trim();
1202        const weight = style.fontWeight;
1203        const fontStyle = style.fontStyle;
1204        const key = `${family}|${weight}|${fontStyle}`;
1205        let fontInfo = fontMap.get(key);
1206        if (!fontInfo && weight !== "400") {
1207          const fallbackKey = `${family}|400|${fontStyle}`;
1208          fontInfo = fontMap.get(fallbackKey);
1209        }
1210        return fontInfo;
1211      };
1212      elements.forEach((element) => {
1213        if (element.textContent.trim()) {
1214          const style = window.getComputedStyle(element);
1215          const fontInfo = getFontInfoForElement(style);
1216          if (fontInfo) {
1217            if (!this.isExcluded(fontInfo.family, [fontInfo.url]) && !matches.has(fontInfo.url)) {
1218              matches.set(fontInfo.url, {
1219                elements: /* @__PURE__ */ new Set(),
1220                variations: /* @__PURE__ */ new Set()
1221              });
1222              matches.get(fontInfo.url).elements.add(element);
1223              matches.get(fontInfo.url).variations.add(JSON.stringify({
1224                family: fontInfo.family,
1225                weight: fontInfo.weight,
1226                style: fontInfo.style
1227              }));
1228            }
1229          }
1230        }
1231        ["::before", "::after"].forEach((pseudo) => {
1232          const pseudoStyle = window.getComputedStyle(element, pseudo);
1233          if (pseudoStyle.content !== "none" && pseudoStyle.content !== '""') {
1234            const fontInfo = getFontInfoForElement(pseudoStyle);
1235            if (fontInfo) {
1236              if (!this.isExcluded(fontInfo.family, [fontInfo.url]) && !matches.has(fontInfo.url)) {
1237                matches.set(fontInfo.url, {
1238                  elements: /* @__PURE__ */ new Set(),
1239                  variations: /* @__PURE__ */ new Set()
1240                });
1241                matches.get(fontInfo.url).elements.add(element);
1242                matches.get(fontInfo.url).variations.add(JSON.stringify({
1243                  family: fontInfo.family,
1244                  weight: fontInfo.weight,
1245                  style: fontInfo.style
1246                }));
1247              }
1248            }
1249          }
1250        });
1251      });
1252      return Object.fromEntries(
1253        Array.from(matches.entries()).map(([url, data]) => [
1254          url,
1255          {
1256            elementCount: {
1257              aboveFold: Array.from(data.elements).filter((el) => this.isElementAboveFold(el)).length,
1258              total: data.elements.size
1259            },
1260            variations: Array.from(data.variations).map((v) => JSON.parse(v)),
1261            elements: Array.from(data.elements)
1262          }
1263        ])
1264      );
1265    }
1266    /**
1267     * Retrieves the results of the font analysis, specifically the fonts used above the fold.
1268     * This method returns an array containing the URLs of the fonts used above the fold.
1269     *
1270     * @returns {Array<string>} An array of URLs of the fonts used above the fold.
1271     */
1272    getResults() {
1273      return this.aboveTheFoldFonts;
1274    }
1275  };
1276  var BeaconPreloadFonts_default = BeaconPreloadFonts;
1277
1278  
1278// src/BeaconPreconnectExternalDomain.js
1279  var BeaconPreconnectExternalDomain = class {
1280    constructor(config, logger) {
1281      this.logger = logger;
1282      this.result = [];
1283      this.excludedPatterns = config.preconnect_external_domain_exclusions;
1284      this.eligibleElements = config.preconnect_external_domain_elements;
1285      this.matchedItems = /* @__PURE__ */ new Set();
1286      this.excludedItems = /* @__PURE__ */ new Set();
1287    }
1288    /**
1289     * Initiates the process of identifying and logging external domains that require preconnection.
1290     * This method queries the document for eligible elements, processes each element to determine
1291     * if it should be preconnected, and logs the results.
1292     */
1293    async run() {
1294      const elements = document.querySelectorAll(
1295        `${this.eligibleElements.join(", ")}[src], ${this.eligibleElements.join(", ")}[href], ${this.eligibleElements.join(", ")}[rel], ${this.eligibleElements.join(", ")}[type]`
1296      );
1297      elements.forEach((el) => this.processElement(el));
1298      this.logger.logMessage({ matchedItems: this.getMatchedItems(), excludedItems: Array.from(this.excludedItems) });
1299    }
1300    /**
1301     * Processes a single element to determine if it should be preconnected.
1302     *
1303     * This method checks if the element is excluded based on attribute or domain rules.
1304     * If not excluded, it checks if the element's URL is an external domain and adds it to the list of matched items.
1305     *
1306     * @param {Element} el - The element to process.
1307     */
1308    processElement(el) {
1309      try {
1310        const url = new URL(el.src || el.href || "", location.href);
1311        if (this.isExcluded(el)) {
1312          this.excludedItems.add(this.createExclusionObject(url, el));
1313          return;
1314        }
1315        if (this.isExternalDomain(url)) {
1316          this.matchedItems.add(`${url.hostname}-${el.tagName.toLowerCase()}`);
1317          this.result = [...new Set(this.result.concat(url.origin))];
1318        }
1319      } catch (e) {
1320        this.logger.logMessage(e);
1321      }
1322    }
1323    /**
1324     * Checks if an element is excluded based on exclusions patterns.
1325     *
1326     * This method iterates through the excludedPatterns array and checks if any pattern matches any of the element's attribute or values.
1327     * If a match is found, it returns true, indicating the element is excluded.
1328     *
1329     * @param {Element} el - The element to check.
1330     * @returns {boolean} True if the element is excluded by an attribute rule, false otherwise.
1331     */
1332    isExcluded(el) {
1333      const outerHTML = el.outerHTML.substring(0, el.outerHTML.indexOf(">") + 1);
1334      return this.excludedPatterns.some(
1335        (pattern) => outerHTML.includes(pattern)
1336      );
1337    }
1338    /**
1339     * Checks if a URL is excluded based on domain rules.
1340     *
1341     * This method iterates through the excludedPatterns array and checks if any pattern matches the URL's hostname.
1342     * If a match is found, it returns true, indicating the URL is excluded.
1343     *
1344     * @param {URL} url - The URL to check.
1345     * @returns {boolean} True if the URL is excluded by a domain rule, false otherwise.
1346     */
1347    isExcludedByDomain(url) {
1348      return this.excludedPatterns.some(
1349        (pattern) => pattern.type === "domain" && url.hostname.includes(pattern.value)
1350      );
1351    }
1352    /**
1353     * Checks if a URL is from an external domain.
1354     *
1355     * This method compares the hostname of the given URL with the hostname of the current location.
1356     * If they are not the same, it indicates the URL is from an external domain.
1357     *
1358     * @param {URL} url - The URL to check.
1359     * @returns {boolean} True if the URL is from an external domain, false otherwise.
1360     */
1361    isExternalDomain(url) {
1362      return url.hostname !== location.hostname && url.hostname;
1363    }
1364    /**
1365     * Creates an exclusion object based on the URL, element.
1366     *
1367     * @param {URL} url - The URL to create the exclusion object for.
1368     * @param {Element} el - The element to create the exclusion object for.
1369     * @returns {Object} An object with the URL's hostname, the element's tag name, and the reason.
1370     */
1371    createExclusionObject(url, el) {
1372      return { domain: url.hostname, elementType: el.tagName.toLowerCase() };
1373    }
1374    /**
1375     * Returns an array of matched items, each item split into its domain and element type.
1376     *
1377     * This method iterates through the matchedItems set, splits each item into its domain and element type using the last hyphen as a delimiter,
1378     * and returns an array of these split items.
1379     *
1380     * @returns {Array} An array of arrays, each containing a domain and an element type.
1381     */
1382    getMatchedItems() {
1383      return Array.from(this.matchedItems).map((item) => {
1384        const lastHyphenIndex = item.lastIndexOf("-");
1385        return [
1386          item.substring(0, lastHyphenIndex),
1387          // Domain
1388          item.substring(lastHyphenIndex + 1)
1389          // Element type
1390        ];
1391      });
1392    }
1393    /**
1394     * Returns the array of unique domain names that were found to be external.
1395     *
1396     * This method returns the result array, which contains a list of unique domain names that were identified as external during the analysis process.
1397     *
1398     * @returns {Array} An array of unique domain names.
1399     */
1400    getResults() {
1401      return this.result;
1402    }
1403  };
1404  var BeaconPreconnectExternalDomain_default = BeaconPreconnectExternalDomain;
1405
1406  
1406// src/Logger.js
1407  var Logger = class {
1408    constructor(enabled) {
1409      this.enabled = enabled;
1410    }
1411    logMessage(label, msg = "") {
1412      if (!this.enabled) {
1413        return;
1414      }
1415      if (msg !== "") {
1416        console.log(label, msg);
1417        return;
1418      }
1419      console.log(label);
1420    }
1421    logColoredMessage(msg, color = "green") {
1422      if (!this.enabled) {
1423        return;
1424      }
1425      console.log(`%c${msg}`, `color: ${color};`);
1426    }
1427  };
1428  var Logger_default = Logger;
1429
1430  
1430// src/BeaconManager.js
1431  var BeaconManager = class {
1432    constructor(config) {
1433      this.config = config;
1434      this.lcpBeacon = null;
1435      this.lrcBeacon = null;
1436      this.preloadFontsBeacon = null;
1437      this.preconnectExternalDomainBeacon = null;
1438      this.infiniteLoopId = null;
1439      this.errorCode = "";
1440      this.logger = new Logger_default(this.config.debug);
1441    }
1442    async init() {
1443      this.scriptTimer = /* @__PURE__ */ new Date();
1444      if (!await this._isValidPreconditions()) {
1445        this._finalize();
1446        return;
1447      }
1448      if (Utils_default.isPageScrolled()) {
1449        this.logger.logMessage("Bailing out because the page has been scrolled");
1450        this._finalize();
1451        return;
1452      }
1453      this.infiniteLoopId = setTimeout(() => {
1454        this._handleInfiniteLoop();
1455      }, 1e4);
1456      const isGeneratedBefore = await this._getGeneratedBefore();
1457      const shouldGenerateLcp = this.config.status.atf && (isGeneratedBefore === false || isGeneratedBefore.lcp === false);
1458      const shouldGeneratelrc = this.config.status.lrc && (isGeneratedBefore === false || isGeneratedBefore.lrc === false);
1459      const shouldGeneratePreloadFonts = this.config.status.preload_fonts && (isGeneratedBefore === false || isGeneratedBefore.preload_fonts === false);
1460      const shouldGeneratePreconnectExternalDomain = this.config.status.preconnect_external_domain && (isGeneratedBefore === false || isGeneratedBefore.preconnect_external_domain === false);
1461      if (shouldGenerateLcp) {
1462        this.lcpBeacon = new BeaconLcp_default(this.config, this.logger);
1463        await this.lcpBeacon.run();
1464      } else {
1465        this.logger.logMessage("Not running BeaconLcp because data is already available or feature is disabled");
1466      }
1467      if (shouldGeneratelrc) {
1468        this.lrcBeacon = new BeaconLrc_default(this.config, this.logger);
1469        await this.lrcBeacon.run();
1470      } else {
1471        this.logger.logMessage("Not running BeaconLrc because data is already available or feature is disabled");
1472      }
1473      if (shouldGeneratePreloadFonts) {
1474        this.preloadFontsBeacon = new BeaconPreloadFonts_default(this.config, this.logger);
1475        await this.preloadFontsBeacon.run();
1476      } else {
1477        this.logger.logMessage("Not running BeaconPreloadFonts because data is already available or feature is disabled");
1478      }
1479      if (shouldGeneratePreconnectExternalDomain) {
1480        this.preconnectExternalDomainBeacon = new BeaconPreconnectExternalDomain_default(this.config, this.logger);
1481        await this.preconnectExternalDomainBeacon.run();
1482      } else {
1483        this.logger.logMessage("Not running BeaconPreconnectExternalDomain because data is already available or feature is disabled");
1484      }
1485      if (shouldGenerateLcp || shouldGeneratelrc || shouldGeneratePreloadFonts || shouldGeneratePreconnectExternalDomain) {
1486        this._saveFinalResultIntoDB();
1487      } else {
1488        this.logger.logMessage("Not saving results into DB as no beacon features ran.");
1489        this._finalize();
1490      }
1491    }
1492    async _isValidPreconditions() {
1493      const threshold = {
1494        width: this.config.width_threshold,
1495        height: this.config.height_threshold
1496      };
1497      if (Utils_default.isNotValidScreensize(this.config.is_mobile, threshold)) {
1498        this.logger.logMessage("Bailing out because screen size is not acceptable");
1499        return false;
1500      }
1501      return true;
1502    }
1503    async _getGeneratedBefore() {
1504      if (!Utils_default.isPageCached()) {
1505        return false;
1506      }
1507      let data_check = new FormData();
1508      data_check.append("action", "rocket_check_beacon");
1509      data_check.append("rocket_beacon_nonce", this.config.nonce);
1510      data_check.append("url", this.config.url);
1511      data_check.append("is_mobile", this.config.is_mobile);
1512      const beacon_data_response = await fetch(this.config.ajax_url, {
1513        method: "POST",
1514        credentials: "same-origin",
1515        body: data_check
1516      }).then((data) => data.json());
1517      return beacon_data_response.data;
1518    }
1519    _saveFinalResultIntoDB() {
1520      const results = {
1521        lcp: this.lcpBeacon ? this.lcpBeacon.getResults() : null,
1522        lrc: this.lrcBeacon ? this.lrcBeacon.getResults() : null,
1523        preload_fonts: this.preloadFontsBeacon ? this.preloadFontsBeacon.getResults() : null,
1524        preconnect_external_domain: this.preconnectExternalDomainBeacon ? this.preconnectExternalDomainBeacon.getResults() : null
1525      };
1526      const data = new FormData();
1527      data.append("action", "rocket_beacon");
1528      data.append("rocket_beacon_nonce", this.config.nonce);
1529      data.append("url", this.config.url);
1530      data.append("is_mobile", this.config.is_mobile);
1531      data.append("status", this._getFinalStatus());
1532      data.append("results", JSON.stringify(results));
1533      fetch(this.config.ajax_url, {
1534        method: "POST",
1535        credentials: "same-origin",
1536        body: data,
1537        headers: {
1538          "wpr-saas-no-intercept": true
1539        }
1540      }).then((response) => response.json()).then((data2) => {
1541        this.logger.logMessage(data2.data.lcp);
1542      }).catch((error) => {
1543        this.logger.logMessage(error);
1544      }).finally(() => {
1545        this._finalize();
1546      });
1547    }
1548    _getFinalStatus() {
1549      if ("" !== this.errorCode) {
1550        return this.errorCode;
1551      }
1552      const scriptTime = (/* @__PURE__ */ new Date() - this.scriptTimer) / 1e3;
1553      if (10 <= scriptTime) {
1554        return "timeout";
1555      }
1556      return "success";
1557    }
1558    _handleInfiniteLoop() {
1559      this._saveFinalResultIntoDB();
1560    }
1561    _finalize() {
1562      const beaconscript = document.querySelector('[data-name="wpr-wpr-beacon"]');
1563      beaconscript.setAttribute("beacon-completed", "true");
1564      clearTimeout(this.infiniteLoopId);
1565    }
1566  };
1567  var BeaconManager_default = BeaconManager;
1568
1569  
1569// src/BeaconEntryPoint.js
1570  ((rocket_beacon_data) => {
1571    if (!rocket_beacon_data) {
1572      return;
1573    }
1574    const instance = new BeaconManager_default(rocket_beacon_data);
1575    if (document.readyState !== "loading") {
1576      setTimeout(() => {
1577        instance.init();
1578      }, rocket_beacon_data.delay);
1579      return;
1580    }
1581    document.addEventListener("DOMContentLoaded", () => {
1582      setTimeout(() => {
1583        instance.init();
1584      }, rocket_beacon_data.delay);
1585    });
1586  })(window.rocket_beacon_data);
1587  var BeaconEntryPoint_default = BeaconManager_default;
1588})();

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.