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): Detect invalid directive usage. All @imports must occur before | 8 // TODO(terry): Detect invalid directive usage. All @imports must occur before |
9 // all rules other than @charset directive. Any @import directive | 9 // all rules other than @charset directive. Any @import directive |
10 // after any non @charset or @import directive are ignored. e.g., | 10 // after any non @charset or @import directive are ignored. e.g., |
11 // @import "a.css"; | 11 // @import "a.css"; |
12 // div { color: red; } | 12 // div { color: red; } |
13 // @import "b.css"; | 13 // @import "b.css"; |
14 // becomes: | 14 // becomes: |
15 // @import "a.css"; | 15 // @import "a.css"; |
16 // div { color: red; } | 16 // div { color: red; } |
17 // <http://www.w3.org/TR/css3-syntax/#at-rules> | 17 // <http://www.w3.org/TR/css3-syntax/#at-rules> |
18 | 18 |
19 /** | 19 /** |
20 * Analysis phase will validate/fixup any new CSS feature or any SASS style | 20 * Analysis phase will validate/fixup any new CSS feature or any SASS style |
21 * feature. | 21 * feature. |
22 */ | 22 */ |
23 class Analyzer { | 23 class Analyzer { |
24 final List<StyleSheet> _styleSheets; | 24 final List<StyleSheet> _styleSheets; |
25 final Messages _messages; | 25 final Messages _messages; |
26 MixinDefinitions mixinDefs; | |
26 VarDefinitions varDefs; | 27 VarDefinitions varDefs; |
27 | 28 |
28 Analyzer(this._styleSheets, this._messages); | 29 Analyzer(this._styleSheets, this._messages); |
29 | 30 |
30 void run() { | 31 void run() { |
32 mixinDefs = new MixinDefinitions(_styleSheets); | |
nweiz
2013/09/18 22:40:54
It's not correct to locate all mixin definitions b
terry
2013/10/09 03:40:33
Yeah, I liked the idea of hoisting like a class bu
| |
33 | |
34 // Any mixin cycles? | |
35 var mixinCycles = findAllMixinCycles(); | |
36 for (var cycle in mixinCycles) { | |
37 _messages.warning("mixin cycle detected @mixin ${cycle.def.name}", | |
nweiz
2013/09/18 22:40:54
"Mixin cycle detected for @mixin ${cycle.def.name}
terry
2013/10/09 03:40:33
Without hoisting cycle detection is removed.
On 20
| |
38 cycle.def.span); | |
39 // TODO(terry): What if no mixin definition for a @include an error? | |
nweiz
2013/09/18 22:40:54
"What if there are no mixin definitions for an @in
| |
40 } | |
41 | |
31 varDefs = new VarDefinitions(_styleSheets); | 42 varDefs = new VarDefinitions(_styleSheets); |
32 | 43 |
33 // Any cycles? | 44 // Any var cycles? |
34 var cycles = findAllCycles(); | 45 var varCycles = findAllVarCycles(); |
nweiz
2013/09/18 22:40:54
Variables should be eagerly evaluated, so cycle de
terry
2013/10/09 03:40:33
Yes also removed variable hoisting.
On 2013/09/18
| |
35 for (var cycle in cycles) { | 46 for (var cycle in varCycles) { |
36 _messages.warning("var cycle detected var-${cycle.definedName}", | 47 _messages.warning("var cycle detected var-${cycle.definedName}", |
37 cycle.span); | 48 cycle.span); |
38 // TODO(terry): What if no var definition for a var usage an error? | 49 // 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 | 50 // TODO(terry): Ensure a var definition imported from a different style |
40 // sheet works. | 51 // sheet works. |
41 } | 52 } |
42 | 53 |
43 // Remove any var definition from the stylesheet that has a cycle. | 54 // Remove any var definition from the stylesheet that has a cycle. |
44 _styleSheets.forEach((styleSheet) => | 55 _styleSheets.forEach((styleSheet) => |
45 new RemoveVarDefinitions(cycles).visitStyleSheet(styleSheet)); | 56 new RemoveVarDefinitions(varCycles).visitStyleSheet(styleSheet)); |
57 | |
58 // Expand top-level @include. | |
59 _styleSheets.forEach((styleSheet) => | |
60 new ExpandTopLevelIncludes(mixinDefs, _messages) | |
61 ..visitStyleSheet(styleSheet)); | |
nweiz
2013/09/18 22:40:54
Running separate visitors for all of these seems i
terry
2013/10/09 03:40:33
Yes, for now that is the mode we've taken in the d
| |
62 | |
63 // Expand @include in declarations. | |
nweiz
2013/09/18 22:40:54
Nit: extra space before "@include".
terry
2013/10/09 03:40:33
Done.
| |
64 _styleSheets.forEach((styleSheet) => | |
65 new ExpandDeclarationIncludes(styleSheet, mixinDefs, _messages) | |
66 ..visitStyleSheet(styleSheet)); | |
67 | |
68 // Any missing @mixins only check if everything is good up to this point. | |
nweiz
2013/09/18 22:40:54
I don't understand what this comment means.
terry
2013/10/09 03:40:33
Functionality removed when hoisting of mixin remov
| |
69 if (_messages.messages.isEmpty) { | |
70 _styleSheets.forEach((styleSheet) => | |
71 new IncludesMissingMixins(mixinDefs, this._messages) | |
72 ..visitStyleSheet(styleSheet)); | |
73 } | |
74 | |
75 // Remove all @mixin and @include | |
76 _styleSheets.forEach((styleSheet) => | |
77 new RemoveMixinsAndIncludes()..visitStyleSheet(styleSheet)); | |
nweiz
2013/09/18 22:40:54
Style nit: indent +2 spaces.
terry
2013/10/09 03:40:33
Code removed.
On 2013/09/18 22:40:54, nweiz wrote:
| |
46 | 78 |
47 // Expand any nested selectors using selector desendant combinator to | 79 // Expand any nested selectors using selector desendant combinator to |
48 // signal CSS inheritance notation. | 80 // signal CSS inheritance notation. |
49 _styleSheets.forEach((styleSheet) => new ExpandNestedSelectors() | 81 _styleSheets.forEach((styleSheet) => new ExpandNestedSelectors() |
50 ..visitStyleSheet(styleSheet) | 82 ..visitStyleSheet(styleSheet) |
51 ..flatten(styleSheet)); | 83 ..flatten(styleSheet)); |
52 } | 84 } |
53 | 85 |
54 List<VarDefinition> findAllCycles() { | 86 List<MixinDefinition> findAllMixinCycles() { |
87 var cycles = []; | |
88 | |
89 mixinDefs.map.values.forEach((value) { | |
nweiz
2013/09/18 22:40:54
As John's mentioned elsewhere, use a for loop rath
terry
2013/10/09 03:40:33
Cycle code removed.
On 2013/09/18 22:40:54, nweiz
| |
90 if (hasMixinCycle(value.includes)) cycles.add(value); | |
91 }); | |
92 | |
93 // Update our local list of known mixinDefinitions remove any mixins with a | |
94 // cycle. So the same mixin cycle isn't reported for each style sheet | |
95 // processed. | |
96 for (var cycle in cycles) { | |
97 mixinDefs.map.remove(cycle.def.name); | |
98 } | |
99 | |
100 return cycles; | |
101 } | |
102 | |
103 /** | |
104 * [includes] list of IncludeDirective or IncludeMixinAtDeclaration. | |
nweiz
2013/09/18 22:40:54
If you're going to include a doc comment, it shoul
terry
2013/10/09 03:40:33
Cycle code removed.
On 2013/09/18 22:40:54, nweiz
| |
105 */ | |
106 bool hasMixinCycle(List includes, {Set<String> visiting, | |
107 Set<String> visited}) { | |
108 if (visiting == null) visiting = new Set(); | |
109 if (visited == null) visited = new Set(); | |
nweiz
2013/09/18 22:40:54
Explain the function of [visited] and how it diffe
terry
2013/10/09 03:40:33
cycle code removed.
On 2013/09/18 22:40:54, nweiz
| |
110 | |
111 bool cycleDetected = false; | |
112 | |
113 var name; | |
114 for (var include in includes) { | |
115 if (include is IncludeMixinAtDeclaration) { | |
116 // Get to the IncludeDirective. | |
117 name = include.include.name; | |
118 } else { | |
119 name = include.name; | |
120 } | |
121 | |
122 if (visiting.contains(name)) { | |
123 cycleDetected = true; | |
124 break; | |
125 } | |
126 if (!visited.contains(name)) { | |
127 visiting.add(name); | |
128 visited.add(name); | |
129 } | |
130 if (mixinDefs.map[name] != null) { | |
131 if (hasMixinCycle(mixinDefs.map[name].includes, visiting: visiting, | |
132 visited: visited)) { | |
133 cycleDetected = true; | |
134 break; | |
135 } | |
136 } | |
137 | |
138 | |
139 | |
140 /* | |
Jennifer Messerly
2013/09/18 06:44:15
remove?
terry
2013/10/09 03:40:33
entire function removed.
On 2013/09/18 06:44:15, J
| |
141 for (var usage in mixinInclude(mixinDefs.map[name].includes)) { | |
142 if (hasMixinCycle(usage.name, visiting: visiting, visited: visited)) { | |
143 cycleDetected = true; | |
144 break; | |
145 } | |
146 } | |
147 */ | |
148 visiting.remove(include); | |
149 } | |
150 | |
151 return cycleDetected; | |
152 } | |
153 | |
154 // TODO(terry): Only checks @includes in declarations not ruleset top-level. | |
155 Iterable<IncludeDirective> mixinInclude(List includes) => | |
156 includes.where((e) => e is IncludeMixinAtDeclaration); | |
157 | |
158 // ===================== | |
159 // Var cycles functions: | |
160 // ===================== | |
161 | |
162 List<VarDefinition> findAllVarCycles() { | |
55 var cycles = []; | 163 var cycles = []; |
56 | 164 |
57 varDefs.map.values.forEach((value) { | 165 varDefs.map.values.forEach((value) { |
58 if (hasCycle(value.property)) cycles.add(value); | 166 if (hasCycle(value.property)) cycles.add(value); |
59 }); | 167 }); |
60 | 168 |
61 // Update our local list of known varDefs remove any varDefs with a cycle. | 169 // 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. | 170 // So the same varDef cycle isn't reported for each style sheet processed. |
63 for (var cycle in cycles) { | 171 for (var cycle in cycles) { |
64 varDefs.map.remove(cycle.property); | 172 varDefs.map.remove(cycle.property); |
(...skipping 439 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
504 _MediaRulesReplacer(this._ruleSet, this._newRules); | 612 _MediaRulesReplacer(this._ruleSet, this._newRules); |
505 | 613 |
506 visitMediaDirective(MediaDirective node) { | 614 visitMediaDirective(MediaDirective node) { |
507 var index = node.rulesets.indexOf(_ruleSet); | 615 var index = node.rulesets.indexOf(_ruleSet); |
508 if (index != -1) { | 616 if (index != -1) { |
509 node.rulesets.insertAll(index + 1, _newRules); | 617 node.rulesets.insertAll(index + 1, _newRules); |
510 _foundAndReplaced = true; | 618 _foundAndReplaced = true; |
511 } | 619 } |
512 } | 620 } |
513 } | 621 } |
622 | |
623 class MixinDef { | |
624 MixinDefinition def; | |
625 List<IncludeDirective> includes; | |
626 | |
627 MixinDef(this.def, this.includes); | |
628 } | |
629 | |
630 /** Find all mixin definitions from a list of stylesheets. */ | |
631 class MixinDefinitions extends Visitor { | |
632 /** Map of variable name key to it's definition. */ | |
633 final Map<String, MixinDef> map = new Map<String, MixinDef>(); | |
nweiz
2013/09/18 22:40:54
This type declaration is unnecessary. The type is
terry
2013/10/09 03:40:33
Yep but editor code completion didn't work w/o it
| |
634 | |
635 var _includes = []; | |
636 | |
637 MixinDefinitions(List<StyleSheet> styleSheets) { | |
638 for (var styleSheet in styleSheets) { | |
639 visitTree(styleSheet); | |
640 } | |
641 } | |
642 | |
643 void visitIncludeDirective(IncludeDirective node) { | |
644 _includes.add(node); | |
645 } | |
646 | |
647 void visitIncludeMixinAtDeclaration(IncludeMixinAtDeclaration node) { | |
648 _includes.add(node); | |
649 } | |
650 | |
651 void visitMixinRulesetDirective(MixinRulesetDirective node) { | |
652 _includes = []; | |
653 | |
654 super.visitMixinRulesetDirective(node); | |
655 | |
656 // Replace with latest top-level mixin definition. | |
657 map[node.name] = new MixinDef(node, _includes); | |
658 _includes = []; | |
659 } | |
660 | |
661 void visitMixinDeclarationDirective(MixinDeclarationDirective node) { | |
662 _includes = []; | |
663 | |
664 super.visitMixinDeclarationDirective(node); | |
665 | |
666 // Replace with latest mixin definition. | |
667 map[node.name] = new MixinDef(node, _includes); | |
668 _includes = []; | |
669 } | |
670 } | |
671 | |
672 /** | |
673 * Expand all @include at the top-level the ruleset(s) associated with the | |
674 * mixin. | |
675 */ | |
676 class ExpandTopLevelIncludes extends Visitor { | |
677 StyleSheet _styleSheet; | |
678 final MixinDefinitions defs; | |
679 final Messages _messages; | |
680 | |
681 ExpandTopLevelIncludes(this.defs, this._messages); | |
682 | |
683 void visitStyleSheet(StyleSheet ss) { | |
684 _styleSheet = ss; | |
685 super.visitStyleSheet(ss); | |
686 _styleSheet = null; | |
687 } | |
688 | |
689 void visitIncludeDirective(IncludeDirective node) { | |
690 if (defs.map.containsKey(node.name)) { | |
691 var mixinDef = defs.map[node.name].def; | |
692 if (mixinDef is MixinRulesetDirective) { | |
693 _TopLevelIncludeReplacer.replace(_styleSheet, node, mixinDef.rulesets); | |
694 } | |
695 } | |
696 super.visitIncludeDirective(node); | |
697 } | |
698 } | |
699 | |
700 /** @include as a top-level with ruleset(s). */ | |
701 class _TopLevelIncludeReplacer extends Visitor { | |
702 final IncludeDirective _include; | |
703 final List<RuleSet> _newRules; | |
704 bool _foundAndReplaced = false; | |
705 | |
706 /** | |
707 * Look for the [ruleSet] inside of an @media directive; if found then replace | |
708 * with the [newRules]. If [ruleSet] is found and replaced return true. | |
709 */ | |
710 static bool replace(StyleSheet styleSheet, IncludeDirective include, | |
nweiz
2013/09/18 22:40:54
You have three different ways of invoking a visito
terry
2013/10/09 03:40:33
Function was re-written this no longer exist.
On 2
| |
711 List<RuleSet>newRules) { | |
712 var visitor = new _TopLevelIncludeReplacer(include, newRules); | |
713 visitor.visitStyleSheet(styleSheet); | |
714 return visitor._foundAndReplaced; | |
nweiz
2013/09/18 22:40:54
This value is never used.
terry
2013/10/09 03:40:33
function re-written not an issue now.
On 2013/09/1
| |
715 } | |
716 | |
717 _TopLevelIncludeReplacer(this._include, this._newRules); | |
718 | |
719 visitStyleSheet(StyleSheet node) { | |
720 var index = node.topLevels.indexOf(_include); | |
721 if (index != -1) { | |
722 node.topLevels.insertAll(index + 1, _newRules); | |
723 node.topLevels.replaceRange(index, index + 1, [new NoOp()]); | |
724 _foundAndReplaced = true; | |
725 } | |
726 super.visitStyleSheet(node); | |
727 } | |
728 | |
729 void visitMixinRulesetDirective(MixinRulesetDirective node) { | |
730 var index = node.rulesets.indexOf(_include as dynamic); | |
nweiz
2013/09/18 22:40:54
What is "as dynamic" doing here?
terry
2013/10/09 03:40:33
_include can be different either declarative vs/ r
| |
731 if (index != -1) { | |
732 node.rulesets.insertAll(index + 1, _newRules); | |
733 node.rulesets.replaceRange(index, index + 1, [new NoOp()]); | |
nweiz
2013/09/18 22:40:54
Why aren't you replacing the include with _newRule
terry
2013/10/09 03:40:33
Only the resolve the @include once.
On 2013/09/18
| |
734 _foundAndReplaced = true; | |
735 } | |
736 super.visitMixinRulesetDirective(node); | |
737 } | |
738 } | |
739 | |
740 /** Expand all @include inside of a declaration associated with a mixin. */ | |
741 class ExpandDeclarationIncludes extends Visitor { | |
742 final StyleSheet _styleSheet; | |
743 final MixinDefinitions defs; | |
744 final Messages _messages; | |
745 | |
746 ExpandDeclarationIncludes(this._styleSheet, this.defs, this._messages); | |
747 | |
748 bool _allIncludes(rulesets) => | |
749 rulesets.every((rule) => rule is IncludeDirective || rule is NoOp); | |
750 | |
751 void visitIncludeMixinAtDeclaration(IncludeMixinAtDeclaration node) { | |
752 if (defs.map.containsKey(node.include.name)) { | |
nweiz
2013/09/18 22:40:54
Short-circuit if this is false to avoid extra nest
terry
2013/10/09 03:40:33
Really, seems clunky to call super for the short-c
| |
753 var mixinDef = defs.map[node.include.name].def; | |
754 if (mixinDef is MixinDeclarationDirective) { | |
755 _IncludeReplacer.replace(_styleSheet, node, | |
756 mixinDef.declarations.declarations); | |
757 } else if (mixinDef is MixinRulesetDirective) { | |
758 // We're a list of @include(s) inside of a mixin ruleset if so then | |
759 // we'll convert to a list of IncludeMixinAtDeclaration(s). | |
nweiz
2013/09/18 22:40:54
It doesn't make sense to say "We're X. If so, ..."
terry
2013/10/09 03:40:33
yep, changed.
On 2013/09/18 22:40:54, nweiz wrote:
| |
760 var orgRulesets = mixinDef.rulesets; | |
nweiz
2013/09/18 22:40:54
What does "org" mean?
terry
2013/10/09 03:40:33
original - missing 'i' origRulesets
On 2013/09/18
| |
761 var rulesets = []; | |
762 if (orgRulesets.every((ruleset) => ruleset is IncludeDirective)) { | |
763 orgRulesets.forEach((ruleset) { | |
764 rulesets.add(new IncludeMixinAtDeclaration(ruleset, ruleset.span)); | |
765 }); | |
766 _IncludeReplacer.replace(_styleSheet, node, rulesets); | |
767 } else { | |
768 _messages.warning( | |
769 "Trying to use top-level ruleset in a declaration - " | |
770 "@include ${node.include.name}", | |
771 node.span); | |
772 } | |
773 } | |
774 } | |
775 | |
776 super.visitIncludeMixinAtDeclaration(node); | |
777 } | |
778 } | |
779 | |
780 /** @include as a top-level with ruleset(s). */ | |
781 class _IncludeReplacer extends Visitor { | |
782 final _include; | |
783 final List<Declaration> _newDeclarations; | |
784 bool _foundAndReplaced = false; | |
785 | |
786 /** | |
787 * Look for the [ruleSet] inside of an @media directive; if found then replace | |
nweiz
2013/09/18 22:40:54
"a @media"
terry
2013/10/09 03:40:33
Done.
| |
788 * with the [newRules]. If [ruleSet] is found and replaced return true. | |
789 */ | |
790 static bool replace(StyleSheet ss, var include, | |
791 List<Declaration> newDeclarations) { | |
792 var visitor = new _IncludeReplacer(include, newDeclarations); | |
793 visitor.visitStyleSheet(ss); | |
794 return visitor._foundAndReplaced; | |
nweiz
2013/09/18 22:40:54
This is also never used.
terry
2013/10/09 03:40:33
Done.
| |
795 } | |
796 | |
797 _IncludeReplacer(this._include, this._newDeclarations); | |
798 | |
799 void visitDeclarationGroup(DeclarationGroup node) { | |
800 var index = -1; | |
801 var i = 0; | |
802 for (var decl in node.declarations) { | |
803 if (decl == _include) { | |
804 index = i; | |
805 break; | |
806 } else if (decl is IncludeMixinAtDeclaration) { | |
807 if (decl.include == _include) { | |
nweiz
2013/09/18 22:40:54
Use "&&" rather than nested if statements.
terry
2013/10/09 03:40:33
While re-writing not an issue anymore.
On 2013/09/
| |
808 index = i; | |
809 break; | |
810 } | |
811 } | |
812 i++; | |
813 } | |
nweiz
2013/09/18 22:40:54
This i++/for/break stuff is really complicated and
terry
2013/10/09 03:40:33
ditto on re-write.On 2013/09/18 22:40:54, nweiz wr
| |
814 | |
815 if (index != -1) { | |
nweiz
2013/09/18 22:40:54
Use null rather than -1 as a flag value.
terry
2013/10/09 03:40:33
ditto re-wrote not an issue.
On 2013/09/18 22:40:5
| |
816 node.declarations.insertAll(index + 1, _newDeclarations); | |
817 // Change @include to NoOp so it's processed only once. | |
818 node.declarations.replaceRange(index, index + 1, [new NoOp()]); | |
819 _foundAndReplaced = true; | |
820 } | |
821 super.visitDeclarationGroup(node); | |
822 } | |
823 } | |
824 | |
825 class IncludesMissingMixins extends Visitor { | |
826 final MixinDefinitions defs; | |
827 final Messages _messages; | |
828 | |
829 IncludesMissingMixins(this.defs, this._messages); | |
830 | |
831 void visitIncludeDirective(IncludeDirective node) { | |
832 if (!defs.map.containsKey(node.name)) { | |
833 // @mixin is undefined | |
834 _messages.warning("@mixin ${node.name} is undefined.", node.span); | |
nweiz
2013/09/18 22:40:54
This should be an error.
terry
2013/10/09 03:40:33
Class removed.
On 2013/09/18 22:40:54, nweiz wrote
| |
835 } | |
836 | |
837 super.visitIncludeDirective(node); | |
838 } | |
839 } | |
840 | |
841 /** | |
842 * Remove all @mixin and @include and any NoOp used as placeholder for @include. | |
843 */ | |
844 class RemoveMixinsAndIncludes extends Visitor { | |
nweiz
2013/09/18 22:40:54
Don't the other visitors remove the mixins and inc
terry
2013/10/09 03:40:33
class was eliminated when cycles removed.
On 2013/
| |
845 RemoveMixinsAndIncludes(); | |
846 | |
847 bool _nodesToRemove(node) => | |
nweiz
2013/09/18 22:40:54
This name sounds like a variable, not a function.
| |
848 node is IncludeDirective || node is MixinDefinition || node is NoOp; | |
849 | |
850 void visitStyleSheet(StyleSheet ss) { | |
851 var index = ss.topLevels.length; | |
852 while (--index >= 0) { | |
nweiz
2013/09/18 22:40:54
It's never clearer to use "--variable" in an expre
| |
853 if (_nodesToRemove(ss.topLevels[index])) { | |
854 ss.topLevels.removeAt(index); | |
855 } | |
856 } | |
857 | |
858 super.visitStyleSheet(ss); | |
859 } | |
860 | |
861 void visitDeclarationGroup(DeclarationGroup node) { | |
862 var index = node.declarations.length; | |
863 while (--index >= 0) { | |
864 if (_nodesToRemove(node.declarations[index])) { | |
865 node.declarations.removeAt(index); | |
866 } | |
867 } | |
868 | |
869 super.visitDeclarationGroup(node); | |
870 } | |
871 } | |
OLD | NEW |