Skip to content

Commit

Permalink
Merge pull request #48 from FullHuman/atrules-update
Browse files Browse the repository at this point in the history
Atrules update - Purge @font-face, Small refactor
  • Loading branch information
Ffloriel authored Jan 30, 2018
2 parents b9bc37d + feed327 commit 3c5bb94
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 62 deletions.
20 changes: 20 additions & 0 deletions __tests__/purgecssDefault.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,26 @@ describe('purge methods with files and default extractor', () => {
expect(purgecssResult.includes('@keyframes rotateAni')).toBe(true)
})
})

// Font Face
describe('purge unused font-face', () => {
let purgecssResult
beforeAll(() => {
purgecssResult = new Purgecss({
content: [`${root}font_face/font_face.html`],
css: [`${root}font_face/font_face.css`],
fontFace: true
}).purge()[0].css
})
it("keep @font-face 'Cerebri Sans'", () => {
expect(purgecssResult.includes(`src: url('../fonts/CerebriSans-Regular.eot?')`)).toBe(
true
)
})
it("remove @font-face 'OtherFont'", () => {
expect(purgecssResult.includes(`src: url('xxx')`)).toBe(false)
})
})
})

describe('purge methods with raw content and default extractor', () => {
Expand Down
10 changes: 9 additions & 1 deletion __tests__/test_examples/font_face/font_face.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
src: url('../fonts/CerebriSans-Regular.eot?') format('eot'), url('../fonts/CerebriSans-Regular.otf') format('opentype'), url('../fonts/CerebriSans-Regular.svg#Cerebri_Sans') format('svg'), url('../fonts/CerebriSans-Regular.ttf') format('truetype'), url('../fonts/CerebriSans-Regular.woff') format('woff');
}

@font-face {
font-family: 'OtherFont';
font-weight: 400;
font-style: normal;
src: url('xxx')
}

.unused {
color: black;
}

used {
color: red;
}
font-family: 'Cerebri Sans';
}
18 changes: 18 additions & 0 deletions __tests__/test_examples/keyframes/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
@-webkit-keyframes rotateAni {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
@keyframes rotateAni {
from {
transform: rotate(0deg);
Expand All @@ -11,6 +19,16 @@
animation: rotateAni 200ms ease-out both;
}

@-webkit-keyframes flashAni
{
from, 50%, to {
opacity: 1;
}

25%, 75% {
opacity: 0;
}
}
@keyframes flashAni
{
from, 50%, to {
Expand Down
4 changes: 4 additions & 0 deletions __tests__/test_examples/keyframes/keyframes.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@
25%, 75% {
opacity: 0.5;
}
}

.flash {
animation: flash
}
8 changes: 7 additions & 1 deletion flow-typed/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ declare type Options = {
info?: boolean,
rejected?: boolean,
legacy?: boolean,
keyframes?: boolean
keyframes?: boolean,
fontFace?: boolean
}

declare type ExtractorsObj = {
Expand All @@ -29,3 +30,8 @@ declare type ResultPurge = {
file: ?string,
css: string
}

declare type AtRules = {
keyframes: Array<Object>,
fontFace: Array<Object>
}
2 changes: 1 addition & 1 deletion lib/purgecss.es.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/purgecss.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/constants/defaultOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const defaultOptions: Options = {
info: false,
rejected: false,
legacy: false,
keyframes: false
keyframes: false,
fontFace: false
}

export default defaultOptions
174 changes: 117 additions & 57 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ import LegacyExtractor from './Extractors/LegacyExtractor'
class Purgecss {
options: Options
root: Object
atRules: Object = {
keyframes: {}
atRules: AtRules = {
keyframes: [],
fontFace: []
}
usedAnimations: Set<string> = new Set()
usedFontFaces: Set<string> = new Set()
selectorsRemoved: Set<string> = new Set()

constructor(options: Options | string) {
if (typeof options === 'string' || typeof options === 'undefined')
Expand Down Expand Up @@ -128,6 +132,9 @@ class Purgecss {
// purge keyframes
if (this.options.keyframes) this.removeUnusedKeyframes()

// purge font face
if (this.options.fontFace) this.removeUnusedFontFaces()

sources.push({
file,
css: this.root.toString()
Expand All @@ -138,24 +145,28 @@ class Purgecss {
}

/**
* Remove Keyframes that are never used
* Remove Keyframes that were never used
*/
removeUnusedKeyframes() {
const usedAnimations = new Set()
for (const node of this.atRules.keyframes) {
const nodeName = node.params
const used = this.usedAnimations.has(nodeName)

// list all used animations
this.root.walkDecls(/animation/, decl => {
for (const word of decl.value.split(' ')) {
usedAnimations.add(word)
if (!used) {
node.remove()
}
})
}
}

// remove unused keyframes
for (const nodeName in this.atRules.keyframes) {
const keyframeUsed = usedAnimations.has(nodeName)
/**
* Remove Font-Faces that were never used
*/
removeUnusedFontFaces() {
for (const { node, name } of this.atRules.fontFace) {
const used = this.usedFontFaces.has(name)

if (!keyframeUsed) {
this.atRules.keyframes[nodeName].remove()
if (!used) {
node.remove()
}
}
}
Expand Down Expand Up @@ -237,59 +248,108 @@ class Purgecss {
* @param {*} selectors selectors used in content files
*/
getSelectorsCss(selectors: Set<string>) {
this.root.walkRules(node => {
const annotation = node.prev()
if (this.isIgnoreAnnotation(annotation)) return
node.selector = selectorParser(selectorsParsed => {
selectorsParsed.walk(selector => {
const selectorsInRule = []
if (selector.type === 'selector') {
// if inside :not pseudo class, ignore
this.root.walk(node => {
if (node.type === 'rule') {
return this.evaluateRule(node, selectors)
}
if (node.type === 'atrule') {
return this.evaluateAtRule(node)
}
})
}

/**
* Evaluate css selector and decide if it should be removed or not
* @param {AST} node postcss ast node
* @param {Set} selectors selectors used in content files
*/
evaluateRule(node: Object, selectors: Set<string>) {
const annotation = node.prev()
if (this.isIgnoreAnnotation(annotation)) return

let keepSelector = true
node.selector = selectorParser(selectorsParsed => {
selectorsParsed.walk(selector => {
const selectorsInRule = []
if (selector.type === 'selector') {
// if inside :not pseudo class, ignore
if (
selector.parent &&
selector.parent.value === ':not' &&
selector.parent.type === 'pseudo'
) {
return
}
for (const { type, value } of selector.nodes) {
if (
selector.parent &&
selector.parent.value === ':not' &&
selector.parent.type === 'pseudo'
SELECTOR_STANDARD_TYPES.includes(type) &&
typeof value !== 'undefined'
) {
return
}
for (const { type, value } of selector.nodes) {
if (
SELECTOR_STANDARD_TYPES.includes(type) &&
typeof value !== 'undefined'
) {
selectorsInRule.push(value)
} else if (
type === 'tag' &&
!/[+]|(even)|(odd)|^from$|^to$|^\d/.test(value)
) {
// test if we do not have a pseudo class parameter (e.g. 2n in :nth-child(2n))
selectorsInRule.push(value)
}
selectorsInRule.push(value)
} else if (
type === 'tag' &&
!/[+]|(even)|(odd)|^from$|^to$|^\d/.test(value)
) {
// test if we do not have a pseudo class parameter (e.g. 2n in :nth-child(2n))
selectorsInRule.push(value)
}
}

let keepSelector = this.shouldKeepSelector(selectors, selectorsInRule)
keepSelector = this.shouldKeepSelector(selectors, selectorsInRule)

if (!keepSelector) {
selector.remove()
if (!keepSelector) {
selector.remove()
}
}
})
}).processSync(node.selector)

// loop declarations
if (keepSelector) {
for (const { prop, value } of node.nodes) {
if (this.options.keyframes) {
if (prop === 'animation' || prop === 'animation-name') {
for (const word of value.split(' ')) {
this.usedAnimations.add(word)
}
}
})
}).processSync(node.selector)
}
if (this.options.fontFace) {
if (prop === 'font-family') {
this.usedFontFaces.add(value)
}
}
}
}

const parent = node.parent
const parent = node.parent

// register atrules to purgecss
if (
parent.type === 'atrule' &&
(this.options.keyframes && parent.name === 'keyframes')
) {
this.atRules.keyframes[parent.params] = parent
}
// Remove empty rules
if (!node.selector) node.remove()
if (this.isRuleEmpty(parent)) parent.remove()
}

// Remove empty rules
if (!node.selector) node.remove()
if (this.isRuleEmpty(parent)) parent.remove()
})
/**
* Evaluate at-rule and register it for future reference
* @param {AST} node postcss ast node
*/
evaluateAtRule(node: Object) {
if (this.options.keyframes && node.name.endsWith('keyframes')) {
this.atRules.keyframes.push(node)
return
}

if (this.options.fontFace && node.name === 'font-face') {
for (const { prop, value } of node.nodes) {
if (prop === 'font-family') {
this.atRules.fontFace.push({
name: value,
node
})
}
}
return
}
}

/**
Expand Down

0 comments on commit 3c5bb94

Please sign in to comment.