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