package com.github.sommeri.less4j.core.compiler.selectors; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import com.github.sommeri.less4j.core.ast.ASTCssNode; import com.github.sommeri.less4j.core.ast.ASTCssNode.Visibility; import com.github.sommeri.less4j.core.ast.ASTCssNodeType; import com.github.sommeri.less4j.core.ast.Extend; import com.github.sommeri.less4j.core.ast.MultiTargetExtend; import com.github.sommeri.less4j.core.ast.RuleSet; import com.github.sommeri.less4j.core.ast.Selector; import com.github.sommeri.less4j.core.compiler.stages.ASTManipulator; import com.github.sommeri.less4j.utils.ArraysUtils; import com.github.sommeri.less4j.utils.Couple; public class ExtendsSolver { private GeneralComparatorForExtend comparator = new GeneralComparatorForExtend(); private ASTManipulator manipulator = new ASTManipulator(); private List<RuleSet> allRulesets = new ArrayList<RuleSet>(); private List<Selector> inlineExtends = new ArrayList<Selector>(); private PerformedExtendsDB performedExtends = new PerformedExtendsDB(); public void solveExtends(ASTCssNode node) { collectRulesets(node); solveInlineExtends(); } private void solveInlineExtends() { for (Selector selector : inlineExtends) { solveInlineExtends(selector); } } private void solveInlineExtends(Selector extendingSelector) { // performance optimization - create the array only when it is needed // this came out of profiling and is worth keeping this way ArrayList<ExtendRefs> doExtends = null; for (RuleSet ruleSet : allRulesets) { for (Selector targetSelector : ruleSet.getSelectors()) { Selector newSelector = constructNewSelector(extendingSelector, targetSelector); if (newSelector != null && canExtend(extendingSelector, newSelector, ruleSet)) { if (doExtends == null) { doExtends = new ArrayList<ExtendRefs>(); } doExtends.add(new ExtendRefs(ruleSet, targetSelector, newSelector)); } } } if (doExtends != null) { for (ExtendRefs refs : doExtends) { doTheExtend(extendingSelector, refs.newSelector, refs.ruleSet, refs.targetSelector); } } } private void doTheExtend(Selector originalExtendingSelector, Selector addSelector, RuleSet target, Selector targetSelector) { addSelector(target, addSelector); performedExtends.register(targetSelector, originalExtendingSelector, addSelector.getVisibility()); // Following solves this situation: // ** extendingSelector: b // ** targetSelector: a // // a {} // c:extends(b) {} // b:extends(a) {} // // Since c extended b before. Therefore, when b is extending a, c needs to extend it too. // Collection<PerformedExtend> mayNeedToExtendTargetToo = performedExtends.getThoseWhoExtended(originalExtendingSelector); for (PerformedExtend info : mayNeedToExtendTargetToo) { Selector selector = info.getSelector(); if (canExtend(selector, target)) { Selector nextSelector = selector.clone(); manipulator.setTreeVisibility(nextSelector, info.getVisibility()); doTheExtend(selector, nextSelector, target, targetSelector); } } } private boolean canExtend(Selector extendingSelector, RuleSet targetRuleSet) { return canExtend(extendingSelector, extendingSelector, targetRuleSet); } private boolean canExtend(Selector extendingSelector, Selector newSelector, RuleSet targetRuleSet) { if (containsSelector(newSelector, targetRuleSet)) { return false; } // selectors are able to extend only rulesets inside the same @media body. return compatibleMediaLocation(extendingSelector, targetRuleSet); } private boolean compatibleMediaLocation(Selector extendingSelector, RuleSet targetRuleSet) { ASTCssNode grandParent = findOwnerNode(extendingSelector); if (grandParent == null || grandParent.getType() == ASTCssNodeType.STYLE_SHEET) return true; boolean result = grandParent == findOwnerNode(targetRuleSet); return result; } private boolean containsSelector(Selector extendingSelector, RuleSet targetRuleSet) { for (Selector selector : targetRuleSet.getSelectors()) { // if (comparator.contains(selector, extendingSelector)) if (comparator.equals(selector, extendingSelector)) return true; } return false; } private ASTCssNode findOwnerNode(ASTCssNode extendingSelector) { return manipulator.findParentOfType(extendingSelector, ASTCssNodeType.STYLE_SHEET, ASTCssNodeType.MEDIA); } private void addSelector(RuleSet ruleSet, Selector selector) { selector.setParent(ruleSet); ruleSet.addSelector(selector); if (selector.getVisibility() == Visibility.VISIBLE) { //visibility of children manipulator.setTreeVisibility(ruleSet.getBody(), Visibility.VISIBLE); ruleSet.setVisibility(Visibility.VISIBLE); //visibility of parents ASTCssNode node = ruleSet; while (node.hasParent()) { node = node.getParent(); switch (node.getVisibility()) { case DEFAULT: node.setVisibility(Visibility.VISIBLE); break; case VISIBLE: break; } } } } private Selector constructNewSelector(Selector extending, Selector possibleTarget) { if (possibleTarget == extending) return null; List<Extend> allExtends = extending.getExtend(); for (Extend extend : allExtends) { if (!extend.isAll() && comparator.equals(possibleTarget, extend.getTarget())) return setNewSelectorVisibility(extend, extending.clone()); if (extend.isAll()) { Selector addSelector = comparator.replaceInside(extend.getTarget(), possibleTarget, extend.getParentAsSelector()); if (addSelector != null) return setNewSelectorVisibility(extend, addSelector); } } return null; } private Selector setNewSelectorVisibility(Extend extend, Selector newSelector) { manipulator.setTreeVisibility(newSelector, extend.getVisibility()); return newSelector; } private void collectRulesets(ASTCssNode node) { switch (node.getType()) { case RULE_SET: { RuleSet ruleset = (RuleSet) node; allRulesets.add(ruleset); collectExtendingSelectors(ruleset); break; } default: List<? extends ASTCssNode> childs = new ArrayList<ASTCssNode>(node.getChilds()); for (ASTCssNode kid : childs) { collectRulesets(kid); } break; } } private void collectExtendingSelectors(RuleSet ruleset) { List<Extend> directExtends = collectDirectExtendDeclarations(ruleset); for (Selector selector : ruleset.getSelectors()) { addClones(selector, directExtends); if (selector.isExtending()) { inlineExtends.add(selector); } } } private void addClones(Selector selector, List<Extend> newExtends) { List<Extend> clones = ArraysUtils.deeplyClonedList(newExtends); selector.addExtends(clones); for (Extend extend : clones) { extend.setParent(selector); } } private List<Extend> collectDirectExtendDeclarations(RuleSet ruleset) { List<Extend> result = new ArrayList<Extend>(); List<ASTCssNode> members = new ArrayList<ASTCssNode>(ruleset.getBody().getMembers()); for (ASTCssNode node : members) { if (node.getType() == ASTCssNodeType.EXTEND) { Extend extend = (Extend) node; manipulator.removeFromBody(extend); result.add(extend); } else if (node.getType() == ASTCssNodeType.MULTI_TARGET_EXTEND) { MultiTargetExtend extend = (MultiTargetExtend) node; manipulator.removeFromBody(extend); result.addAll(extend.getAllExtends()); } } return result; } private static class ExtendRefs { ExtendRefs(RuleSet ruleSet, Selector targetSelector, Selector newSelector) { this.ruleSet = ruleSet; this.targetSelector = targetSelector; this.newSelector = newSelector; } RuleSet ruleSet; Selector targetSelector; Selector newSelector; @Override public String toString() { String visibility = "[" + ruleSet.getVisibility() + ", " + targetSelector.getVisibility() + ", "+newSelector.getVisibility() + "]"; String string = "ExtendRefs "+visibility+"[\n ruleSet=" + ruleSet + ",\n" + " targetSelector=" + targetSelector + ",\n" + " newSelector=" + newSelector + "\n]"; return string; } } private static class PerformedExtendsDB { private Map<Selector, List<PerformedExtend>> allSelectorExtends = new HashMap<Selector, List<PerformedExtend>>(); protected List<PerformedExtend> getThoseWhoExtended(Selector selector) { List<PerformedExtend> result = allSelectorExtends.get(selector); if (result == null) { result = new ArrayList<PerformedExtend>(); allSelectorExtends.put(selector, result); } return result; } protected void register(Selector targetSelector, Selector extendingSelector, Visibility extendingSelectorVisibility) { List<PerformedExtend> tied = getThoseWhoExtended(targetSelector); tied.add(new PerformedExtend(extendingSelector, extendingSelectorVisibility)); } } private static class PerformedExtend extends Couple<Selector, Visibility> { public PerformedExtend(Selector extendingSelector, Visibility extendingSelectorVisibility) { super(extendingSelector, extendingSelectorVisibility); } public Selector getSelector() { return getT(); } public Visibility getVisibility() { return getM(); } } }