diff --git a/lib/gatherStylesheetsWithPredicates.js b/lib/gatherStylesheetsWithPredicates.js index 7325fa17..dd2e48dd 100644 --- a/lib/gatherStylesheetsWithPredicates.js +++ b/lib/gatherStylesheetsWithPredicates.js @@ -19,6 +19,7 @@ module.exports = function gatherStylesheetsWithPredicates( type: { $in: [ 'HtmlStyle', + 'SvgStyle', 'CssImport', 'HtmlConditionalComment', 'HtmlNoscript', diff --git a/lib/getCssRulesByProperty.js b/lib/getCssRulesByProperty.js index 28adbd1f..36907f9b 100644 --- a/lib/getCssRulesByProperty.js +++ b/lib/getCssRulesByProperty.js @@ -41,7 +41,7 @@ function getCssRulesByProperty(properties, cssSource, existingPredicates) { existingPredicates = existingPredicates || {}; const parseTree = postcss.parse(cssSource); - let defaultNamespaceURI = 'http://www.w3.org/1999/xhtml'; + let defaultNamespaceURI; parseTree.walkAtRules('namespace', (rule) => { const fragments = rule.params.split(/\s+/); if (fragments.length === 1) { diff --git a/lib/subfont.js b/lib/subfont.js index c9f75749..a1d9eb59 100644 --- a/lib/subfont.js +++ b/lib/subfont.js @@ -212,7 +212,7 @@ module.exports = async function subfont( isInline: false, isLoaded: true, type: { - $in: ['Html', 'Css', 'JavaScript'], + $in: ['Html', 'Svg', 'Css', 'JavaScript'], }, })) { sumSizesBefore += asset.rawSrc.length; @@ -233,7 +233,7 @@ module.exports = async function subfont( isInline: false, isLoaded: true, type: { - $in: ['Html', 'Css', 'JavaScript'], + $in: ['Html', 'Svg', 'Css', 'JavaScript'], }, })) { sumSizesAfter += asset.rawSrc.length; @@ -370,7 +370,9 @@ module.exports = async function subfont( } } log( - `HTML/JS/CSS size increase: ${prettyBytes(sumSizesAfter - sumSizesBefore)}` + `HTML/SVG/JS/CSS size increase: ${prettyBytes( + sumSizesAfter - sumSizesBefore + )}` ); log(`Total savings: ${prettyBytes(totalSavings)}`); if (!dryRun) { diff --git a/lib/subsetFonts.js b/lib/subsetFonts.js index e41b31f1..ed7cb749 100644 --- a/lib/subsetFonts.js +++ b/lib/subsetFonts.js @@ -578,11 +578,14 @@ async function subsetFonts( // Collect texts by page const memoizedGetCssRulesByProperty = memoizeSync(getCssRulesByProperty); - const htmlAssets = assetGraph.findAssets({ type: 'Html', isInline: false }); + const htmlAssets = assetGraph.findAssets({ + type: { $in: ['Html', 'Svg'] }, + isInline: false, + }); const traversalRelationQuery = { $or: [ { - type: { $in: ['HtmlStyle', 'CssImport'] }, + type: { $in: ['HtmlStyle', 'SvgStyle', 'CssImport'] }, }, { to: { @@ -672,7 +675,7 @@ async function subsetFonts( htmlAsset ), getCssRulesByProperty: memoizedGetCssRulesByProperty, - htmlAsset, + asset: htmlAsset, }); if (headlessBrowser) { textByProps.push(...(await headlessBrowser.tracePage(htmlAsset))); @@ -870,7 +873,7 @@ These glyphs are used on your site, but they don't exist in the font you applied accumulatedFontFaceDeclarations, } of htmlAssetTextsWithProps) { const insertionPoint = assetGraph.findRelations({ - type: 'HtmlStyle', + type: `${htmlAsset.type}Style`, from: htmlAsset, })[0]; const subsetFontUsages = fontUsages.filter( @@ -1051,21 +1054,23 @@ These glyphs are used on your site, but they don't exist in the font you applied // - https://caniuse.com/#search=woff2 // - https://caniuse.com/#search=preload - htmlAsset.addRelation( - { - type: 'HtmlPreloadLink', - hrefType, - to: fontAsset, - as: 'font', - }, - 'before', - insertionPoint - ); + if (htmlAsset.type === 'Html') { + htmlAsset.addRelation( + { + type: 'HtmlPreloadLink', + hrefType, + to: fontAsset, + as: 'font', + }, + 'before', + insertionPoint + ); + } } } const cssRelation = htmlAsset.addRelation( { - type: 'HtmlStyle', + type: `${htmlAsset.type}Style`, hrefType: inlineCss ? 'inline' : hrefType, to: cssAsset, }, @@ -1151,18 +1156,20 @@ These glyphs are used on your site, but they don't exist in the font you applied cssAsset.url = cssAssetUrl; } - // Create a that asyncLoadStyleRelationWithFallback can convert to async with noscript fallback: - const fallbackHtmlStyle = htmlAsset.addRelation({ - type: 'HtmlStyle', - to: cssAsset, - }); + if (htmlAsset.type === 'Html') { + // Create a that asyncLoadStyleRelationWithFallback can convert to async with noscript fallback: + const fallbackHtmlStyle = htmlAsset.addRelation({ + type: 'HtmlStyle', + to: cssAsset, + }); - asyncLoadStyleRelationWithFallback( - htmlAsset, - fallbackHtmlStyle, - hrefType - ); - relationsToRemove.add(fallbackHtmlStyle); + asyncLoadStyleRelationWithFallback( + htmlAsset, + fallbackHtmlStyle, + hrefType + ); + relationsToRemove.add(fallbackHtmlStyle); + } } } @@ -1200,11 +1207,13 @@ These glyphs are used on your site, but they don't exist in the font you applied if (googleFontStylesheetRelation.type === 'CssImport') { // Gather Html parents. Relevant if we are dealing with CSS @import relations htmlParents = getParents(googleFontStylesheetRelation.to, { - type: 'Html', + type: { $in: ['Html', 'Svg'] }, isInline: false, isLoaded: true, }); - } else if (googleFontStylesheetRelation.from.type === 'Html') { + } else if ( + ['Html', 'Svg'].includes(googleFontStylesheetRelation.from.type) + ) { htmlParents = [googleFontStylesheetRelation.from]; } else { htmlParents = []; @@ -1235,18 +1244,20 @@ These glyphs are used on your site, but they don't exist in the font you applied } const selfHostedFallbackRelation = htmlParent.addRelation( { - type: 'HtmlStyle', + type: `${htmlParent.type}Style`, to: selfHostedGoogleFontsCssAsset, hrefType, }, 'lastInBody' ); relationsToRemove.add(selfHostedFallbackRelation); - asyncLoadStyleRelationWithFallback( - htmlParent, - selfHostedFallbackRelation, - hrefType - ); + if (htmlParent.type === 'Html') { + asyncLoadStyleRelationWithFallback( + htmlParent, + selfHostedFallbackRelation, + hrefType + ); + } } relationsToRemove.add(googleFontStylesheetRelation); } @@ -1276,7 +1287,37 @@ These glyphs are used on your site, but they don't exist in the font you applied } let customPropertyDefinitions; // Avoid computing this unless necessary - // Inject subset font name before original webfont + // Inject subset font name before original webfont in SVG font-family attributes + const svgAssets = assetGraph.findAssets({ type: 'Svg' }); + for (const svgAsset of svgAssets) { + let changesMade = false; + for (const element of Array.from( + svgAsset.parseTree.querySelectorAll('[font-family]') + )) { + const fontFamilies = cssListHelpers.splitByCommas( + element.getAttribute('font-family') + ); + for (let i = 0; i < fontFamilies.length; i += 1) { + const subsetFontFamily = + webfontNameMap[fontFamily.parse(fontFamilies[i])[0].toLowerCase()]; + if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) { + fontFamilies.splice( + i, + omitFallbacks ? 1 : 0, + cssQuoteIfNecessary(subsetFontFamily) + ); + i += 1; + element.setAttribute('font-family', fontFamilies.join(', ')); + changesMade = true; + } + } + } + if (changesMade) { + svgAsset.markDirty(); + } + } + + // Inject subset font name before original webfont in CSS const cssAssets = assetGraph.findAssets({ type: 'Css', isLoaded: true, diff --git a/test/getCssRulesByProperty.js b/test/getCssRulesByProperty.js index 8c76c46d..a68b893f 100644 --- a/test/getCssRulesByProperty.js +++ b/test/getCssRulesByProperty.js @@ -45,7 +45,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'color', value: 'red', @@ -54,7 +54,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h2', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'color', value: 'blue', @@ -76,7 +76,7 @@ describe('getCssRulesByProperty', function () { { selector: undefined, predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [1, 0, 0, 0], prop: 'color', value: 'red', @@ -99,7 +99,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'color', value: 'red', @@ -108,7 +108,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'color', value: 'blue', @@ -135,7 +135,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font', value: '15px serif', @@ -146,7 +146,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font', value: '15px serif', @@ -170,7 +170,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font', value: '15px serif', @@ -181,7 +181,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font', value: '15px serif', @@ -192,7 +192,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font', value: '15px serif', @@ -203,7 +203,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font', value: '15px serif', @@ -227,7 +227,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font', value: '15px serif', @@ -238,7 +238,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font-size', value: '10px', @@ -247,7 +247,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font', value: '15px serif', @@ -256,7 +256,7 @@ describe('getCssRulesByProperty', function () { { selector: 'h1', predicates: {}, - namespaceURI: 'http://www.w3.org/1999/xhtml', + namespaceURI: undefined, specificityArray: [0, 0, 0, 1], prop: 'font-size', value: '20px', diff --git a/test/subsetFonts.js b/test/subsetFonts.js index b7f2ed0a..9f02583b 100644 --- a/test/subsetFonts.js +++ b/test/subsetFonts.js @@ -3276,4 +3276,154 @@ describe('subsetFonts', function () { ]); }); }); + + describe('with SVG using webfonts', function () { + describe('in a standalone SVG', function () { + it('should trace the correct characters and patch up the stylesheet', async function () { + const assetGraph = new AssetGraph({ + root: pathModule.resolve( + __dirname, + '../testdata/subsetFonts/svg/img-element/' + ), + }); + await assetGraph.loadAssets('index.html'); + await assetGraph.populate({ + followRelations: { + crossorigin: false, + }, + }); + const result = await subsetFonts(assetGraph); + + expect(result, 'to satisfy', { + fontInfo: [ + { + fontUsages: [ + { + text: ' !,Hdelorw', + props: { + 'font-stretch': 'normal', + 'font-weight': '400', + 'font-style': 'normal', + 'font-family': 'Roboto', + src: expect.it('to contain', "format('woff')"), + }, + }, + ], + }, + ], + }); + + const svgAsset = assetGraph.findAssets({ type: 'Svg' })[0]; + expect( + svgAsset.text, + 'to contain', + 'Hello, world!' + ); + + const svgStyle = assetGraph.findRelations({ type: 'SvgStyle' })[0]; + expect(svgStyle, 'to be defined'); + expect( + svgStyle.to.text, + 'to contain', + '@font-face{font-family:Roboto__subset;' + ); + }); + }); + + describe('within HTML', function () { + describe('using webfonts defined in a stylesheet in the HTML', function () { + it('should trace the correct characters and patch up the font-family attribute', async function () { + const assetGraph = new AssetGraph({ + root: pathModule.resolve( + __dirname, + '../testdata/subsetFonts/svg/inline-in-html-with-html-font-face/' + ), + }); + const [htmlAsset] = await assetGraph.loadAssets('index.html'); + await assetGraph.populate({ + followRelations: { + crossorigin: false, + }, + }); + const result = await subsetFonts(assetGraph); + + expect(result, 'to satisfy', { + fontInfo: [ + { + fontUsages: [ + { + text: ' !,Hdelorw', + props: { + 'font-stretch': 'normal', + 'font-weight': '400', + 'font-style': 'normal', + 'font-family': 'Roboto', + src: expect.it('to contain', "format('woff')"), + }, + }, + ], + }, + ], + }); + + expect( + htmlAsset.text, + 'to contain', + 'Hello, world!' + ); + }); + }); + + describe('using webfonts defined in a stylesheet defined in the SVG', function () { + it('should trace the correct characters and patch up the SVG stylesheet', async function () { + const assetGraph = new AssetGraph({ + root: pathModule.resolve( + __dirname, + '../testdata/subsetFonts/svg/inline-in-html-with-own-font-face/' + ), + }); + const [htmlAsset] = await assetGraph.loadAssets('index.html'); + await assetGraph.populate({ + followRelations: { + crossorigin: false, + }, + }); + const result = await subsetFonts(assetGraph); + + expect(result, 'to satisfy', { + fontInfo: [ + { + fontUsages: [ + { + text: ' !,Hdelorw', + props: { + 'font-stretch': 'normal', + 'font-weight': '400', + 'font-style': 'normal', + 'font-family': 'Roboto', + src: expect.it('to contain', "format('woff')"), + }, + }, + ], + }, + ], + }); + + expect( + htmlAsset.text, + 'to contain', + 'Hello, world!' + ); + + const svgStyle = assetGraph.findRelations({ type: 'SvgStyle' })[0]; + expect(svgStyle, 'to be defined'); + expect( + svgStyle.to.text, + 'to contain', + '@font-face{font-family:Roboto__subset;' + ); + }); + }); + }); + }); }); diff --git a/testdata/subsetFonts/svg/img-element/KFOmCnqEu92Fr1Mu4mxM.woff b/testdata/subsetFonts/svg/img-element/KFOmCnqEu92Fr1Mu4mxM.woff new file mode 100644 index 00000000..92dfacc6 Binary files /dev/null and b/testdata/subsetFonts/svg/img-element/KFOmCnqEu92Fr1Mu4mxM.woff differ diff --git a/testdata/subsetFonts/svg/img-element/image.svg b/testdata/subsetFonts/svg/img-element/image.svg new file mode 100644 index 00000000..d4253a77 --- /dev/null +++ b/testdata/subsetFonts/svg/img-element/image.svg @@ -0,0 +1,13 @@ + + + + + Hello, world! + diff --git a/testdata/subsetFonts/svg/img-element/index.html b/testdata/subsetFonts/svg/img-element/index.html new file mode 100644 index 00000000..0c5f8c1a --- /dev/null +++ b/testdata/subsetFonts/svg/img-element/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/testdata/subsetFonts/svg/inline-in-html-with-html-font-face/KFOmCnqEu92Fr1Mu4mxM.woff b/testdata/subsetFonts/svg/inline-in-html-with-html-font-face/KFOmCnqEu92Fr1Mu4mxM.woff new file mode 100644 index 00000000..92dfacc6 Binary files /dev/null and b/testdata/subsetFonts/svg/inline-in-html-with-html-font-face/KFOmCnqEu92Fr1Mu4mxM.woff differ diff --git a/testdata/subsetFonts/svg/inline-in-html-with-html-font-face/index.html b/testdata/subsetFonts/svg/inline-in-html-with-html-font-face/index.html new file mode 100644 index 00000000..022104d9 --- /dev/null +++ b/testdata/subsetFonts/svg/inline-in-html-with-html-font-face/index.html @@ -0,0 +1,17 @@ + + + + + + + Hello, world! + + + diff --git a/testdata/subsetFonts/svg/inline-in-html-with-own-font-face/KFOmCnqEu92Fr1Mu4mxM.woff b/testdata/subsetFonts/svg/inline-in-html-with-own-font-face/KFOmCnqEu92Fr1Mu4mxM.woff new file mode 100644 index 00000000..92dfacc6 Binary files /dev/null and b/testdata/subsetFonts/svg/inline-in-html-with-own-font-face/KFOmCnqEu92Fr1Mu4mxM.woff differ diff --git a/testdata/subsetFonts/svg/inline-in-html-with-own-font-face/index.html b/testdata/subsetFonts/svg/inline-in-html-with-own-font-face/index.html new file mode 100644 index 00000000..a69e4be0 --- /dev/null +++ b/testdata/subsetFonts/svg/inline-in-html-with-own-font-face/index.html @@ -0,0 +1,19 @@ + + + + + + + + + Hello, world! + + +