intial source from ttp://www.sf.net/projects/wdte
[phpeclipse.git] / archive / net.sourceforge.phpeclipse.css.core / src / net / sourceforge / phpeclipse / css / core / internal / parser / DefaultCssParser.java
1 /*
2  * Copyright (c) 2003-2004 Christopher Lenz and others.
3  * All rights reserved. This program and the accompanying materials 
4  * are made available under the terms of the Common Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/cpl-v10.html
7  * 
8  * Contributors:
9  *     Christopher Lenz - initial API and implementation
10  * 
11  * $Id: DefaultCssParser.java,v 1.1 2004-09-02 18:07:13 jsurfer Exp $
12  */
13
14 package net.sourceforge.phpeclipse.css.core.internal.parser;
15
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.List;
19
20 import net.sourceforge.phpeclipse.core.model.ISourceReference;
21 import net.sourceforge.phpeclipse.core.model.SourceReference;
22 import net.sourceforge.phpeclipse.css.core.internal.model.AtRule;
23 import net.sourceforge.phpeclipse.css.core.internal.model.Declaration;
24 import net.sourceforge.phpeclipse.css.core.internal.model.StyleRule;
25 import net.sourceforge.phpeclipse.css.core.internal.text.CssTextUtils;
26 import net.sourceforge.phpeclipse.css.core.model.IAtRule;
27 import net.sourceforge.phpeclipse.css.core.model.IDeclaration;
28 import net.sourceforge.phpeclipse.css.core.model.IRule;
29 import net.sourceforge.phpeclipse.css.core.model.IStyleRule;
30 import net.sourceforge.phpeclipse.css.core.model.IStyleSheet;
31 import net.sourceforge.phpeclipse.css.core.parser.ICssParser;
32 import net.sourceforge.phpeclipse.css.core.parser.ICssScanner;
33 import net.sourceforge.phpeclipse.css.core.parser.ICssTokens;
34 import net.sourceforge.phpeclipse.css.core.parser.IProblem;
35 import net.sourceforge.phpeclipse.css.core.parser.IProblemCollector;
36 import net.sourceforge.phpeclipse.css.core.parser.LexicalErrorException;
37 import net.sourceforge.phpeclipse.css.core.parser.SyntaxErrorException;
38
39 import org.eclipse.jface.text.IDocument;
40
41 /**
42  * A simple CSS parser implementation.
43  * 
44  * TODO At-rules can contain child rules as well as declarations. Currently we
45  *      only handle child rules.
46  */
47 public class DefaultCssParser extends AbstractProblemReporter
48         implements ICssParser {
49
50         // Instance Variables ------------------------------------------------------
51
52         /**
53          * The underlying CSS token scanner.
54          */
55         private ICssScanner scanner;
56
57         /**
58          * The token being currently processed.
59          */
60         private int currentToken;
61
62         /**
63          * Whether the parser is currently initializer. Is <code>true</code> as soon
64          * as currentToken has been initialized.
65          */
66         private boolean initialized;
67
68         // ICssParser Implementation -----------------------------------------------
69
70         /**
71          * @see ICssParser#parseRules(IStyleSheet)
72          */
73         public IRule[] parseRules(IStyleSheet styleSheet)
74                 throws LexicalErrorException, SyntaxErrorException {
75                 List rules = new ArrayList();
76                 nextToken();
77                 if (currentToken == ICssTokens.CDO) {
78                         // ignore an opening SGML comment delimiter at the start of the
79                         // style sheet
80                         nextToken();
81                 }
82                 while (currentToken != ICssTokens.EOF) {
83                         if (currentToken == ICssTokens.CDC) {
84                                 // ignore a closing SGML comment delimiter at the end of the
85                                 // style sheet
86                                 IProblem problem = createWarning(
87                                         "illegalPositionForCDC", //$NON-NLS-1$
88                                         scanner.getTokenRegion());
89                                 if (nextToken() != ICssTokens.EOF) {
90                                         reportProblem(problem);
91                                 }
92                         } else if (currentToken == ICssTokens.CDO) {
93                                 reportWarning("illegalPositionForCDO", //$NON-NLS-1$
94                                         scanner.getTokenRegion());
95                                 nextToken();
96                         } else {
97                                 IRule rule = parseRule(styleSheet, null);
98                                 if (rule != null) {
99                                         rules.add(rule);
100                                 } else {
101                                         break;
102                                 }
103                         }
104                 }
105                 return (IRule[]) rules.toArray(new IRule[rules.size()]);
106         }
107
108         /**
109          * Parses a CSS rule, which can be either an at-rule like
110          * <code>@import</code> or a normal style rule.
111          * 
112          * @param parent the parent rule in which the rule to parse is nested, or
113          *        <code>null</code> if the rule to parse is at the top level of the
114          *        style sheet
115          * @return the parsed rule
116          * @throws LexicalErrorException if a lexical error is reported by the
117          *         scanner
118          * @throws SyntaxErrorException if a syntax error has been encountered from
119          *         which recovery is not possible
120          */
121         public IRule parseRule(IStyleSheet styleSheet, IRule parent)
122                 throws LexicalErrorException, SyntaxErrorException {
123                 if (!initialized) {
124                         nextToken();
125                 }
126                 if ((currentToken == -1) || (currentToken == '}')) {
127                         reportError("expectedRule", //$NON-NLS-1$
128                                 scanner.getTokenRegion());
129                         return null;
130                 }
131                 switch (currentToken) {
132                         case ICssTokens.AT: {
133                                 return parseAtRule(styleSheet, parent);
134                         }
135                         default: {
136                                 return parseStyleRule(styleSheet, parent);
137                         }
138                 }
139         }
140
141         /**
142          * Parses a CSS selector. The provided reader is expected to be positioned
143          * at the beginning of the selector (leading whitespace will be ignored).
144          * 
145          * @return the parsed selector
146          * @throws LexicalErrorException if a lexical error is reported by the
147          *         scanner
148          * @throws SyntaxErrorException if a syntax error has been encountered from
149          *         which recovery is not possible
150          */
151         public ISourceReference parseSelector(IRule rule)
152                 throws LexicalErrorException, SyntaxErrorException {
153                 if (!initialized) {
154                         nextToken();
155                 }
156                 return parseAnythingUpto(new int[] { ICssTokens.LBRACE });
157         }
158
159         /**
160          * Parses a declaration. The provided reader is expected to be positioned at
161          * the beginning of the property name (leading whitespace will be ignored).
162          * The return declaration will start with the start of the property, and
163          * will end at the end of the value, excluding the semicolon, if one is 
164          * present.
165          * 
166          * @return the parsed style declaration
167          * @throws LexicalErrorException if a lexical error is reported by the
168          *         scanner
169          * @throws SyntaxErrorException if a syntax error has been encountered from
170          *         which recovery is not possible
171          */
172         public IDeclaration parseDeclaration(IRule rule)
173                 throws LexicalErrorException, SyntaxErrorException {
174                 if (!initialized) {
175                         nextToken();
176                 }
177                 if ((currentToken == -1) || (currentToken == '}')) {
178                         return null;
179                 }
180                 Declaration declaration = new Declaration(document, rule);
181                 MutableRegion declarationRegion = newRegion();
182
183                 // parse the property
184                 if (currentToken == ICssTokens.IDENT) {
185                         SourceReference property = new SourceReference(document);
186                         property.setSourceRegion(scanner.getTokenRegion());
187                         declaration.setProperty(property);
188                         nextToken(declarationRegion);
189                 } else {
190                         reportError("expectedProperty", //$NON-NLS-1$
191                                 scanner.getTokenRegion());
192                 }
193
194                 // expect a colon to delimit the property from the value
195                 if (currentToken == ICssTokens.COLON) {
196                         nextToken(declarationRegion);
197                 } else {
198                         reportError("expectedToken", ":", //$NON-NLS-1$ //$NON-NLS-2$
199                                 scanner.getTokenRegion());
200                 }
201
202                 // parse the declaration value (excluding the priority)
203                 ISourceReference value = parseAnythingUpto(new int[] {
204                         ICssTokens.RBRACE, ICssTokens.SEMICOLON, ICssTokens.EXCLAMATION
205                 });
206                 if (value != null) {
207                         declaration.setValue(value);
208                         declarationRegion.add(value.getSourceRegion());
209                 } else {
210                         reportError("emptyDeclarationValue", //$NON-NLS-1$
211                                 scanner.getTokenRegion());
212                 }
213
214                 // if present, parse the declaration priority
215                 if (currentToken == '!') {
216                         SourceReference priority = new SourceReference(document);
217                         MutableRegion priorityRegion = newRegion();
218                         if (nextToken() == ICssTokens.IDENT) {
219                                 nextToken(priorityRegion);
220                         } else {
221                                 reportError("priorityExpected", //$NON-NLS-1$
222                                         scanner.getTokenRegion());
223                         }
224                         priority.setSourceRegion(priorityRegion);
225                         declaration.setPriority(priority);
226                         declarationRegion.add(priority.getSourceRegion());
227                 }
228
229                 declaration.setSourceRegion(declarationRegion);
230                 return declaration;
231         }
232
233         /**
234          * @see ICssParser#setProblemCollector(IProblemCollector)
235          */
236         public void setProblemCollector(IProblemCollector problemCollector) {
237                 super.setProblemCollector(problemCollector);
238                 if (scanner != null) {
239                         scanner.setProblemCollector(problemCollector);
240                 }
241         }
242
243         /**
244          * @see ICssParser#setSource(IDocument)
245          */
246         public void setSource(IDocument document) {
247                 setDocument(document);
248                 if (scanner != null) {
249                         scanner.setSource(document);
250                 }
251         }
252
253         // Protected Methods -------------------------------------------------------
254
255         protected ICssScanner createScanner(IDocument document) {
256                 ICssScanner retVal = new DefaultCssScanner();
257                 retVal.setSource(document);
258                 retVal.setProblemCollector(problemCollector);
259                 return retVal;
260         }
261
262         /**
263          * Parses a region of the source until one of the specified tokens is 
264          * encountered. In addition, this method keeps track of opened parenthesis
265          * and brackets, and will report an error if they are not closed.
266          * 
267          * @param tokens an array containing the tokens that should delimit the
268          *        range of source to parse
269          * @return the parsed source reference
270          * @throws LexicalErrorException if a lexical error is reported by the
271          *         scanner
272          * @throws SyntaxErrorException if a syntax error has been encountered from
273          *         which recovery is not possible
274          */
275         protected final ISourceReference parseAnythingUpto(int tokens[])
276                 throws LexicalErrorException, SyntaxErrorException {
277                 Arrays.sort(tokens);
278                 SourceReference any = null;
279                 MutableRegion region = newRegion();
280                 while (currentToken != ICssTokens.EOF) {
281                         if (Arrays.binarySearch(tokens, currentToken) >= 0) {
282                                 break;
283                         } else {
284                                 any = new SourceReference(document);
285                                 if ((currentToken == '(') || (currentToken == '[')) {
286                                         char peer = CssTextUtils.getPeerCharacter(
287                                                 (char) currentToken);
288                                         nextToken(region);
289                                         ISourceReference ref =
290                                                 parseAnythingUpto(new int[] { peer } );
291                                         if (currentToken != peer) {
292                                                 reportError("expectedToken", //$NON-NLS-1$
293                                                         String.valueOf(peer), scanner.getTokenRegion());
294                                         }
295                                         if (ref != null) {
296                                                 region.add(ref.getSourceRegion());
297                                         }
298                                 }
299                         }
300                         nextToken(region);
301                 }
302                 if (any != null) {
303                         any.setSourceRegion(region);
304                 }
305                 return any;
306         }
307
308         /**
309          * Parses an at-rule.
310          *      
311          * @param styleSheet the owning style sheet
312          * @param parent the rule in which the rule to parse is nested, or
313          *        <code>null</code> if this rule is at the top level of the style
314          *        sheet
315          * @return the parsed rule
316          * @throws LexicalErrorException if a lexical error is reported by the
317          *         scanner
318          * @throws SyntaxErrorException if a syntax error has been encountered from
319          *         which recovery is not possible
320          */
321         protected final IAtRule parseAtRule(IStyleSheet styleSheet, IRule parent)
322                 throws LexicalErrorException, SyntaxErrorException {
323                 AtRule rule = new AtRule(document, styleSheet, parent);
324                 MutableRegion ruleRegion = newRegion();
325                 if (currentToken == ICssTokens.AT) {
326                         nextToken();
327                 } else {
328                         reportError("expectedToken", "@",  //$NON-NLS-1$//$NON-NLS-2$
329                                 scanner.getTokenRegion());
330                 }
331                 if (currentToken == ICssTokens.IDENT) {
332                         SourceReference name = new SourceReference(document);
333                         name.setSourceRegion(scanner.getTokenRegion());
334                         rule.setName(name);
335                         nextToken(ruleRegion);
336                 } else {
337                         reportError("expectedAtKeyword", //$NON-NLS-1$
338                                 scanner.getTokenRegion());
339                 }
340                 if ((currentToken != ICssTokens.LBRACE)
341                  && (currentToken != ICssTokens.SEMICOLON)
342                  && (currentToken != ICssTokens.EOF)) {
343                         ISourceReference value = parseAnythingUpto(new int[] {
344                                 ICssTokens.LBRACE, ICssTokens.SEMICOLON
345                         });
346                         rule.setValue(value);
347                         ruleRegion.add(value.getSourceRegion());
348                 }
349                 if (currentToken == ICssTokens.LBRACE) {
350                         nextToken(ruleRegion);
351                         while (currentToken != ICssTokens.RBRACE) {
352                                 IRule child = parseRule(styleSheet, rule);
353                                 if (child != null) {
354                                         rule.addChild(child);
355                                         ruleRegion.add(child.getSourceRegion());
356                                 }
357                                 if (currentToken == ICssTokens.EOF) {
358                                         reportError("unexpectedEndOfFile", //$NON-NLS-1$
359                                                 scanner.getTokenRegion());
360                                         break;
361                                 }
362                         }
363                         if (currentToken == ICssTokens.RBRACE) {
364                                 nextToken(ruleRegion);
365                         } else {
366                                 reportError("expectedToken", "}", //$NON-NLS-1$ //$NON-NLS-2$
367                                         scanner.getTokenRegion());
368                         }
369                 } else {
370                         if ((rule.getValue() == null) && (rule.getChildren().length == 0)) {
371                                 reportError("expectedValueOrBlock", //$NON-NLS-1$
372                                         scanner.getTokenRegion());
373                         } else if (currentToken != ICssTokens.SEMICOLON) {
374                                 reportError("expectedToken", ";", //$NON-NLS-1$ //$NON-NLS-2$
375                                         scanner.getTokenRegion());
376                         }
377                         if (currentToken == ICssTokens.SEMICOLON) {
378                                 nextToken(ruleRegion);
379                         }
380                 }
381                 rule.setSourceRegion(ruleRegion);
382                 return rule;
383         }
384
385         /**
386          * Parses a style rule.
387          * 
388          * <p>
389          *  This corresponds to the <code>ruleset</code> production in the
390          *  specification       of the CSS 3 syntax module:
391          * </p>
392          * <pre>
393          *  selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*
394          * </pre>
395          * 
396          * <p>
397          *  <strong>Question</strong>: why is the selector optional in the above
398          *  production? In section 4.8 the specification says that <em>"A rule set
399          *  (also called 'rule') consists of a selector followed by a declaration
400          *  block."</em> So we should be able to assume that there
401          *  <strong>must</strong> be a selector, and otherwise report an error.
402          * </p>
403          * 
404          * @param parent the parent rule in which the style rule is nested, or
405          *        <code>null</code> if the style rule to parse is at the top level
406          *        of the style sheet
407          * @return the parsed style rule
408          * @throws LexicalErrorException if a lexical error is reported by the
409          *         scanner
410          * @throws SyntaxErrorException if a syntax error has been encountered from
411          *         which recovery is not possible
412          */
413         protected final IStyleRule parseStyleRule(IStyleSheet styleSheet,
414                 IRule parent) throws LexicalErrorException, SyntaxErrorException {
415                 StyleRule rule = new StyleRule(document, styleSheet, parent);
416                 MutableRegion ruleRegion = newRegion();
417                 ISourceReference selector = parseSelector(rule);
418                 if (selector != null) {
419                         rule.setSelector(selector);
420                 } else {
421                         reportError("expectedSelectorOrAtKeyword", //$NON-NLS-1$
422                                 scanner.getTokenRegion());
423                 }
424                 if (currentToken == ICssTokens.LBRACE) {
425                         nextToken(ruleRegion);
426                         do {
427                                 IDeclaration declaration = parseDeclaration(rule);
428                                 if (declaration != null) {
429                                         rule.addDeclaration(declaration);
430                                         ruleRegion.add(declaration.getSourceRegion());
431                                 }
432                                 if (currentToken == ';') {
433                                         nextToken(ruleRegion);
434                                 } else {
435                                         break;
436                                 }
437                         } while ((currentToken != ICssTokens.RBRACE)
438                                   && (currentToken != ICssTokens.EOF));
439                         if (currentToken == ICssTokens.RBRACE) {
440                                 nextToken(ruleRegion);
441                         } else {
442                                 reportError("expectedToken", "}", //$NON-NLS-1$ //$NON-NLS-2$
443                                         scanner.getTokenRegion());
444                         }
445                 } else {
446                         reportError("expectedToken", "{", //$NON-NLS-1$ //$NON-NLS-2$
447                                 scanner.getTokenRegion());
448                 }
449                 rule.setSourceRegion(ruleRegion);
450                 return rule;
451         }
452
453         // Private Methods ---------------------------------------------------------
454
455         private int nextToken() throws LexicalErrorException {
456                 return nextToken(null);
457         }
458
459         private int nextToken(MutableRegion region) throws LexicalErrorException {
460                 initialized = true;
461                 if (scanner == null) {
462                         scanner = createScanner(document);
463                 }
464                 if (region != null) {
465                         region.add(scanner.getTokenRegion());
466                 }
467                 currentToken = scanner.getNextToken();
468                 return currentToken;
469         }
470
471         private MutableRegion newRegion() {
472                 return new MutableRegion(scanner.getTokenRegion());
473         }
474 }