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

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

Issue 23168002: move csslib into dart svn (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 4 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
« no previous file with comments | « pkg/csslib/lib/parser.dart ('k') | pkg/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
8 // TODO(terry): Detect invalid directive usage. All @imports must occur before
9 // all rules other than @charset directive. Any @import directive
10 // after any non @charset or @import directive are ignored. e.g.,
11 // @import "a.css";
12 // div { color: red; }
13 // @import "b.css";
14 // becomes:
15 // @import "a.css";
16 // div { color: red; }
17 // <http://www.w3.org/TR/css3-syntax/#at-rules>
18
19 /**
20 * Analysis phase will validate/fixup any new CSS feature or any SASS style
21 * feature.
22 */
23 class Analyzer {
24 final List<StyleSheet> _styleSheets;
25 final Messages _messages;
26 VarDefinitions varDefs;
27
28 Analyzer(this._styleSheets, this._messages);
29
30 void run() {
31 varDefs = new VarDefinitions(_styleSheets);
32
33 // Any cycles?
34 var cycles = findAllCycles();
35 for (var cycle in cycles) {
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 // Remove any var definition from the stylesheet that has a cycle.
44 _styleSheets.forEach((styleSheet) =>
45 new RemoveVarDefinitions(cycles).visitStyleSheet(styleSheet));
46
47 // Expand any nested selectors using selector desendant combinator to
48 // signal CSS inheritance notation.
49 _styleSheets.forEach((styleSheet) => new ExpandNestedSelectors()
50 ..visitStyleSheet(styleSheet)
51 ..flatten(styleSheet));
52 }
53
54 List<VarDefinition> findAllCycles() {
55 var cycles = [];
56
57 varDefs.map.values.forEach((value) {
58 if (hasCycle(value.property)) cycles.add(value);
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 }
118 }
119
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 /**
156 * 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
158 * level 0 of CSS using selector inheritance syntax (flattens the nesting).
159 *
160 * 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.,
162 *
163 * selector {
164 * declaration
165 * }
166 *
167 * AST structure of a [RuleSet] is:
168 *
169 * RuleSet
170 * SelectorGroup
171 * List<Selector>
172 * List<SimpleSelectorSequence>
173 * Combinator // +, >, ~, DESCENDENT, or NONE
174 * SimpleSelector // class, id, element, namespace, attribute
175 * DeclarationGroup
176 * List // Declaration or RuleSet
177 *
178 * For the simple rule:
179 *
180 * div + span { color: red; }
181 *
182 * the AST [RuleSet] is:
183 *
184 * RuleSet
185 * SelectorGroup
186 * List<Selector>
187 * [0]
188 * List<SimpleSelectorSequence>
189 * [0] Combinator = COMBINATOR_NONE
190 * ElementSelector (name = div)
191 * [1] Combinator = COMBINATOR_PLUS
192 * ElementSelector (name = span)
193 * DeclarationGroup
194 * List // Declarations or RuleSets
195 * [0]
196 * Declaration (property = color, expression = red)
197 *
198 * Usually a SelectorGroup contains 1 Selector. Consider the selectors:
199 *
200 * div { color: red; }
201 * a { color: red; }
202 *
203 * are equivalent to
204 *
205 * div, a { color : red; }
206 *
207 * In the above the RuleSet would have a SelectorGroup with 2 selectors e.g.,
208 *
209 * RuleSet
210 * SelectorGroup
211 * List<Selector>
212 * [0]
213 * List<SimpleSelectorSequence>
214 * [0] Combinator = COMBINATOR_NONE
215 * ElementSelector (name = div)
216 * [1]
217 * List<SimpleSelectorSequence>
218 * [0] Combinator = COMBINATOR_NONE
219 * ElementSelector (name = a)
220 * DeclarationGroup
221 * List // Declarations or RuleSets
222 * [0]
223 * Declaration (property = color, expression = red)
224 *
225 * For a nested rule e.g.,
226 *
227 * div {
228 * color : blue;
229 * a { color : red; }
230 * }
231 *
232 * Would map to the follow CSS rules:
233 *
234 * div { color: blue; }
235 * div a { color: red; }
236 *
237 * The AST for the former nested rule is:
238 *
239 * RuleSet
240 * SelectorGroup
241 * List<Selector>
242 * [0]
243 * List<SimpleSelectorSequence>
244 * [0] Combinator = COMBINATOR_NONE
245 * ElementSelector (name = div)
246 * DeclarationGroup
247 * List // Declarations or RuleSets
248 * [0]
249 * Declaration (property = color, expression = blue)
250 * [1]
251 * RuleSet
252 * SelectorGroup
253 * List<Selector>
254 * [0]
255 * List<SimpleSelectorSequence>
256 * [0] Combinator = COMBINATOR_NONE
257 * ElementSelector (name = a)
258 * DeclarationGroup
259 * List // Declarations or RuleSets
260 * [0]
261 * Declaration (property = color, expression = red)
262 *
263 * Nested rules is a terse mechanism to describe CSS inheritance. The analyzer
264 * will flatten and expand the nested rules to it's flatten strucure. Using the
265 * all parent [RuleSets] (selector expressions) and applying each nested
266 * [RuleSet] to the list of [Selectors] in a [SelectorGroup].
267 *
268 * Then result is a style sheet where all nested rules have been flatten and
269 * expanded.
270 */
271 class ExpandNestedSelectors extends Visitor {
272 /** Parent [RuleSet] if a nested rule otherwise [null]. */
273 RuleSet _parentRuleSet;
274
275 /** Top-most rule if nested rules. */
276 SelectorGroup _topLevelSelectorGroup;
277
278 /** SelectorGroup at each nesting level. */
279 SelectorGroup _nestedSelectorGroup;
280
281 /** Declaration (sans the nested selectors). */
282 DeclarationGroup _flatDeclarationGroup;
283
284 /** Each nested selector get's a flatten RuleSet. */
285 List<RuleSet> _expandedRuleSets = [];
286
287 /** Maping of a nested rule set to the fully expanded list of RuleSet(s). */
288 final Map<RuleSet, List<RuleSet>> _expansions = new Map();
289
290 void visitRuleSet(RuleSet node) {
291 final oldParent = _parentRuleSet;
292
293 var oldNestedSelectorGroups = _nestedSelectorGroup;
294
295 if (_nestedSelectorGroup == null) {
296 // Create top-level selector (may have nested rules).
297 final newSelectors = node.selectorGroup.selectors.toList();
298 _topLevelSelectorGroup = new SelectorGroup(newSelectors, node.span);
299 _nestedSelectorGroup = _topLevelSelectorGroup;
300 } else {
301 // Generate new selector groups from the nested rules.
302 _nestedSelectorGroup = _mergeToFlatten(node);
303 }
304
305 _parentRuleSet = node;
306
307 super.visitRuleSet(node);
308
309 _parentRuleSet = oldParent;
310
311 // Remove nested rules; they're all flatten and in the _expandedRuleSets.
312 node.declarationGroup.declarations.removeWhere((declaration) =>
313 declaration is RuleSet);
314
315 _nestedSelectorGroup = oldNestedSelectorGroups;
316
317 // If any expandedRuleSets and we're back at the top-level rule set then
318 // there were nested rule set(s).
319 if (_parentRuleSet == null) {
320 if (!_expandedRuleSets.isEmpty) {
321 // Remember ruleset to replace with these flattened rulesets.
322 _expansions[node] = _expandedRuleSets;
323 _expandedRuleSets = [];
324 }
325 assert(_flatDeclarationGroup == null);
326 assert(_nestedSelectorGroup == null);
327 }
328 }
329
330 /**
331 * Build up the list of all inherited sequences from the parent selector
332 * [node] is the current nested selector and it's parent is the last entry in
333 * the [_nestedSelectorGroup].
334 */
335 SelectorGroup _mergeToFlatten(RuleSet node) {
336 // Create a new SelectorGroup for this nesting level.
337 var nestedSelectors = _nestedSelectorGroup.selectors;
338 var selectors = node.selectorGroup.selectors;
339
340 // Create a merged set of previous parent selectors and current selectors.
341 var newSelectors = [];
342 for (Selector selector in selectors) {
343 for (Selector nestedSelector in nestedSelectors) {
344 var seq = _mergeNestedSelector(nestedSelector.simpleSelectorSequences,
345 selector.simpleSelectorSequences);
346 newSelectors.add(new Selector(seq, node.span));
347 }
348 }
349
350 return new SelectorGroup(newSelectors, node.span);
351 }
352
353 /**
354 * Merge the nested selector sequences [current] to the [parent] sequences or
355 * substitue any & with the parent selector.
356 */
357 List<SimpleSelectorSequence> _mergeNestedSelector(
358 List<SimpleSelectorSequence> parent,
359 List<SimpleSelectorSequence> current) {
360
361 // If any & operator then the parent selector will be substituted otherwise
362 // the parent selector is pre-pended to the current selector.
363 var hasThis = current.any((s) => s.simpleSelector.isThis);
364
365 var newSequence = [];
366
367 if (!hasThis) {
368 // If no & in the sector group then prefix with the parent selector.
369 newSequence.addAll(parent);
370 newSequence.addAll(_convertToDescendentSequence(current));
371 } else {
372 for (var sequence in current) {
373 if (sequence.simpleSelector.isThis) {
374 // Substitue the & with the parent selector and only use a combinator
375 // descendant if & is prefix by a sequence with an empty name e.g.,
376 // "... + &", "&", "... ~ &", etc.
377 var hasPrefix = !newSequence.isEmpty &&
378 !newSequence.last.simpleSelector.name.isEmpty;
379 newSequence.addAll(
380 hasPrefix ? _convertToDescendentSequence(parent) : parent);
381 } else {
382 newSequence.add(sequence);
383 }
384 }
385 }
386
387 return newSequence;
388 }
389
390 /**
391 * Return selector sequences with first sequence combinator being a
392 * descendant. Used for nested selectors when the parent selector needs to
393 * be prefixed to a nested selector or to substitute the this (&) with the
394 * parent selector.
395 */
396 List<SimpleSelectorSequence> _convertToDescendentSequence(
397 List<SimpleSelectorSequence> sequences) {
398 if (sequences.isEmpty) return sequences;
399
400 var newSequences = [];
401 var first = sequences.first;
402 newSequences.add(new SimpleSelectorSequence(first.simpleSelector,
403 first.span, TokenKind.COMBINATOR_DESCENDANT));
404 newSequences.addAll(sequences.skip(1));
405
406 return newSequences;
407 }
408
409 void visitDeclarationGroup(DeclarationGroup node) {
410 var span = node.span;
411
412 var currentGroup = new DeclarationGroup([], span);
413
414 var oldGroup = _flatDeclarationGroup;
415 _flatDeclarationGroup = currentGroup;
416
417 var expandedLength = _expandedRuleSets.length;
418
419 super.visitDeclarationGroup(node);
420
421 // We're done with the group.
422 _flatDeclarationGroup = oldGroup;
423
424 // No nested rule to process it's a top-level rule.
425 if (_nestedSelectorGroup == _topLevelSelectorGroup) return;
426
427 // If flatten selector's declaration is empty skip this selector, no need
428 // to emit an empty nested selector.
429 if (currentGroup.declarations.isEmpty) return;
430
431 var selectorGroup = _nestedSelectorGroup;
432
433 // Build new rule set from the nested selectors and declarations.
434 var newRuleSet = new RuleSet(selectorGroup, currentGroup, span);
435
436 // Place in order so outer-most rule is first.
437 if (expandedLength == _expandedRuleSets.length) {
438 _expandedRuleSets.add(newRuleSet);
439 } else {
440 _expandedRuleSets.insert(expandedLength, newRuleSet);
441 }
442 }
443
444 // Record all declarations in a nested selector (Declaration, VarDefinition
445 // and MarginGroup) but not the nested rule in the Declaration.
446
447 void visitDeclaration(Declaration node) {
448 if (_parentRuleSet != null) {
449 _flatDeclarationGroup.declarations.add(node);
450 }
451 super.visitDeclaration(node);
452 }
453
454 void visitVarDefinition(VarDefinition node) {
455 if (_parentRuleSet != null) {
456 _flatDeclarationGroup.declarations.add(node);
457 }
458 super.visitVarDefinition(node);
459 }
460
461 void visitMarginGroup(MarginGroup node) {
462 if (_parentRuleSet != null) {
463 _flatDeclarationGroup.declarations.add(node);
464 }
465 super.visitMarginGroup(node);
466 }
467
468 /**
469 * Replace the rule set that contains nested rules with the flatten rule sets.
470 */
471 void flatten(StyleSheet styleSheet) {
472 // TODO(terry): Iterate over topLevels instead of _expansions it's already
473 // a map (this maybe quadratic).
474 _expansions.forEach((RuleSet ruleSet, List<RuleSet> newRules) {
475 var index = styleSheet.topLevels.indexOf(ruleSet);
476 if (index == -1) {
477 // Check any @media directives for nested rules and replace them.
478 var found = _MediaRulesReplacer.replace(styleSheet, ruleSet, newRules);
479 assert(found);
480 } else {
481 styleSheet.topLevels.insertAll(index + 1, newRules);
482 }
483 });
484 _expansions.clear();
485 }
486 }
487
488 class _MediaRulesReplacer extends Visitor {
489 RuleSet _ruleSet;
490 List<RuleSet> _newRules;
491 bool _foundAndReplaced = false;
492
493 /**
494 * Look for the [ruleSet] inside of an @media directive; if found then replace
495 * with the [newRules]. If [ruleSet] is found and replaced return true.
496 */
497 static bool replace(StyleSheet styleSheet, RuleSet ruleSet,
498 List<RuleSet>newRules) {
499 var visitor = new _MediaRulesReplacer(ruleSet, newRules);
500 visitor.visitStyleSheet(styleSheet);
501 return visitor._foundAndReplaced;
502 }
503
504 _MediaRulesReplacer(this._ruleSet, this._newRules);
505
506 visitMediaDirective(MediaDirective node) {
507 var index = node.rulesets.indexOf(_ruleSet);
508 if (index != -1) {
509 node.rulesets.insertAll(index + 1, _newRules);
510 _foundAndReplaced = true;
511 }
512 }
513 }
OLDNEW
« no previous file with comments | « pkg/csslib/lib/parser.dart ('k') | pkg/csslib/lib/src/css_printer.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698