Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 225 additions & 38 deletions assets/js/wpr-beacon.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,55 @@
this.aboveTheFoldFonts = [];
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("|");
this.FONT_FILE_REGEX = new RegExp(`\\.(${extensions})(\\?.*)?$`, "i");
this.EXCLUDED_TAG_NAMES = /* @__PURE__ */ new Set([
// Metadata/document head
"BASE",
"HEAD",
"LINK",
"META",
"STYLE",
"TITLE",
"SCRIPT",
// Media
"IMG",
"VIDEO",
"AUDIO",
"EMBED",
"OBJECT",
"IFRAME",
// Templating, wrappers, components, fallback
"NOSCRIPT",
"TEMPLATE",
"SLOT",
"CANVAS",
// Resources
"SOURCE",
"TRACK",
"PARAM",
// SVG references
"USE",
"SYMBOL",
// Layout work
"BR",
"HR",
"WBR",
// Obsolete/deprecated
"APPLET",
"ACRONYM",
"BGSOUND",
"BIG",
"BLINK",
"CENTER",
"FONT",
"FRAME",
"FRAMESET",
"MARQUEE",
"NOFRAMES",
"STRIKE",
"TT",
"U",
"XMP"
]);
}
Comment thread
Miraeld marked this conversation as resolved.
/**
* Checks if a font family or URL should be excluded from preloading.
Expand Down Expand Up @@ -419,6 +468,18 @@
}
return false;
}
/**
* Checks if an element can be styled with font-family.
*
* This method determines if the provided element's tag name is not in the list
* of excluded tag names that cannot be styled with font-family CSS property.
*
* @param {Element} element - The element to check.
* @returns {boolean} True if the element can be styled with font-family, false otherwise.
*/
canElementBeStyledWithFontFamily(element) {
return !this.EXCLUDED_TAG_NAMES.has(element.tagName);
}
/**
* Checks if an element is visible in the viewport.
*
Expand Down Expand Up @@ -691,46 +752,162 @@ CSS (first 200 chars): ${txt.substring(0, 200)}...`
* font-face rules, including their source URLs, font families, weights,
* and styles. It returns an object containing the collected font data.
*
* @returns {Object} An object mapping font families to their respective
* @returns {Promise<Object>} An object mapping font families to their respective
* URLs and variations.
*/
getFontFaceRules() {
async getFontFaceRules() {
const stylesheetFonts = {};
Array.from(Array.from(document.styleSheets)).filter((sheet) => !sheet.href || new URL(sheet.href).origin === location.origin).forEach((sheet) => {
const processedUrls = /* @__PURE__ */ new Set();
const processFontFaceRule = (rule, baseHref = null) => {
const src = rule.style.getPropertyValue("src");
const fontFamily = rule.style.getPropertyValue("font-family").replace(/['"]/g, "").trim();
const weight = rule.style.getPropertyValue("font-weight") || "400";
const style = rule.style.getPropertyValue("font-style") || "normal";
if (!stylesheetFonts[fontFamily]) {
stylesheetFonts[fontFamily] = { urls: [], variations: /* @__PURE__ */ new Set() };
}
const extractFirstUrlFromSrc = (srcValue) => {
if (!srcValue) return null;
const urlMatch = srcValue.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/);
return urlMatch ? urlMatch[2] : null;
};
const firstUrl = extractFirstUrlFromSrc(src);
if (firstUrl) {
let rawUrl = firstUrl;
if (baseHref) {
rawUrl = new URL(rawUrl, baseHref).href;
}
const normalized = this.cleanUrl(rawUrl);
if (!stylesheetFonts[fontFamily].urls.includes(normalized)) {
stylesheetFonts[fontFamily].urls.push(normalized);
stylesheetFonts[fontFamily].variations.add(
JSON.stringify({ weight, style })
);
}
}
};
const processImportRule = async (rule) => {
try {
const importUrl = rule.href;
if (processedUrls.has(importUrl)) {
return;
}
processedUrls.add(importUrl);
const response = await fetch(importUrl, { mode: "cors" });
if (!response.ok) {
this.logger.logMessage(`Failed to fetch @import CSS: ${response.status}`);
return;
}
const cssText = await response.text();
const tempSheet = new CSSStyleSheet();
tempSheet.replaceSync(cssText);
Array.from(tempSheet.cssRules || []).forEach((importedRule) => {
if (importedRule instanceof CSSFontFaceRule) {
processFontFaceRule(importedRule, importUrl);
}
});
} catch (error) {
this.logger.logMessage(`Error processing @import rule: ${error.message}`);
}
};
const processSheet = async (sheet) => {
try {
Array.from(sheet.cssRules || []).forEach((rule) => {
const rules = Array.from(sheet.cssRules || []);
for (const rule of rules) {
if (rule instanceof CSSFontFaceRule) {
const src = rule.style.getPropertyValue("src");
const fontFamily = rule.style.getPropertyValue("font-family").replace(/['"]+/g, "").trim();
const weight = rule.style.getPropertyValue("font-weight") || "400";
const style = rule.style.getPropertyValue("font-style") || "normal";
if (!stylesheetFonts[fontFamily]) {
stylesheetFonts[fontFamily] = {
urls: [],
variations: /* @__PURE__ */ new Set()
};
processFontFaceRule(rule, sheet.href);
} else if (rule instanceof CSSImportRule) {
if (rule.styleSheet) {
await processSheet(rule.styleSheet);
} else {
await processImportRule(rule);
}
const urls = src.match(/url\(['"]?([^'"]+)['"]?\)/g) || [];
urls.forEach((urlMatch) => {
let rawUrl = urlMatch.match(/url\(['"]?([^'"]+)['"]?\)/)[1];
if (sheet.href) {
rawUrl = new URL(rawUrl, sheet.href).href;
} else if (rule.styleSheet) {
await processSheet(rule.styleSheet);
}
}
} catch (e) {
if (e.name === "SecurityError" && sheet.href) {
if (processedUrls.has(sheet.href)) {
return;
}
processedUrls.add(sheet.href);
try {
const response = await fetch(sheet.href, { mode: "cors" });
if (response.ok) {
const cssText = await response.text();
const tempSheet = new CSSStyleSheet();
tempSheet.replaceSync(cssText);
Array.from(tempSheet.cssRules || []).forEach((rule) => {
if (rule instanceof CSSFontFaceRule) {
processFontFaceRule(rule, sheet.href);
}
});
const importRegex = /@import\s+url\(['"]?([^'")]+)['"]?\);?/g;
let importMatch;
while ((importMatch = importRegex.exec(cssText)) !== null) {
const importUrl = new URL(importMatch[1], sheet.href).href;
if (processedUrls.has(importUrl)) {
continue;
}
processedUrls.add(importUrl);
try {
const importResponse = await fetch(importUrl, { mode: "cors" });
if (importResponse.ok) {
const importCssText = await importResponse.text();
const tempImportSheet = new CSSStyleSheet();
tempImportSheet.replaceSync(importCssText);
Array.from(tempImportSheet.cssRules || []).forEach((importedRule) => {
if (importedRule instanceof CSSFontFaceRule) {
processFontFaceRule(importedRule, importUrl);
}
});
}
} catch (importError) {
this.logger.logMessage(`Error fetching @import ${importUrl}: ${importError.message}`);
}
}
const normalizedUrl = this.cleanUrl(rawUrl);
if (!stylesheetFonts[fontFamily].urls.includes(normalizedUrl)) {
stylesheetFonts[fontFamily].urls.push(normalizedUrl);
stylesheetFonts[fontFamily].variations.add(JSON.stringify({
weight,
style
}));
}
} catch (fetchError) {
this.logger.logMessage(`Error fetching stylesheet ${sheet.href}: ${fetchError.message}`);
}
} else {
this.logger.logMessage(`Error processing stylesheet: ${e.message}`);
}
}
};
const sheets = Array.from(document.styleSheets);
for (const sheet of sheets) {
await processSheet(sheet);
}
const inlineStyleElements = document.querySelectorAll("style");
for (const styleElement of inlineStyleElements) {
const cssText = styleElement.textContent || styleElement.innerHTML || "";
const importRegex = /@import\s+url\s*\(\s*['"]?([^'")]+)['"]?\s*\)\s*;?/g;
let importMatch;
while ((importMatch = importRegex.exec(cssText)) !== null) {
const importUrl = importMatch[1];
if (processedUrls.has(importUrl)) {
continue;
}
processedUrls.add(importUrl);
try {
const response = await fetch(importUrl, { mode: "cors" });
if (response.ok) {
const importCssText = await response.text();
const tempSheet = new CSSStyleSheet();
tempSheet.replaceSync(importCssText);
Array.from(tempSheet.cssRules || []).forEach((importedRule) => {
if (importedRule instanceof CSSFontFaceRule) {
processFontFaceRule(importedRule, importUrl);
}
});
}
});
} catch (e) {
this.logger.logMessage(e);
} catch (importError) {
this.logger.logMessage(`Error fetching inline @import ${importUrl}: ${importError.message}`);
}
}
});
}
Object.values(stylesheetFonts).forEach((fontData) => {
fontData.variations = Array.from(fontData.variations).map((v) => JSON.parse(v));
});
Expand All @@ -750,6 +927,19 @@ CSS (first 200 chars): ${txt.substring(0, 200)}...`
const foldPosition = window.innerHeight || document.documentElement.clientHeight;
return elementTop <= foldPosition;
}
/**
* Checks if an element can be processed for font analysis.
*
* This method combines checks for whether an element can be styled with font-family
* and whether it is above the fold, providing a single method to determine if an
* element should be processed during font analysis.
*
* @param {Element} element - The element to check.
* @returns {boolean} True if the element can be processed, false otherwise.
*/
canElementBeProcessed(element) {
return this.canElementBeStyledWithFontFamily(element) && this.isElementAboveFold(element);
}
/**
* Initiates the process of analyzing and summarizing font usage on the page.
* This method fetches network-loaded fonts, stylesheet fonts, and external font pairs.
Expand All @@ -762,10 +952,10 @@ CSS (first 200 chars): ${txt.substring(0, 200)}...`
await document.fonts.ready;
await this._initializeExternalFontSheets();
const networkLoadedFonts = this.getNetworkLoadedFonts();
const stylesheetFonts = this.getFontFaceRules();
const stylesheetFonts = await this.getFontFaceRules();
const hostedFonts = /* @__PURE__ */ new Map();
const externalFontsResults = await this.processExternalFonts(this.externalParsedPairs);
const elements = Array.from(document.getElementsByTagName("*")).filter((el) => this.isElementAboveFold(el));
const elements = Array.from(document.getElementsByTagName("*")).filter((el) => this.canElementBeProcessed(el));
elements.forEach((element) => {
const processElementFont = (style, pseudoElement = null) => {
if (!style || !this.isElementVisible(element)) return;
Expand Down Expand Up @@ -861,9 +1051,6 @@ CSS (first 200 chars): ${txt.substring(0, 200)}...`
}
}
});
if (!Object.prototype.hasOwnProperty.call(allFonts, fontFamily)) {
return;
}
if (allFonts[fontFamily]) {
hostedFontsResults[fontFamily] = {
variations: allFonts[fontFamily].variations,
Expand All @@ -876,7 +1063,9 @@ CSS (first 200 chars): ${txt.substring(0, 200)}...`
}
if (Object.keys(externalFontsResults).length > 0) {
Object.entries(externalFontsResults).forEach(([url, data]) => {
if (data.elementCount.aboveFold > 0) {
const aboveElements = Array.from(data.elements).filter((el) => this.isElementAboveFold(el));
const belowElements = Array.from(data.elements).filter((el) => !this.isElementAboveFold(el));
if (data.elementCount.aboveFold > 0 || aboveElements.length > 0) {
data.variations.forEach((variation) => {
if (!allFonts[variation.family]) {
allFonts[variation.family] = {
Expand All @@ -895,8 +1084,6 @@ CSS (first 200 chars): ${txt.substring(0, 200)}...`
}
};
}
const aboveElements = Array.from(data.elements).filter((el) => this.isElementAboveFold(el));
const belowElements = Array.from(data.elements).filter((el) => !this.isElementAboveFold(el));
allFonts[variation.family].variations.push({
weight: variation.weight,
style: variation.style,
Expand Down Expand Up @@ -958,7 +1145,7 @@ CSS (first 200 chars): ${txt.substring(0, 200)}...`
*/
async processExternalFonts(fontPairs) {
const matches = /* @__PURE__ */ new Map();
const elements = Array.from(document.getElementsByTagName("*")).filter((el) => this.isElementAboveFold(el));
const elements = Array.from(document.getElementsByTagName("*")).filter((el) => this.canElementBeProcessed(el));
const fontMap = /* @__PURE__ */ new Map();
Object.entries(fontPairs).forEach(([url, variations]) => {
variations.forEach((variation) => {
Expand Down
Loading
Loading