1 | var resolveProperty = require('css-tree').property;
|
---|
2 | var resolveKeyword = require('css-tree').keyword;
|
---|
3 | var walk = require('css-tree').walk;
|
---|
4 | var generate = require('css-tree').generate;
|
---|
5 | var fingerprintId = 1;
|
---|
6 | var dontRestructure = {
|
---|
7 | 'src': 1 // https://github.com/afelix/csso/issues/50
|
---|
8 | };
|
---|
9 |
|
---|
10 | var DONT_MIX_VALUE = {
|
---|
11 | // https://developer.mozilla.org/en-US/docs/Web/CSS/display#Browser_compatibility
|
---|
12 | 'display': /table|ruby|flex|-(flex)?box$|grid|contents|run-in/i,
|
---|
13 | // https://developer.mozilla.org/en/docs/Web/CSS/text-align
|
---|
14 | 'text-align': /^(start|end|match-parent|justify-all)$/i
|
---|
15 | };
|
---|
16 |
|
---|
17 | var SAFE_VALUES = {
|
---|
18 | cursor: [
|
---|
19 | 'auto', 'crosshair', 'default', 'move', 'text', 'wait', 'help',
|
---|
20 | 'n-resize', 'e-resize', 's-resize', 'w-resize',
|
---|
21 | 'ne-resize', 'nw-resize', 'se-resize', 'sw-resize',
|
---|
22 | 'pointer', 'progress', 'not-allowed', 'no-drop', 'vertical-text', 'all-scroll',
|
---|
23 | 'col-resize', 'row-resize'
|
---|
24 | ],
|
---|
25 | overflow: [
|
---|
26 | 'hidden', 'visible', 'scroll', 'auto'
|
---|
27 | ],
|
---|
28 | position: [
|
---|
29 | 'static', 'relative', 'absolute', 'fixed'
|
---|
30 | ]
|
---|
31 | };
|
---|
32 |
|
---|
33 | var NEEDLESS_TABLE = {
|
---|
34 | 'border-width': ['border'],
|
---|
35 | 'border-style': ['border'],
|
---|
36 | 'border-color': ['border'],
|
---|
37 | 'border-top': ['border'],
|
---|
38 | 'border-right': ['border'],
|
---|
39 | 'border-bottom': ['border'],
|
---|
40 | 'border-left': ['border'],
|
---|
41 | 'border-top-width': ['border-top', 'border-width', 'border'],
|
---|
42 | 'border-right-width': ['border-right', 'border-width', 'border'],
|
---|
43 | 'border-bottom-width': ['border-bottom', 'border-width', 'border'],
|
---|
44 | 'border-left-width': ['border-left', 'border-width', 'border'],
|
---|
45 | 'border-top-style': ['border-top', 'border-style', 'border'],
|
---|
46 | 'border-right-style': ['border-right', 'border-style', 'border'],
|
---|
47 | 'border-bottom-style': ['border-bottom', 'border-style', 'border'],
|
---|
48 | 'border-left-style': ['border-left', 'border-style', 'border'],
|
---|
49 | 'border-top-color': ['border-top', 'border-color', 'border'],
|
---|
50 | 'border-right-color': ['border-right', 'border-color', 'border'],
|
---|
51 | 'border-bottom-color': ['border-bottom', 'border-color', 'border'],
|
---|
52 | 'border-left-color': ['border-left', 'border-color', 'border'],
|
---|
53 | 'margin-top': ['margin'],
|
---|
54 | 'margin-right': ['margin'],
|
---|
55 | 'margin-bottom': ['margin'],
|
---|
56 | 'margin-left': ['margin'],
|
---|
57 | 'padding-top': ['padding'],
|
---|
58 | 'padding-right': ['padding'],
|
---|
59 | 'padding-bottom': ['padding'],
|
---|
60 | 'padding-left': ['padding'],
|
---|
61 | 'font-style': ['font'],
|
---|
62 | 'font-variant': ['font'],
|
---|
63 | 'font-weight': ['font'],
|
---|
64 | 'font-size': ['font'],
|
---|
65 | 'font-family': ['font'],
|
---|
66 | 'list-style-type': ['list-style'],
|
---|
67 | 'list-style-position': ['list-style'],
|
---|
68 | 'list-style-image': ['list-style']
|
---|
69 | };
|
---|
70 |
|
---|
71 | function getPropertyFingerprint(propertyName, declaration, fingerprints) {
|
---|
72 | var realName = resolveProperty(propertyName).basename;
|
---|
73 |
|
---|
74 | if (realName === 'background') {
|
---|
75 | return propertyName + ':' + generate(declaration.value);
|
---|
76 | }
|
---|
77 |
|
---|
78 | var declarationId = declaration.id;
|
---|
79 | var fingerprint = fingerprints[declarationId];
|
---|
80 |
|
---|
81 | if (!fingerprint) {
|
---|
82 | switch (declaration.value.type) {
|
---|
83 | case 'Value':
|
---|
84 | var vendorId = '';
|
---|
85 | var iehack = '';
|
---|
86 | var special = {};
|
---|
87 | var raw = false;
|
---|
88 |
|
---|
89 | declaration.value.children.each(function walk(node) {
|
---|
90 | switch (node.type) {
|
---|
91 | case 'Value':
|
---|
92 | case 'Brackets':
|
---|
93 | case 'Parentheses':
|
---|
94 | node.children.each(walk);
|
---|
95 | break;
|
---|
96 |
|
---|
97 | case 'Raw':
|
---|
98 | raw = true;
|
---|
99 | break;
|
---|
100 |
|
---|
101 | case 'Identifier':
|
---|
102 | var name = node.name;
|
---|
103 |
|
---|
104 | if (!vendorId) {
|
---|
105 | vendorId = resolveKeyword(name).vendor;
|
---|
106 | }
|
---|
107 |
|
---|
108 | if (/\\[09]/.test(name)) {
|
---|
109 | iehack = RegExp.lastMatch;
|
---|
110 | }
|
---|
111 |
|
---|
112 | if (SAFE_VALUES.hasOwnProperty(realName)) {
|
---|
113 | if (SAFE_VALUES[realName].indexOf(name) === -1) {
|
---|
114 | special[name] = true;
|
---|
115 | }
|
---|
116 | } else if (DONT_MIX_VALUE.hasOwnProperty(realName)) {
|
---|
117 | if (DONT_MIX_VALUE[realName].test(name)) {
|
---|
118 | special[name] = true;
|
---|
119 | }
|
---|
120 | }
|
---|
121 |
|
---|
122 | break;
|
---|
123 |
|
---|
124 | case 'Function':
|
---|
125 | var name = node.name;
|
---|
126 |
|
---|
127 | if (!vendorId) {
|
---|
128 | vendorId = resolveKeyword(name).vendor;
|
---|
129 | }
|
---|
130 |
|
---|
131 | if (name === 'rect') {
|
---|
132 | // there are 2 forms of rect:
|
---|
133 | // rect(<top>, <right>, <bottom>, <left>) - standart
|
---|
134 | // rect(<top> <right> <bottom> <left>) – backwards compatible syntax
|
---|
135 | // only the same form values can be merged
|
---|
136 | var hasComma = node.children.some(function(node) {
|
---|
137 | return node.type === 'Operator' && node.value === ',';
|
---|
138 | });
|
---|
139 | if (!hasComma) {
|
---|
140 | name = 'rect-backward';
|
---|
141 | }
|
---|
142 | }
|
---|
143 |
|
---|
144 | special[name + '()'] = true;
|
---|
145 |
|
---|
146 | // check nested tokens too
|
---|
147 | node.children.each(walk);
|
---|
148 |
|
---|
149 | break;
|
---|
150 |
|
---|
151 | case 'Dimension':
|
---|
152 | var unit = node.unit;
|
---|
153 |
|
---|
154 | if (/\\[09]/.test(unit)) {
|
---|
155 | iehack = RegExp.lastMatch;
|
---|
156 | }
|
---|
157 |
|
---|
158 | switch (unit) {
|
---|
159 | // is not supported until IE11
|
---|
160 | case 'rem':
|
---|
161 |
|
---|
162 | // v* units is too buggy across browsers and better
|
---|
163 | // don't merge values with those units
|
---|
164 | case 'vw':
|
---|
165 | case 'vh':
|
---|
166 | case 'vmin':
|
---|
167 | case 'vmax':
|
---|
168 | case 'vm': // IE9 supporting "vm" instead of "vmin".
|
---|
169 | special[unit] = true;
|
---|
170 | break;
|
---|
171 | }
|
---|
172 | break;
|
---|
173 | }
|
---|
174 | });
|
---|
175 |
|
---|
176 | fingerprint = raw
|
---|
177 | ? '!' + fingerprintId++
|
---|
178 | : '!' + Object.keys(special).sort() + '|' + iehack + vendorId;
|
---|
179 | break;
|
---|
180 |
|
---|
181 | case 'Raw':
|
---|
182 | fingerprint = '!' + declaration.value.value;
|
---|
183 | break;
|
---|
184 |
|
---|
185 | default:
|
---|
186 | fingerprint = generate(declaration.value);
|
---|
187 | }
|
---|
188 |
|
---|
189 | fingerprints[declarationId] = fingerprint;
|
---|
190 | }
|
---|
191 |
|
---|
192 | return propertyName + fingerprint;
|
---|
193 | }
|
---|
194 |
|
---|
195 | function needless(props, declaration, fingerprints) {
|
---|
196 | var property = resolveProperty(declaration.property);
|
---|
197 |
|
---|
198 | if (NEEDLESS_TABLE.hasOwnProperty(property.basename)) {
|
---|
199 | var table = NEEDLESS_TABLE[property.basename];
|
---|
200 |
|
---|
201 | for (var i = 0; i < table.length; i++) {
|
---|
202 | var ppre = getPropertyFingerprint(property.prefix + table[i], declaration, fingerprints);
|
---|
203 | var prev = props.hasOwnProperty(ppre) ? props[ppre] : null;
|
---|
204 |
|
---|
205 | if (prev && (!declaration.important || prev.item.data.important)) {
|
---|
206 | return prev;
|
---|
207 | }
|
---|
208 | }
|
---|
209 | }
|
---|
210 | }
|
---|
211 |
|
---|
212 | function processRule(rule, item, list, props, fingerprints) {
|
---|
213 | var declarations = rule.block.children;
|
---|
214 |
|
---|
215 | declarations.eachRight(function(declaration, declarationItem) {
|
---|
216 | var property = declaration.property;
|
---|
217 | var fingerprint = getPropertyFingerprint(property, declaration, fingerprints);
|
---|
218 | var prev = props[fingerprint];
|
---|
219 |
|
---|
220 | if (prev && !dontRestructure.hasOwnProperty(property)) {
|
---|
221 | if (declaration.important && !prev.item.data.important) {
|
---|
222 | props[fingerprint] = {
|
---|
223 | block: declarations,
|
---|
224 | item: declarationItem
|
---|
225 | };
|
---|
226 |
|
---|
227 | prev.block.remove(prev.item);
|
---|
228 |
|
---|
229 | // TODO: use it when we can refer to several points in source
|
---|
230 | // declaration.loc = {
|
---|
231 | // primary: declaration.loc,
|
---|
232 | // merged: prev.item.data.loc
|
---|
233 | // };
|
---|
234 | } else {
|
---|
235 | declarations.remove(declarationItem);
|
---|
236 |
|
---|
237 | // TODO: use it when we can refer to several points in source
|
---|
238 | // prev.item.data.loc = {
|
---|
239 | // primary: prev.item.data.loc,
|
---|
240 | // merged: declaration.loc
|
---|
241 | // };
|
---|
242 | }
|
---|
243 | } else {
|
---|
244 | var prev = needless(props, declaration, fingerprints);
|
---|
245 |
|
---|
246 | if (prev) {
|
---|
247 | declarations.remove(declarationItem);
|
---|
248 |
|
---|
249 | // TODO: use it when we can refer to several points in source
|
---|
250 | // prev.item.data.loc = {
|
---|
251 | // primary: prev.item.data.loc,
|
---|
252 | // merged: declaration.loc
|
---|
253 | // };
|
---|
254 | } else {
|
---|
255 | declaration.fingerprint = fingerprint;
|
---|
256 |
|
---|
257 | props[fingerprint] = {
|
---|
258 | block: declarations,
|
---|
259 | item: declarationItem
|
---|
260 | };
|
---|
261 | }
|
---|
262 | }
|
---|
263 | });
|
---|
264 |
|
---|
265 | if (declarations.isEmpty()) {
|
---|
266 | list.remove(item);
|
---|
267 | }
|
---|
268 | }
|
---|
269 |
|
---|
270 | module.exports = function restructBlock(ast) {
|
---|
271 | var stylesheetMap = {};
|
---|
272 | var fingerprints = Object.create(null);
|
---|
273 |
|
---|
274 | walk(ast, {
|
---|
275 | visit: 'Rule',
|
---|
276 | reverse: true,
|
---|
277 | enter: function(node, item, list) {
|
---|
278 | var stylesheet = this.block || this.stylesheet;
|
---|
279 | var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id;
|
---|
280 | var ruleMap;
|
---|
281 | var props;
|
---|
282 |
|
---|
283 | if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
|
---|
284 | ruleMap = {};
|
---|
285 | stylesheetMap[stylesheet.id] = ruleMap;
|
---|
286 | } else {
|
---|
287 | ruleMap = stylesheetMap[stylesheet.id];
|
---|
288 | }
|
---|
289 |
|
---|
290 | if (ruleMap.hasOwnProperty(ruleId)) {
|
---|
291 | props = ruleMap[ruleId];
|
---|
292 | } else {
|
---|
293 | props = {};
|
---|
294 | ruleMap[ruleId] = props;
|
---|
295 | }
|
---|
296 |
|
---|
297 | processRule.call(this, node, item, list, props, fingerprints);
|
---|
298 | }
|
---|
299 | });
|
---|
300 | };
|
---|