diff --git a/src/BeaconPreloadFonts.js b/src/BeaconPreloadFonts.js index 36fa8cd..aca9338 100644 --- a/src/BeaconPreloadFonts.js +++ b/src/BeaconPreloadFonts.js @@ -40,6 +40,22 @@ class BeaconPreloadFonts { ]); } + /** + * Checks if a URL should be excluded from external font processing based on domain exclusions. + * + * @param {string} url - The URL to check. + * @returns {boolean} True if the URL should be excluded, false otherwise. + */ + isUrlExcludedFromExternalProcessing(url) { + if (!url) return false; + + // Check both exclusion arrays for domain filtering + const externalFontExclusions = this.config.external_font_exclusions || []; + const preloadFontsExclusions = this.config.preload_fonts_exclusions || []; + const allExclusions = [...externalFontExclusions, ...preloadFontsExclusions]; + return allExclusions.some((exclusion) => url.includes(exclusion)); + } + /** * Checks if a font family or URL should be excluded from preloading. * @@ -247,9 +263,8 @@ class BeaconPreloadFonts { return false; } - // Check exclusions instead of allowlist - const exclusions = this.config.external_font_exclusions || []; - return !exclusions.some(exclusion => link.href.includes(exclusion)); + // Use the helper method for consistent exclusion logic + return !this.isUrlExcludedFromExternalProcessing(link.href); } catch (e) { return false; } @@ -427,6 +442,11 @@ class BeaconPreloadFonts { try { const importUrl = rule.href; + // Check if URL should be excluded based on external font exclusions + if (this.isUrlExcludedFromExternalProcessing(importUrl)) { + return; + } + // Prevent infinite loops by checking if URL already processed if (processedUrls.has(importUrl)) { return; @@ -474,6 +494,11 @@ class BeaconPreloadFonts { } catch (e) { // If we can't access cssRules due to CORS, try to process the stylesheet content directly if (e.name === 'SecurityError' && sheet.href) { + // Check if URL should be excluded based on external font exclusions + if (this.isUrlExcludedFromExternalProcessing(sheet.href)) { + return; + } + // Prevent infinite loops for CORS fallback too if (processedUrls.has(sheet.href)) { return; @@ -502,6 +527,11 @@ class BeaconPreloadFonts { while ((importMatch = importRegex.exec(cssText)) !== null) { const importUrl = new URL(importMatch[1], sheet.href).href; + // Check if URL should be excluded based on external font exclusions + if (this.isUrlExcludedFromExternalProcessing(importUrl)) { + continue; + } + // Prevent infinite loops if (processedUrls.has(importUrl)) { continue; @@ -553,6 +583,11 @@ class BeaconPreloadFonts { while ((importMatch = importRegex.exec(cssText)) !== null) { const importUrl = importMatch[1]; + // Check if URL should be excluded based on external font exclusions + if (this.isUrlExcludedFromExternalProcessing(importUrl)) { + continue; + } + // Prevent infinite loops if (processedUrls.has(importUrl)) { continue; diff --git a/test/BeaconPreloadFonts.test.js b/test/BeaconPreloadFonts.test.js index 481f665..3bd1e0c 100644 --- a/test/BeaconPreloadFonts.test.js +++ b/test/BeaconPreloadFonts.test.js @@ -138,6 +138,40 @@ describe('BeaconPreloadFonts', () => { sinon.restore(); // Restore sinon mocks }); + describe('isUrlExcludedFromExternalProcessing', () => { + it('should return false when no exclusions are configured', () => { + const result = beaconPreloadFonts.isUrlExcludedFromExternalProcessing('https://fonts.googleapis.com/css'); + assert.strictEqual(result, false); + }); + + it('should return true when URL matches external font exclusion', () => { + beaconPreloadFonts.config.external_font_exclusions = ['fonts.googleapis.com']; + const result = beaconPreloadFonts.isUrlExcludedFromExternalProcessing('https://fonts.googleapis.com/css'); + assert.strictEqual(result, true); + }); + + it('should return true when URL matches preload fonts exclusion', () => { + beaconPreloadFonts.config.preload_fonts_exclusions = ['fonts.googleapis.com']; + beaconPreloadFonts.config.external_font_exclusions = []; // explicitly empty + const result = beaconPreloadFonts.isUrlExcludedFromExternalProcessing('https://fonts.googleapis.com/css'); + assert.strictEqual(result, true); + }); + + it('should return false when URL does not match any exclusion', () => { + beaconPreloadFonts.config.external_font_exclusions = ['fonts.adobe.com']; + beaconPreloadFonts.config.preload_fonts_exclusions = ['Roboto']; + const result = beaconPreloadFonts.isUrlExcludedFromExternalProcessing('https://fonts.googleapis.com/css'); + assert.strictEqual(result, false); + }); + + it('should return false for null or undefined URLs', () => { + beaconPreloadFonts.config.external_font_exclusions = ['fonts.googleapis.com']; + assert.strictEqual(beaconPreloadFonts.isUrlExcludedFromExternalProcessing(null), false); + assert.strictEqual(beaconPreloadFonts.isUrlExcludedFromExternalProcessing(undefined), false); + assert.strictEqual(beaconPreloadFonts.isUrlExcludedFromExternalProcessing(''), false); + }); + }); + describe('isExcluded', () => { it('should return true when fontFamily exactly matches exclusion', () => { beaconPreloadFonts.config.preload_fonts_exclusions = ['Arial']; @@ -1117,6 +1151,18 @@ describe('BeaconPreloadFonts', () => { assert.strictEqual(result.styleSheets.length, 0); assert.deepStrictEqual(result.fontPairs, {}); }); + + it('should exclude links based on preload_fonts_exclusions config (backward compatibility)', async () => { + // Set preload_fonts_exclusions to exclude domain + beaconPreloadFonts.config.preload_fonts_exclusions = ['fonts.googleapis.com']; + beaconPreloadFonts.config.external_font_exclusions = []; // explicitly empty + + const result = await beaconPreloadFonts.externalStylesheetsDoc(); + + // Should exclude the link since preload_fonts_exclusions can also contain domains + assert.strictEqual(result.styleSheets.length, 0); + assert.deepStrictEqual(result.fontPairs, {}); + }); it('should process links from different origins only', async () => { // Set up links with same and different origins