/** * This file Copyright (c) 2005-2008 Aptana, Inc. This program is * dual-licensed under both the Aptana Public License and the GNU General * Public license. You may elect to use one or the other of these licenses. * * This program is distributed in the hope that it will be useful, but * AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or * NONINFRINGEMENT. Redistribution, except as permitted by whichever of * the GPL or APL you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or modify this * program under the terms of the GNU General Public License, * Version 3, as published by the Free Software Foundation. You should * have received a copy of the GNU General Public License, Version 3 along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Aptana provides a special exception to allow redistribution of this file * with certain other free and open source software ("FOSS") code and certain additional terms * pursuant to Section 7 of the GPL. You may view the exception and these * terms on the web at http://www.aptana.com/legal/gpl/. * * 2. For the Aptana Public License (APL), this program and the * accompanying materials are made available under the terms of the APL * v1.0 which accompanies this distribution, and is available at * http://www.aptana.com/legal/apl/. * * You may view the GPL, Aptana's exception and additional terms, and the * APL in the file titled license.html at the root of the corresponding * plugin containing this source file. * * Any modifications to this file must keep this entire header intact. */ package com.aptana.ide.editor.css.parsing; import java.text.ParseException; import java.util.Arrays; import com.aptana.ide.editor.css.lexing.CSSTokenTypes; import com.aptana.ide.editor.css.parsing.nodes.CSSCharSetNode; import com.aptana.ide.editor.css.parsing.nodes.CSSDeclarationNode; import com.aptana.ide.editor.css.parsing.nodes.CSSExprNode; import com.aptana.ide.editor.css.parsing.nodes.CSSImportNode; import com.aptana.ide.editor.css.parsing.nodes.CSSListNode; import com.aptana.ide.editor.css.parsing.nodes.CSSMediaNode; import com.aptana.ide.editor.css.parsing.nodes.CSSMediumNode; import com.aptana.ide.editor.css.parsing.nodes.CSSPageNode; import com.aptana.ide.editor.css.parsing.nodes.CSSParseNode; import com.aptana.ide.editor.css.parsing.nodes.CSSParseNodeTypes; import com.aptana.ide.editor.css.parsing.nodes.CSSRuleSetNode; import com.aptana.ide.editor.css.parsing.nodes.CSSSelectorNode; import com.aptana.ide.editor.css.parsing.nodes.CSSSimpleSelectorNode; import com.aptana.ide.editor.css.parsing.nodes.CSSTermNode; import com.aptana.ide.editor.css.parsing.nodes.CSSTextNode; import com.aptana.ide.lexer.ILexer; import com.aptana.ide.lexer.IToken; import com.aptana.ide.lexer.Lexeme; import com.aptana.ide.lexer.LexemeList; import com.aptana.ide.lexer.LexerException; import com.aptana.ide.parsing.IParseState; import com.aptana.ide.parsing.ParserInitializationException; import com.aptana.ide.parsing.nodes.IParseNode; import com.aptana.ide.parsing.nodes.ParseNodeBase; import com.aptana.ide.parsing.nodes.ParseRootNode; /** * @author Kevin Lindsey */ public class CSSParser extends CSSParserBase { private static final String HTML_QUOTE_TYPE = "QUOTE"; private static final String HTML_MIME_TYPE = "text/html"; private static final int[] atKeywordSet = new int[] { CSSTokenTypes.AT_KEYWORD, CSSTokenTypes.IMPORT, CSSTokenTypes.PAGE, CSSTokenTypes.MEDIA, CSSTokenTypes.CHARSET }; private static final int[] attributeValueOperator = new int[] { CSSTokenTypes.EQUAL, CSSTokenTypes.INCLUDES, CSSTokenTypes.DASHMATCH }; private static final int[] attributeSet2 = new int[] { CSSTokenTypes.IDENTIFIER, CSSTokenTypes.STRING }; private static final int[] combinatorSet = new int[] { CSSTokenTypes.PLUS, CSSTokenTypes.GREATER }; private static final int[] dimensionsSet = new int[] { CSSTokenTypes.DIMENSION, CSSTokenTypes.EMS, CSSTokenTypes.EXS, CSSTokenTypes.LENGTH, CSSTokenTypes.ANGLE, CSSTokenTypes.TIME, CSSTokenTypes.FREQUENCY }; private static final int[] typeOrUniversalSelector = new int[] { CSSTokenTypes.IDENTIFIER, CSSTokenTypes.STAR, CSSTokenTypes.SELECTOR }; private static final int[] operatorSet = new int[] { CSSTokenTypes.FORWARD_SLASH, CSSTokenTypes.COMMA, CSSTokenTypes.EQUAL // added to support 'filter: alpha(opacity = 70);' }; private static final int[] simpleSelectorSet1 = new int[] { CSSTokenTypes.IDENTIFIER, CSSTokenTypes.STAR, CSSTokenTypes.HASH, CSSTokenTypes.CLASS, CSSTokenTypes.LBRACKET, CSSTokenTypes.COLON }; private static final int[] attributeSelector = new int[] { CSSTokenTypes.HASH, CSSTokenTypes.CLASS, CSSTokenTypes.LBRACKET, CSSTokenTypes.COLON, CSSTokenTypes.COLOR }; // CSSTokenTypes.COLOR is temporary until we know when we're inside a ruleset or not private static final int[] primitivesAndOperators = new int[] { CSSTokenTypes.FORWARD_SLASH, CSSTokenTypes.COMMA, CSSTokenTypes.PLUS, CSSTokenTypes.MINUS, CSSTokenTypes.NUMBER, CSSTokenTypes.PERCENTAGE, CSSTokenTypes.LENGTH, CSSTokenTypes.EMS, CSSTokenTypes.EXS, CSSTokenTypes.ANGLE, CSSTokenTypes.TIME, CSSTokenTypes.FREQUENCY, CSSTokenTypes.FUNCTION, CSSTokenTypes.STRING, CSSTokenTypes.IDENTIFIER, CSSTokenTypes.URL, CSSTokenTypes.COLOR, CSSTokenTypes.EQUAL // added to support 'filter: alpha(opacity = 70);' }; private static final int[] primitives = new int[] { CSSTokenTypes.PLUS, CSSTokenTypes.MINUS, CSSTokenTypes.NUMBER, CSSTokenTypes.PERCENTAGE, CSSTokenTypes.LENGTH, CSSTokenTypes.EMS, CSSTokenTypes.EXS, CSSTokenTypes.ANGLE, CSSTokenTypes.TIME, CSSTokenTypes.FREQUENCY, CSSTokenTypes.FUNCTION, CSSTokenTypes.STRING, CSSTokenTypes.IDENTIFIER, CSSTokenTypes.URL, CSSTokenTypes.COLOR, CSSTokenTypes.FORWARD_SLASH, CSSTokenTypes.COMMA }; private static final int[] termSet3 = new int[] { CSSTokenTypes.NUMBER, CSSTokenTypes.PERCENTAGE, CSSTokenTypes.LENGTH, CSSTokenTypes.EMS, CSSTokenTypes.EXS, CSSTokenTypes.ANGLE, CSSTokenTypes.TIME, CSSTokenTypes.FREQUENCY, CSSTokenTypes.FUNCTION, CSSTokenTypes.STRING, CSSTokenTypes.IDENTIFIER, CSSTokenTypes.URL, CSSTokenTypes.COLOR }; private static final int[] unaryOperatorSet = new int[] { CSSTokenTypes.PLUS, CSSTokenTypes.MINUS }; private static final String DEFAULT_GROUP = "default"; //$NON-NLS-1$ /** * static constructor */ static { Arrays.sort(atKeywordSet); Arrays.sort(attributeValueOperator); Arrays.sort(attributeSet2); Arrays.sort(combinatorSet); Arrays.sort(dimensionsSet); Arrays.sort(typeOrUniversalSelector); Arrays.sort(operatorSet); Arrays.sort(simpleSelectorSet1); Arrays.sort(attributeSelector); Arrays.sort(primitivesAndOperators); Arrays.sort(primitives); Arrays.sort(termSet3); Arrays.sort(unaryOperatorSet); } /** * Create a new instance of CSSParser * * @throws ParserInitializationException */ public CSSParser() throws ParserInitializationException { this(CSSMimeType.MimeType); } /** * Create a new instance of CSSParser * * @param mimeType * @throws ParserInitializationException */ public CSSParser(String mimeType) throws ParserInitializationException { super(mimeType); } /** * createNode * * @param type * @param startingLexeme * @return CSSParseNode */ private CSSParseNode createNode(int type, Lexeme startingLexeme) { return (CSSParseNode) this.getParseNodeFactory().createParseNode(type, startingLexeme); } /** * <pre> * CSS * : Import * | Page * | Media * | CharSet * | RuleSet * ; * </pre> * * @see com.aptana.ide.parsing.AbstractParser#parseAll(com.aptana.ide.parsing.nodes.IParseNode) */ public synchronized void parseAll(IParseNode parentNode) throws LexerException { IParseNode rootNode = this.getParseRootNode(parentNode, ParseRootNode.class); ILexer lexer = this.getLexer(); lexer.setLanguageAndGroup(this.getLanguage(), DEFAULT_GROUP); // HACK [KEL]: check if the lexeme before our current offset is an HTML quote. // If it is, then parse as a style: no rule sets, only property definitions IParseState parseState = this.getParseState(); boolean inStyleElement = false; if (parseState != null) { LexemeList lexemes = parseState.getLexemeList(); Lexeme lastLexeme = lexemes.getFloorLexeme(lexer.getCurrentOffset()); inStyleElement = (lastLexeme != null) ? (lastLexeme.getLanguage().equals(HTML_MIME_TYPE) && lastLexeme .getType().equals(HTML_QUOTE_TYPE)) : false; } this.advance(); while (this.isEOS() == false) { IParseNode result = null; try { if (inStyleElement) { Lexeme currentLexeme = this.currentLexeme; result = this.parseRuleSetBody(); if (currentLexeme == this.currentLexeme) { this.advance(); } } else { switch (this.currentLexeme.typeIndex) { case CSSTokenTypes.AT_KEYWORD: result = this.parseAtRule(); break; case CSSTokenTypes.CHARSET: result = this.parseCharSet(); break; case CSSTokenTypes.IMPORT: result = this.parseImport(); break; case CSSTokenTypes.PAGE: result = this.parsePage(); break; case CSSTokenTypes.MEDIA: result = this.parseMedia(); break; case CSSTokenTypes.COLOR: // temporary until we know when we're inside a ruleset or not case CSSTokenTypes.IDENTIFIER: case CSSTokenTypes.SELECTOR: case CSSTokenTypes.PROPERTY: case CSSTokenTypes.STAR: case CSSTokenTypes.HASH: case CSSTokenTypes.CLASS: case CSSTokenTypes.LBRACKET: case CSSTokenTypes.COLON: result = this.parseRuleSet(); break; default: this.advance(); break; } } } catch (ParseException e) { // reset group lexer.setGroup(DEFAULT_GROUP); this.advance(); } finally { if (rootNode != null && result != null) { rootNode.appendChild(result); } } } } /** * parseCharSet * <p> * <code> * CharSet * : CHARSET STRING SEMICOLON * ; * </code> * * @return CSSCharSetNode * @throws ParseException * @throws LexerException */ private CSSCharSetNode parseCharSet() throws LexerException, ParseException { CSSCharSetNode result = (CSSCharSetNode) this.createNode(CSSParseNodeTypes.CHAR_SET, this.currentLexeme); // advance over '@charset' this.assertAndAdvance(CSSTokenTypes.CHARSET, "error.charset"); //$NON-NLS-1$ // advance over name this.assertType(CSSTokenTypes.STRING, "error.charset.name"); //$NON-NLS-1$ CSSTextNode name = (CSSTextNode) this.createNode(CSSParseNodeTypes.TEXT, this.currentLexeme); result.appendChild(name); this.advance(); // advance over ';' this.assertType(CSSTokenTypes.SEMICOLON, "error.charset.semicolon"); //$NON-NLS-1$ result.includeLexemeInRange(this.currentLexeme); this.advance(); return result; } /** * parseImport * <p> * Import : IMPORT (STRING | URL) (IDENTIFIER (COMMA IDENTIFIER)*)? SEMICOLON ; <code> * </code> * * @return ParseNode * @throws ParseException * @throws LexerException */ private ParseNodeBase parseImport() throws LexerException, ParseException { CSSImportNode result = (CSSImportNode) this.createNode(CSSParseNodeTypes.IMPORT, this.currentLexeme); // advance over '@import' this.assertAndAdvance(CSSTokenTypes.IMPORT, "error.import"); //$NON-NLS-1$ switch (this.currentLexeme.typeIndex) { case CSSTokenTypes.STRING: case CSSTokenTypes.URL: // lexer no longer creates this token type String name = this.currentLexeme.getText(); result.setAttribute("name", name); //$NON-NLS-1$ this.advance(); break; case CSSTokenTypes.FUNCTION: StringBuilder buffer = new StringBuilder(); buffer.append(this.currentLexeme.getText()); this.advance(); while (this.isEOS() == false && this.isType(CSSTokenTypes.RPAREN) == false) { buffer.append(this.currentLexeme.getText()); this.advance(); } if (this.isType(CSSTokenTypes.RPAREN)) { buffer.append(this.currentLexeme.getText()); result.setAttribute("name", buffer.toString()); //$NON-NLS-1$ this.advance(); } else { this.throwParseError("error.import.name"); //$NON-NLS-1$ } break; default: this.throwParseError("error.import.name"); //$NON-NLS-1$ } if (this.isType(CSSTokenTypes.IDENTIFIER)) { // create list CSSListNode list = (CSSListNode) this.createNode(CSSParseNodeTypes.LIST, this.currentLexeme); // set list item delimiter list.setDelimiter(", "); //$NON-NLS-1$ // get first medium name CSSTextNode medium = (CSSTextNode) this.createNode(CSSParseNodeTypes.TEXT, this.currentLexeme); // and add it to the list list.appendChild(medium); // advance over the medium name this.advance(); // process all remaining medium names while (this.isEOS() == false && this.isType(CSSTokenTypes.COMMA)) { // advance over ',' this.advance(); this.assertType(CSSTokenTypes.IDENTIFIER, "error.import.medium"); //$NON-NLS-1$ medium = (CSSTextNode) this.createNode(CSSParseNodeTypes.TEXT, this.currentLexeme); list.appendChild(medium); this.advance(); } // add list to import node result.appendChild(list); } // advance over ';' this.assertType(CSSTokenTypes.SEMICOLON, "error.import.semicolon"); //$NON-NLS-1$ result.includeLexemeInRange(this.currentLexeme); this.advance(); return result; } /** * parseMedia * <p> * <code> * Media * : MEDIA IDENTIFIER (COMMA IDENTIFIER)* LBRACE RuleSet* RBRACE * ; * </code> * * @return ParseNode * @throws ParseException * @throws LexerException */ private CSSMediaNode parseMedia() throws LexerException, ParseException { CSSMediaNode result = (CSSMediaNode) this.createNode(CSSParseNodeTypes.MEDIA, this.currentLexeme); // advance over '@media" this.assertAndAdvance(CSSTokenTypes.MEDIA, "error.media"); //$NON-NLS-1$ // create media list CSSListNode media = (CSSListNode) this.createNode(CSSParseNodeTypes.LIST, null); // set media list delimiter media.setDelimiter(", "); //$NON-NLS-1$ // grab first medium this.assertType(CSSTokenTypes.IDENTIFIER, "error.media.identifier"); //$NON-NLS-1$ CSSMediumNode medium = (CSSMediumNode) this.createNode(CSSParseNodeTypes.MEDIUM, this.currentLexeme); // add it to our list media.appendChild(medium); // advance over medium name this.advance(); // process any remaining medium names while (this.isEOS() == false && this.isType(CSSTokenTypes.COMMA)) { // advance over ',' this.advance(); this.assertType(CSSTokenTypes.IDENTIFIER, "error.import.medium"); //$NON-NLS-1$ medium = (CSSMediumNode) this.createNode(CSSParseNodeTypes.MEDIUM, this.currentLexeme); media.appendChild(medium); this.advance(); } // add list to result result.appendChild(media); // advance over '{' this.assertAndAdvance(CSSTokenTypes.LCURLY, "error.import.open"); //$NON-NLS-1$ if (this.isType(CSSTokenTypes.IDENTIFIER)) { CSSListNode ruleSets = (CSSListNode) this.createNode(CSSParseNodeTypes.LIST, this.currentLexeme); // advance over rulesets while (this.isEOS() == false && this.isType(CSSTokenTypes.IDENTIFIER)) { ruleSets.appendChild(this.parseRuleSet()); } result.appendChild(ruleSets); } else { result.appendChild(CSSParseNode.Empty); } // advance over '}' this.assertType(CSSTokenTypes.RCURLY, "error.import.close"); //$NON-NLS-1$ result.includeLexemeInRange(this.currentLexeme); this.advance(); return result; } /** * parsePage * <p> * <code> * Page * : PAGE (COLON IDENTIFIER)? LCURLY (Declaration SEMICOLON)* RCURLY * ; * </code> * * @return ParseNode * @throws ParseException * @throws LexerException */ private CSSPageNode parsePage() throws LexerException, ParseException { CSSPageNode result = (CSSPageNode) this.createNode(CSSParseNodeTypes.PAGE, this.currentLexeme); // advance over '@page' this.assertAndAdvance(CSSTokenTypes.PAGE, "error.page"); //$NON-NLS-1$ // check for pseudo page if (this.isType(CSSTokenTypes.COLON)) { // advance over ':' this.advance(); this.assertType(CSSTokenTypes.IDENTIFIER, "error.page.name"); //$NON-NLS-1$ String pseudoPage = this.currentLexeme.getText(); result.setAttribute("name", pseudoPage); //$NON-NLS-1$ this.advance(); } // advance over '{' this.assertAndAdvance(CSSTokenTypes.LCURLY, "error.page.open"); //$NON-NLS-1$ CSSListNode declarations = (CSSListNode) this.createNode(CSSParseNodeTypes.LIST, null); declarations.setDelimiter("\n"); //$NON-NLS-1$ // parse declaration if (this.isType(CSSTokenTypes.IDENTIFIER) || this.isType(CSSTokenTypes.PROPERTY)) { declarations.appendChild(this.parseDeclaration()); } while (this.isType(CSSTokenTypes.SEMICOLON)) { // advance over ';' this.advance(); // parse declaration if (this.isType(CSSTokenTypes.IDENTIFIER) || this.isType(CSSTokenTypes.PROPERTY)) { declarations.appendChild(this.parseDeclaration()); } else { declarations.appendChild(CSSParseNode.Empty); } } result.appendChild(declarations); // advance over '}' this.assertType(CSSTokenTypes.RCURLY, "error.page.close"); //$NON-NLS-1$ result.includeLexemeInRange(this.currentLexeme); this.advance(); return result; } /** * parseAtRule * <p> * <code> * AtRule * : ATKEYWORD STRING (Block | SEMICOLON) * ; * </code> * * @throws LexerException * @throws ParseException * @return CSSAtRuleNode */ private CSSParseNode parseAtRule() throws LexerException, ParseException { CSSParseNode result = this.createNode(CSSParseNodeTypes.AT_RULE, this.currentLexeme); // consume @ keyword this.inSet(atKeywordSet); this.advance(); // zero or more "any" tokens if (this.isType(CSSTokenTypes.STRING)) { this.advance(); } // block or semicolon switch (this.currentLexeme.typeIndex) { case CSSTokenTypes.LCURLY: this.advance(); CSSListNode declarations = parseRuleSetBody(); result.appendChild(declarations); result.includeLexemeInRange(this.currentLexeme); this.assertAndAdvance(CSSTokenTypes.RCURLY, "error.rule-set.close"); //$NON-NLS-1$ break; case CSSTokenTypes.SEMICOLON: result.includeLexemeInRange(this.currentLexeme); this.advance(); break; default: this.throwParseError("error-at-rule.semicolon"); //$NON-NLS-1$ } return result; } /** * parseBlock * <p> * <code> * Block * : LCURLY ^RCURLY* RCURLY * ; * </code> * * @return CSSBlockNode * @throws ParseException * @throws LexerException */ private CSSParseNode parseBlock() throws LexerException, ParseException { CSSParseNode result = this.createNode(CSSParseNodeTypes.BLOCK, this.currentLexeme); this.assertAndAdvance(CSSTokenTypes.LCURLY, "error.block.open"); //$NON-NLS-1$ while (this.isEOS() == false && this.isType(CSSTokenTypes.RCURLY) == false) { this.advance(); } if (this.isType(CSSTokenTypes.RCURLY)) { this.advance(); } return result; } /** * parseDeclaration * <p> * <code> * Declaration * : IDENTIFIER COLON Expression IMPORTANT? * ; * </code> * * @throws LexerException * @throws ParseException * @return CSSDeclarationNode */ private CSSDeclarationNode parseDeclaration() throws LexerException, ParseException { CSSDeclarationNode result; // grab property name try { this.assertType(CSSTokenTypes.IDENTIFIER, "error.property"); //$NON-NLS-1$ } catch (ParseException e) { this.assertType(CSSTokenTypes.PROPERTY, "error.property"); //$NON-NLS-1$ } // change identifier to PROPERTY token type for proper semantics and colorization this.changeTokenType(PROPERTY_TOKEN); String name = this.currentLexeme.getText(); result = (CSSDeclarationNode) this.createNode(CSSParseNodeTypes.DECLARATION, this.currentLexeme); result.setAttribute("name", name); //$NON-NLS-1$ this.advance(); // advance over ':' this.assertAndAdvance(CSSTokenTypes.COLON, "error.declaration.colon"); //$NON-NLS-1$ result.includeLexemeInRange(currentLexeme); // ensure there is an end lexeme // handle expr result.appendChild(this.parseExpression()); // check for "!important" if (this.isType(CSSTokenTypes.IMPORTANT)) { String important = this.currentLexeme.getText(); result.setAttribute("status", important); //$NON-NLS-1$ result.includeLexemeInRange(this.currentLexeme); this.advance(); } return result; } /** * changeTokenType */ private void changeTokenType(IToken token) { if (this.currentLexeme.getToken() != token) { // change token type this.currentLexeme.setToken(token); // force refresh of this token this.getParseState().addUpdateRegion(this.currentLexeme); } } /** * parseExpression * <p> * <code> * Expression * : Term ((SLASH | COMMA)? (PLUS | MINUS | NUMBER | PERCENTAGE | LENGTH | * EMS | EXS | ANGLE | TIME | FREQUENCY | FUNCTION | * STRING | IDENTIFIER | URL | COLOR | SLASH | COMMA) )* * ; * </code> * * @return CSSExprNode * @throws LexerException * @throws ParseException */ private CSSExprNode parseExpression() throws LexerException, ParseException { CSSExprNode result = (CSSExprNode) this.createNode(CSSParseNodeTypes.EXPR, this.currentLexeme); // get at least one term result.appendChild(this.parseTerm()); // keep collecting terms until there are no more primitives nor separators while (this.inSet(primitivesAndOperators)) { String operator = " "; //$NON-NLS-1$ CSSTermNode term = null; if (this.inSet(operatorSet)) { operator = this.currentLexeme.getText(); this.advance(); } if (this.inSet(primitives)) { term = this.parseTerm(); term.setAttribute("joining-operator", operator); //$NON-NLS-1$ result.appendChild(term); } else { throwParseError("error.expression.term"); //$NON-NLS-1$ } } return result; } /** * parseTerm * <p> * <code> * Term * : (PLUS | MINUS)? (NUMBER | PERCENTAGE | LENGTH | EMS | EXS | ANGLE | TIME | FREQUENCY | STRING | IDENTIFIER | URL | COLOR | FUNCTION Expression) ; * </code> * * @return CSSTermNode * @throws LexerException * @throws ParseException */ private CSSTermNode parseTerm() throws LexerException, ParseException { CSSTermNode result = (CSSTermNode) this.createNode(CSSParseNodeTypes.TERM, this.currentLexeme); if (this.inSet(unaryOperatorSet)) { // grab '-' or '+' and advance String operator = this.currentLexeme.getText(); result.setAttribute("operator", operator); //$NON-NLS-1$ this.advance(); } // make sure this is a primitive this.assertInSet(termSet3, "error.term"); //$NON-NLS-1$ String value = this.currentLexeme.getText(); result.setAttribute("value", value); //$NON-NLS-1$ result.includeLexemeInRange(this.currentLexeme); if (this.isType(CSSTokenTypes.FUNCTION)) { // advance over function this.advance(); CSSExprNode expr = this.parseExpression(); result.appendChild(expr); if (this.isType(CSSTokenTypes.RPAREN)) { result.includeLexemeInRange(this.currentLexeme); } this.assertAndAdvance(CSSTokenTypes.RPAREN, "error.function.close"); //$NON-NLS-1$ } else { this.advance(); } return result; } /** * parseRuleSet * <p> * <code> * RuleSet * : Selector (COMMA Selector)* LCURLY (Declaration | SEMICOLON)* RCURLY * ; * </code> * * @return CSSRuleSetNode * @throws ParseException * @throws LexerException */ private CSSRuleSetNode parseRuleSet() throws ParseException, LexerException { CSSRuleSetNode result = (CSSRuleSetNode) this.createNode(CSSParseNodeTypes.RULE_SET, this.currentLexeme); CSSListNode selectors = (CSSListNode) this.createNode(CSSParseNodeTypes.LIST, this.currentLexeme); selectors.setListName("selectors"); //$NON-NLS-1$ // add first selector selectors.appendChild(this.parseSelector()); // add any following selectors while (this.isType(CSSTokenTypes.COMMA)) { // advance over ',' this.advance(); // add selector selectors.appendChild(this.parseSelector()); } result.appendChild(selectors); this.assertAndAdvance(CSSTokenTypes.LCURLY, "error.rule-set.open"); //$NON-NLS-1$ CSSListNode declarations = parseRuleSetBody(); result.appendChild(declarations); result.includeLexemeInRange(this.currentLexeme); this.assertAndAdvance(CSSTokenTypes.RCURLY, "error.rule-set.close"); //$NON-NLS-1$ return result; } /** * parseRuleSetBody * * @return CSSListNode * @throws LexerException * @throws ParseException */ private CSSListNode parseRuleSetBody() throws LexerException, ParseException { CSSListNode declarations = (CSSListNode) this.createNode(CSSParseNodeTypes.LIST, null); declarations.setDelimiter("\n"); //$NON-NLS-1$ declarations.setListName("properties"); //$NON-NLS-1$ // add declaration if (this.isType(CSSTokenTypes.IDENTIFIER) || this.isType(CSSTokenTypes.PROPERTY)) { declarations.appendChild(this.parseDeclaration()); } else { if (this.isType(CSSTokenTypes.SEMICOLON)) { declarations.appendChild(CSSParseNode.Empty); } } // add any following declarations while (this.isType(CSSTokenTypes.SEMICOLON)) { // advance over ';' this.advance(); // add declaration if (this.isType(CSSTokenTypes.IDENTIFIER) || this.isType(CSSTokenTypes.PROPERTY)) { declarations.appendChild(this.parseDeclaration()); } else { if (this.isType(CSSTokenTypes.SEMICOLON)) { declarations.appendChild(CSSParseNode.Empty); } } } return declarations; } /** * parseSelector * <p> * <code> * Selector * : SimpleSelector ((PLUS | GREATER_THAN)? SimpleSelector?)* * ; * </code> * * @throws ParseException * @throws LexerException */ private CSSSelectorNode parseSelector() throws ParseException, LexerException { boolean process = true; CSSSelectorNode result = (CSSSelectorNode) this.createNode(CSSParseNodeTypes.SELECTOR, this.currentLexeme); // change identifier to SELECTOR token type for proper semantics and colorization if (this.isType(CSSTokenTypes.IDENTIFIER)) { this.changeTokenType(SELECTOR_TOKEN); } CSSSimpleSelectorNode simpleSelector = this.parseSimpleSelector(); simpleSelector.appendChild(CSSParseNode.Empty); result.appendChild(simpleSelector); while (process) { CSSTextNode combinator = null; simpleSelector = null; // assume we're done process = false; // see if we have a combinator if (this.inSet(combinatorSet)) { // grab combinator combinator = (CSSTextNode) this.createNode(CSSParseNodeTypes.TEXT, this.currentLexeme); // advance this.advance(); // continue process = true; } // see if we have a selector if (this.inSet(simpleSelectorSet1)) { simpleSelector = parseSimpleSelector(); if (combinator != null) { simpleSelector.appendChild(combinator); } else { simpleSelector.appendChild(CSSParseNode.Empty); } // continue process = true; } if (simpleSelector != null) { result.appendChild(simpleSelector); } } return result; } /** * parseSimpleSelector * <p> * <code> * SimpleSelector * : (PLUS | GREATER_THAN) * | (PLUS | GREATER_THAN)? (HASH | CLASS | LBRACKET IDENTIFIER (EQUALS INCLUDES DASHMATCH)? RBRACKET * | COLON (IDENTIFIER | FUNCTION) * ; * </code> * * @throws ParseException * @throws LexerException */ private CSSSimpleSelectorNode parseSimpleSelector() throws ParseException, LexerException { CSSSimpleSelectorNode result = (CSSSimpleSelectorNode) this.createNode(CSSParseNodeTypes.SIMPLE_SELECTOR, this.currentLexeme); CSSListNode components = (CSSListNode) this.createNode(CSSParseNodeTypes.LIST, null); components.setListName("components"); //$NON-NLS-1$ CSSTextNode component; boolean needsOne = true; if (this.inSet(typeOrUniversalSelector)) { component = (CSSTextNode) this.createNode(CSSParseNodeTypes.TEXT, this.currentLexeme); components.appendChild(component); // set possible end result.includeLexemeInRange(this.currentLexeme); // advance over element name this.advance(); // clear flag indicating that we need to consume more needsOne = false; } while (this.inSet(attributeSelector) || needsOne) { Lexeme startingLexeme = this.currentLexeme; Lexeme endingLexeme; switch (this.currentLexeme.typeIndex) { case CSSTokenTypes.COLOR: case CSSTokenTypes.HASH: case CSSTokenTypes.CLASS: component = (CSSTextNode) this.createNode(CSSParseNodeTypes.TEXT, this.currentLexeme); components.appendChild(component); // set possible end result.includeLexemeInRange(this.currentLexeme); // advance over class this.advance(); break; case CSSTokenTypes.LBRACKET: String name; String assertion = ""; //$NON-NLS-1$ startingLexeme = this.currentLexeme; // advance over '[' this.advance(); // advance over attribute name this.assertType(CSSTokenTypes.IDENTIFIER, "error.attrib.name"); //$NON-NLS-1$ name = this.currentLexeme.getText(); this.advance(); if (this.inSet(attributeValueOperator)) { // advance over '=', '~=', or '|=' assertion = this.currentLexeme.getText(); this.advance(); this.assertInSet(attributeSet2, "error.attribute.assignment"); //$NON-NLS-1$ assertion += this.currentLexeme.getText(); this.advance(); } // set possible end result.includeLexemeInRange(this.currentLexeme); // advance over ']' endingLexeme = this.currentLexeme; this.assertAndAdvance(CSSTokenTypes.RBRACKET, "error.attrib.close"); //$NON-NLS-1$ component = (CSSTextNode) this.createNode(CSSParseNodeTypes.TEXT, startingLexeme); component.setText("[" + name + assertion + "]"); //$NON-NLS-1$//$NON-NLS-2$ component.includeLexemeInRange(endingLexeme); components.appendChild(component); break; case CSSTokenTypes.COLON: String text = ":"; //$NON-NLS-1$ startingLexeme = this.currentLexeme; endingLexeme = this.currentLexeme; // make compiler happy // advance over ':' this.advance(); switch (this.currentLexeme.typeIndex) { case CSSTokenTypes.IDENTIFIER: // grab identifier and advance text += this.currentLexeme.getText(); // set possible end endingLexeme = this.currentLexeme; result.includeLexemeInRange(this.currentLexeme); // advance over name this.advance(); break; case CSSTokenTypes.FUNCTION: text += this.currentLexeme.getText(); this.advance(); if (this.isType(CSSTokenTypes.IDENTIFIER)) { text += this.currentLexeme.getText(); this.advance(); } // set possible end endingLexeme = this.currentLexeme; result.includeLexemeInRange(this.currentLexeme); // make sure we have ')' this.assertAndAdvance(CSSTokenTypes.RPAREN, "error.pseudo.function.close"); //$NON-NLS-1$ text += ")"; //$NON-NLS-1$ break; default: throwParseError("error.pseudo"); //$NON-NLS-1$ } component = (CSSTextNode) this.createNode(CSSParseNodeTypes.TEXT, startingLexeme); component.setText(text); component.includeLexemeInRange(endingLexeme); components.appendChild(component); break; default: throwParseError("error.simple-selector"); //$NON-NLS-1$ } // clear flag indicating that we need to consume more needsOne = false; } result.appendChild(components); return result; } }