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