/* * 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.libideas.resources.css; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.libideas.resources.css.ast.CssDef; import com.google.gwt.libideas.resources.css.ast.CssEval; import com.google.gwt.libideas.resources.css.ast.CssIf; import com.google.gwt.libideas.resources.css.ast.CssMediaRule; import com.google.gwt.libideas.resources.css.ast.CssNoFlip; import com.google.gwt.libideas.resources.css.ast.CssNode; import com.google.gwt.libideas.resources.css.ast.CssPageRule; import com.google.gwt.libideas.resources.css.ast.CssProperty; import com.google.gwt.libideas.resources.css.ast.CssRule; import com.google.gwt.libideas.resources.css.ast.CssSelector; import com.google.gwt.libideas.resources.css.ast.CssSprite; import com.google.gwt.libideas.resources.css.ast.CssStylesheet; import com.google.gwt.libideas.resources.css.ast.CssUrl; import com.google.gwt.libideas.resources.css.ast.HasNodes; import com.google.gwt.libideas.resources.css.ast.HasProperties; import com.google.gwt.libideas.resources.css.ast.CssProperty.DotPathValue; import com.google.gwt.libideas.resources.css.ast.CssProperty.IdentValue; import com.google.gwt.libideas.resources.css.ast.CssProperty.ListValue; import com.google.gwt.libideas.resources.css.ast.CssProperty.NumberValue; import com.google.gwt.libideas.resources.css.ast.CssProperty.StringValue; import com.google.gwt.libideas.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.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; 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. */ 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 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) { fatalErrorEncountered |= type == TreeLogger.Type.ERROR; if (parentLogger.isLoggable(type)) { maybeBranch(); logger.log(type, message, t); } } 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 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 { String ruleName = atRule.substring(1, atRule.indexOf(" ")); String methodName = "parse" + (Character.toUpperCase(ruleName.charAt(0))) + ruleName.substring(1); try { Method parseMethod = getClass().getDeclaredMethod(methodName, String.class); parseMethod.invoke(this, atRule); } catch (NoSuchMethodException e) { errors.log(TreeLogger.WARN, "Ignoring @" + ruleName); } catch (IllegalAccessException e) { errors.log(TreeLogger.ERROR, "Unable to invoke parse method ", e); } catch (InvocationTargetException e) { if (e.getCause() instanceof CSSException) { throw (CSSException) e.getCause(); } errors.log(TreeLogger.ERROR, "Unable to invoke parse method ", 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 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 { } } private static final String LITERAL_FUNCTION_NAME = "literal"; private static final String VALUE_FUNCTION_NAME = "value"; /** * Create a CssStylesheet from the contents of a URL. */ public static CssStylesheet exec(TreeLogger logger, URL[] stylesheets) throws UnableToCompleteException { Parser p = new Parser(); Errors errors = new Errors(logger); GenerationHandler g = new GenerationHandler(errors); p.setDocumentHandler(g); p.setErrorHandler(errors); for (URL stylesheet : stylesheets) { 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(); } return g.css; } /** * Expresses an rgb function as a hex expression. * * @param colors a sequence of LexicalUnits, assumed to be * <code>(INT COMMA INT COMMA INT)</code> * @return the minimal hex expression for the RGB color values */ private static Value colorValue(LexicalUnit colors) { LexicalUnit red = colors; assert red.getLexicalUnitType() == LexicalUnit.SAC_INTEGER; LexicalUnit green = red.getNextLexicalUnit().getNextLexicalUnit(); assert green.getLexicalUnitType() == LexicalUnit.SAC_INTEGER; LexicalUnit blue = green.getNextLexicalUnit().getNextLexicalUnit(); assert blue.getLexicalUnitType() == LexicalUnit.SAC_INTEGER; int r = Math.min(red.getIntegerValue(), 255); int g = Math.min(green.getIntegerValue(), 255); int b = Math.min(blue.getIntegerValue(), 255); 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 (!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; } private static boolean isIdentPart(char c) { return Character.isLetterOrDigit(c) || (c == '\\') || (c == '-'); } private static boolean isIdentStart(char c) { return Character.isLetter(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 IdentValue(","); 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 IdentValue("^"); case LexicalUnit.SAC_OPERATOR_GE: return new IdentValue(">="); case LexicalUnit.SAC_OPERATOR_GT: return new IdentValue(">"); case LexicalUnit.SAC_OPERATOR_LE: return new IdentValue("<="); case LexicalUnit.SAC_OPERATOR_LT: return new IdentValue("<"); case LexicalUnit.SAC_OPERATOR_MINUS: return new IdentValue("-"); case LexicalUnit.SAC_OPERATOR_MOD: return new IdentValue("%"); case LexicalUnit.SAC_OPERATOR_MULTIPLY: return new IdentValue("*"); case LexicalUnit.SAC_OPERATOR_PLUS: return new IdentValue("+"); case LexicalUnit.SAC_OPERATOR_SLASH: return new IdentValue("/"); 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() { } }