com.google.gwt.resources.css.GenerateCssAst.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.resources.css.GenerateCssAst.java

Source

/*
 * Copyright 2008 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.UnableToCompleteException;
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.CssNoFlip;
import com.google.gwt.resources.css.ast.CssNode;
import com.google.gwt.resources.css.ast.CssPageRule;
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.CssUnknownAtRule;
import com.google.gwt.resources.css.ast.CssUrl;
import com.google.gwt.resources.css.ast.HasNodes;
import com.google.gwt.resources.css.ast.HasProperties;
import com.google.gwt.resources.css.ast.CssProperty.DotPathValue;
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.StringValue;
import com.google.gwt.resources.css.ast.CssProperty.TokenValue;
import com.google.gwt.resources.css.ast.CssProperty.Value;

import org.w3c.css.sac.AttributeCondition;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.CharacterDataSelector;
import org.w3c.css.sac.CombinatorCondition;
import org.w3c.css.sac.Condition;
import org.w3c.css.sac.ConditionalSelector;
import org.w3c.css.sac.ContentCondition;
import org.w3c.css.sac.DescendantSelector;
import org.w3c.css.sac.DocumentHandler;
import org.w3c.css.sac.ElementSelector;
import org.w3c.css.sac.ErrorHandler;
import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.LangCondition;
import org.w3c.css.sac.LexicalUnit;
import org.w3c.css.sac.NegativeCondition;
import org.w3c.css.sac.NegativeSelector;
import org.w3c.css.sac.PositionalCondition;
import org.w3c.css.sac.ProcessingInstructionSelector;
import org.w3c.css.sac.SACMediaList;
import org.w3c.css.sac.Selector;
import org.w3c.css.sac.SelectorList;
import org.w3c.css.sac.SiblingSelector;
import org.w3c.flute.parser.Parser;

import java.io.IOException;
import java.io.StringReader;
import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Generates a CssStylesheet from the contents of a URL.
 */
@SuppressWarnings("unused")
public class GenerateCssAst {

    /**
     * Maps SAC CSSParseExceptions into a TreeLogger. All parsing errors will be
     * recorded in a single TreeLogger branch, which will be created only if a
     * loggable error message is emitted.
     */
    private static class Errors implements ErrorHandler {
        /**
         * A flag that controls whether or not the exec method will fail.
         */
        private boolean fatalErrorEncountered;
        private TreeLogger logger;
        private final TreeLogger parentLogger;

        /**
         * Constructor.
         * 
         * @param parentLogger the TreeLogger that should be branched to produce the
         *          CSS parsing messages.
         */
        public Errors(TreeLogger parentLogger) {
            this.parentLogger = parentLogger;
        }

        public TreeLogger branch(TreeLogger.Type type, String message) {
            return branch(type, message, null);
        }

        public TreeLogger branch(TreeLogger.Type type, String message, Throwable t) {
            return logOrBranch(type, message, t, true);
        }

        public void error(CSSParseException exception) throws CSSException {
            // TODO Since this indicates a loss of data, should this be a fatal error?
            log(TreeLogger.WARN, exception);
        }

        public void fatalError(CSSParseException exception) throws CSSException {
            log(TreeLogger.ERROR, exception);
        }

        public void log(TreeLogger.Type type, String message) {
            log(type, message, null);
        }

        public void log(TreeLogger.Type type, String message, Throwable t) {
            logOrBranch(type, message, t, false);
        }

        public void warning(CSSParseException exception) throws CSSException {
            log(TreeLogger.DEBUG, exception);
        }

        private void log(TreeLogger.Type type, CSSParseException e) {
            log(type, "Line " + e.getLineNumber() + " column " + e.getColumnNumber() + ": " + e.getMessage());
        }

        private TreeLogger logOrBranch(TreeLogger.Type type, String message, Throwable t, boolean branch) {
            fatalErrorEncountered |= type == TreeLogger.ERROR;
            if (parentLogger.isLoggable(type)) {
                maybeBranch();
                if (branch) {
                    return logger.branch(type, message, t);
                } else {
                    logger.log(type, message, t);
                    return null;
                }
            } else {
                return TreeLogger.NULL;
            }
        }

        private void maybeBranch() {
            if (logger == null) {
                logger = parentLogger.branch(TreeLogger.INFO, "The following problems were detected");
            }
        }
    }

    /**
     * Maps the SAC model into our own CSS AST nodes.
     */
    private static class GenerationHandler implements DocumentHandler {
        /**
         * The stylesheet that is being composed.
         */
        private final CssStylesheet css = new CssStylesheet();

        /**
         * Accumulates CSS nodes as they are created.
         */
        private final Stack<HasNodes> currentParent = new Stack<HasNodes>();

