com.google.common.css.compiler.ast.GssParserCC.jj Maven / Gradle / Ivy
Show all versions of closure-stylesheets Show documentation
// 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 extends CssNode> locations) {
Iterator extends CssNode> 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 =