/* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.resources.gss; import com.google.gwt.thirdparty.common.css.SourceCodeLocation; import com.google.gwt.thirdparty.common.css.compiler.ast.CssClassSelectorNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssCompilerPass; import com.google.gwt.thirdparty.common.css.compiler.ast.CssCompositeValueNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssLiteralNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssStringNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssUnknownAtRuleNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssValueNode; import com.google.gwt.thirdparty.common.css.compiler.ast.DefaultTreeVisitor; import com.google.gwt.thirdparty.common.css.compiler.ast.ErrorManager; import com.google.gwt.thirdparty.common.css.compiler.ast.GssError; import com.google.gwt.thirdparty.common.css.compiler.ast.MutatingVisitController; import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet; import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet.Builder; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; /** * Compiler pass that collects external styles declared with the {@code @external} at-rule. * * <p>This pass also removes the {@code @external} nodes from the AST. */ public class ExternalClassesCollector extends DefaultTreeVisitor implements CssCompilerPass { public static final String EXTERNAL_AT_RULE = "external"; private static final String STAR_SUFFIX = "*"; private final MutatingVisitController visitController; private final ErrorManager errorManager; private Set<String> externalClassNames; private Set<String> remainingStyleClassNames; private List<String> externalClassPrefixes; private boolean matchAll; public ExternalClassesCollector(MutatingVisitController visitController, ErrorManager errorManager) { this.visitController = visitController; this.errorManager = errorManager; } @Override public void runPass() { externalClassNames = new HashSet<String>(); remainingStyleClassNames = new HashSet<String>(); externalClassPrefixes = new ArrayList<String>(); visitController.startVisit(this); } @Override public boolean enterClassSelector(CssClassSelectorNode classSelector) { remainingStyleClassNames.add(classSelector.getRefinerName()); return true; } @Override public void leaveUnknownAtRule(CssUnknownAtRuleNode node) { if (EXTERNAL_AT_RULE.equals(node.getName().getValue())) { if (!matchAll) { processParameters(node.getParameters(), node.getSourceCodeLocation()); } visitController.removeCurrentNode(); } } /** * Returns an immutable set of external class names that should not be renamed. * The returned set contains all complete class names defined with * {@code @external} as well as all of the class names from * {@code styleClassesSet} that match prefixes defined with {@code @external}. * * <p>The set will contain also the class names that are not in the AST anymore (defined in a * conditional node that has been evaluated to false) and are not associated to a java method. * That doesn't make sense to rename these class names because they are not in the final css * and javascript. Moreover we handle the case where an {@code @external} related to these * style classes has been removed from the AST (because it was also defined in a conditional * node evaluated to false) and the compiler doesn't have to throw and error for this case. * <pre> * /{@literal *} conditional node evaluated to false at compile time {@literal *}/ * @if (is("property", "true")) { * @external foo; * .foo { * width: 100%; * } * } * </pre> * * @param styleClassesSet a set of class names that should be filtered to * return those matching external prefixes. Note that the passed-in set is not * modified. * @param orphanClassName a set of class names that aren't associated to a java method of the * CssResource interface. * @return an immutable set of class names. Note that the returned names are * not prefixed with "."; they are the raw name. */ public ImmutableSet<String> getExternalClassNames(Set<String> styleClassesSet, Set<String> orphanClassName) { if (matchAll) { return ImmutableSet.copyOf(styleClassesSet); } SortedSet<String> classNames = new TreeSet<String>(styleClassesSet); Builder<String> externalClassesSetBuilder = ImmutableSet.builder(); externalClassesSetBuilder.addAll(externalClassNames); for (String prefix : externalClassPrefixes) { for (String styleClass : classNames.tailSet(prefix)) { if (styleClass.startsWith(prefix)) { externalClassesSetBuilder.add(styleClass); } else { break; } } } // all style classes that are not in the AST anymore (mean they were part of a conditional // node that has been evaluated to false) and that aren't associated to a method should be // considered as external. See javadoc above for (String className : orphanClassName) { if (!remainingStyleClassNames.contains(className)) { externalClassesSetBuilder.add(className); } } return externalClassesSetBuilder.build(); } private void processParameters(List<CssValueNode> values, SourceCodeLocation sourceCodeLocation) { for (CssValueNode value : values) { if (value instanceof CssCompositeValueNode) { processParameters(((CssCompositeValueNode) value).getValues(), sourceCodeLocation); } else if (value instanceof CssStringNode) { String selector = ((CssStringNode) value).getConcreteValue(); if (STAR_SUFFIX.equals(selector)) { matchAll = true; return; } else if (selector.endsWith(STAR_SUFFIX)) { externalClassPrefixes.add(selector.substring(0, selector.length() - 1)); } else { externalClassNames.add(selector); } } else if (value instanceof CssLiteralNode) { externalClassNames.add(value.getValue()); } else { errorManager.report(new GssError("External at-rule invalid. The following terms is not " + "accepted in an external at-rule [" + value.getValue() + "]", sourceCodeLocation)); } } } }