Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/wp-rocket/assets/js/wpr-beacon.js
Keine Baseline-Datei – Diff nur gegen leer.
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
+