Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Atrules update - Purge @font-face, Small refactor #48

Merged
merged 6 commits into from
Jan 30, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`],
font_face: 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,
font_face?: boolean
}

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

declare type AtRules = {
keyframes: Array<Object>,
font_face: 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,
font_face: 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: [],
font_face: []
}
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.font_face) 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.font_face) {
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.font_face) {
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.font_face && node.name === 'font-face') {
for (const { prop, value } of node.nodes) {
if (prop === 'font-family') {
this.atRules.font_face.push({
name: value,
node
})
}
}
return
}
}

/**
Expand Down