3 var promise = require('bluebird'),
4 phantom = require('./phantom.js'),
5 postcss = require('postcss'),
7 /* Some styles are applied only with user interaction, and therefore its
8 * selectors cannot be used with querySelectorAll.
9 * http://www.w3.org/TR/2001/CR-css3-selectors-20011113/
11 var dePseudify = (function () {
12 var ignoredPseudos = [
16 ':hover', ':active', ':focus',
17 /* UI element states */
18 ':enabled', ':disabled', ':checked', ':indeterminate',
20 '::first-line', '::first-letter', '::selection', '::before', '::after',
23 /* CSS2 pseudo elements */
25 /* Vendor-specific pseudo-elements:
26 * https://developer.mozilla.org/ja/docs/Glossary/Vendor_Prefix
28 '::?-(?:moz|ms|webkit|o)-[a-z0-9-]+'
30 pseudosRegex = new RegExp(ignoredPseudos.join('|'), 'g');
32 return function (selector) {
33 return selector.replace(pseudosRegex, '');
38 * Private function used in filterUnusedRules.
39 * @param {Array} selectors CSS selectors created by the CSS parser
40 * @param {Array} ignore List of selectors to be ignored
41 * @param {Array} usedSelectors List of Selectors found in the PhantomJS pages
42 * @return {Array} The selectors matched in the DOMs
44 function filterUnusedSelectors(selectors, ignore, usedSelectors) {
45 /* There are some selectors not supported for matching, like
47 * They should be removed only if the parent is not found.
48 * Example: '.clearfix:before' should be removed only if there
51 return selectors.filter(function (selector) {
52 selector = dePseudify(selector);
53 /* TODO: process @-rules */
54 if (selector[0] === '@') {
57 for (var i = 0, len = ignore.length; i < len; ++i) {
58 if (_.isRegExp(ignore[i]) && ignore[i].test(selector)) {
61 if (ignore[i] === selector) {
65 return usedSelectors.indexOf(selector) !== -1;
70 * Find which animations are used
71 * @param {Object} css The postcss.Root node
74 function getUsedAnimations(css) {
75 var usedAnimations = [];
76 css.walkDecls(function (decl) {
77 if (_.endsWith(decl.prop, 'animation-name')) {
78 /* Multiple animations, separated by comma */
79 usedAnimations.push.apply(usedAnimations, postcss.list.comma(decl.value));
80 } else if (_.endsWith(decl.prop, 'animation')) {
81 /* Support multiple animations */
82 postcss.list.comma(decl.value).forEach(function (anim) {
83 /* If declared as animation, it should be in the form 'name Xs etc..' */
84 usedAnimations.push(postcss.list.space(anim)[0]);
88 return usedAnimations;
92 * Filter @keyframes that are not used
93 * @param {Object} css The postcss.Root node
94 * @param {Array} animations
95 * @param {Array} unusedRules
98 function filterKeyframes(css, animations, unusedRules) {
99 css.walkAtRules(/keyframes$/, function (atRule) {
100 if (animations.indexOf(atRule.params) === -1) {
101 unusedRules.push(atRule);
108 * Filter rules with no selectors remaining
109 * @param {Object} css The postcss.Root node
112 function filterEmptyAtRules(css) {
113 /* Filter media queries with no remaining rules */
114 css.walkAtRules(function (atRule) {
115 if (atRule.name === 'media' && atRule.nodes.length === 0) {
122 * Find which selectors are used in {pages}
123 * @param {Array} pages List of PhantomJS pages
124 * @param {Object} css The postcss.Root node
127 function getUsedSelectors(page, css) {
128 var usedSelectors = [];
129 css.walkRules(function (rule) {
130 usedSelectors = _.concat(usedSelectors, rule.selectors.map(dePseudify));
132 // TODO: Can this be written in a more straightforward fashion?
133 return promise.map(usedSelectors, function (selector) {
135 }).then(function(selector) {
136 return phantom.findAll(page, selector);
141 * Get all the selectors mentioned in {css}
142 * @param {Object} css The postcss.Root node
145 function getAllSelectors(css) {
147 css.walkRules(function (rule) {
148 selectors.concat(rule.selector);
154 * Remove css rules not used in the dom
155 * @param {Array} pages List of PhantomJS pages
156 * @param {Object} css The postcss.Root node
157 * @param {Array} ignore List of selectors to be ignored
158 * @param {Array} usedSelectors List of selectors that are found in {pages}
159 * @return {Object} A css_parse-compatible stylesheet
161 function filterUnusedRules(pages, css, ignore, usedSelectors) {
162 var ignoreNextRule = false,
167 * { selectors: [ '...', '...' ],
168 * declarations: [ { property: '...', value: '...' } ]
170 * Two steps: filter the unused selectors for each rule,
171 * filter the rules with no selectors
173 ignoreNextRule = false;
174 css.walk(function (rule) {
175 if (rule.type === 'comment') {
176 // ignore next rule while using comment `/* uncss:ignore */`
177 if (/^!?\s?uncss:ignore\s?$/.test(rule.text)) {
178 ignoreNextRule = true;
180 } else if (rule.type === 'rule') {
181 if (rule.parent.type === 'atrule' && _.endsWith(rule.parent.name, 'keyframes')) {
182 // Don't remove animation keyframes that have selector names of '30%' or 'to'
185 if (ignoreNextRule) {
186 ignoreNextRule = false;
187 ignore = ignore.concat(rule.selectors);
190 usedRuleSelectors = filterUnusedSelectors(
195 unusedRuleSelectors = rule.selectors.filter(function (selector) {
196 return usedRuleSelectors.indexOf(selector) < 0;
198 if (unusedRuleSelectors && unusedRuleSelectors.length) {
201 selectors: unusedRuleSelectors,
202 position: rule.source
205 if (usedRuleSelectors.length === 0) {
208 rule.selectors = usedRuleSelectors;
213 /* Filter the @media rules with no rules */
214 filterEmptyAtRules(css);
216 /* Filter unused @keyframes */
217 filterKeyframes(css, getUsedAnimations(css), unusedRules);
223 * Main exposed function
224 * @param {Array} pages List of PhantomJS pages
225 * @param {Object} css The postcss.Root node
226 * @param {Array} ignore List of selectors to be ignored
229 module.exports = function uncss(pages, css, ignore) {
230 return promise.map(pages, function (page) {
231 return getUsedSelectors(page, css);
232 }).then(function (usedSelectors) {
233 usedSelectors = _.flatten(usedSelectors);
234 var filteredCss = filterUnusedRules(pages, css, ignore, usedSelectors);
235 return [filteredCss, {
236 /* Get the selectors for the report */
237 all: getAllSelectors(css),