OLD | NEW |
(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 } |
OLD | NEW |