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

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

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 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
« no previous file with comments | « csslib/lib/parser.dart ('k') | csslib/lib/src/css_printer.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 part of csslib.parser;
6
7 // TODO(terry): Add optimizing phase to remove duplicated selectors in the same
8 // selector group (e.g., .btn, .btn { color: red; }). Also, look
9 // at simplifying selectors expressions too (much harder).
10 // TODO(terry): Detect invalid directive usage. All @imports must occur before
11 // all rules other than @charset directive. Any @import directive
12 // after any non @charset or @import directive are ignored. e.g.,
13 // @import "a.css";
14 // div { color: red; }
15 // @import "b.css";
16 // becomes:
17 // @import "a.css";
18 // div { color: red; }
19 // <http://www.w3.org/TR/css3-syntax/#at-rules>
20
21 /**
22 * Analysis phase will validate/fixup any new CSS feature or any SASS style
23 * feature.
24 */
25 class Analyzer {
26 final List<StyleSheet> _styleSheets;
27 final Messages _messages;
28
29 Analyzer(this._styleSheets, this._messages);
30
31 // TODO(terry): Currently each feature walks the AST each time. Once we have
32 // our complete feature set consider benchmarking the cost and
33 // possibly combine in one walk.
34 void run() {
35 // Expand top-level @include.
36 _styleSheets.forEach(
37 (styleSheet) => TopLevelIncludes.expand(_messages, _styleSheets));
38
39 // Expand @include in declarations.
40 _styleSheets.forEach(
41 (styleSheet) => DeclarationIncludes.expand(_messages, _styleSheets));
42
43 // Remove all @mixin and @include
44 _styleSheets.forEach((styleSheet) => MixinsAndIncludes.remove(styleSheet));
45
46 // Expand any nested selectors using selector desendant combinator to
47 // signal CSS inheritance notation.
48 _styleSheets.forEach((styleSheet) => new ExpandNestedSelectors()
49 ..visitStyleSheet(styleSheet)
50 ..flatten(styleSheet));
51
52 // Expand any @extend.
53 _styleSheets.forEach((styleSheet) {
54 var allExtends = new AllExtends()..visitStyleSheet(styleSheet);
55 new InheritExtends(_messages, allExtends)..visitStyleSheet(styleSheet);
56 });
57 }
58 }
59
60 /**
61 * Traverse all rulesets looking for nested ones. If a ruleset is in a
62 * declaration group (implies nested selector) then generate new ruleset(s) at
63 * level 0 of CSS using selector inheritance syntax (flattens the nesting).
64 *
65 * How the AST works for a rule [RuleSet] and nested rules. First of all a
66 * CSS rule [RuleSet] consist of a selector and a declaration e.g.,
67 *
68 * selector {
69 * declaration
70 * }
71 *
72 * AST structure of a [RuleSet] is:
73 *
74 * RuleSet
75 * SelectorGroup
76 * List<Selector>
77 * List<SimpleSelectorSequence>
78 * Combinator // +, >, ~, DESCENDENT, or NONE
79 * SimpleSelector // class, id, element, namespace, attribute
80 * DeclarationGroup
81 * List // Declaration or RuleSet
82 *
83 * For the simple rule:
84 *
85 * div + span { color: red; }
86 *
87 * the AST [RuleSet] is:
88 *
89 * RuleSet
90 * SelectorGroup
91 * List<Selector>
92 * [0]
93 * List<SimpleSelectorSequence>
94 * [0] Combinator = COMBINATOR_NONE
95 * ElementSelector (name = div)
96 * [1] Combinator = COMBINATOR_PLUS
97 * ElementSelector (name = span)
98 * DeclarationGroup
99 * List // Declarations or RuleSets
100 * [0]
101 * Declaration (property = color, expression = red)
102 *
103 * Usually a SelectorGroup contains 1 Selector. Consider the selectors:
104 *
105 * div { color: red; }
106 * a { color: red; }
107 *
108 * are equivalent to
109 *
110 * div, a { color : red; }
111 *
112 * In the above the RuleSet would have a SelectorGroup with 2 selectors e.g.,
113 *
114 * RuleSet
115 * SelectorGroup
116 * List<Selector>
117 * [0]
118 * List<SimpleSelectorSequence>
119 * [0] Combinator = COMBINATOR_NONE
120 * ElementSelector (name = div)
121 * [1]
122 * List<SimpleSelectorSequence>
123 * [0] Combinator = COMBINATOR_NONE
124 * ElementSelector (name = a)
125 * DeclarationGroup
126 * List // Declarations or RuleSets
127 * [0]
128 * Declaration (property = color, expression = red)
129 *
130 * For a nested rule e.g.,
131 *
132 * div {
133 * color : blue;
134 * a { color : red; }
135 * }
136 *
137 * Would map to the follow CSS rules:
138 *
139 * div { color: blue; }
140 * div a { color: red; }
141 *
142 * The AST for the former nested rule is:
143 *
144 * RuleSet
145 * SelectorGroup
146 * List<Selector>
147 * [0]
148 * List<SimpleSelectorSequence>
149 * [0] Combinator = COMBINATOR_NONE
150 * ElementSelector (name = div)
151 * DeclarationGroup
152 * List // Declarations or RuleSets
153 * [0]
154 * Declaration (property = color, expression = blue)
155 * [1]
156 * RuleSet
157 * SelectorGroup
158 * List<Selector>
159 * [0]
160 * List<SimpleSelectorSequence>
161 * [0] Combinator = COMBINATOR_NONE
162 * ElementSelector (name = a)
163 * DeclarationGroup
164 * List // Declarations or RuleSets
165 * [0]
166 * Declaration (property = color, expression = red)
167 *
168 * Nested rules is a terse mechanism to describe CSS inheritance. The analyzer
169 * will flatten and expand the nested rules to it's flatten strucure. Using the
170 * all parent [RuleSets] (selector expressions) and applying each nested
171 * [RuleSet] to the list of [Selectors] in a [SelectorGroup].
172 *
173 * Then result is a style sheet where all nested rules have been flatten and
174 * expanded.
175 */
176 class ExpandNestedSelectors extends Visitor {
177 /** Parent [RuleSet] if a nested rule otherwise [:null:]. */
178 RuleSet _parentRuleSet;
179
180 /** Top-most rule if nested rules. */
181 SelectorGroup _topLevelSelectorGroup;
182
183 /** SelectorGroup at each nesting level. */
184 SelectorGroup _nestedSelectorGroup;
185
186 /** Declaration (sans the nested selectors). */
187 DeclarationGroup _flatDeclarationGroup;
188
189 /** Each nested selector get's a flatten RuleSet. */
190 List<RuleSet> _expandedRuleSets = [];
191
192 /** Maping of a nested rule set to the fully expanded list of RuleSet(s). */
193 final Map<RuleSet, List<RuleSet>> _expansions = new Map();
194
195 void visitRuleSet(RuleSet node) {
196 final oldParent = _parentRuleSet;
197
198 var oldNestedSelectorGroups = _nestedSelectorGroup;
199
200 if (_nestedSelectorGroup == null) {
201 // Create top-level selector (may have nested rules).
202 final newSelectors = node.selectorGroup.selectors.toList();
203 _topLevelSelectorGroup = new SelectorGroup(newSelectors, node.span);
204 _nestedSelectorGroup = _topLevelSelectorGroup;
205 } else {
206 // Generate new selector groups from the nested rules.
207 _nestedSelectorGroup = _mergeToFlatten(node);
208 }
209
210 _parentRuleSet = node;
211
212 super.visitRuleSet(node);
213
214 _parentRuleSet = oldParent;
215
216 // Remove nested rules; they're all flatten and in the _expandedRuleSets.
217 node.declarationGroup.declarations
218 .removeWhere((declaration) => declaration is RuleSet);
219
220 _nestedSelectorGroup = oldNestedSelectorGroups;
221
222 // If any expandedRuleSets and we're back at the top-level rule set then
223 // there were nested rule set(s).
224 if (_parentRuleSet == null) {
225 if (!_expandedRuleSets.isEmpty) {
226 // Remember ruleset to replace with these flattened rulesets.
227 _expansions[node] = _expandedRuleSets;
228 _expandedRuleSets = [];
229 }
230 assert(_flatDeclarationGroup == null);
231 assert(_nestedSelectorGroup == null);
232 }
233 }
234
235 /**
236 * Build up the list of all inherited sequences from the parent selector
237 * [node] is the current nested selector and it's parent is the last entry in
238 * the [_nestedSelectorGroup].
239 */
240 SelectorGroup _mergeToFlatten(RuleSet node) {
241 // Create a new SelectorGroup for this nesting level.
242 var nestedSelectors = _nestedSelectorGroup.selectors;
243 var selectors = node.selectorGroup.selectors;
244
245 // Create a merged set of previous parent selectors and current selectors.
246 var newSelectors = [];
247 for (Selector selector in selectors) {
248 for (Selector nestedSelector in nestedSelectors) {
249 var seq = _mergeNestedSelector(nestedSelector.simpleSelectorSequences,
250 selector.simpleSelectorSequences);
251 newSelectors.add(new Selector(seq, node.span));
252 }
253 }
254
255 return new SelectorGroup(newSelectors, node.span);
256 }
257
258 /**
259 * Merge the nested selector sequences [current] to the [parent] sequences or
260 * substitue any & with the parent selector.
261 */
262 List<SimpleSelectorSequence> _mergeNestedSelector(
263 List<SimpleSelectorSequence> parent,
264 List<SimpleSelectorSequence> current) {
265
266 // If any & operator then the parent selector will be substituted otherwise
267 // the parent selector is pre-pended to the current selector.
268 var hasThis = current.any((s) => s.simpleSelector.isThis);
269
270 var newSequence = [];
271
272 if (!hasThis) {
273 // If no & in the sector group then prefix with the parent selector.
274 newSequence.addAll(parent);
275 newSequence.addAll(_convertToDescendentSequence(current));
276 } else {
277 for (var sequence in current) {
278 if (sequence.simpleSelector.isThis) {
279 // Substitue the & with the parent selector and only use a combinator
280 // descendant if & is prefix by a sequence with an empty name e.g.,
281 // "... + &", "&", "... ~ &", etc.
282 var hasPrefix = !newSequence.isEmpty &&
283 !newSequence.last.simpleSelector.name.isEmpty;
284 newSequence.addAll(
285 hasPrefix ? _convertToDescendentSequence(parent) : parent);
286 } else {
287 newSequence.add(sequence);
288 }
289 }
290 }
291
292 return newSequence;
293 }
294
295 /**
296 * Return selector sequences with first sequence combinator being a
297 * descendant. Used for nested selectors when the parent selector needs to
298 * be prefixed to a nested selector or to substitute the this (&) with the
299 * parent selector.
300 */
301 List<SimpleSelectorSequence> _convertToDescendentSequence(
302 List<SimpleSelectorSequence> sequences) {
303 if (sequences.isEmpty) return sequences;
304
305 var newSequences = [];
306 var first = sequences.first;
307 newSequences.add(new SimpleSelectorSequence(
308 first.simpleSelector, first.span, TokenKind.COMBINATOR_DESCENDANT));
309 newSequences.addAll(sequences.skip(1));
310
311 return newSequences;
312 }
313
314 void visitDeclarationGroup(DeclarationGroup node) {
315 var span = node.span;
316
317 var currentGroup = new DeclarationGroup([], span);
318
319 var oldGroup = _flatDeclarationGroup;
320 _flatDeclarationGroup = currentGroup;
321
322 var expandedLength = _expandedRuleSets.length;
323
324 super.visitDeclarationGroup(node);
325
326 // We're done with the group.
327 _flatDeclarationGroup = oldGroup;
328
329 // No nested rule to process it's a top-level rule.
330 if (_nestedSelectorGroup == _topLevelSelectorGroup) return;
331
332 // If flatten selector's declaration is empty skip this selector, no need
333 // to emit an empty nested selector.
334 if (currentGroup.declarations.isEmpty) return;
335
336 var selectorGroup = _nestedSelectorGroup;
337
338 // Build new rule set from the nested selectors and declarations.
339 var newRuleSet = new RuleSet(selectorGroup, currentGroup, span);
340
341 // Place in order so outer-most rule is first.
342 if (expandedLength == _expandedRuleSets.length) {
343 _expandedRuleSets.add(newRuleSet);
344 } else {
345 _expandedRuleSets.insert(expandedLength, newRuleSet);
346 }
347 }
348
349 // Record all declarations in a nested selector (Declaration, VarDefinition
350 // and MarginGroup) but not the nested rule in the Declaration.
351
352 void visitDeclaration(Declaration node) {
353 if (_parentRuleSet != null) {
354 _flatDeclarationGroup.declarations.add(node);
355 }
356 super.visitDeclaration(node);
357 }
358
359 void visitVarDefinition(VarDefinition node) {
360 if (_parentRuleSet != null) {
361 _flatDeclarationGroup.declarations.add(node);
362 }
363 super.visitVarDefinition(node);
364 }
365
366 void visitExtendDeclaration(ExtendDeclaration node) {
367 if (_parentRuleSet != null) {
368 _flatDeclarationGroup.declarations.add(node);
369 }
370 super.visitExtendDeclaration(node);
371 }
372
373 void visitMarginGroup(MarginGroup node) {
374 if (_parentRuleSet != null) {
375 _flatDeclarationGroup.declarations.add(node);
376 }
377 super.visitMarginGroup(node);
378 }
379
380 /**
381 * Replace the rule set that contains nested rules with the flatten rule sets.
382 */
383 void flatten(StyleSheet styleSheet) {
384 // TODO(terry): Iterate over topLevels instead of _expansions it's already
385 // a map (this maybe quadratic).
386 _expansions.forEach((RuleSet ruleSet, List<RuleSet> newRules) {
387 var index = styleSheet.topLevels.indexOf(ruleSet);
388 if (index == -1) {
389 // Check any @media directives for nested rules and replace them.
390 var found = _MediaRulesReplacer.replace(styleSheet, ruleSet, newRules);
391 assert(found);
392 } else {
393 styleSheet.topLevels.insertAll(index + 1, newRules);
394 }
395 });
396 _expansions.clear();
397 }
398 }
399
400 class _MediaRulesReplacer extends Visitor {
401 RuleSet _ruleSet;
402 List<RuleSet> _newRules;
403 bool _foundAndReplaced = false;
404
405 /**
406 * Look for the [ruleSet] inside of an @media directive; if found then replace
407 * with the [newRules]. If [ruleSet] is found and replaced return true.
408 */
409 static bool replace(
410 StyleSheet styleSheet, RuleSet ruleSet, List<RuleSet> newRules) {
411 var visitor = new _MediaRulesReplacer(ruleSet, newRules);
412 visitor.visitStyleSheet(styleSheet);
413 return visitor._foundAndReplaced;
414 }
415
416 _MediaRulesReplacer(this._ruleSet, this._newRules);
417
418 visitMediaDirective(MediaDirective node) {
419 var index = node.rulesets.indexOf(_ruleSet);
420 if (index != -1) {
421 node.rulesets.insertAll(index + 1, _newRules);
422 _foundAndReplaced = true;
423 }
424 }
425 }
426
427 /**
428 * Expand all @include at the top-level the ruleset(s) associated with the
429 * mixin.
430 */
431 class TopLevelIncludes extends Visitor {
432 StyleSheet _styleSheet;
433 final Messages _messages;
434 /** Map of variable name key to it's definition. */
435 final Map<String, MixinDefinition> map = new Map<String, MixinDefinition>();
436 MixinDefinition currDef;
437
438 static void expand(Messages messages, List<StyleSheet> styleSheets) {
439 new TopLevelIncludes(messages, styleSheets);
440 }
441
442 bool _anyRulesets(MixinRulesetDirective def) =>
443 def.rulesets.any((rule) => rule is RuleSet);
444
445 TopLevelIncludes(this._messages, List<StyleSheet> styleSheets) {
446 for (var styleSheet in styleSheets) {
447 visitTree(styleSheet);
448 }
449 }
450
451 void visitStyleSheet(StyleSheet ss) {
452 _styleSheet = ss;
453 super.visitStyleSheet(ss);
454 _styleSheet = null;
455 }
456
457 void visitIncludeDirective(IncludeDirective node) {
458 if (map.containsKey(node.name)) {
459 var mixinDef = map[node.name];
460 if (mixinDef is MixinRulesetDirective) {
461 _TopLevelIncludeReplacer.replace(
462 _messages, _styleSheet, node, mixinDef.rulesets);
463 } else if (currDef is MixinRulesetDirective && _anyRulesets(currDef)) {
464 // currDef is MixinRulesetDirective
465 MixinRulesetDirective mixinRuleset = currDef;
466 int index = mixinRuleset.rulesets.indexOf(node as dynamic);
467 mixinRuleset.rulesets.replaceRange(index, index + 1, [new NoOp()]);
468 _messages.warning(
469 'Using declaration mixin ${node.name} as top-level mixin',
470 node.span);
471 }
472 } else {
473 if (currDef is MixinRulesetDirective) {
474 MixinRulesetDirective rulesetDirect = currDef as MixinRulesetDirective;
475 var index = 0;
476 rulesetDirect.rulesets.forEach((entry) {
477 if (entry == node) {
478 rulesetDirect.rulesets.replaceRange(index, index + 1, [new NoOp()]);
479 _messages.warning('Undefined mixin ${node.name}', node.span);
480 }
481 index++;
482 });
483 }
484 }
485 super.visitIncludeDirective(node);
486 }
487
488 void visitMixinRulesetDirective(MixinRulesetDirective node) {
489 currDef = node;
490
491 super.visitMixinRulesetDirective(node);
492
493 // Replace with latest top-level mixin definition.
494 map[node.name] = node;
495 currDef = null;
496 }
497
498 void visitMixinDeclarationDirective(MixinDeclarationDirective node) {
499 currDef = node;
500
501 super.visitMixinDeclarationDirective(node);
502
503 // Replace with latest mixin definition.
504 map[node.name] = node;
505 currDef = null;
506 }
507 }
508
509 /** @include as a top-level with ruleset(s). */
510 class _TopLevelIncludeReplacer extends Visitor {
511 final Messages _messages;
512 final IncludeDirective _include;
513 final List<RuleSet> _newRules;
514 bool _foundAndReplaced = false;
515
516 /**
517 * Look for the [ruleSet] inside of an @media directive; if found then replace
518 * with the [newRules]. If [ruleSet] is found and replaced return true.
519 */
520 static bool replace(Messages messages, StyleSheet styleSheet,
521 IncludeDirective include, List<RuleSet> newRules) {
522 var visitor = new _TopLevelIncludeReplacer(messages, include, newRules);
523 visitor.visitStyleSheet(styleSheet);
524 return visitor._foundAndReplaced;
525 }
526
527 _TopLevelIncludeReplacer(this._messages, this._include, this._newRules);
528
529 visitStyleSheet(StyleSheet node) {
530 var index = node.topLevels.indexOf(_include);
531 if (index != -1) {
532 node.topLevels.insertAll(index + 1, _newRules);
533 node.topLevels.replaceRange(index, index + 1, [new NoOp()]);
534 _foundAndReplaced = true;
535 }
536 super.visitStyleSheet(node);
537 }
538
539 void visitMixinRulesetDirective(MixinRulesetDirective node) {
540 var index = node.rulesets.indexOf(_include as dynamic);
541 if (index != -1) {
542 node.rulesets.insertAll(index + 1, _newRules);
543 // Only the resolve the @include once.
544 node.rulesets.replaceRange(index, index + 1, [new NoOp()]);
545 _foundAndReplaced = true;
546 }
547 super.visitMixinRulesetDirective(node);
548 }
549 }
550
551 /**
552 * Utility function to match an include to a list of either Declarations or
553 * RuleSets, depending on type of mixin (ruleset or declaration). The include
554 * can be an include in a declaration or an include directive (top-level).
555 */
556 int _findInclude(List list, var node) {
557 IncludeDirective matchNode =
558 (node is IncludeMixinAtDeclaration) ? node.include : node;
559
560 var index = 0;
561 for (var item in list) {
562 var includeNode = (item is IncludeMixinAtDeclaration) ? item.include : item;
563 if (includeNode == matchNode) return index;
564 index++;
565 }
566 return -1;
567 }
568
569 /**
570 * Stamp out a mixin with the defined args substituted with the user's
571 * parameters.
572 */
573 class CallMixin extends Visitor {
574 final MixinDefinition mixinDef;
575 List _definedArgs;
576 Expressions _currExpressions;
577 int _currIndex = -1;
578
579 final varUsages = new Map<String, Map<Expressions, Set<int>>>();
580
581 /** Only var defs with more than one expression (comma separated). */
582 final Map<String, VarDefinition> varDefs;
583
584 CallMixin(this.mixinDef, [this.varDefs]) {
585 if (mixinDef is MixinRulesetDirective) {
586 visitMixinRulesetDirective(mixinDef);
587 } else {
588 visitMixinDeclarationDirective(mixinDef);
589 }
590 }
591
592 /**
593 * Given a mixin's defined arguments return a cloned mixin defintion that has
594 * replaced all defined arguments with user's supplied VarUsages.
595 */
596 MixinDefinition transform(List callArgs) {
597 // TODO(terry): Handle default arguments and varArgs.
598 // Transform mixin with callArgs.
599 for (var index = 0; index < _definedArgs.length; index++) {
600 var definedArg = _definedArgs[index];
601 VarDefinition varDef;
602 if (definedArg is VarDefinition) {
603 varDef = definedArg;
604 } else if (definedArg is VarDefinitionDirective) {
605 VarDefinitionDirective varDirective = definedArg;
606 varDef = varDirective.def;
607 }
608 var callArg = callArgs[index];
609
610 // Is callArg a var definition with multi-args (expressions > 1).
611 var defArgs = _varDefsAsCallArgs(callArg);
612 if (defArgs.isNotEmpty) {
613 // Replace call args with the var def parameters.
614 callArgs.insertAll(index, defArgs);
615 callArgs.removeAt(index + defArgs.length);
616 callArg = callArgs[index];
617 }
618
619 var expressions = varUsages[varDef.definedName];
620 expressions.forEach((k, v) {
621 for (var usagesIndex in v) {
622 k.expressions.replaceRange(usagesIndex, usagesIndex + 1, callArg);
623 }
624 });
625 }
626
627 // Clone the mixin
628 return mixinDef.clone();
629 }
630
631 /** Rip apart var def with multiple parameters. */
632 List<List<TreeNode>> _varDefsAsCallArgs(var callArg) {
633 var defArgs = [];
634 if (callArg is List && callArg[0] is VarUsage) {
635 var varDef = varDefs[callArg[0].name];
636 var expressions = varDef.expression.expressions;
637 assert(expressions.length > 1);
638 for (var expr in expressions) {
639 if (expr is! OperatorComma) {
640 defArgs.add([expr]);
641 }
642 }
643 }
644 return defArgs;
645 }
646
647 void visitExpressions(Expressions node) {
648 var oldExpressions = _currExpressions;
649 var oldIndex = _currIndex;
650
651 _currExpressions = node;
652 for (_currIndex = 0; _currIndex < node.expressions.length; _currIndex++) {
653 node.expressions[_currIndex].visit(this);
654 }
655
656 _currIndex = oldIndex;
657 _currExpressions = oldExpressions;
658 }
659
660 void _addExpression(Map<Expressions, Set<int>> expressions) {
661 var indexSet = new Set<int>();
662 indexSet.add(_currIndex);
663 expressions[_currExpressions] = indexSet;
664 }
665
666 void visitVarUsage(VarUsage node) {
667 assert(_currIndex != -1);
668 assert(_currExpressions != null);
669 if (varUsages.containsKey(node.name)) {
670 Map<Expressions, Set<int>> expressions = varUsages[node.name];
671 Set<int> allIndexes = expressions[_currExpressions];
672 if (allIndexes == null) {
673 _addExpression(expressions);
674 } else {
675 allIndexes.add(_currIndex);
676 }
677 } else {
678 var newExpressions = new Map<Expressions, Set<int>>();
679 _addExpression(newExpressions);
680 varUsages[node.name] = newExpressions;
681 }
682 super.visitVarUsage(node);
683 }
684
685 void visitMixinDeclarationDirective(MixinDeclarationDirective node) {
686 _definedArgs = node.definedArgs;
687 super.visitMixinDeclarationDirective(node);
688 }
689
690 void visitMixinRulesetDirective(MixinRulesetDirective node) {
691 _definedArgs = node.definedArgs;
692 super.visitMixinRulesetDirective(node);
693 }
694 }
695
696 /** Expand all @include inside of a declaration associated with a mixin. */
697 class DeclarationIncludes extends Visitor {
698 StyleSheet _styleSheet;
699 final Messages _messages;
700 /** Map of variable name key to it's definition. */
701 final Map<String, MixinDefinition> map = new Map<String, MixinDefinition>();
702 /** Cache of mixin called with parameters. */
703 final Map<String, CallMixin> callMap = new Map<String, CallMixin>();
704 MixinDefinition currDef;
705 DeclarationGroup currDeclGroup;
706
707 /** Var definitions with more than 1 expression. */
708 final Map<String, VarDefinition> varDefs = new Map<String, VarDefinition>();
709
710 static void expand(Messages messages, List<StyleSheet> styleSheets) {
711 new DeclarationIncludes(messages, styleSheets);
712 }
713
714 DeclarationIncludes(this._messages, List<StyleSheet> styleSheets) {
715 for (var styleSheet in styleSheets) {
716 visitTree(styleSheet);
717 }
718 }
719
720 bool _allIncludes(rulesets) =>
721 rulesets.every((rule) => rule is IncludeDirective || rule is NoOp);
722
723 CallMixin _createCallDeclMixin(MixinDefinition mixinDef) {
724 callMap.putIfAbsent(mixinDef.name,
725 () => callMap[mixinDef.name] = new CallMixin(mixinDef, varDefs));
726 return callMap[mixinDef.name];
727 }
728
729 void visitStyleSheet(StyleSheet ss) {
730 _styleSheet = ss;
731 super.visitStyleSheet(ss);
732 _styleSheet = null;
733 }
734
735 void visitDeclarationGroup(DeclarationGroup node) {
736 currDeclGroup = node;
737 super.visitDeclarationGroup(node);
738 currDeclGroup = null;
739 }
740
741 void visitIncludeMixinAtDeclaration(IncludeMixinAtDeclaration node) {
742 if (map.containsKey(node.include.name)) {
743 var mixinDef = map[node.include.name];
744
745 // Fix up any mixin that is really a Declaration but has includes.
746 if (mixinDef is MixinRulesetDirective) {
747 if (!_allIncludes(mixinDef.rulesets) && currDeclGroup != null) {
748 var index = _findInclude(currDeclGroup.declarations, node);
749 if (index != -1) {
750 currDeclGroup.declarations.replaceRange(
751 index, index + 1, [new NoOp()]);
752 }
753 _messages.warning(
754 "Using top-level mixin ${node.include.name} as a declaration",
755 node.span);
756 } else {
757 // We're a list of @include(s) inside of a mixin ruleset - convert
758 // to a list of IncludeMixinAtDeclaration(s).
759 var origRulesets = mixinDef.rulesets;
760 var rulesets = [];
761 if (origRulesets.every((ruleset) => ruleset is IncludeDirective)) {
762 origRulesets.forEach((ruleset) {
763 rulesets
764 .add(new IncludeMixinAtDeclaration(ruleset, ruleset.span));
765 });
766 _IncludeReplacer.replace(_styleSheet, node, rulesets);
767 }
768 }
769 }
770
771 if (mixinDef.definedArgs.length > 0 && node.include.args.length > 0) {
772 var callMixin = _createCallDeclMixin(mixinDef);
773 mixinDef = callMixin.transform(node.include.args);
774 }
775
776 if (mixinDef is MixinDeclarationDirective) {
777 _IncludeReplacer.replace(
778 _styleSheet, node, mixinDef.declarations.declarations);
779 }
780 } else {
781 _messages.warning("Undefined mixin ${node.include.name}", node.span);
782 }
783
784 super.visitIncludeMixinAtDeclaration(node);
785 }
786
787 void visitIncludeDirective(IncludeDirective node) {
788 if (map.containsKey(node.name)) {
789 var mixinDef = map[node.name];
790 if (currDef is MixinDeclarationDirective &&
791 mixinDef is MixinDeclarationDirective) {
792 _IncludeReplacer.replace(
793 _styleSheet, node, mixinDef.declarations.declarations);
794 } else if (currDef is MixinDeclarationDirective) {
795 var decls =
796 (currDef as MixinDeclarationDirective).declarations.declarations;
797 var index = _findInclude(decls, node);
798 if (index != -1) {
799 decls.replaceRange(index, index + 1, [new NoOp()]);
800 }
801 }
802 }
803
804 super.visitIncludeDirective(node);
805 }
806
807 void visitMixinRulesetDirective(MixinRulesetDirective node) {
808 currDef = node;
809
810 super.visitMixinRulesetDirective(node);
811
812 // Replace with latest top-level mixin definition.
813 map[node.name] = node;
814 currDef = null;
815 }
816
817 void visitMixinDeclarationDirective(MixinDeclarationDirective node) {
818 currDef = node;
819
820 super.visitMixinDeclarationDirective(node);
821
822 // Replace with latest mixin definition.
823 map[node.name] = node;
824 currDef = null;
825 }
826
827 void visitVarDefinition(VarDefinition node) {
828 // Only record var definitions that have multiple expressions (comma
829 // separated for mixin parameter substitution.
830 var exprs = (node.expression as Expressions).expressions;
831 if (exprs.length > 1) {
832 varDefs[node.definedName] = node;
833 }
834 super.visitVarDefinition(node);
835 }
836
837 void visitVarDefinitionDirective(VarDefinitionDirective node) {
838 visitVarDefinition(node.def);
839 }
840 }
841
842 /** @include as a top-level with ruleset(s). */
843 class _IncludeReplacer extends Visitor {
844 final _include;
845 final List<Declaration> _newDeclarations;
846 bool _foundAndReplaced = false;
847
848 /**
849 * Look for the [ruleSet] inside of a @media directive; if found then replace
850 * with the [newRules].
851 */
852 static void replace(
853 StyleSheet ss, var include, List<Declaration> newDeclarations) {
854 var visitor = new _IncludeReplacer(include, newDeclarations);
855 visitor.visitStyleSheet(ss);
856 }
857
858 _IncludeReplacer(this._include, this._newDeclarations);
859
860 void visitDeclarationGroup(DeclarationGroup node) {
861 var index = _findInclude(node.declarations, _include);
862 if (index != -1) {
863 node.declarations.insertAll(index + 1, _newDeclarations);
864 // Change @include to NoOp so it's processed only once.
865 node.declarations.replaceRange(index, index + 1, [new NoOp()]);
866 _foundAndReplaced = true;
867 }
868 super.visitDeclarationGroup(node);
869 }
870 }
871
872 /**
873 * Remove all @mixin and @include and any NoOp used as placeholder for @include.
874 */
875 class MixinsAndIncludes extends Visitor {
876 static void remove(StyleSheet styleSheet) {
877 new MixinsAndIncludes()..visitStyleSheet(styleSheet);
878 }
879
880 bool _nodesToRemove(node) =>
881 node is IncludeDirective || node is MixinDefinition || node is NoOp;
882
883 void visitStyleSheet(StyleSheet ss) {
884 var index = ss.topLevels.length;
885 while (--index >= 0) {
886 if (_nodesToRemove(ss.topLevels[index])) {
887 ss.topLevels.removeAt(index);
888 }
889 }
890 super.visitStyleSheet(ss);
891 }
892
893 void visitDeclarationGroup(DeclarationGroup node) {
894 var index = node.declarations.length;
895 while (--index >= 0) {
896 if (_nodesToRemove(node.declarations[index])) {
897 node.declarations.removeAt(index);
898 }
899 }
900 super.visitDeclarationGroup(node);
901 }
902 }
903
904 /** Find all @extend to create inheritance. */
905 class AllExtends extends Visitor {
906 final Map<String, List<SelectorGroup>> inherits =
907 new Map<String, List<SelectorGroup>>();
908
909 SelectorGroup _currSelectorGroup;
910 int _currDeclIndex;
911 List<int> _extendsToRemove = [];
912
913 void visitRuleSet(RuleSet node) {
914 var oldSelectorGroup = _currSelectorGroup;
915 _currSelectorGroup = node.selectorGroup;
916
917 super.visitRuleSet(node);
918
919 _currSelectorGroup = oldSelectorGroup;
920 }
921
922 void visitExtendDeclaration(ExtendDeclaration node) {
923 var inheritName = "";
924 for (var selector in node.selectors) {
925 inheritName += selector.toString();
926 }
927 if (inherits.containsKey(inheritName)) {
928 inherits[inheritName].add(_currSelectorGroup);
929 } else {
930 inherits[inheritName] = [_currSelectorGroup];
931 }
932
933 // Remove this @extend
934 _extendsToRemove.add(_currDeclIndex);
935
936 super.visitExtendDeclaration(node);
937 }
938
939 void visitDeclarationGroup(DeclarationGroup node) {
940 var oldDeclIndex = _currDeclIndex;
941
942 var decls = node.declarations;
943 for (_currDeclIndex = 0; _currDeclIndex < decls.length; _currDeclIndex++) {
944 decls[_currDeclIndex].visit(this);
945 }
946
947 if (_extendsToRemove.isNotEmpty) {
948 var removeTotal = _extendsToRemove.length - 1;
949 for (var index = removeTotal; index >= 0; index--) {
950 decls.removeAt(_extendsToRemove[index]);
951 }
952 _extendsToRemove.clear();
953 }
954
955 _currDeclIndex = oldDeclIndex;
956 }
957 }
958
959 // TODO(terry): Need to handle merging selector sequences
960 // TODO(terry): Need to handle @extend-Only selectors.
961 // TODO(terry): Need to handle !optional glag.
962 /**
963 * Changes any selector that matches @extend.
964 */
965 class InheritExtends extends Visitor {
966 final Messages _messages;
967 final AllExtends _allExtends;
968
969 InheritExtends(this._messages, this._allExtends);
970
971 void visitSelectorGroup(SelectorGroup node) {
972 for (var selectorsIndex = 0;
973 selectorsIndex < node.selectors.length;
974 selectorsIndex++) {
975 var selectors = node.selectors[selectorsIndex];
976 var isLastNone = false;
977 var selectorName = "";
978 for (var index = 0;
979 index < selectors.simpleSelectorSequences.length;
980 index++) {
981 var simpleSeq = selectors.simpleSelectorSequences[index];
982 var namePart = simpleSeq.simpleSelector.toString();
983 selectorName = (isLastNone) ? (selectorName + namePart) : namePart;
984 List<SelectorGroup> matches = _allExtends.inherits[selectorName];
985 if (matches != null) {
986 for (var match in matches) {
987 // Create a new group.
988 var newSelectors = selectors.clone();
989 var newSeq = match.selectors[0].clone();
990 if (isLastNone) {
991 // Add the inherited selector.
992 node.selectors.add(newSeq);
993 } else {
994 // Replace the selector sequence to the left of the pseudo class
995 // or pseudo element.
996
997 // Make new selector seq combinator the same as the original.
998 var orgCombinator =
999 newSelectors.simpleSelectorSequences[index].combinator;
1000 newSeq.simpleSelectorSequences[0].combinator = orgCombinator;
1001
1002 newSelectors.simpleSelectorSequences.replaceRange(
1003 index, index + 1, newSeq.simpleSelectorSequences);
1004 node.selectors.add(newSelectors);
1005 }
1006 isLastNone = false;
1007 }
1008 } else {
1009 isLastNone = simpleSeq.isCombinatorNone;
1010 }
1011 }
1012 }
1013 super.visitSelectorGroup(node);
1014 }
1015 }
OLDNEW
« no previous file with comments | « csslib/lib/parser.dart ('k') | csslib/lib/src/css_printer.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698