        /**
         * Accumulates CSS properties as they are seen.
         */
        private HasProperties currentRule;

        /**
         * Records references to {@code @def} rules.
         */
        private final Map<String, CssDef> defs = new HashMap<String, CssDef>();

        /**
         * Used when parsing the contents of meta-styles.
         */
        private final Errors errors;

        /**
         * Used by {@link #startSelector(SelectorList)} to suppress the creation of
         * new CssRules in favor of retaining {@link #currentRule}.
         */
        private boolean nextSelectorCreatesRule = true;

        public GenerationHandler(Errors errors) {
            this.errors = errors;
            currentParent.push(css);
        }

        public void comment(String text) throws CSSException {
            // Ignore comments
            // TODO Should comments be retained but not generally printed?
        }

        public void endDocument(InputSource source) throws CSSException {
        }

        public void endFontFace() throws CSSException {
        }

        public void endMedia(SACMediaList media) throws CSSException {
            currentParent.pop();
        }

        public void endPage(String name, String pseudoPage) throws CSSException {
        }

        public void endSelector(SelectorList selectors) throws CSSException {
        }

        /**
         * Reflectively invoke a method named parseRule on this instance.
         */
        public void ignorableAtRule(String atRule) throws CSSException {
            int idx = atRule.indexOf(" ");
            if (idx == -1) {
                // Empty rule like @foo;
                addNode(new CssUnknownAtRule(atRule));
                return;
            }
            String ruleName = atRule.substring(1, idx);
            String methodName = "parse" + (Character.toUpperCase(ruleName.charAt(0)))
                    + ruleName.substring(1).toLowerCase();
            try {
                Method parseMethod = getClass().getDeclaredMethod(methodName, String.class);
                parseMethod.invoke(this, atRule);
            } catch (NoSuchMethodException e) {
                // A rule like @-webkit-keyframe {...} that we can't process
                addNode(new CssUnknownAtRule(atRule));
            } catch (IllegalAccessException e) {
                errors.log(TreeLogger.ERROR, "Unable to invoke parse method ", e);
            } catch (InvocationTargetException e) {
                Throwable cause = e.getCause();

                if (cause instanceof CSSException) {
                    // Unwind a CSSException normally
                    throw (CSSException) cause;
                } else if (cause != null) {
                    // Otherwise, report the message nicely
                    TreeLogger details = errors.branch(TreeLogger.ERROR, cause.getMessage());
                    details.log(TreeLogger.DEBUG, "Full stack trace", cause);
                } else {
                    TreeLogger details = errors.branch(TreeLogger.ERROR, "Unknown failure parsing " + ruleName);
                    details.log(TreeLogger.DEBUG, "Full stack trace", e);
                }
            }
        }

        public void importStyle(String uri, SACMediaList media, String defaultNamespaceURI) throws CSSException {
        }

        public void namespaceDeclaration(String prefix, String uri) throws CSSException {
        }

        public void property(String name, LexicalUnit value, boolean important) throws CSSException {
            List<Value> values = new ArrayList<Value>();
            if (value != null) {
                extractValueOf(values, value);
            }
            currentRule.getProperties().add(new CssProperty(escapeIdent(name), new ListValue(values), important));
        }

        public void startDocument(InputSource source) throws CSSException {
        }

        public void startFontFace() throws CSSException {
        }

        public void startMedia(SACMediaList media) throws CSSException {
            CssMediaRule r = new CssMediaRule();
            for (int i = 0; i < media.getLength(); i++) {
                r.getMedias().add(media.item(i));
            }

            pushParent(r);
        }

        public void startPage(String name, String pseudoPage) throws CSSException {
            CssPageRule r = new CssPageRule();
            // name appears to be unused in CSS2
            r.setPseudoPage(pseudoPage);
            addNode(r);
            currentRule = r;
        }

        public void startSelector(SelectorList selectors) throws CSSException {
            CssRule r;

            if (nextSelectorCreatesRule) {
                r = new CssRule();
                addNode(r);
                currentRule = r;
            } else {
                r = (CssRule) currentRule;
                nextSelectorCreatesRule = true;
            }

            for (int i = 0; i < selectors.getLength(); i++) {
                r.getSelectors().add(new CssSelector(valueOf(selectors.item(i))));
            }
        }

