Security update for permissions_by_term
[yaffs-website] / node_modules / uncss / src / lib.js
1 'use strict';
2
3 var promise = require('bluebird'),
4     phantom = require('./phantom.js'),
5     postcss = require('postcss'),
6     _ = require('lodash');
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/
10  */
11 var dePseudify = (function () {
12     var ignoredPseudos = [
13             /* link */
14             ':link', ':visited',
15             /* user action */
16             ':hover', ':active', ':focus',
17             /* UI element states */
18             ':enabled', ':disabled', ':checked', ':indeterminate',
19             /* pseudo elements */
20             '::first-line', '::first-letter', '::selection', '::before', '::after',
21             /* pseudo classes */
22             ':target',
23             /* CSS2 pseudo elements */
24             ':before', ':after',
25             /* Vendor-specific pseudo-elements:
26              * https://developer.mozilla.org/ja/docs/Glossary/Vendor_Prefix
27              */
28             '::?-(?:moz|ms|webkit|o)-[a-z0-9-]+'
29         ],
30         pseudosRegex = new RegExp(ignoredPseudos.join('|'), 'g');
31
32     return function (selector) {
33         return selector.replace(pseudosRegex, '');
34     };
35 }());
36
37 /**
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
43  */
44 function filterUnusedSelectors(selectors, ignore, usedSelectors) {
45     /* There are some selectors not supported for matching, like
46      *   :before, :after
47      * They should be removed only if the parent is not found.
48      * Example: '.clearfix:before' should be removed only if there
49      *          is no '.clearfix'
50      */
51     return selectors.filter(function (selector) {
52         selector = dePseudify(selector);
53         /* TODO: process @-rules */
54         if (selector[0] === '@') {
55             return true;
56         }
57         for (var i = 0, len = ignore.length; i < len; ++i) {
58             if (_.isRegExp(ignore[i]) && ignore[i].test(selector)) {
59                 return true;
60             }
61             if (ignore[i] === selector) {
62                 return true;
63             }
64         }
65         return usedSelectors.indexOf(selector) !== -1;
66     });
67 }
68
69 /**
70  * Find which animations are used
71  * @param  {Object} css             The postcss.Root node
72  * @return {Array}
73  */
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]);
85             });
86         }
87     });
88     return usedAnimations;
89 }
90
91 /**
92  * Filter @keyframes that are not used
93  * @param  {Object} css             The postcss.Root node
94  * @param  {Array}  animations
95  * @param  {Array}  unusedRules
96  * @return {Array}
97  */
98 function filterKeyframes(css, animations, unusedRules) {
99     css.walkAtRules(/keyframes$/, function (atRule) {
100         if (animations.indexOf(atRule.params) === -1) {
101             unusedRules.push(atRule);
102             atRule.remove();
103         }
104     });
105 }
106
107 /**
108  * Filter rules with no selectors remaining
109  * @param  {Object} css             The postcss.Root node
110  * @return {Array}
111  */
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) {
116             atRule.remove();
117         }
118     });
119 }
120
121 /**
122  * Find which selectors are used in {pages}
123  * @param  {Array}    pages         List of PhantomJS pages
124  * @param  {Object}   css           The postcss.Root node
125  * @return {promise}
126  */
127 function getUsedSelectors(page, css) {
128     var usedSelectors = [];
129     css.walkRules(function (rule) {
130         usedSelectors = _.concat(usedSelectors, rule.selectors.map(dePseudify));
131     });
132     // TODO: Can this be written in a more straightforward fashion?
133     return promise.map(usedSelectors, function (selector) {
134         return selector;
135     }).then(function(selector) {
136         return phantom.findAll(page, selector);
137     });
138 }
139
140 /**
141  * Get all the selectors mentioned in {css}
142  * @param  {Object} css        The postcss.Root node
143  * @return {Array}
144  */
145 function getAllSelectors(css) {
146     var selectors = [];
147     css.walkRules(function (rule) {
148         selectors.concat(rule.selector);
149     });
150     return selectors;
151 }
152
153 /**
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
160  */
161 function filterUnusedRules(pages, css, ignore, usedSelectors) {
162     var ignoreNextRule = false,
163         unusedRules = [],
164         unusedRuleSelectors,
165         usedRuleSelectors;
166     /* Rule format:
167      *  { selectors: [ '...', '...' ],
168      *    declarations: [ { property: '...', value: '...' } ]
169      *  },.
170      * Two steps: filter the unused selectors for each rule,
171      *            filter the rules with no selectors
172      */
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;
179             }
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'
183                 return;
184             }
185             if (ignoreNextRule) {
186                 ignoreNextRule = false;
187                 ignore = ignore.concat(rule.selectors);
188             }
189
190             usedRuleSelectors = filterUnusedSelectors(
191                 rule.selectors,
192                 ignore,
193                 usedSelectors
194             );
195             unusedRuleSelectors = rule.selectors.filter(function (selector) {
196                 return usedRuleSelectors.indexOf(selector) < 0;
197             });
198             if (unusedRuleSelectors && unusedRuleSelectors.length) {
199                 unusedRules.push({
200                     type: 'rule',
201                     selectors: unusedRuleSelectors,
202                     position: rule.source
203                 });
204             }
205             if (usedRuleSelectors.length === 0) {
206                 rule.remove();
207             } else {
208                 rule.selectors = usedRuleSelectors;
209             }
210         }
211     });
212
213     /* Filter the @media rules with no rules */
214     filterEmptyAtRules(css);
215
216     /* Filter unused @keyframes */
217     filterKeyframes(css, getUsedAnimations(css), unusedRules);
218
219     return css;
220 }
221
222 /**
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
227  * @return {promise}
228  */
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),
238             used: usedSelectors
239         }];
240     });
241 };