/* * Copyright (c) 2003-2004 Christopher Lenz and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/cpl-v10.html * * Contributors: * Christopher Lenz - initial API and implementation * * $Id: DefaultCssParser.java,v 1.1 2004-09-02 18:07:13 jsurfer Exp $ */ package net.sourceforge.phpeclipse.css.core.internal.parser; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import net.sourceforge.phpeclipse.core.model.ISourceReference; import net.sourceforge.phpeclipse.core.model.SourceReference; import net.sourceforge.phpeclipse.css.core.internal.model.AtRule; import net.sourceforge.phpeclipse.css.core.internal.model.Declaration; import net.sourceforge.phpeclipse.css.core.internal.model.StyleRule; import net.sourceforge.phpeclipse.css.core.internal.text.CssTextUtils; import net.sourceforge.phpeclipse.css.core.model.IAtRule; import net.sourceforge.phpeclipse.css.core.model.IDeclaration; import net.sourceforge.phpeclipse.css.core.model.IRule; import net.sourceforge.phpeclipse.css.core.model.IStyleRule; import net.sourceforge.phpeclipse.css.core.model.IStyleSheet; import net.sourceforge.phpeclipse.css.core.parser.ICssParser; import net.sourceforge.phpeclipse.css.core.parser.ICssScanner; import net.sourceforge.phpeclipse.css.core.parser.ICssTokens; import net.sourceforge.phpeclipse.css.core.parser.IProblem; import net.sourceforge.phpeclipse.css.core.parser.IProblemCollector; import net.sourceforge.phpeclipse.css.core.parser.LexicalErrorException; import net.sourceforge.phpeclipse.css.core.parser.SyntaxErrorException; import org.eclipse.jface.text.IDocument; /** * A simple CSS parser implementation. * * TODO At-rules can contain child rules as well as declarations. Currently we * only handle child rules. */ public class DefaultCssParser extends AbstractProblemReporter implements ICssParser { // Instance Variables ------------------------------------------------------ /** * The underlying CSS token scanner. */ private ICssScanner scanner; /** * The token being currently processed. */ private int currentToken; /** * Whether the parser is currently initializer. Is true as soon * as currentToken has been initialized. */ private boolean initialized; // ICssParser Implementation ----------------------------------------------- /** * @see ICssParser#parseRules(IStyleSheet) */ public IRule[] parseRules(IStyleSheet styleSheet) throws LexicalErrorException, SyntaxErrorException { List rules = new ArrayList(); nextToken(); if (currentToken == ICssTokens.CDO) { // ignore an opening SGML comment delimiter at the start of the // style sheet nextToken(); } while (currentToken != ICssTokens.EOF) { if (currentToken == ICssTokens.CDC) { // ignore a closing SGML comment delimiter at the end of the // style sheet IProblem problem = createWarning( "illegalPositionForCDC", //$NON-NLS-1$ scanner.getTokenRegion()); if (nextToken() != ICssTokens.EOF) { reportProblem(problem); } } else if (currentToken == ICssTokens.CDO) { reportWarning("illegalPositionForCDO", //$NON-NLS-1$ scanner.getTokenRegion()); nextToken(); } else { IRule rule = parseRule(styleSheet, null); if (rule != null) { rules.add(rule); } else { break; } } } return (IRule[]) rules.toArray(new IRule[rules.size()]); } /** * Parses a CSS rule, which can be either an at-rule like * @import or a normal style rule. * * @param parent the parent rule in which the rule to parse is nested, or * null if the rule to parse is at the top level of the * style sheet * @return the parsed rule * @throws LexicalErrorException if a lexical error is reported by the * scanner * @throws SyntaxErrorException if a syntax error has been encountered from * which recovery is not possible */ public IRule parseRule(IStyleSheet styleSheet, IRule parent) throws LexicalErrorException, SyntaxErrorException { if (!initialized) { nextToken(); } if ((currentToken == -1) || (currentToken == '}')) { reportError("expectedRule", //$NON-NLS-1$ scanner.getTokenRegion()); return null; } switch (currentToken) { case ICssTokens.AT: { return parseAtRule(styleSheet, parent); } default: { return parseStyleRule(styleSheet, parent); } } } /** * Parses a CSS selector. The provided reader is expected to be positioned * at the beginning of the selector (leading whitespace will be ignored). * * @return the parsed selector * @throws LexicalErrorException if a lexical error is reported by the * scanner * @throws SyntaxErrorException if a syntax error has been encountered from * which recovery is not possible */ public ISourceReference parseSelector(IRule rule) throws LexicalErrorException, SyntaxErrorException { if (!initialized) { nextToken(); } return parseAnythingUpto(new int[] { ICssTokens.LBRACE }); } /** * Parses a declaration. The provided reader is expected to be positioned at * the beginning of the property name (leading whitespace will be ignored). * The return declaration will start with the start of the property, and * will end at the end of the value, excluding the semicolon, if one is * present. * * @return the parsed style declaration * @throws LexicalErrorException if a lexical error is reported by the * scanner * @throws SyntaxErrorException if a syntax error has been encountered from * which recovery is not possible */ public IDeclaration parseDeclaration(IRule rule) throws LexicalErrorException, SyntaxErrorException { if (!initialized) { nextToken(); } if ((currentToken == -1) || (currentToken == '}')) { return null; } Declaration declaration = new Declaration(document, rule); MutableRegion declarationRegion = newRegion(); // parse the property if (currentToken == ICssTokens.IDENT) { SourceReference property = new SourceReference(document); property.setSourceRegion(scanner.getTokenRegion()); declaration.setProperty(property); nextToken(declarationRegion); } else { reportError("expectedProperty", //$NON-NLS-1$ scanner.getTokenRegion()); } // expect a colon to delimit the property from the value if (currentToken == ICssTokens.COLON) { nextToken(declarationRegion); } else { reportError("expectedToken", ":", //$NON-NLS-1$ //$NON-NLS-2$ scanner.getTokenRegion()); } // parse the declaration value (excluding the priority) ISourceReference value = parseAnythingUpto(new int[] { ICssTokens.RBRACE, ICssTokens.SEMICOLON, ICssTokens.EXCLAMATION }); if (value != null) { declaration.setValue(value); declarationRegion.add(value.getSourceRegion()); } else { reportError("emptyDeclarationValue", //$NON-NLS-1$ scanner.getTokenRegion()); } // if present, parse the declaration priority if (currentToken == '!') { SourceReference priority = new SourceReference(document); MutableRegion priorityRegion = newRegion(); if (nextToken() == ICssTokens.IDENT) { nextToken(priorityRegion); } else { reportError("priorityExpected", //$NON-NLS-1$ scanner.getTokenRegion()); } priority.setSourceRegion(priorityRegion); declaration.setPriority(priority); declarationRegion.add(priority.getSourceRegion()); } declaration.setSourceRegion(declarationRegion); return declaration; } /** * @see ICssParser#setProblemCollector(IProblemCollector) */ public void setProblemCollector(IProblemCollector problemCollector) { super.setProblemCollector(problemCollector); if (scanner != null) { scanner.setProblemCollector(problemCollector); } } /** * @see ICssParser#setSource(IDocument) */ public void setSource(IDocument document) { setDocument(document); if (scanner != null) { scanner.setSource(document); } } // Protected Methods ------------------------------------------------------- protected ICssScanner createScanner(IDocument document) { ICssScanner retVal = new DefaultCssScanner(); retVal.setSource(document); retVal.setProblemCollector(problemCollector); return retVal; } /** * Parses a region of the source until one of the specified tokens is * encountered. In addition, this method keeps track of opened parenthesis * and brackets, and will report an error if they are not closed. * * @param tokens an array containing the tokens that should delimit the * range of source to parse * @return the parsed source reference * @throws LexicalErrorException if a lexical error is reported by the * scanner * @throws SyntaxErrorException if a syntax error has been encountered from * which recovery is not possible */ protected final ISourceReference parseAnythingUpto(int tokens[]) throws LexicalErrorException, SyntaxErrorException { Arrays.sort(tokens); SourceReference any = null; MutableRegion region = newRegion(); while (currentToken != ICssTokens.EOF) { if (Arrays.binarySearch(tokens, currentToken) >= 0) { break; } else { any = new SourceReference(document); if ((currentToken == '(') || (currentToken == '[')) { char peer = CssTextUtils.getPeerCharacter( (char) currentToken); nextToken(region); ISourceReference ref = parseAnythingUpto(new int[] { peer } ); if (currentToken != peer) { reportError("expectedToken", //$NON-NLS-1$ String.valueOf(peer), scanner.getTokenRegion()); } if (ref != null) { region.add(ref.getSourceRegion()); } } } nextToken(region); } if (any != null) { any.setSourceRegion(region); } return any; } /** * Parses an at-rule. * * @param styleSheet the owning style sheet * @param parent the rule in which the rule to parse is nested, or * null if this rule is at the top level of the style * sheet * @return the parsed rule * @throws LexicalErrorException if a lexical error is reported by the * scanner * @throws SyntaxErrorException if a syntax error has been encountered from * which recovery is not possible */ protected final IAtRule parseAtRule(IStyleSheet styleSheet, IRule parent) throws LexicalErrorException, SyntaxErrorException { AtRule rule = new AtRule(document, styleSheet, parent); MutableRegion ruleRegion = newRegion(); if (currentToken == ICssTokens.AT) { nextToken(); } else { reportError("expectedToken", "@", //$NON-NLS-1$//$NON-NLS-2$ scanner.getTokenRegion()); } if (currentToken == ICssTokens.IDENT) { SourceReference name = new SourceReference(document); name.setSourceRegion(scanner.getTokenRegion()); rule.setName(name); nextToken(ruleRegion); } else { reportError("expectedAtKeyword", //$NON-NLS-1$ scanner.getTokenRegion()); } if ((currentToken != ICssTokens.LBRACE) && (currentToken != ICssTokens.SEMICOLON) && (currentToken != ICssTokens.EOF)) { ISourceReference value = parseAnythingUpto(new int[] { ICssTokens.LBRACE, ICssTokens.SEMICOLON }); rule.setValue(value); ruleRegion.add(value.getSourceRegion()); } if (currentToken == ICssTokens.LBRACE) { nextToken(ruleRegion); while (currentToken != ICssTokens.RBRACE) { IRule child = parseRule(styleSheet, rule); if (child != null) { rule.addChild(child); ruleRegion.add(child.getSourceRegion()); } if (currentToken == ICssTokens.EOF) { reportError("unexpectedEndOfFile", //$NON-NLS-1$ scanner.getTokenRegion()); break; } } if (currentToken == ICssTokens.RBRACE) { nextToken(ruleRegion); } else { reportError("expectedToken", "}", //$NON-NLS-1$ //$NON-NLS-2$ scanner.getTokenRegion()); } } else { if ((rule.getValue() == null) && (rule.getChildren().length == 0)) { reportError("expectedValueOrBlock", //$NON-NLS-1$ scanner.getTokenRegion()); } else if (currentToken != ICssTokens.SEMICOLON) { reportError("expectedToken", ";", //$NON-NLS-1$ //$NON-NLS-2$ scanner.getTokenRegion()); } if (currentToken == ICssTokens.SEMICOLON) { nextToken(ruleRegion); } } rule.setSourceRegion(ruleRegion); return rule; } /** * Parses a style rule. * *

* This corresponds to the ruleset production in the * specification of the CSS 3 syntax module: *

*
	 *  selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*
	 * 
* *

* Question: why is the selector optional in the above * production? In section 4.8 the specification says that "A rule set * (also called 'rule') consists of a selector followed by a declaration * block." So we should be able to assume that there * must be a selector, and otherwise report an error. *

* * @param parent the parent rule in which the style rule is nested, or * null if the style rule to parse is at the top level * of the style sheet * @return the parsed style rule * @throws LexicalErrorException if a lexical error is reported by the * scanner * @throws SyntaxErrorException if a syntax error has been encountered from * which recovery is not possible */ protected final IStyleRule parseStyleRule(IStyleSheet styleSheet, IRule parent) throws LexicalErrorException, SyntaxErrorException { StyleRule rule = new StyleRule(document, styleSheet, parent); MutableRegion ruleRegion = newRegion(); ISourceReference selector = parseSelector(rule); if (selector != null) { rule.setSelector(selector); } else { reportError("expectedSelectorOrAtKeyword", //$NON-NLS-1$ scanner.getTokenRegion()); } if (currentToken == ICssTokens.LBRACE) { nextToken(ruleRegion); do { IDeclaration declaration = parseDeclaration(rule); if (declaration != null) { rule.addDeclaration(declaration); ruleRegion.add(declaration.getSourceRegion()); } if (currentToken == ';') { nextToken(ruleRegion); } else { break; } } while ((currentToken != ICssTokens.RBRACE) && (currentToken != ICssTokens.EOF)); if (currentToken == ICssTokens.RBRACE) { nextToken(ruleRegion); } else { reportError("expectedToken", "}", //$NON-NLS-1$ //$NON-NLS-2$ scanner.getTokenRegion()); } } else { reportError("expectedToken", "{", //$NON-NLS-1$ //$NON-NLS-2$ scanner.getTokenRegion()); } rule.setSourceRegion(ruleRegion); return rule; } // Private Methods --------------------------------------------------------- private int nextToken() throws LexicalErrorException { return nextToken(null); } private int nextToken(MutableRegion region) throws LexicalErrorException { initialized = true; if (scanner == null) { scanner = createScanner(document); } if (region != null) { region.add(scanner.getTokenRegion()); } currentToken = scanner.getNextToken(); return currentToken; } private MutableRegion newRegion() { return new MutableRegion(scanner.getTokenRegion()); } }