        void parseDef(String atRule) {
            String value = atRule.substring(4, atRule.length()).trim();

            InputSource s = new InputSource();
            s.setCharacterStream(new StringReader(value));
            Parser parser = new Parser();
            parser.setErrorHandler(errors);

            final List<Value> values = new ArrayList<Value>();
            parser.setDocumentHandler(new PropertyExtractor(values));

            try {
                String dummy = "* { prop : " + value + "}";
                parser.parseStyleSheet(new InputSource(new StringReader(dummy)));
            } catch (IOException e) {
                assert false : "Should never happen";
            }

            if (values.size() < 2) {
                throw new CSSException(CSSException.SAC_SYNTAX_ERR,
                        "@def rules must specify an identifier and one or more values", null);
            }

            IdentValue defName = values.get(0).isIdentValue();

            if (defName == null) {
                throw new CSSException(CSSException.SAC_SYNTAX_ERR, "First lexical unit must be an identifier",
                        null);
            }

            /*
             * Replace any references to previously-seen @def constructs. We do
             * expansion up-front to prevent the need for cycle-detection later.
             */
            for (ListIterator<Value> it = values.listIterator(1); it.hasNext();) {
                IdentValue maybeDefReference = it.next().isIdentValue();
                if (maybeDefReference != null) {
                    CssDef previousDef = defs.get(maybeDefReference.getIdent());
                    if (previousDef != null) {
                        it.remove();
                        for (Value previousValue : previousDef.getValues()) {
                            it.add(previousValue);
                        }
                    }
                }
            }

            CssDef def = new CssDef(defName.getIdent());
            def.getValues().addAll(values.subList(1, values.size()));
            addNode(def);

            defs.put(defName.getIdent(), def);
        }

        /**
         * The elif nodes are processed as though they were {@code @if} nodes. The
         * newly-generated CssIf node will be attached to the last CssIf in the
         * if/else chain.
         */
        void parseElif(String atRule) throws CSSException {
            List<CssNode> nodes = currentParent.peek().getNodes();
            CssIf lastIf = findLastIfInChain(nodes);

            if (lastIf == null) {
                throw new CSSException(CSSException.SAC_SYNTAX_ERR, "@elif must immediately follow an @if or @elif",
                        null);
            }

            assert lastIf.getElseNodes().isEmpty();

            // @elif -> lif (because parseIf strips the first three chars)
            parseIf(atRule.substring(2));

            // Fix up the structure by remove the newly-created node from the parent
            // context and moving it to the end of the @if chain
            lastIf.getElseNodes().add(nodes.remove(nodes.size() - 1));
        }

        /**
         * The else nodes are processed as though they were written as {@code @elif
         * true} rules.
         */
        void parseElse(String atRule) throws CSSException {
            // The last CssIf in the if/else chain
            CssIf lastIf = findLastIfInChain(currentParent.peek().getNodes());

            if (lastIf == null) {
                throw new CSSException(CSSException.SAC_SYNTAX_ERR, "@else must immediately follow an @if or @elif",
                        null);
            }

            // Create the CssIf to hold the @else rules
            String fakeElif = "@elif (true) " + atRule.substring(atRule.indexOf("{"));
            parseElif(fakeElif);
            CssIf elseIf = findLastIfInChain(currentParent.peek().getNodes());

            assert lastIf.getElseNodes().size() == 1 && lastIf.getElseNodes().get(0) == elseIf;
            assert elseIf.getElseNodes().isEmpty();

            // Merge the rules into the last CssIf to break the chain and prevent
            // @else followed by @else
            lastIf.getElseNodes().clear();
            lastIf.getElseNodes().addAll(elseIf.getNodes());
        }

        void parseEval(String atRule) throws CSSException {
            // @eval key com.google.Type.staticFunction
            String[] parts = atRule.substring(0, atRule.length() - 1).split("\\s");

            if (parts.length != 3) {
                throw new CSSException(CSSException.SAC_SYNTAX_ERR, "Incorrect number of parts for @eval", null);
            }

            CssEval eval = new CssEval(parts[1], parts[2]);
            addNode(eval);
        }

        void parseExternal(String atRule) throws CSSException {
            // @external .foo, bar; Drop the dots and commas
            String[] parts = atRule.substring(10, atRule.length() - 1).replaceAll("(, *)|( +)", " ")
                    .replaceAll("\\.", "").split(" ");

            CssExternalSelectors externals = new CssExternalSelectors();
            Collections.addAll(externals.getClasses(), parts);
            addNode(externals);
        }

