1 | 'use strict';
|
2 |
|
3 | exports.type = 'full';
|
4 |
|
5 | exports.active = true;
|
6 |
|
7 | exports.description = 'minifies styles and removes unused styles based on usage data';
|
8 |
|
9 | exports.params = {
|
10 |
|
11 |
|
12 |
|
13 | usage: {
|
14 | force: false,
|
15 | ids: true,
|
16 | classes: true,
|
17 | tags: true
|
18 | }
|
19 | };
|
20 |
|
21 | var csso = require('csso');
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | exports.fn = function(ast, options) {
|
29 | options = options || {};
|
30 |
|
31 | var minifyOptionsForStylesheet = cloneObject(options);
|
32 | var minifyOptionsForAttribute = cloneObject(options);
|
33 | var elems = findStyleElems(ast);
|
34 |
|
35 | minifyOptionsForStylesheet.usage = collectUsageData(ast, options);
|
36 | minifyOptionsForAttribute.usage = null;
|
37 |
|
38 | elems.forEach(function(elem) {
|
39 | if (elem.isElem('style')) {
|
40 |
|
41 | var styleCss = elem.content[0].text || elem.content[0].cdata || [];
|
42 | var DATA = styleCss.indexOf('>') >= 0 || styleCss.indexOf('<') >= 0 ? 'cdata' : 'text';
|
43 |
|
44 | elem.content[0][DATA] = csso.minify(styleCss, minifyOptionsForStylesheet).css;
|
45 | } else {
|
46 |
|
47 | var elemStyle = elem.attr('style').value;
|
48 |
|
49 | elem.attr('style').value = csso.minifyBlock(elemStyle, minifyOptionsForAttribute).css;
|
50 | }
|
51 | });
|
52 |
|
53 | return ast;
|
54 | };
|
55 |
|
56 | function cloneObject(obj) {
|
57 | var result = {};
|
58 |
|
59 | for (var key in obj) {
|
60 | result[key] = obj[key];
|
61 | }
|
62 |
|
63 | return result;
|
64 | }
|
65 |
|
66 | function findStyleElems(ast) {
|
67 |
|
68 | function walk(items, styles) {
|
69 | for (var i = 0; i < items.content.length; i++) {
|
70 | var item = items.content[i];
|
71 |
|
72 |
|
73 | if (item.content) {
|
74 | walk(item, styles);
|
75 | }
|
76 |
|
77 | if (item.isElem('style') && !item.isEmpty()) {
|
78 | styles.push(item);
|
79 | } else if (item.isElem() && item.hasAttr('style')) {
|
80 | styles.push(item);
|
81 | }
|
82 | }
|
83 |
|
84 | return styles;
|
85 | }
|
86 |
|
87 | return walk(ast, []);
|
88 | }
|
89 |
|
90 | function shouldFilter(options, name) {
|
91 | if ('usage' in options === false) {
|
92 | return true;
|
93 | }
|
94 |
|
95 | if (options.usage && name in options.usage === false) {
|
96 | return true;
|
97 | }
|
98 |
|
99 | return Boolean(options.usage && options.usage[name]);
|
100 | }
|
101 |
|
102 | function collectUsageData(ast, options) {
|
103 |
|
104 | function walk(items, usageData) {
|
105 | for (var i = 0; i < items.content.length; i++) {
|
106 | var item = items.content[i];
|
107 |
|
108 |
|
109 | if (item.content) {
|
110 | walk(item, usageData);
|
111 | }
|
112 |
|
113 | if (item.isElem('script')) {
|
114 | safe = false;
|
115 | }
|
116 |
|
117 | if (item.isElem()) {
|
118 | usageData.tags[item.elem] = true;
|
119 |
|
120 | if (item.hasAttr('id')) {
|
121 | usageData.ids[item.attr('id').value] = true;
|
122 | }
|
123 |
|
124 | if (item.hasAttr('class')) {
|
125 | item.attr('class').value.replace(/^\s+|\s+$/g, '').split(/\s+/).forEach(function(className) {
|
126 | usageData.classes[className] = true;
|
127 | });
|
128 | }
|
129 |
|
130 | if (item.attrs && Object.keys(item.attrs).some(function(name) { return /^on/i.test(name); })) {
|
131 | safe = false;
|
132 | }
|
133 | }
|
134 | }
|
135 |
|
136 | return usageData;
|
137 | }
|
138 |
|
139 | var safe = true;
|
140 | var usageData = {};
|
141 | var hasData = false;
|
142 | var rawData = walk(ast, {
|
143 | ids: Object.create(null),
|
144 | classes: Object.create(null),
|
145 | tags: Object.create(null)
|
146 | });
|
147 |
|
148 | if (!safe && options.usage && options.usage.force) {
|
149 | safe = true;
|
150 | }
|
151 |
|
152 | for (var key in rawData) {
|
153 | if (shouldFilter(options, key)) {
|
154 | usageData[key] = Object.keys(rawData[key]);
|
155 | hasData = true;
|
156 | }
|
157 | }
|
158 |
|
159 | return safe && hasData ? usageData : null;
|
160 | }
|