Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/wp-rocket/assets/js/wpr-beacon.js

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + (() => {
2 + // src/Utils.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 + // 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 + // 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 + // 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 + `
719 + CSS (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 || []).forEach((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 containing 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 + // 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 + // 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 + // 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 + // 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 + })();
1589 +