        void parseIf(String atRule) throws CSSException {
            String predicate = atRule.substring(3, atRule.indexOf('{') - 1).trim();
            String blockContents = atRule.substring(atRule.indexOf('{') + 1, atRule.length() - 1);

            CssIf cssIf = new CssIf();

            if (predicate.startsWith("(") && predicate.endsWith(")")) {
                cssIf.setExpression(predicate);
            } else {

                String[] predicateParts = predicate.split("\\s");

                switch (predicateParts.length) {
                case 0:
                    throw new CSSException(CSSException.SAC_SYNTAX_ERR, "Incorrect format for @if predicate", null);
                case 1:
                    if (predicateParts[0].length() == 0) {
                        throw new CSSException(CSSException.SAC_SYNTAX_ERR, "Incorrect format for @if predicate",
                                null);
                    }
                    errors.log(TreeLogger.WARN,
                            "Deprecated syntax for Java expression detected. Enclose the expression in parentheses");
                    cssIf.setExpression(predicateParts[0]);
                    break;
                default:
                    if (predicateParts[0].startsWith("!")) {
                        cssIf.setNegated(true);
                        cssIf.setProperty(predicateParts[0].substring(1));
                    } else {
                        cssIf.setProperty(predicateParts[0]);
                    }
                    String[] values = new String[predicateParts.length - 1];
                    System.arraycopy(predicateParts, 1, values, 0, values.length);
                    cssIf.setPropertyValues(values);
                }
            }

            parseInnerStylesheet("@if", cssIf, blockContents);
        }

        void parseNoflip(String atRule) throws CSSException {
            String blockContents = atRule.substring(atRule.indexOf('{') + 1, atRule.length() - 1);

            parseInnerStylesheet("@noflip", new CssNoFlip(), blockContents);
        }

        void parseSprite(String atRule) throws CSSException {
            CssSprite sprite = new CssSprite();
            currentRule = sprite;
            addNode(sprite);

            // Flag to tell startSelector() to use the CssSprite instead of creating
            // its own CssRule.
            nextSelectorCreatesRule = false;

            // parse the inner text
            InputSource s = new InputSource();
            s.setCharacterStream(new StringReader(atRule.substring(7)));
            Parser parser = new Parser();
            parser.setDocumentHandler(this);
            parser.setErrorHandler(errors);

            try {
                parser.parseRule(s);
            } catch (IOException e) {
                throw new CSSException(CSSException.SAC_SYNTAX_ERR, "Unable to parse @sprite", e);
            }
        }

        void parseUrl(String atRule) throws CSSException {
            // @url key dataResourceFunction
            String[] parts = atRule.substring(0, atRule.length() - 1).split("\\s");

            if (parts.length != 3) {
                throw new CSSException(CSSException.SAC_SYNTAX_ERR, "Incorrect number of parts for @url", null);
            }

            CssUrl url = new CssUrl(parts[1], parts[2]);
            addNode(url);
        }

        /**
         * Add a node to the current parent.
         */
        private void addNode(CssNode node) {
            currentParent.peek().getNodes().add(node);
        }

        private <T extends CssNode & HasNodes> void parseInnerStylesheet(String tagName, T parent,
                String blockContents) {
            pushParent(parent);

            // parse the inner text
            InputSource s = new InputSource();
            s.setCharacterStream(new StringReader(blockContents));
            Parser parser = new Parser();
            parser.setDocumentHandler(this);
            parser.setErrorHandler(errors);

            try {
                parser.parseStyleSheet(s);
            } catch (IOException e) {
                throw new CSSException(CSSException.SAC_SYNTAX_ERR, "Unable to parse " + tagName, e);
            }

            if (currentParent.pop() != parent) {
                // This is a coding error
                throw new RuntimeException("Incorrect element popped");
            }
        }

        /**
         * Adds a node to the current parent and then makes the node the current
         * parent node.
         */
        private <T extends CssNode & HasNodes> void pushParent(T newParent) {
            addNode(newParent);
            currentParent.push(newParent);
        }
    }

    /**
     * Extracts all properties in a document into a List.
     */
    private static class PropertyExtractor implements DocumentHandler {
        private final List<Value> values;

        private PropertyExtractor(List<Value> values) {
            this.values = values;
        }

        public void comment(String text) throws CSSException {
        }

        public void endDocument(InputSource source) throws CSSException {
        }

        public void endFontFace() throws CSSException {
        }

        public void endMedia(SACMediaList media) throws CSSException {
        }

        public void endPage(String name, String pseudoPage) throws CSSException {
        }

        public void endSelector(SelectorList selectors) throws CSSException {
        }

        public void ignorableAtRule(String atRule) throws CSSException {
        }

        public void importStyle(String uri, SACMediaList media, String defaultNamespaceURI) throws CSSException {
        }

        public void namespaceDeclaration(String prefix, String uri) throws CSSException {
        }

        public void property(String name, LexicalUnit value, boolean important) throws CSSException {
            extractValueOf(values, value);
        }

        public void startDocument(InputSource source) throws CSSException {
        }

        public void startFontFace() throws CSSException {
        }

