Reorganize the bulk of the CssResourceGenerator code to move the visitors into the resources.css package.

Patch by: bobv
Review by: rjrjr

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6251 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/resources/css/ClassRenamer.java b/user/src/com/google/gwt/resources/css/ClassRenamer.java
new file mode 100644
index 0000000..a78ccbd
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ClassRenamer.java
@@ -0,0 +1,271 @@
+/*
+ * 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;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.client.CssResource.ClassName;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssSelector;
+import com.google.gwt.resources.css.ast.CssStylesheet;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+
+/**
+ * Renames class selectors to their obfuscated names.
+ */
+public class ClassRenamer extends CssVisitor {
+
+  /**
+   * A tag to indicate that an externally-defined CSS class has no JMethod that
+   * is used to access it.
+   */
+  private static final Replacement UNREFERENCED_EXTERNAL = new Replacement(
+      null, null);
+
+  /*
+   * TODO: Replace with Pair<A, B>.
+   */
+  private static class Replacement {
+
+    private JMethod method;
+    private String obfuscatedClassName;
+
+    public Replacement(JMethod method, String obfuscatedClassName) {
+      this.method = method;
+      this.obfuscatedClassName = obfuscatedClassName;
+    }
+
+    public JMethod getMethod() {
+      return method;
+    }
+
+    public String getObfuscatedClassName() {
+      return obfuscatedClassName;
+    }
+
+    /**
+     * For debugging use only.
+     */
+    public String toString() {
+      if (this == UNREFERENCED_EXTERNAL) {
+        return "Unreferenced external class name";
+      } else {
+        return method.getName() + "=" + obfuscatedClassName;
+      }
+    }
+  }
+
+  /**
+   * Records replacements that have actually been performed.
+   */
+  private final Map<JMethod, String> actualReplacements = new IdentityHashMap<JMethod, String>();
+  private final Set<String> cssDefs = new HashSet<String>();
+
+  /**
+   * The task-list of replacements to perform in the stylesheet.
+   */
+  private final Map<String, Replacement> potentialReplacements;
+  private final TreeLogger logger;
+  private final Set<JMethod> missingClasses;
+  private final boolean strict;
+  private final Set<String> unknownClasses = new HashSet<String>();
+
+  public ClassRenamer(TreeLogger logger,
+      Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
+      boolean strict, Set<String> externalClasses) {
+    this.logger = logger.branch(TreeLogger.DEBUG, "Replacing CSS class names");
+    this.strict = strict;
+
+    potentialReplacements = computeReplacements(classReplacementsWithPrefix,
+        externalClasses);
+
+    // Require a definition for all classes in the default namespace
+    assert classReplacementsWithPrefix.containsKey("");
+    missingClasses = new HashSet<JMethod>(
+        classReplacementsWithPrefix.get("").keySet());
+  }
+
+  @Override
+  public void endVisit(CssDef x, Context ctx) {
+    cssDefs.add(x.getKey());
+  }
+
+  @Override
+  public void endVisit(CssSelector x, Context ctx) {
+
+    String sel = x.getSelector();
+    int originalLength = sel.length();
+
+    Matcher ma = CssSelector.CLASS_SELECTOR_PATTERN.matcher(sel);
+    StringBuilder sb = new StringBuilder(originalLength);
+    int start = 0;
+
+    while (ma.find()) {
+      String sourceClassName = ma.group(1);
+
+      Replacement entry = potentialReplacements.get(sourceClassName);
+
+      if (entry == null) {
+        unknownClasses.add(sourceClassName);
+        continue;
+
+      } else if (entry == UNREFERENCED_EXTERNAL) {
+        // An @external without an accessor method. This is OK.
+        continue;
+      }
+
+      JMethod method = entry.getMethod();
+      String obfuscatedClassName = entry.getObfuscatedClassName();
+
+      // Consume the interstitial portion of the original selector
+      sb.append(sel.subSequence(start, ma.start(1)));
+      sb.append(obfuscatedClassName);
+      start = ma.end(1);
+
+      actualReplacements.put(method, obfuscatedClassName);
+      missingClasses.remove(method);
+    }
+
+    if (start != 0) {
+      // Consume the remainder and update the selector
+      sb.append(sel.subSequence(start, originalLength));
+      x.setSelector(sb.toString());
+    }
+  }
+
+  @Override
+  public void endVisit(CssStylesheet x, Context ctx) {
+    boolean stop = false;
+
+    // Skip names corresponding to @def entries. They too can be declared as
+    // String accessors.
+    List<JMethod> toRemove = new ArrayList<JMethod>();
+    for (JMethod method : missingClasses) {
+      if (cssDefs.contains(method.getName())) {
+        toRemove.add(method);
+      }
+    }
+    for (JMethod method : toRemove) {
+      missingClasses.remove(method);
+    }
+
+    if (!missingClasses.isEmpty()) {
+      stop = true;
+      TreeLogger errorLogger = logger.branch(TreeLogger.INFO,
+          "The following obfuscated style classes were missing from "
+              + "the source CSS file:");
+      for (JMethod m : missingClasses) {
+        String name = m.getName();
+        ClassName className = m.getAnnotation(ClassName.class);
+        if (className != null) {
+          name = className.value();
+        }
+        errorLogger.log(TreeLogger.ERROR, name + ": Fix by adding ." + name
+            + "{}");
+      }
+    }
+
+    if (strict && !unknownClasses.isEmpty()) {
+      stop = true;
+      TreeLogger errorLogger = logger.branch(TreeLogger.ERROR,
+          "The following unobfuscated classes were present in a strict CssResource:");
+      for (String s : unknownClasses) {
+        errorLogger.log(TreeLogger.ERROR, s);
+      }
+      errorLogger.log(TreeLogger.INFO, "Fix by adding String accessor "
+          + "method(s) to the CssResource interface for obfuscated classes, "
+          + "or using an @external declaration for unobfuscated classes.");
+    }
+
+    if (stop) {
+      throw new CssCompilerException("Missing a CSS replacement");
+    }
+  }
+
+  /**
+   * Reports the replacements that were actually performed by this visitor.
+   */
+  public Map<JMethod, String> getReplacements() {
+    return actualReplacements;
+  }
+
+  /**
+   * Flatten class name lookups to speed selector rewriting.
+   * 
+   * @param classReplacementsWithPrefix a map of local prefixes to the
+   *          obfuscated names of imported methods. If a CssResource makes use
+   *          of the {@link CssResource.Import} annotation, the keys of this map
+   *          will correspond to the {@link CssResource.ImportedWithPrefix}
+   *          value defined on the imported CssResource. The zero-length string
+   *          key holds the obfuscated names for the CssResource that is being
+   *          generated.
+   * @return A flattened version of the classReplacementWithPrefix map, where
+   *         the keys are the source class name (with prefix included), and
+   *         values have the obfuscated class name and associated JMethod.
+   */
+  private Map<String, Replacement> computeReplacements(
+      Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
+      Set<String> externalClasses) {
+
+    Map<String, Replacement> toReturn = new HashMap<String, Replacement>();
+
+    for (String externalClass : externalClasses) {
+      toReturn.put(externalClass, UNREFERENCED_EXTERNAL);
+    }
+
+    for (Map.Entry<String, Map<JMethod, String>> outerEntry : classReplacementsWithPrefix.entrySet()) {
+      String prefix = outerEntry.getKey();
+
+      for (Map.Entry<JMethod, String> entry : outerEntry.getValue().entrySet()) {
+        JMethod method = entry.getKey();
+        String sourceClassName = method.getName();
+        String obfuscatedClassName = entry.getValue();
+
+        ClassName className = method.getAnnotation(ClassName.class);
+        if (className != null) {
+          sourceClassName = className.value();
+        }
+
+        sourceClassName = prefix + sourceClassName;
+
+        if (externalClasses.contains(sourceClassName)) {
+          /*
+           * It simplifies the sanity-checking logic to treat external classes
+           * as though they were simply obfuscated to exactly the value the user
+           * wants.
+           */
+          obfuscatedClassName = sourceClassName;
+        }
+
+        toReturn.put(sourceClassName, new Replacement(method,
+            obfuscatedClassName));
+      }
+    }
+    return Collections.unmodifiableMap(toReturn);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/DefsCollector.java b/user/src/com/google/gwt/resources/css/DefsCollector.java
new file mode 100644
index 0000000..4cb11c8
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/DefsCollector.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Collects the names of all user-defined {@literal @def} constants in the
+ * stylesheet.
+ */
+public class DefsCollector extends CssVisitor {
+  private final Set<String> defs = new HashSet<String>();
+
+  @Override
+  public void endVisit(CssDef x, Context ctx) {
+    defs.add(x.getKey());
+  }
+
+  public Set<String> getDefs() {
+    return defs;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/ExternalClassesCollector.java b/user/src/com/google/gwt/resources/css/ExternalClassesCollector.java
new file mode 100644
index 0000000..d6cb7bf
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ExternalClassesCollector.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssExternalSelectors;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Collects all {@code @external} declarations in the stylesheet.
+ */
+public class ExternalClassesCollector extends CssVisitor {
+  private final Set<String> classes = new HashSet<String>();
+
+  @Override
+  public void endVisit(CssExternalSelectors x, Context ctx) {
+    classes.addAll(x.getClasses());
+  }
+
+  public Set<String> getClasses() {
+    return classes;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/IfEvaluator.java b/user/src/com/google/gwt/resources/css/IfEvaluator.java
new file mode 100644
index 0000000..5e02ad3
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/IfEvaluator.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.ConfigurationProperty;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.SelectionProperty;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNode;
+
+import java.util.Arrays;
+
+/**
+ * Statically evaluates {@literal @if} rules.
+ */
+public class IfEvaluator extends CssModVisitor {
+  private final TreeLogger logger;
+  private final PropertyOracle oracle;
+
+  public IfEvaluator(TreeLogger logger, PropertyOracle oracle) {
+    this.logger = logger.branch(TreeLogger.DEBUG,
+        "Replacing property-based @if blocks");
+    this.oracle = oracle;
+  }
+
+  @Override
+  public void endVisit(CssIf x, Context ctx) {
+    if (x.getExpression() != null) {
+      // This gets taken care of by the runtime substitution visitor
+    } else {
+      try {
+        String propertyName = x.getPropertyName();
+        String propValue = null;
+        try {
+          SelectionProperty selProp = oracle.getSelectionProperty(logger,
+              propertyName);
+          propValue = selProp.getCurrentValue();
+        } catch (BadPropertyValueException e) {
+          ConfigurationProperty confProp = oracle.getConfigurationProperty(propertyName);
+          propValue = confProp.getValues().get(0);
+        }
+
+        /*
+         * If the deferred binding property's value is in the list of values in
+         * the @if rule, move the rules into the @if's context.
+         */
+        if (Arrays.asList(x.getPropertyValues()).contains(propValue)
+            ^ x.isNegated()) {
+          for (CssNode n : x.getNodes()) {
+            ctx.insertBefore(n);
+          }
+        } else {
+          // Otherwise, move the else block into the if statement's position
+          for (CssNode n : x.getElseNodes()) {
+            ctx.insertBefore(n);
+          }
+        }
+
+        // Always delete @if rules that we can statically evaluate
+        ctx.removeMe();
+      } catch (BadPropertyValueException e) {
+        logger.log(TreeLogger.ERROR, "Unable to evaluate @if block", e);
+        throw new CssCompilerException("Unable to parse CSS", e);
+      }
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/MergeIdenticalSelectorsVisitor.java b/user/src/com/google/gwt/resources/css/MergeIdenticalSelectorsVisitor.java
new file mode 100644
index 0000000..08a03ed
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/MergeIdenticalSelectorsVisitor.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssMediaRule;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNode;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.css.ast.CssSelector;
+import com.google.gwt.resources.rg.CssResourceGenerator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Merges rules that have matching selectors.
+ */
+public class MergeIdenticalSelectorsVisitor extends CssModVisitor {
+  private final Map<String, CssRule> canonicalRules = new HashMap<String, CssRule>();
+  private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
+
+  @Override
+  public boolean visit(CssIf x, Context ctx) {
+    visitInNewContext(x.getNodes());
+    visitInNewContext(x.getElseNodes());
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssMediaRule x, Context ctx) {
+    visitInNewContext(x.getNodes());
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssRule x, Context ctx) {
+    // Assumed to run immediately after SplitRulesVisitor
+    assert x.getSelectors().size() == 1;
+    CssSelector sel = x.getSelectors().get(0);
+
+    if (canonicalRules.containsKey(sel.getSelector())) {
+      CssRule canonical = canonicalRules.get(sel.getSelector());
+
+      // Check everything between the canonical rule and this rule for common
+      // properties. If there are common properties, it would be unsafe to
+      // promote the rule.
+      boolean hasCommon = false;
+      int index = rulesInOrder.indexOf(canonical) + 1;
+      assert index != 0;
+
+      for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
+          && !hasCommon;) {
+        hasCommon = CssResourceGenerator.haveCommonProperties(i.next(), x);
+      }
+
+      if (!hasCommon) {
+        // It's safe to promote the rule
+        canonical.getProperties().addAll(x.getProperties());
+        ctx.removeMe();
+        return false;
+      }
+    }
+
+    canonicalRules.put(sel.getSelector(), x);
+    rulesInOrder.add(x);
+    return false;
+  }
+
+  private void visitInNewContext(List<CssNode> nodes) {
+    MergeIdenticalSelectorsVisitor v = new MergeIdenticalSelectorsVisitor();
+    v.acceptWithInsertRemove(nodes);
+    rulesInOrder.addAll(v.rulesInOrder);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/MergeRulesByContentVisitor.java b/user/src/com/google/gwt/resources/css/MergeRulesByContentVisitor.java
new file mode 100644
index 0000000..ea3e276
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/MergeRulesByContentVisitor.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssMediaRule;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNode;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.rg.CssResourceGenerator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Merges rules that have identical content.
+ */
+public class MergeRulesByContentVisitor extends CssModVisitor {
+  private Map<String, CssRule> rulesByContents = new HashMap<String, CssRule>();
+  private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
+
+  @Override
+  public boolean visit(CssIf x, Context ctx) {
+    visitInNewContext(x.getNodes());
+    visitInNewContext(x.getElseNodes());
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssMediaRule x, Context ctx) {
+    visitInNewContext(x.getNodes());
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssRule x, Context ctx) {
+    StringBuilder b = new StringBuilder();
+    for (CssProperty p : x.getProperties()) {
+      b.append(p.getName()).append(":").append(p.getValues().getExpression());
+    }
+
+    String content = b.toString();
+    CssRule canonical = rulesByContents.get(content);
+
+    // Check everything between the canonical rule and this rule for common
+    // properties. If there are common properties, it would be unsafe to
+    // promote the rule.
+    if (canonical != null) {
+      boolean hasCommon = false;
+      int index = rulesInOrder.indexOf(canonical) + 1;
+      assert index != 0;
+
+      for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
+          && !hasCommon;) {
+        hasCommon = CssResourceGenerator.haveCommonProperties(i.next(), x);
+      }
+
+      if (!hasCommon) {
+        canonical.getSelectors().addAll(x.getSelectors());
+        ctx.removeMe();
+        return false;
+      }
+    }
+
+    rulesByContents.put(content, x);
+    rulesInOrder.add(x);
+    return false;
+  }
+
+  private void visitInNewContext(List<CssNode> nodes) {
+    MergeRulesByContentVisitor v = new MergeRulesByContentVisitor();
+    v.acceptWithInsertRemove(nodes);
+    rulesInOrder.addAll(v.rulesInOrder);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/RequirementsCollector.java b/user/src/com/google/gwt/resources/css/RequirementsCollector.java
new file mode 100644
index 0000000..125e348
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/RequirementsCollector.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssVisitor;
+import com.google.gwt.resources.ext.ClientBundleRequirements;
+
+/**
+ * Analyzes a stylesheet to update the ClientBundleRequirements interface.
+ */
+public class RequirementsCollector extends CssVisitor {
+  private final TreeLogger logger;
+  private final ClientBundleRequirements requirements;
+
+  public RequirementsCollector(TreeLogger logger,
+      ClientBundleRequirements requirements) {
+    this.logger = logger.branch(TreeLogger.DEBUG,
+        "Scanning CSS for requirements");
+    this.requirements = requirements;
+  }
+
+  @Override
+  public void endVisit(CssIf x, Context ctx) {
+    String propertyName = x.getPropertyName();
+    if (propertyName != null) {
+      try {
+        requirements.addPermutationAxis(propertyName);
+      } catch (BadPropertyValueException e) {
+        logger.log(TreeLogger.ERROR, "Unknown deferred-binding property "
+            + propertyName, e);
+        throw new CssCompilerException("Unknown deferred-binding property", e);
+      }
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/RtlVisitor.java b/user/src/com/google/gwt/resources/css/RtlVisitor.java
new file mode 100644
index 0000000..3994231
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/RtlVisitor.java
@@ -0,0 +1,290 @@
+/*
+ * 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;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNoFlip;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
+import com.google.gwt.resources.css.ast.CssProperty.NumberValue;
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Applies RTL transforms to a stylesheet.
+ */
+public class RtlVisitor extends CssModVisitor {
+  /**
+   * Records if we're currently visiting a CssRule whose only selector is
+   * "body".
+   */
+  private boolean inBodyRule;
+
+  @Override
+  public void endVisit(CssProperty x, Context ctx) {
+    String name = x.getName();
+
+    if (name.equalsIgnoreCase("left")) {
+      x.setName("right");
+    } else if (name.equalsIgnoreCase("right")) {
+      x.setName("left");
+    } else if (name.endsWith("-left")) {
+      int len = name.length();
+      x.setName(name.substring(0, len - 4) + "right");
+    } else if (name.endsWith("-right")) {
+      int len = name.length();
+      x.setName(name.substring(0, len - 5) + "left");
+    } else if (name.contains("-right-")) {
+      x.setName(name.replace("-right-", "-left-"));
+    } else if (name.contains("-left-")) {
+      x.setName(name.replace("-left-", "-right-"));
+    } else {
+      List<Value> values = new ArrayList<Value>(x.getValues().getValues());
+      invokePropertyHandler(x.getName(), values);
+      x.setValue(new CssProperty.ListValue(values));
+    }
+  }
+
+  @Override
+  public boolean visit(CssNoFlip x, Context ctx) {
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssRule x, Context ctx) {
+    inBodyRule = x.getSelectors().size() == 1
+        && x.getSelectors().get(0).getSelector().equals("body");
+    return true;
+  }
+
+  void propertyHandlerBackground(List<Value> values) {
+    /*
+     * The first numeric value will be treated as the left position only if we
+     * havn't seen any value that could potentially be the left value.
+     */
+    boolean seenLeft = false;
+
+    for (ListIterator<Value> it = values.listIterator(); it.hasNext();) {
+      Value v = it.next();
+      Value maybeFlipped = flipLeftRightIdentValue(v);
+      NumberValue nv = v.isNumberValue();
+      if (v != maybeFlipped) {
+        it.set(maybeFlipped);
+        seenLeft = true;
+
+      } else if (isIdent(v, "center")) {
+        seenLeft = true;
+
+      } else if (!seenLeft && (nv != null)) {
+        seenLeft = true;
+        if ("%".equals(nv.getUnits())) {
+          float position = 100f - nv.getValue();
+          it.set(new NumberValue(position, "%"));
+          break;
+        }
+      }
+    }
+  }
+
+  void propertyHandlerBackgroundPosition(List<Value> values) {
+    propertyHandlerBackground(values);
+  }
+
+  Value propertyHandlerBackgroundPositionX(Value v) {
+    ArrayList<Value> list = new ArrayList<Value>(1);
+    list.add(v);
+    propertyHandlerBackground(list);
+    return list.get(0);
+  }
+
+  /**
+   * Note there should be no propertyHandlerBorder(). The CSS spec states that
+   * the border property must set all values at once.
+   */
+  void propertyHandlerBorderColor(List<Value> values) {
+    swapFour(values);
+  }
+
+  void propertyHandlerBorderStyle(List<Value> values) {
+    swapFour(values);
+  }
+
+  void propertyHandlerBorderWidth(List<Value> values) {
+    swapFour(values);
+  }
+
+  Value propertyHandlerClear(Value v) {
+    return propertyHandlerFloat(v);
+  }
+
+  Value propertyHandlerCursor(Value v) {
+    IdentValue identValue = v.isIdentValue();
+    if (identValue == null) {
+      return v;
+    }
+
+    String ident = identValue.getIdent().toLowerCase();
+    if (!ident.endsWith("-resize")) {
+      return v;
+    }
+
+    StringBuffer newIdent = new StringBuffer();
+
+    if (ident.length() == 9) {
+      if (ident.charAt(0) == 'n') {
+        newIdent.append('n');
+        ident = ident.substring(1);
+      } else if (ident.charAt(0) == 's') {
+        newIdent.append('s');
+        ident = ident.substring(1);
+      } else {
+        return v;
+      }
+    }
+
+    if (ident.length() == 8) {
+      if (ident.charAt(0) == 'e') {
+        newIdent.append("w-resize");
+      } else if (ident.charAt(0) == 'w') {
+        newIdent.append("e-resize");
+      } else {
+        return v;
+      }
+      return new IdentValue(newIdent.toString());
+    } else {
+      return v;
+    }
+  }
+
+  Value propertyHandlerDirection(Value v) {
+    if (inBodyRule) {
+      if (isIdent(v, "ltr")) {
+        return new IdentValue("rtl");
+      } else if (isIdent(v, "rtl")) {
+        return new IdentValue("ltr");
+      }
+    }
+    return v;
+  }
+
+  Value propertyHandlerFloat(Value v) {
+    return flipLeftRightIdentValue(v);
+  }
+
+  void propertyHandlerMargin(List<Value> values) {
+    swapFour(values);
+  }
+
+  void propertyHandlerPadding(List<Value> values) {
+    swapFour(values);
+  }
+
+  Value propertyHandlerPageBreakAfter(Value v) {
+    return flipLeftRightIdentValue(v);
+  }
+
+  Value propertyHandlerPageBreakBefore(Value v) {
+    return flipLeftRightIdentValue(v);
+  }
+
+  Value propertyHandlerTextAlign(Value v) {
+    return flipLeftRightIdentValue(v);
+  }
+
+  private Value flipLeftRightIdentValue(Value v) {
+    if (isIdent(v, "right")) {
+      return new IdentValue("left");
+
+    } else if (isIdent(v, "left")) {
+      return new IdentValue("right");
+    }
+    return v;
+  }
+
+  /**
+   * Reflectively invokes a propertyHandler method for the named property.
+   * Dashed names are transformed into camel-case names; only letters following
+   * a dash will be capitalized when looking for a method to prevent
+   * <code>fooBar<code> and <code>foo-bar</code> from colliding.
+   */
+  private void invokePropertyHandler(String name, List<Value> values) {
+    // See if we have a property-handler function
+    try {
+      String[] parts = name.toLowerCase().split("-");
+      StringBuffer methodName = new StringBuffer("propertyHandler");
+      for (String part : parts) {
+        methodName.append(Character.toUpperCase(part.charAt(0)));
+        methodName.append(part, 1, part.length());
+      }
+
+      try {
+        // Single-arg for simplicity
+        Method m = getClass().getDeclaredMethod(methodName.toString(),
+            Value.class);
+        assert Value.class.isAssignableFrom(m.getReturnType());
+        Value newValue = (Value) m.invoke(this, values.get(0));
+        values.set(0, newValue);
+      } catch (NoSuchMethodException e) {
+        // OK
+      }
+
+      try {
+        // Or the whole List for completeness
+        Method m = getClass().getDeclaredMethod(methodName.toString(),
+            List.class);
+        m.invoke(this, values);
+      } catch (NoSuchMethodException e) {
+        // OK
+      }
+
+    } catch (SecurityException e) {
+      throw new CssCompilerException(
+          "Unable to invoke property handler function for " + name, e);
+    } catch (IllegalArgumentException e) {
+      throw new CssCompilerException(
+          "Unable to invoke property handler function for " + name, e);
+    } catch (IllegalAccessException e) {
+      throw new CssCompilerException(
+          "Unable to invoke property handler function for " + name, e);
+    } catch (InvocationTargetException e) {
+      throw new CssCompilerException(
+          "Unable to invoke property handler function for " + name, e);
+    }
+  }
+
+  private boolean isIdent(Value value, String query) {
+    IdentValue v = value.isIdentValue();
+    return v != null && v.getIdent().equalsIgnoreCase(query);
+  }
+
+  /**
+   * Swaps the second and fourth values in a list of four values.
+   */
+  private void swapFour(List<Value> values) {
+    if (values.size() == 4) {
+      Collections.swap(values, 1, 3);
+    }
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/SplitRulesVisitor.java b/user/src/com/google/gwt/resources/css/SplitRulesVisitor.java
new file mode 100644
index 0000000..646bed7
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/SplitRulesVisitor.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNodeCloner;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.css.ast.CssSelector;
+
+/**
+ * Splits rules with compound selectors into multiple rules.
+ */
+public class SplitRulesVisitor extends CssModVisitor {
+  @Override
+  public void endVisit(CssRule x, Context ctx) {
+    if (x.getSelectors().size() == 1) {
+      return;
+    }
+
+    for (CssSelector sel : x.getSelectors()) {
+      CssRule newRule = new CssRule();
+      newRule.getSelectors().add(sel);
+      newRule.getProperties().addAll(
+          CssNodeCloner.clone(CssProperty.class, x.getProperties()));
+      ctx.insertBefore(newRule);
+    }
+    ctx.removeMe();
+    return;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/Spriter.java b/user/src/com/google/gwt/resources/css/Spriter.java
new file mode 100644
index 0000000..6e48d37
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/Spriter.java
@@ -0,0 +1,150 @@
+/*
+ * 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;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.resources.client.ImageResource.ImageOptions;
+import com.google.gwt.resources.client.ImageResource.RepeatStyle;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.css.ast.CssSprite;
+import com.google.gwt.resources.css.ast.CssProperty.ExpressionValue;
+import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
+import com.google.gwt.resources.ext.ResourceContext;
+
+import java.util.List;
+
+/**
+ * Replaces CssSprite nodes with CssRule nodes that will display the sprited
+ * image. The real trick with spriting the images is to reuse the ImageResource
+ * processing framework by requiring the sprite to be defined in terms of an
+ * ImageResource.
+ */
+public class Spriter extends CssModVisitor {
+  private final ResourceContext context;
+  private final TreeLogger logger;
+
+  public Spriter(TreeLogger logger, ResourceContext context) {
+    this.logger = logger.branch(TreeLogger.DEBUG,
+        "Creating image sprite classes");
+    this.context = context;
+  }
+
+  @Override
+  public void endVisit(CssSprite x, Context ctx) {
+    JClassType bundleType = context.getClientBundleType();
+    String functionName = x.getResourceFunction();
+
+    if (functionName == null) {
+      logger.log(TreeLogger.ERROR, "The @sprite rule " + x.getSelectors()
+          + " must specify the " + CssSprite.IMAGE_PROPERTY_NAME + " property");
+      throw new CssCompilerException("No image property specified");
+    }
+
+    // Find the image accessor method
+    JMethod imageMethod = null;
+    JMethod[] allMethods = bundleType.getOverridableMethods();
+    for (int i = 0; imageMethod == null && i < allMethods.length; i++) {
+      JMethod candidate = allMethods[i];
+      // If the function name matches and takes no parameters
+      if (candidate.getName().equals(functionName)
+          && candidate.getParameters().length == 0) {
+        // We have a match
+        imageMethod = candidate;
+      }
+    }
+
+    // Method unable to be located
+    if (imageMethod == null) {
+      logger.log(TreeLogger.ERROR, "Unable to find ImageResource method "
+          + functionName + " in " + bundleType.getQualifiedSourceName());
+      throw new CssCompilerException("Cannot find image function");
+    }
+
+    JClassType imageResourceType = context.getGeneratorContext().getTypeOracle().findType(
+        ImageResource.class.getName());
+    assert imageResourceType != null;
+
+    if (!imageResourceType.isAssignableFrom(imageMethod.getReturnType().isClassOrInterface())) {
+      logger.log(TreeLogger.ERROR, "The return type of " + functionName
+          + " is not assignable to " + imageResourceType.getSimpleSourceName());
+      throw new CssCompilerException("Incorrect return type for "
+          + CssSprite.IMAGE_PROPERTY_NAME + " method");
+    }
+
+    ImageOptions options = imageMethod.getAnnotation(ImageOptions.class);
+    RepeatStyle repeatStyle;
+    if (options != null) {
+      repeatStyle = options.repeatStyle();
+    } else {
+      repeatStyle = RepeatStyle.None;
+    }
+
+    String instance = "(" + context.getImplementationSimpleSourceName()
+        + ".this." + functionName + "())";
+
+    CssRule replacement = new CssRule();
+    replacement.getSelectors().addAll(x.getSelectors());
+    List<CssProperty> properties = replacement.getProperties();
+
+    if (repeatStyle == RepeatStyle.None
+        || repeatStyle == RepeatStyle.Horizontal) {
+      properties.add(new CssProperty("height", new ExpressionValue(instance
+          + ".getHeight() + \"px\""), false));
+    }
+
+    if (repeatStyle == RepeatStyle.None || repeatStyle == RepeatStyle.Vertical) {
+      properties.add(new CssProperty("width", new ExpressionValue(instance
+          + ".getWidth() + \"px\""), false));
+    }
+    properties.add(new CssProperty("overflow", new IdentValue("hidden"), false));
+
+    String repeatText;
+    switch (repeatStyle) {
+      case None:
+        repeatText = " no-repeat";
+        break;
+      case Horizontal:
+        repeatText = " repeat-x";
+        break;
+      case Vertical:
+        repeatText = " repeat-y";
+        break;
+      case Both:
+        repeatText = " repeat";
+        break;
+      default:
+        throw new RuntimeException("Unknown repeatStyle " + repeatStyle);
+    }
+
+    String backgroundExpression = "\"url(\\\"\" + " + instance
+        + ".getURL() + \"\\\") -\" + " + instance + ".getLeft() + \"px -\" + "
+        + instance + ".getTop() + \"px " + repeatText + "\"";
+    properties.add(new CssProperty("background", new ExpressionValue(
+        backgroundExpression), false));
+
+    // Retain any user-specified properties
+    properties.addAll(x.getProperties());
+
+    ctx.replaceMe(replacement);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/SubstitutionCollector.java b/user/src/com/google/gwt/resources/css/SubstitutionCollector.java
new file mode 100644
index 0000000..d6cf2a7
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/SubstitutionCollector.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssEval;
+import com.google.gwt.resources.css.ast.CssUrl;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Collects all user-defined constant nodes in the stylesheet.
+ */
+public class SubstitutionCollector extends CssVisitor {
+  private final Map<String, CssDef> substitutions = new HashMap<String, CssDef>();
+
+  @Override
+  public void endVisit(CssDef x, Context ctx) {
+    substitutions.put(x.getKey(), x);
+  }
+
+  @Override
+  public void endVisit(CssEval x, Context ctx) {
+    substitutions.put(x.getKey(), x);
+  }
+
+  @Override
+  public void endVisit(CssUrl x, Context ctx) {
+    substitutions.put(x.getKey(), x);
+  }
+
+  public Map<String, CssDef> getSubstitutions() {
+    return substitutions;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/SubstitutionReplacer.java b/user/src/com/google/gwt/resources/css/SubstitutionReplacer.java
new file mode 100644
index 0000000..9cbbcc4
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/SubstitutionReplacer.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssUrl;
+import com.google.gwt.resources.css.ast.CssVisitor;
+import com.google.gwt.resources.css.ast.CssProperty.ExpressionValue;
+import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
+import com.google.gwt.resources.css.ast.CssProperty.ListValue;
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+import com.google.gwt.resources.ext.ResourceContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/**
+ * Substitute symbolic replacements into string values.
+ */
+public class SubstitutionReplacer extends CssVisitor {
+  private final ResourceContext context;
+  private final TreeLogger logger;
+  private final Map<String, CssDef> substitutions;
+
+  public SubstitutionReplacer(TreeLogger logger, ResourceContext context,
+      Map<String, CssDef> substitutions) {
+    this.context = context;
+    this.logger = logger;
+    this.substitutions = substitutions;
+  }
+
+  @Override
+  public void endVisit(CssProperty x, Context ctx) {
+    if (x.getValues() == null) {
+      // Nothing to do
+      return;
+    }
+
+    List<Value> values = new ArrayList<Value>(x.getValues().getValues());
+
+    for (ListIterator<Value> i = values.listIterator(); i.hasNext();) {
+      IdentValue v = i.next().isIdentValue();
+
+      if (v == null) {
+        // Don't try to substitute into anything other than idents
+        continue;
+      }
+
+      String value = v.getIdent();
+      CssDef def = substitutions.get(value);
+
+      if (def == null) {
+        continue;
+      } else if (def instanceof CssUrl) {
+        assert def.getValues().size() == 1;
+        assert def.getValues().get(0).isIdentValue() != null;
+        String functionName = def.getValues().get(0).isIdentValue().getIdent();
+
+        // Find the method
+        JMethod methods[] = context.getClientBundleType().getOverridableMethods();
+        boolean foundMethod = false;
+        if (methods != null) {
+          for (JMethod method : methods) {
+            if (method.getName().equals(functionName)) {
+              foundMethod = true;
+              break;
+            }
+          }
+        }
+
+        if (!foundMethod) {
+          logger.log(TreeLogger.ERROR, "Unable to find DataResource method "
+              + functionName + " in "
+              + context.getClientBundleType().getQualifiedSourceName());
+          throw new CssCompilerException("Cannot find data function");
+        }
+
+        String instance = "((" + DataResource.class.getName() + ")("
+            + context.getImplementationSimpleSourceName() + ".this."
+            + functionName + "()))";
+
+        StringBuilder expression = new StringBuilder();
+        expression.append("\"url('\" + ");
+        expression.append(instance).append(".getUrl()");
+        expression.append(" + \"')\"");
+        i.set(new ExpressionValue(expression.toString()));
+
+      } else {
+        i.remove();
+        for (Value defValue : def.getValues()) {
+          i.add(defValue);
+        }
+      }
+    }
+
+    x.setValue(new ListValue(values));
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
index adbf29c..db31336 100644
--- a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
@@ -19,7 +19,6 @@
 import com.google.gwt.core.ext.ConfigurationProperty;
 import com.google.gwt.core.ext.Generator;
 import com.google.gwt.core.ext.PropertyOracle;
-import com.google.gwt.core.ext.SelectionProperty;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
@@ -33,41 +32,36 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.resources.client.CssResource.ClassName;
 import com.google.gwt.resources.client.CssResource.Import;
 import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
 import com.google.gwt.resources.client.CssResource.NotStrict;
 import com.google.gwt.resources.client.CssResource.Shared;
 import com.google.gwt.resources.client.CssResource.Strict;
-import com.google.gwt.resources.client.ImageResource.ImageOptions;
-import com.google.gwt.resources.client.ImageResource.RepeatStyle;
+import com.google.gwt.resources.css.ClassRenamer;
 import com.google.gwt.resources.css.CssGenerationVisitor;
+import com.google.gwt.resources.css.DefsCollector;
+import com.google.gwt.resources.css.ExternalClassesCollector;
 import com.google.gwt.resources.css.GenerateCssAst;
+import com.google.gwt.resources.css.IfEvaluator;
+import com.google.gwt.resources.css.MergeIdenticalSelectorsVisitor;
+import com.google.gwt.resources.css.MergeRulesByContentVisitor;
+import com.google.gwt.resources.css.RequirementsCollector;
+import com.google.gwt.resources.css.RtlVisitor;
+import com.google.gwt.resources.css.SplitRulesVisitor;
+import com.google.gwt.resources.css.Spriter;
+import com.google.gwt.resources.css.SubstitutionCollector;
+import com.google.gwt.resources.css.SubstitutionReplacer;
 import com.google.gwt.resources.css.ast.CollapsedNode;
-import com.google.gwt.resources.css.ast.Context;
 import com.google.gwt.resources.css.ast.CssCompilerException;
 import com.google.gwt.resources.css.ast.CssDef;
-import com.google.gwt.resources.css.ast.CssEval;
-import com.google.gwt.resources.css.ast.CssExternalSelectors;
 import com.google.gwt.resources.css.ast.CssIf;
-import com.google.gwt.resources.css.ast.CssMediaRule;
-import com.google.gwt.resources.css.ast.CssModVisitor;
-import com.google.gwt.resources.css.ast.CssNoFlip;
 import com.google.gwt.resources.css.ast.CssNode;
-import com.google.gwt.resources.css.ast.CssNodeCloner;
 import com.google.gwt.resources.css.ast.CssProperty;
 import com.google.gwt.resources.css.ast.CssRule;
-import com.google.gwt.resources.css.ast.CssSelector;
-import com.google.gwt.resources.css.ast.CssSprite;
 import com.google.gwt.resources.css.ast.CssStylesheet;
-import com.google.gwt.resources.css.ast.CssUrl;
-import com.google.gwt.resources.css.ast.CssVisitor;
 import com.google.gwt.resources.css.ast.HasNodes;
 import com.google.gwt.resources.css.ast.CssProperty.DotPathValue;
-import com.google.gwt.resources.css.ast.CssProperty.ExpressionValue;
-import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
 import com.google.gwt.resources.css.ast.CssProperty.ListValue;
 import com.google.gwt.resources.css.ast.CssProperty.NumberValue;
 import com.google.gwt.resources.css.ast.CssProperty.Value;
@@ -79,341 +73,23 @@
 import com.google.gwt.user.rebind.StringSourceWriter;
 
 import java.io.Serializable;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.net.URL;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
-import java.util.regex.Matcher;
 import java.util.zip.Adler32;
 
 /**
  * Provides implementations of CSSResources.
  */
 public final class CssResourceGenerator extends AbstractResourceGenerator {
-  static class ClassRenamer extends CssVisitor {
-
-    /**
-     * A tag to indicate that an externally-defined CSS class has no JMethod
-     * that is used to access it.
-     */
-    private static final Replacement UNREFERENCED_EXTERNAL = new Replacement(
-        null, null);
-
-    /*
-     * TODO: Replace with Pair<A, B>.
-     */
-    private static class Replacement {
-
-      private JMethod method;
-      private String obfuscatedClassName;
-
-      public Replacement(JMethod method, String obfuscatedClassName) {
-        this.method = method;
-        this.obfuscatedClassName = obfuscatedClassName;
-      }
-
-      public JMethod getMethod() {
-        return method;
-      }
-
-      public String getObfuscatedClassName() {
-        return obfuscatedClassName;
-      }
-
-      /**
-       * For debugging use only.
-       */
-      public String toString() {
-        if (this == UNREFERENCED_EXTERNAL) {
-          return "Unreferenced external class name";
-        } else {
-          return method.getName() + "=" + obfuscatedClassName;
-        }
-      }
-    }
-
-    /**
-     * Records replacements that have actually been performed.
-     */
-    private final Map<JMethod, String> actualReplacements = new IdentityHashMap<JMethod, String>();
-    private final Set<String> cssDefs = new HashSet<String>();
-
-    /**
-     * The task-list of replacements to perform in the stylesheet.
-     */
-    private final Map<String, Replacement> potentialReplacements;
-    private final TreeLogger logger;
-    private final Set<JMethod> missingClasses;
-    private final boolean strict;
-    private final Set<String> unknownClasses = new HashSet<String>();
-
-    public ClassRenamer(TreeLogger logger,
-        Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
-        boolean strict, Set<String> externalClasses) {
-      this.logger = logger.branch(TreeLogger.DEBUG, "Replacing CSS class names");
-      this.strict = strict;
-
-      potentialReplacements = computeReplacements(classReplacementsWithPrefix,
-          externalClasses);
-
-      // Require a definition for all classes in the default namespace
-      assert classReplacementsWithPrefix.containsKey("");
-      missingClasses = new HashSet<JMethod>(
-          classReplacementsWithPrefix.get("").keySet());
-    }
-
-    @Override
-    public void endVisit(CssDef x, Context ctx) {
-      cssDefs.add(x.getKey());
-    }
-
-    @Override
-    public void endVisit(CssSelector x, Context ctx) {
-
-      String sel = x.getSelector();
-      int originalLength = sel.length();
-
-      Matcher ma = CssSelector.CLASS_SELECTOR_PATTERN.matcher(sel);
-      StringBuilder sb = new StringBuilder(originalLength);
-      int start = 0;
-
-      while (ma.find()) {
-        String sourceClassName = ma.group(1);
-
-        Replacement entry = potentialReplacements.get(sourceClassName);
-
-        if (entry == null) {
-          unknownClasses.add(sourceClassName);
-          continue;
-
-        } else if (entry == UNREFERENCED_EXTERNAL) {
-          // An @external without an accessor method. This is OK.
-          continue;
-        }
-
-        JMethod method = entry.getMethod();
-        String obfuscatedClassName = entry.getObfuscatedClassName();
-
-        // Consume the interstitial portion of the original selector
-        sb.append(sel.subSequence(start, ma.start(1)));
-        sb.append(obfuscatedClassName);
-        start = ma.end(1);
-
-        actualReplacements.put(method, obfuscatedClassName);
-        missingClasses.remove(method);
-      }
-
-      if (start != 0) {
-        // Consume the remainder and update the selector
-        sb.append(sel.subSequence(start, originalLength));
-        x.setSelector(sb.toString());
-      }
-    }
-
-    @Override
-    public void endVisit(CssStylesheet x, Context ctx) {
-      boolean stop = false;
-
-      // Skip names corresponding to @def entries. They too can be declared as
-      // String accessors.
-      List<JMethod> toRemove = new ArrayList<JMethod>();
-      for (JMethod method : missingClasses) {
-        if (cssDefs.contains(method.getName())) {
-          toRemove.add(method);
-        }
-      }
-      for (JMethod method : toRemove) {
-        missingClasses.remove(method);
-      }
-
-      if (!missingClasses.isEmpty()) {
-        stop = true;
-        TreeLogger errorLogger = logger.branch(TreeLogger.INFO,
-            "The following obfuscated style classes were missing from "
-                + "the source CSS file:");
-        for (JMethod m : missingClasses) {
-          String name = m.getName();
-          ClassName className = m.getAnnotation(ClassName.class);
-          if (className != null) {
-            name = className.value();
-          }
-          errorLogger.log(TreeLogger.ERROR, name + ": Fix by adding ." + name
-              + "{}");
-        }
-      }
-
-      if (strict && !unknownClasses.isEmpty()) {
-        stop = true;
-        TreeLogger errorLogger = logger.branch(TreeLogger.ERROR,
-            "The following unobfuscated classes were present in a strict CssResource:");
-        for (String s : unknownClasses) {
-          errorLogger.log(TreeLogger.ERROR, s);
-        }
-        errorLogger.log(TreeLogger.INFO, "Fix by adding String accessor "
-            + "method(s) to the CssResource interface for obfuscated classes, "
-            + "or using an @external declaration for unobfuscated classes.");
-      }
-
-      if (stop) {
-        throw new CssCompilerException("Missing a CSS replacement");
-      }
-    }
-
-    /**
-     * Reports the replacements that were actually performed by this visitor.
-     */
-    public Map<JMethod, String> getReplacements() {
-      return actualReplacements;
-    }
-
-    /**
-     * Flatten class name lookups to speed selector rewriting.
-     * 
-     * @param classReplacementsWithPrefix a map of local prefixes to the
-     *          obfuscated names of imported methods. If a CssResource makes use
-     *          of the {@link Import} annotation, the keys of this map will
-     *          correspond to the {@link ImportedWithPrefix} value defined on
-     *          the imported CssResource. The zero-length string key holds the
-     *          obfuscated names for the CssResource that is being generated.
-     * @return A flattened version of the classReplacementWithPrefix map, where
-     *         the keys are the source class name (with prefix included), and
-     *         values have the obfuscated class name and associated JMethod.
-     */
-    private Map<String, Replacement> computeReplacements(
-        Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
-        Set<String> externalClasses) {
-
-      Map<String, Replacement> toReturn = new HashMap<String, Replacement>();
-
-      for (String externalClass : externalClasses) {
-        toReturn.put(externalClass, UNREFERENCED_EXTERNAL);
-      }
-
-      for (Map.Entry<String, Map<JMethod, String>> outerEntry : classReplacementsWithPrefix.entrySet()) {
-        String prefix = outerEntry.getKey();
-
-        for (Map.Entry<JMethod, String> entry : outerEntry.getValue().entrySet()) {
-          JMethod method = entry.getKey();
-          String sourceClassName = method.getName();
-          String obfuscatedClassName = entry.getValue();
-
-          ClassName className = method.getAnnotation(ClassName.class);
-          if (className != null) {
-            sourceClassName = className.value();
-          }
-
-          sourceClassName = prefix + sourceClassName;
-
-          if (externalClasses.contains(sourceClassName)) {
-            /*
-             * It simplifies the sanity-checking logic to treat external classes
-             * as though they were simply obfuscated to exactly the value the
-             * user wants.
-             */
-            obfuscatedClassName = sourceClassName;
-          }
-
-          toReturn.put(sourceClassName, new Replacement(method,
-              obfuscatedClassName));
-        }
-      }
-      return Collections.unmodifiableMap(toReturn);
-    }
-  }
-
-  static class DefsCollector extends CssVisitor {
-    private final Set<String> defs = new HashSet<String>();
-
-    @Override
-    public void endVisit(CssDef x, Context ctx) {
-      defs.add(x.getKey());
-    }
-  }
-
-  /**
-   * Collects all {@code @external} declarations in the stylesheet.
-   */
-  static class ExternalClassesCollector extends CssVisitor {
-    private final Set<String> classes = new HashSet<String>();
-
-    @Override
-    public void endVisit(CssExternalSelectors x, Context ctx) {
-      classes.addAll(x.getClasses());
-    }
-
-    public Set<String> getClasses() {
-      return classes;
-    }
-  }
-
-  /**
-   * Statically evaluates {@literal @if} rules.
-   */
-  static class IfEvaluator extends CssModVisitor {
-    private final TreeLogger logger;
-    private final PropertyOracle oracle;
-
-    public IfEvaluator(TreeLogger logger, PropertyOracle oracle) {
-      this.logger = logger.branch(TreeLogger.DEBUG,
-          "Replacing property-based @if blocks");
-      this.oracle = oracle;
-    }
-
-    @Override
-    public void endVisit(CssIf x, Context ctx) {
-      if (x.getExpression() != null) {
-        // This gets taken care of by the runtime substitution visitor
-      } else {
-        try {
-          String propertyName = x.getPropertyName();
-          String propValue = null;
-          try {
-            SelectionProperty selProp = oracle.getSelectionProperty(logger,
-                propertyName);
-            propValue = selProp.getCurrentValue();
-          } catch (BadPropertyValueException e) {
-            ConfigurationProperty confProp = oracle.getConfigurationProperty(propertyName);
-            propValue = confProp.getValues().get(0);
-          }
-
-          /*
-           * If the deferred binding property's value is in the list of values
-           * in the @if rule, move the rules into the @if's context.
-           */
-          if (Arrays.asList(x.getPropertyValues()).contains(propValue)
-              ^ x.isNegated()) {
-            for (CssNode n : x.getNodes()) {
-              ctx.insertBefore(n);
-            }
-          } else {
-            // Otherwise, move the else block into the if statement's position
-            for (CssNode n : x.getElseNodes()) {
-              ctx.insertBefore(n);
-            }
-          }
-
-          // Always delete @if rules that we can statically evaluate
-          ctx.removeMe();
-        } catch (BadPropertyValueException e) {
-          logger.log(TreeLogger.ERROR, "Unable to evaluate @if block", e);
-          throw new CssCompilerException("Unable to parse CSS", e);
-        }
-      }
-    }
-  }
 
   @SuppressWarnings("serial")
   static class JClassOrderComparator implements Comparator<JClassType>,
@@ -424,654 +100,6 @@
   }
 
   /**
-   * Merges rules that have matching selectors.
-   */
-  static class MergeIdenticalSelectorsVisitor extends CssModVisitor {
-    private final Map<String, CssRule> canonicalRules = new HashMap<String, CssRule>();
-    private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
-
-    @Override
-    public boolean visit(CssIf x, Context ctx) {
-      visitInNewContext(x.getNodes());
-      visitInNewContext(x.getElseNodes());
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssMediaRule x, Context ctx) {
-      visitInNewContext(x.getNodes());
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssRule x, Context ctx) {
-      // Assumed to run immediately after SplitRulesVisitor
-      assert x.getSelectors().size() == 1;
-      CssSelector sel = x.getSelectors().get(0);
-
-      if (canonicalRules.containsKey(sel.getSelector())) {
-        CssRule canonical = canonicalRules.get(sel.getSelector());
-
-        // Check everything between the canonical rule and this rule for common
-        // properties. If there are common properties, it would be unsafe to
-        // promote the rule.
-        boolean hasCommon = false;
-        int index = rulesInOrder.indexOf(canonical) + 1;
-        assert index != 0;
-
-        for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
-            && !hasCommon;) {
-          hasCommon = haveCommonProperties(i.next(), x);
-        }
-
-        if (!hasCommon) {
-          // It's safe to promote the rule
-          canonical.getProperties().addAll(x.getProperties());
-          ctx.removeMe();
-          return false;
-        }
-      }
-
-      canonicalRules.put(sel.getSelector(), x);
-      rulesInOrder.add(x);
-      return false;
-    }
-
-    private void visitInNewContext(List<CssNode> nodes) {
-      MergeIdenticalSelectorsVisitor v = new MergeIdenticalSelectorsVisitor();
-      v.acceptWithInsertRemove(nodes);
-      rulesInOrder.addAll(v.rulesInOrder);
-    }
-  }
-
-  /**
-   * Merges rules that have identical content.
-   */
-  static class MergeRulesByContentVisitor extends CssModVisitor {
-    private Map<String, CssRule> rulesByContents = new HashMap<String, CssRule>();
-    private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
-
-    @Override
-    public boolean visit(CssIf x, Context ctx) {
-      visitInNewContext(x.getNodes());
-      visitInNewContext(x.getElseNodes());
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssMediaRule x, Context ctx) {
-      visitInNewContext(x.getNodes());
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssRule x, Context ctx) {
-      StringBuilder b = new StringBuilder();
-      for (CssProperty p : x.getProperties()) {
-        b.append(p.getName()).append(":").append(p.getValues().getExpression());
-      }
-
-      String content = b.toString();
-      CssRule canonical = rulesByContents.get(content);
-
-      // Check everything between the canonical rule and this rule for common
-      // properties. If there are common properties, it would be unsafe to
-      // promote the rule.
-      if (canonical != null) {
-        boolean hasCommon = false;
-        int index = rulesInOrder.indexOf(canonical) + 1;
-        assert index != 0;
-
-        for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
-            && !hasCommon;) {
-          hasCommon = haveCommonProperties(i.next(), x);
-        }
-
-        if (!hasCommon) {
-          canonical.getSelectors().addAll(x.getSelectors());
-          ctx.removeMe();
-          return false;
-        }
-      }
-
-      rulesByContents.put(content, x);
-      rulesInOrder.add(x);
-      return false;
-    }
-
-    private void visitInNewContext(List<CssNode> nodes) {
-      MergeRulesByContentVisitor v = new MergeRulesByContentVisitor();
-      v.acceptWithInsertRemove(nodes);
-      rulesInOrder.addAll(v.rulesInOrder);
-    }
-  }
-
-  static class RequirementsCollector extends CssVisitor {
-    private final TreeLogger logger;
-    private final ClientBundleRequirements requirements;
-
-    public RequirementsCollector(TreeLogger logger,
-        ClientBundleRequirements requirements) {
-      this.logger = logger.branch(TreeLogger.DEBUG,
-          "Scanning CSS for requirements");
-      this.requirements = requirements;
-    }
-
-    @Override
-    public void endVisit(CssIf x, Context ctx) {
-      String propertyName = x.getPropertyName();
-      if (propertyName != null) {
-        try {
-          requirements.addPermutationAxis(propertyName);
-        } catch (BadPropertyValueException e) {
-          logger.log(TreeLogger.ERROR, "Unknown deferred-binding property "
-              + propertyName, e);
-          throw new CssCompilerException("Unknown deferred-binding property", e);
-        }
-      }
-    }
-  }
-
-  static class RtlVisitor extends CssModVisitor {
-    /**
-     * Records if we're currently visiting a CssRule whose only selector is
-     * "body".
-     */
-    private boolean inBodyRule;
-
-    @Override
-    public void endVisit(CssProperty x, Context ctx) {
-      String name = x.getName();
-
-      if (name.equalsIgnoreCase("left")) {
-        x.setName("right");
-      } else if (name.equalsIgnoreCase("right")) {
-        x.setName("left");
-      } else if (name.endsWith("-left")) {
-        int len = name.length();
-        x.setName(name.substring(0, len - 4) + "right");
-      } else if (name.endsWith("-right")) {
-        int len = name.length();
-        x.setName(name.substring(0, len - 5) + "left");
-      } else if (name.contains("-right-")) {
-        x.setName(name.replace("-right-", "-left-"));
-      } else if (name.contains("-left-")) {
-        x.setName(name.replace("-left-", "-right-"));
-      } else {
-        List<Value> values = new ArrayList<Value>(x.getValues().getValues());
-        invokePropertyHandler(x.getName(), values);
-        x.setValue(new CssProperty.ListValue(values));
-      }
-    }
-
-    @Override
-    public boolean visit(CssNoFlip x, Context ctx) {
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssRule x, Context ctx) {
-      inBodyRule = x.getSelectors().size() == 1
-          && x.getSelectors().get(0).getSelector().equals("body");
-      return true;
-    }
-
-    void propertyHandlerBackground(List<Value> values) {
-      /*
-       * The first numeric value will be treated as the left position only if we
-       * havn't seen any value that could potentially be the left value.
-       */
-      boolean seenLeft = false;
-
-      for (ListIterator<Value> it = values.listIterator(); it.hasNext();) {
-        Value v = it.next();
-        Value maybeFlipped = flipLeftRightIdentValue(v);
-        NumberValue nv = v.isNumberValue();
-        if (v != maybeFlipped) {
-          it.set(maybeFlipped);
-          seenLeft = true;
-
-        } else if (isIdent(v, "center")) {
-          seenLeft = true;
-
-        } else if (!seenLeft && (nv != null)) {
-          seenLeft = true;
-          if ("%".equals(nv.getUnits())) {
-            float position = 100f - nv.getValue();
-            it.set(new NumberValue(position, "%"));
-            break;
-          }
-        }
-      }
-    }
-
-    void propertyHandlerBackgroundPosition(List<Value> values) {
-      propertyHandlerBackground(values);
-    }
-
-    Value propertyHandlerBackgroundPositionX(Value v) {
-      ArrayList<Value> list = new ArrayList<Value>(1);
-      list.add(v);
-      propertyHandlerBackground(list);
-      return list.get(0);
-    }
-
-    /**
-     * Note there should be no propertyHandlerBorder(). The CSS spec states that
-     * the border property must set all values at once.
-     */
-    void propertyHandlerBorderColor(List<Value> values) {
-      swapFour(values);
-    }
-
-    void propertyHandlerBorderStyle(List<Value> values) {
-      swapFour(values);
-    }
-
-    void propertyHandlerBorderWidth(List<Value> values) {
-      swapFour(values);
-    }
-
-    Value propertyHandlerClear(Value v) {
-      return propertyHandlerFloat(v);
-    }
-
-    Value propertyHandlerCursor(Value v) {
-      IdentValue identValue = v.isIdentValue();
-      if (identValue == null) {
-        return v;
-      }
-
-      String ident = identValue.getIdent().toLowerCase();
-      if (!ident.endsWith("-resize")) {
-        return v;
-      }
-
-      StringBuffer newIdent = new StringBuffer();
-
-      if (ident.length() == 9) {
-        if (ident.charAt(0) == 'n') {
-          newIdent.append('n');
-          ident = ident.substring(1);
-        } else if (ident.charAt(0) == 's') {
-          newIdent.append('s');
-          ident = ident.substring(1);
-        } else {
-          return v;
-        }
-      }
-
-      if (ident.length() == 8) {
-        if (ident.charAt(0) == 'e') {
-          newIdent.append("w-resize");
-        } else if (ident.charAt(0) == 'w') {
-          newIdent.append("e-resize");
-        } else {
-          return v;
-        }
-        return new IdentValue(newIdent.toString());
-      } else {
-        return v;
-      }
-    }
-
-    Value propertyHandlerDirection(Value v) {
-      if (inBodyRule) {
-        if (isIdent(v, "ltr")) {
-          return new IdentValue("rtl");
-        } else if (isIdent(v, "rtl")) {
-          return new IdentValue("ltr");
-        }
-      }
-      return v;
-    }
-
-    Value propertyHandlerFloat(Value v) {
-      return flipLeftRightIdentValue(v);
-    }
-
-    void propertyHandlerMargin(List<Value> values) {
-      swapFour(values);
-    }
-
-    void propertyHandlerPadding(List<Value> values) {
-      swapFour(values);
-    }
-
-    Value propertyHandlerPageBreakAfter(Value v) {
-      return flipLeftRightIdentValue(v);
-    }
-
-    Value propertyHandlerPageBreakBefore(Value v) {
-      return flipLeftRightIdentValue(v);
-    }
-
-    Value propertyHandlerTextAlign(Value v) {
-      return flipLeftRightIdentValue(v);
-    }
-
-    private Value flipLeftRightIdentValue(Value v) {
-      if (isIdent(v, "right")) {
-        return new IdentValue("left");
-
-      } else if (isIdent(v, "left")) {
-        return new IdentValue("right");
-      }
-      return v;
-    }
-
-    /**
-     * Reflectively invokes a propertyHandler method for the named property.
-     * Dashed names are transformed into camel-case names; only letters
-     * following a dash will be capitalized when looking for a method to prevent
-     * <code>fooBar<code> and <code>foo-bar</code> from colliding.
-     */
-    private void invokePropertyHandler(String name, List<Value> values) {
-      // See if we have a property-handler function
-      try {
-        String[] parts = name.toLowerCase().split("-");
-        StringBuffer methodName = new StringBuffer("propertyHandler");
-        for (String part : parts) {
-          methodName.append(Character.toUpperCase(part.charAt(0)));
-          methodName.append(part, 1, part.length());
-        }
-
-        try {
-          // Single-arg for simplicity
-          Method m = getClass().getDeclaredMethod(methodName.toString(),
-              Value.class);
-          assert Value.class.isAssignableFrom(m.getReturnType());
-          Value newValue = (Value) m.invoke(this, values.get(0));
-          values.set(0, newValue);
-        } catch (NoSuchMethodException e) {
-          // OK
-        }
-
-        try {
-          // Or the whole List for completeness
-          Method m = getClass().getDeclaredMethod(methodName.toString(),
-              List.class);
-          m.invoke(this, values);
-        } catch (NoSuchMethodException e) {
-          // OK
-        }
-
-      } catch (SecurityException e) {
-        throw new CssCompilerException(
-            "Unable to invoke property handler function for " + name, e);
-      } catch (IllegalArgumentException e) {
-        throw new CssCompilerException(
-            "Unable to invoke property handler function for " + name, e);
-      } catch (IllegalAccessException e) {
-        throw new CssCompilerException(
-            "Unable to invoke property handler function for " + name, e);
-      } catch (InvocationTargetException e) {
-        throw new CssCompilerException(
-            "Unable to invoke property handler function for " + name, e);
-      }
-    }
-
-    private boolean isIdent(Value value, String query) {
-      IdentValue v = value.isIdentValue();
-      return v != null && v.getIdent().equalsIgnoreCase(query);
-    }
-
-    /**
-     * Swaps the second and fourth values in a list of four values.
-     */
-    private void swapFour(List<Value> values) {
-      if (values.size() == 4) {
-        Collections.swap(values, 1, 3);
-      }
-    }
-  }
-
-  /**
-   * Splits rules with compound selectors into multiple rules.
-   */
-  static class SplitRulesVisitor extends CssModVisitor {
-    @Override
-    public void endVisit(CssRule x, Context ctx) {
-      if (x.getSelectors().size() == 1) {
-        return;
-      }
-
-      for (CssSelector sel : x.getSelectors()) {
-        CssRule newRule = new CssRule();
-        newRule.getSelectors().add(sel);
-        newRule.getProperties().addAll(
-            CssNodeCloner.clone(CssProperty.class, x.getProperties()));
-        ctx.insertBefore(newRule);
-      }
-      ctx.removeMe();
-      return;
-    }
-  }
-
-  /**
-   * Replaces CssSprite nodes with CssRule nodes that will display the sprited
-   * image. The real trick with spriting the images is to reuse the
-   * ImageResource processing framework by requiring the sprite to be defined in
-   * terms of an ImageResource.
-   */
-  static class Spriter extends CssModVisitor {
-    private final ResourceContext context;
-    private final TreeLogger logger;
-
-    public Spriter(TreeLogger logger, ResourceContext context) {
-      this.logger = logger.branch(TreeLogger.DEBUG,
-          "Creating image sprite classes");
-      this.context = context;
-    }
-
-    @Override
-    public void endVisit(CssSprite x, Context ctx) {
-      JClassType bundleType = context.getClientBundleType();
-      String functionName = x.getResourceFunction();
-
-      if (functionName == null) {
-        logger.log(TreeLogger.ERROR, "The @sprite rule " + x.getSelectors()
-            + " must specify the " + CssSprite.IMAGE_PROPERTY_NAME
-            + " property");
-        throw new CssCompilerException("No image property specified");
-      }
-
-      // Find the image accessor method
-      JMethod imageMethod = null;
-      JMethod[] allMethods = bundleType.getOverridableMethods();
-      for (int i = 0; imageMethod == null && i < allMethods.length; i++) {
-        JMethod candidate = allMethods[i];
-        // If the function name matches and takes no parameters
-        if (candidate.getName().equals(functionName)
-            && candidate.getParameters().length == 0) {
-          // We have a match
-          imageMethod = candidate;
-        }
-      }
-
-      // Method unable to be located
-      if (imageMethod == null) {
-        logger.log(TreeLogger.ERROR, "Unable to find ImageResource method "
-            + functionName + " in " + bundleType.getQualifiedSourceName());
-        throw new CssCompilerException("Cannot find image function");
-      }
-
-      JClassType imageResourceType = context.getGeneratorContext().getTypeOracle().findType(
-          ImageResource.class.getName());
-      assert imageResourceType != null;
-
-      if (!imageResourceType.isAssignableFrom(imageMethod.getReturnType().isClassOrInterface())) {
-        logger.log(TreeLogger.ERROR, "The return type of " + functionName
-            + " is not assignable to "
-            + imageResourceType.getSimpleSourceName());
-        throw new CssCompilerException("Incorrect return type for "
-            + CssSprite.IMAGE_PROPERTY_NAME + " method");
-      }
-
-      ImageOptions options = imageMethod.getAnnotation(ImageOptions.class);
-      RepeatStyle repeatStyle;
-      if (options != null) {
-        repeatStyle = options.repeatStyle();
-      } else {
-        repeatStyle = RepeatStyle.None;
-      }
-
-      String instance = "(" + context.getImplementationSimpleSourceName()
-          + ".this." + functionName + "())";
-
-      CssRule replacement = new CssRule();
-      replacement.getSelectors().addAll(x.getSelectors());
-      List<CssProperty> properties = replacement.getProperties();
-
-      if (repeatStyle == RepeatStyle.None
-          || repeatStyle == RepeatStyle.Horizontal) {
-        properties.add(new CssProperty("height", new ExpressionValue(instance
-            + ".getHeight() + \"px\""), false));
-      }
-
-      if (repeatStyle == RepeatStyle.None
-          || repeatStyle == RepeatStyle.Vertical) {
-        properties.add(new CssProperty("width", new ExpressionValue(instance
-            + ".getWidth() + \"px\""), false));
-      }
-      properties.add(new CssProperty("overflow", new IdentValue("hidden"),
-          false));
-
-      String repeatText;
-      switch (repeatStyle) {
-        case None:
-          repeatText = " no-repeat";
-          break;
-        case Horizontal:
-          repeatText = " repeat-x";
-          break;
-        case Vertical:
-          repeatText = " repeat-y";
-          break;
-        case Both:
-          repeatText = " repeat";
-          break;
-        default:
-          throw new RuntimeException("Unknown repeatStyle " + repeatStyle);
-      }
-
-      String backgroundExpression = "\"url(\\\"\" + " + instance
-          + ".getURL() + \"\\\") -\" + " + instance
-          + ".getLeft() + \"px -\" + " + instance + ".getTop() + \"px "
-          + repeatText + "\"";
-      properties.add(new CssProperty("background", new ExpressionValue(
-          backgroundExpression), false));
-
-      // Retain any user-specified properties
-      properties.addAll(x.getProperties());
-
-      ctx.replaceMe(replacement);
-    }
-  }
-
-  static class SubstitutionCollector extends CssVisitor {
-    private final Map<String, CssDef> substitutions = new HashMap<String, CssDef>();
-
-    @Override
-    public void endVisit(CssDef x, Context ctx) {
-      substitutions.put(x.getKey(), x);
-    }
-
-    @Override
-    public void endVisit(CssEval x, Context ctx) {
-      substitutions.put(x.getKey(), x);
-    }
-
-    @Override
-    public void endVisit(CssUrl x, Context ctx) {
-      substitutions.put(x.getKey(), x);
-    }
-  }
-
-  /**
-   * Substitute symbolic replacements into string values.
-   */
-  static class SubstitutionReplacer extends CssVisitor {
-    private final ResourceContext context;
-    private final TreeLogger logger;
-    private final Map<String, CssDef> substitutions;
-
-    public SubstitutionReplacer(TreeLogger logger, ResourceContext context,
-        Map<String, CssDef> substitutions) {
-      this.context = context;
-      this.logger = logger;
-      this.substitutions = substitutions;
-    }
-
-    @Override
-    public void endVisit(CssProperty x, Context ctx) {
-      if (x.getValues() == null) {
-        // Nothing to do
-        return;
-      }
-
-      List<Value> values = new ArrayList<Value>(x.getValues().getValues());
-
-      for (ListIterator<Value> i = values.listIterator(); i.hasNext();) {
-        IdentValue v = i.next().isIdentValue();
-
-        if (v == null) {
-          // Don't try to substitute into anything other than idents
-          continue;
-        }
-
-        String value = v.getIdent();
-        CssDef def = substitutions.get(value);
-
-        if (def == null) {
-          continue;
-        } else if (def instanceof CssUrl) {
-          assert def.getValues().size() == 1;
-          assert def.getValues().get(0).isIdentValue() != null;
-          String functionName = def.getValues().get(0).isIdentValue().getIdent();
-
-          // Find the method
-          JMethod methods[] = context.getClientBundleType().getOverridableMethods();
-          boolean foundMethod = false;
-          if (methods != null) {
-            for (JMethod method : methods) {
-              if (method.getName().equals(functionName)) {
-                foundMethod = true;
-                break;
-              }
-            }
-          }
-
-          if (!foundMethod) {
-            logger.log(TreeLogger.ERROR, "Unable to find DataResource method "
-                + functionName + " in "
-                + context.getClientBundleType().getQualifiedSourceName());
-            throw new CssCompilerException("Cannot find data function");
-          }
-
-          String instance = "((" + DataResource.class.getName() + ")("
-              + context.getImplementationSimpleSourceName() + ".this."
-              + functionName + "()))";
-
-          StringBuilder expression = new StringBuilder();
-          expression.append("\"url('\" + ");
-          expression.append(instance).append(".getUrl()");
-          expression.append(" + \"')\"");
-          i.set(new ExpressionValue(expression.toString()));
-
-        } else {
-          i.remove();
-          for (Value defValue : def.getValues()) {
-            i.add(defValue);
-          }
-        }
-      }
-
-      x.setValue(new ListValue(values));
-    }
-  }
-
-  /**
    * A lookup table of base-32 chars we use to encode CSS idents. Because CSS
    * class selectors may be case-insensitive, we don't have enough characters to
    * use a base-64 encoding.
@@ -1096,13 +124,7 @@
   private static final String KEY_CLASS_PREFIX = "prefix";
   private static final String KEY_CLASS_COUNTER = "counter";
 
-  public static void main(String[] args) {
-    for (int i = 0; i < 1000; i++) {
-      System.out.println(makeIdent(i));
-    }
-  }
-
-  static boolean haveCommonProperties(CssRule a, CssRule b) {
+  public static boolean haveCommonProperties(CssRule a, CssRule b) {
     if (a.getProperties().size() == 0 || b.getProperties().size() == 0) {
       return false;
     }
@@ -1149,6 +171,12 @@
     return false;
   }
 
+  public static void main(String[] args) {
+    for (int i = 0; i < 1000; i++) {
+      System.out.println(makeIdent(i));
+    }
+  }
+
   /**
    * Create a Java expression that evaluates to a string representation of the
    * given node. Visible only for testing.
@@ -1763,7 +791,7 @@
       SubstitutionCollector collector = new SubstitutionCollector();
       collector.accept(sheet);
 
-      (new SubstitutionReplacer(logger, context, collector.substitutions)).accept(sheet);
+      (new SubstitutionReplacer(logger, context, collector.getSubstitutions())).accept(sheet);
 
       // Evaluate @if statements based on deferred binding properties
       (new IfEvaluator(logger,
@@ -1834,7 +862,7 @@
     String name = toImplement.getName();
     // TODO: Annotation for override
 
-    CssDef def = collector.substitutions.get(name);
+    CssDef def = collector.getSubstitutions().get(name);
     if (def == null) {
       logger.log(TreeLogger.ERROR, "No @def rule for name " + name);
       throw new UnableToCompleteException();
@@ -1895,6 +923,7 @@
     // Get list of @defs
     DefsCollector collector = new DefsCollector();
     collector.accept(sheet);
+    Set<String> defs = collector.getDefs();
 
     for (JMethod toImplement : methods) {
       String name = toImplement.getName();
@@ -1903,14 +932,13 @@
       }
 
       // Bomb out if there is a collision between @def and a style name
-      if (collector.defs.contains(name)
-          && obfuscatedClassNames.containsKey(toImplement)) {
+      if (defs.contains(name) && obfuscatedClassNames.containsKey(toImplement)) {
         logger.log(TreeLogger.ERROR, "@def shadows CSS class name: " + name
             + ". Fix by renaming the @def name or the CSS class name.");
         throw new UnableToCompleteException();
       }
 
-      if (collector.defs.contains(toImplement.getName())
+      if (defs.contains(toImplement.getName())
           && toImplement.getParameters().length == 0) {
         writeDefAssignment(logger, sw, toImplement, sheet);
       } else if (toImplement.getReturnType().equals(stringType)
diff --git a/user/test/com/google/gwt/resources/ResourcesSuite.java b/user/test/com/google/gwt/resources/ResourcesSuite.java
index 7826df2..98538b9 100644
--- a/user/test/com/google/gwt/resources/ResourcesSuite.java
+++ b/user/test/com/google/gwt/resources/ResourcesSuite.java
@@ -21,10 +21,10 @@
 import com.google.gwt.resources.client.ImageResourceTest;
 import com.google.gwt.resources.client.NestedBundleTest;
 import com.google.gwt.resources.client.TextResourceTest;
+import com.google.gwt.resources.css.CssNodeClonerTest;
+import com.google.gwt.resources.css.CssReorderTest;
+import com.google.gwt.resources.css.CssRtlTest;
 import com.google.gwt.resources.css.ExtractClassNamesVisitorTest;
-import com.google.gwt.resources.rg.CssNodeClonerTest;
-import com.google.gwt.resources.rg.CssReorderTest;
-import com.google.gwt.resources.rg.CssRtlTest;
 
 import junit.framework.Test;
 
diff --git a/user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java b/user/test/com/google/gwt/resources/css/CssNodeClonerTest.java
similarity index 96%
rename from user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java
rename to user/test/com/google/gwt/resources/css/CssNodeClonerTest.java
index 24c5042..517de81 100644
--- a/user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java
+++ b/user/test/com/google/gwt/resources/css/CssNodeClonerTest.java
@@ -13,16 +13,16 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.resources.rg;
+package com.google.gwt.resources.css;
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.resources.css.GenerateCssAst;
 import com.google.gwt.resources.css.ast.CssNode;
 import com.google.gwt.resources.css.ast.CssNodeCloner;
 import com.google.gwt.resources.css.ast.CssProperty;
 import com.google.gwt.resources.css.ast.CssSelector;
 import com.google.gwt.resources.css.ast.CssStylesheet;
+import com.google.gwt.resources.rg.CssTestCase;
 
 import java.net.URL;
 import java.util.List;
diff --git a/user/test/com/google/gwt/resources/rg/CssReorderTest.java b/user/test/com/google/gwt/resources/css/CssReorderTest.java
similarity index 82%
rename from user/test/com/google/gwt/resources/rg/CssReorderTest.java
rename to user/test/com/google/gwt/resources/css/CssReorderTest.java
index 2777548..0b3a62e 100644
--- a/user/test/com/google/gwt/resources/rg/CssReorderTest.java
+++ b/user/test/com/google/gwt/resources/css/CssReorderTest.java
@@ -13,14 +13,12 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.resources.rg;
+package com.google.gwt.resources.css;
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.resources.css.ast.CssVisitor;
-import com.google.gwt.resources.rg.CssResourceGenerator.MergeIdenticalSelectorsVisitor;
-import com.google.gwt.resources.rg.CssResourceGenerator.MergeRulesByContentVisitor;
-import com.google.gwt.resources.rg.CssResourceGenerator.SplitRulesVisitor;
+import com.google.gwt.resources.rg.CssTestCase;
 
 /**
  * Tests CSS reordering visitors.
diff --git a/user/test/com/google/gwt/resources/rg/CssRtlTest.java b/user/test/com/google/gwt/resources/css/CssRtlTest.java
similarity index 94%
rename from user/test/com/google/gwt/resources/rg/CssRtlTest.java
rename to user/test/com/google/gwt/resources/css/CssRtlTest.java
index ac0bc8e..fa75951 100644
--- a/user/test/com/google/gwt/resources/rg/CssRtlTest.java
+++ b/user/test/com/google/gwt/resources/css/CssRtlTest.java
@@ -13,12 +13,12 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.resources.rg;
+package com.google.gwt.resources.css;
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.resources.css.ast.CssVisitor;
-import com.google.gwt.resources.rg.CssResourceGenerator.RtlVisitor;
+import com.google.gwt.resources.rg.CssTestCase;
 
 /**
  * This is a static test of the automatic RTL support.
diff --git a/user/test/com/google/gwt/resources/rg/backgroundProperties_expected.css b/user/test/com/google/gwt/resources/css/backgroundProperties_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/backgroundProperties_expected.css
rename to user/test/com/google/gwt/resources/css/backgroundProperties_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/backgroundProperties_test.css b/user/test/com/google/gwt/resources/css/backgroundProperties_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/backgroundProperties_test.css
rename to user/test/com/google/gwt/resources/css/backgroundProperties_test.css
diff --git a/user/test/com/google/gwt/resources/rg/cursorProperties_expected.css b/user/test/com/google/gwt/resources/css/cursorProperties_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/cursorProperties_expected.css
rename to user/test/com/google/gwt/resources/css/cursorProperties_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/cursorProperties_test.css b/user/test/com/google/gwt/resources/css/cursorProperties_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/cursorProperties_test.css
rename to user/test/com/google/gwt/resources/css/cursorProperties_test.css
diff --git a/user/test/com/google/gwt/resources/rg/directionProperty_expected.css b/user/test/com/google/gwt/resources/css/directionProperty_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/directionProperty_expected.css
rename to user/test/com/google/gwt/resources/css/directionProperty_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/directionProperty_test.css b/user/test/com/google/gwt/resources/css/directionProperty_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/directionProperty_test.css
rename to user/test/com/google/gwt/resources/css/directionProperty_test.css
diff --git a/user/test/com/google/gwt/resources/rg/fourValuedProperties_expected.css b/user/test/com/google/gwt/resources/css/fourValuedProperties_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/fourValuedProperties_expected.css
rename to user/test/com/google/gwt/resources/css/fourValuedProperties_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/fourValuedProperties_test.css b/user/test/com/google/gwt/resources/css/fourValuedProperties_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/fourValuedProperties_test.css
rename to user/test/com/google/gwt/resources/css/fourValuedProperties_test.css
diff --git a/user/test/com/google/gwt/resources/rg/leftRightProperties_expected.css b/user/test/com/google/gwt/resources/css/leftRightProperties_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/leftRightProperties_expected.css
rename to user/test/com/google/gwt/resources/css/leftRightProperties_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/leftRightProperties_test.css b/user/test/com/google/gwt/resources/css/leftRightProperties_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/leftRightProperties_test.css
rename to user/test/com/google/gwt/resources/css/leftRightProperties_test.css
diff --git a/user/test/com/google/gwt/resources/rg/noflip_expected.css b/user/test/com/google/gwt/resources/css/noflip_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/noflip_expected.css
rename to user/test/com/google/gwt/resources/css/noflip_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/noflip_test.css b/user/test/com/google/gwt/resources/css/noflip_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/noflip_test.css
rename to user/test/com/google/gwt/resources/css/noflip_test.css
diff --git a/user/test/com/google/gwt/resources/rg/propertyMerging_expected.css b/user/test/com/google/gwt/resources/css/propertyMerging_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/propertyMerging_expected.css
rename to user/test/com/google/gwt/resources/css/propertyMerging_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/propertyMerging_test.css b/user/test/com/google/gwt/resources/css/propertyMerging_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/propertyMerging_test.css
rename to user/test/com/google/gwt/resources/css/propertyMerging_test.css
diff --git a/user/test/com/google/gwt/resources/rg/selectorMerging_expected.css b/user/test/com/google/gwt/resources/css/selectorMerging_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/selectorMerging_expected.css
rename to user/test/com/google/gwt/resources/css/selectorMerging_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/selectorMerging_test.css b/user/test/com/google/gwt/resources/css/selectorMerging_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/selectorMerging_test.css
rename to user/test/com/google/gwt/resources/css/selectorMerging_test.css
diff --git a/user/test/com/google/gwt/resources/rg/CssTestCase.java b/user/test/com/google/gwt/resources/rg/CssTestCase.java
index 01e7555..33cbc0b 100644
--- a/user/test/com/google/gwt/resources/rg/CssTestCase.java
+++ b/user/test/com/google/gwt/resources/rg/CssTestCase.java
@@ -38,13 +38,17 @@
  * modifications to the CSS AST.
  */
 public class CssTestCase extends TestCase {
+  /*
+   * NB: This class is in the resources.rg package so that it can acess
+   * package-protected methods in CssResourceGenerator.
+   */
 
   /**
    * Triggers an assertion if a CssNode is traversed more than once.
    * 
    * @see CssTestCase#assertNoAliasing(CssNode)
    */
-  protected static class AliasDetector extends CssVisitor {
+  public static class AliasDetector extends CssVisitor {
     private final Map<CssNode, Void> seen = new IdentityHashMap<CssNode, Void>();
 
     @Override