com.google.common.css.compiler.passes.BiDiFlipper Maven / Gradle / Ivy
Show all versions of closure-stylesheets Show documentation
/*
* 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.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
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 [email protected] (Roozbeh Pournader)
*/
public class BiDiFlipper extends DefaultTreeVisitor implements CssCompilerPass {
private final DecimalFormat percentFormatter =
new DecimalFormat("#.########", DecimalFormatSymbols.getInstance(Locale.US));
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 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 Map 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 Map 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 Set 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 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 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 Map 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 Map URL_LEFTRIGHT_PATTERN_FOR_FLIPPING =
new ImmutableMap.Builder()
.put(Pattern.compile("(?
* 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 List flipCorners(List 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 flipBorderRadius(List 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 slashNodeValues = slashNode.getValues();
// Create a list of horizontal values and flip them
List horizontalValues = Lists.newArrayList();
horizontalValues.addAll(valueNodes.subList(0, slashLocation));
horizontalValues.add(slashNodeValues.get(0));
List newHorizontalValues = flipCorners(horizontalValues);
// Do the same for vertical values
List verticalValues = Lists.newArrayList();
verticalValues.add(slashNodeValues.get(1));
verticalValues.addAll(valueNodes.subList(slashLocation + 1,
valueNodes.size()));
List newVerticalValues = flipCorners(verticalValues);
// Create a new slash node
List 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 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 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 (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 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 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 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 = 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 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 = Lists.newArrayList();
replacementList.add(newDeclarationNode);
visitController.replaceCurrentBlockChildWith(replacementList, false);
return true;
}
@Override
public void runPass() {
visitController.startVisit(this);
}
}