        public void startMedia(SACMediaList media) throws CSSException {
        }

        public void startPage(String name, String pseudoPage) throws CSSException {
        }

        public void startSelector(SelectorList selectors) throws CSSException {
        }
    }

    /**
     * Associates a template CssStylesheet with a timestamp.
     */
    private static class CachedStylesheet {
        private final CssStylesheet sheet;
        private final long timestamp;

        public CachedStylesheet(CssStylesheet sheet, long timestamp) {
            this.sheet = sheet;
            this.timestamp = timestamp;
        }

        public CssStylesheet getCopyOfStylesheet() {
            return new CssStylesheet(sheet);
        }

        public long getTimestamp() {
            return timestamp;
        }
    }

    private static final String LITERAL_FUNCTION_NAME = "literal";
    /**
     * We cache the stylesheets to prevent repeated parsing of the same source
     * material. This is a common case if the user is using UiBinder's implicit
     * stylesheets. It is necessary to use the list of URLs passed to exec because
     * of the eager variable expansion performed by
     * {@link GenerationHandler#parseDef(String)}.
     */
    private static final Map<List<URL>, SoftReference<CachedStylesheet>> SHEETS = Collections
            .synchronizedMap(new HashMap<List<URL>, SoftReference<CachedStylesheet>>());
    private static final String VALUE_FUNCTION_NAME = "value";

    /**
     * Create a CssStylesheet from the contents of one or more URLs. If multiple
     * URLs are provided, the generated stylesheet will be created as though the
     * contents of the URLs had been concatenated.
     */
    public static CssStylesheet exec(TreeLogger logger, URL... stylesheets) throws UnableToCompleteException {

        long mtime = 0;
        for (URL url : stylesheets) {
            long lastModified;
            try {
                lastModified = url.openConnection().getLastModified();
            } catch (IOException e) {
                // Non-fatal, assuming we can re-open the stream later
                logger.log(TreeLogger.DEBUG, "Could not determine cached time", e);
                lastModified = 0;
            }
            if (lastModified == 0) {
                /*
                 * We have to refresh, since the modification date can't be determined,
                 * either due to IOException or getLastModified() not providing useful
                 * data.
                 */
                mtime = Long.MAX_VALUE;
                break;
            } else {
                mtime = Math.max(mtime, lastModified);
            }
        }

        List<URL> sheets = Arrays.asList(stylesheets);
        SoftReference<CachedStylesheet> ref = SHEETS.get(sheets);
        CachedStylesheet toReturn = ref == null ? null : ref.get();
        if (toReturn != null) {
            if (mtime <= toReturn.getTimestamp()) {
                logger.log(TreeLogger.DEBUG, "Using cached result");
                return toReturn.getCopyOfStylesheet();
            } else {
                logger.log(TreeLogger.DEBUG, "Invalidating cached stylesheet");
            }
        }

        Parser p = new Parser();
        Errors errors = new Errors(logger);
        GenerationHandler g = new GenerationHandler(errors);
        p.setDocumentHandler(g);
        p.setErrorHandler(errors);

        for (URL stylesheet : sheets) {
            TreeLogger branchLogger = logger.branch(TreeLogger.DEBUG,
                    "Parsing CSS stylesheet " + stylesheet.toExternalForm());
            try {
                p.parseStyleSheet(stylesheet.toURI().toString());
                continue;
            } catch (CSSException e) {
                branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
            } catch (IOException e) {
                branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
            } catch (URISyntaxException e) {
                branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
            }
            throw new UnableToCompleteException();
        }

        if (errors.fatalErrorEncountered) {
            // Logging will have been performed by the Errors instance, just exit
            throw new UnableToCompleteException();
        }

        toReturn = new CachedStylesheet(g.css, mtime == Long.MAX_VALUE ? 0 : mtime);
        SHEETS.put(new ArrayList<URL>(sheets), new SoftReference<CachedStylesheet>(toReturn));
        return toReturn.getCopyOfStylesheet();
    }

