UNPKG

8.94 kBJavaScriptView Raw
1
2/*!
3 * Stylus - Normalizer
4 * Copyright (c) Automattic <developer.wordpress.com>
5 * MIT Licensed
6 */
7
8/**
9 * Module dependencies.
10 */
11
12var 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
30var 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
43Normalizer.prototype.__proto__ = Visitor.prototype;
44
45/**
46 * Normalize the node tree.
47 *
48 * @return {Node}
49 * @api private
50 */
51
52Normalizer.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
73Normalizer.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
142Normalizer.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
155Normalizer.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
181Normalizer.prototype.visitProperty = function(prop){
182 this.visit(prop.expr);
183 return prop;
184};
185
186/**
187 * Visit Expression.
188 */
189
190Normalizer.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
209Normalizer.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
242Normalizer.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
289Normalizer.prototype.visitFunction = function(){
290 return nodes.null;
291};
292
293/**
294 * Visit Media.
295 */
296
297Normalizer.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
345Normalizer.prototype.visitSupports = function(node){
346 this.bubble(node);
347 return node;
348};
349
350/**
351 * Visit Atrule.
352 */
353
354Normalizer.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
363Normalizer.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
375Normalizer.prototype.visitImport = function(node){
376 this.imports.push(node);
377 return this.hoist ? nodes.null : node;
378};
379
380/**
381 * Visit Charset.
382 */
383
384Normalizer.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
397Normalizer.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 var err = new Error('Failed to @extend "' + extend.selector + '"');
407 err.lineno = extend.lineno;
408 err.column = extend.column;
409 throw err;
410 }
411 selectors.forEach(function(selector){
412 var node = new nodes.Selector;
413 node.val = selector;
414 node.inherits = false;
415 groups.forEach(function(group){
416 // prevent recursive extend
417 if (!parent || (parent != group)) self.extend(group, selectors);
418 group.push(node);
419 });
420 });
421 });
422
423 group.block = this.visit(group.block);
424};