All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.common.css.compiler.ast.GssParserCC.jj Maven / Gradle / Ivy

Go to download

Closure Stylesheets is an extension to CSS that adds variables, functions, conditionals, and mixins to standard CSS. The tool also supports minification, linting, RTL flipping, and CSS class renaming.

The newest version!
// Copyright 2009 Google Inc. All Rights Reserved.

/**
 * GssParserCC.jj : parses gss files, produces the new AST.
 *
 * @author [email protected] (Florian Benz)
 */

options {
  STATIC = false ;
  //the next two are just for testing
  //DEBUG_TOKEN_MANAGER = true;
  //DEBUG_PARSER = true;
  // Suppress ambiguity warnings:
  SANITY_CHECK = false;
  UNICODE_INPUT = true;
  USER_CHAR_STREAM = true;
}

PARSER_BEGIN(GssParserCC)

package com.google.common.css.compiler.ast;

import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.css.*;

import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * A parser that recognizes GSS files and builds the new AST.
 */
public class GssParserCC {

  /**
   * Pattern for functions that are allowed to be separated by spaces.
   *
   * 

The non-standard {@code rect(0 0 0 0)} is sometimes used for IE instead * of the standard {@code rect(0,0,0,0)}. * *

The legacy {@code -webkit-gradient} function takes its arguments in the * form: * {@code (linear, left center, right center, from(color1), to(color2))}. * As does {@code -khtml-gradient}. * *

The gradient proposals for CSS 3 are: *

    *
  • linear-gradient *
  • radial-gradient *
  • repeating-linear-gradient *
  • repeating-radial-gradient *
* As CSS 3 is in draft stage they come in current browsers with a vendor * prefix but leaving out the vendor prefix already works. */ private static final Pattern FUNCTIONS_WITH_SPACE_SEP_OK = Pattern.compile( "(?:-(?:O|MOZ|WEBKIT|MS)-)?(?:REPEATING-)?(?:LINEAR|RADIAL)-GRADIENT" + "|(?:-(?:O|MOZ|WEBKIT|MS)-)?IMAGE-SET" + "|RECT|INSET|CIRCLE|ELLIPSE|POLYGON|-KHTML-GRADIENT|-WEBKIT-GRADIENT" + "|(?:-WEBKIT-)?DROP-SHADOW|(?:-WEBKIT-)?CUSTOM|LOCAL", Pattern.CASE_INSENSITIVE); private static final Set URL_FUNCTIONS = ImmutableSet.of( "domain", "url", "url-prefix"); private static final CharMatcher CSS_WHITESPACE = CharMatcher.anyOf(" \t\r\n\f"); private static final Pattern VALID_BLOCK_COMMENT_PATTERN = Pattern.compile(".*/\\*.*\\*/.*", Pattern.DOTALL); private CssBlockNode globalBlock; private SourceCode sourceCode; private final CssNodeBuilder nodeBuilder = new CssNodeBuilder(); private StringCharStream charStream; /** * CSS Error Handling (http://www.w3.org/TR/css-syntax-3/#error-handling) is implemented by the * demand from Play Book. It still may abort throwing GssParserException due to unhandled errors. */ private boolean enableErrorRecovery; /** * List of handled errors if error handling enabled. */ private final List handledErrors = Lists.newArrayList(); public ImmutableList getHandledErrors() { return ImmutableList.copyOf(handledErrors); } public GssParserCC(CssBlockNode globalBlock, SourceCode sourceCode) { this(new StringCharStream(sourceCode.getFileContents()), globalBlock, sourceCode, false); } public GssParserCC(StringCharStream charStream, CssBlockNode globalBlock, SourceCode sourceCode) { this(charStream, globalBlock, sourceCode, false); } /** * @enableErrorRecovery If true, it recovers as many errors as possible and continue parsing * instead of throwing ParseException and handled errors are available as * getHandledErrors(). */ public GssParserCC(CssBlockNode globalBlock, SourceCode sourceCode, boolean enableErrorRecovery) { this(new StringCharStream(sourceCode.getFileContents()), globalBlock, sourceCode, enableErrorRecovery); } public GssParserCC(StringCharStream charStream, CssBlockNode globalBlock, SourceCode sourceCode, boolean enableErrorRecovery) { this((CharStream) charStream); this.charStream = charStream; this.sourceCode = sourceCode; this.globalBlock = globalBlock; this.enableErrorRecovery = enableErrorRecovery; } private SourceCodeLocation getLocation() { return getLocation(token); } private SourceCodeLocation getLocation(Token t) { int lineNumber1 = t.beginLine; int indexInLine1 = t.beginColumn; int charIndex1 = charStream.convertToCharacterIndex(lineNumber1, indexInLine1); int lineNumber2 = t.endLine; int indexInLine2 = t.endColumn; int charIndex2 = charStream.convertToCharacterIndex(lineNumber2, indexInLine2); return new SourceCodeLocation(sourceCode, charIndex1, lineNumber1, indexInLine1, charIndex2, lineNumber2, indexInLine2); } /** * Returns a new SourceCodeLocation which covers everything between the * beginning of the first location and the end of the second location. */ private SourceCodeLocation mergeLocations(SourceCodeLocation beginLocation, SourceCodeLocation endLocation) { return new SourceCodeLocation(sourceCode, beginLocation.getBeginCharacterIndex(), beginLocation.getBeginLineNumber(), beginLocation.getBeginIndexInLine(), endLocation.getEndCharacterIndex(), endLocation.getEndLineNumber(), endLocation.getEndIndexInLine()); } private SourceCodeLocation mergeLocations( Iterable locations) { Iterator i = locations.iterator(); SourceCodeLocation loc = i.next().getSourceCodeLocation(); while (i.hasNext()) { SourceCodeLocation iLoc = i.next().getSourceCodeLocation(); if (iLoc == null) continue; loc = mergeLocations(loc, iLoc); } return loc; } private CssFunctionNode createUrlFunction(Token t) { // We tried using finer-grained tokens for URI parsing, but an // unquoted URI can look just like an identifier or even a whole // declaration. We tried using lexical states to recognize bare // URIs only within URL_FUNCTIONS, but that had two problems: // (1) it means ordinary functions can't take bare URLs, which // harms orthogonality of GSS and outlaws some custom functions // we've found in the wild // (2) it means our special lexical state must either exclude // ordinary functions, or we must import many other tokens into this // new state, which adds complexity for little benefit. // Therefore, we ask the lexer to distinguish only big // recognizably-URLish chunks of input and break those down here. SourceCodeLocation loc = this.getLocation(t); int pi = t.image.indexOf('('); String funName = t.image.substring(0, pi); Preconditions.checkState(URL_FUNCTIONS.contains(funName)); CssFunctionNode.Function funType = CssFunctionNode.Function.byName(funName); CssFunctionNode fun = new CssFunctionNode(funType, loc); String parenContents = trim(t.image.substring(pi + 1, t.image.length() - 1)); CssValueNode arg = new CssLiteralNode(parenContents, loc); fun.setArguments(new CssFunctionArgumentsNode(ImmutableList.of(arg))); return fun; } /** * Adds the given arguments explicitly separated by the given * separator to the given node. If an argument is a composite node * separated by commas, this method adds its children explicitly * separated by commas instead of the composite node, in order to * flatten out the argument list. * *

Separators such as commas in function calls have to be added * explicitly, in order to match the output from the old tree * converter. * * @param node The function arguments node to add to * @param args The real arguments to add * @param numArgs The number of real arguments * @param sep The separator to add between real arguments */ private void addArgumentsWithSeparator( CssFunctionArgumentsNode node, Iterable args, int numArgs, String sep) { int current = 0; for (CssValueNode arg : args) { if (arg instanceof CssCompositeValueNode && ((CssCompositeValueNode) arg).getOperator() == CssCompositeValueNode.Operator.COMMA) { CssCompositeValueNode composite = (CssCompositeValueNode) arg; addArgumentsWithSeparator(node, composite.getValues(), composite.getValues().size(), ","); } else { node.addChildToBack(arg); } current++; if (current < numArgs) { // Note that the source location for the separator is not entirely // accurate, but adding the separators as values is a hack anyways. node.addChildToBack( new CssLiteralNode(sep, arg.getSourceCodeLocation())); } } } private static String trim(String input) { return CSS_WHITESPACE.trimFrom(input); } public void parse() throws GssParserException { try { start(); } catch (ParseException e) { // token.next can be null if there is an error after EOF, such as an unterminated block // comment. Token tokenWithError = token.next == null ? token : token.next; throw new GssParserException(this.getLocation(tokenWithError), e); } } /** * Wrapper around {@code parse()} that (re-)initializes the parser and * clears its state afterwards. * @param globalBlock the place to store parsing result * @param sourceCode the source code to parse * @param errorHandling whether error handling is enabled * @param parsingErrors the place to store errors occured during parsing */ public void parse(CssBlockNode globalBlock, SourceCode sourceCode, boolean errorHandling, ImmutableList.Builder parsingErrors) throws GssParserException { try { initialize(globalBlock, sourceCode, errorHandling); parse(); } finally { parsingErrors.addAll(handledErrors); clearState(); } } /** * This helper class takes care of the creation of nodes of the AST, and * attaching comments to them. */ private class CssNodeBuilder { protected CssNode attachComments(List tokens, CssNode node) { for (Token t : tokens) { node = attachComment(t, node); } return node; } public CssNode attachComment(Token t, CssNode node) { if (t.specialToken == null) { return node; } Token special = t.specialToken; // Walking back the special token chain until we reach the first special token // which is after the previous regular (non-comment) token. while (special.specialToken != null) { special = special.specialToken; } // Visiting comments in their normal appearing order. while (special != null) { node.appendComment(new CssCommentNode(trim(special.image), getLocation(special))); special = special.next; } return node; } public CssStringNode buildStringNode(CssStringNode.Type type, String image, SourceCodeLocation location, Token token) { Preconditions.checkNotNull(image, "image should be non-null"); Preconditions.checkArgument( image.length() > 1, "the image argument must be quoted", image); CssStringNode node = new CssStringNode(type, location); attachComments(Lists.newArrayList(token), node); return node; } public CssHexColorNode buildHexColorNode(String image, SourceCodeLocation location, List tokens) { CssHexColorNode node = new CssHexColorNode(image, location); attachComments(tokens, node); return node; } public CssRulesetNode buildRulesetNode(CssDeclarationBlockNode declarations, CssSelectorListNode selectors, SourceCodeLocation location, List tokens) { CssRulesetNode node = new CssRulesetNode(declarations); node.setSelectors(selectors); node.setSourceCodeLocation(location); attachComments(tokens, node); return node; } public CssKeyframeRulesetNode buildKeyframeRulesetNode(CssDeclarationBlockNode declarations, CssKeyListNode keys, List tokens) { CssKeyframeRulesetNode node = new CssKeyframeRulesetNode(declarations); node.setKeys(keys); attachComments(tokens, node); return node; } public CssKeyNode buildKeyNode(Token token, String value, SourceCodeLocation location) { CssKeyNode node = new CssKeyNode(value, location); if (token != null) { attachComment(token, node); } return node; } public CssClassSelectorNode buildClassSelectorNode(String name, SourceCodeLocation location, boolean isComponentScoped, List tokens) { CssClassSelectorNode node = new CssClassSelectorNode(name, isComponentScoped, location); attachComments(tokens, node); return node; } public CssIdSelectorNode buildIdSelectorNode(String id, SourceCodeLocation location, List tokens) { CssIdSelectorNode node = new CssIdSelectorNode(id, location); attachComments(tokens, node); return node; } public CssPseudoClassNode buildPseudoClassNode(String name, SourceCodeLocation location, List tokens) { CssPseudoClassNode node = new CssPseudoClassNode(name, location); attachComments(tokens, node); return node; } public CssPseudoClassNode buildPseudoClassNode( CssPseudoClassNode.FunctionType functionType, String name, String argument, SourceCodeLocation location, List tokens) { CssPseudoClassNode node = new CssPseudoClassNode(functionType, name, argument, location); attachComments(tokens, node); return node; } public CssPseudoClassNode buildPseudoClassNode(String name, CssSelectorNode notSelector, SourceCodeLocation location, List tokens) { CssPseudoClassNode node = new CssPseudoClassNode(name, notSelector, location); attachComments(tokens, node); return node; } public CssPseudoElementNode buildPseudoElementNode(String name, SourceCodeLocation location, List tokens) { CssPseudoElementNode node = new CssPseudoElementNode(name, location); attachComments(tokens, node); return node; } public CssAttributeSelectorNode buildAttributeSelectorNode( CssAttributeSelectorNode.MatchType matchType, String attribute, CssValueNode value, SourceCodeLocation location, List tokens) { CssAttributeSelectorNode node = new CssAttributeSelectorNode(matchType, attribute, value, location); attachComments(tokens, node); return node; } public CssSelectorNode buildSelectorNode(Token token, SourceCodeLocation location, CssRefinerListNode refiners) { String name = ""; if (token != null) { name = token.image; } CssSelectorNode node = new CssSelectorNode(name, location); if (token != null) { attachComment(token, node); } node.setRefiners(refiners); return node; } public CssCombinatorNode buildCombinatorNode(CssCombinatorNode.Combinator combinator, SourceCodeLocation location, List tokens) { CssCombinatorNode node = new CssCombinatorNode(combinator, location); attachComments(tokens, node); return node; } public CssDeclarationNode buildDeclarationNode(CssPropertyNode property, CssPropertyValueNode value, List tokens) { CssDeclarationNode node = new CssDeclarationNode(property, value); node.setSourceCodeLocation( mergeLocations( property.getSourceCodeLocation(), value.getSourceCodeLocation())); attachComments(tokens, node); return node; } public CssCompositeValueNode buildCompositeValueNode(List list, CssCompositeValueNode.Operator op, SourceCodeLocation location, List tokens) { CssCompositeValueNode node = new CssCompositeValueNode(list, op, location); attachComments(tokens, node); return node; } public CssBooleanExpressionNode buildBoolExpressionNode(CssBooleanExpressionNode.Type type, String value, CssBooleanExpressionNode left, CssBooleanExpressionNode right, SourceCodeLocation loc, List tokens) { CssBooleanExpressionNode node = new CssBooleanExpressionNode(type, value, left, right, loc); attachComments(tokens, node); return node; } public CssLiteralNode buildLiteralNode(String value, SourceCodeLocation location, List tokens) { CssLiteralNode node = new CssLiteralNode(value, location); attachComments(tokens, node); return node; } public CssUnicodeRangeNode buildUnicodeRangeNode(String value, SourceCodeLocation location, List tokens) { CssUnicodeRangeNode node = new CssUnicodeRangeNode(value, location); attachComments(tokens, node); return node; } public CssNumericNode buildNumericNode(String num, String unit, SourceCodeLocation location, List tokens) { CssNumericNode node = new CssNumericNode(num, unit, location); attachComments(tokens, node); return node; } public CssLiteralNode buildLoopVariableNode(String value, SourceCodeLocation location, List tokens) { CssLoopVariableNode node = new CssLoopVariableNode(value, location); attachComments(tokens, node); return node; } public CssFunctionNode buildFunctionNode(String name, SourceCodeLocation location, CssFunctionArgumentsNode args, List tokens) { CssFunctionNode.Function functionType = CssFunctionNode.Function.byName(name); if (functionType == null) { functionType = CssFunctionNode.Function.CUSTOM; } CssFunctionNode functionNode = (functionType != CssFunctionNode.Function.CUSTOM) ? new CssFunctionNode(functionType, location) : new CssCustomFunctionNode(name, location); functionNode.setArguments(args); attachComments(tokens, functionNode); return functionNode; } public CssPriorityNode buildPriorityNode(SourceCodeLocation location, List tokens) { CssPriorityNode node = new CssPriorityNode(CssPriorityNode.PriorityType.IMPORTANT, location); attachComments(tokens, node); return node; } public CssUnknownAtRuleNode buildUnknownAtRuleNode(CssLiteralNode name, CssAbstractBlockNode block, SourceCodeLocation location, List parameters, List tokens) { boolean hasBlock = (block != null); CssUnknownAtRuleNode at = new CssUnknownAtRuleNode(name, hasBlock); at.setSourceCodeLocation(location); if (hasBlock) { at.setBlock(block); } at.setParameters(parameters); attachComments(tokens, at); return at; } public CssKeyframesNode buildWebkitKeyframesNode(CssLiteralNode name, CssBlockNode block, SourceCodeLocation location, List parameters, List tokens) { CssKeyframesNode at = new CssKeyframesNode(name); at.setSourceCodeLocation(location); at.setBlock(block); at.setParameters(parameters); attachComments(tokens, at); return at; } } private void initialize(CssBlockNode globalBlock, SourceCode sourceCode, boolean errorHandling) { this.enableErrorRecovery = errorHandling; this.sourceCode = sourceCode; this.globalBlock = globalBlock; this.handledErrors.clear(); StringCharStream charStream = new StringCharStream(sourceCode.getFileContents()); this.charStream = charStream; this.ReInit(charStream); } private void clearState() { this.sourceCode = null; this.globalBlock = null; this.handledErrors.clear(); this.charStream = null; } } PARSER_END(GssParserCC) // Pass comments to the parser as special token SPECIAL_TOKEN : { // Consume the comment until the first "*/" or end of file (whichever is first), and then any // whitespace that may follow. // We do this by consuming the following: // 1. Start of comment. // 2. Longest string not containing "*/". If there is no terminating "*/", this will consume until // EOF. This ensures the contents of the comment are never consumed as a CSS token. // 3. Optionally the closing "*/". // 4. Any remaining whitespace. < "/" "*" ((("*")* ~["*", "/"]) | "/")* ("*")* ("*/")? ("\r\n"| "\n" | "\r" | "\f" |" " |"\t")* > : DEFAULT } // Some ideas taken from: // http://cssparser.cvs.sourceforge.net/viewvc/*checkout*/cssparser/cssparser/ // src/main/javacc/SACParserCSS21.jj // Our grammar follows - not so strictly - the CSS grammar specification at: // http://www.w3.org/TR/CSS2/grammar.html // Original CSS grammar is case-insensitive, but this grammar is case-sensitive // to speed up parsing. Both upper-case and lower-case characters are accepted. TOKEN : { < SEMICOLON: ";" > | < COLON: ":" > | < DOT: "." > | < ASTERISK: "*" > | < SLASH: "/" > | < MINUS: "-" > | < EQUALS: "=" > | < LEFTSQUARE: "[" > | < RIGHTSQUARE: "]" > | < LEFTROUND: "(" > | < RIGHTROUND: ")" > | < LEFTBRACE: "{" > | < RIGHTBRACE: "}" > | < COMMA: "," > | < EXCL_MARK: "!" > | < PERCENT: "%" > | < #PLUS: "+" > | < #GREATER: ">" > | < #TILDE: "~" > | < #DOUBLE_QUOTE: "\"" > | < #SINGLE_QUOTE: "\'" > | < #AT: "@" > | < #HASH: "#" > | < #UNDERSCORE: "_" > | < #AMPERSAND: "&" > | < #CARET: "^" > | < #DOLLAR: "$" > | < #PIPE: "|" > | < AND: > | < OR: > | < TILDE_EQUALS: > | < CARET_EQUALS: > | < DOLLAR_EQUALS: > | < ASTERISK_EQUALS: > | < PIPE_EQUALS: > | < CDO: "" > // CSS2 grammar allows up to 6-digit hex strings for unicode // hex [0-9a-f] | < #HEX: ["0"-"9","a"-"f"] > // hexnum between 1 and 6 hex digits | < #HEXNUM: (){1,6} > // unicode \\{h}{1,6}(\r\n|[ \t\r\n\f])? | < #UNICODE: "\\" ("\r\n" | [" ","\t","\r","\n","\f"])? > // escape {unicode}|\\[^\r\n\f0-9a-f] | < #ESCAPE: | ("\\" ~["\r","\n","\f","0"-"9","a"-"f"]) > | < #DIGIT: ["0"-"9"] > | < #NL1: "\n" | "\r" | "\f" > // s [ \t\r\n\f]+ // {s} {return S;} | < S: ( " " | "\t" | )+ > | < #W: ( )* > | < #NL: "\r\n" | > | < WMINUSW: ()+ ()+> | < WPLUS: > | < WGREATER: > | < WTILDE: > | < WDEEP: "deep" > | < #ALPHA: ["a"-"z","A"-"Z"] > // nonascii [\200-\377] // The occurrences of "\377" represent the highest character number that // we can deal with (decimal 255). They should be read // as "\4177777" (decimal 1114111), which is the highest possible code point // in Unicode/ISO-10646. // Limitation: This parser can only handle Unicode characters up to \uFFFF // (decimal 65535). | < #NONASCII: ["\u0080"-"\uFFFF"] > // nmstart [_a-z]|{nonascii}|{escape} | < #NAMESTART: | | > // nmchar [_a-z0-9-]|{nonascii}|{escape} | < #NAMECHAR: | | | | > // name {nmchar}+ | < #NAME: ()+ > // num [0-9]+|[0-9]*"."[0-9]+ | < NUMBER: ()+ | ()* ()+ > // "#"{name} {return HASH;} | < HASH_NAME: > // Spaces are allowed between ! and "important". // Unlike the specification, no comments are allowed between '!' and // 'important' because comments are special tokens and there is no easy way // to allow them in between if we combine '!' and 'important' to a single // token. We are doing that do allow the identifier 'important' in other // places (e.g. as a class name). We found no file in the Google code base // that had a comment in between. | < IMPORTANT_SYM: "important" > // string1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\" | < DOUBLE_QUOTED_STRING: (~["\n","\r","\f","\\","\""] | "\\" | )* > // string2 \'([^\n\r\f\\']|\\{nl}|{escape})*\' | < SINGLE_QUOTED_STRING: (~["\n","\r","\f","\\","\'"] | "\\" | )* > // Bare URIs. URIs with quoted string arguments are parsed as ordinary // functions. See the comments in createUrlFunction to understand why // it is hard to unify parsing for urls and other functions; then you // can understand why we divide url processing at all and why it is // useful to avoid duplicating string-parsing logic in the treatment of // the special case for url functions. // "url("{w}{url}{w}")" {return URI;} | < URI: ("url" | "url-prefix" | "domain") "(" ()* ")" > // Unquoted URLs don't accept escape sequences unlike the spec so far. // url ([!#$%&*-~]|{nonascii})* | < #URL: | | | | | | | | | | "/" | | | | "<" | | | "?" | | | | | | | "`" | | | | | > | < FOR_VARIABLE: ()* > // ident -?{nmstart}{nmchar}* // {ident} {return IDENT;} | < IDENTIFIER: ()? ( | )* > // "not(" // Special handling needed because ':not' takes simple selectors as // an argument. | < NOTFUNCTION: "not" > | < LANGFUNCTION: "lang" > // "calc(" // Special handling because the content between the parentheses is a // mathematical expression tree, not an argument list, so it uses a different // parse tree. | < CALC: ("calc"|"-webkit-calc"|"-moz-calc") > | < #UNICODE_RANGE_TEXT: ( ["0"-"9","a"-"f", "A"-"F"] ){1,6} > // Unicode range options: U+26, U+0015-00FF, U+4?? | < UNICODE_RANGE: "U+" ( ( ( )? ) | ( ("?"){1,6} ) ) > // {ident}"( {return FUNCTION;} | < FUNCTION: > | < ATLIST: "list" > // "@-webkit-keyframes" // Special handling needed because of the keys instead of the selectors | < WEBKITKEYFRAMES: ( "keyframes" | "-webkit-keyframes" | "-moz-keyframes" | "-ms-keyframes" | "-o-keyframes" ) > // Top-level @-rules that contain a declaration block. General top-level // @-rules contain a block with rulesets. | < ATRULESWITHDECLBLOCK: ( "page" | "defmixin" | "font-face" ) > | < ATKEYWORD: > | < BAD_TOKEN: ~[] > } // The rest of the file contains the production rules of the grammar and to // increase readability every rule also has a comment with the rule it // implements in Extended Backus–Naur Form (EBNF) that follows this syntax: // name_of_rule // : X // ; // where X is the actual production consisting of non terminals and terminals. // Terminals that exactly represent some characters are not written with their // name but the characters are written instead, e.g. '(' instead of LEFTROUND, // but IDENTIFIER stays the same. // string // : DOUBLE_QUOTED_STRING | SINGLE_QUOTED_STRING // ; CssStringNode string() : { Token t; CssStringNode.Type type; SourceCodeLocation beginLocation; } { { beginLocation = this.getLocation(token.next); } ( t = { type = CssStringNode.Type.DOUBLE_QUOTED_STRING; } | t = { type = CssStringNode.Type.SINGLE_QUOTED_STRING; } ) { SourceCodeLocation endLocation = this.getLocation(); return nodeBuilder.buildStringNode( type, t.image, this.mergeLocations(beginLocation, endLocation), t); } } // ruleset // : selector_list '{' style_declarations '}' // ; CssRulesetNode ruleSet() : { CssSelectorListNode selectors; CssDeclarationBlockNode declarations; Token t; List tokens = Lists.newArrayList(); } { try { try { selectors = selectorList() t = { tokens.add(t); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(LEFTBRACE); throw e; } declarations = styleDeclaration() t = { tokens.add(t); } { CssRulesetNode ruleSet = nodeBuilder.buildRulesetNode(declarations, selectors, this.getLocation(), tokens); return ruleSet; } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTBRACE); throw e; } } // selector_list // : selector [ ',' S* selector ]* // ; CssSelectorListNode selectorList() : { CssSelectorListNode list = new CssSelectorListNode(); CssSelectorNode selector; Token t; } { selector = selector() { list.addChildToBack(selector); } ( t = { nodeBuilder.attachComment(t, selector); } ( )* selector = selector() { list.addChildToBack(selector); } )* { return list; } } // selector // : simple_selector [ combinator simple_selector ]* S* // ; CssSelectorNode selector() : { CssSelectorNode first; CssCombinatorNode c; CssSelectorNode next; CssSelectorNode prev; Token t; List tokens = Lists.newArrayList(); } { first = simpleSelector() { prev = first; } ( // We need a lookahead here to be able to decide if a space is a combinator or just a // space before the opening '{' of the following rule. LOOKAHEAD(2) c = combinator() next = simpleSelector() { c.setSelector(next); prev.setCombinator(c); prev = next; } )* ( t = { tokens.add(t); } )* { nodeBuilder.attachComments(tokens, first); return first; } } // class // : '.' IDENTIFIER // ; CssClassSelectorNode className() : { Token t; List tokens = Lists.newArrayList(); boolean isComponentScoped = false; } { t = { tokens.add(t); } ( { isComponentScoped = true; } )? t = { tokens.add(t); return nodeBuilder.buildClassSelectorNode(t.image, this.getLocation(), isComponentScoped, tokens); } } // id // : HASH_NAME // ; CssRefinerNode id() : { Token t; List tokens = Lists.newArrayList(); } { t = { tokens.add(t); String name = t.image.substring(1); return nodeBuilder.buildIdSelectorNode(name, this.getLocation(), tokens); } } // pseudo // : ':' [ IDENT | [ ':' IDENT ] | [ 'not(' S* simple_selector S* ')' ] // | [ 'lang(' S* IDENT S* ')' ] | [ FUNCTION S* nth S* ')' ] ]? // ; CssRefinerNode pseudo() : { Token t; SourceCodeLocation beginLocation = null; SourceCodeLocation endLocation = null; String pseudo = null; String argument = null; List tokens = Lists.newArrayList(); CssSelectorNode notSelector = null; } { t = { beginLocation = this.getLocation(); tokens.add(t); } ( ( t = { pseudo = t.image; tokens.add(t); } ) | ( // ::identifier (pseudo-element) t = { tokens.add(t); } t = { pseudo = t.image; tokens.add(t); endLocation = this.getLocation(); return nodeBuilder.buildPseudoElementNode(pseudo, this.mergeLocations(beginLocation, endLocation), tokens); } ) | ( // :not( simple_selector ) t = ( )* { beginLocation = this.getLocation(); pseudo = t.image; tokens.add(t); } notSelector = simpleSelector() ( )* t = { tokens.add(t); endLocation = this.getLocation(); return nodeBuilder.buildPseudoClassNode(pseudo, notSelector, this.mergeLocations(beginLocation, endLocation), tokens); } ) | ( // :lang( ) t = ( )* { beginLocation = this.getLocation(); pseudo = t.image; tokens.add(t); } t = { argument = t.image; tokens.add(t); } ( )* t = { tokens.add(t); endLocation = this.getLocation(); return nodeBuilder.buildPseudoClassNode( CssPseudoClassNode.FunctionType.LANG, pseudo, argument, this.mergeLocations(beginLocation, endLocation), tokens); } ) | ( // :nth-function( nth ) t = ( )* { beginLocation = this.getLocation(); pseudo = t.image; tokens.add(t); } argument = nth() ( )* t = { tokens.add(t); endLocation = this.getLocation(); return nodeBuilder.buildPseudoClassNode( CssPseudoClassNode.FunctionType.NTH, pseudo, argument, this.mergeLocations(beginLocation, endLocation), tokens); } ) )? { // non-function pseudo-class endLocation = this.getLocation(); return nodeBuilder.buildPseudoClassNode(pseudo, this.mergeLocations(beginLocation, endLocation), tokens); } } // nth // : [ [ [ S* '+' ] | '-' | NUMBER | IDENTIFIER | FOR_VARIABLE ] S* ]+ // ; String nth() : { Token t; StringBuilder argument = new StringBuilder(); } { ( ( t = { argument.append(t.image); } | t = { argument.append(t.image); } | t = { argument.append(trim(t.image)); } | t = { argument.append(t.image); } | t = { argument.append(t.image); } | t = { argument.append(t.image); } ) ( )* )+ { return argument.toString(); } } // attribute // : '[' S* IDENT S* [ // [ '=' | '~=' | '^=' | '$=' | '*=' | '|=' ] S* [ IDENTIFIER | STRING ] // S* // ]? ']' // ; CssAttributeSelectorNode attribute() : { Token t; CssStringNode stringNode = null; CssLiteralNode idNode = null; String attribute; List tokens = Lists.newArrayList(); SourceCodeLocation beginLocation; CssAttributeSelectorNode.MatchType matchType = CssAttributeSelectorNode.MatchType.ANY; } { t = { beginLocation = this.getLocation(); tokens.add(t); } try { ( )* t = { attribute = t.image; tokens.add(t); } ( )* ( ( t = { tokens.add(t); matchType = CssAttributeSelectorNode.MatchType.EXACT; } | t = { tokens.add(t); matchType = CssAttributeSelectorNode.MatchType.ONE_WORD; } | t = { tokens.add(t); matchType = CssAttributeSelectorNode.MatchType.PREFIX; } | t = { tokens.add(t); matchType = CssAttributeSelectorNode.MatchType.SUFFIX; } | t = { tokens.add(t); matchType = CssAttributeSelectorNode.MatchType.CONTAINS; } | t = { tokens.add(t); matchType = CssAttributeSelectorNode.MatchType.EXACT_OR_DASH; } ) ( )* ( t = { idNode = new CssLiteralNode(t.image, this.getLocation()); tokens.add(t); } | stringNode = string() ) ( )* )? t = { tokens.add(t); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTSQUARE); throw e; } { SourceCodeLocation endLocation = this.getLocation(); CssValueNode v; if (stringNode != null) { v = stringNode; } else if (idNode != null) { v = idNode; } else { v = new CssLiteralNode(""); } return nodeBuilder.buildAttributeSelectorNode( matchType, attribute, v, this.mergeLocations(beginLocation, endLocation), tokens); } } // simple_selector // : [ element_name [ id | class | attribute | pseudo ]* ] // | [ id | class | attribute | pseudo ]+ // ; CssSelectorNode simpleSelector() : { Token t; CssRefinerNode n = null; Token selectorName = null; CssRefinerListNode refiners = new CssRefinerListNode(); SourceCodeLocation beginLocation; } { { beginLocation = this.getLocation(token.next); } ( ( selectorName = elementName() ( ( n = id() | n = className() | n = attribute() | n = pseudo()) { refiners.addChildToBack(n); } )* ) | ( ( n = id() | n = className() | n = attribute() | n = pseudo()) { refiners.addChildToBack(n); } )+ ) { SourceCodeLocation endLocation = this.getLocation(); CssSelectorNode selectorNode = nodeBuilder.buildSelectorNode(selectorName, this.mergeLocations(beginLocation, endLocation), refiners); return selectorNode; } } // element_name // : IDENTIFIER | '*' // ; Token elementName() : { Token t; } { ( t = | t = ) { return t; } } // combinator // : [ '+' S* ] | [ '>' S* ] | [ '~' S* ] | [ WDEEP S*] | S+ // ; CssCombinatorNode combinator() : { Token t; List tokens = Lists.newArrayList(); } { // it doesn't work if the "( )*"are after the { ... } part ( t = ( )* { tokens.add(t); return nodeBuilder.buildCombinatorNode( CssCombinatorNode.Combinator.ADJACENT_SIBLING, this.getLocation(), tokens); } ) | ( t = ( )* { tokens.add(t); return nodeBuilder.buildCombinatorNode( CssCombinatorNode.Combinator.CHILD, this.getLocation(), tokens); } ) | ( t = ( )* { tokens.add(t); return nodeBuilder.buildCombinatorNode( CssCombinatorNode.Combinator.GENERAL_SIBLING, this.getLocation(), tokens); } ) | ( t = ( )* { tokens.add(t); return nodeBuilder.buildCombinatorNode( CssCombinatorNode.Combinator.DEEP, this.getLocation(), tokens); } ) | ( ( t = {tokens. add(t); } )+ { return nodeBuilder.buildCombinatorNode( CssCombinatorNode.Combinator.DESCENDANT, this.getLocation(), tokens); } ) } // style_declaration // : S* standard_declaration? S* // [ // [ inner_at_rule S* standard_declaration ] // | ';' S* standard_declaration // ]* // ; // This production rule returns a block that contains a list of declarations and // @-rules. The declarations and rules must be separated by semicolons. If the // last thing in the block is a declaration, the trailing semicolon is optional. // Note that the trailing semicolon is NOT optional if the last thing is an // @-rule. This is a limitation of the current grammar. CssDeclarationBlockNode styleDeclaration() : { CssDeclarationBlockNode block = new CssDeclarationBlockNode(); CssNode decl; } { try { ( )* ( decl = standardDeclaration() { block.addChildToBack(decl); } )? ( )* } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; handledErrors.add(new GssParserException(getLocation(e.currentToken.next), e)); } ( try { ( try { decl = innerAtRule() { block.addChildToBack(decl); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; handledErrors.add(new GssParserException(getLocation(e.currentToken.next), e)); } ( )* ( decl = standardDeclaration() { block.addChildToBack(decl); } )? ) | ( ( )* // The last declaration within a block not necessarily has a semicolon // at the end. ( decl = standardDeclaration() { block.addChildToBack(decl); } )? ) } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; handledErrors.add(new GssParserException(getLocation(e.currentToken.next), e)); } )* { return block; } } // standard_declaration // : '*'? IDENTIFIER S* ':' S* expr S* important? // ; CssDeclarationNode standardDeclaration() : { Token t; CssPropertyNode property; CssPropertyValueNode valueNode; CssPriorityNode priority = null; List tokens = Lists.newArrayList(); String propertyName = ""; } { try { ( t = { tokens.add(t); propertyName = "*"; } )? // allows "star hack" t = { propertyName = propertyName + t.image; property = new CssPropertyNode(propertyName, this.getLocation()); tokens.add(t); } ( )* t = { tokens.add(t); } ( )* valueNode = expr() ( )* ( priority = important() )? { if (priority != null) { valueNode.addChildToBack(priority); } CssDeclarationNode node = nodeBuilder.buildDeclarationNode(property, valueNode, tokens); return node; } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToBefore(RIGHTBRACE, SEMICOLON); throw e; } } // expr // : composite_term [ composite_term ]* // ; CssPropertyValueNode expr() : { List lst = Lists.newArrayList(); CssValueNode value; } { value = composite_term() { lst.add(value); } ( value = composite_term() { lst.add(value); } )* { CssPropertyValueNode result = new CssPropertyValueNode(lst); result.setSourceCodeLocation(mergeLocations(lst)); return result; } } // (non-standard GSS extension) // composite_term // : assign_term [ ',' assign_term ]* // ; CssValueNode composite_term() : { CssValueNode value; List lst = Lists.newArrayList(); SourceCodeLocation beginLocation; Token t; List tokens = Lists.newArrayList(); } { { beginLocation = this.getLocation(token.next); } value = assign_term() { lst.add(value); } ( t = { tokens.add(t); } ( )* value = assign_term() { lst.add(value); } )* { if (lst.size() == 1) { return lst.get(0); } else { return nodeBuilder.buildCompositeValueNode(lst, CssCompositeValueNode.Operator.COMMA, this.mergeLocations(beginLocation, this.getLocation()), tokens); } } } // (non-standard GSS extension) // assign_term // : slash_term [ '=' slash_term ]* // ; CssValueNode assign_term() : { CssValueNode value; List lst = Lists.newArrayList(); SourceCodeLocation beginLocation; Token t; List tokens = Lists.newArrayList(); } { { beginLocation = this.getLocation(token.next); } value = slash_term() { lst.add(value); } ( t = { tokens.add(t); } ( )* value = slash_term() { lst.add(value); } )* { if (lst.size() == 1) { return lst.get(0); } return nodeBuilder.buildCompositeValueNode(lst, CssCompositeValueNode.Operator.EQUALS, this.mergeLocations(beginLocation, this.getLocation()), tokens); } } // (non-standard GSS extension) // slash_term // : term [ '/' term ]* // ; CssValueNode slash_term() : { CssValueNode value; List lst = Lists.newArrayList(); SourceCodeLocation beginLocation; Token t; List tokens = Lists.newArrayList(); } { { beginLocation = this.getLocation(token.next); } value = term() { lst.add(value); } ( t = { tokens.add(t); } ( )* value = term() { lst.add(value); } )* { if (lst.size() == 1) { return lst.get(0); } else { return nodeBuilder.buildCompositeValueNode(lst, CssCompositeValueNode.Operator.SLASH, this.mergeLocations(beginLocation, this.getLocation()), tokens); } } } // term // : unary_operator? // [ [ NUMBER [ PERCENT | IDENTIFIER ] ] | STRING | IDENT | FOR_VARIABLE // | '(' S? IDENT ':' S* NUMBER [ S* '/' S* NUMBER | IDENT ]? ')' // | URI | hexcolor | function | math // ] S* // ; CssValueNode term() : { Token t = null; Token dim = null; String unit = null; String unop = ""; CssFunctionNode function = null; CssStringNode stringNode = null; List tokens = Lists.newArrayList(); boolean hexcolor = false; boolean loopVariable = false; boolean unicodeRange = false; } { ( ( t = unary_operator() { unop = trim(t.image); tokens.add(t); } )? ( // Number with optional arbitrary dimension or percent. t = { unit = CssNumericNode.NO_UNITS; tokens.add(t); } ( (dim = | dim = ) { unit = dim.image.toLowerCase(); tokens.add(dim);} )? ) | t = { unicodeRange = true; tokens.add(t); } | stringNode = string() | // This lookahead is to support function names with . and :. ( LOOKAHEAD({getToken(2).kind != DOT && getToken(2).kind != COLON}) t = { tokens.add(t); } ) | // For variables will be evaluated to a number eventually. t = { loopVariable = true; tokens.add(t); } | // Support for syntax like '(max-height: 300px)' that is needed for // the @media rule. // Without the look ahead the parser can't distinguish between // '@page (not max-height: 300px)' and '@if (COND)'. // The look ahead ensures that the first token is '(' and then there might // be one or two arbitrary tokens followed by a colon. ( LOOKAHEAD({getToken(1).kind == LEFTROUND && (getToken(3).kind == COLON || getToken(4).kind == COLON)}) t = { tokens.add(t); } try { ( )? t = { tokens.add(t); } t = { tokens.add(t); } ( )* ( ( t = { tokens.add(t); } ( ( )* (t = ) { tokens.add(t); } ( )* (t = ) {tokens.add(t); } )? ( (dim = ) { tokens.add(dim); } )? ) | ( t = { tokens.add(t); } ) ) ( )* t = { tokens.add(t); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTROUND); throw e; } ) | LOOKAHEAD({getToken(1).kind == URI}) function = uri() | t = hexcolor() { tokens.add(t); hexcolor = true; } | function = calc() | function = function() ) ( )* { if (unit != null) { SourceCodeLocation location; if (dim != null) { location = this.mergeLocations(this.getLocation(t), this.getLocation(dim)); } else { location = this.getLocation(t); } return nodeBuilder.buildNumericNode(unop + t.image, unit, location, tokens); } else if (function != null) { return function; } else if (hexcolor) { return nodeBuilder.buildHexColorNode(t.image, this.getLocation(t), tokens); } else if (stringNode != null) { return stringNode; } else { StringBuilder sb = new StringBuilder(); for (Token token : tokens) { sb.append(token.image); } if (loopVariable) { return nodeBuilder.buildLoopVariableNode(sb.toString(), this.getLocation(t), tokens); } else if (unicodeRange) { return nodeBuilder.buildUnicodeRangeNode(sb.toString(), this.getLocation(t), tokens); } else { return nodeBuilder.buildLiteralNode(sb.toString(), this.getLocation(t), tokens); } } } } // (non-standard GSS extension) // extended_term // : boolean_and_term [ '||' S* boolean_and_term ]* // ; CssBooleanExpressionNode extended_term() : { SourceCodeLocation beginLocation; SourceCodeLocation endLocation; CssBooleanExpressionNode newNode = null; CssBooleanExpressionNode left = null; CssBooleanExpressionNode right = null; String value = ""; Token t; List tokens = Lists.newArrayList(); } { ( { beginLocation = this.getLocation(token.next); } left = boolean_and_term() { value += left.toString(); } ( t = { tokens.add(t); } ( )* right = boolean_and_term() { value += right.toString(); endLocation = this.getLocation(); newNode = nodeBuilder.buildBoolExpressionNode( CssBooleanExpressionNode.Type.OR, value, left, right, this.mergeLocations(beginLocation, endLocation), tokens); left = newNode; } )* ) { return left; } } // (non-standard GSS extension) // boolean_and_term // : [ boolean_negated_term | basic_term] // [ '&&' S* [ boolean_negated_term | basic_term ] ]* // ; CssBooleanExpressionNode boolean_and_term() : { SourceCodeLocation beginLocation = null; SourceCodeLocation endLocation; CssBooleanExpressionNode newNode= null; CssBooleanExpressionNode left= null; CssBooleanExpressionNode right = null; String value = ""; Token t; List tokens = Lists.newArrayList(); } { ( { beginLocation = this.getLocation(token.next); } ( left = boolean_negated_term() | left = basic_term() ) { value += left.toString(); } ( t = { tokens.add(t); } ( )* ( right = boolean_negated_term() | right = basic_term() ) { value += right.toString(); endLocation = this.getLocation(); newNode = nodeBuilder.buildBoolExpressionNode( CssBooleanExpressionNode.Type.AND, value, left, right, this.mergeLocations(beginLocation, endLocation), tokens); left = newNode; } )* ) { return left; } } // (non-standard GSS extension) // boolean_negated_term // : '!' S* basic_term // ; CssBooleanExpressionNode boolean_negated_term() : { SourceCodeLocation beginLocation; String value = ""; CssBooleanExpressionNode boolNode = null; Token t; List tokens = Lists.newArrayList(); } { ( t = { value = "!"; beginLocation = this.getLocation(); tokens.add(t); } ( )* boolNode = basic_term() ) { SourceCodeLocation endLocation = this.getLocation(); return nodeBuilder.buildBoolExpressionNode(CssBooleanExpressionNode.Type.NOT, value, boolNode, null, this.mergeLocations(beginLocation, endLocation), tokens); } } // (non-standard GSS extension) // basic_term // : term | parenthesized_term // ; CssBooleanExpressionNode basic_term() : { SourceCodeLocation beginLocation; String value = ""; CssBooleanExpressionNode node = null; CssValueNode termNode = null; List tokens = Lists.newArrayList(); } { { beginLocation = this.getLocation(token.next); } ( termNode = term() { value = termNode.toString(); } | node = parenthesized_term() { value = node.toString(); } ) { SourceCodeLocation endLocation = this.getLocation(); if (node == null) { return nodeBuilder.buildBoolExpressionNode(CssBooleanExpressionNode.Type.CONSTANT, value, null, null, this.mergeLocations(beginLocation, endLocation), tokens); } else { return node; } } } // (non-standard GSS extension) // parenthesized_term // : '(' S* extended_term ')' S* // ; CssBooleanExpressionNode parenthesized_term() : { Token t; List tokens = Lists.newArrayList(); CssBooleanExpressionNode node; } { t = { tokens.add(t); } try { ( )* node = extended_term() t = { tokens.add(t); } ( )* } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTROUND); throw e; } { nodeBuilder.attachComments(tokens, node); return node; } } // unary_operator // : '-' | '+' // ; Token unary_operator() : { Token t; } { ( t = | t = ) { return t; } } /* * There is a constraint on the color that it must * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) * after the "#"; e.g., "#000" is OK, but "#abcd" is not. */ // hexcolor // : HASH_NAME S* // ; Token hexcolor() : { Token t; } { t = { return t; } } CssFunctionNode uri() : { Token t; String funName; CssValueNode arg = null; CssPropertyValueNode expr = null; SourceCodeLocation beginLocation; } { { beginLocation = this.getLocation(token.next); } t = { return createUrlFunction(t); } } // function // : [ FUNCTION // | [ IDENTIFIER [ '.' | ':' ] [ IDENTIFIER [ '.' | ':' ] ]* FUNCTION ] // S* expr ')' S* // ; // Note: We allow the function name to have : and . to support non-standard IE // functions. CssFunctionNode function() : { Token t; CssPropertyValueNode expr; SourceCodeLocation beginLocation; StringBuilder functionName = new StringBuilder(); List tokens = Lists.newArrayList(); } { { beginLocation = this.getLocation(token.next); } ( t = { functionName.append(t.image); functionName.setLength(functionName.length() - 1); tokens.add(t); } | ( t = { functionName.append(t.image); tokens.add(t); } ( { functionName.append("."); } | { functionName.append(":"); } ) ( t = { functionName.append(t.image); } ( { functionName.append("."); } | { functionName.append(":"); } ) )* t = { functionName.append(t.image); functionName.setLength(functionName.length() - 1); } ) ) ( )* expr = expr() t = { tokens.add(t); } { SourceCodeLocation endLocation = this.getLocation(); CssFunctionArgumentsNode args = new CssFunctionArgumentsNode(); if (expr.numChildren() == 1) { CssValueNode child = expr.getChildAt(0); CssCompositeValueNode composite = null; if (child instanceof CssCompositeValueNode) { composite = (CssCompositeValueNode) child; } addArgumentsWithSeparator(args, ImmutableList.of(child), 1, " "); } else if (FUNCTIONS_WITH_SPACE_SEP_OK.matcher(functionName).matches()) { addArgumentsWithSeparator(args, expr.childIterable(), expr.numChildren(), " "); } else { throw generateParseException(); } CssFunctionNode functionNode = nodeBuilder.buildFunctionNode( functionName.toString(), this.mergeLocations(beginLocation, endLocation), args, tokens); return functionNode; } } // calc // : "calc(" S* sum S* ")" // ; CssFunctionNode calc() : { Token t; SourceCodeLocation beginLocation; StringBuilder functionName = new StringBuilder(); String sum = ""; List tokens = Lists.newArrayList(); } { { beginLocation = this.getLocation(token.next); } t = { functionName.append(t.image); functionName.setLength(functionName.length() - 1); tokens.add(t); } ()* sum = sum() ()* t = { tokens.add(t); } { SourceCodeLocation endLocation = this.getLocation(t); CssFunctionArgumentsNode args = new CssFunctionArgumentsNode(); CssMathNode arg = new CssMathNode(sum); addArgumentsWithSeparator(args, ImmutableList.of(arg), 1, ""); return nodeBuilder.buildFunctionNode( functionName.toString(), this.mergeLocations(beginLocation, endLocation), args, tokens); } } // sum // : product [ S+ [ "+" | "-" ] S+ product ]* // ; String sum() : { Token t; StringBuilder content = new StringBuilder(); String p = ""; } { ( p = product() {content.append(p);} ( (((()+) {content.append(" + ");}) | ( {content.append(" - ");})) p = product() {content.append(p);} )* ) { return content.toString(); } } // product // : unit [ S* [ "*" S* unit | "/" S* NUMBER ] ]* // ; String product() : { Token t; StringBuilder content = new StringBuilder(); String u = ""; } { ( u = unit() {content.append(u);} ( ()* ( ( {content.append("*");} ()* u = unit() {content.append(u);} ) | ( {content.append("/");} ()* t = {content.append(t.image);} ) ) )* ) { return content.toString(); } } // unit // : [ NUMBER | DIMENSION | PERCENTAGE | "(" S* sum S* ")" | calc ]; // ; String unit() : { Token t; Token dim; StringBuilder content = new StringBuilder(); String sum = ""; CssFunctionNode math = null; } { ( ( (t = unary_operator() {content.append(t.image);})? t = {content.append(t.image);} ( (dim = | dim = ) {content.append(dim.image.toLowerCase());} )? ) | ( { content.append("("); } try { ()* sum = sum() {content.append(sum);} ()* { content.append(")"); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTROUND); throw e; } ) | (math = calc() {content.append(math.getValue());}) ) { return content.toString(); } } // (non-standard GSS extension) // at_function // : FUNCTION S* expr? ')' S* // ; CssFunctionNode atFunction() : { Token t; CssPropertyValueNode expr = null; SourceCodeLocation beginLocation; StringBuilder functionName = new StringBuilder(); List tokens = Lists.newArrayList(); } { { beginLocation = this.getLocation(token.next); } t = { functionName.append(t.image); functionName.setLength(functionName.length() - 1); tokens.add(t); } ( )* ( expr = expr() )? t = { tokens.add(t); } ( )* { SourceCodeLocation endLocation = this.getLocation(); CssFunctionArgumentsNode args = new CssFunctionArgumentsNode(); if (expr != null && expr.numChildren() == 1) { CssValueNode child = expr.getChildAt(0); addArgumentsWithSeparator(args, ImmutableList.of(child), 1, " "); } else if (expr != null) { addArgumentsWithSeparator(args, expr.childIterable(), expr.numChildren(), " "); } CssFunctionNode functionNode = nodeBuilder.buildFunctionNode( functionName.toString(), this.mergeLocations(beginLocation, endLocation), args, tokens); return functionNode; } } // important // : IMPORTANT_SYM S* // ; CssPriorityNode important() : { Token t; List tokens = Lists.newArrayList(); SourceCodeLocation beginLocation, endLocation; } { t = { beginLocation = this.getLocation(); endLocation = this.getLocation(); tokens.add(t); } ( )* { return nodeBuilder.buildPriorityNode(this.mergeLocations(beginLocation, endLocation), tokens); } } // (non-standard GSS extension) // at_rule // : ATKEYWORD S* [ [ composite_term | extended_term ] S* ]* // [ [ '{' S* block '}' ] | ';' ] S* // ; CssAtRuleNode at_rule() : { Token t; SourceCodeLocation beginLocation = null; CssLiteralNode name; CssValueNode v; CssBlockNode block = null; List parameters = Lists.newArrayList(); List tokens = Lists.newArrayList(); } { try { t = { beginLocation = this.getLocation(t); name = new CssLiteralNode(t.image.substring(1), beginLocation); tokens.add(t); } ( )* ( ( v = composite_term() | v = extended_term() ) { parameters.add(v); } ( )* )* } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; if (skipComponentValuesToAfter(SEMICOLON, LEFTBRACE) == LEFTBRACE) { skipComponentValuesToAfter(RIGHTBRACE); } throw e; } ( ( t = { tokens.add(t); } try { ( )* block = block(true) t = { tokens.add(t); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTBRACE); throw e; } ) | t = { tokens.add(t); } | { ParseException e = generateParseException(); if (enableErrorRecovery && e.currentToken != null) { if (skipComponentValuesToAfter(SEMICOLON, LEFTBRACE) == LEFTBRACE) { skipComponentValuesToAfter(RIGHTBRACE); } } throw e; } ) { SourceCodeLocation endLocation = getLocation(t); CssAtRuleNode at = nodeBuilder.buildUnknownAtRuleNode(name, block, this.mergeLocations(beginLocation, endLocation), parameters, tokens); return at; } } // (non-standard GSS extension) // at_rule_with_decl_block // : ATRULESWITHDECLBLOCK S* // [ at_function // | [ IDENT? ':' IDENT S* ] // | [ [ composite_term | extended_term ] S* ]* // ] // [ [ '{' S* style_declaration '} ] | ';' ] S* // ; // TODO(fbenz): Try to reuse selctor parsing instead of [ IDENT? ':' IDENT S* ]. // The problem is that @-rules take a list of value nodes and selectors are not // value nodes. CssAtRuleNode atRuleWithDeclBlock() : { Token t; SourceCodeLocation beginLocation = null; CssLiteralNode name; CssValueNode v; CssAbstractBlockNode block = null; List parameters = Lists.newArrayList(); List tokens = Lists.newArrayList(); List pseudoPageTokens = Lists.newArrayList(); } { try { t = { beginLocation = this.getLocation(t); name = new CssLiteralNode(t.image.substring(1), beginLocation); tokens.add(t); } ( )* ( v = atFunction() { parameters.add(v); } | ( // This is needed for @page (e.g. @page :left {} and @page bla:first {}). LOOKAHEAD({getToken(1).kind == COLON || getToken(2).kind == COLON}) ( t = { pseudoPageTokens.add(t); v = nodeBuilder.buildLiteralNode(t.image, getLocation(t), pseudoPageTokens); parameters.add(v); pseudoPageTokens.clear(); } )? t = { pseudoPageTokens.add(t); } t = { pseudoPageTokens.add(t); v = nodeBuilder.buildLiteralNode(":" + t.image, getLocation(t), pseudoPageTokens); parameters.add(v); } ( )* ) | ( ( v = composite_term() | v = extended_term() ) { parameters.add(v); } ( )* )* ) } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; if (skipComponentValuesToAfter(SEMICOLON, LEFTBRACE) == LEFTBRACE) { skipComponentValuesToAfter(RIGHTBRACE); } throw e; } ( ( t = { tokens.add(t); } try { ( )* block = styleDeclaration() t = { tokens.add(t); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTBRACE); throw e; } ) | t = { tokens.add(t); } | { ParseException e = generateParseException(); if (enableErrorRecovery && e.currentToken != null) { if (skipComponentValuesToAfter(SEMICOLON, LEFTBRACE) == LEFTBRACE) { skipComponentValuesToAfter(RIGHTBRACE); } } throw e; } ) { SourceCodeLocation endLocation = getLocation(t); CssAtRuleNode at = nodeBuilder.buildUnknownAtRuleNode(name, block, this.mergeLocations(beginLocation, endLocation), parameters, tokens); return at; } } // (non-standard GSS extension) // inner_at_rule // : ATKEYWORD S* // [ at_function // | [ IDENT? ':' IDENT S* ] // | [[ composite_term | extended_term ] S* ]* // ] // [ [ '{' S* style_declaration '} ] | ';' ] S* // ; CssAtRuleNode innerAtRule() : { Token t; SourceCodeLocation beginLocation = null; CssLiteralNode name; CssValueNode v; CssAbstractBlockNode block = null; List parameters = Lists.newArrayList(); List tokens = Lists.newArrayList(); List pseudoPageTokens = Lists.newArrayList(); } { try { t = { beginLocation = this.getLocation(t); name = new CssLiteralNode(t.image.substring(1), beginLocation); tokens.add(t); } ( )* ( v = atFunction() { parameters.add(v); } | ( // This is needed for @page (e.g. @page :left {} and @page bla:first {}). LOOKAHEAD({getToken(1).kind == COLON || getToken(2).kind == COLON}) ( t = { pseudoPageTokens.add(t); v = nodeBuilder.buildLiteralNode(t.image, getLocation(t), pseudoPageTokens); parameters.add(v); pseudoPageTokens.clear(); } )? t = { pseudoPageTokens.add(t); } t = { pseudoPageTokens.add(t); v = nodeBuilder.buildLiteralNode(":" + t.image, getLocation(t), pseudoPageTokens); parameters.add(v); } ( )* ) | ( ( v = composite_term() | v = extended_term() ) { parameters.add(v); } ( )* )* ) } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; if (skipComponentValuesToAfter(SEMICOLON, LEFTBRACE) == LEFTBRACE) { skipComponentValuesToAfter(RIGHTBRACE); } throw e; } ( ( t = { tokens.add(t); } try { ( )* block = styleDeclaration() t = { tokens.add(t); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTBRACE); throw e; } ) | t = { tokens.add(t); } | { ParseException e = generateParseException(); if (enableErrorRecovery && e.currentToken != null) { if (skipComponentValuesToAfter(SEMICOLON, LEFTBRACE) == LEFTBRACE) { skipComponentValuesToAfter(RIGHTBRACE); } } throw e; } ) { SourceCodeLocation endLocation = getLocation(t); CssAtRuleNode at = nodeBuilder.buildUnknownAtRuleNode(name, block, this.mergeLocations(beginLocation, endLocation), parameters, tokens); return at; } } // (WebKit specific extension. We need a separate rule for the // WebKit keyframes, because they don't follow the standard grammar exactly.) // webkit_keyframes_rule // : '@-webkit-keyframes' S* IDENTIFIER S* // '{' S* webkit_keyframes_block '}'* // ; CssAtRuleNode webkit_keyframes_rule() : { Token t; SourceCodeLocation beginLocation = null; CssLiteralNode name; CssBlockNode block = null; List parameters = Lists.newArrayList(); List tokens = Lists.newArrayList(); } { try { t = { beginLocation = this.getLocation(t); name = new CssLiteralNode(t.image.substring(1), beginLocation); tokens.add(t); } ( )* t = { List identifierTokens = Lists.newArrayList(); identifierTokens.add(t); CssLiteralNode l = nodeBuilder.buildLiteralNode(t.image, getLocation(t), identifierTokens); parameters.add(l); } ( )* } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; if (skipComponentValuesToAfter(SEMICOLON, LEFTBRACE) == LEFTBRACE) { skipComponentValuesToAfter(RIGHTBRACE); } throw e; } ( t = { tokens.add(t); } try { ( )* block = webkit_keyframes_block() t = { tokens.add(t);} } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTBRACE); throw e; } ) { SourceCodeLocation endLocation = getLocation(t); CssAtRuleNode at = nodeBuilder.buildWebkitKeyframesNode(name, block, this.mergeLocations(beginLocation, endLocation), parameters, tokens); return at; } } // (WebKit specific extension) // webkit_keyframes_block // : [ webkit_keyframe_ruleset S* ]* // ; CssBlockNode webkit_keyframes_block() : { CssBlockNode block; CssNode n; } { { block = new CssBlockNode(true); } ((n = webkit_keyframe_ruleSet()) { block.addChildToBack(n); } ( )*)* { return block; } } // (WebKit specific extension) // webkit_keyframe_ruleset // : key_list '{' style_declarations '}' // ; CssKeyframeRulesetNode webkit_keyframe_ruleSet() : { CssKeyListNode keys; CssDeclarationBlockNode declarations; Token t; List tokens = Lists.newArrayList(); } { try { keys = keyList() t = { tokens.add(t); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; if (skipComponentValuesToAfter(SEMICOLON, LEFTBRACE) == LEFTBRACE) { skipComponentValuesToAfter(RIGHTBRACE); } throw e; } try { declarations = styleDeclaration() t = { tokens.add(t); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; skipComponentValuesToAfter(RIGHTBRACE); throw e; } { CssKeyframeRulesetNode ruleSet = nodeBuilder.buildKeyframeRulesetNode(declarations, keys, tokens); return ruleSet; } } // (WebKit specific extension) // key_list // : key [ ',' S* key ]* // ; CssKeyListNode keyList() : { CssKeyListNode list = new CssKeyListNode(); CssKeyNode key; Token t; } { key = key() { list.addChildToBack(key); } ( t = { nodeBuilder.attachComment(t, key); } ( )* key = key() { list.addChildToBack(key); } )* { return list; } } // (WebKit specific extension) // key // : PERCENTAGE | IDENTIFIER // ; CssKeyNode key() : { CssKeyNode n; Token key, t, dim; String value; List tokens = Lists.newArrayList(); SourceCodeLocation beginLocation; } { { beginLocation = this.getLocation(token.next); } ( (key = ) { tokens.add(key); } (dim = ) { tokens.add(dim); value = key.image + dim.image; } | key = { tokens.add(key); value = key.image; } ) ( t = { tokens.add(t); } )* { SourceCodeLocation endLocation = this.getLocation(); CssKeyNode keyNode = nodeBuilder.buildKeyNode(key, value, this.mergeLocations(beginLocation, endLocation)); nodeBuilder.attachComments(tokens, keyNode); return keyNode; } } // (non-standard GSS extension) // block // : [ [ ruleset | at_rule | webkit_keyframes_rule // | at_rule_with_decl_block // ] S* // ]* // ; CssBlockNode block(boolean isEnclosedWithBraces) : { CssBlockNode block; CssNode n; } { { if (isEnclosedWithBraces) { block = new CssBlockNode(isEnclosedWithBraces); } else { block = globalBlock; } } ( try { ( n = ruleSet() | LOOKAHEAD( ) n = atRuleWithCrazySyntax() | n = at_rule() | n = webkit_keyframes_rule() | n = atRuleWithDeclBlock() ) { block.addChildToBack(n); } } catch (ParseException e) { if (!enableErrorRecovery || e.currentToken == null) throw e; handledErrors.add(new GssParserException(getLocation(e.currentToken.next), e)); } ( | | )* )* { return block; } } void start() : {} { ( | | )* block(false) { try { validateFinalBlockCommentIfPresent(); } catch (ParseException e) { if (!enableErrorRecovery) throw e; handledErrors.add(new GssParserException(getLocation(), e)); } } } // CSS3 has very few syntactic constraints on at-rules. We shouldn't let // our inability to understand the details of future or non-standard at-rules // prevent us from parsing the rest of the stylesheet. // at_rule_with_crazy_syntax // : ATKEYWORD S* [^;{] LOOKAHEAD( ( ';' | ) ) ';'? CssAtRuleNode atRuleWithCrazySyntax() : { Token t; String s; SourceCodeLocation beginLocation = null; CssLiteralNode name; CssLiteralNode nonBlockContent; CssLiteralNode blockishContent = null; List tokens = Lists.newArrayList(); SourceCodeLocation endLocation = null; } { // Don't add more special cases like this one and the webkit keyframes // one. If you want to support a new block type that follows the CSS 2.1 // and 3 grammars and doesn't quite fit into the traditional GssParser // expectations, just change ATLIST below to ATKEYWORD, move the // use site in the block() to the bottom of its disjunction, and use // syntactic LOOKAHEAD(foo()) as needed for each foo() in the other choices // at that disjunction to ensure that the parser eventually falls back to // this rule. It will cost about 1.5% cpu time, but it will keep us from // adding any more code complexity here. t = { beginLocation = this.getLocation(t); name = new CssLiteralNode(t.image.substring(1), beginLocation); tokens.add(t); } ( ) * s = scanCrazyContent(";{") { nonBlockContent = new CssLiteralNode(s); tokens.add(t); } ( [blockishContent = crazyBlockBrace() ] [t = { tokens.add(t); } ] ) { endLocation = this.getLocation(tokens.get(tokens.size() - 1)); List parameters = Lists.newArrayList(); if (nonBlockContent == null) { throw new AssertionError("nonBlockContent should not be null"); } parameters.add(nonBlockContent); if (blockishContent != null) { parameters.add(blockishContent); } return nodeBuilder.buildUnknownAtRuleNode( name, null, this.mergeLocations(beginLocation, endLocation), parameters, tokens); } } // A last-resort, minimally-restrictive brace-delimited production. CssLiteralNode crazyBlockBrace() : { Token t; String s; SourceCodeLocation beginLocation; SourceCodeLocation endLocation; CssLiteralNode childContent = null; CssLiteralNode childCrazy = null; StringBuilder result = new StringBuilder(); } { t = ( )* s = scanCrazyContent("{[()]}") { childContent = new CssLiteralNode(s); } ( ( (childCrazy = crazyBlockBrace() | childCrazy = crazyBlockBracket() | childCrazy = crazyBlockParen()))? t = { endLocation = this.getLocation(t); } ) { result.append("{"); if (childContent != null) result.append(childContent.getValue()); if (childContent != null && childCrazy != null) result.append(" "); if (childCrazy != null) result.append(childCrazy.getValue()); result.append("}"); return new CssLiteralNode(result.toString()); } } // Inside blocks, brackets, parens, and braces must be balanced. CssLiteralNode crazyBlockBracket() : { Token t; String s; SourceCodeLocation beginLocation; SourceCodeLocation endLocation; CssLiteralNode childContent = null; CssLiteralNode childCrazy = null; StringBuilder result = new StringBuilder(); } { t = ( )* s = scanCrazyContent("{[()]}") { childContent = new CssLiteralNode(s); } ( ( (childCrazy = crazyBlockBrace() | childCrazy = crazyBlockBracket() | childCrazy = crazyBlockParen()))? t = { endLocation = this.getLocation(t); } ) { result.append("["); if (childContent != null) result.append(childContent.getValue()); if (childContent != null && childCrazy != null) result.append(" "); if (childCrazy != null) result.append(childCrazy.getValue()); result.append("]"); return new CssLiteralNode(result.toString()); } } // Inside blocks, brackets, parens, and braces must be balanced. CssLiteralNode crazyBlockParen() : { Token t; String s; SourceCodeLocation beginLocation; SourceCodeLocation endLocation; CssLiteralNode childContent = null; CssLiteralNode childCrazy = null; StringBuilder result = new StringBuilder(); } { t = ( )* s = scanCrazyContent("{[()]}") { childContent = new CssLiteralNode(s); } ( ( (childCrazy = crazyBlockBrace() | childCrazy = crazyBlockBracket() | childCrazy = crazyBlockParen()))? t = { endLocation = this.getLocation(t); } ) { result.append("("); if (childContent != null) result.append(childContent.getValue()); if (childContent != null && childCrazy != null) result.append(" "); if (childCrazy != null) result.append(childCrazy.getValue()); result.append(")"); return new CssLiteralNode(result.toString()); } } JAVACODE String scanCrazyContent(String endChars) { StringBuilder sb = new StringBuilder(); Token t; while (true) { t = getToken(1); if (t.kind == EOF) { break; } if (t.image.length() == 1 && endChars.indexOf(t.image) != -1) { break; } sb.append(t.image); t = getNextToken(); } if (sb.length() < 1) { throw generateParseException(); } return sb.toString(); } // Consumes tokens until just before specified kinds of token or EOF, matching braces {[()]}. // It doesn't match tokens inside blocks surrounded by braces performing leftmost derivation. JAVACODE int skipComponentValuesToBefore(Integer... kinds) { Set kindset = ImmutableSet.builder().add(EOF).add(kinds).build(); Token t; do { t = getToken(1); if (kindset.contains(t.kind)) { return t.kind; } getNextToken(); } while ((t.kind != LEFTBRACE || skipComponentValuesToAfter(RIGHTBRACE) != EOF) && (t.kind != LEFTROUND || skipComponentValuesToAfter(RIGHTROUND) != EOF) && (t.kind != LEFTSQUARE || skipComponentValuesToAfter(RIGHTSQUARE) != EOF)); return EOF; } // Consumes tokens until just after specified kinds of token or EOF, matching braces {[()]}. JAVACODE int skipComponentValuesToAfter(Integer... kinds) { int kind = skipComponentValuesToBefore(kinds); if (kind != EOF) { getNextToken(); } return kind; } JAVACODE void validateFinalBlockCommentIfPresent() throws ParseException { if (token.specialToken != null && !VALID_BLOCK_COMMENT_PATTERN.matcher(token.specialToken.image).matches()) { // Manually construct a ParseException since this syntax error occurs after the last token, // and we don't want the ParseException to reference a non-existent token. throw new ParseException("unterminated block comment at EOF"); } }