source: trip-planner-front/node_modules/stylus/lib/visitor/evaluator.js@ 1ad8e64

Last change on this file since 1ad8e64 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 35.0 KB
Line 
1
2/*!
3 * Stylus - Evaluator
4 * Copyright (c) Automattic <developer.wordpress.com>
5 * MIT Licensed
6 */
7
8/**
9 * Module dependencies.
10 */
11
12var Visitor = require('./')
13 , units = require('../units')
14 , nodes = require('../nodes')
15 , Stack = require('../stack')
16 , Frame = require('../stack/frame')
17 , utils = require('../utils')
18 , bifs = require('../functions')
19 , dirname = require('path').dirname
20 , colors = require('../colors')
21 , debug = require('debug')('stylus:evaluator')
22 , fs = require('fs');
23
24/**
25 * Import `file` and return Block node.
26 *
27 * @api private
28 */
29function importFile(node, file, literal) {
30 var importStack = this.importStack
31 , Parser = require('../parser')
32 , stat;
33
34 // Handling the `require`
35 if (node.once) {
36 if (this.requireHistory[file]) return nodes.null;
37 this.requireHistory[file] = true;
38
39 if (literal && !this.includeCSS) {
40 return node;
41 }
42 }
43
44 // Avoid overflows from importing the same file over again
45 if (~importStack.indexOf(file))
46 throw new Error('import loop has been found');
47
48 var str = fs.readFileSync(file, 'utf8');
49
50 // shortcut for empty files
51 if (!str.trim()) return nodes.null;
52
53 // Expose imports
54 node.path = file;
55 node.dirname = dirname(file);
56 // Store the modified time
57 stat = fs.statSync(file);
58 node.mtime = stat.mtime;
59 this.paths.push(node.dirname);
60
61 if (this.options._imports) this.options._imports.push(node.clone());
62
63 // Parse the file
64 importStack.push(file);
65 nodes.filename = file;
66
67 if (literal) {
68 literal = new nodes.Literal(str.replace(/\r\n?/g, '\n'));
69 literal.lineno = literal.column = 1;
70 if (!this.resolveURL) return literal;
71 }
72
73 // parse
74 var block = new nodes.Block
75 , parser = new Parser(str, utils.merge({ root: block }, this.options));
76
77 try {
78 block = parser.parse();
79 } catch (err) {
80 var line = parser.lexer.lineno
81 , column = parser.lexer.column;
82
83 if (literal && this.includeCSS && this.resolveURL) {
84 this.warn('ParseError: ' + file + ':' + line + ':' + column + '. This file included as-is');
85 return literal;
86 } else {
87 err.filename = file;
88 err.lineno = line;
89 err.column = column;
90 err.input = str;
91 throw err;
92 }
93 }
94
95 // Evaluate imported "root"
96 block = block.clone(this.currentBlock);
97 block.parent = this.currentBlock;
98 block.scope = false;
99 var ret = this.visit(block);
100 importStack.pop();
101 if (!this.resolveURL || this.resolveURL.nocheck) this.paths.pop();
102
103 return ret;
104}
105
106/**
107 * Initialize a new `Evaluator` with the given `root` Node
108 * and the following `options`.
109 *
110 * Options:
111 *
112 * - `compress` Compress the css output, defaults to false
113 * - `warn` Warn the user of duplicate function definitions etc
114 *
115 * @param {Node} root
116 * @api private
117 */
118
119var Evaluator = module.exports = function Evaluator(root, options) {
120 options = options || {};
121 Visitor.call(this, root);
122 var functions = this.functions = options.functions || {};
123 this.stack = new Stack;
124 this.imports = options.imports || [];
125 this.globals = options.globals || {};
126 this.paths = options.paths || [];
127 this.prefix = options.prefix || '';
128 this.filename = options.filename;
129 this.includeCSS = options['include css'];
130 this.resolveURL = functions.url
131 && 'resolver' == functions.url.name
132 && functions.url.options;
133 this.paths.push(dirname(options.filename || '.'));
134 this.stack.push(this.global = new Frame(root));
135 this.warnings = options.warn;
136 this.options = options;
137 this.calling = []; // TODO: remove, use stack
138 this.importStack = [];
139 this.requireHistory = {};
140 this.return = 0;
141};
142
143/**
144 * Inherit from `Visitor.prototype`.
145 */
146
147Evaluator.prototype.__proto__ = Visitor.prototype;
148
149/**
150 * Proxy visit to expose node line numbers.
151 *
152 * @param {Node} node
153 * @return {Node}
154 * @api private
155 */
156
157var visit = Visitor.prototype.visit;
158Evaluator.prototype.visit = function(node){
159 try {
160 return visit.call(this, node);
161 } catch (err) {
162 if (err.filename) throw err;
163 err.lineno = node.lineno;
164 err.column = node.column;
165 err.filename = node.filename;
166 err.stylusStack = this.stack.toString();
167 try {
168 err.input = fs.readFileSync(err.filename, 'utf8');
169 } catch (err) {
170 // ignore
171 }
172 throw err;
173 }
174};
175
176/**
177 * Perform evaluation setup:
178 *
179 * - populate global scope
180 * - iterate imports
181 *
182 * @api private
183 */
184
185Evaluator.prototype.setup = function(){
186 var root = this.root;
187 var imports = [];
188
189 this.populateGlobalScope();
190 this.imports.forEach(function(file){
191 var expr = new nodes.Expression;
192 expr.push(new nodes.String(file));
193 imports.push(new nodes.Import(expr));
194 }, this);
195
196 root.nodes = imports.concat(root.nodes);
197};
198
199/**
200 * Populate the global scope with:
201 *
202 * - css colors
203 * - user-defined globals
204 *
205 * @api private
206 */
207
208Evaluator.prototype.populateGlobalScope = function(){
209 var scope = this.global.scope;
210
211 // colors
212 Object.keys(colors).forEach(function(name){
213 var color = colors[name]
214 , rgba = new nodes.RGBA(color[0], color[1], color[2], color[3])
215 , node = new nodes.Ident(name, rgba);
216 rgba.name = name;
217 scope.add(node);
218 });
219
220 // expose url function
221 scope.add(new nodes.Ident(
222 'embedurl',
223 new nodes.Function('embedurl', require('../functions/url')({
224 limit: false
225 }))
226 ));
227
228 // user-defined globals
229 var globals = this.globals;
230 Object.keys(globals).forEach(function(name){
231 var val = globals[name];
232 if (!val.nodeName) val = new nodes.Literal(val);
233 scope.add(new nodes.Ident(name, val));
234 });
235};
236
237/**
238 * Evaluate the tree.
239 *
240 * @return {Node}
241 * @api private
242 */
243
244Evaluator.prototype.evaluate = function(){
245 debug('eval %s', this.filename);
246 this.setup();
247 return this.visit(this.root);
248};
249
250/**
251 * Visit Group.
252 */
253
254Evaluator.prototype.visitGroup = function(group){
255 group.nodes = group.nodes.map(function(selector){
256 selector.val = this.interpolate(selector);
257 debug('ruleset %s', selector.val);
258 return selector;
259 }, this);
260
261 group.block = this.visit(group.block);
262 return group;
263};
264
265/**
266 * Visit Return.
267 */
268
269Evaluator.prototype.visitReturn = function(ret){
270 ret.expr = this.visit(ret.expr);
271 throw ret;
272};
273
274/**
275 * Visit Media.
276 */
277
278Evaluator.prototype.visitMedia = function(media){
279 media.block = this.visit(media.block);
280 media.val = this.visit(media.val);
281 return media;
282};
283
284/**
285 * Visit QueryList.
286 */
287
288Evaluator.prototype.visitQueryList = function(queries){
289 var val, query;
290 queries.nodes.forEach(this.visit, this);
291
292 if (1 == queries.nodes.length) {
293 query = queries.nodes[0];
294 if (val = this.lookup(query.type)) {
295 val = val.first.string;
296 if (!val) return queries;
297 var Parser = require('../parser')
298 , parser = new Parser(val, this.options);
299 queries = this.visit(parser.queries());
300 }
301 }
302 return queries;
303};
304
305/**
306 * Visit Query.
307 */
308
309Evaluator.prototype.visitQuery = function(node){
310 node.predicate = this.visit(node.predicate);
311 node.type = this.visit(node.type);
312 node.nodes.forEach(this.visit, this);
313 return node;
314};
315
316/**
317 * Visit Feature.
318 */
319
320Evaluator.prototype.visitFeature = function(node){
321 node.name = this.interpolate(node);
322 if (node.expr) {
323 this.return++;
324 node.expr = this.visit(node.expr);
325 this.return--;
326 }
327 return node;
328};
329
330/**
331 * Visit Object.
332 */
333
334Evaluator.prototype.visitObject = function(obj){
335 for (var key in obj.vals) {
336 obj.vals[key] = this.visit(obj.vals[key]);
337 }
338 return obj;
339};
340
341/**
342 * Visit Member.
343 */
344
345Evaluator.prototype.visitMember = function(node){
346 var left = node.left
347 , right = node.right
348 , obj = this.visit(left).first;
349
350 if ('object' != obj.nodeName) {
351 throw new Error(left.toString() + ' has no property .' + right);
352 }
353 if (node.val) {
354 this.return++;
355 obj.set(right.name, this.visit(node.val));
356 this.return--;
357 }
358 return obj.get(right.name);
359};
360
361/**
362 * Visit Keyframes.
363 */
364
365Evaluator.prototype.visitKeyframes = function(keyframes){
366 var val;
367 if (keyframes.fabricated) return keyframes;
368 keyframes.val = this.interpolate(keyframes).trim();
369 if (val = this.lookup(keyframes.val)) {
370 keyframes.val = val.first.string || val.first.name;
371 }
372 keyframes.block = this.visit(keyframes.block);
373
374 if ('official' != keyframes.prefix) return keyframes;
375
376 this.vendors.forEach(function(prefix){
377 // IE never had prefixes for keyframes
378 if ('ms' == prefix) return;
379 var node = keyframes.clone();
380 node.val = keyframes.val;
381 node.prefix = prefix;
382 node.block = keyframes.block;
383 node.fabricated = true;
384 this.currentBlock.push(node);
385 }, this);
386
387 return nodes.null;
388};
389
390/**
391 * Visit Function.
392 */
393
394Evaluator.prototype.visitFunction = function(fn){
395 // check local
396 var local = this.stack.currentFrame.scope.lookup(fn.name);
397 if (local) this.warn('local ' + local.nodeName + ' "' + fn.name + '" previously defined in this scope');
398
399 // user-defined
400 var user = this.functions[fn.name];
401 if (user) this.warn('user-defined function "' + fn.name + '" is already defined');
402
403 // BIF
404 var bif = bifs[fn.name];
405 if (bif) this.warn('built-in function "' + fn.name + '" is already defined');
406
407 return fn;
408};
409
410/**
411 * Visit Each.
412 */
413
414Evaluator.prototype.visitEach = function(each){
415 this.return++;
416 var expr = utils.unwrap(this.visit(each.expr))
417 , len = expr.nodes.length
418 , val = new nodes.Ident(each.val)
419 , key = new nodes.Ident(each.key || '__index__')
420 , scope = this.currentScope
421 , block = this.currentBlock
422 , vals = []
423 , self = this
424 , body
425 , obj;
426 this.return--;
427
428 each.block.scope = false;
429
430 function visitBody(key, val) {
431 scope.add(val);
432 scope.add(key);
433 body = self.visit(each.block.clone());
434 vals = vals.concat(body.nodes);
435 }
436
437 // for prop in obj
438 if (1 == len && 'object' == expr.nodes[0].nodeName) {
439 obj = expr.nodes[0];
440 for (var prop in obj.vals) {
441 val.val = new nodes.String(prop);
442 key.val = obj.get(prop);
443 visitBody(key, val);
444 }
445 } else {
446 for (var i = 0; i < len; ++i) {
447 val.val = expr.nodes[i];
448 key.val = new nodes.Unit(i);
449 visitBody(key, val);
450 }
451 }
452
453 this.mixin(vals, block);
454 return vals[vals.length - 1] || nodes.null;
455};
456
457/**
458 * Visit Call.
459 */
460
461Evaluator.prototype.visitCall = function(call){
462 debug('call %s', call);
463 var fn = this.lookup(call.name)
464 , literal
465 , ret;
466
467 // url()
468 this.ignoreColors = 'url' == call.name;
469
470 // Variable function
471 if (fn && 'expression' == fn.nodeName) {
472 fn = fn.nodes[0];
473 }
474
475 // Not a function? try user-defined or built-ins
476 if (fn && 'function' != fn.nodeName) {
477 fn = this.lookupFunction(call.name);
478 }
479
480 // Undefined function? render literal CSS
481 if (!fn || fn.nodeName != 'function') {
482 debug('%s is undefined', call);
483 // Special case for `calc`
484 if ('calc' == this.unvendorize(call.name)) {
485 literal = call.args.nodes && call.args.nodes[0];
486 if (literal) ret = new nodes.Literal(call.name + literal);
487 } else {
488 ret = this.literalCall(call);
489 }
490 this.ignoreColors = false;
491 return ret;
492 }
493
494 this.calling.push(call.name);
495
496 // Massive stack
497 if (this.calling.length > 200) {
498 throw new RangeError('Maximum stylus call stack size exceeded');
499 }
500
501 // First node in expression
502 if ('expression' == fn.nodeName) fn = fn.first;
503
504 // Evaluate arguments
505 this.return++;
506 var args = this.visit(call.args);
507
508 for (var key in args.map) {
509 args.map[key] = this.visit(args.map[key].clone());
510 }
511 this.return--;
512
513 // Built-in
514 if (fn.fn) {
515 debug('%s is built-in', call);
516 ret = this.invokeBuiltin(fn.fn, args);
517 // User-defined
518 } else if ('function' == fn.nodeName) {
519 debug('%s is user-defined', call);
520 // Evaluate mixin block
521 if (call.block) call.block = this.visit(call.block);
522 ret = this.invokeFunction(fn, args, call.block);
523 }
524
525 this.calling.pop();
526 this.ignoreColors = false;
527 return ret;
528};
529
530/**
531 * Visit Ident.
532 */
533
534Evaluator.prototype.visitIdent = function(ident){
535 var prop;
536 // Property lookup
537 if (ident.property) {
538 if (prop = this.lookupProperty(ident.name)) {
539 return this.visit(prop.expr.clone());
540 }
541 return nodes.null;
542 // Lookup
543 } else if (ident.val.isNull) {
544 var val = this.lookup(ident.name);
545 // Object or Block mixin
546 if (val && ident.mixin) this.mixinNode(val);
547 return val ? this.visit(val) : ident;
548 // Assign
549 } else {
550 this.return++;
551 ident.val = this.visit(ident.val);
552 this.return--;
553 this.currentScope.add(ident);
554 return ident.val;
555 }
556};
557
558/**
559 * Visit BinOp.
560 */
561
562Evaluator.prototype.visitBinOp = function(binop){
563 // Special-case "is defined" pseudo binop
564 if ('is defined' == binop.op) return this.isDefined(binop.left);
565
566 this.return++;
567 // Visit operands
568 var op = binop.op
569 , left = this.visit(binop.left)
570 , right = ('||' == op || '&&' == op)
571 ? binop.right : this.visit(binop.right);
572
573 // HACK: ternary
574 var val = binop.val
575 ? this.visit(binop.val)
576 : null;
577 this.return--;
578
579 // Operate
580 try {
581 return this.visit(left.operate(op, right, val));
582 } catch (err) {
583 // disregard coercion issues in equality
584 // checks, and simply return false
585 if ('CoercionError' == err.name) {
586 switch (op) {
587 case '==':
588 return nodes.false;
589 case '!=':
590 return nodes.true;
591 }
592 }
593 throw err;
594 }
595};
596
597/**
598 * Visit UnaryOp.
599 */
600
601Evaluator.prototype.visitUnaryOp = function(unary){
602 var op = unary.op
603 , node = this.visit(unary.expr);
604
605 if ('!' != op) {
606 node = node.first.clone();
607 utils.assertType(node, 'unit');
608 }
609
610 switch (op) {
611 case '-':
612 node.val = -node.val;
613 break;
614 case '+':
615 node.val = +node.val;
616 break;
617 case '~':
618 node.val = ~node.val;
619 break;
620 case '!':
621 return node.toBoolean().negate();
622 }
623
624 return node;
625};
626
627/**
628 * Visit TernaryOp.
629 */
630
631Evaluator.prototype.visitTernary = function(ternary){
632 var ok = this.visit(ternary.cond).toBoolean();
633 return ok.isTrue
634 ? this.visit(ternary.trueExpr)
635 : this.visit(ternary.falseExpr);
636};
637
638/**
639 * Visit Expression.
640 */
641
642Evaluator.prototype.visitExpression = function(expr){
643 for (var i = 0, len = expr.nodes.length; i < len; ++i) {
644 expr.nodes[i] = this.visit(expr.nodes[i]);
645 }
646
647 // support (n * 5)px etc
648 if (this.castable(expr)) expr = this.cast(expr);
649
650 return expr;
651};
652
653/**
654 * Visit Arguments.
655 */
656
657Evaluator.prototype.visitArguments = Evaluator.prototype.visitExpression;
658
659/**
660 * Visit Property.
661 */
662
663Evaluator.prototype.visitProperty = function(prop){
664 var name = this.interpolate(prop)
665 , fn = this.lookup(name)
666 , call = fn && 'function' == fn.first.nodeName
667 , literal = ~this.calling.indexOf(name)
668 , _prop = this.property;
669
670 // Function of the same name
671 if (call && !literal && !prop.literal) {
672 var args = nodes.Arguments.fromExpression(utils.unwrap(prop.expr.clone()));
673 prop.name = name;
674 this.property = prop;
675 this.return++;
676 this.property.expr = this.visit(prop.expr);
677 this.return--;
678 var ret = this.visit(new nodes.Call(name, args));
679 this.property = _prop;
680 return ret;
681 // Regular property
682 } else {
683 this.return++;
684 prop.name = name;
685 prop.literal = true;
686 this.property = prop;
687 prop.expr = this.visit(prop.expr);
688 this.property = _prop;
689 this.return--;
690 return prop;
691 }
692};
693
694/**
695 * Visit Root.
696 */
697
698Evaluator.prototype.visitRoot = function(block){
699 // normalize cached imports
700 if (block != this.root) {
701 block.constructor = nodes.Block;
702 return this.visit(block);
703 }
704
705 for (var i = 0; i < block.nodes.length; ++i) {
706 block.index = i;
707 block.nodes[i] = this.visit(block.nodes[i]);
708 }
709 return block;
710};
711
712/**
713 * Visit Block.
714 */
715
716Evaluator.prototype.visitBlock = function(block){
717 this.stack.push(new Frame(block));
718 for (block.index = 0; block.index < block.nodes.length; ++block.index) {
719 try {
720 block.nodes[block.index] = this.visit(block.nodes[block.index]);
721 } catch (err) {
722 if ('return' == err.nodeName) {
723 if (this.return) {
724 this.stack.pop();
725 throw err;
726 } else {
727 block.nodes[block.index] = err;
728 break;
729 }
730 } else {
731 throw err;
732 }
733 }
734 }
735 this.stack.pop();
736 return block;
737};
738
739/**
740 * Visit Atblock.
741 */
742
743Evaluator.prototype.visitAtblock = function(atblock){
744 atblock.block = this.visit(atblock.block);
745 return atblock;
746};
747
748/**
749 * Visit Atrule.
750 */
751
752Evaluator.prototype.visitAtrule = function(atrule){
753 atrule.val = this.interpolate(atrule);
754 if (atrule.block) atrule.block = this.visit(atrule.block);
755 return atrule;
756};
757
758/**
759 * Visit Supports.
760 */
761
762Evaluator.prototype.visitSupports = function(node){
763 var condition = node.condition
764 , val;
765
766 this.return++;
767 node.condition = this.visit(condition);
768 this.return--;
769
770 val = condition.first;
771 if (1 == condition.nodes.length
772 && 'string' == val.nodeName) {
773 node.condition = val.string;
774 }
775 node.block = this.visit(node.block);
776 return node;
777};
778
779/**
780 * Visit If.
781 */
782
783Evaluator.prototype.visitIf = function(node){
784 var ret
785 , block = this.currentBlock
786 , negate = node.negate;
787
788 this.return++;
789 var ok = this.visit(node.cond).first.toBoolean();
790 this.return--;
791
792 node.block.scope = node.block.hasMedia;
793
794 // Evaluate body
795 if (negate) {
796 // unless
797 if (ok.isFalse) {
798 ret = this.visit(node.block);
799 }
800 } else {
801 // if
802 if (ok.isTrue) {
803 ret = this.visit(node.block);
804 // else
805 } else if (node.elses.length) {
806 var elses = node.elses
807 , len = elses.length
808 , cond;
809 for (var i = 0; i < len; ++i) {
810 // else if
811 if (elses[i].cond) {
812 elses[i].block.scope = elses[i].block.hasMedia;
813 this.return++;
814 cond = this.visit(elses[i].cond).first.toBoolean();
815 this.return--;
816 if (cond.isTrue) {
817 ret = this.visit(elses[i].block);
818 break;
819 }
820 // else
821 } else {
822 elses[i].scope = elses[i].hasMedia;
823 ret = this.visit(elses[i]);
824 }
825 }
826 }
827 }
828
829 // mixin conditional statements within
830 // a selector group or at-rule
831 if (ret && !node.postfix && block.node
832 && ~['group'
833 , 'atrule'
834 , 'media'
835 , 'supports'
836 , 'keyframes'].indexOf(block.node.nodeName)) {
837 this.mixin(ret.nodes, block);
838 return nodes.null;
839 }
840
841 return ret || nodes.null;
842};
843
844/**
845 * Visit Extend.
846 */
847
848Evaluator.prototype.visitExtend = function(extend){
849 var block = this.currentBlock;
850 if ('group' != block.node.nodeName) block = this.closestGroup;
851 extend.selectors.forEach(function(selector){
852 block.node.extends.push({
853 // Cloning the selector for when we are in a loop and don't want it to affect
854 // the selector nodes and cause the values to be different to expected
855 selector: this.interpolate(selector.clone()).trim(),
856 optional: selector.optional,
857 lineno: selector.lineno,
858 column: selector.column
859 });
860 }, this);
861 return nodes.null;
862};
863
864/**
865 * Visit Import.
866 */
867
868Evaluator.prototype.visitImport = function(imported){
869 this.return++;
870
871 var path = this.visit(imported.path).first
872 , nodeName = imported.once ? 'require' : 'import'
873 , found
874 , literal;
875
876 this.return--;
877 debug('import %s', path);
878
879 // url() passed
880 if ('url' == path.name) {
881 if (imported.once) throw new Error('You cannot @require a url');
882
883 return imported;
884 }
885
886 // Ensure string
887 if (!path.string) throw new Error('@' + nodeName + ' string expected');
888
889 var name = path = path.string;
890
891 // Absolute URL or hash
892 if (/(?:url\s*\(\s*)?['"]?(?:#|(?:https?:)?\/\/)/i.test(path)) {
893 if (imported.once) throw new Error('You cannot @require a url');
894 return imported;
895 }
896
897 // Literal
898 if (/\.css(?:"|$)/.test(path)) {
899 literal = true;
900 if (!imported.once && !this.includeCSS) {
901 return imported;
902 }
903 }
904
905 // support optional .styl
906 if (!literal && !/\.styl$/i.test(path)) path += '.styl';
907
908 // Lookup
909 found = utils.find(path, this.paths, this.filename);
910 if (!found) {
911 found = utils.lookupIndex(name, this.paths, this.filename);
912 }
913
914 // Throw if import failed
915 if (!found) throw new Error('failed to locate @' + nodeName + ' file ' + path);
916
917 var block = new nodes.Block;
918
919 for (var i = 0, len = found.length; i < len; ++i) {
920 block.push(importFile.call(this, imported, found[i], literal));
921 }
922
923 return block;
924};
925
926/**
927 * Invoke `fn` with `args`.
928 *
929 * @param {Function} fn
930 * @param {Array} args
931 * @return {Node}
932 * @api private
933 */
934
935Evaluator.prototype.invokeFunction = function(fn, args, content){
936 var block = new nodes.Block(fn.block.parent);
937
938 // Clone the function body
939 // to prevent mutation of subsequent calls
940 var body = fn.block.clone(block);
941
942 // mixin block
943 var mixinBlock = this.stack.currentFrame.block;
944
945 // new block scope
946 this.stack.push(new Frame(block));
947 var scope = this.currentScope;
948
949 // normalize arguments
950 if ('arguments' != args.nodeName) {
951 var expr = new nodes.Expression;
952 expr.push(args);
953 args = nodes.Arguments.fromExpression(expr);
954 }
955
956 // arguments local
957 scope.add(new nodes.Ident('arguments', args));
958
959 // mixin scope introspection
960 scope.add(new nodes.Ident('mixin', this.return
961 ? nodes.false
962 : new nodes.String(mixinBlock.nodeName)));
963
964 // current property
965 if (this.property) {
966 var prop = this.propertyExpression(this.property, fn.name);
967 scope.add(new nodes.Ident('current-property', prop));
968 } else {
969 scope.add(new nodes.Ident('current-property', nodes.null));
970 }
971
972 // current call stack
973 var expr = new nodes.Expression;
974 for (var i = this.calling.length - 1; i-- ; ) {
975 expr.push(new nodes.Literal(this.calling[i]));
976 };
977 scope.add(new nodes.Ident('called-from', expr));
978
979 // inject arguments as locals
980 var i = 0
981 , len = args.nodes.length;
982 fn.params.nodes.forEach(function(node){
983 // rest param support
984 if (node.rest) {
985 node.val = new nodes.Expression;
986 for (; i < len; ++i) node.val.push(args.nodes[i]);
987 node.val.preserve = true;
988 node.val.isList = args.isList;
989 // argument default support
990 } else {
991 var arg = args.map[node.name] || args.nodes[i++];
992 node = node.clone();
993 if (arg) {
994 arg.isEmpty ? args.nodes[i - 1] = this.visit(node) : node.val = arg;
995 } else {
996 args.push(node.val);
997 }
998
999 // required argument not satisfied
1000 if (node.val.isNull) {
1001 throw new Error('argument "' + node + '" required for ' + fn);
1002 }
1003 }
1004
1005 scope.add(node);
1006 }, this);
1007
1008 // mixin block
1009 if (content) scope.add(new nodes.Ident('block', content, true));
1010
1011 // invoke
1012 return this.invoke(body, true, fn.filename);
1013};
1014
1015/**
1016 * Invoke built-in `fn` with `args`.
1017 *
1018 * @param {Function} fn
1019 * @param {Array} args
1020 * @return {Node}
1021 * @api private
1022 */
1023
1024Evaluator.prototype.invokeBuiltin = function(fn, args){
1025 // Map arguments to first node
1026 // providing a nicer js api for
1027 // BIFs. Functions may specify that
1028 // they wish to accept full expressions
1029 // via .raw
1030 if (fn.raw) {
1031 args = args.nodes;
1032 } else {
1033 if (!fn.params) {
1034 fn.params = utils.params(fn);
1035 }
1036 args = fn.params.reduce(function(ret, param){
1037 var arg = args.map[param] || args.nodes.shift()
1038 if (arg) {
1039 arg = utils.unwrap(arg);
1040 var len = arg.nodes.length;
1041 if (len > 1) {
1042 for (var i = 0; i < len; ++i) {
1043 ret.push(utils.unwrap(arg.nodes[i].first));
1044 }
1045 } else {
1046 ret.push(arg.first);
1047 }
1048 }
1049 return ret;
1050 }, []);
1051 }
1052
1053 // Invoke the BIF
1054 var body = utils.coerce(fn.apply(this, args));
1055
1056 // Always wrapping allows js functions
1057 // to return several values with a single
1058 // Expression node
1059 var expr = new nodes.Expression;
1060 expr.push(body);
1061 body = expr;
1062
1063 // Invoke
1064 return this.invoke(body);
1065};
1066
1067/**
1068 * Invoke the given function `body`.
1069 *
1070 * @param {Block} body
1071 * @return {Node}
1072 * @api private
1073 */
1074
1075Evaluator.prototype.invoke = function(body, stack, filename){
1076 var self = this
1077 , ret;
1078
1079 if (filename) this.paths.push(dirname(filename));
1080
1081 // Return
1082 if (this.return) {
1083 ret = this.eval(body.nodes);
1084 if (stack) this.stack.pop();
1085 // Mixin
1086 } else {
1087 body = this.visit(body);
1088 if (stack) this.stack.pop();
1089 this.mixin(body.nodes, this.currentBlock);
1090 ret = nodes.null;
1091 }
1092
1093 if (filename) this.paths.pop();
1094
1095 return ret;
1096};
1097
1098/**
1099 * Mixin the given `nodes` to the given `block`.
1100 *
1101 * @param {Array} nodes
1102 * @param {Block} block
1103 * @api private
1104 */
1105
1106Evaluator.prototype.mixin = function(nodes, block){
1107 if (!nodes.length) return;
1108 var len = block.nodes.length
1109 , head = block.nodes.slice(0, block.index)
1110 , tail = block.nodes.slice(block.index + 1, len);
1111 this._mixin(nodes, head, block);
1112 block.index = 0;
1113 block.nodes = head.concat(tail);
1114};
1115
1116/**
1117 * Mixin the given `items` to the `dest` array.
1118 *
1119 * @param {Array} items
1120 * @param {Array} dest
1121 * @param {Block} block
1122 * @api private
1123 */
1124
1125Evaluator.prototype._mixin = function(items, dest, block){
1126 var node
1127 , len = items.length;
1128 for (var i = 0; i < len; ++i) {
1129 switch ((node = items[i]).nodeName) {
1130 case 'return':
1131 return;
1132 case 'block':
1133 this._mixin(node.nodes, dest, block);
1134 break;
1135 case 'media':
1136 // fix link to the parent block
1137 var parentNode = node.block.parent.node;
1138 if (parentNode && 'call' != parentNode.nodeName) {
1139 node.block.parent = block;
1140 }
1141 case 'property':
1142 var val = node.expr;
1143 // prevent `block` mixin recursion
1144 if (node.literal && 'block' == val.first.name) {
1145 val = utils.unwrap(val);
1146 val.nodes[0] = new nodes.Literal('block');
1147 }
1148 default:
1149 dest.push(node);
1150 }
1151 }
1152};
1153
1154/**
1155 * Mixin the given `node` to the current block.
1156 *
1157 * @param {Node} node
1158 * @api private
1159 */
1160
1161Evaluator.prototype.mixinNode = function(node){
1162 node = this.visit(node.first);
1163 switch (node.nodeName) {
1164 case 'object':
1165 this.mixinObject(node);
1166 return nodes.null;
1167 case 'block':
1168 case 'atblock':
1169 this.mixin(node.nodes, this.currentBlock);
1170 return nodes.null;
1171 }
1172};
1173
1174/**
1175 * Mixin the given `object` to the current block.
1176 *
1177 * @param {Object} object
1178 * @api private
1179 */
1180
1181Evaluator.prototype.mixinObject = function(object){
1182 var Parser = require('../parser')
1183 , root = this.root
1184 , str = '$block ' + object.toBlock()
1185 , parser = new Parser(str, utils.merge({ root: block }, this.options))
1186 , block;
1187
1188 try {
1189 block = parser.parse();
1190 } catch (err) {
1191 err.filename = this.filename;
1192 err.lineno = parser.lexer.lineno;
1193 err.column = parser.lexer.column;
1194 err.input = str;
1195 throw err;
1196 }
1197
1198 block.parent = root;
1199 block.scope = false;
1200 var ret = this.visit(block)
1201 , vals = ret.first.nodes;
1202 for (var i = 0, len = vals.length; i < len; ++i) {
1203 if (vals[i].block) {
1204 this.mixin(vals[i].block.nodes, this.currentBlock);
1205 break;
1206 }
1207 }
1208};
1209
1210/**
1211 * Evaluate the given `vals`.
1212 *
1213 * @param {Array} vals
1214 * @return {Node}
1215 * @api private
1216 */
1217
1218Evaluator.prototype.eval = function(vals){
1219 if (!vals) return nodes.null;
1220 var len = vals.length
1221 , node = nodes.null;
1222
1223 try {
1224 for (var i = 0; i < len; ++i) {
1225 node = vals[i];
1226 switch (node.nodeName) {
1227 case 'if':
1228 if ('block' != node.block.nodeName) {
1229 node = this.visit(node);
1230 break;
1231 }
1232 case 'each':
1233 case 'block':
1234 node = this.visit(node);
1235 if (node.nodes) node = this.eval(node.nodes);
1236 break;
1237 default:
1238 node = this.visit(node);
1239 }
1240 }
1241 } catch (err) {
1242 if ('return' == err.nodeName) {
1243 return err.expr;
1244 } else {
1245 throw err;
1246 }
1247 }
1248
1249 return node;
1250};
1251
1252/**
1253 * Literal function `call`.
1254 *
1255 * @param {Call} call
1256 * @return {call}
1257 * @api private
1258 */
1259
1260Evaluator.prototype.literalCall = function(call){
1261 call.args = this.visit(call.args);
1262 return call;
1263};
1264
1265/**
1266 * Lookup property `name`.
1267 *
1268 * @param {String} name
1269 * @return {Property}
1270 * @api private
1271 */
1272
1273Evaluator.prototype.lookupProperty = function(name){
1274 var i = this.stack.length
1275 , index = this.currentBlock.index
1276 , top = i
1277 , nodes
1278 , block
1279 , len
1280 , other;
1281
1282 while (i--) {
1283 block = this.stack[i].block;
1284 if (!block.node) continue;
1285 switch (block.node.nodeName) {
1286 case 'group':
1287 case 'function':
1288 case 'if':
1289 case 'each':
1290 case 'atrule':
1291 case 'media':
1292 case 'atblock':
1293 case 'call':
1294 nodes = block.nodes;
1295 // scan siblings from the property index up
1296 if (i + 1 == top) {
1297 while (index--) {
1298 // ignore current property
1299 if (this.property == nodes[index]) continue;
1300 other = this.interpolate(nodes[index]);
1301 if (name == other) return nodes[index].clone();
1302 }
1303 // sequential lookup for non-siblings (for now)
1304 } else {
1305 len = nodes.length;
1306 while (len--) {
1307 if ('property' != nodes[len].nodeName
1308 || this.property == nodes[len]) continue;
1309 other = this.interpolate(nodes[len]);
1310 if (name == other) return nodes[len].clone();
1311 }
1312 }
1313 break;
1314 }
1315 }
1316
1317 return nodes.null;
1318};
1319
1320/**
1321 * Return the closest mixin-able `Block`.
1322 *
1323 * @return {Block}
1324 * @api private
1325 */
1326
1327Evaluator.prototype.__defineGetter__('closestBlock', function(){
1328 var i = this.stack.length
1329 , block;
1330 while (i--) {
1331 block = this.stack[i].block;
1332 if (block.node) {
1333 switch (block.node.nodeName) {
1334 case 'group':
1335 case 'keyframes':
1336 case 'atrule':
1337 case 'atblock':
1338 case 'media':
1339 case 'call':
1340 return block;
1341 }
1342 }
1343 }
1344});
1345
1346/**
1347 * Return the closest group block.
1348 *
1349 * @return {Block}
1350 * @api private
1351 */
1352
1353Evaluator.prototype.__defineGetter__('closestGroup', function(){
1354 var i = this.stack.length
1355 , block;
1356 while (i--) {
1357 block = this.stack[i].block;
1358 if (block.node && 'group' == block.node.nodeName) {
1359 return block;
1360 }
1361 }
1362});
1363
1364/**
1365 * Return the current selectors stack.
1366 *
1367 * @return {Array}
1368 * @api private
1369 */
1370
1371Evaluator.prototype.__defineGetter__('selectorStack', function(){
1372 var block
1373 , stack = [];
1374 for (var i = 0, len = this.stack.length; i < len; ++i) {
1375 block = this.stack[i].block;
1376 if (block.node && 'group' == block.node.nodeName) {
1377 block.node.nodes.forEach(function(selector) {
1378 if (!selector.val) selector.val = this.interpolate(selector);
1379 }, this);
1380 stack.push(block.node.nodes);
1381 }
1382 }
1383 return stack;
1384});
1385
1386/**
1387 * Lookup `name`, with support for JavaScript
1388 * functions, and BIFs.
1389 *
1390 * @param {String} name
1391 * @return {Node}
1392 * @api private
1393 */
1394
1395Evaluator.prototype.lookup = function(name){
1396 var val;
1397 if (this.ignoreColors && name in colors) return;
1398 if (val = this.stack.lookup(name)) {
1399 return utils.unwrap(val);
1400 } else {
1401 return this.lookupFunction(name);
1402 }
1403};
1404
1405/**
1406 * Map segments in `node` returning a string.
1407 *
1408 * @param {Node} node
1409 * @return {String}
1410 * @api private
1411 */
1412
1413Evaluator.prototype.interpolate = function(node){
1414 var self = this
1415 , isSelector = ('selector' == node.nodeName);
1416 function toString(node) {
1417 switch (node.nodeName) {
1418 case 'function':
1419 case 'ident':
1420 return node.name;
1421 case 'literal':
1422 case 'string':
1423 if (self.prefix && !node.prefixed && !node.val.nodeName) {
1424 node.val = node.val.replace(/\.(?=[\w-])|^\.$/g, '.' + self.prefix);
1425 node.prefixed = true;
1426 }
1427 return node.val;
1428 case 'unit':
1429 // Interpolation inside keyframes
1430 return '%' == node.type ? node.val + '%' : node.val;
1431 case 'member':
1432 return toString(self.visit(node));
1433 case 'expression':
1434 // Prevent cyclic `selector()` calls.
1435 if (self.calling && ~self.calling.indexOf('selector') && self._selector) return self._selector;
1436 self.return++;
1437 var ret = toString(self.visit(node).first);
1438 self.return--;
1439 if (isSelector) self._selector = ret;
1440 return ret;
1441 }
1442 }
1443
1444 if (node.segments) {
1445 return node.segments.map(toString).join('');
1446 } else {
1447 return toString(node);
1448 }
1449};
1450
1451/**
1452 * Lookup JavaScript user-defined or built-in function.
1453 *
1454 * @param {String} name
1455 * @return {Function}
1456 * @api private
1457 */
1458
1459Evaluator.prototype.lookupFunction = function(name){
1460 var fn = this.functions[name] || bifs[name];
1461 if (fn) return new nodes.Function(name, fn);
1462};
1463
1464/**
1465 * Check if the given `node` is an ident, and if it is defined.
1466 *
1467 * @param {Node} node
1468 * @return {Boolean}
1469 * @api private
1470 */
1471
1472Evaluator.prototype.isDefined = function(node){
1473 if ('ident' == node.nodeName) {
1474 return nodes.Boolean(this.lookup(node.name));
1475 } else {
1476 throw new Error('invalid "is defined" check on non-variable ' + node);
1477 }
1478};
1479
1480/**
1481 * Return `Expression` based on the given `prop`,
1482 * replacing cyclic calls to the given function `name`
1483 * with "__CALL__".
1484 *
1485 * @param {Property} prop
1486 * @param {String} name
1487 * @return {Expression}
1488 * @api private
1489 */
1490
1491Evaluator.prototype.propertyExpression = function(prop, name){
1492 var expr = new nodes.Expression
1493 , val = prop.expr.clone();
1494
1495 // name
1496 expr.push(new nodes.String(prop.name));
1497
1498 // replace cyclic call with __CALL__
1499 function replace(node) {
1500 if ('call' == node.nodeName && name == node.name) {
1501 return new nodes.Literal('__CALL__');
1502 }
1503
1504 if (node.nodes) node.nodes = node.nodes.map(replace);
1505 return node;
1506 }
1507
1508 replace(val);
1509 expr.push(val);
1510 return expr;
1511};
1512
1513/**
1514 * Cast `expr` to the trailing ident.
1515 *
1516 * @param {Expression} expr
1517 * @return {Unit}
1518 * @api private
1519 */
1520
1521Evaluator.prototype.cast = function(expr){
1522 return new nodes.Unit(expr.first.val, expr.nodes[1].name);
1523};
1524
1525/**
1526 * Check if `expr` is castable.
1527 *
1528 * @param {Expression} expr
1529 * @return {Boolean}
1530 * @api private
1531 */
1532
1533Evaluator.prototype.castable = function(expr){
1534 return 2 == expr.nodes.length
1535 && 'unit' == expr.first.nodeName
1536 && ~units.indexOf(expr.nodes[1].name);
1537};
1538
1539/**
1540 * Warn with the given `msg`.
1541 *
1542 * @param {String} msg
1543 * @api private
1544 */
1545
1546Evaluator.prototype.warn = function(msg){
1547 if (!this.warnings) return;
1548 console.warn('\u001b[33mWarning:\u001b[0m ' + msg);
1549};
1550
1551/**
1552 * Return the current `Block`.
1553 *
1554 * @return {Block}
1555 * @api private
1556 */
1557
1558Evaluator.prototype.__defineGetter__('currentBlock', function(){
1559 return this.stack.currentFrame.block;
1560});
1561
1562/**
1563 * Return an array of vendor names.
1564 *
1565 * @return {Array}
1566 * @api private
1567 */
1568
1569Evaluator.prototype.__defineGetter__('vendors', function(){
1570 return this.lookup('vendors').nodes.map(function(node){
1571 return node.string;
1572 });
1573});
1574
1575/**
1576 * Return the property name without vendor prefix.
1577 *
1578 * @param {String} prop
1579 * @return {String}
1580 * @api public
1581 */
1582
1583Evaluator.prototype.unvendorize = function(prop){
1584 for (var i = 0, len = this.vendors.length; i < len; i++) {
1585 if ('official' != this.vendors[i]) {
1586 var vendor = '-' + this.vendors[i] + '-';
1587 if (~prop.indexOf(vendor)) return prop.replace(vendor, '');
1588 }
1589 }
1590 return prop;
1591};
1592
1593/**
1594 * Return the current frame `Scope`.
1595 *
1596 * @return {Scope}
1597 * @api private
1598 */
1599
1600Evaluator.prototype.__defineGetter__('currentScope', function(){
1601 return this.stack.currentFrame.scope;
1602});
1603
1604/**
1605 * Return the current `Frame`.
1606 *
1607 * @return {Frame}
1608 * @api private
1609 */
1610
1611Evaluator.prototype.__defineGetter__('currentFrame', function(){
1612 return this.stack.currentFrame;
1613});
Note: See TracBrowser for help on using the repository browser.