/* * 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() { } }