    /**
     * Expresses an rgb function as a hex expression.
     * 
     * @param colors a sequence of LexicalUnits, assumed to be
     *          <code>(VAL COMMA VAL COMMA VAL)</code> where VAL can be an INT or
     *          a PERCENT (which is then converted to INT)
     * @return the minimal hex expression for the RGB color values
     */
    private static Value colorValue(LexicalUnit colors) {
        LexicalUnit red = colors;
        int r = getRgbComponentValue(red);
        LexicalUnit green = red.getNextLexicalUnit().getNextLexicalUnit();
        int g = getRgbComponentValue(green);
        LexicalUnit blue = green.getNextLexicalUnit().getNextLexicalUnit();
        int b = getRgbComponentValue(blue);

        String sr = Integer.toHexString(r);
        if (sr.length() == 1) {
            sr = "0" + sr;
        }

        String sg = Integer.toHexString(g);
        if (sg.length() == 1) {
            sg = "0" + sg;
        }

        String sb = Integer.toHexString(b);
        if (sb.length() == 1) {
            sb = "0" + sb;
        }

        // #AABBCC --> #ABC
        if (sr.charAt(0) == sr.charAt(1) && sg.charAt(0) == sg.charAt(1) && sb.charAt(0) == sb.charAt(1)) {
            sr = sr.substring(1);
            sg = sg.substring(1);
            sb = sb.substring(1);
        }

        return new IdentValue("#" + sr + sg + sb);
    }

    private static String escapeIdent(String selector) {
        assert selector.length() > 0;

        StringBuilder toReturn = new StringBuilder();

        if (selector.charAt(0) == '-') {
            // Allow leading hyphen
            selector = selector.substring(1);
            toReturn.append('-');
        }

        if (!isIdentStart(selector.charAt(0))) {
            toReturn.append('\\');
        }
        toReturn.append(selector.charAt(0));

        if (selector.length() > 1) {
            for (char c : selector.substring(1).toCharArray()) {
                if (!isIdentPart(c)) {
                    toReturn.append('\\');
                }
                toReturn.append(c);
            }
        }
        return toReturn.toString();
    }

    /**
     * Convert a LexicalUnit list into a List of Values.
     */
    private static void extractValueOf(List<Value> accumulator, LexicalUnit value) {
        do {
            accumulator.add(valueOf(value));
            value = value.getNextLexicalUnit();
        } while (value != null);
    }

    /**
     * The elif and else constructs are modeled as nested if statements in the
     * CssIf's elseNodes field. This method will search a list of CssNodes and
     * remove the last chained CssIf from the last element in the list of nodes.
     */
    private static CssIf findLastIfInChain(List<CssNode> nodes) {
        if (nodes.isEmpty()) {
            return null;
        }

        CssNode lastNode = nodes.get(nodes.size() - 1);
        if (lastNode instanceof CssIf) {
            CssIf asIf = (CssIf) lastNode;
            if (asIf.getElseNodes().isEmpty()) {
                return asIf;
            } else {
                return findLastIfInChain(asIf.getElseNodes());
            }
        }
        return null;
    }

    /**
     * Return an integer value from 0-255 for a component of an RGB color.
     * 
     * @param color typed value from the CSS parser, which may be an INTEGER or a
     *          PERCENTAGE
     * @return integer value from 0-255
     * @throws IllegalArgumentException if the color is not an INTEGER or
     *           PERCENTAGE value
     */
    private static int getRgbComponentValue(LexicalUnit color) {
        switch (color.getLexicalUnitType()) {
        case LexicalUnit.SAC_INTEGER:
            return Math.min(color.getIntegerValue(), 255);
        case LexicalUnit.SAC_PERCENTAGE:
            return (int) Math.min(color.getFloatValue() * 255, 255);
        default:
            throw new CSSException(CSSException.SAC_SYNTAX_ERR,
                    "RGB component value must be integer or percentage, was " + color, null);
        }
    }

    private static boolean isIdentPart(char c) {
        return Character.isLetterOrDigit(c) || (c == '\\') || (c == '-') || (c == '_');
    }

    private static boolean isIdentStart(char c) {
        return Character.isLetter(c) || (c == '\\') || (c == '_');
    }

    /**
     * Utility method to concatenate strings.
     */
    private static String join(Iterable<Value> elements, String separator) {
        StringBuilder b = new StringBuilder();
        for (Iterator<Value> i = elements.iterator(); i.hasNext();) {
            b.append(i.next().toCss());
            if (i.hasNext()) {
                b.append(separator);
            }
        }
        return b.toString();
    }

    private static String maybeUnquote(String s) {
        if (s.startsWith("\"") && s.endsWith("\"")) {
            return s.substring(1, s.length() - 1);
        }
        return s;
    }

    /**
     * Used when evaluating literal() rules.
     */
    private static String unescapeLiteral(String s) {
        s = s.replaceAll(Pattern.quote("\\\""), "\"");
        s = s.replaceAll(Pattern.quote("\\\\"), Matcher.quoteReplacement("\\"));
        return s;
    }

