From b7dbc23ff1952dd4de931032c0317bb4f172bae9 Mon Sep 17 00:00:00 2001 From: Jonathan Felchlin Date: Tue, 26 Mar 2019 22:30:48 -0700 Subject: [PATCH] fix: Discarding used font-families due to mixed quotation types --- .../src/__tests__/index.js | 30 +++++++++- packages/postcss-discard-unused/src/index.js | 56 ++++++++++++++----- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/packages/postcss-discard-unused/src/__tests__/index.js b/packages/postcss-discard-unused/src/__tests__/index.js index 56bb24be5..911b88a39 100644 --- a/packages/postcss-discard-unused/src/__tests__/index.js +++ b/packages/postcss-discard-unused/src/__tests__/index.js @@ -86,10 +86,34 @@ test( ); test( - 'should not be responsible for normalising fonts', - processCSS, + 'should normalize fonts', + passthroughCSS, '@font-face {font-family:"Does Exist";src:url("fonts/does-exist.ttf") format("truetype")}body{font-family:Does Exist}', - 'body{font-family:Does Exist}' +); + +test( + 'should work with mixed quote styles', + passthroughCSS, + '@font-face {font-family:"Does Exist";src:url("fonts/does-exist.ttf") format("truetype")}@font-face {font-family:\'DoesExist\';src:url("fonts/does-exist.ttf") format("truetype")}body{font-family: \'Does Exist\'; font: 10px DoesExist}' +); + +test( + 'should work spaces in font lists', + passthroughCSS, + '@font-face {font-family:"Does Exist";src: url("fonts/does-exist.ttf") format("truetype")} @font-face {font-family:\'DoesExist\';src:url("fonts/does-exist.ttf") format("truetype")} body { font-family: "Does Exist", Helvetica, Arial, sans-serif; font: 10px/1.5 DoesExist, Helvetica, Arial, sans-serif }' +); + +test( + 'should work with weird font names', + passthroughCSS, + '@font-face {font-family:"ൠ😳ฬ𝔢IяĎ 🐉💩👍Iñtërnâtiônàlizætiøn☃💩";src: url("fonts/weird.woff") format("truetype")}body {font-family: "ൠ😳ฬ𝔢IяĎ 🐉💩👍Iñtërnâtiônàlizætiøn☃💩"}' +); + +test( + "should handle font shorthands that don't include a font-family", + processCSS, + '@font-face {font-family:"Does Exist";src: url("fonts/does-exist.ttf") format("truetype")}body{font: italic bold 10px/1.5}', + 'body{font: italic bold 10px/1.5}' ); test( diff --git a/packages/postcss-discard-unused/src/index.js b/packages/postcss-discard-unused/src/index.js index 7bc03b6ee..8d97aca65 100644 --- a/packages/postcss-discard-unused/src/index.js +++ b/packages/postcss-discard-unused/src/index.js @@ -8,6 +8,40 @@ const atrule = 'atrule'; const decl = 'decl'; const rule = 'rule'; +const FONT_SIZE_KEYWORDS = [ + '(x{1,2}-)?(small|large)', + '(larg|small)er', + 'medium', + 'auto', + 'inherit', + 'initial', + 'unset', +]; +const SYSTEM_FONTS = [ + 'caption', + 'icon', + 'menu', + 'message-box', + 'small-caption', + 'status-bar', +]; +const CSS_UNITS_RE = '[+-]?\\d+(\\.\\d*)?(px|em|ex|%|in|cm|mm|pt|pc)?'; +// Matches all properties in a font shorthand value through the font-size, +// which is required and is always the last value before the font-family +const MATCH_NON_FONT_FAMILY_PROPERTIES = new RegExp( + `(.*(^| )(${FONT_SIZE_KEYWORDS.join('|')}|${CSS_UNITS_RE})( *\\/ *${CSS_UNITS_RE})?|` + + `(${SYSTEM_FONTS.join('|')})?( |$))` +); + +function getFontFamilyFromShorthand (font) { + return font.replace(MATCH_NON_FONT_FAMILY_PROPERTIES, ''); +} + +function normalizeFontName (value) { + // Ignore casing and wrapping quotes when checking for font use. + return value.toLowerCase().replace(/^\s*(['"])(.*?)(\1)\s*$/, '$2'); +} + function addValues (cache, {value}) { return comma(value).reduce((memo, val) => [...memo, ...space(val)], cache); } @@ -36,24 +70,15 @@ function filterNamespace ({atRules, rules}) { }); } -function hasFont (fontFamily, cache) { - return comma(fontFamily).some(font => cache.some(c => ~c.indexOf(font))); -} - // fonts have slightly different logic function filterFont ({atRules, values}) { values = uniqs(values); atRules.forEach(r => { - const families = r.nodes.filter(({prop}) => prop === 'font-family'); - // Discard the @font-face if it has no font-family - if (!families.length) { - return r.remove(); + const fontFamilyRule = r.nodes.find(({prop}) => prop === 'font-family'); + // Discard the @font-face if it has no font-family rule or if it is unused + if (!fontFamilyRule || !values.includes(normalizeFontName(fontFamilyRule.value))) { + r.remove(); } - families.forEach(family => { - if (!hasFont(family.value.toLowerCase(), values)) { - r.remove(); - } - }); }); } @@ -94,7 +119,10 @@ export default plugin('postcss-discard-unused', opts => { counterStyleCache.values = addValues(counterStyleCache.values, node); } if (fontFace && node.parent.type === rule && /font(|-family)/.test(prop)) { - fontCache.values = fontCache.values.concat(comma(node.value.toLowerCase())); + const fontFamilies = comma( + prop === 'font' ? getFontFamilyFromShorthand(node.value) : node.value + ); + fontCache.values.push(...fontFamilies.map(normalizeFontName)); } if (keyframes && /animation/.test(prop)) { keyframesCache.values = addValues(keyframesCache.values, node);