OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 part of csslib.parser; | 5 part of csslib.parser; |
6 | 6 |
7 | 7 |
| 8 // TODO(terry): Add optimizing phase to remove duplicated selectors in the same |
| 9 // selector group (e.g., .btn, .btn { color: red; }). Also, look |
| 10 // at simplifying selectors expressions too (much harder). |
8 // TODO(terry): Detect invalid directive usage. All @imports must occur before | 11 // TODO(terry): Detect invalid directive usage. All @imports must occur before |
9 // all rules other than @charset directive. Any @import directive | 12 // all rules other than @charset directive. Any @import directive |
10 // after any non @charset or @import directive are ignored. e.g., | 13 // after any non @charset or @import directive are ignored. e.g., |
11 // @import "a.css"; | 14 // @import "a.css"; |
12 // div { color: red; } | 15 // div { color: red; } |
13 // @import "b.css"; | 16 // @import "b.css"; |
14 // becomes: | 17 // becomes: |
15 // @import "a.css"; | 18 // @import "a.css"; |
16 // div { color: red; } | 19 // div { color: red; } |
17 // <http://www.w3.org/TR/css3-syntax/#at-rules> | 20 // <http://www.w3.org/TR/css3-syntax/#at-rules> |
18 | 21 |
19 /** | 22 /** |
20 * Analysis phase will validate/fixup any new CSS feature or any SASS style | 23 * Analysis phase will validate/fixup any new CSS feature or any SASS style |
21 * feature. | 24 * feature. |
22 */ | 25 */ |
23 class Analyzer { | 26 class Analyzer { |
24 final List<StyleSheet> _styleSheets; | 27 final List<StyleSheet> _styleSheets; |
25 final Messages _messages; | 28 final Messages _messages; |
26 VarDefinitions varDefs; | |
27 | 29 |
28 Analyzer(this._styleSheets, this._messages); | 30 Analyzer(this._styleSheets, this._messages); |
29 | 31 |
| 32 // TODO(terry): Currently each feature walks the AST each time. Once we have |
| 33 // our complete feature set consider benchmarking the cost and |
| 34 // possibly combine in one walk. |
30 void run() { | 35 void run() { |
31 varDefs = new VarDefinitions(_styleSheets); | 36 // Expand top-level @include. |
| 37 _styleSheets.forEach((styleSheet) => |
| 38 TopLevelIncludes.expand(_messages, _styleSheets)); |
32 | 39 |
33 // Any cycles? | 40 // Expand @include in declarations. |
34 var cycles = findAllCycles(); | 41 _styleSheets.forEach((styleSheet) => |
35 for (var cycle in cycles) { | 42 DeclarationIncludes.expand(_messages, _styleSheets)); |
36 _messages.warning("var cycle detected var-${cycle.definedName}", | |
37 cycle.span); | |
38 // TODO(terry): What if no var definition for a var usage an error? | |
39 // TODO(terry): Ensure a var definition imported from a different style | |
40 // sheet works. | |
41 } | |
42 | 43 |
43 // Remove any var definition from the stylesheet that has a cycle. | 44 // Remove all @mixin and @include |
44 _styleSheets.forEach((styleSheet) => | 45 _styleSheets.forEach((styleSheet) => MixinsAndIncludes.remove(styleSheet)); |
45 new RemoveVarDefinitions(cycles).visitStyleSheet(styleSheet)); | |
46 | 46 |
47 // Expand any nested selectors using selector desendant combinator to | 47 // Expand any nested selectors using selector desendant combinator to |
48 // signal CSS inheritance notation. | 48 // signal CSS inheritance notation. |
49 _styleSheets.forEach((styleSheet) => new ExpandNestedSelectors() | 49 _styleSheets.forEach((styleSheet) => new ExpandNestedSelectors() |
50 ..visitStyleSheet(styleSheet) | 50 ..visitStyleSheet(styleSheet) |
51 ..flatten(styleSheet)); | 51 ..flatten(styleSheet)); |
52 } | |
53 | 52 |
54 List<VarDefinition> findAllCycles() { | 53 // Expand any @extend. |
55 var cycles = []; | 54 _styleSheets.forEach((styleSheet) { |
56 | 55 var allExtends = new AllExtends()..visitStyleSheet(styleSheet); |
57 varDefs.map.values.forEach((value) { | 56 new InheritExtends(_messages, allExtends)..visitStyleSheet(styleSheet); |
58 if (hasCycle(value.property)) cycles.add(value); | 57 }); |
59 }); | |
60 | |
61 // Update our local list of known varDefs remove any varDefs with a cycle. | |
62 // So the same varDef cycle isn't reported for each style sheet processed. | |
63 for (var cycle in cycles) { | |
64 varDefs.map.remove(cycle.property); | |
65 } | |
66 | |
67 return cycles; | |
68 } | |
69 | |
70 Iterable<VarUsage> variablesOf(Expressions exprs) => | |
71 exprs.expressions.where((e) => e is VarUsage); | |
72 | |
73 bool hasCycle(String varName, {Set<String> visiting, Set<String> visited}) { | |
74 if (visiting == null) visiting = new Set(); | |
75 if (visited == null) visited = new Set(); | |
76 if (visiting.contains(varName)) return true; | |
77 if (visited.contains(varName)) return false; | |
78 visiting.add(varName); | |
79 visited.add(varName); | |
80 bool cycleDetected = false; | |
81 if (varDefs.map[varName] != null) { | |
82 for (var usage in variablesOf(varDefs.map[varName].expression)) { | |
83 if (hasCycle(usage.name, visiting: visiting, visited: visited)) { | |
84 cycleDetected = true; | |
85 break; | |
86 } | |
87 } | |
88 } | |
89 visiting.remove(varName); | |
90 return cycleDetected; | |
91 } | |
92 | |
93 // TODO(terry): Need to start supporting @host, custom pseudo elements, | |
94 // composition, intrinsics, etc. | |
95 } | |
96 | |
97 | |
98 /** Find all var definitions from a list of stylesheets. */ | |
99 class VarDefinitions extends Visitor { | |
100 /** Map of variable name key to it's definition. */ | |
101 final Map<String, VarDefinition> map = new Map<String, VarDefinition>(); | |
102 | |
103 VarDefinitions(List<StyleSheet> styleSheets) { | |
104 for (var styleSheet in styleSheets) { | |
105 visitTree(styleSheet); | |
106 } | |
107 } | |
108 | |
109 void visitVarDefinition(VarDefinition node) { | |
110 // Replace with latest variable definition. | |
111 map[node.definedName] = node; | |
112 super.visitVarDefinition(node); | |
113 } | |
114 | |
115 void visitVarDefinitionDirective(VarDefinitionDirective node) { | |
116 visitVarDefinition(node.def); | |
117 } | 58 } |
118 } | 59 } |
119 | 60 |
120 /** | |
121 * Remove the var definition from the stylesheet where it is defined; if it is | |
122 * a definition from the list to delete. | |
123 */ | |
124 class RemoveVarDefinitions extends Visitor { | |
125 final List<VarDefinition> _varDefsToRemove; | |
126 | |
127 RemoveVarDefinitions(this._varDefsToRemove); | |
128 | |
129 void visitStyleSheet(StyleSheet ss) { | |
130 var idx = ss.topLevels.length; | |
131 while(--idx >= 0) { | |
132 var topLevel = ss.topLevels[idx]; | |
133 if (topLevel is VarDefinitionDirective && | |
134 _varDefsToRemove.contains(topLevel.def)) { | |
135 ss.topLevels.removeAt(idx); | |
136 } | |
137 } | |
138 | |
139 super.visitStyleSheet(ss); | |
140 } | |
141 | |
142 void visitDeclarationGroup(DeclarationGroup node) { | |
143 var idx = node.declarations.length; | |
144 while (--idx >= 0) { | |
145 var decl = node.declarations[idx]; | |
146 if (decl is VarDefinition && _varDefsToRemove.contains(decl)) { | |
147 node.declarations.removeAt(idx); | |
148 } | |
149 } | |
150 | |
151 super.visitDeclarationGroup(node); | |
152 } | |
153 } | |
154 | |
155 /** | 61 /** |
156 * Traverse all rulesets looking for nested ones. If a ruleset is in a | 62 * Traverse all rulesets looking for nested ones. If a ruleset is in a |
157 * declaration group (implies nested selector) then generate new ruleset(s) at | 63 * declaration group (implies nested selector) then generate new ruleset(s) at |
158 * level 0 of CSS using selector inheritance syntax (flattens the nesting). | 64 * level 0 of CSS using selector inheritance syntax (flattens the nesting). |
159 * | 65 * |
160 * How the AST works for a rule [RuleSet] and nested rules. First of all a | 66 * How the AST works for a rule [RuleSet] and nested rules. First of all a |
161 * CSS rule [RuleSet] consist of a selector and a declaration e.g., | 67 * CSS rule [RuleSet] consist of a selector and a declaration e.g., |
162 * | 68 * |
163 * selector { | 69 * selector { |
164 * declaration | 70 * declaration |
(...skipping 286 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
451 super.visitDeclaration(node); | 357 super.visitDeclaration(node); |
452 } | 358 } |
453 | 359 |
454 void visitVarDefinition(VarDefinition node) { | 360 void visitVarDefinition(VarDefinition node) { |
455 if (_parentRuleSet != null) { | 361 if (_parentRuleSet != null) { |
456 _flatDeclarationGroup.declarations.add(node); | 362 _flatDeclarationGroup.declarations.add(node); |
457 } | 363 } |
458 super.visitVarDefinition(node); | 364 super.visitVarDefinition(node); |
459 } | 365 } |
460 | 366 |
| 367 void visitExtendDeclaration(ExtendDeclaration node) { |
| 368 if (_parentRuleSet != null) { |
| 369 _flatDeclarationGroup.declarations.add(node); |
| 370 } |
| 371 super.visitExtendDeclaration(node); |
| 372 } |
| 373 |
461 void visitMarginGroup(MarginGroup node) { | 374 void visitMarginGroup(MarginGroup node) { |
462 if (_parentRuleSet != null) { | 375 if (_parentRuleSet != null) { |
463 _flatDeclarationGroup.declarations.add(node); | 376 _flatDeclarationGroup.declarations.add(node); |
464 } | 377 } |
465 super.visitMarginGroup(node); | 378 super.visitMarginGroup(node); |
466 } | 379 } |
467 | 380 |
468 /** | 381 /** |
469 * Replace the rule set that contains nested rules with the flatten rule sets. | 382 * Replace the rule set that contains nested rules with the flatten rule sets. |
470 */ | 383 */ |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
504 _MediaRulesReplacer(this._ruleSet, this._newRules); | 417 _MediaRulesReplacer(this._ruleSet, this._newRules); |
505 | 418 |
506 visitMediaDirective(MediaDirective node) { | 419 visitMediaDirective(MediaDirective node) { |
507 var index = node.rulesets.indexOf(_ruleSet); | 420 var index = node.rulesets.indexOf(_ruleSet); |
508 if (index != -1) { | 421 if (index != -1) { |
509 node.rulesets.insertAll(index + 1, _newRules); | 422 node.rulesets.insertAll(index + 1, _newRules); |
510 _foundAndReplaced = true; | 423 _foundAndReplaced = true; |
511 } | 424 } |
512 } | 425 } |
513 } | 426 } |
| 427 |
| 428 /** |
| 429 * Expand all @include at the top-level the ruleset(s) associated with the |
| 430 * mixin. |
| 431 */ |
| 432 class TopLevelIncludes extends Visitor { |
| 433 StyleSheet _styleSheet; |
| 434 final Messages _messages; |
| 435 /** Map of variable name key to it's definition. */ |
| 436 final Map<String, MixinDefinition> map = new Map<String, MixinDefinition>(); |
| 437 MixinDefinition currDef; |
| 438 |
| 439 static void expand(Messages messages, List<StyleSheet> styleSheets) { |
| 440 new TopLevelIncludes(messages, styleSheets); |
| 441 } |
| 442 |
| 443 bool _anyRulesets(MixinRulesetDirective def) => |
| 444 def.rulesets.any((rule) => rule is RuleSet); |
| 445 |
| 446 TopLevelIncludes(this._messages, List<StyleSheet> styleSheets) { |
| 447 for (var styleSheet in styleSheets) { |
| 448 visitTree(styleSheet); |
| 449 } |
| 450 } |
| 451 |
| 452 void visitStyleSheet(StyleSheet ss) { |
| 453 _styleSheet = ss; |
| 454 super.visitStyleSheet(ss); |
| 455 _styleSheet = null; |
| 456 } |
| 457 |
| 458 void visitIncludeDirective(IncludeDirective node) { |
| 459 if (map.containsKey(node.name)) { |
| 460 var mixinDef = map[node.name]; |
| 461 if (mixinDef is MixinRulesetDirective) { |
| 462 _TopLevelIncludeReplacer.replace(_messages, _styleSheet, node, |
| 463 mixinDef.rulesets); |
| 464 } else if (currDef is MixinRulesetDirective && _anyRulesets(currDef)) { |
| 465 // currDef is MixinRulesetDirective |
| 466 MixinRulesetDirective mixinRuleset = currDef; |
| 467 int index = mixinRuleset.rulesets.indexOf(node as dynamic); |
| 468 mixinRuleset.rulesets.replaceRange(index, index + 1, [new NoOp()]); |
| 469 _messages.warning( |
| 470 'Using declaration mixin ${node.name} as top-level mixin', |
| 471 node.span); |
| 472 } |
| 473 } else { |
| 474 if (currDef is MixinRulesetDirective) { |
| 475 MixinRulesetDirective rulesetDirect = currDef as MixinRulesetDirective; |
| 476 var index = 0; |
| 477 rulesetDirect.rulesets.forEach((entry) { |
| 478 if (entry == node) { |
| 479 rulesetDirect.rulesets.replaceRange(index, index + 1, [new NoOp()]); |
| 480 _messages.warning('Undefined mixin ${node.name}', node.span); |
| 481 } |
| 482 index++; |
| 483 }); |
| 484 } |
| 485 } |
| 486 super.visitIncludeDirective(node); |
| 487 } |
| 488 |
| 489 void visitMixinRulesetDirective(MixinRulesetDirective node) { |
| 490 currDef = node; |
| 491 |
| 492 super.visitMixinRulesetDirective(node); |
| 493 |
| 494 // Replace with latest top-level mixin definition. |
| 495 map[node.name] = node; |
| 496 currDef = null; |
| 497 } |
| 498 |
| 499 void visitMixinDeclarationDirective(MixinDeclarationDirective node) { |
| 500 currDef = node; |
| 501 |
| 502 super.visitMixinDeclarationDirective(node); |
| 503 |
| 504 // Replace with latest mixin definition. |
| 505 map[node.name] = node; |
| 506 currDef = null; |
| 507 } |
| 508 } |
| 509 |
| 510 /** @include as a top-level with ruleset(s). */ |
| 511 class _TopLevelIncludeReplacer extends Visitor { |
| 512 final Messages _messages; |
| 513 final IncludeDirective _include; |
| 514 final List<RuleSet> _newRules; |
| 515 bool _foundAndReplaced = false; |
| 516 |
| 517 /** |
| 518 * Look for the [ruleSet] inside of an @media directive; if found then replace |
| 519 * with the [newRules]. If [ruleSet] is found and replaced return true. |
| 520 */ |
| 521 static bool replace(Messages messages, StyleSheet styleSheet, |
| 522 IncludeDirective include, List<RuleSet>newRules) { |
| 523 var visitor = new _TopLevelIncludeReplacer(messages, include, newRules); |
| 524 visitor.visitStyleSheet(styleSheet); |
| 525 return visitor._foundAndReplaced; |
| 526 } |
| 527 |
| 528 _TopLevelIncludeReplacer(this._messages, this._include, this._newRules); |
| 529 |
| 530 visitStyleSheet(StyleSheet node) { |
| 531 var index = node.topLevels.indexOf(_include); |
| 532 if (index != -1) { |
| 533 node.topLevels.insertAll(index + 1, _newRules); |
| 534 node.topLevels.replaceRange(index, index + 1, [new NoOp()]); |
| 535 _foundAndReplaced = true; |
| 536 } |
| 537 super.visitStyleSheet(node); |
| 538 } |
| 539 |
| 540 void visitMixinRulesetDirective(MixinRulesetDirective node) { |
| 541 var index = node.rulesets.indexOf(_include as dynamic); |
| 542 if (index != -1) { |
| 543 node.rulesets.insertAll(index + 1, _newRules); |
| 544 // Only the resolve the @include once. |
| 545 node.rulesets.replaceRange(index, index + 1, [new NoOp()]); |
| 546 _foundAndReplaced = true; |
| 547 } |
| 548 super.visitMixinRulesetDirective(node); |
| 549 } |
| 550 } |
| 551 |
| 552 /** |
| 553 * Utility function to match an include to a list of either Declarations or |
| 554 * RuleSets, depending on type of mixin (ruleset or declaration). The include |
| 555 * can be an include in a declaration or an include directive (top-level). |
| 556 */ |
| 557 int _findInclude(List list, var node) { |
| 558 IncludeDirective matchNode = (node is IncludeMixinAtDeclaration) ? |
| 559 node.include : node; |
| 560 |
| 561 var index = 0; |
| 562 for (var item in list) { |
| 563 var includeNode = (item is IncludeMixinAtDeclaration) ? |
| 564 item.include : item; |
| 565 if (includeNode == matchNode) return index; |
| 566 index++; |
| 567 } |
| 568 return -1; |
| 569 } |
| 570 |
| 571 /** |
| 572 * Stamp out a mixin with the defined args substituted with the user's |
| 573 * parameters. |
| 574 */ |
| 575 class CallMixin extends Visitor { |
| 576 var mixinDef; |
| 577 List _definedArgs; |
| 578 Expressions _currExpressions; |
| 579 int _currIndex = -1; |
| 580 |
| 581 final varUsages = new Map<String, Map<Expressions, Set<int>>>(); |
| 582 |
| 583 /** Only var defs with more than one expression (comma separated). */ |
| 584 final Map<String, VarDefinition> varDefs; |
| 585 |
| 586 CallMixin(this.mixinDef, [this.varDefs]) { |
| 587 if (mixinDef is MixinRulesetDirective) { |
| 588 visitMixinRulesetDirective(mixinDef); |
| 589 } else { |
| 590 visitMixinDeclarationDirective(mixinDef); |
| 591 } |
| 592 } |
| 593 |
| 594 /** |
| 595 * Given a mixin's defined arguments return a cloned mixin defintion that has |
| 596 * replaced all defined arguments with user's supplied VarUsages. |
| 597 */ |
| 598 transform(List<TreeNode> callArgs) { |
| 599 // TODO(terry): Handle default arguments and varArgs. |
| 600 // Transform mixin with callArgs. |
| 601 var index = 0; |
| 602 for (var index = 0; index < _definedArgs.length; index++) { |
| 603 var definedArg = _definedArgs[index]; |
| 604 VarDefinition varDef; |
| 605 if (definedArg is VarDefinition) { |
| 606 varDef = definedArg; |
| 607 } else if (definedArg is VarDefinitionDirective) { |
| 608 VarDefinitionDirective varDirective = definedArg; |
| 609 varDef = varDirective.def; |
| 610 } |
| 611 var callArg = callArgs[index]; |
| 612 |
| 613 // Is callArg a var definition with multi-args (expressions > 1). |
| 614 var defArgs = _varDefsAsCallArgs(callArg); |
| 615 if (defArgs.isNotEmpty) { |
| 616 // Replace call args with the var def parameters. |
| 617 callArgs.insertAll(index, defArgs); |
| 618 callArgs.removeAt(index + defArgs.length); |
| 619 callArg = callArgs[index]; |
| 620 } |
| 621 |
| 622 var expressions = varUsages[varDef.definedName]; |
| 623 var expressionsLength = expressions.length; |
| 624 expressions.forEach((k, v) { |
| 625 for (var usagesIndex in v) { |
| 626 k.expressions.replaceRange(usagesIndex, usagesIndex + 1, callArg); |
| 627 } |
| 628 }); |
| 629 } |
| 630 |
| 631 // Clone the mixin |
| 632 return mixinDef.clone(); |
| 633 } |
| 634 |
| 635 /** Rip apart var def with multiple parameters. */ |
| 636 List<List<TreeNode>> _varDefsAsCallArgs(var callArg) { |
| 637 var defArgs = []; |
| 638 if (callArg is List && callArg[0] is VarUsage) { |
| 639 var varDef = varDefs[callArg[0].name]; |
| 640 var expressions = varDef.expression.expressions; |
| 641 assert(expressions.length > 1); |
| 642 for (var expr in expressions) { |
| 643 if (expr is! OperatorComma) { |
| 644 defArgs.add([expr]); |
| 645 } |
| 646 } |
| 647 } |
| 648 return defArgs; |
| 649 } |
| 650 |
| 651 void visitExpressions(Expressions node) { |
| 652 var oldExpressions = _currExpressions; |
| 653 var oldIndex = _currIndex; |
| 654 |
| 655 _currExpressions = node; |
| 656 for (_currIndex = 0; _currIndex < node.expressions.length; _currIndex++) { |
| 657 node.expressions[_currIndex].visit(this); |
| 658 } |
| 659 |
| 660 _currIndex = oldIndex; |
| 661 _currExpressions = oldExpressions; |
| 662 } |
| 663 |
| 664 void _addExpression(Map<Expressions, Set<int>> expressions) { |
| 665 var indexSet = new Set<int>(); |
| 666 indexSet.add(_currIndex); |
| 667 expressions[_currExpressions] = indexSet; |
| 668 } |
| 669 |
| 670 void visitVarUsage(VarUsage node) { |
| 671 assert(_currIndex != -1); |
| 672 assert(_currExpressions != null); |
| 673 if (varUsages.containsKey(node.name)) { |
| 674 Map<Expressions, Set<int>> expressions = varUsages[node.name]; |
| 675 Set<int> allIndexes = expressions[_currExpressions]; |
| 676 if (allIndexes == null) { |
| 677 _addExpression(expressions); |
| 678 } else { |
| 679 allIndexes.add(_currIndex); |
| 680 } |
| 681 } else { |
| 682 var newExpressions = new Map<Expressions, Set<int>>(); |
| 683 _addExpression(newExpressions); |
| 684 varUsages[node.name] = newExpressions; |
| 685 } |
| 686 super.visitVarUsage(node); |
| 687 } |
| 688 |
| 689 void visitMixinDeclarationDirective(MixinDeclarationDirective node) { |
| 690 _definedArgs = node.definedArgs; |
| 691 super.visitMixinDeclarationDirective(node); |
| 692 } |
| 693 |
| 694 void visitMixinRulesetDirective(MixinRulesetDirective node) { |
| 695 _definedArgs = node.definedArgs; |
| 696 super.visitMixinRulesetDirective(node); |
| 697 } |
| 698 } |
| 699 |
| 700 /** Expand all @include inside of a declaration associated with a mixin. */ |
| 701 class DeclarationIncludes extends Visitor { |
| 702 StyleSheet _styleSheet; |
| 703 final Messages _messages; |
| 704 /** Map of variable name key to it's definition. */ |
| 705 final Map<String, MixinDefinition> map = new Map<String, MixinDefinition>(); |
| 706 /** Cache of mixin called with parameters. */ |
| 707 final Map<String, CallMixin> callMap = new Map<String, CallMixin>(); |
| 708 MixinDefinition currDef; |
| 709 DeclarationGroup currDeclGroup; |
| 710 |
| 711 /** Var definitions with more than 1 expression. */ |
| 712 final Map<String, VarDefinition> varDefs = new Map<String, VarDefinition>(); |
| 713 |
| 714 static void expand(Messages messages, List<StyleSheet> styleSheets) { |
| 715 new DeclarationIncludes(messages, styleSheets); |
| 716 } |
| 717 |
| 718 DeclarationIncludes(this._messages, List<StyleSheet> styleSheets) { |
| 719 for (var styleSheet in styleSheets) { |
| 720 visitTree(styleSheet); |
| 721 } |
| 722 } |
| 723 |
| 724 bool _allIncludes(rulesets) => |
| 725 rulesets.every((rule) => rule is IncludeDirective || rule is NoOp); |
| 726 |
| 727 CallMixin _createCallDeclMixin(mixinDef) { |
| 728 callMap.putIfAbsent(mixinDef.name, () => |
| 729 callMap[mixinDef.name] = new CallMixin(mixinDef, varDefs)); |
| 730 return callMap[mixinDef.name]; |
| 731 } |
| 732 |
| 733 void visitStyleSheet(StyleSheet ss) { |
| 734 _styleSheet = ss; |
| 735 super.visitStyleSheet(ss); |
| 736 _styleSheet = null; |
| 737 } |
| 738 |
| 739 void visitDeclarationGroup(DeclarationGroup node) { |
| 740 currDeclGroup = node; |
| 741 super.visitDeclarationGroup(node); |
| 742 currDeclGroup = null; |
| 743 } |
| 744 |
| 745 void visitIncludeMixinAtDeclaration(IncludeMixinAtDeclaration node) { |
| 746 if (map.containsKey(node.include.name)) { |
| 747 var mixinDef = map[node.include.name]; |
| 748 |
| 749 // Fix up any mixin that is really a Declaration but has includes. |
| 750 if (mixinDef is MixinRulesetDirective) { |
| 751 if (!_allIncludes(mixinDef.rulesets) && currDeclGroup != null) { |
| 752 var index = _findInclude(currDeclGroup.declarations, node); |
| 753 if (index != -1) { |
| 754 currDeclGroup.declarations.replaceRange(index, index + 1, |
| 755 [new NoOp()]); |
| 756 } |
| 757 _messages.warning( |
| 758 "Using top-level mixin ${node.include.name} as a declaration", |
| 759 node.span); |
| 760 } else { |
| 761 // We're a list of @include(s) inside of a mixin ruleset - convert |
| 762 // to a list of IncludeMixinAtDeclaration(s). |
| 763 var origRulesets = mixinDef.rulesets; |
| 764 var rulesets = []; |
| 765 if (origRulesets.every((ruleset) => ruleset is IncludeDirective)) { |
| 766 origRulesets.forEach((ruleset) { |
| 767 rulesets.add(new IncludeMixinAtDeclaration(ruleset, |
| 768 ruleset.span)); |
| 769 }); |
| 770 _IncludeReplacer.replace(_styleSheet, node, rulesets); |
| 771 } |
| 772 } |
| 773 } |
| 774 |
| 775 if ( mixinDef.definedArgs.length > 0 && node.include.args.length > 0) { |
| 776 var callMixin = _createCallDeclMixin(mixinDef); |
| 777 mixinDef = callMixin.transform(node.include.args); |
| 778 } |
| 779 |
| 780 if (mixinDef is MixinDeclarationDirective) { |
| 781 _IncludeReplacer.replace(_styleSheet, node, |
| 782 mixinDef.declarations.declarations); |
| 783 } |
| 784 } else { |
| 785 _messages.warning("Undefined mixin ${node.include.name}", node.span); |
| 786 } |
| 787 |
| 788 super.visitIncludeMixinAtDeclaration(node); |
| 789 } |
| 790 |
| 791 void visitIncludeDirective(IncludeDirective node) { |
| 792 if (map.containsKey(node.name)) { |
| 793 var mixinDef = map[node.name]; |
| 794 if (currDef is MixinDeclarationDirective && |
| 795 mixinDef is MixinDeclarationDirective) { |
| 796 _IncludeReplacer.replace(_styleSheet, node, |
| 797 mixinDef.declarations.declarations); |
| 798 } else if (currDef is MixinDeclarationDirective) { |
| 799 var decls = (currDef as MixinDeclarationDirective) |
| 800 .declarations.declarations; |
| 801 var index = _findInclude(decls, node); |
| 802 if (index != -1) { |
| 803 decls.replaceRange(index, index + 1, [new NoOp()]); |
| 804 } |
| 805 } |
| 806 } |
| 807 |
| 808 super.visitIncludeDirective(node); |
| 809 } |
| 810 |
| 811 void visitMixinRulesetDirective(MixinRulesetDirective node) { |
| 812 currDef = node; |
| 813 |
| 814 super.visitMixinRulesetDirective(node); |
| 815 |
| 816 // Replace with latest top-level mixin definition. |
| 817 map[node.name] = node; |
| 818 currDef = null; |
| 819 } |
| 820 |
| 821 void visitMixinDeclarationDirective(MixinDeclarationDirective node) { |
| 822 currDef = node; |
| 823 |
| 824 super.visitMixinDeclarationDirective(node); |
| 825 |
| 826 // Replace with latest mixin definition. |
| 827 map[node.name] = node; |
| 828 currDef = null; |
| 829 } |
| 830 |
| 831 void visitVarDefinition(VarDefinition node) { |
| 832 // Only record var definitions that have multiple expressions (comma |
| 833 // separated for mixin parameter substitution. |
| 834 var exprs = (node.expression as Expressions).expressions; |
| 835 if (exprs.length > 1) { |
| 836 varDefs[node.definedName] = node; |
| 837 } |
| 838 super.visitVarDefinition(node); |
| 839 } |
| 840 |
| 841 void visitVarDefinitionDirective(VarDefinitionDirective node) { |
| 842 visitVarDefinition(node.def); |
| 843 } |
| 844 } |
| 845 |
| 846 /** @include as a top-level with ruleset(s). */ |
| 847 class _IncludeReplacer extends Visitor { |
| 848 final _include; |
| 849 final List<Declaration> _newDeclarations; |
| 850 bool _foundAndReplaced = false; |
| 851 |
| 852 /** |
| 853 * Look for the [ruleSet] inside of a @media directive; if found then replace |
| 854 * with the [newRules]. |
| 855 */ |
| 856 static void replace(StyleSheet ss, var include, |
| 857 List<Declaration> newDeclarations) { |
| 858 var visitor = new _IncludeReplacer(include, newDeclarations); |
| 859 visitor.visitStyleSheet(ss); |
| 860 } |
| 861 |
| 862 _IncludeReplacer(this._include, this._newDeclarations); |
| 863 |
| 864 void visitDeclarationGroup(DeclarationGroup node) { |
| 865 var index = _findInclude(node.declarations, _include); |
| 866 if (index != -1) { |
| 867 node.declarations.insertAll(index + 1, _newDeclarations); |
| 868 // Change @include to NoOp so it's processed only once. |
| 869 node.declarations.replaceRange(index, index + 1, [new NoOp()]); |
| 870 _foundAndReplaced = true; |
| 871 } |
| 872 super.visitDeclarationGroup(node); |
| 873 } |
| 874 } |
| 875 |
| 876 /** |
| 877 * Remove all @mixin and @include and any NoOp used as placeholder for @include. |
| 878 */ |
| 879 class MixinsAndIncludes extends Visitor { |
| 880 static void remove(StyleSheet styleSheet) { |
| 881 new MixinsAndIncludes()..visitStyleSheet(styleSheet); |
| 882 } |
| 883 |
| 884 bool _nodesToRemove(node) => |
| 885 node is IncludeDirective || node is MixinDefinition || node is NoOp; |
| 886 |
| 887 void visitStyleSheet(StyleSheet ss) { |
| 888 var index = ss.topLevels.length; |
| 889 while (--index >= 0) { |
| 890 if (_nodesToRemove(ss.topLevels[index])) { |
| 891 ss.topLevels.removeAt(index); |
| 892 } |
| 893 } |
| 894 super.visitStyleSheet(ss); |
| 895 } |
| 896 |
| 897 void visitDeclarationGroup(DeclarationGroup node) { |
| 898 var index = node.declarations.length; |
| 899 while (--index >= 0) { |
| 900 if (_nodesToRemove(node.declarations[index])) { |
| 901 node.declarations.removeAt(index); |
| 902 } |
| 903 } |
| 904 super.visitDeclarationGroup(node); |
| 905 } |
| 906 } |
| 907 |
| 908 /** Find all @extend to create inheritance. */ |
| 909 class AllExtends extends Visitor { |
| 910 final Map<String, List<SelectorGroup>> inherits = |
| 911 new Map<String, List<SelectorGroup>>(); |
| 912 |
| 913 SelectorGroup _currSelectorGroup; |
| 914 List _currDecls; |
| 915 int _currDeclIndex; |
| 916 List<int> _extendsToRemove = []; |
| 917 |
| 918 void visitRuleSet(RuleSet node) { |
| 919 var oldSelectorGroup = _currSelectorGroup; |
| 920 _currSelectorGroup = node.selectorGroup; |
| 921 |
| 922 super.visitRuleSet(node); |
| 923 |
| 924 _currSelectorGroup = oldSelectorGroup; |
| 925 } |
| 926 |
| 927 void visitExtendDeclaration(ExtendDeclaration node) { |
| 928 var inheritName = ""; |
| 929 for (var selector in node.selectors) { |
| 930 inheritName += selector.toString(); |
| 931 } |
| 932 if (inherits.containsKey(inheritName)) { |
| 933 inherits[inheritName].add(_currSelectorGroup); |
| 934 } else { |
| 935 inherits[inheritName] = [_currSelectorGroup]; |
| 936 } |
| 937 |
| 938 // Remove this @extend |
| 939 _extendsToRemove.add(_currDeclIndex); |
| 940 |
| 941 super.visitExtendDeclaration(node); |
| 942 } |
| 943 |
| 944 void visitDeclarationGroup(DeclarationGroup node) { |
| 945 var oldDeclIndex = _currDeclIndex; |
| 946 |
| 947 var decls = node.declarations; |
| 948 for (_currDeclIndex = 0; _currDeclIndex < decls.length; _currDeclIndex++) { |
| 949 decls[_currDeclIndex].visit(this); |
| 950 } |
| 951 |
| 952 if (_extendsToRemove.isNotEmpty) { |
| 953 var removeTotal = _extendsToRemove.length - 1; |
| 954 for (var index = removeTotal; index >= 0; index--) { |
| 955 decls.removeAt(_extendsToRemove[index]); |
| 956 } |
| 957 _extendsToRemove.clear(); |
| 958 } |
| 959 |
| 960 _currDeclIndex = oldDeclIndex; |
| 961 } |
| 962 } |
| 963 |
| 964 // TODO(terry): Need to handle merging selector sequences |
| 965 // TODO(terry): Need to handle @extend-Only selectors. |
| 966 // TODO(terry): Need to handle !optional glag. |
| 967 /** |
| 968 * Changes any selector that matches @extend. |
| 969 */ |
| 970 class InheritExtends extends Visitor { |
| 971 Messages _messages; |
| 972 AllExtends _allExtends; |
| 973 |
| 974 InheritExtends(this._messages, this._allExtends); |
| 975 |
| 976 void visitSelectorGroup(SelectorGroup node) { |
| 977 for (var selectorsIndex = 0; selectorsIndex < node.selectors.length; |
| 978 selectorsIndex++) { |
| 979 var selectors = node.selectors[selectorsIndex]; |
| 980 var isLastNone = false; |
| 981 var selectorName = ""; |
| 982 for (var index = 0; index < selectors.simpleSelectorSequences.length; |
| 983 index++) { |
| 984 var simpleSeq = selectors.simpleSelectorSequences[index]; |
| 985 var namePart = simpleSeq.simpleSelector.toString(); |
| 986 selectorName = (isLastNone) ? (selectorName + namePart) : namePart; |
| 987 List<SelectorGroup> matches = _allExtends.inherits[selectorName]; |
| 988 if (matches != null) { |
| 989 for (var match in matches) { |
| 990 // Create a new group. |
| 991 var newSelectors = selectors.clone(); |
| 992 var newSeq = match.selectors[0].clone(); |
| 993 if (isLastNone) { |
| 994 // Add the inherited selector. |
| 995 node.selectors.add(newSeq); |
| 996 } else { |
| 997 // Replace the selector sequence to the left of the pseudo class |
| 998 // or pseudo element. |
| 999 |
| 1000 // Make new selector seq combinator the same as the original. |
| 1001 var orgCombinator = |
| 1002 newSelectors.simpleSelectorSequences[index].combinator; |
| 1003 newSeq.simpleSelectorSequences[0].combinator = orgCombinator; |
| 1004 |
| 1005 newSelectors.simpleSelectorSequences.replaceRange(index, |
| 1006 index + 1, newSeq.simpleSelectorSequences); |
| 1007 node.selectors.add(newSelectors); |
| 1008 } |
| 1009 isLastNone = false; |
| 1010 } |
| 1011 } else { |
| 1012 isLastNone = simpleSeq.isCombinatorNone; |
| 1013 } |
| 1014 } |
| 1015 } |
| 1016 super.visitSelectorGroup(node); |
| 1017 } |
| 1018 } |
OLD | NEW |