All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.common.css.compiler.passes.ReplaceMixins Maven / Gradle / Ivy

Go to download

Closure Stylesheets is an extension to CSS that adds variables, functions, conditionals, and mixins to standard CSS. The tool also supports minification, linting, RTL flipping, and CSS class renaming.

The newest version!
/*
 * Copyright 2011 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.common.css.compiler.passes;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.css.SourceCodeLocation;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssConstantReferenceNode;
import com.google.common.css.compiler.ast.CssDeclarationBlockNode;
import com.google.common.css.compiler.ast.CssDeclarationNode;
import com.google.common.css.compiler.ast.CssMixinDefinitionNode;
import com.google.common.css.compiler.ast.CssMixinNode;
import com.google.common.css.compiler.ast.CssNode;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.DefaultTreeVisitor;
import com.google.common.css.compiler.ast.ErrorManager;
import com.google.common.css.compiler.ast.GssError;
import com.google.common.css.compiler.ast.MutatingVisitController;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

/**
 * Compiler pass that replaces mixins with the corresponding mixin definitions
 * and replaces the variables in the definitions with the values given by the
 * mixin.
 *
 * 

In addition, this pass ensures that each mixin has a matching definition * and that the argument count of the use and the definition is equal. * *

{@link CollectMixinDefinitions} has to run before. * {@link ReplaceConstantReferences} has to run afterwards. * * @author [email protected] (Florian Benz) */ public class ReplaceMixins extends DefaultTreeVisitor implements CssCompilerPass { @VisibleForTesting static final String NO_MATCHING_MIXIN_DEFINITION_ERROR_MESSAGE = "The name of the mixin matches no mixin definition name"; @VisibleForTesting static final String ARGUMENT_MISMATCH_ERROR_MESSAGE = "The number of arguments of the mixin and the corresponding definition " + "are different"; @VisibleForTesting static final String CYCLE_ERROR_MESSAGE = "A nested mixin call produces a cycle"; @VisibleForTesting static final String NO_MATCHING_MIXIN_FOR_REF_ERROR_MESSAGE = "Internal compiler error: The current definition reference belongs to " + "a mixin but does not match the mixin on top of the stack"; private final MutatingVisitController visitController; private final ErrorManager errorManager; private final Map definitions; /** * the current call stack of the mixins */ private Stack currentMixinStack; public ReplaceMixins(MutatingVisitController visitController, ErrorManager errorManager, Map definitions) { this.visitController = visitController; this.errorManager = errorManager; this.definitions = definitions; this.currentMixinStack = new Stack(); } @Override public void leaveMixin(CssMixinNode node) { if (!currentMixinStack.empty()) { currentMixinStack.peek().decreaseDeclarationCount(); } // This pushes the mixin on the stack if the corresponding definition // contains at least one declaration. replaceMixin(node); // Goes up the stack if this is the last declaration inserted by a mixin. // This is done for the case where no mixin is added to the stack. while (!currentMixinStack.empty() && currentMixinStack.peek().isDeclarationCountZero()) { currentMixinStack.pop(); } } @Override public void leaveDeclaration(CssDeclarationNode node) { // Updates the stacks if the last declaration that was added by the last // mixin call is reached. if (currentMixinStack.empty()) { return; } // get the number of declarations left that were added by the last // mixin call currentMixinStack.peek().decreaseDeclarationCount(); // go up the stack if this is the last declaration inserted by a mixin while (!currentMixinStack.empty() && currentMixinStack.peek().isDeclarationCountZero()) { currentMixinStack.pop(); } } @Override public void leaveDeclarationBlock(CssDeclarationBlockNode node) { currentMixinStack.clear(); } /** * Replaces a variable inside the copy of the mixin definition with the * value given by the mixin. */ @Override public boolean enterValueNode(CssValueNode node) { return replaceReference(node, false /* isArgument */); } @Override public boolean enterArgumentNode(CssValueNode node) { return replaceReference(node, true /* isArgument */); } private boolean replaceReference(CssValueNode node, boolean isArgument) { if (!(node instanceof CssConstantReferenceNode)) { return true; } List values = getValuesForReference( (CssConstantReferenceNode) node, isArgument); if (values == null) { return true; } visitController.replaceCurrentBlockChildWith(values, false); return true; } /** * Replaces a mixin with the declarations of the corresponding definition. */ private void replaceMixin(CssMixinNode mixin) { if (containsCycle(mixin)) { return; } CssMixinDefinitionNode currentMixinDefinition = definitions.get(mixin.getDefinitionName()); if (currentMixinDefinition == null) { errorManager.report(new GssError( NO_MATCHING_MIXIN_DEFINITION_ERROR_MESSAGE, mixin.getSourceCodeLocation())); return; } // Adds deep copies of the declarations in the definition to the current // declaration block. The variables are visited and replaced afterwards. List mixinDecls = currentMixinDefinition.getBlock().deepCopy().getChildren(); visitController.replaceCurrentBlockChildWith(mixinDecls, /* visitTheReplacementNodes */ true); // Create a mapping so that references can easily be replaced by their // value. Map> refMap = createReferenceMapping(mixin, currentMixinDefinition); if (refMap == null) { visitController.stopVisit(); return; } if (mixinDecls.size() == 0) { return; } // Add the mixin and the number of declarations to the stack currentMixinStack.push(new StackFrame(mixin, mixinDecls.size(), refMap)); } /** * Returns the value of the given reference. The value is defined by the * mixin referring to the mixin definition the reference is in. */ private List getValuesForReference( CssConstantReferenceNode ref, boolean isArgument) { if (!(ref.getScope() instanceof CssMixinDefinitionNode)) { return null; } String defName = ref.getValue(); CssMixinDefinitionNode currentMixinDefinition = (CssMixinDefinitionNode) ref.getScope(); CssMixinNode currentMixin = currentMixinStack.peek().getMixin(); if (!currentMixin.getDefinitionName().equals( currentMixinDefinition.getDefinitionName())) { errorManager.report(new GssError( NO_MATCHING_MIXIN_FOR_REF_ERROR_MESSAGE, ref.getSourceCodeLocation())); return null; } List values = currentMixinStack.peek().getValuesForReference( ref.getValue()); Preconditions.checkNotNull(values); // Create deep copies because the values can be inserted in several places. ImmutableList.Builder builder = ImmutableList.builder(); for (CssValueNode val : values) { if (isArgument || !" ".equals(val.getValue())) { // Values only containing a whitespace are only added if they inside // the argument of a function as they are inserted by the parser to // separate values that are together seen as one argument. builder.add(val.deepCopy()); } } return builder.build(); } /** * Ensures that no cyclic mixin calls occur. */ private boolean containsCycle(CssMixinNode mixin) { for (StackFrame frame : currentMixinStack) { if (mixin.getDefinitionName().equals( frame.getMixin().getDefinitionName())) { errorManager.report(new GssError( CYCLE_ERROR_MESSAGE, frame.getMixin().getSourceCodeLocation())); return true; } } return false; } /** * Creates a mapping between argument names of the mixin definition and the * values that are provided by the mixin. */ private Map> createReferenceMapping( CssMixinNode mixin, CssMixinDefinitionNode def) { Map> refMap = Maps.newHashMap(); List currentValues = Lists.newArrayList(); Iterator definitionArgumentIterator = def.getArguments().getChildIterator(); // Collects all values up to a comma and then adds these values to the map. for (CssValueNode arg : mixin.getArguments().getChildren()) { if (",".equals(arg.getValue())) { if (!addValuesToMap(refMap, definitionArgumentIterator, currentValues, arg.getSourceCodeLocation())) { return null; } currentValues.clear(); } else { currentValues.add(arg); } } if (!currentValues.isEmpty()) { // Add values for last argument. if (!addValuesToMap(refMap, definitionArgumentIterator, currentValues, mixin.getSourceCodeLocation())) { return null; } } if (definitionArgumentIterator.hasNext()) { // The definition takes more arguments than the mixin provides. errorManager.report(new GssError( ARGUMENT_MISMATCH_ERROR_MESSAGE, mixin.getSourceCodeLocation())); return null; } return refMap; } /** * Adds the given values to the map that maps argument names to their values. */ private boolean addValuesToMap(Map> refMap, Iterator definitionArgumentIterator, List values, SourceCodeLocation location) { if (values.isEmpty() || !definitionArgumentIterator.hasNext()) { // There is no value between two commas or the mixin provides more // arguments than the definition takes. errorManager.report(new GssError( ARGUMENT_MISMATCH_ERROR_MESSAGE, location)); return false; } CssValueNode argument = definitionArgumentIterator.next(); // Commas are skipped. if (",".equals(argument.getValue())) { if (values.isEmpty() || !definitionArgumentIterator.hasNext()) { errorManager.report(new GssError( ARGUMENT_MISMATCH_ERROR_MESSAGE, location)); return false; } argument = definitionArgumentIterator.next(); } refMap.put(argument.getValue(), ImmutableList.copyOf(values)); return true; } @Override public void runPass() { visitController.startVisit(this); } /** * Helper class that is used to keep track of the called mixins inside of * other mixins. */ private static class StackFrame { private CssMixinNode mixin; private int declarationCount; private final Map> valueMap; StackFrame(CssMixinNode mixin, int declarationCount, Map> valueMap) { Preconditions.checkNotNull(mixin); Preconditions.checkArgument(declarationCount > 0); Preconditions.checkNotNull(valueMap); this.mixin = mixin; this.declarationCount = declarationCount; this.valueMap = valueMap; } CssMixinNode getMixin() { return mixin; } void decreaseDeclarationCount() { if (declarationCount > 0) { declarationCount--; } } boolean isDeclarationCountZero() { return this.declarationCount == 0; } List getValuesForReference(String refName) { return valueMap.get(refName); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy