// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. part of '../parser.dart'; // TODO(terry): Add optimizing phase to remove duplicated selectors in the same // selector group (e.g., .btn, .btn { color: red; }). Also, look // at simplifying selectors expressions too (much harder). // TODO(terry): Detect invalid directive usage. All @imports must occur before // all rules other than @charset directive. Any @import directive // after any non @charset or @import directive are ignored. e.g., // @import "a.css"; // div { color: red; } // @import "b.css"; // becomes: // @import "a.css"; // div { color: red; } // /// Analysis phase will validate/fixup any new CSS feature or any Sass style /// feature. class Analyzer { final List _styleSheets; final Messages _messages; Analyzer(this._styleSheets, this._messages); // TODO(terry): Currently each feature walks the AST each time. Once we have // our complete feature set consider benchmarking the cost and // possibly combine in one walk. void run() { // Expand top-level @include. TopLevelIncludes.expand(_messages, _styleSheets); // Expand @include in declarations. DeclarationIncludes.expand(_messages, _styleSheets); // Remove all @mixin and @include for (var styleSheet in _styleSheets) { MixinsAndIncludes.remove(styleSheet); } // Expand any nested selectors using selector descendant combinator to // signal CSS inheritance notation. for (var styleSheet in _styleSheets) { ExpandNestedSelectors() ..visitStyleSheet(styleSheet) ..flatten(styleSheet); } // Expand any @extend. for (var styleSheet in _styleSheets) { var allExtends = AllExtends()..visitStyleSheet(styleSheet); InheritExtends(_messages, allExtends).visitStyleSheet(styleSheet); } } } /// Traverse all rulesets looking for nested ones. If a ruleset is in a /// declaration group (implies nested selector) then generate new ruleset(s) at /// level 0 of CSS using selector inheritance syntax (flattens the nesting). /// /// How the AST works for a rule [RuleSet] and nested rules. First of all a /// CSS rule [RuleSet] consist of a selector and a declaration e.g., /// /// selector { /// declaration /// } /// /// AST structure of a [RuleSet] is: /// /// RuleSet /// SelectorGroup /// List /// List /// Combinator // +, >, ~, DESCENDENT, or NONE /// SimpleSelector // class, id, element, namespace, attribute /// DeclarationGroup /// List // Declaration or RuleSet /// /// For the simple rule: /// /// div + span { color: red; } /// /// the AST [RuleSet] is: /// /// RuleSet /// SelectorGroup /// List /// [0] /// List /// [0] Combinator = COMBINATOR_NONE /// ElementSelector (name = div) /// [1] Combinator = COMBINATOR_PLUS /// ElementSelector (name = span) /// DeclarationGroup /// List // Declarations or RuleSets /// [0] /// Declaration (property = color, expression = red) /// /// Usually a SelectorGroup contains 1 Selector. Consider the selectors: /// /// div { color: red; } /// a { color: red; } /// /// are equivalent to /// /// div, a { color : red; } /// /// In the above the RuleSet would have a SelectorGroup with 2 selectors e.g., /// /// RuleSet /// SelectorGroup /// List /// [0] /// List /// [0] Combinator = COMBINATOR_NONE /// ElementSelector (name = div) /// [1] /// List /// [0] Combinator = COMBINATOR_NONE /// ElementSelector (name = a) /// DeclarationGroup /// List // Declarations or RuleSets /// [0] /// Declaration (property = color, expression = red) /// /// For a nested rule e.g., /// /// div { /// color : blue; /// a { color : red; } /// } /// /// Would map to the follow CSS rules: /// /// div { color: blue; } /// div a { color: red; } /// /// The AST for the former nested rule is: /// /// RuleSet /// SelectorGroup /// List /// [0] /// List /// [0] Combinator = COMBINATOR_NONE /// ElementSelector (name = div) /// DeclarationGroup /// List // Declarations or RuleSets /// [0] /// Declaration (property = color, expression = blue) /// [1] /// RuleSet /// SelectorGroup /// List /// [0] /// List /// [0] Combinator = COMBINATOR_NONE /// ElementSelector (name = a) /// DeclarationGroup /// List // Declarations or RuleSets /// [0] /// Declaration (property = color, expression = red) /// /// Nested rules is a terse mechanism to describe CSS inheritance. The analyzer /// will flatten and expand the nested rules to it's flatten structure. Using /// the all parent [RuleSets] (selector expressions) and applying each nested /// [RuleSet] to the list of [Selectors] in a [SelectorGroup]. /// /// Then result is a style sheet where all nested rules have been flatten and /// expanded. class ExpandNestedSelectors extends Visitor { /// Parent [RuleSet] if a nested rule otherwise `null`. RuleSet? _parentRuleSet; /// Top-most rule if nested rules. SelectorGroup? _topLevelSelectorGroup; /// SelectorGroup at each nesting level. SelectorGroup? _nestedSelectorGroup; /// Declaration (sans the nested selectors). DeclarationGroup? _flatDeclarationGroup; /// Each nested selector get's a flatten RuleSet. List _expandedRuleSets = []; /// Maping of a nested rule set to the fully expanded list of RuleSet(s). final _expansions = >{}; @override void visitRuleSet(RuleSet node) { final oldParent = _parentRuleSet; var oldNestedSelectorGroups = _nestedSelectorGroup; if (_nestedSelectorGroup == null) { // Create top-level selector (may have nested rules). final newSelectors = node.selectorGroup!.selectors.toList(); _topLevelSelectorGroup = SelectorGroup(newSelectors, node.span); _nestedSelectorGroup = _topLevelSelectorGroup; } else { // Generate new selector groups from the nested rules. _nestedSelectorGroup = _mergeToFlatten(node); } _parentRuleSet = node; super.visitRuleSet(node); _parentRuleSet = oldParent; // Remove nested rules; they're all flatten and in the _expandedRuleSets. node.declarationGroup.declarations .removeWhere((declaration) => declaration is RuleSet); _nestedSelectorGroup = oldNestedSelectorGroups; // If any expandedRuleSets and we're back at the top-level rule set then // there were nested rule set(s). if (_parentRuleSet == null) { if (_expandedRuleSets.isNotEmpty) { // Remember ruleset to replace with these flattened rulesets. _expansions[node] = _expandedRuleSets; _expandedRuleSets = []; } assert(_flatDeclarationGroup == null); assert(_nestedSelectorGroup == null); } } /// Build up the list of all inherited sequences from the parent selector /// [node] is the current nested selector and it's parent is the last entry in /// the [_nestedSelectorGroup]. SelectorGroup _mergeToFlatten(RuleSet node) { // Create a new SelectorGroup for this nesting level. var nestedSelectors = _nestedSelectorGroup!.selectors; var selectors = node.selectorGroup!.selectors; // Create a merged set of previous parent selectors and current selectors. var newSelectors = []; for (var selector in selectors) { for (var nestedSelector in nestedSelectors) { var seq = _mergeNestedSelector(nestedSelector.simpleSelectorSequences, selector.simpleSelectorSequences); newSelectors.add(Selector(seq, node.span)); } } return SelectorGroup(newSelectors, node.span); } /// Merge the nested selector sequences [current] to the [parent] sequences or /// substitute any & with the parent selector. List _mergeNestedSelector( List parent, List current) { // If any & operator then the parent selector will be substituted otherwise // the parent selector is pre-pended to the current selector. var hasThis = current.any((s) => s.simpleSelector.isThis); var newSequence = []; if (!hasThis) { // If no & in the sector group then prefix with the parent selector. newSequence.addAll(parent); newSequence.addAll(_convertToDescendentSequence(current)); } else { for (var sequence in current) { if (sequence.simpleSelector.isThis) { // Substitute the & with the parent selector and only use a combinator // descendant if & is prefix by a sequence with an empty name e.g., // "... + &", "&", "... ~ &", etc. var hasPrefix = newSequence.isNotEmpty && newSequence.last.simpleSelector.name.isNotEmpty; newSequence.addAll( hasPrefix ? _convertToDescendentSequence(parent) : parent); } else { newSequence.add(sequence); } } } return newSequence; } /// Return selector sequences with first sequence combinator being a /// descendant. Used for nested selectors when the parent selector needs to /// be prefixed to a nested selector or to substitute the this (&) with the /// parent selector. List _convertToDescendentSequence( List sequences) { if (sequences.isEmpty) return sequences; var newSequences = []; var first = sequences.first; newSequences.add(SimpleSelectorSequence( first.simpleSelector, first.span, TokenKind.COMBINATOR_DESCENDANT)); newSequences.addAll(sequences.skip(1)); return newSequences; } @override void visitDeclarationGroup(DeclarationGroup node) { var span = node.span; var currentGroup = DeclarationGroup([], span); var oldGroup = _flatDeclarationGroup; _flatDeclarationGroup = currentGroup; var expandedLength = _expandedRuleSets.length; super.visitDeclarationGroup(node); // We're done with the group. _flatDeclarationGroup = oldGroup; // No nested rule to process it's a top-level rule. if (_nestedSelectorGroup == _topLevelSelectorGroup) return; // If flatten selector's declaration is empty skip this selector, no need // to emit an empty nested selector. if (currentGroup.declarations.isEmpty) return; var selectorGroup = _nestedSelectorGroup; // Build new rule set from the nested selectors and declarations. var newRuleSet = RuleSet(selectorGroup, currentGroup, span); // Place in order so outer-most rule is first. if (expandedLength == _expandedRuleSets.length) { _expandedRuleSets.add(newRuleSet); } else { _expandedRuleSets.insert(expandedLength, newRuleSet); } } // Record all declarations in a nested selector (Declaration, VarDefinition // and MarginGroup) but not the nested rule in the Declaration. @override void visitDeclaration(Declaration node) { if (_parentRuleSet != null) { _flatDeclarationGroup!.declarations.add(node); } super.visitDeclaration(node); } @override void visitVarDefinition(VarDefinition node) { if (_parentRuleSet != null) { _flatDeclarationGroup!.declarations.add(node); } super.visitVarDefinition(node); } @override void visitExtendDeclaration(ExtendDeclaration node) { if (_parentRuleSet != null) { _flatDeclarationGroup!.declarations.add(node); } super.visitExtendDeclaration(node); } @override void visitMarginGroup(MarginGroup node) { if (_parentRuleSet != null) { _flatDeclarationGroup!.declarations.add(node); } super.visitMarginGroup(node); } /// Replace the rule set that contains nested rules with the flatten rule /// sets. void flatten(StyleSheet styleSheet) { // TODO(terry): Iterate over topLevels instead of _expansions it's already // a map (this maybe quadratic). _expansions.forEach((RuleSet ruleSet, List newRules) { var index = styleSheet.topLevels.indexOf(ruleSet); if (index == -1) { // Check any @media directives for nested rules and replace them. var found = _MediaRulesReplacer.replace(styleSheet, ruleSet, newRules); assert(found); } else { styleSheet.topLevels.insertAll(index + 1, newRules); } }); _expansions.clear(); } } class _MediaRulesReplacer extends Visitor { final RuleSet _ruleSet; final List _newRules; bool _foundAndReplaced = false; /// Look for the [ruleSet] inside of an @media directive; if found then /// replace with the [newRules]. If [ruleSet] is found and replaced return /// true. static bool replace( StyleSheet styleSheet, RuleSet ruleSet, List newRules) { var visitor = _MediaRulesReplacer(ruleSet, newRules); visitor.visitStyleSheet(styleSheet); return visitor._foundAndReplaced; } _MediaRulesReplacer(this._ruleSet, this._newRules); @override void visitMediaDirective(MediaDirective node) { var index = node.rules.indexOf(_ruleSet); if (index != -1) { node.rules.insertAll(index + 1, _newRules); _foundAndReplaced = true; } } } /// Expand all @include at the top-level the ruleset(s) associated with the /// mixin. class TopLevelIncludes extends Visitor { StyleSheet? _styleSheet; final Messages _messages; /// Map of variable name key to it's definition. final map = {}; MixinDefinition? currDef; static void expand(Messages messages, List styleSheets) { TopLevelIncludes(messages, styleSheets); } bool _anyRulesets(MixinRulesetDirective def) => def.rulesets.any((rule) => rule is RuleSet); TopLevelIncludes(this._messages, List styleSheets) { for (var styleSheet in styleSheets) { visitTree(styleSheet); } } @override void visitStyleSheet(StyleSheet ss) { _styleSheet = ss; super.visitStyleSheet(ss); _styleSheet = null; } @override void visitIncludeDirective(IncludeDirective node) { final currDef = this.currDef; if (map.containsKey(node.name)) { var mixinDef = map[node.name]; if (mixinDef is MixinRulesetDirective) { _TopLevelIncludeReplacer.replace(_styleSheet!, node, mixinDef.rulesets); } else if (currDef is MixinRulesetDirective && _anyRulesets(currDef)) { final mixinRuleset = currDef; var index = mixinRuleset.rulesets.indexOf(node); mixinRuleset.rulesets.removeAt(index); _messages.warning( 'Using declaration mixin ${node.name} as top-level mixin', node.span); } } else { if (currDef is MixinRulesetDirective) { var rulesetDirect = currDef; rulesetDirect.rulesets.removeWhere((entry) { if (entry == node) { _messages.warning('Undefined mixin ${node.name}', node.span); return true; } return false; }); } } super.visitIncludeDirective(node); } @override void visitMixinRulesetDirective(MixinRulesetDirective node) { currDef = node; super.visitMixinRulesetDirective(node); // Replace with latest top-level mixin definition. map[node.name] = node; currDef = null; } @override void visitMixinDeclarationDirective(MixinDeclarationDirective node) { currDef = node; super.visitMixinDeclarationDirective(node); // Replace with latest mixin definition. map[node.name] = node; currDef = null; } } /// @include as a top-level with ruleset(s). class _TopLevelIncludeReplacer extends Visitor { final IncludeDirective _include; final List _newRules; /// Look for the [ruleSet] inside of an @media directive; if found then /// replace with the [newRules]. If [ruleSet] is found and replaced return /// true. static void replace(StyleSheet styleSheet, IncludeDirective include, List newRules) { _TopLevelIncludeReplacer(include, newRules).visitStyleSheet(styleSheet); } _TopLevelIncludeReplacer(this._include, this._newRules); @override void visitStyleSheet(StyleSheet node) { var index = node.topLevels.indexOf(_include); if (index != -1) { node.topLevels.insertAll(index + 1, _newRules); node.topLevels.replaceRange(index, index + 1, [NoOp()]); } super.visitStyleSheet(node); } @override void visitMixinRulesetDirective(MixinRulesetDirective node) { var index = node.rulesets.indexOf(_include); if (index != -1) { node.rulesets.insertAll(index + 1, _newRules); // Only the resolve the @include once. node.rulesets.replaceRange(index, index + 1, [NoOp()]); } super.visitMixinRulesetDirective(node); } } /// Utility function to match an include to a list of either Declarations or /// RuleSets, depending on type of mixin (ruleset or declaration). The include /// can be an include in a declaration or an include directive (top-level). int _findInclude(List list, TreeNode node) { final matchNode = (node is IncludeMixinAtDeclaration) ? node.include : node as IncludeDirective; var index = 0; for (var item in list) { var includeNode = (item is IncludeMixinAtDeclaration) ? item.include : item; if (includeNode == matchNode) return index; index++; } return -1; } /// Stamp out a mixin with the defined args substituted with the user's /// parameters. class CallMixin extends Visitor { final MixinDefinition mixinDef; List? _definedArgs; Expressions? _currExpressions; int _currIndex = -1; final varUsages = >>{}; /// Only var defs with more than one expression (comma separated). final Map? varDefs; CallMixin(this.mixinDef, [this.varDefs]) { if (mixinDef is MixinRulesetDirective) { visitMixinRulesetDirective(mixinDef as MixinRulesetDirective); } else { visitMixinDeclarationDirective(mixinDef as MixinDeclarationDirective); } } /// Given a mixin's defined arguments return a cloned mixin definition that /// has replaced all defined arguments with user's supplied VarUsages. MixinDefinition transform(List> callArgs) { // TODO(terry): Handle default arguments and varArgs. // Transform mixin with callArgs. for (var index = 0; index < _definedArgs!.length; index++) { var definedArg = _definedArgs![index]; VarDefinition? varDef; if (definedArg is VarDefinition) { varDef = definedArg; } else if (definedArg is VarDefinitionDirective) { var varDirective = definedArg; varDef = varDirective.def; } var callArg = callArgs[index]; // Is callArg a var definition with multi-args (expressions > 1). var defArgs = _varDefsAsCallArgs(callArg); if (defArgs.isNotEmpty) { // Replace call args with the var def parameters. callArgs.insertAll(index, defArgs); callArgs.removeAt(index + defArgs.length); callArg = callArgs[index]; } var expressions = varUsages[varDef!.definedName]; expressions!.forEach((k, v) { for (var usagesIndex in v) { k.expressions.replaceRange(usagesIndex, usagesIndex + 1, callArg); } }); } // Clone the mixin return mixinDef.clone(); } /// Rip apart var def with multiple parameters. List> _varDefsAsCallArgs(List callArg) { var defArgs = >[]; var firstCallArg = callArg[0]; if (firstCallArg is VarUsage) { var varDef = varDefs![firstCallArg.name]; var expressions = (varDef!.expression as Expressions).expressions; assert(expressions.length > 1); for (var expr in expressions) { if (expr is! OperatorComma) { defArgs.add([expr]); } } } return defArgs; } @override void visitExpressions(Expressions node) { var oldExpressions = _currExpressions; var oldIndex = _currIndex; _currExpressions = node; for (_currIndex = 0; _currIndex < node.expressions.length; _currIndex++) { node.expressions[_currIndex].visit(this); } _currIndex = oldIndex; _currExpressions = oldExpressions; } void _addExpression(Map> expressions) { var indexSet = {}; indexSet.add(_currIndex); expressions[_currExpressions!] = indexSet; } @override void visitVarUsage(VarUsage node) { assert(_currIndex != -1); assert(_currExpressions != null); if (varUsages.containsKey(node.name)) { var expressions = varUsages[node.name]; var allIndexes = expressions![_currExpressions]; if (allIndexes == null) { _addExpression(expressions); } else { allIndexes.add(_currIndex); } } else { var newExpressions = >{}; _addExpression(newExpressions); varUsages[node.name] = newExpressions; } super.visitVarUsage(node); } @override void visitMixinDeclarationDirective(MixinDeclarationDirective node) { _definedArgs = node.definedArgs; super.visitMixinDeclarationDirective(node); } @override void visitMixinRulesetDirective(MixinRulesetDirective node) { _definedArgs = node.definedArgs; super.visitMixinRulesetDirective(node); } } /// Expand all @include inside of a declaration associated with a mixin. class DeclarationIncludes extends Visitor { StyleSheet? _styleSheet; final Messages _messages; /// Map of variable name key to it's definition. final Map map = {}; /// Cache of mixin called with parameters. final Map callMap = {}; MixinDefinition? currDef; DeclarationGroup? currDeclGroup; /// Var definitions with more than 1 expression. final varDefs = {}; static void expand(Messages messages, List styleSheets) { DeclarationIncludes(messages, styleSheets); } DeclarationIncludes(this._messages, List styleSheets) { for (var styleSheet in styleSheets) { visitTree(styleSheet); } } bool _allIncludes(List rulesets) => rulesets.every((rule) => rule is IncludeDirective || rule is NoOp); CallMixin _createCallDeclMixin(MixinDefinition mixinDef) => callMap[mixinDef.name] ??= CallMixin(mixinDef, varDefs); @override void visitStyleSheet(StyleSheet ss) { _styleSheet = ss; super.visitStyleSheet(ss); _styleSheet = null; } @override void visitDeclarationGroup(DeclarationGroup node) { currDeclGroup = node; super.visitDeclarationGroup(node); currDeclGroup = null; } @override void visitIncludeMixinAtDeclaration(IncludeMixinAtDeclaration node) { if (map.containsKey(node.include.name)) { var mixinDef = map[node.include.name]; // Fix up any mixin that is really a Declaration but has includes. if (mixinDef is MixinRulesetDirective) { if (!_allIncludes(mixinDef.rulesets) && currDeclGroup != null) { var index = _findInclude(currDeclGroup!.declarations, node); if (index != -1) { currDeclGroup!.declarations .replaceRange(index, index + 1, [NoOp()]); } _messages.warning( 'Using top-level mixin ${node.include.name} as a declaration', node.span); } else { // We're a list of @include(s) inside of a mixin ruleset - convert // to a list of IncludeMixinAtDeclaration(s). var origRulesets = mixinDef.rulesets; var rulesets = []; if (origRulesets.every((ruleset) => ruleset is IncludeDirective)) { for (var ruleset in origRulesets) { rulesets.add(IncludeMixinAtDeclaration( ruleset as IncludeDirective, ruleset.span)); } _IncludeReplacer.replace(_styleSheet!, node, rulesets); } } } if (mixinDef!.definedArgs.isNotEmpty && node.include.args.isNotEmpty) { var callMixin = _createCallDeclMixin(mixinDef); mixinDef = callMixin.transform(node.include.args); } if (mixinDef is MixinDeclarationDirective) { _IncludeReplacer.replace( _styleSheet!, node, mixinDef.declarations.declarations); } } else { _messages.warning('Undefined mixin ${node.include.name}', node.span); } super.visitIncludeMixinAtDeclaration(node); } @override void visitIncludeDirective(IncludeDirective node) { if (map.containsKey(node.name)) { var mixinDef = map[node.name]; if (currDef is MixinDeclarationDirective && mixinDef is MixinDeclarationDirective) { _IncludeReplacer.replace( _styleSheet!, node, mixinDef.declarations.declarations); } else if (currDef is MixinDeclarationDirective) { var decls = (currDef as MixinDeclarationDirective).declarations.declarations; var index = _findInclude(decls, node); if (index != -1) { decls.replaceRange(index, index + 1, [NoOp()]); } } } super.visitIncludeDirective(node); } @override void visitMixinRulesetDirective(MixinRulesetDirective node) { currDef = node; super.visitMixinRulesetDirective(node); // Replace with latest top-level mixin definition. map[node.name] = node; currDef = null; } @override void visitMixinDeclarationDirective(MixinDeclarationDirective node) { currDef = node; super.visitMixinDeclarationDirective(node); // Replace with latest mixin definition. map[node.name] = node; currDef = null; } @override void visitVarDefinition(VarDefinition node) { // Only record var definitions that have multiple expressions (comma // separated for mixin parameter substitution. var exprs = (node.expression as Expressions).expressions; if (exprs.length > 1) { varDefs[node.definedName] = node; } super.visitVarDefinition(node); } @override void visitVarDefinitionDirective(VarDefinitionDirective node) { visitVarDefinition(node.def); } } /// @include as a top-level with ruleset(s). class _IncludeReplacer extends Visitor { final TreeNode _include; final List _newDeclarations; /// Look for the [ruleSet] inside of a @media directive; if found then replace /// with the [newRules]. static void replace( StyleSheet ss, TreeNode include, List newDeclarations) { var visitor = _IncludeReplacer(include, newDeclarations); visitor.visitStyleSheet(ss); } _IncludeReplacer(this._include, this._newDeclarations); @override void visitDeclarationGroup(DeclarationGroup node) { var index = _findInclude(node.declarations, _include); if (index != -1) { node.declarations.insertAll(index + 1, _newDeclarations); // Change @include to NoOp so it's processed only once. node.declarations.replaceRange(index, index + 1, [NoOp()]); } super.visitDeclarationGroup(node); } } /// Remove all @mixin and @include and any NoOp used as placeholder for /// @include. class MixinsAndIncludes extends Visitor { static void remove(StyleSheet styleSheet) { MixinsAndIncludes().visitStyleSheet(styleSheet); } bool _nodesToRemove(Object node) => node is IncludeDirective || node is MixinDefinition || node is NoOp; @override void visitStyleSheet(StyleSheet ss) { var index = ss.topLevels.length; while (--index >= 0) { if (_nodesToRemove(ss.topLevels[index])) { ss.topLevels.removeAt(index); } } super.visitStyleSheet(ss); } @override void visitDeclarationGroup(DeclarationGroup node) { var index = node.declarations.length; while (--index >= 0) { if (_nodesToRemove(node.declarations[index])) { node.declarations.removeAt(index); } } super.visitDeclarationGroup(node); } } /// Find all @extend to create inheritance. class AllExtends extends Visitor { final inherits = >{}; SelectorGroup? _currSelectorGroup; int? _currDeclIndex; final _extendsToRemove = []; @override void visitRuleSet(RuleSet node) { var oldSelectorGroup = _currSelectorGroup; _currSelectorGroup = node.selectorGroup; super.visitRuleSet(node); _currSelectorGroup = oldSelectorGroup; } @override void visitExtendDeclaration(ExtendDeclaration node) { var inheritName = ''; for (var selector in node.selectors) { inheritName += selector.toString(); } if (inherits.containsKey(inheritName)) { inherits[inheritName]!.add(_currSelectorGroup!); } else { inherits[inheritName] = [_currSelectorGroup!]; } // Remove this @extend _extendsToRemove.add(_currDeclIndex!); super.visitExtendDeclaration(node); } @override void visitDeclarationGroup(DeclarationGroup node) { var oldDeclIndex = _currDeclIndex; var decls = node.declarations; for (_currDeclIndex = 0; _currDeclIndex! < decls.length; _currDeclIndex = _currDeclIndex! + 1) { decls[_currDeclIndex!].visit(this); } if (_extendsToRemove.isNotEmpty) { var removeTotal = _extendsToRemove.length - 1; for (var index = removeTotal; index >= 0; index--) { decls.removeAt(_extendsToRemove[index]); } _extendsToRemove.clear(); } _currDeclIndex = oldDeclIndex; } } // TODO(terry): Need to handle merging selector sequences // TODO(terry): Need to handle @extend-Only selectors. // TODO(terry): Need to handle !optional glag. /// Changes any selector that matches @extend. class InheritExtends extends Visitor { final AllExtends _allExtends; InheritExtends(Messages messages, this._allExtends); @override void visitSelectorGroup(SelectorGroup node) { for (var selectorsIndex = 0; selectorsIndex < node.selectors.length; selectorsIndex++) { var selectors = node.selectors[selectorsIndex]; var isLastNone = false; var selectorName = ''; for (var index = 0; index < selectors.simpleSelectorSequences.length; index++) { var simpleSeq = selectors.simpleSelectorSequences[index]; var namePart = simpleSeq.simpleSelector.toString(); selectorName = isLastNone ? (selectorName + namePart) : namePart; var matches = _allExtends.inherits[selectorName]; if (matches != null) { for (var match in matches) { // Create a new group. var newSelectors = selectors.clone(); var newSeq = match.selectors[0].clone(); if (isLastNone) { // Add the inherited selector. node.selectors.add(newSeq); } else { // Replace the selector sequence to the left of the pseudo class // or pseudo element. // Make new selector seq combinator the same as the original. var orgCombinator = newSelectors.simpleSelectorSequences[index].combinator; newSeq.simpleSelectorSequences[0].combinator = orgCombinator; newSelectors.simpleSelectorSequences.replaceRange( index, index + 1, newSeq.simpleSelectorSequences); node.selectors.add(newSelectors); } isLastNone = false; } } else { isLastNone = simpleSeq.isCombinatorNone; } } } super.visitSelectorGroup(node); } }