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

com.google.common.css.compiler.passes.BiDiFlipper 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.

There is a newer version: 1.8.0
Show newest version
/*
 * Copyright 2009 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.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.css.compiler.ast.*;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;

/**
 * Compiler pass that BiDi flips all the flippable nodes.
 * TODO(user): Need to add a function to return tree before flipping.
 *
 * @author [email protected] (Roozbeh Pournader)
 */
public class BiDiFlipper extends DefaultTreeVisitor implements CssCompilerPass {

    private static final DecimalFormat PERCENT_FORMATTER =
            new DecimalFormat("#.########", DecimalFormatSymbols.getInstance(Locale.US));
    private final MutatingVisitController visitController;

    private final boolean shouldSwapLeftRightInUrl;
    private final boolean shouldSwapLtrRtlInUrl;
    private final boolean shouldFlipConstantReferences;

    public BiDiFlipper(MutatingVisitController visitController,
                       boolean swapLtrRtlInUrl,
                       boolean swapLeftRightInUrl,
                       boolean shouldFlipConstantReferences) {
        this.visitController = visitController;
        this.shouldSwapLtrRtlInUrl = swapLtrRtlInUrl;
        this.shouldSwapLeftRightInUrl = swapLeftRightInUrl;
        this.shouldFlipConstantReferences = shouldFlipConstantReferences;
    }

    public BiDiFlipper(MutatingVisitController visitController,
                       boolean swapLtrRtlInUrl,
                       boolean swapLeftRightInUrl) {
        this(visitController,
                swapLtrRtlInUrl,
                swapLeftRightInUrl,
                false /* Don't flip constant reference by default. */);
    }

    public static final String RIGHT = "right";
    /**
     * Map with exact strings to match and their corresponding flipped value. For example, in "float:
     * left" we need an exact match to flip "left" because we don't want to touch things like
     * "background: left.png".
     */
    private static final ImmutableMap EXACT_MATCHING_FOR_FLIPPING =
            new ImmutableMap.Builder()
                    .put("ltr", "rtl")
                    .put("rtl", "ltr")
                    .put("left", RIGHT)
                    .put(RIGHT, "left")
                    .put("e-resize", "w-resize")
                    .put("w-resize", "e-resize")
                    .put("ne-resize", "nw-resize")
                    .put("nw-resize", "ne-resize")
                    .put("nesw-resize", "nwse-resize")
                    .put("nwse-resize", "nesw-resize")
                    .put("se-resize", "sw-resize")
                    .put("sw-resize", "se-resize")
                    .build();