    private static String valueOf(Condition condition) {
        if (condition instanceof AttributeCondition) {
            AttributeCondition c = (AttributeCondition) condition;
            switch (c.getConditionType()) {
            case Condition.SAC_ATTRIBUTE_CONDITION:
                return "[" + c.getLocalName() + (c.getValue() != null ? "=\"" + c.getValue() + '"' : "") + "]";
            case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION:
                return "[" + c.getLocalName() + "~=\"" + c.getValue() + "\"]";
            case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
                return "[" + c.getLocalName() + "|=\"" + c.getValue() + "\"]";
            case Condition.SAC_ID_CONDITION:
                return "#" + c.getValue();
            case Condition.SAC_CLASS_CONDITION:
                return "." + c.getValue();
            case Condition.SAC_PSEUDO_CLASS_CONDITION:
                return ":" + c.getValue();
            }

        } else if (condition instanceof CombinatorCondition) {
            CombinatorCondition c = (CombinatorCondition) condition;
            switch (condition.getConditionType()) {
            case Condition.SAC_AND_CONDITION:
                return valueOf(c.getFirstCondition()) + valueOf(c.getSecondCondition());
            case Condition.SAC_OR_CONDITION:
                // Unimplemented in CSS2?
            }

        } else if (condition instanceof ContentCondition) {
            // Unimplemented in CSS2?

        } else if (condition instanceof LangCondition) {
            LangCondition c = (LangCondition) condition;
            return ":lang(" + c.getLang() + ")";

        } else if (condition instanceof NegativeCondition) {
            // Unimplemented in CSS2?
        } else if (condition instanceof PositionalCondition) {
            // Unimplemented in CSS2?
        }

        throw new RuntimeException("Unhandled condition of type " + condition.getConditionType() + " "
                + condition.getClass().getName());
    }

    private static Value valueOf(LexicalUnit value) {
        switch (value.getLexicalUnitType()) {
        case LexicalUnit.SAC_ATTR:
            return new IdentValue("attr(" + value.getStringValue() + ")");
        case LexicalUnit.SAC_IDENT:
            return new IdentValue(escapeIdent(value.getStringValue()));
        case LexicalUnit.SAC_STRING_VALUE:
            return new StringValue(value.getStringValue());
        case LexicalUnit.SAC_RGBCOLOR:
            // flute models the commas as operators so no separator needed
            return colorValue(value.getParameters());
        case LexicalUnit.SAC_INTEGER:
            return new NumberValue(value.getIntegerValue());
        case LexicalUnit.SAC_REAL:
            return new NumberValue(value.getFloatValue());
        case LexicalUnit.SAC_CENTIMETER:
        case LexicalUnit.SAC_DEGREE:
        case LexicalUnit.SAC_DIMENSION:
        case LexicalUnit.SAC_EM:
        case LexicalUnit.SAC_EX:
        case LexicalUnit.SAC_GRADIAN:
        case LexicalUnit.SAC_HERTZ:
        case LexicalUnit.SAC_KILOHERTZ:
        case LexicalUnit.SAC_MILLIMETER:
        case LexicalUnit.SAC_MILLISECOND:
        case LexicalUnit.SAC_PERCENTAGE:
        case LexicalUnit.SAC_PICA:
        case LexicalUnit.SAC_PIXEL:
        case LexicalUnit.SAC_POINT:
        case LexicalUnit.SAC_RADIAN:
        case LexicalUnit.SAC_SECOND:
            return new NumberValue(value.getFloatValue(), value.getDimensionUnitText());
        case LexicalUnit.SAC_URI:
            return new IdentValue("url(" + value.getStringValue() + ")");
        case LexicalUnit.SAC_OPERATOR_COMMA:
            return new TokenValue(",");
        case LexicalUnit.SAC_COUNTER_FUNCTION:
        case LexicalUnit.SAC_COUNTERS_FUNCTION:
        case LexicalUnit.SAC_FUNCTION: {
            if (value.getFunctionName().equals(VALUE_FUNCTION_NAME)) {
                // This is a call to value()
                List<Value> params = new ArrayList<Value>();
                extractValueOf(params, value.getParameters());

                if (params.size() != 1 && params.size() != 3) {
                    throw new CSSException(CSSException.SAC_SYNTAX_ERR,
                            "Incorrect number of parameters to " + VALUE_FUNCTION_NAME, null);
                }

                Value dotPathValue = params.get(0);
                String dotPath = maybeUnquote(((StringValue) dotPathValue).getValue());
                String suffix = params.size() == 3 ? maybeUnquote(((StringValue) params.get(2)).getValue()) : "";

                return new DotPathValue(dotPath, suffix);
            } else if (value.getFunctionName().equals(LITERAL_FUNCTION_NAME)) {
                // This is a call to value()
                List<Value> params = new ArrayList<Value>();
                extractValueOf(params, value.getParameters());

                if (params.size() != 1) {
                    throw new CSSException(CSSException.SAC_SYNTAX_ERR,
                            "Incorrect number of parameters to " + LITERAL_FUNCTION_NAME, null);
                }

                Value expression = params.get(0);
                if (!(expression instanceof StringValue)) {
                    throw new CSSException(CSSException.SAC_SYNTAX_ERR,
                            "The single argument to " + LITERAL_FUNCTION_NAME + " must be a string value", null);
                }

                String s = maybeUnquote(((StringValue) expression).getValue());
                s = unescapeLiteral(s);

                return new IdentValue(s);

            } else {
                // Just return a String representation of the original value
                List<Value> parameters = new ArrayList<Value>();
                extractValueOf(parameters, value.getParameters());
                return new IdentValue(value.getFunctionName() + "(" + join(parameters, "") + ")");
            }
        }
        case LexicalUnit.SAC_INHERIT:
            return new IdentValue("inherit");
        case LexicalUnit.SAC_OPERATOR_EXP:
            return new TokenValue("^");
        case LexicalUnit.SAC_OPERATOR_GE:
            return new TokenValue(">=");
        case LexicalUnit.SAC_OPERATOR_GT:
            return new TokenValue(">");
        case LexicalUnit.SAC_OPERATOR_LE:
            return new TokenValue("<=");
        case LexicalUnit.SAC_OPERATOR_LT:
            return new TokenValue("<");
        case LexicalUnit.SAC_OPERATOR_MINUS:
            return new TokenValue("-");
        case LexicalUnit.SAC_OPERATOR_MOD:
            return new TokenValue("%");
        case LexicalUnit.SAC_OPERATOR_MULTIPLY:
            return new TokenValue("*");
        case LexicalUnit.SAC_OPERATOR_PLUS:
            return new TokenValue("+");
        case LexicalUnit.SAC_OPERATOR_SLASH:
            return new TokenValue("/");
        case LexicalUnit.SAC_OPERATOR_TILDE:
            return new IdentValue("~");
        case LexicalUnit.SAC_RECT_FUNCTION: {
            // Just return this as a String
            List<Value> parameters = new ArrayList<Value>();
            extractValueOf(parameters, value.getParameters());
            return new IdentValue("rect(" + join(parameters, "") + ")");
        }
        case LexicalUnit.SAC_SUB_EXPRESSION:
            // Should have been taken care of by our own traversal
        case LexicalUnit.SAC_UNICODERANGE:
            // Cannot be expressed in CSS2
        }
        throw new RuntimeException("Unhandled LexicalUnit type " + value.getLexicalUnitType());
    }

