com.google.common.css.compiler.passes.BiDiFlipper.java Source code

Java tutorial

Introduction

Here is the source code for com.google.common.css.compiler.passes.BiDiFlipper.java

Source

/*
 * 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.collect.Lists;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssCompositeValueNode;
import com.google.common.css.compiler.ast.CssConstantReferenceNode;
import com.google.common.css.compiler.ast.CssDeclarationNode;
import com.google.common.css.compiler.ast.CssFunctionArgumentsNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
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.CssPriorityNode;
import com.google.common.css.compiler.ast.CssPropertyNode;
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 java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
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 roozbeh@google.com (Roozbeh Pournader)
 */
public class BiDiFlipper extends DefaultTreeVisitor implements CssCompilerPass {

    private final DecimalFormat percentFormatter = new DecimalFormat("#.########");
    private final MutatingVisitController visitController;

    boolean shouldSwapLeftRightInUrl;
    boolean shouldSwapLtrRtlInUrl;
    boolean shouldFlipConstantReferences;

    private static final Logger logger = Logger.getLogger(BiDiFlipper.class.getName());

    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. */);
    }

    /**
     * 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 Map<String, String> EXACT_MATCHING_FOR_FLIPPING = new ImmutableMap.Builder<String, String>()
            .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
     * <p>
     *   padding-right: 2px
     * <p>
     * we need to match that the property name ends with "-right".
     */
    private static final Map<String, String> ENDS_WITH_MATCHING_FOR_FLIPPING = new ImmutableMap.Builder<String, String>()
            .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
     * <p>
     *   border-right-width: 2px
     * <p>
     * we need to match that the property name contains "-right-".
     */
    private static final Map<String, String> CONTAINS_MATCHING_FOR_FLIPPING = new ImmutableMap.Builder<String, String>()
            .put("-left-", "-right-").put("-right-", "-left-").build();

    /**
     * Set of properties that have flippable percentage values.
     */
    private static final Set<String> 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.
     */
    private static final Set<String> 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.
     */
    private static final Set<String> 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
     * <p>
     *   background: url(/foo/rtl/bkg.gif)
     * <p>
     * the flipped value would be
     * <p>
     *   background: url(/foo/ltr/bkg.gif)
     * <p>
     * whereas for
     * <p>
     *   background: url(/foo/bkg-ltr.gif)
     * <p>
     * the flipped value would be
     * <p>
     *   background: url(/foo/bkg-rtl.gif)
     * <p>
     */
    private static final Map<Pattern, String> URL_LTRTL_PATTERN_FOR_FLIPPING = new ImmutableMap.Builder<Pattern, String>()
            .put(Pattern.compile("(?<![a-zA-Z])([-_\\./]*)ltr([-_\\./]+)"), "$1rtl$2")
            .put(Pattern.compile("(?<![a-zA-Z])([-_\\./]*)rtl([-_\\./]+)"), "$1ltr$2").build();

    /**
     * Map with the patterns to match URLs against if swap_left_right_in_url flag
     * is true, and their replacement string. Only the first occurrence of the
     * pattern is flipped. This would match "left" and "right" if they occur as a
     * word inside the path specified by the url.
     * For example, for
     * <p>
     *   background: url(/foo/right/bkg.gif)
     * <p>
     * the flipped value would be
     * <p>
     *   background: url(/foo/left/bkg.gif)
     * <p>
     * whereas for
     * <p>
     *   background: url(/foo/bkg-left.gif)
     * <p>
     * the flipped value would be
     * <p>
     *   background: url(/foo/bkg-right.gif)
     * <p>
     */
    private static final Map<Pattern, String> URL_LEFTRIGHT_PATTERN_FOR_FLIPPING = new ImmutableMap.Builder<Pattern, String>()
            .put(Pattern.compile("(?<![a-zA-Z])([-_\\./]*)left([-_\\./]+)"), "$1right$2")
            .put(Pattern.compile("(?<![a-zA-Z])([-_\\./]*)right([-_\\./]+)"), "$1left$2").build();

    /**
     * Return if the string is "left" or "center" or "right".
     */
    private boolean isLeftOrCenterOrRight(String value) {
        return "left".equals(value) || "center".equals(value) || "right".equals(value);
    }

    /**
     * Return if the node is CssHexColorNode.
     */
    private boolean isCssHexColorNode(CssValueNode valueNode) {
        return (valueNode instanceof CssHexColorNode);
    }

    /**
     * Return if the node is CssLiteralNode.
     */
    private boolean isCssLiteralNode(CssValueNode valueNode) {
        return (valueNode instanceof CssLiteralNode);
    }

    /**
     * Return if the node is CssNumericNode.
     */
    private boolean isNumericNode(CssValueNode valueNode) {
        return (valueNode instanceof CssNumericNode);
    }

    /**
     * Return if the node is a slash operator node.
     */
    private boolean isSlashNode(CssValueNode valueNode) {
        if (valueNode instanceof CssCompositeValueNode) {
            CssCompositeValueNode compositeNode = (CssCompositeValueNode) valueNode;
            return compositeNode.getOperator() == CssCompositeValueNode.Operator.SLASH;
        }
        return false;
    }

    /**
     * Return if the node is ConstantReference and also flippable.
     */
    private boolean shouldFlipConstantReference(CssValueNode valueNode) {
        if (!shouldFlipConstantReferences) {
            return false;
        }
        if (!(valueNode instanceof CssConstantReferenceNode)) {
            return false;
        }
        String ref = valueNode.getValue();
        if (ref.startsWith(ResolveCustomFunctionNodesForChunks.DEF_PREFIX)) {
            // Since gss function could generate multiple values, we can't do flip if
            // there's gss function call in place, simply skip this case.
            return false;
        }
        return true;
    }

    /**
     * Return if the node is numeric and also has '%'.
     */
    private boolean isNumericAndHasPercentage(CssValueNode value) {
        if (!isNumericNode(value)) {
            return false;
        }
        CssNumericNode numericNode = (CssNumericNode) value;
        return "%".equals(numericNode.getUnit());
    }

    /**
     * Returns if the percentage value of this node is flippable.
     *
     * 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 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);
                if (!isNumericNode(previousValueNode) && !isLeftOrCenterOrRight(previousValueNode.getValue())) {
                    return true;
                }
            }
        }

        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();
        double newPercentValue = 100 - Double.parseDouble(oldPercentageValue);
        CssValueNode newNumericNode = new CssNumericNode(percentFormatter.format(newPercentValue), "%");

        return newNumericNode;
    }

    /**
     * Flips corners of a border-radius property. Corners are reordered in the
     * following way:
     * <ul>
     *   <li>0 1 is replaced with 1 0,
     *   <li> 0 1 2 is replaced with 1 0 1 2, and
     *   <li> 0 1 2 3 is replaced with 1 0 3 2.
     * </ul>
     *
     * <p>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 List<CssValueNode> flipCorners(List<CssValueNode> valueNodes) {
        switch (valueNodes.size()) {
        case 2:
            return Lists.newArrayList(valueNodes.get(1), valueNodes.get(0));
        case 3:
            return Lists.newArrayList(valueNodes.get(1), valueNodes.get(0), valueNodes.get(1).deepCopy(),
                    valueNodes.get(2));
        case 4:
            return Lists.newArrayList(valueNodes.get(1), valueNodes.get(0), valueNodes.get(3), valueNodes.get(2));
        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 List<CssValueNode> flipBorderRadius(List<CssValueNode> valueNodes) {

        int count = 0, 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<CssValueNode> slashNodeValues = slashNode.getValues();

        // Create a list of horizontal values and flip them
        List<CssValueNode> horizontalValues = Lists.newArrayList();
        horizontalValues.addAll(valueNodes.subList(0, slashLocation));
        horizontalValues.add(slashNodeValues.get(0));
        List<CssValueNode> newHorizontalValues = flipCorners(horizontalValues);

        // Do the same for vertical values
        List<CssValueNode> verticalValues = Lists.newArrayList();
        verticalValues.add(slashNodeValues.get(1));
        verticalValues.addAll(valueNodes.subList(slashLocation + 1, valueNodes.size()));
        List<CssValueNode> newVerticalValues = flipCorners(verticalValues);

        // Create a new slash node
        List<CssValueNode> newSlashNodeValues = Lists.newArrayList();
        newSlashNodeValues.add(newHorizontalValues.get(newHorizontalValues.size() - 1));
        newSlashNodeValues.add(newVerticalValues.get(0));
        CssCompositeValueNode newSlashNode = new CssCompositeValueNode(newSlashNodeValues,
                CssCompositeValueNode.Operator.SLASH, null);

        List<CssValueNode> newValueList = Lists.newArrayList();
        newValueList.addAll(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<CssValueNode> flipNumericValues(List<CssValueNode> 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 (isNumericNode(valueNode) || isCssLiteralNode(valueNode) || isCssHexColorNode(valueNode)
                    || shouldFlipConstantReference(valueNode)) {
                switch (count) {
                case 3:
                    fourthValueNode = valueNode.deepCopy();
                    break;
                case 1:
                    secondValueNode = valueNode.deepCopy();
                }
            } else {
                return valueNodes;
            }
            count++;
        }

        // Swap second and last in the new list.
        count = 0;
        List<CssValueNode> newValueList = Lists.newArrayList();
        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.
     */
    private String flipValue(String value) {
        for (String s : EXACT_MATCHING_FOR_FLIPPING.keySet()) {
            if (value.equals(s)) {
                value = EXACT_MATCHING_FOR_FLIPPING.get(s);
                break;
            }
        }
        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.indexOf(s) > 0) {
                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'.
     */
    private <T extends CssValueNode> T flipNode(T tNode) {
        if (tNode instanceof CssLiteralNode) {
            CssLiteralNode literalNode = (CssLiteralNode) tNode;
            String oldValue = literalNode.getValue();
            if (null == oldValue) {
                return tNode;
            }
            String flippedValue = flipValue(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 = flipValue(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<CssValueNode> newArgumentsList = new ArrayList<CssValueNode>();
        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<CssValueNode> valueNodes = Lists.newArrayList();
        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.size() != 0) {
            CssValueNode priority = null;
            // Remove possible !important priority node.
            if (!valueNodes.isEmpty() && Iterables.getLast(valueNodes) instanceof CssPriorityNode) {
                priority = Iterables.getLast(valueNodes);
                valueNodes = valueNodes.subList(0, valueNodes.size() - 1);
            }
            List<CssValueNode> 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<CssNode> replacementList = Lists.newArrayList();
        replacementList.add(newDeclarationNode);
        visitController.replaceCurrentBlockChildWith(replacementList, false);
        return true;
    }

    @Override
    public void runPass() {
        visitController.startVisit(this);
    }
}