blob: bffbd36cd6382827efaa769a1f6e3e3d5d6f313d [file] [log] [blame]
/*
* 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.gwt.resources.css.ast;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
/**
* Clones CssNodes.
*/
public class CssNodeCloner extends CssVisitor {
/**
* Clone a list of nodes.
*/
public static <T extends CssNode> List<T> clone(Class<T> clazz, List<T> nodes) {
// Push a fake context that will contain the cloned node
List<CssNode> topContext = new ArrayList<CssNode>();
final List<CssProperty> topProperties = new ArrayList<CssProperty>();
final List<CssSelector> topSelectors = new ArrayList<CssSelector>();
CssNodeCloner cloner = new CssNodeCloner();
cloner.curentNodes.push(topContext);
cloner.currentHasProperties = new HasProperties() {
public List<CssProperty> getProperties() {
return topProperties;
}
};
cloner.currentHasSelectors = new HasSelectors() {
public List<CssSelector> getSelectors() {
return topSelectors;
}
};
// Process the nodes
cloner.accept(nodes);
/*
* Return the requested data. Different AST nodes will have been collected
* in different parts of the initial context.
*/
List<?> toIterate;
if (CssProperty.class.isAssignableFrom(clazz)) {
toIterate = topProperties;
} else if (CssSelector.class.isAssignableFrom(clazz)) {
toIterate = topSelectors;
} else {
toIterate = topContext;
}
assert toIterate.size() == nodes.size() : "Wrong number of nodes in top "
+ "context. Expecting: " + nodes.size() + " Found: " + toIterate.size();
// Create the final return value
List<T> toReturn = new ArrayList<T>(toIterate.size());
for (Object node : toIterate) {
assert clazz.isInstance(node) : "Return type mismatch. Expecting: "
+ clazz.getName() + " Found: " + node.getClass().getName();
// Cast to the correct type to avoid an unchecked generic cast
toReturn.add(clazz.cast(node));
}
return toReturn;
}
/**
* Clone a single node.
*/
public static <T extends CssNode> T clone(Class<T> clazz, T node) {
return clone(clazz, Collections.singletonList(node)).get(0);
}
private HasProperties currentHasProperties;
private HasSelectors currentHasSelectors;
private final Stack<List<CssNode>> curentNodes = new Stack<List<CssNode>>();
private CssNodeCloner() {
}
@Override
public void endVisit(CssMediaRule x, Context ctx) {
popNodes(x);
}
@Override
public void endVisit(CssNoFlip x, Context ctx) {
popNodes(x);
}
@Override
public void endVisit(CssStylesheet x, Context ctx) {
popNodes(x);
}
@Override
public boolean visit(CssDef x, Context ctx) {
CssDef newDef = new CssDef(x.getKey());
newDef.getValues().addAll(x.getValues());
addToNodes(newDef);
return true;
}
@Override
public boolean visit(CssEval x, Context ctx) {
assert x.getValues().size() == 1;
assert x.getValues().get(0).isExpressionValue() != null;
String value = x.getValues().get(0).isExpressionValue().getExpression();
CssEval newEval = new CssEval(x.getKey(), value);
addToNodes(newEval);
return true;
}
@Override
public boolean visit(CssExternalSelectors x, Context ctx) {
CssExternalSelectors newExternals = new CssExternalSelectors();
newExternals.getClasses().addAll(x.getClasses());
addToNodes(newExternals);
return true;
}
/**
* A CssIf has two lists of nodes, so we want to handle traversal in this
* visitor.
*/
@Override
public boolean visit(CssIf x, Context ctx) {
CssIf newIf = new CssIf();
if (x.getExpression() != null) {
newIf.setExpression(x.getExpression());
} else {
newIf.setProperty(x.getPropertyName());
String[] newValues = new String[x.getPropertyValues().length];
System.arraycopy(x.getPropertyValues(), 0, newValues, 0, newValues.length);
newIf.setPropertyValues(newValues);
newIf.setNegated(x.isNegated());
}
// Handle the "then" part
pushNodes(newIf);
accept(x.getNodes());
popNodes(x, newIf);
/*
* Push the "else" part as though it were its own node, but don't add it as
* its own top-level node.
*/
CollapsedNode oldElseNodes = new CollapsedNode(x.getElseNodes());
CollapsedNode newElseNodes = new CollapsedNode(newIf.getElseNodes());
pushNodes(newElseNodes, false);
accept(oldElseNodes);
popNodes(oldElseNodes, newElseNodes);
return false;
}
@Override
public boolean visit(CssMediaRule x, Context ctx) {
CssMediaRule newRule = new CssMediaRule();
newRule.getMedias().addAll(newRule.getMedias());
pushNodes(newRule);
return true;
}
@Override
public boolean visit(CssNoFlip x, Context ctx) {
pushNodes(new CssNoFlip());
return true;
}
@Override
public boolean visit(CssPageRule x, Context ctx) {
CssPageRule newRule = new CssPageRule();
newRule.setPseudoPage(x.getPseudoPage());
addToNodes(newRule);
return true;
}
@Override
public boolean visit(CssProperty x, Context ctx) {
CssProperty newProperty = new CssProperty(x.getName(), x.getValues(),
x.isImportant());
currentHasProperties.getProperties().add(newProperty);
return true;
}
@Override
public boolean visit(CssFontFace x, Context ctx) {
CssFontFace newRule = new CssFontFace();
addToNodes(newRule);
return true;
}
@Override
public boolean visit(CssRule x, Context ctx) {
CssRule newRule = new CssRule();
addToNodes(newRule);
return true;
}
@Override
public boolean visit(CssSelector x, Context ctx) {
CssSelector newSelector = new CssSelector(x.getSelector());
currentHasSelectors.getSelectors().add(newSelector);
return true;
}
@Override
public boolean visit(CssSprite x, Context ctx) {
CssSprite newSprite = new CssSprite();
newSprite.setResourceFunction(x.getResourceFunction());
addToNodes(newSprite);
return true;
}
@Override
public boolean visit(CssStylesheet x, Context ctx) {
CssStylesheet newSheet = new CssStylesheet();
pushNodes(newSheet);
return true;
}
@Override
public boolean visit(CssUrl x, Context ctx) {
assert x.getValues().size() == 1;
assert x.getValues().get(0).isDotPathValue() != null;
CssUrl newUrl = new CssUrl(x.getKey(),
x.getValues().get(0).isDotPathValue());
addToNodes(newUrl);
return true;
}
@Override
public boolean visit(CssUnknownAtRule x, Context ctx) {
CssUnknownAtRule newRule = new CssUnknownAtRule(x.getRule());
addToNodes(newRule);
return true;
}
/**
* Add a cloned node instance to the output.
*/
private void addToNodes(CssNode node) {
curentNodes.peek().add(node);
currentHasProperties = node instanceof HasProperties ? (HasProperties) node
: null;
currentHasSelectors = node instanceof HasSelectors ? (HasSelectors) node
: null;
}
/**
* Remove a frame.
*
* @param original the node that was being cloned so that validity checks may
* be performed
*/
private List<CssNode> popNodes(HasNodes original) {
List<CssNode> toReturn = curentNodes.pop();
if (toReturn.size() != original.getNodes().size()) {
throw new RuntimeException("Insufficient number of nodes for a "
+ original.getClass().getName() + " Expected: "
+ original.getNodes().size() + " Found: " + toReturn.size());
}
return toReturn;
}
/**
* Remove a frame.
*
* @param original the node that was being cloned so that validity checks may
* be performed
* @param expected the HasNodes whose nodes were being populated by the frame
* being removed.
*/
private List<CssNode> popNodes(HasNodes original, HasNodes expected) {
List<CssNode> toReturn = popNodes(original);
if (toReturn != expected.getNodes()) {
throw new RuntimeException("Incorrect parent node list popped");
}
return toReturn;
}
/**
* Push a new frame, adding the new parent as a child of the current parent.
*/
private <T extends CssNode & HasNodes> void pushNodes(T parent) {
pushNodes(parent, true);
}
/**
* Push a new frame.
*
* @param addToNodes if <code>true</code> add the new parent node as a child
* of the current parent.
*/
private <T extends CssNode & HasNodes> void pushNodes(T parent,
boolean addToNodes) {
if (addToNodes) {
addToNodes(parent);
}
this.curentNodes.push(parent.getNodes());
}
}