    private static String valueOf(Selector selector) {
        if (selector instanceof CharacterDataSelector) {
            // Unimplemented in CSS2?

        } else if (selector instanceof ConditionalSelector) {
            ConditionalSelector s = (ConditionalSelector) selector;
            String simpleSelector = valueOf(s.getSimpleSelector());

            if ("*".equals(simpleSelector)) {
                // Don't need the extra * for compound selectors
                return valueOf(s.getCondition());
            } else {
                return simpleSelector + valueOf(s.getCondition());
            }

        } else if (selector instanceof DescendantSelector) {
            DescendantSelector s = (DescendantSelector) selector;
            switch (s.getSelectorType()) {
            case Selector.SAC_CHILD_SELECTOR:
                if (s.getSimpleSelector().getSelectorType() == Selector.SAC_PSEUDO_ELEMENT_SELECTOR) {
                    return valueOf(s.getAncestorSelector()) + ":" + valueOf(s.getSimpleSelector());
                } else {
                    return valueOf(s.getAncestorSelector()) + ">" + valueOf(s.getSimpleSelector());
                }
            case Selector.SAC_DESCENDANT_SELECTOR:
                return valueOf(s.getAncestorSelector()) + " " + valueOf(s.getSimpleSelector());
            }

        } else if (selector instanceof ElementSelector) {
            ElementSelector s = (ElementSelector) selector;
            if (s.getLocalName() == null) {
                return "*";
            } else {
                return escapeIdent(s.getLocalName());
            }

        } else if (selector instanceof NegativeSelector) {
            // Unimplemented in CSS2?

        } else if (selector instanceof ProcessingInstructionSelector) {
            // Unimplemented in CSS2?

        } else if (selector instanceof SiblingSelector) {
            SiblingSelector s = (SiblingSelector) selector;
            return valueOf(s.getSelector()) + "+" + valueOf(s.getSiblingSelector());
        }

        throw new RuntimeException("Unhandled selector of type " + selector.getClass().getName());
    }

    /**
     * Utility class.
     */
    private GenerateCssAst() {
    }
}