OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * A simple recursive descent parser for CSS. | |
7 */ | |
8 class Parser { | |
9 Tokenizer tokenizer; | |
10 | |
11 final lang.SourceFile source; | |
12 | |
13 // TODO(jimhug): 1. Try to kill initializers, if fails, clean this up. | |
jimhug
2011/11/10 17:58:56
Field and comment shouldn't be needed for css.
terry
2011/11/16 14:00:22
Done.
| |
14 bool _inInitializers; | |
15 | |
16 lang.Token _previousToken; | |
17 lang.Token _peekToken; | |
18 | |
19 Parser(this.source, [int startOffset = 0]) { | |
20 tokenizer = new Tokenizer(source, true, startOffset); | |
21 _peekToken = tokenizer.next(); | |
22 _previousToken = null; | |
23 _inInitializers = false; | |
24 } | |
25 | |
26 /** Generate an error if [source] has not been completely consumed. */ | |
27 void checkEndOfFile() { | |
28 _eat(TokenKind.END_OF_FILE); | |
29 } | |
30 | |
31 /** Guard to break out of parser when an unexpected end of file is found. */ | |
32 // TODO(jimhug): Failure to call this method can lead to inifinite parser | |
33 // loops. Consider embracing exceptions for more errors to reduce | |
34 // the danger here. | |
35 bool isPrematureEndOfFile() { | |
36 if (_maybeEat(TokenKind.END_OF_FILE)) { | |
37 _error('unexpected end of file', _peekToken.span); | |
38 return true; | |
39 } else { | |
40 return false; | |
41 } | |
42 } | |
43 | |
44 /////////////////////////////////////////////////////////////////// | |
45 // Basic support methods | |
46 /////////////////////////////////////////////////////////////////// | |
47 int _peek() { | |
48 return _peekToken.kind; | |
49 } | |
50 | |
51 lang.Token _next() { | |
52 _previousToken = _peekToken; | |
53 _peekToken = tokenizer.next(); | |
54 return _previousToken; | |
55 } | |
56 | |
57 bool _peekKind(int kind) { | |
58 return _peekToken.kind == kind; | |
59 } | |
60 | |
61 /* Is the next token a legal identifier? This includes pseudo-keywords. */ | |
62 bool _peekIdentifier() { | |
63 return TokenKind.isIdentifier(_peekToken.kind); | |
64 } | |
65 | |
66 bool _maybeEat(int kind) { | |
67 if (_peekToken.kind == kind) { | |
68 _previousToken = _peekToken; | |
69 _peekToken = tokenizer.next(); | |
70 return true; | |
71 } else { | |
72 return false; | |
73 } | |
74 } | |
75 | |
76 void _eat(int kind) { | |
77 if (!_maybeEat(kind)) { | |
78 _errorExpected(TokenKind.kindToString(kind)); | |
79 } | |
80 } | |
81 | |
82 void _eatSemicolon() { | |
83 _eat(TokenKind.SEMICOLON); | |
84 } | |
85 | |
86 void _errorExpected(String expected) { | |
87 var tok = _next(); | |
88 var message; | |
89 try { | |
90 message = 'expected $expected, but found $tok'; | |
91 } catch (var e) { | |
92 message = 'parsing error expected $expected'; | |
93 } | |
94 _error(message, tok.span); | |
95 } | |
96 | |
97 void _error(String message, [lang.SourceSpan location=null]) { | |
98 if (location === null) { | |
99 location = _peekToken.span; | |
100 } | |
101 | |
102 lang.world.fatal(message, location); // syntax errors are fatal for now | |
103 } | |
104 | |
105 lang.SourceSpan _makeSpan(int start) { | |
106 return new lang.SourceSpan(source, start, _previousToken.end); | |
107 } | |
108 | |
109 /////////////////////////////////////////////////////////////////// | |
110 // Top level productions | |
111 /////////////////////////////////////////////////////////////////// | |
112 | |
113 List<SelectorGroup> expression() { | |
114 List<SelectorGroup> groups = []; | |
115 while (!_maybeEat(TokenKind.END_OF_FILE)) { | |
116 do { | |
117 int start = _peekToken.start; | |
118 groups.add(new SelectorGroup(selectorExpression(), _makeSpan(start))); | |
119 } while (_maybeEat(TokenKind.COMMA)); | |
120 } | |
121 | |
122 return groups; | |
123 } | |
124 | |
125 // Templates are @{selectors} single line nothing else. | |
126 SelectorGroup template() { | |
127 SelectorGroup selectorGroup; | |
128 if (!_maybeEat(TokenKind.END_OF_FILE)) { | |
129 selectorGroup = templateExpression(); | |
130 if (!_maybeEat(TokenKind.END_OF_FILE)) { | |
131 // TODO(terry): Error should be done. | |
132 } | |
133 } | |
134 | |
135 return selectorGroup; | |
136 } | |
137 | |
138 /* | |
139 * Expect @{css_expression} | |
140 */ | |
141 templateExpression() { | |
142 SelectorGroup selectorGroup = null; | |
143 int start = _peekToken.start; | |
144 | |
145 if (_peek() == TokenKind.AT) { | |
146 _eat(TokenKind.AT); | |
147 if (_peek() == TokenKind.LBRACE) { | |
148 _eat(TokenKind.LBRACE); | |
149 List<SimpleSelector> selectors= selectorExpression(); | |
150 SelectorGroup exprResult = new SelectorGroup(selectors, | |
151 _makeSpan(start)); | |
152 | |
153 if (_peek() == TokenKind.RBRACE) { | |
154 _eat(TokenKind.RBRACE); | |
155 selectorGroup = exprResult; | |
156 } | |
157 } | |
158 } | |
159 | |
160 if (selectorGroup == null) { | |
161 _errorExpected('css template @{css selector}'); | |
162 } else { | |
163 return selectorGroup; | |
164 } | |
165 } | |
166 | |
167 int classNameCheck(var selector, int matches) { | |
168 if (selector.isCombinatorNone()) { | |
169 if (matches < 0) { | |
170 String tooMany = selector.toString(); | |
171 throw new CssSelectorException( | |
172 'Can not mix Id selector with class selector(s). Id ' + | |
173 'selector must be singleton too many starting at $tooMany', | |
174 selector.span); | |
175 } | |
176 | |
177 return ++matches; | |
178 } else { | |
179 String error = selector.toString(); | |
180 throw new CssSelectorException( | |
181 'Selectors can not have combinators (>, +, or ~) before $error', | |
182 selector.span); | |
183 } | |
184 } | |
185 | |
186 int elementIdCheck(var selector, int matches) { | |
187 if (selector.isCombinatorNone()) { | |
188 if (matches != 0) { | |
189 String tooMany = selector.toString(); | |
190 throw new CssSelectorException( | |
191 'Use of Id selector must be singleton starting at $tooMany', | |
192 selector.span); | |
193 } | |
194 return --matches; | |
195 } else { | |
196 String error = selector.toString(); | |
197 throw new CssSelectorException( | |
198 'Selectors can not have combinators (>, +, or ~) before $error', | |
199 selector.span); | |
200 } | |
201 } | |
202 | |
203 // Validate the @{css expression} only .class and #elementId are valid inside | |
204 // of @{...}. | |
205 validateTemplate(List<lang.Node> selectors, CssWorld cssWorld) { | |
206 var errorSelector; | |
207 bool found = false; | |
208 | |
209 int matches = 0; // < 0 IdSelectors, > 0 ClassSelector | |
210 for (selector in selectors) { | |
211 found = false; | |
212 if (selector is ClassSelector) { | |
213 // Any class name starting with an underscore is a private class name | |
214 // that doesn't have to match the world of known classes. | |
215 if (!selector.name.startsWith('_')) { | |
216 for (className in cssWorld.classes) { | |
217 if (selector.name == className) { | |
218 matches = classNameCheck(selector, matches); | |
219 found = true; | |
220 break; | |
221 } | |
222 } | |
223 } else { | |
224 // Don't check any class name that is prefixed with an underscore. | |
225 // However, signal as found and bump up matches; it's a valid class | |
226 // name. | |
227 matches = classNameCheck(selector, matches); | |
228 found = true; | |
229 } | |
230 } else if (selector is IdSelector) { | |
231 // Any element id starting with an underscore is a private element id | |
232 // that doesn't have to match the world of known elemtn ids. | |
233 if (!selector.name.startsWith('_')) { | |
234 for (id in cssWorld.ids) { | |
235 if (selector.name == id) { | |
236 matches = elementIdCheck(selector, matches); | |
237 found = true; | |
238 break; | |
239 } | |
240 } | |
241 } else { | |
242 // Don't check any element ID that is prefixed with an underscore. | |
243 // However, signal as found and bump up matches; it's a valid element | |
244 // ID. | |
245 matches = elementIdCheck(selector, matches); | |
246 found = true; | |
247 } | |
248 } else { | |
249 String badSelector = selector.toString(); | |
250 throw new CssSelectorException( | |
251 'Invalid selector $badSelector', selector.span); | |
252 } | |
253 | |
254 if (!found) { | |
255 errorSelector = selector; // Flag the problem selector. | |
256 break; | |
257 } | |
258 } | |
259 | |
260 assert(matches >= 0 || matches == -1); | |
261 | |
262 if (!found && errorSelector != null) { | |
263 String unknownName = errorSelector.toString(); | |
264 throw new CssSelectorException('Unknown selector name $unknownName', | |
265 errorSelector.span); | |
266 } | |
267 } | |
268 | |
269 /////////////////////////////////////////////////////////////////// | |
270 // Productions | |
271 /////////////////////////////////////////////////////////////////// | |
272 | |
273 selectorExpression() { | |
274 return simpleSelectorSequence(); | |
275 } | |
276 | |
277 simpleSelectorSequence() { | |
278 List<SimpleSelector> simpleSelectors = []; | |
279 while (true) { | |
280 var selectorItem = combinator(); | |
281 if (selectorItem != null) { | |
282 simpleSelectors.add(selectorItem); | |
283 } else { | |
284 break; | |
285 } | |
286 } | |
287 | |
288 return simpleSelectors; | |
289 } | |
290 | |
291 combinator() { | |
292 int combinatorType = TokenKind.COMBINATOR_NONE; | |
293 switch (_peek()) { | |
294 case TokenKind.COMBINATOR_PLUS: | |
295 _eat(TokenKind.COMBINATOR_PLUS); | |
296 combinatorType = TokenKind.COMBINATOR_PLUS; | |
297 break; | |
298 case TokenKind.COMBINATOR_GREATER: | |
299 _eat(TokenKind.COMBINATOR_GREATER); | |
300 combinatorType = TokenKind.COMBINATOR_GREATER; | |
301 break; | |
302 case TokenKind.COMBINATOR_TILDE: | |
303 _eat(TokenKind.COMBINATOR_TILDE); | |
304 combinatorType = TokenKind.COMBINATOR_TILDE; | |
305 break; | |
306 } | |
307 | |
308 return namespaceElementUniversal(combinatorType); | |
309 } | |
310 | |
311 /** | |
312 * Simple selector grammar: | |
313 * simple_selector_sequence | |
314 * : [ type_selector | universal ] | |
315 * [ HASH | class | attrib | pseudo | negation ]* | |
316 * | [ HASH | class | attrib | pseudo | negation ]+ | |
317 * type_selector | |
318 * : [ namespace_prefix ]? element_name | |
319 * namespace_prefix | |
320 * : [ IDENT | '*' ]? '|' | |
321 * element_name | |
322 * : IDENT | |
323 * universal | |
324 * : [ namespace_prefix ]? '*' | |
325 * class | |
326 * : '.' IDENT | |
327 */ | |
328 namespaceElementUniversal(int combinator) { | |
329 String first; | |
330 switch (_peek()) { | |
331 case TokenKind.ASTERISK: | |
332 first = '*'; // Mark as universal namespace. | |
333 _next(); | |
334 break; | |
335 case TokenKind.IDENTIFIER: | |
336 int startIdent = _peekToken.start; | |
337 first = identifier(); | |
338 break; | |
339 } | |
340 | |
341 if (first != null) { | |
342 // Could be a namespace? | |
343 var isNamespace = _maybeEat(TokenKind.NAMESPACE); | |
344 if (isNamespace) { | |
345 var element; | |
346 switch (_peek()) { | |
347 case TokenKind.ASTERISK: | |
348 element = '*'; // Mark as universal. | |
349 _next(); | |
350 break; | |
351 case TokenKind.IDENTIFIER: | |
352 int startIdent = _peekToken.start; | |
353 element = identifier(); | |
354 break; | |
355 default: | |
356 _error('expected element name or universal, but found $_peekToken', | |
357 _peekToken.span); | |
358 } | |
359 | |
360 return new NamespaceSelector(first, new ElementSelector(element), | |
361 combinator); | |
362 } else { | |
363 return new ElementSelector(first, combinator); | |
364 } | |
365 } else { | |
366 // Check for HASH | class | attrib | pseudo | negation | |
367 return selectorNameType(combinator); | |
368 } | |
369 } | |
370 | |
371 selectorNameType(int combinator) { | |
372 // Check for HASH | class | attrib | pseudo | negation | |
373 switch (_peek()) { | |
374 case TokenKind.HASH: | |
375 int startHash = _peekToken.start; | |
376 _eat(TokenKind.HASH); | |
377 var name = identifier(); | |
378 return new IdSelector(name, combinator); | |
379 case TokenKind.DOT: | |
380 _eat(TokenKind.DOT); | |
381 var name = identifier(); | |
382 return new ClassSelector(name, combinator); | |
383 case TokenKind.PSEUDO: | |
384 // :pseudo-class ::pseudo-element | |
385 _eat(TokenKind.PSEUDO); | |
386 bool pseudoClass = _peek() != TokenKind.PSEUDO; | |
387 var name = identifier(); | |
388 // TODO(terry): Need to handle specific pseudo class/element name and | |
389 // backward compatible names that are : as well as ::. | |
390 return pseudoClass ? | |
391 new PseudoClassSelector(name, combinator) : | |
392 new PseudoElementSelector(name, combinator); | |
393 | |
394 // TODO(terry): attrib, negation. | |
395 } | |
396 } | |
397 | |
398 identifier() { | |
399 var tok = _next(); | |
400 if (!TokenKind.isIdentifier(tok.kind)) { | |
401 try { | |
402 _error('expected identifier, but found $tok', tok.span); | |
403 } catch (var e) { | |
404 _error('expected identifier', tok.span); | |
405 } | |
406 } | |
407 | |
408 return new Identifier(tok.text, _makeSpan(tok.start)); | |
409 } | |
410 } | |
OLD | NEW |