Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(387)

Side by Side Diff: pkg/csslib/lib/src/analyzer.dart

Issue 23819036: Support for @mixin, @include and @extend (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: mixin w/o parameters Created 7 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698