/* * Copyright 2010 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.collect.Lists; import com.google.common.css.compiler.ast.CssCompilerPass; import com.google.common.css.compiler.ast.CssDeclarationNode; import com.google.common.css.compiler.ast.CssHexColorNode; import com.google.common.css.compiler.ast.CssLiteralNode; import com.google.common.css.compiler.ast.CssNode; import com.google.common.css.compiler.ast.CssNumericNode; import com.google.common.css.compiler.ast.CssPropertyValueNode; import com.google.common.css.compiler.ast.CssValueNode; import com.google.common.css.compiler.ast.DefaultTreeVisitor; import com.google.common.css.compiler.ast.MutatingVisitController; import com.google.common.css.compiler.ast.Property; import java.util.List; /** * Check shorthand rule declarations that define positional values, * such as padding and margin, and eliminate duplicate values if possible. * For example, "margin: 1px 2px 3px 2px" will be shortened to just * "margin: 1px 2px 3px", since the final 2px is redundant. * * Note: At present, this pass applies to border-width, but not border. * @see Property#hasPositionalParameters() */ public class AbbreviatePositionalValues extends DefaultTreeVisitor implements CssCompilerPass { private final MutatingVisitController visitController; public AbbreviatePositionalValues( MutatingVisitController visitController) { this.visitController = visitController; } @Override public boolean enterDeclaration(CssDeclarationNode declaration) { Property property = declaration.getPropertyName().getProperty(); if (property.hasPositionalParameters()) { CssPropertyValueNode valueNode = declaration.getPropertyValue(); List<CssValueNode> newValues = abbreviateValues(valueNode.getChildren()); if (newValues != null) { CssDeclarationNode newDeclaration = new CssDeclarationNode(declaration); CssPropertyValueNode newValuesNode = new CssPropertyValueNode(newValues); newDeclaration.setSourceCodeLocation(declaration.getSourceCodeLocation()); newValuesNode.setSourceCodeLocation(valueNode.getSourceCodeLocation()); newDeclaration.setPropertyValue(newValuesNode); List<CssNode> replacementList = Lists.newArrayList(); replacementList.add(newDeclaration); visitController.replaceCurrentBlockChildWith(replacementList, false); } } return true; } /** * Attempt to shorten a CSS positional list containing values for * -top, -right, -bottom, and -left properties of a shorthand rule. * Returns null if the list is not shortenable. * @param values List of values. * @return A shortened version of the input list, or null if not possible * to abbreviate. The returned list is a "shallow" copy. */ private List<CssValueNode> abbreviateValues(List<CssValueNode> values) { int numValues = values.size(); if (numValues <= 1 || numValues > 4) { // Already abbreviated, or unexpected input. return null; } List<CssValueNode> mutableList = Lists.newArrayList(values); if (numValues == 4) { // Compare foo-left to foo-right. if (equalValues(values.get(3), values.get(1))) { numValues--; mutableList.remove(3); } } if (numValues == 3) { // Compare foo-bottom to foo-top. if (equalValues(values.get(2), values.get(0))) { numValues--; mutableList.remove(2); } } if (numValues == 2) { // Compare foo-{right,left} to foo-{top,bottom}. if (equalValues(values.get(1), values.get(0))) { numValues--; mutableList.remove(1); } } return numValues != values.size() ? mutableList : null; } /** * Compare 2 value nodes to see if they represent the same value for the * purposes of this compiler pass. See {@link CssNode#equals} for an * explanation of why this is not defined elsewhere. * @param v1 First value node. * @param v2 Second value node. * @return Whether the values are equivalent. */ @VisibleForTesting static boolean equalValues(CssValueNode v1, CssValueNode v2) { if (v1.equals(v2)) { return true; } if (v1 instanceof CssNumericNode && v2 instanceof CssNumericNode) { CssNumericNode numeric1 = (CssNumericNode) v1; CssNumericNode numeric2 = (CssNumericNode) v2; return numeric1.getNumericPart().equals(numeric2.getNumericPart()) && numeric1.getUnit().equals(numeric2.getUnit()); } if (v1 instanceof CssLiteralNode && v2 instanceof CssLiteralNode) { return v1.getValue().equals(v2.getValue()); } if (v1 instanceof CssHexColorNode && v2 instanceof CssHexColorNode) { CssHexColorNode hex1 = (CssHexColorNode) v1; CssHexColorNode hex2 = (CssHexColorNode) v2; return hex1.toString().equals(hex2.toString()); } return false; } @Override public void runPass() { visitController.startVisit(this); } }