| /* |
| * 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(x.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; |
| } |
| |
| @Override |
| public boolean visit(CssCharset x, Context ctx) { |
| CssCharset newRule = new CssCharset(x.getCharset()); |
| 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()); |
| } |
| } |