1 |
|
---|
2 | /*!
|
---|
3 | * Stylus - Normalizer
|
---|
4 | * Copyright (c) Automattic <developer.wordpress.com>
|
---|
5 | * MIT Licensed
|
---|
6 | */
|
---|
7 |
|
---|
8 | /**
|
---|
9 | * Module dependencies.
|
---|
10 | */
|
---|
11 |
|
---|
12 | var Visitor = require('./')
|
---|
13 | , nodes = require('../nodes')
|
---|
14 | , utils = require('../utils');
|
---|
15 |
|
---|
16 | /**
|
---|
17 | * Initialize a new `Normalizer` with the given `root` Node.
|
---|
18 | *
|
---|
19 | * This visitor implements the first stage of the duel-stage
|
---|
20 | * compiler, tasked with stripping the "garbage" from
|
---|
21 | * the evaluated nodes, ditching null rules, resolving
|
---|
22 | * ruleset selectors etc. This step performs the logic
|
---|
23 | * necessary to facilitate the "@extend" functionality,
|
---|
24 | * as these must be resolved _before_ buffering output.
|
---|
25 | *
|
---|
26 | * @param {Node} root
|
---|
27 | * @api public
|
---|
28 | */
|
---|
29 |
|
---|
30 | var Normalizer = module.exports = function Normalizer(root, options) {
|
---|
31 | options = options || {};
|
---|
32 | Visitor.call(this, root);
|
---|
33 | this.hoist = options['hoist atrules'];
|
---|
34 | this.stack = [];
|
---|
35 | this.map = {};
|
---|
36 | this.imports = [];
|
---|
37 | };
|
---|
38 |
|
---|
39 | /**
|
---|
40 | * Inherit from `Visitor.prototype`.
|
---|
41 | */
|
---|
42 |
|
---|
43 | Normalizer.prototype.__proto__ = Visitor.prototype;
|
---|
44 |
|
---|
45 | /**
|
---|
46 | * Normalize the node tree.
|
---|
47 | *
|
---|
48 | * @return {Node}
|
---|
49 | * @api private
|
---|
50 | */
|
---|
51 |
|
---|
52 | Normalizer.prototype.normalize = function(){
|
---|
53 | var ret = this.visit(this.root);
|
---|
54 |
|
---|
55 | if (this.hoist) {
|
---|
56 | // hoist @import
|
---|
57 | if (this.imports.length) ret.nodes = this.imports.concat(ret.nodes);
|
---|
58 |
|
---|
59 | // hoist @charset
|
---|
60 | if (this.charset) ret.nodes = [this.charset].concat(ret.nodes);
|
---|
61 | }
|
---|
62 |
|
---|
63 | return ret;
|
---|
64 | };
|
---|
65 |
|
---|
66 | /**
|
---|
67 | * Bubble up the given `node`.
|
---|
68 | *
|
---|
69 | * @param {Node} node
|
---|
70 | * @api private
|
---|
71 | */
|
---|
72 |
|
---|
73 | Normalizer.prototype.bubble = function(node){
|
---|
74 | var props = []
|
---|
75 | , other = []
|
---|
76 | , self = this;
|
---|
77 |
|
---|
78 | function filterProps(block) {
|
---|
79 | block.nodes.forEach(function(node) {
|
---|
80 | node = self.visit(node);
|
---|
81 |
|
---|
82 | switch (node.nodeName) {
|
---|
83 | case 'property':
|
---|
84 | props.push(node);
|
---|
85 | break;
|
---|
86 | case 'block':
|
---|
87 | filterProps(node);
|
---|
88 | break;
|
---|
89 | default:
|
---|
90 | other.push(node);
|
---|
91 | }
|
---|
92 | });
|
---|
93 | }
|
---|
94 |
|
---|
95 | filterProps(node.block);
|
---|
96 |
|
---|
97 | if (props.length) {
|
---|
98 | var selector = new nodes.Selector([new nodes.Literal('&')]);
|
---|
99 | selector.lineno = node.lineno;
|
---|
100 | selector.column = node.column;
|
---|
101 | selector.filename = node.filename;
|
---|
102 | selector.val = '&';
|
---|
103 |
|
---|
104 | var group = new nodes.Group;
|
---|
105 | group.lineno = node.lineno;
|
---|
106 | group.column = node.column;
|
---|
107 | group.filename = node.filename;
|
---|
108 |
|
---|
109 | var block = new nodes.Block(node.block, group);
|
---|
110 | block.lineno = node.lineno;
|
---|
111 | block.column = node.column;
|
---|
112 | block.filename = node.filename;
|
---|
113 |
|
---|
114 | props.forEach(function(prop){
|
---|
115 | block.push(prop);
|
---|
116 | });
|
---|
117 |
|
---|
118 | group.push(selector);
|
---|
119 | group.block = block;
|
---|
120 |
|
---|
121 | node.block.nodes = [];
|
---|
122 | node.block.push(group);
|
---|
123 | other.forEach(function(n){
|
---|
124 | node.block.push(n);
|
---|
125 | });
|
---|
126 |
|
---|
127 | var group = this.closestGroup(node.block);
|
---|
128 | if (group) node.group = group.clone();
|
---|
129 |
|
---|
130 | node.bubbled = true;
|
---|
131 | }
|
---|
132 | };
|
---|
133 |
|
---|
134 | /**
|
---|
135 | * Return group closest to the given `block`.
|
---|
136 | *
|
---|
137 | * @param {Block} block
|
---|
138 | * @return {Group}
|
---|
139 | * @api private
|
---|
140 | */
|
---|
141 |
|
---|
142 | Normalizer.prototype.closestGroup = function(block){
|
---|
143 | var parent = block.parent
|
---|
144 | , node;
|
---|
145 | while (parent && (node = parent.node)) {
|
---|
146 | if ('group' == node.nodeName) return node;
|
---|
147 | parent = node.block && node.block.parent;
|
---|
148 | }
|
---|
149 | };
|
---|
150 |
|
---|
151 | /**
|
---|
152 | * Visit Root.
|
---|
153 | */
|
---|
154 |
|
---|
155 | Normalizer.prototype.visitRoot = function(block){
|
---|
156 | var ret = new nodes.Root
|
---|
157 | , node;
|
---|
158 |
|
---|
159 | for (var i = 0; i < block.nodes.length; ++i) {
|
---|
160 | node = block.nodes[i];
|
---|
161 | switch (node.nodeName) {
|
---|
162 | case 'null':
|
---|
163 | case 'expression':
|
---|
164 | case 'function':
|
---|
165 | case 'unit':
|
---|
166 | case 'atblock':
|
---|
167 | continue;
|
---|
168 | default:
|
---|
169 | this.rootIndex = i;
|
---|
170 | ret.push(this.visit(node));
|
---|
171 | }
|
---|
172 | }
|
---|
173 |
|
---|
174 | return ret;
|
---|
175 | };
|
---|
176 |
|
---|
177 | /**
|
---|
178 | * Visit Property.
|
---|
179 | */
|
---|
180 |
|
---|
181 | Normalizer.prototype.visitProperty = function(prop){
|
---|
182 | this.visit(prop.expr);
|
---|
183 | return prop;
|
---|
184 | };
|
---|
185 |
|
---|
186 | /**
|
---|
187 | * Visit Expression.
|
---|
188 | */
|
---|
189 |
|
---|
190 | Normalizer.prototype.visitExpression = function(expr){
|
---|
191 | expr.nodes = expr.nodes.map(function(node){
|
---|
192 | // returns `block` literal if mixin's block
|
---|
193 | // is used as part of a property value
|
---|
194 | if ('block' == node.nodeName) {
|
---|
195 | var literal = new nodes.Literal('block');
|
---|
196 | literal.lineno = expr.lineno;
|
---|
197 | literal.column = expr.column;
|
---|
198 | return literal;
|
---|
199 | }
|
---|
200 | return node;
|
---|
201 | });
|
---|
202 | return expr;
|
---|
203 | };
|
---|
204 |
|
---|
205 | /**
|
---|
206 | * Visit Block.
|
---|
207 | */
|
---|
208 |
|
---|
209 | Normalizer.prototype.visitBlock = function(block){
|
---|
210 | var node;
|
---|
211 |
|
---|
212 | if (block.hasProperties) {
|
---|
213 | for (var i = 0, len = block.nodes.length; i < len; ++i) {
|
---|
214 | node = block.nodes[i];
|
---|
215 | switch (node.nodeName) {
|
---|
216 | case 'null':
|
---|
217 | case 'expression':
|
---|
218 | case 'function':
|
---|
219 | case 'group':
|
---|
220 | case 'unit':
|
---|
221 | case 'atblock':
|
---|
222 | continue;
|
---|
223 | default:
|
---|
224 | block.nodes[i] = this.visit(node);
|
---|
225 | }
|
---|
226 | }
|
---|
227 | }
|
---|
228 |
|
---|
229 | // nesting
|
---|
230 | for (var i = 0, len = block.nodes.length; i < len; ++i) {
|
---|
231 | node = block.nodes[i];
|
---|
232 | block.nodes[i] = this.visit(node);
|
---|
233 | }
|
---|
234 |
|
---|
235 | return block;
|
---|
236 | };
|
---|
237 |
|
---|
238 | /**
|
---|
239 | * Visit Group.
|
---|
240 | */
|
---|
241 |
|
---|
242 | Normalizer.prototype.visitGroup = function(group){
|
---|
243 | var stack = this.stack
|
---|
244 | , map = this.map
|
---|
245 | , parts;
|
---|
246 |
|
---|
247 | // normalize interpolated selectors with comma
|
---|
248 | group.nodes.forEach(function(selector, i){
|
---|
249 | if (!~selector.val.indexOf(',')) return;
|
---|
250 | if (~selector.val.indexOf('\\,')) {
|
---|
251 | selector.val = selector.val.replace(/\\,/g, ',');
|
---|
252 | return;
|
---|
253 | }
|
---|
254 | parts = selector.val.split(',');
|
---|
255 | var root = '/' == selector.val.charAt(0)
|
---|
256 | , part, s;
|
---|
257 | for (var k = 0, len = parts.length; k < len; ++k){
|
---|
258 | part = parts[k].trim();
|
---|
259 | if (root && k > 0 && !~part.indexOf('&')) {
|
---|
260 | part = '/' + part;
|
---|
261 | }
|
---|
262 | s = new nodes.Selector([new nodes.Literal(part)]);
|
---|
263 | s.val = part;
|
---|
264 | s.block = group.block;
|
---|
265 | group.nodes[i++] = s;
|
---|
266 | }
|
---|
267 | });
|
---|
268 | stack.push(group.nodes);
|
---|
269 |
|
---|
270 | var selectors = utils.compileSelectors(stack, true);
|
---|
271 |
|
---|
272 | // map for extension lookup
|
---|
273 | selectors.forEach(function(selector){
|
---|
274 | map[selector] = map[selector] || [];
|
---|
275 | map[selector].push(group);
|
---|
276 | });
|
---|
277 |
|
---|
278 | // extensions
|
---|
279 | this.extend(group, selectors);
|
---|
280 |
|
---|
281 | stack.pop();
|
---|
282 | return group;
|
---|
283 | };
|
---|
284 |
|
---|
285 | /**
|
---|
286 | * Visit Function.
|
---|
287 | */
|
---|
288 |
|
---|
289 | Normalizer.prototype.visitFunction = function(){
|
---|
290 | return nodes.null;
|
---|
291 | };
|
---|
292 |
|
---|
293 | /**
|
---|
294 | * Visit Media.
|
---|
295 | */
|
---|
296 |
|
---|
297 | Normalizer.prototype.visitMedia = function(media){
|
---|
298 | var medias = []
|
---|
299 | , group = this.closestGroup(media.block)
|
---|
300 | , parent;
|
---|
301 |
|
---|
302 | function mergeQueries(block) {
|
---|
303 | block.nodes.forEach(function(node, i){
|
---|
304 | switch (node.nodeName) {
|
---|
305 | case 'media':
|
---|
306 | node.val = media.val.merge(node.val);
|
---|
307 | medias.push(node);
|
---|
308 | block.nodes[i] = nodes.null;
|
---|
309 | break;
|
---|
310 | case 'block':
|
---|
311 | mergeQueries(node);
|
---|
312 | break;
|
---|
313 | default:
|
---|
314 | if (node.block && node.block.nodes)
|
---|
315 | mergeQueries(node.block);
|
---|
316 | }
|
---|
317 | });
|
---|
318 | }
|
---|
319 |
|
---|
320 | mergeQueries(media.block);
|
---|
321 | this.bubble(media);
|
---|
322 |
|
---|
323 | if (medias.length) {
|
---|
324 | medias.forEach(function(node){
|
---|
325 | if (group) {
|
---|
326 | group.block.push(node);
|
---|
327 | } else {
|
---|
328 | this.root.nodes.splice(++this.rootIndex, 0, node);
|
---|
329 | }
|
---|
330 | node = this.visit(node);
|
---|
331 | parent = node.block.parent;
|
---|
332 | if (node.bubbled && (!group || 'group' == parent.node.nodeName)) {
|
---|
333 | node.group.block = node.block.nodes[0].block;
|
---|
334 | node.block.nodes[0] = node.group;
|
---|
335 | }
|
---|
336 | }, this);
|
---|
337 | }
|
---|
338 | return media;
|
---|
339 | };
|
---|
340 |
|
---|
341 | /**
|
---|
342 | * Visit Supports.
|
---|
343 | */
|
---|
344 |
|
---|
345 | Normalizer.prototype.visitSupports = function(node){
|
---|
346 | this.bubble(node);
|
---|
347 | return node;
|
---|
348 | };
|
---|
349 |
|
---|
350 | /**
|
---|
351 | * Visit Atrule.
|
---|
352 | */
|
---|
353 |
|
---|
354 | Normalizer.prototype.visitAtrule = function(node){
|
---|
355 | if (node.block) node.block = this.visit(node.block);
|
---|
356 | return node;
|
---|
357 | };
|
---|
358 |
|
---|
359 | /**
|
---|
360 | * Visit Keyframes.
|
---|
361 | */
|
---|
362 |
|
---|
363 | Normalizer.prototype.visitKeyframes = function(node){
|
---|
364 | var frames = node.block.nodes.filter(function(frame){
|
---|
365 | return frame.block && frame.block.hasProperties;
|
---|
366 | });
|
---|
367 | node.frames = frames.length;
|
---|
368 | return node;
|
---|
369 | };
|
---|
370 |
|
---|
371 | /**
|
---|
372 | * Visit Import.
|
---|
373 | */
|
---|
374 |
|
---|
375 | Normalizer.prototype.visitImport = function(node){
|
---|
376 | this.imports.push(node);
|
---|
377 | return this.hoist ? nodes.null : node;
|
---|
378 | };
|
---|
379 |
|
---|
380 | /**
|
---|
381 | * Visit Charset.
|
---|
382 | */
|
---|
383 |
|
---|
384 | Normalizer.prototype.visitCharset = function(node){
|
---|
385 | this.charset = node;
|
---|
386 | return this.hoist ? nodes.null : node;
|
---|
387 | };
|
---|
388 |
|
---|
389 | /**
|
---|
390 | * Apply `group` extensions.
|
---|
391 | *
|
---|
392 | * @param {Group} group
|
---|
393 | * @param {Array} selectors
|
---|
394 | * @api private
|
---|
395 | */
|
---|
396 |
|
---|
397 | Normalizer.prototype.extend = function(group, selectors){
|
---|
398 | var map = this.map
|
---|
399 | , self = this
|
---|
400 | , parent = this.closestGroup(group.block);
|
---|
401 |
|
---|
402 | group.extends.forEach(function(extend){
|
---|
403 | var groups = map[extend.selector];
|
---|
404 | if (!groups) {
|
---|
405 | if (extend.optional) return;
|
---|
406 | groups = self._checkForPrefixedGroups(extend.selector);
|
---|
407 | if(!groups) {
|
---|
408 | var err = new Error('Failed to @extend "' + extend.selector + '"');
|
---|
409 | err.lineno = extend.lineno;
|
---|
410 | err.column = extend.column;
|
---|
411 | throw err;
|
---|
412 | }
|
---|
413 | }
|
---|
414 | selectors.forEach(function(selector){
|
---|
415 | var node = new nodes.Selector;
|
---|
416 | node.val = selector;
|
---|
417 | node.inherits = false;
|
---|
418 | groups.forEach(function(group){
|
---|
419 | // prevent recursive extend
|
---|
420 | if (!parent || (parent != group)) self.extend(group, selectors);
|
---|
421 | group.push(node);
|
---|
422 | });
|
---|
423 | });
|
---|
424 | });
|
---|
425 |
|
---|
426 | group.block = this.visit(group.block);
|
---|
427 | };
|
---|
428 |
|
---|
429 | Normalizer.prototype._checkForPrefixedGroups = function (selector) {
|
---|
430 | var prefix = [];
|
---|
431 | var map = this.map;
|
---|
432 | var result = null;
|
---|
433 | for (var i = 0; i < this.stack.length; i++) {
|
---|
434 | var stackElementArray=this.stack[i];
|
---|
435 | var stackElement = stackElementArray[0];
|
---|
436 | prefix.push(stackElement.val);
|
---|
437 | var fullSelector = prefix.join(" ") + " " + selector;
|
---|
438 | result = map[fullSelector];
|
---|
439 | if (result)
|
---|
440 | break;
|
---|
441 | }
|
---|
442 | return result;
|
---|
443 | }; |
---|