UNPKG

4.07 kBJavaScriptView Raw
1'use strict';
2
3exports.type = 'full';
4
5exports.active = true;
6
7exports.description = 'minifies styles and removes unused styles based on usage data';
8
9exports.params = {
10 // ... CSSO options goes here
11
12 // additional
13 usage: {
14 force: false, // force to use usage data even if it unsafe (document contains <script> or on* attributes)
15 ids: true,
16 classes: true,
17 tags: true
18 }
19};
20
21var csso = require('csso');
22
23/**
24 * Minifies styles (<style> element + style attribute) using CSSO
25 *
26 * @author strarsis <strarsis@gmail.com>
27 */
28exports.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 // <style> element
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 // style attribute
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
56function cloneObject(obj) {
57 var result = {};
58
59 for (var key in obj) {
60 result[key] = obj[key];
61 }
62
63 return result;
64}
65
66function 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 // go deeper
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
90function 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
102function 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 // go deeper
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}