    /**
     * Map with the "ends-with" substrings that can be flipped and their corresponding flipped value.
     * For example, for
     *
     * 

padding-right: 2px * *

we need to match that the property name ends with "-right". */ private static final ImmutableMap ENDS_WITH_MATCHING_FOR_FLIPPING = new ImmutableMap.Builder() .put("-left", "-right") .put("-right", "-left") .put("-bottomleft", "-bottomright") .put("-topleft", "-topright") .put("-bottomright", "-bottomleft") .put("-topright", "-topleft") .build(); /** * Map with the "contains" substrings that can be flipped and their corresponding flipped value. * For example, for * *

border-right-width: 2px * *

we need to match that the property name contains "-right-". */ private static final ImmutableMap CONTAINS_MATCHING_FOR_FLIPPING = new ImmutableMap.Builder() .put("-left-", "-right-") .put("-right-", "-left-") .build(); /** * Set of properties that have flippable percentage values. */ private static final ImmutableSet PROPERTIES_WITH_FLIPPABLE_PERCENTAGE = ImmutableSet.of( "background", "background-position", "background-position-x", "-ms-background-position-x"); /* * Set of properties that are equivalent to border-radius. * TODO(roozbeh): Replace the explicit listing of prefixes with a general * pattern of "-[a-z]+-" to avoid maintaining a prefix list. */ public static final ImmutableSet BORDER_RADIUS_PROPERTIES = ImmutableSet.of( "border-radius", "-webkit-border-radius", "-moz-border-radius"); /** * Set of properties whose property values may flip if they match the four-part pattern. */ public static final ImmutableSet FOUR_PART_PROPERTIES_THAT_SHOULD_FLIP = ImmutableSet.of("border-color", "border-style", "border-width", "margin", "padding"); /** * Map with the patterns to match URLs against if swap_ltr_rtl_in_url flag is true, and their * replacement string. Only the first occurrence of the pattern is flipped. This would match "ltr" * and "rtl" if they occur as a word inside the path specified by the url. For example, for * *

background: url(/foo/rtl/bkg.gif) * *

the flipped value would be * *

background: url(/foo/ltr/bkg.gif) * *

whereas for * *

background: url(/foo/bkg-ltr.gif) * *

the flipped value would be * *

background: url(/foo/bkg-rtl.gif) * *

*/ private static final ImmutableMap URL_LTRTL_PATTERN_FOR_FLIPPING = new ImmutableMap.Builder() .put(Pattern.compile("(?background: url(/foo/right/bkg.gif) * *

the flipped value would be * *

background: url(/foo/left/bkg.gif) * *

whereas for * *

background: url(/foo/bkg-left.gif) * *

the flipped value would be * *

background: url(/foo/bkg-right.gif) * *

*/ private static final ImmutableMap URL_LEFTRIGHT_PATTERN_FOR_FLIPPING = new ImmutableMap.Builder() .put(Pattern.compile("(?Assumes simpler CSS 2.1 use of background and background-position (multi-layer is not * supported yet, neither is the extended CSS 3 syntax for positioning, like "right 10% top 20%"). * TODO(roozbeh): add support CSS 3 multi-layer backgrounds. TODO(roozbeh): add support for * extended CSS 3 syntax for positioning. */ private static boolean isValidForPercentageFlipping( CssPropertyNode propertyNode, CssPropertyValueNode propertyValueNode, int valueIndex) { String propertyName = propertyNode.getPropertyName(); if (PROPERTIES_WITH_FLIPPABLE_PERCENTAGE.contains(propertyName)) { if (valueIndex == 0) { return true; // If this is the first value, it's always flippable } if ("background".equals(propertyName)) { // Make sure this is not the vertical position: Only flip if the // previous value is not numeric or "left", "center", or "right". CssValueNode previousValueNode = propertyValueNode.getChildAt(valueIndex - 1); return !(previousValueNode instanceof CssNumericNode) && !isLeftOrCenterOrRight(previousValueNode.getValue()); } } return false; } /** * Sets the percentage to flipped value(100 - 'old value'), if the node is * valid numeric node with percentage. */ private CssValueNode flipPercentageValueNode(CssValueNode valueNode) { if (!isNumericAndHasPercentage(valueNode)) { return valueNode; } CssNumericNode numericNode = (CssNumericNode) valueNode; String oldPercentageValue = numericNode.getNumericPart(); return new CssNumericNode(flipPercentageValue(oldPercentageValue), "%"); } /** * Returns a formatted string representing 100% - value. Neither the input nor the output contains * a {@code %}. */ public static String flipPercentageValue(String value) { double newValue = 100 - Double.parseDouble(value); return PERCENT_FORMATTER.format(newValue); } /** * Flips corners of a border-radius property. Corners are reordered in the following way: * *

    *
  • 0 1 is replaced with 1 0, *
  • 0 1 2 is replaced with 1 0 1 2, and *
  • 0 1 2 3 is replaced with 1 0 3 2. *
* *

Lists of other lengths are returned unchanged. * * @param valueNodes the list of values representing the corners of a border-radius property. * @return a list of values with the corners flipped. */ private static List flipCorners(List valueNodes) { switch (valueNodes.size()) { case 2: { List flipped = new ArrayList<>(2); flipped.add(valueNodes.get(1)); flipped.add(valueNodes.get(0)); return flipped; } case 3: { List flipped = new ArrayList<>(4); flipped.add(valueNodes.get(1)); flipped.add(valueNodes.get(0)); flipped.add(valueNodes.get(1).deepCopy()); flipped.add(valueNodes.get(2)); return flipped; } case 4: { List flipped = new ArrayList<>(4); flipped.add(valueNodes.get(1)); flipped.add(valueNodes.get(0)); flipped.add(valueNodes.get(3)); flipped.add(valueNodes.get(2)); return flipped; } default: return valueNodes; } } /** * Takes a list of property values that belong to a border-radius property and flips them. If * there is a slash in the values, the data is divided around the slash. Then for each section, * flipCorners is called. */ private static List flipBorderRadius(List valueNodes) { int count = 0; int slashLocation = -1; CssCompositeValueNode slashNode = null; for (CssValueNode valueNode : valueNodes) { if (isSlashNode(valueNode)) { slashLocation = count; slashNode = (CssCompositeValueNode) valueNode; break; } ++count; } if (slashLocation == -1) { // No slash found, just one set of values return flipCorners(valueNodes); } // The parser treats slashes as combinging the two values around the slash // into one composite value node. This is not really the correct semantics // for the border-radius properties, as the parser will treat // "border-radius: 1px 2px / 5px 6px" as having three value nodes: the first // one will be "1px", the second one the composite value "2px / 5px", // and the third one "6px". We work in this unfortunate parser model here, // first deconstructing and later reconstructing that tree. List slashNodeValues = slashNode.getValues(); // Create a list of horizontal values and flip them List horizontalValues = new ArrayList<>(valueNodes.subList(0, slashLocation)); horizontalValues.add(slashNodeValues.get(0)); List newHorizontalValues = flipCorners(horizontalValues); // Do the same for vertical values List verticalValues = new ArrayList<>(); verticalValues.add(slashNodeValues.get(1)); verticalValues.addAll(valueNodes.subList(slashLocation + 1, valueNodes.size())); List newVerticalValues = flipCorners(verticalValues); // Create a new slash node List newSlashNodeValues = new ArrayList<>(); newSlashNodeValues.add(newHorizontalValues.get( newHorizontalValues.size() - 1)); newSlashNodeValues.add(newVerticalValues.get(0)); CssCompositeValueNode newSlashNode = new CssCompositeValueNode( newSlashNodeValues, CssCompositeValueNode.Operator.SLASH, null ); List newValueList = new ArrayList<>(newHorizontalValues.subList(0, newHorizontalValues.size() - 1)); newValueList.add(newSlashNode); newValueList.addAll(newVerticalValues.subList(1, newVerticalValues.size())); return newValueList; } /** * Takes the list of property values, validate them, then swap the second and last values. So that * 0 1 2 3 becomes 0 3 2 1. * *

That is unless the length of the list is not four, it belongs to a property that shouldn't * be flipped, or it's border-radius, where it will be specially handled. * *

TODO(roozbeh): Add explicit flipping for 'border-image*' and '*-shadow' properties. */ private List flipNumericValues(List valueNodes, String propertyName) { if (BORDER_RADIUS_PROPERTIES.contains(propertyName)) { return flipBorderRadius(valueNodes); } else if (valueNodes.size() != 4 || !FOUR_PART_PROPERTIES_THAT_SHOULD_FLIP.contains(propertyName)) { return valueNodes; } int count = 0; CssValueNode secondValueNode = null; CssValueNode fourthValueNode = null; for (CssValueNode valueNode : valueNodes) { if ((valueNode instanceof CssNumericNode) || (valueNode instanceof CssLiteralNode) || (valueNode instanceof CssHexColorNode) || shouldFlipConstantReference(valueNode)) { switch (count) { case 3: fourthValueNode = valueNode.deepCopy(); break; case 1: secondValueNode = valueNode.deepCopy(); break; default: // fall out } } else { return valueNodes; } count++; } // Swap second and last in the new list. count = 0; List newValueList = new ArrayList<>(); for (CssValueNode valueNode : valueNodes) { if (1 == count) { newValueList.add(fourthValueNode); } else if (3 == count) { newValueList.add(secondValueNode); } else { newValueList.add(valueNode); } count++; } return newValueList; } /** * Performs appropriate replacements needed for BiDi flipping a literal value. */ public static String flipLiteralValue(String value) { if (EXACT_MATCHING_FOR_FLIPPING.containsKey(value)) { value = EXACT_MATCHING_FOR_FLIPPING.get(value); } for (String s : ENDS_WITH_MATCHING_FOR_FLIPPING.keySet()) { if (value.endsWith(s)) { value = value.replace(s, ENDS_WITH_MATCHING_FOR_FLIPPING.get(s)); break; } } for (String s : CONTAINS_MATCHING_FOR_FLIPPING.keySet()) { if (value.contains(s)) { value = value.replace(s, CONTAINS_MATCHING_FOR_FLIPPING.get(s)); break; } } return value; } /** * Returns flipped node after making appropriate replacements needed for BiDi flipping, if the * node is either a LiteralNode or PropertyNode. Eg: PropertyNode 'padding-right' would become * 'padding-left'. * *

Subclasses can override to provide custom flipping behavior. */ protected T flipNode(T tNode) { if (tNode instanceof CssLiteralNode) { CssLiteralNode literalNode = (CssLiteralNode) tNode; String oldValue = literalNode.getValue(); if (null == oldValue) { return tNode; } String flippedValue = flipLiteralValue(oldValue); if (flippedValue.equals(oldValue)) { return tNode; } // This is safe because of the instanceof check above. @SuppressWarnings("unchecked") T flippedLiteralNode = (T) new CssLiteralNode(flippedValue); return flippedLiteralNode; } else if (tNode instanceof CssPropertyNode) { CssPropertyNode propertyNode = (CssPropertyNode) tNode; String oldValue = propertyNode.getPropertyName(); if (null == oldValue) { return tNode; } String flippedValue = flipLiteralValue(oldValue); if (flippedValue.equals(oldValue)) { return tNode; } // This is safe because of the instanceof check above. @SuppressWarnings("unchecked") T flippedPropertyNode = (T) new CssPropertyNode(flippedValue); return flippedPropertyNode; } else { return tNode; } } /** * Performs appropriate replacements required for flipping url. */ private String flipUrlValue(String value) { if (null == value) { return null; } if (shouldSwapLtrRtlInUrl) { for (Pattern p : URL_LTRTL_PATTERN_FOR_FLIPPING.keySet()) { if (p.matcher(value).find()) { String s = URL_LTRTL_PATTERN_FOR_FLIPPING.get(p); value = p.matcher(value).replaceFirst(s); break; } } } if (shouldSwapLeftRightInUrl) { for (Pattern p : URL_LEFTRIGHT_PATTERN_FOR_FLIPPING.keySet()) { if (p.matcher(value).find()) { String s = URL_LEFTRIGHT_PATTERN_FOR_FLIPPING.get(p); value = p.matcher(value).replaceFirst(s); break; } } } return value; } /** * Return node with flipped url, if it is a 'CssFunctionNode' with * function 'URL'. */ private CssValueNode flipUrlNode(CssValueNode valueNode) { if (!((valueNode instanceof CssFunctionNode) && ("url".equals(((CssFunctionNode) valueNode).getFunctionName())))) { return valueNode; } // Get the url to be flipped. CssFunctionNode oldFunctionNode = (CssFunctionNode) valueNode; CssFunctionArgumentsNode functionArguments = oldFunctionNode.getArguments(); // Asserting if url function has more than one argument, which // is unusual. Preconditions.checkArgument((1 == functionArguments.numChildren()), "url function taking more than one argument"); CssValueNode oldArgument = functionArguments.getChildAt(0); String oldUrlValue = oldArgument.getValue(); // Get the flipped url. String newUrlValue = flipUrlValue(oldUrlValue); // Make a new FunctionNode out of flipped url argument. CssValueNode newArgument = oldArgument.deepCopy(); newArgument.setValue(newUrlValue); List newArgumentsList = new ArrayList<>(); newArgumentsList.add(newArgument); CssFunctionNode newFunctionNode = oldFunctionNode.deepCopy(); newFunctionNode.setArguments(new CssFunctionArgumentsNode(newArgumentsList)); return newFunctionNode; } @Override public boolean enterDeclaration(CssDeclarationNode declarationNode) { // Return if node is set to non-flippable. if (!declarationNode.getShouldBeFlipped()) { return true; } // Update the property name in the declaration. CssDeclarationNode newDeclarationNode = declarationNode.deepCopy(); CssPropertyNode propertyNode = declarationNode.getPropertyName(); newDeclarationNode.setPropertyName(flipNode(propertyNode)); // Update the property value. CssPropertyValueNode propertyValueNode = declarationNode.getPropertyValue(); List valueNodes = new ArrayList<>(); int valueIndex = 0; for (CssValueNode valueNode : propertyValueNode.childIterable()) { // Flip URL argument, if it is a valid url function. CssValueNode temp = flipUrlNode(valueNode); // Flip node value, if it is a property node or literal node with value // that required flipping. temp = flipNode(temp); // Flip node value, if it is numeric and has percentage that // needs flipping. if (isValidForPercentageFlipping(propertyNode, propertyValueNode, valueIndex)) { temp = flipPercentageValueNode(temp); } valueNodes.add(temp.deepCopy()); valueIndex++; } if (!valueNodes.isEmpty()) { CssValueNode priority = null; // Remove possible !important priority node. if (Iterables.getLast(valueNodes) instanceof CssPriorityNode) { priority = Iterables.getLast(valueNodes); valueNodes = valueNodes.subList(0, valueNodes.size() - 1); } List newValueList = flipNumericValues(valueNodes, propertyNode.getPropertyName()); // Re-add priority node if we removed it earlier. if (priority != null) { newValueList.add(priority); } newDeclarationNode.setPropertyValue(new CssPropertyValueNode(newValueList)); } else { newDeclarationNode.setPropertyValue(propertyValueNode.deepCopy()); } List replacementList = new ArrayList<>(); replacementList.add(newDeclarationNode); visitController.replaceCurrentBlockChildWith(replacementList, false); return true; } @Override public void runPass() { visitController.startVisit(this); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy