OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, 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 library engine.html; | |
6 | |
7 import 'dart:collection'; | |
8 | |
9 import 'ast.dart'; | |
10 import 'element.dart'; | |
11 import 'engine.dart' show AnalysisOptions, AnalysisEngine; | |
12 import 'error.dart' show AnalysisErrorListener; | |
13 import 'java_core.dart'; | |
14 import 'java_engine.dart'; | |
15 import 'parser.dart' show Parser; | |
16 import 'scanner.dart' as sc show Scanner, SubSequenceReader, Token; | |
17 import 'source.dart'; | |
18 | |
19 /** | |
20 * The abstract class `AbstractScanner` implements a scanner for HTML code. Subc
lasses are | |
21 * required to implement the interface used to access the characters being scann
ed. | |
22 */ | |
23 abstract class AbstractScanner { | |
24 static List<String> _NO_PASS_THROUGH_ELEMENTS = <String>[]; | |
25 | |
26 /** | |
27 * The source being scanned. | |
28 */ | |
29 final Source source; | |
30 | |
31 /** | |
32 * The token pointing to the head of the linked list of tokens. | |
33 */ | |
34 Token _tokens; | |
35 | |
36 /** | |
37 * The last token that was scanned. | |
38 */ | |
39 Token _tail; | |
40 | |
41 /** | |
42 * A list containing the offsets of the first character of each line in the so
urce code. | |
43 */ | |
44 List<int> _lineStarts = new List<int>(); | |
45 | |
46 /** | |
47 * An array of element tags for which the content between tags should be consi
der a single token. | |
48 */ | |
49 List<String> _passThroughElements = _NO_PASS_THROUGH_ELEMENTS; | |
50 | |
51 /** | |
52 * Initialize a newly created scanner. | |
53 * | |
54 * @param source the source being scanned | |
55 */ | |
56 AbstractScanner(this.source) { | |
57 _tokens = new Token.con1(TokenType.EOF, -1); | |
58 _tokens.setNext(_tokens); | |
59 _tail = _tokens; | |
60 recordStartOfLine(); | |
61 } | |
62 | |
63 /** | |
64 * Return an array containing the offsets of the first character of each line
in the source code. | |
65 * | |
66 * @return an array containing the offsets of the first character of each line
in the source code | |
67 */ | |
68 List<int> get lineStarts => _lineStarts; | |
69 | |
70 /** | |
71 * Return the current offset relative to the beginning of the file. Return the
initial offset if | |
72 * the scanner has not yet scanned the source code, and one (1) past the end o
f the source code if | |
73 * the source code has been scanned. | |
74 * | |
75 * @return the current offset of the scanner in the source | |
76 */ | |
77 int get offset; | |
78 | |
79 /** | |
80 * Set array of element tags for which the content between tags should be cons
ider a single token. | |
81 */ | |
82 void set passThroughElements(List<String> passThroughElements) { | |
83 this._passThroughElements = passThroughElements != null | |
84 ? passThroughElements | |
85 : _NO_PASS_THROUGH_ELEMENTS; | |
86 } | |
87 | |
88 /** | |
89 * Advance the current position and return the character at the new current po
sition. | |
90 * | |
91 * @return the character at the new current position | |
92 */ | |
93 int advance(); | |
94 | |
95 /** | |
96 * Return the substring of the source code between the start offset and the mo
dified current | |
97 * position. The current position is modified by adding the end delta. | |
98 * | |
99 * @param start the offset to the beginning of the string, relative to the sta
rt of the file | |
100 * @param endDelta the number of character after the current location to be in
cluded in the | |
101 * string, or the number of characters before the current location to
be excluded if the | |
102 * offset is negative | |
103 * @return the specified substring of the source code | |
104 */ | |
105 String getString(int start, int endDelta); | |
106 | |
107 /** | |
108 * Return the character at the current position without changing the current p
osition. | |
109 * | |
110 * @return the character at the current position | |
111 */ | |
112 int peek(); | |
113 | |
114 /** | |
115 * Record the fact that we are at the beginning of a new line in the source. | |
116 */ | |
117 void recordStartOfLine() { | |
118 _lineStarts.add(offset); | |
119 } | |
120 | |
121 /** | |
122 * Scan the source code to produce a list of tokens representing the source. | |
123 * | |
124 * @return the first token in the list of tokens that were produced | |
125 */ | |
126 Token tokenize() { | |
127 _scan(); | |
128 _appendEofToken(); | |
129 return _firstToken(); | |
130 } | |
131 | |
132 void _appendEofToken() { | |
133 Token eofToken = new Token.con1(TokenType.EOF, offset); | |
134 // The EOF token points to itself so that there is always infinite | |
135 // look-ahead. | |
136 eofToken.setNext(eofToken); | |
137 _tail = _tail.setNext(eofToken); | |
138 } | |
139 | |
140 Token _emit(Token token) { | |
141 _tail.setNext(token); | |
142 _tail = token; | |
143 return token; | |
144 } | |
145 | |
146 Token _emitWithOffset(TokenType type, int start) => | |
147 _emit(new Token.con1(type, start)); | |
148 | |
149 Token _emitWithOffsetAndLength(TokenType type, int start, int count) => | |
150 _emit(new Token.con2(type, start, getString(start, count))); | |
151 | |
152 Token _firstToken() => _tokens.next; | |
153 | |
154 int _recordStartOfLineAndAdvance(int c) { | |
155 if (c == 0xD) { | |
156 c = advance(); | |
157 if (c == 0xA) { | |
158 c = advance(); | |
159 } | |
160 recordStartOfLine(); | |
161 } else if (c == 0xA) { | |
162 c = advance(); | |
163 recordStartOfLine(); | |
164 } else { | |
165 c = advance(); | |
166 } | |
167 return c; | |
168 } | |
169 | |
170 void _scan() { | |
171 bool inBrackets = false; | |
172 String endPassThrough = null; | |
173 int c = advance(); | |
174 while (c >= 0) { | |
175 int start = offset; | |
176 if (c == 0x3C) { | |
177 c = advance(); | |
178 if (c == 0x21) { | |
179 c = advance(); | |
180 if (c == 0x2D && peek() == 0x2D) { | |
181 // handle a comment | |
182 c = advance(); | |
183 int dashCount = 1; | |
184 while (c >= 0) { | |
185 if (c == 0x2D) { | |
186 dashCount++; | |
187 } else if (c == 0x3E && dashCount >= 2) { | |
188 c = advance(); | |
189 break; | |
190 } else { | |
191 dashCount = 0; | |
192 } | |
193 c = _recordStartOfLineAndAdvance(c); | |
194 } | |
195 _emitWithOffsetAndLength(TokenType.COMMENT, start, -1); | |
196 // Capture <!--> and <!---> as tokens but report an error | |
197 if (_tail.length < 7) { | |
198 // TODO (danrubel): Report invalid HTML comment | |
199 } | |
200 } else { | |
201 // handle a declaration | |
202 while (c >= 0) { | |
203 if (c == 0x3E) { | |
204 c = advance(); | |
205 break; | |
206 } | |
207 c = _recordStartOfLineAndAdvance(c); | |
208 } | |
209 _emitWithOffsetAndLength(TokenType.DECLARATION, start, -1); | |
210 if (!StringUtilities.endsWithChar(_tail.lexeme, 0x3E)) { | |
211 // TODO (danrubel): Report missing '>' in directive | |
212 } | |
213 } | |
214 } else if (c == 0x3F) { | |
215 // handle a directive | |
216 while (c >= 0) { | |
217 if (c == 0x3F) { | |
218 c = advance(); | |
219 if (c == 0x3E) { | |
220 c = advance(); | |
221 break; | |
222 } | |
223 } else { | |
224 c = _recordStartOfLineAndAdvance(c); | |
225 } | |
226 } | |
227 _emitWithOffsetAndLength(TokenType.DIRECTIVE, start, -1); | |
228 if (_tail.length < 4) { | |
229 // TODO (danrubel): Report invalid directive | |
230 } | |
231 } else if (c == 0x2F) { | |
232 _emitWithOffset(TokenType.LT_SLASH, start); | |
233 inBrackets = true; | |
234 c = advance(); | |
235 } else { | |
236 inBrackets = true; | |
237 _emitWithOffset(TokenType.LT, start); | |
238 // ignore whitespace in braces | |
239 while (Character.isWhitespace(c)) { | |
240 c = _recordStartOfLineAndAdvance(c); | |
241 } | |
242 // get tag | |
243 if (Character.isLetterOrDigit(c)) { | |
244 int tagStart = offset; | |
245 c = advance(); | |
246 while (Character.isLetterOrDigit(c) || c == 0x2D || c == 0x5F) { | |
247 c = advance(); | |
248 } | |
249 _emitWithOffsetAndLength(TokenType.TAG, tagStart, -1); | |
250 // check tag against passThrough elements | |
251 String tag = _tail.lexeme; | |
252 for (String str in _passThroughElements) { | |
253 if (str == tag) { | |
254 endPassThrough = "</$str>"; | |
255 break; | |
256 } | |
257 } | |
258 } | |
259 } | |
260 } else if (c == 0x3E) { | |
261 _emitWithOffset(TokenType.GT, start); | |
262 inBrackets = false; | |
263 c = advance(); | |
264 // if passThrough != null, read until we match it | |
265 if (endPassThrough != null) { | |
266 bool endFound = false; | |
267 int len = endPassThrough.length; | |
268 int firstC = endPassThrough.codeUnitAt(0); | |
269 int index = 0; | |
270 int nextC = firstC; | |
271 while (c >= 0) { | |
272 if (c == nextC) { | |
273 index++; | |
274 if (index == len) { | |
275 endFound = true; | |
276 break; | |
277 } | |
278 nextC = endPassThrough.codeUnitAt(index); | |
279 } else if (c == firstC) { | |
280 index = 1; | |
281 nextC = endPassThrough.codeUnitAt(1); | |
282 } else { | |
283 index = 0; | |
284 nextC = firstC; | |
285 } | |
286 c = _recordStartOfLineAndAdvance(c); | |
287 } | |
288 if (start + 1 < offset) { | |
289 if (endFound) { | |
290 _emitWithOffsetAndLength(TokenType.TEXT, start + 1, -len); | |
291 _emitWithOffset(TokenType.LT_SLASH, offset - len + 1); | |
292 _emitWithOffsetAndLength(TokenType.TAG, offset - len + 3, -1); | |
293 } else { | |
294 _emitWithOffsetAndLength(TokenType.TEXT, start + 1, -1); | |
295 } | |
296 } | |
297 endPassThrough = null; | |
298 } | |
299 } else if (c == 0x2F && peek() == 0x3E) { | |
300 advance(); | |
301 _emitWithOffset(TokenType.SLASH_GT, start); | |
302 inBrackets = false; | |
303 c = advance(); | |
304 } else if (!inBrackets) { | |
305 c = _recordStartOfLineAndAdvance(c); | |
306 while (c != 0x3C && c >= 0) { | |
307 c = _recordStartOfLineAndAdvance(c); | |
308 } | |
309 _emitWithOffsetAndLength(TokenType.TEXT, start, -1); | |
310 } else if (c == 0x22 || c == 0x27) { | |
311 // read a string | |
312 int endQuote = c; | |
313 c = advance(); | |
314 while (c >= 0) { | |
315 if (c == endQuote) { | |
316 c = advance(); | |
317 break; | |
318 } | |
319 c = _recordStartOfLineAndAdvance(c); | |
320 } | |
321 _emitWithOffsetAndLength(TokenType.STRING, start, -1); | |
322 } else if (c == 0x3D) { | |
323 // a non-char token | |
324 _emitWithOffset(TokenType.EQ, start); | |
325 c = advance(); | |
326 } else if (Character.isWhitespace(c)) { | |
327 // ignore whitespace in braces | |
328 do { | |
329 c = _recordStartOfLineAndAdvance(c); | |
330 } while (Character.isWhitespace(c)); | |
331 } else if (Character.isLetterOrDigit(c)) { | |
332 c = advance(); | |
333 while (Character.isLetterOrDigit(c) || c == 0x2D || c == 0x5F) { | |
334 c = advance(); | |
335 } | |
336 _emitWithOffsetAndLength(TokenType.TAG, start, -1); | |
337 } else { | |
338 // a non-char token | |
339 _emitWithOffsetAndLength(TokenType.TEXT, start, 0); | |
340 c = advance(); | |
341 } | |
342 } | |
343 } | |
344 } | |
345 | |
346 /** | |
347 * Instances of the class `HtmlParser` are used to parse tokens into a AST struc
ture comprised | |
348 * of [XmlNode]s. | |
349 */ | |
350 class HtmlParser extends XmlParser { | |
351 static String _APPLICATION_DART_IN_DOUBLE_QUOTES = "\"application/dart\""; | |
352 | |
353 static String _APPLICATION_DART_IN_SINGLE_QUOTES = "'application/dart'"; | |
354 | |
355 static String _SCRIPT = "script"; | |
356 | |
357 static String _TYPE = "type"; | |
358 | |
359 /** | |
360 * A set containing the names of tags that do not have a closing tag. | |
361 */ | |
362 static Set<String> SELF_CLOSING = new HashSet<String>.from(<String>[ | |
363 "area", | |
364 "base", | |
365 "basefont", | |
366 "br", | |
367 "col", | |
368 "frame", | |
369 "hr", | |
370 "img", | |
371 "input", | |
372 "link", | |
373 "meta", | |
374 "param", | |
375 "!" | |
376 ]); | |
377 | |
378 /** | |
379 * The line information associated with the source being parsed. | |
380 */ | |
381 LineInfo _lineInfo; | |
382 | |
383 /** | |
384 * The error listener to which errors will be reported. | |
385 */ | |
386 final AnalysisErrorListener _errorListener; | |
387 | |
388 final AnalysisOptions _options; | |
389 | |
390 /** | |
391 * Construct a parser for the specified source. | |
392 * | |
393 * [source] is the source being parsed. [_errorListener] is the error | |
394 * listener to which errors will be reported. [_options] is the analysis | |
395 * options which should be used for parsing. | |
396 */ | |
397 HtmlParser(Source source, this._errorListener, this._options) : super(source); | |
398 | |
399 @override | |
400 XmlAttributeNode createAttributeNode(Token name, Token equals, Token value) => | |
401 new XmlAttributeNode(name, equals, value); | |
402 | |
403 @override | |
404 XmlTagNode createTagNode( | |
405 Token nodeStart, | |
406 Token tag, | |
407 List<XmlAttributeNode> attributes, | |
408 Token attributeEnd, | |
409 List<XmlTagNode> tagNodes, | |
410 Token contentEnd, | |
411 Token closingTag, | |
412 Token nodeEnd) { | |
413 if (_isScriptNode(tag, attributes, tagNodes)) { | |
414 HtmlScriptTagNode tagNode = new HtmlScriptTagNode(nodeStart, tag, | |
415 attributes, attributeEnd, tagNodes, contentEnd, closingTag, nodeEnd); | |
416 String contents = tagNode.content; | |
417 int contentOffset = attributeEnd.end; | |
418 LineInfo_Location location = _lineInfo.getLocation(contentOffset); | |
419 sc.Scanner scanner = new sc.Scanner(source, | |
420 new sc.SubSequenceReader(contents, contentOffset), _errorListener); | |
421 scanner.setSourceStart(location.lineNumber, location.columnNumber); | |
422 sc.Token firstToken = scanner.tokenize(); | |
423 Parser parser = new Parser(source, _errorListener); | |
424 CompilationUnit unit = parser.parseCompilationUnit(firstToken); | |
425 unit.lineInfo = _lineInfo; | |
426 tagNode.script = unit; | |
427 return tagNode; | |
428 } | |
429 return new XmlTagNode(nodeStart, tag, attributes, attributeEnd, tagNodes, | |
430 contentEnd, closingTag, nodeEnd); | |
431 } | |
432 | |
433 @override | |
434 bool isSelfClosing(Token tag) => SELF_CLOSING.contains(tag.lexeme); | |
435 | |
436 /** | |
437 * Parse the given tokens. | |
438 * | |
439 * @param token the first token in the stream of tokens to be parsed | |
440 * @param lineInfo the line information created by the scanner | |
441 * @return the parse result (not `null`) | |
442 */ | |
443 HtmlUnit parse(Token token, LineInfo lineInfo) { | |
444 this._lineInfo = lineInfo; | |
445 List<XmlTagNode> tagNodes = parseTopTagNodes(token); | |
446 return new HtmlUnit(token, tagNodes, currentToken); | |
447 } | |
448 | |
449 /** | |
450 * Determine if the specified node is a Dart script. | |
451 * | |
452 * @param node the node to be tested (not `null`) | |
453 * @return `true` if the node is a Dart script | |
454 */ | |
455 bool _isScriptNode( | |
456 Token tag, List<XmlAttributeNode> attributes, List<XmlTagNode> tagNodes) { | |
457 if (tagNodes.length != 0 || tag.lexeme != _SCRIPT) { | |
458 return false; | |
459 } | |
460 for (XmlAttributeNode attribute in attributes) { | |
461 if (attribute.name == _TYPE) { | |
462 Token valueToken = attribute.valueToken; | |
463 if (valueToken != null) { | |
464 String value = valueToken.lexeme; | |
465 if (value == _APPLICATION_DART_IN_DOUBLE_QUOTES || | |
466 value == _APPLICATION_DART_IN_SINGLE_QUOTES) { | |
467 return true; | |
468 } | |
469 } | |
470 } | |
471 } | |
472 return false; | |
473 } | |
474 | |
475 /** | |
476 * Given the contents of an embedded expression that occurs at the given offse
t, parse it as a | |
477 * Dart expression. The contents should not include the expression's delimiter
s. | |
478 * | |
479 * @param source the source that contains that given token | |
480 * @param token the token to start parsing from | |
481 * @return the Dart expression that was parsed | |
482 */ | |
483 static Expression parseEmbeddedExpression( | |
484 Source source, sc.Token token, AnalysisErrorListener errorListener) { | |
485 Parser parser = new Parser(source, errorListener); | |
486 return parser.parseExpression(token); | |
487 } | |
488 | |
489 /** | |
490 * Given the contents of an embedded expression that occurs at the given offse
t, scans it as a | |
491 * Dart code. | |
492 * | |
493 * @param source the source of that contains the given contents | |
494 * @param contents the contents to scan | |
495 * @param contentOffset the offset of the contents in the larger file | |
496 * @return the first Dart token | |
497 */ | |
498 static sc.Token scanDartSource(Source source, LineInfo lineInfo, | |
499 String contents, int contentOffset, AnalysisErrorListener errorListener) { | |
500 LineInfo_Location location = lineInfo.getLocation(contentOffset); | |
501 sc.Scanner scanner = new sc.Scanner(source, | |
502 new sc.SubSequenceReader(contents, contentOffset), errorListener); | |
503 scanner.setSourceStart(location.lineNumber, location.columnNumber); | |
504 return scanner.tokenize(); | |
505 } | |
506 } | |
507 | |
508 /** | |
509 * Instances of the class `HtmlScriptTagNode` represent a script tag within an H
TML file that | |
510 * references a Dart script. | |
511 */ | |
512 @deprecated | |
513 class HtmlScriptTagNode extends XmlTagNode { | |
514 /** | |
515 * The AST structure representing the Dart code within this tag. | |
516 */ | |
517 CompilationUnit _script; | |
518 | |
519 /** | |
520 * The element representing this script. | |
521 */ | |
522 HtmlScriptElement scriptElement; | |
523 | |
524 /** | |
525 * Initialize a newly created node to represent a script tag within an HTML fi
le that references a | |
526 * Dart script. | |
527 * | |
528 * @param nodeStart the token marking the beginning of the tag | |
529 * @param tag the name of the tag | |
530 * @param attributes the attributes in the tag | |
531 * @param attributeEnd the token terminating the region where attributes can b
e | |
532 * @param tagNodes the children of the tag | |
533 * @param contentEnd the token that starts the closing tag | |
534 * @param closingTag the name of the tag that occurs in the closing tag | |
535 * @param nodeEnd the last token in the tag | |
536 */ | |
537 HtmlScriptTagNode( | |
538 Token nodeStart, | |
539 Token tag, | |
540 List<XmlAttributeNode> attributes, | |
541 Token attributeEnd, | |
542 List<XmlTagNode> tagNodes, | |
543 Token contentEnd, | |
544 Token closingTag, | |
545 Token nodeEnd) | |
546 : super(nodeStart, tag, attributes, attributeEnd, tagNodes, contentEnd, | |
547 closingTag, nodeEnd); | |
548 | |
549 /** | |
550 * Return the AST structure representing the Dart code within this tag, or `nu
ll` if this | |
551 * tag references an external script. | |
552 * | |
553 * @return the AST structure representing the Dart code within this tag | |
554 */ | |
555 CompilationUnit get script => _script; | |
556 | |
557 /** | |
558 * Set the AST structure representing the Dart code within this tag to the giv
en compilation unit. | |
559 * | |
560 * @param unit the AST structure representing the Dart code within this tag | |
561 */ | |
562 void set script(CompilationUnit unit) { | |
563 _script = unit; | |
564 } | |
565 | |
566 @override | |
567 accept(XmlVisitor visitor) => visitor.visitHtmlScriptTagNode(this); | |
568 } | |
569 | |
570 /** | |
571 * Instances of the class `HtmlUnit` represent the contents of an HTML file. | |
572 */ | |
573 @deprecated | |
574 class HtmlUnit extends XmlNode { | |
575 /** | |
576 * The first token in the token stream that was parsed to form this HTML unit. | |
577 */ | |
578 final Token beginToken; | |
579 | |
580 /** | |
581 * The last token in the token stream that was parsed to form this compilation
unit. This token | |
582 * should always have a type of [TokenType.EOF]. | |
583 */ | |
584 final Token endToken; | |
585 | |
586 /** | |
587 * The tag nodes contained in the receiver (not `null`, contains no `null`s). | |
588 */ | |
589 List<XmlTagNode> _tagNodes; | |
590 | |
591 /** | |
592 * Construct a new instance representing the content of an HTML file. | |
593 * | |
594 * @param beginToken the first token in the file (not `null`) | |
595 * @param tagNodes child tag nodes of the receiver (not `null`, contains no `n
ull`s) | |
596 * @param endToken the last token in the token stream which should be of type | |
597 * [TokenType.EOF] | |
598 */ | |
599 HtmlUnit(this.beginToken, List<XmlTagNode> tagNodes, this.endToken) { | |
600 this._tagNodes = becomeParentOfAll(tagNodes); | |
601 } | |
602 | |
603 /** | |
604 * Return the element associated with this HTML unit. | |
605 * | |
606 * @return the element or `null` if the receiver is not resolved | |
607 */ | |
608 @override | |
609 HtmlElement get element => super.element as HtmlElement; | |
610 | |
611 @override | |
612 void set element(Element element) { | |
613 if (element != null && element is! HtmlElement) { | |
614 throw new IllegalArgumentException( | |
615 "HtmlElement expected, but ${element.runtimeType} given"); | |
616 } | |
617 super.element = element; | |
618 } | |
619 | |
620 /** | |
621 * Answer the tag nodes contained in the receiver. Callers should not manipula
te the returned list | |
622 * to edit the AST structure. | |
623 * | |
624 * @return the children (not `null`, contains no `null`s) | |
625 */ | |
626 List<XmlTagNode> get tagNodes => _tagNodes; | |
627 | |
628 @override | |
629 accept(XmlVisitor visitor) => visitor.visitHtmlUnit(this); | |
630 | |
631 @override | |
632 void visitChildren(XmlVisitor visitor) { | |
633 for (XmlTagNode node in _tagNodes) { | |
634 node.accept(visitor); | |
635 } | |
636 } | |
637 } | |
638 | |
639 /** | |
640 * Instances of the class `RecursiveXmlVisitor` implement an XML visitor that wi
ll recursively | |
641 * visit all of the nodes in an XML structure. For example, using an instance of
this class to visit | |
642 * a [XmlTagNode] will also cause all of the contained [XmlAttributeNode]s and | |
643 * [XmlTagNode]s to be visited. | |
644 * | |
645 * Subclasses that override a visit method must either invoke the overridden vis
it method or must | |
646 * explicitly ask the visited node to visit its children. Failure to do so will
cause the children | |
647 * of the visited node to not be visited. | |
648 */ | |
649 class RecursiveXmlVisitor<R> implements XmlVisitor<R> { | |
650 @override | |
651 R visitHtmlScriptTagNode(HtmlScriptTagNode node) { | |
652 node.visitChildren(this); | |
653 return null; | |
654 } | |
655 | |
656 @override | |
657 R visitHtmlUnit(HtmlUnit node) { | |
658 node.visitChildren(this); | |
659 return null; | |
660 } | |
661 | |
662 @override | |
663 R visitXmlAttributeNode(XmlAttributeNode node) { | |
664 node.visitChildren(this); | |
665 return null; | |
666 } | |
667 | |
668 @override | |
669 R visitXmlTagNode(XmlTagNode node) { | |
670 node.visitChildren(this); | |
671 return null; | |
672 } | |
673 } | |
674 | |
675 /** | |
676 * Instances of the class `SimpleXmlVisitor` implement an AST visitor that will
do nothing | |
677 * when visiting an AST node. It is intended to be a superclass for classes that
use the visitor | |
678 * pattern primarily as a dispatch mechanism (and hence don't need to recursivel
y visit a whole | |
679 * structure) and that only need to visit a small number of node types. | |
680 */ | |
681 class SimpleXmlVisitor<R> implements XmlVisitor<R> { | |
682 @override | |
683 R visitHtmlScriptTagNode(HtmlScriptTagNode node) => null; | |
684 | |
685 @override | |
686 R visitHtmlUnit(HtmlUnit htmlUnit) => null; | |
687 | |
688 @override | |
689 R visitXmlAttributeNode(XmlAttributeNode xmlAttributeNode) => null; | |
690 | |
691 @override | |
692 R visitXmlTagNode(XmlTagNode xmlTagNode) => null; | |
693 } | |
694 | |
695 /** | |
696 * Instances of the class `StringScanner` implement a scanner that reads from a
string. The | |
697 * scanning logic is in the superclass. | |
698 */ | |
699 class StringScanner extends AbstractScanner { | |
700 /** | |
701 * The string from which characters will be read. | |
702 */ | |
703 final String _string; | |
704 | |
705 /** | |
706 * The number of characters in the string. | |
707 */ | |
708 int _stringLength = 0; | |
709 | |
710 /** | |
711 * The index, relative to the string, of the last character that was read. | |
712 */ | |
713 int _charOffset = 0; | |
714 | |
715 /** | |
716 * Initialize a newly created scanner to scan the characters in the given stri
ng. | |
717 * | |
718 * @param source the source being scanned | |
719 * @param string the string from which characters will be read | |
720 */ | |
721 StringScanner(Source source, this._string) : super(source) { | |
722 this._stringLength = _string.length; | |
723 this._charOffset = -1; | |
724 } | |
725 | |
726 @override | |
727 int get offset => _charOffset; | |
728 | |
729 void set offset(int offset) { | |
730 _charOffset = offset; | |
731 } | |
732 | |
733 @override | |
734 int advance() { | |
735 if (++_charOffset < _stringLength) { | |
736 return _string.codeUnitAt(_charOffset); | |
737 } | |
738 _charOffset = _stringLength; | |
739 return -1; | |
740 } | |
741 | |
742 @override | |
743 String getString(int start, int endDelta) => | |
744 _string.substring(start, _charOffset + 1 + endDelta).toString(); | |
745 | |
746 @override | |
747 int peek() { | |
748 if (_charOffset + 1 < _stringLength) { | |
749 return _string.codeUnitAt(_charOffset + 1); | |
750 } | |
751 return -1; | |
752 } | |
753 } | |
754 | |
755 /** | |
756 * Instances of the class `Token` represent a token that was scanned from the in
put. Each | |
757 * token knows which token follows it, acting as the head of a linked list of to
kens. | |
758 */ | |
759 class Token { | |
760 /** | |
761 * The offset from the beginning of the file to the first character in the tok
en. | |
762 */ | |
763 final int offset; | |
764 | |
765 /** | |
766 * The previous token in the token stream. | |
767 */ | |
768 Token previous; | |
769 | |
770 /** | |
771 * The next token in the token stream. | |
772 */ | |
773 Token _next; | |
774 | |
775 /** | |
776 * The type of the token. | |
777 */ | |
778 final TokenType type; | |
779 | |
780 /** | |
781 * The lexeme represented by this token. | |
782 */ | |
783 String _value; | |
784 | |
785 /** | |
786 * Initialize a newly created token. | |
787 * | |
788 * @param type the token type (not `null`) | |
789 * @param offset the offset from the beginning of the file to the first charac
ter in the token | |
790 */ | |
791 Token.con1(TokenType type, int offset) : this.con2(type, offset, type.lexeme); | |
792 | |
793 /** | |
794 * Initialize a newly created token. | |
795 * | |
796 * @param type the token type (not `null`) | |
797 * @param offset the offset from the beginning of the file to the first charac
ter in the token | |
798 * @param value the lexeme represented by this token (not `null`) | |
799 */ | |
800 Token.con2(this.type, this.offset, String value) { | |
801 this._value = StringUtilities.intern(value); | |
802 } | |
803 | |
804 /** | |
805 * Return the offset from the beginning of the file to the character after las
t character of the | |
806 * token. | |
807 * | |
808 * @return the offset from the beginning of the file to the first character af
ter last character | |
809 * of the token | |
810 */ | |
811 int get end => offset + length; | |
812 | |
813 /** | |
814 * Return `true` if this token is a synthetic token. A synthetic token is a to
ken that was | |
815 * introduced by the parser in order to recover from an error in the code. Syn
thetic tokens always | |
816 * have a length of zero (`0`). | |
817 * | |
818 * @return `true` if this token is a synthetic token | |
819 */ | |
820 bool get isSynthetic => length == 0; | |
821 | |
822 /** | |
823 * Return the number of characters in the node's source range. | |
824 * | |
825 * @return the number of characters in the node's source range | |
826 */ | |
827 int get length => lexeme.length; | |
828 | |
829 /** | |
830 * Return the lexeme that represents this token. | |
831 * | |
832 * @return the lexeme (not `null`) | |
833 */ | |
834 String get lexeme => _value; | |
835 | |
836 /** | |
837 * Return the next token in the token stream. | |
838 * | |
839 * @return the next token in the token stream | |
840 */ | |
841 Token get next => _next; | |
842 | |
843 /** | |
844 * Set the next token in the token stream to the given token. This has the sid
e-effect of setting | |
845 * this token to be the previous token for the given token. | |
846 * | |
847 * @param token the next token in the token stream | |
848 * @return the token that was passed in | |
849 */ | |
850 Token setNext(Token token) { | |
851 _next = token; | |
852 token.previous = this; | |
853 return token; | |
854 } | |
855 | |
856 @override | |
857 String toString() => lexeme; | |
858 } | |
859 | |
860 /** | |
861 * The enumeration `TokenType` defines the types of tokens that can be returned
by the | |
862 * scanner. | |
863 */ | |
864 class TokenType extends Enum<TokenType> { | |
865 /** | |
866 * The type of the token that marks the end of the input. | |
867 */ | |
868 static const TokenType EOF = const TokenType_EOF('EOF', 0, ""); | |
869 | |
870 static const TokenType EQ = const TokenType('EQ', 1, "="); | |
871 | |
872 static const TokenType GT = const TokenType('GT', 2, ">"); | |
873 | |
874 static const TokenType LT_SLASH = const TokenType('LT_SLASH', 3, "</"); | |
875 | |
876 static const TokenType LT = const TokenType('LT', 4, "<"); | |
877 | |
878 static const TokenType SLASH_GT = const TokenType('SLASH_GT', 5, "/>"); | |
879 | |
880 static const TokenType COMMENT = const TokenType('COMMENT', 6, null); | |
881 | |
882 static const TokenType DECLARATION = const TokenType('DECLARATION', 7, null); | |
883 | |
884 static const TokenType DIRECTIVE = const TokenType('DIRECTIVE', 8, null); | |
885 | |
886 static const TokenType STRING = const TokenType('STRING', 9, null); | |
887 | |
888 static const TokenType TAG = const TokenType('TAG', 10, null); | |
889 | |
890 static const TokenType TEXT = const TokenType('TEXT', 11, null); | |
891 | |
892 static const List<TokenType> values = const [ | |
893 EOF, | |
894 EQ, | |
895 GT, | |
896 LT_SLASH, | |
897 LT, | |
898 SLASH_GT, | |
899 COMMENT, | |
900 DECLARATION, | |
901 DIRECTIVE, | |
902 STRING, | |
903 TAG, | |
904 TEXT | |
905 ]; | |
906 | |
907 /** | |
908 * The lexeme that defines this type of token, or `null` if there is more than
one possible | |
909 * lexeme for this type of token. | |
910 */ | |
911 final String lexeme; | |
912 | |
913 const TokenType(String name, int ordinal, this.lexeme) : super(name, ordinal); | |
914 } | |
915 | |
916 class TokenType_EOF extends TokenType { | |
917 const TokenType_EOF(String name, int ordinal, String arg0) | |
918 : super(name, ordinal, arg0); | |
919 | |
920 @override | |
921 String toString() => "-eof-"; | |
922 } | |
923 | |
924 /** | |
925 * Instances of the class `ToSourceVisitor` write a source representation of a v
isited XML | |
926 * node (and all of it's children) to a writer. | |
927 */ | |
928 class ToSourceVisitor implements XmlVisitor<Object> { | |
929 /** | |
930 * The writer to which the source is to be written. | |
931 */ | |
932 final PrintWriter _writer; | |
933 | |
934 /** | |
935 * Initialize a newly created visitor to write source code representing the vi
sited nodes to the | |
936 * given writer. | |
937 * | |
938 * @param writer the writer to which the source is to be written | |
939 */ | |
940 ToSourceVisitor(this._writer); | |
941 | |
942 @override | |
943 Object visitHtmlScriptTagNode(HtmlScriptTagNode node) => | |
944 visitXmlTagNode(node); | |
945 | |
946 @override | |
947 Object visitHtmlUnit(HtmlUnit node) { | |
948 for (XmlTagNode child in node.tagNodes) { | |
949 _visit(child); | |
950 } | |
951 return null; | |
952 } | |
953 | |
954 @override | |
955 Object visitXmlAttributeNode(XmlAttributeNode node) { | |
956 String name = node.name; | |
957 Token value = node.valueToken; | |
958 if (name.length == 0) { | |
959 _writer.print("__"); | |
960 } else { | |
961 _writer.print(name); | |
962 } | |
963 _writer.print("="); | |
964 if (value == null) { | |
965 _writer.print("__"); | |
966 } else { | |
967 _writer.print(value.lexeme); | |
968 } | |
969 return null; | |
970 } | |
971 | |
972 @override | |
973 Object visitXmlTagNode(XmlTagNode node) { | |
974 _writer.print("<"); | |
975 String tagName = node.tag; | |
976 _writer.print(tagName); | |
977 for (XmlAttributeNode attribute in node.attributes) { | |
978 _writer.print(" "); | |
979 _visit(attribute); | |
980 } | |
981 _writer.print(node.attributeEnd.lexeme); | |
982 if (node.closingTag != null) { | |
983 for (XmlTagNode child in node.tagNodes) { | |
984 _visit(child); | |
985 } | |
986 _writer.print("</"); | |
987 _writer.print(tagName); | |
988 _writer.print(">"); | |
989 } | |
990 return null; | |
991 } | |
992 | |
993 /** | |
994 * Safely visit the given node. | |
995 * | |
996 * @param node the node to be visited | |
997 */ | |
998 void _visit(XmlNode node) { | |
999 if (node != null) { | |
1000 node.accept(this); | |
1001 } | |
1002 } | |
1003 } | |
1004 | |
1005 /** | |
1006 * Instances of `XmlAttributeNode` represent name/value pairs owned by an [XmlTa
gNode]. | |
1007 */ | |
1008 class XmlAttributeNode extends XmlNode { | |
1009 /** | |
1010 * An empty list of XML attribute nodes. | |
1011 */ | |
1012 static const List<XmlAttributeNode> EMPTY_LIST = const <XmlAttributeNode>[]; | |
1013 | |
1014 final Token _name; | |
1015 | |
1016 final Token equals; | |
1017 | |
1018 final Token _value; | |
1019 | |
1020 List<XmlExpression> expressions = XmlExpression.EMPTY_ARRAY; | |
1021 | |
1022 /** | |
1023 * Construct a new instance representing an XML attribute. | |
1024 * | |
1025 * @param name the name token (not `null`). This may be a zero length token if
the attribute | |
1026 * is badly formed. | |
1027 * @param equals the equals sign or `null` if none | |
1028 * @param value the value token (not `null`) | |
1029 */ | |
1030 XmlAttributeNode(this._name, this.equals, this._value); | |
1031 | |
1032 @override | |
1033 Token get beginToken => _name; | |
1034 | |
1035 @override | |
1036 Token get endToken => _value; | |
1037 | |
1038 /** | |
1039 * Answer the attribute name. This may be a zero length string if the attribut
e is badly formed. | |
1040 * | |
1041 * @return the name (not `null`) | |
1042 */ | |
1043 String get name => _name.lexeme; | |
1044 | |
1045 /** | |
1046 * Answer the attribute name token. This may be a zero length token if the att
ribute is badly | |
1047 * formed. | |
1048 * | |
1049 * @return the name token (not `null`) | |
1050 */ | |
1051 Token get nameToken => _name; | |
1052 | |
1053 /** | |
1054 * Answer the lexeme for the value token without the leading and trailing quot
es. | |
1055 * | |
1056 * @return the text or `null` if the value is not specified | |
1057 */ | |
1058 String get text { | |
1059 if (_value == null) { | |
1060 return null; | |
1061 } | |
1062 //TODO (danrubel): replace HTML character encodings with the actual | |
1063 // characters | |
1064 String text = _value.lexeme; | |
1065 int len = text.length; | |
1066 if (len > 0) { | |
1067 if (text.codeUnitAt(0) == 0x22) { | |
1068 if (len > 1 && text.codeUnitAt(len - 1) == 0x22) { | |
1069 return text.substring(1, len - 1); | |
1070 } else { | |
1071 return text.substring(1); | |
1072 } | |
1073 } else if (text.codeUnitAt(0) == 0x27) { | |
1074 if (len > 1 && text.codeUnitAt(len - 1) == 0x27) { | |
1075 return text.substring(1, len - 1); | |
1076 } else { | |
1077 return text.substring(1); | |
1078 } | |
1079 } | |
1080 } | |
1081 return text; | |
1082 } | |
1083 | |
1084 /** | |
1085 * Answer the offset of the value after the leading quote. | |
1086 * | |
1087 * @return the offset of the value, or `-1` if the value is not specified | |
1088 */ | |
1089 int get textOffset { | |
1090 if (_value == null) { | |
1091 return -1; | |
1092 } | |
1093 String text = _value.lexeme; | |
1094 if (StringUtilities.startsWithChar(text, 0x22) || | |
1095 StringUtilities.startsWithChar(text, 0x27)) { | |
1096 return _value.offset + 1; | |
1097 } | |
1098 return _value.offset; | |
1099 } | |
1100 | |
1101 /** | |
1102 * Answer the attribute value token. A properly formed value will start and en
d with matching | |
1103 * quote characters, but the value returned may not be properly formed. | |
1104 * | |
1105 * @return the value token or `null` if this represents a badly formed attribu
te | |
1106 */ | |
1107 Token get valueToken => _value; | |
1108 | |
1109 @override | |
1110 accept(XmlVisitor visitor) => visitor.visitXmlAttributeNode(this); | |
1111 | |
1112 @override | |
1113 void visitChildren(XmlVisitor visitor) { | |
1114 // no children to visit | |
1115 } | |
1116 } | |
1117 | |
1118 /** | |
1119 * Instances of the class `XmlExpression` represent an abstract expression embed
ded into | |
1120 * [XmlNode]. | |
1121 */ | |
1122 abstract class XmlExpression { | |
1123 /** | |
1124 * An empty list of expressions. | |
1125 */ | |
1126 static const List<XmlExpression> EMPTY_ARRAY = const <XmlExpression>[]; | |
1127 | |
1128 /** | |
1129 * Return the offset of the character immediately following the last character
of this | |
1130 * expression's source range. This is equivalent to `getOffset() + getLength()
`. | |
1131 * | |
1132 * @return the offset of the character just past the expression's source range | |
1133 */ | |
1134 int get end; | |
1135 | |
1136 /** | |
1137 * Return the number of characters in the expression's source range. | |
1138 */ | |
1139 int get length; | |
1140 | |
1141 /** | |
1142 * Return the offset of the first character in the expression's source range. | |
1143 */ | |
1144 int get offset; | |
1145 | |
1146 /** | |
1147 * Check if the given offset belongs to the expression's source range. | |
1148 */ | |
1149 bool contains(int offset) => this.offset <= offset && offset < end; | |
1150 | |
1151 /** | |
1152 * Return the [Reference] at the given offset. | |
1153 * | |
1154 * @param offset the offset from the beginning of the file | |
1155 * @return the [Reference] at the given offset, maybe `null` | |
1156 */ | |
1157 XmlExpression_Reference getReference(int offset); | |
1158 } | |
1159 | |
1160 /** | |
1161 * The reference to the [Element]. | |
1162 */ | |
1163 class XmlExpression_Reference { | |
1164 Element element; | |
1165 | |
1166 int offset = 0; | |
1167 | |
1168 int length = 0; | |
1169 | |
1170 XmlExpression_Reference(this.element, this.offset, this.length); | |
1171 } | |
1172 | |
1173 /** | |
1174 * The abstract class `XmlNode` defines behavior common to all XML/HTML nodes. | |
1175 */ | |
1176 abstract class XmlNode { | |
1177 /** | |
1178 * The parent of the node, or `null` if the node is the root of an AST structu
re. | |
1179 */ | |
1180 XmlNode _parent; | |
1181 | |
1182 /** | |
1183 * The element associated with this node or `null` if the receiver is not reso
lved. | |
1184 */ | |
1185 Element _element; | |
1186 | |
1187 /** | |
1188 * Return the first token included in this node's source range. | |
1189 * | |
1190 * @return the first token or `null` if none | |
1191 */ | |
1192 Token get beginToken; | |
1193 | |
1194 /** | |
1195 * Return the element associated with this node. | |
1196 * | |
1197 * @return the element or `null` if the receiver is not resolved | |
1198 */ | |
1199 Element get element => _element; | |
1200 | |
1201 /** | |
1202 * Set the element associated with this node. | |
1203 * | |
1204 * @param element the element | |
1205 */ | |
1206 void set element(Element element) { | |
1207 this._element = element; | |
1208 } | |
1209 | |
1210 /** | |
1211 * Return the offset of the character immediately following the last character
of this node's | |
1212 * source range. This is equivalent to `node.getOffset() + node.getLength()`.
For an html | |
1213 * unit this will be equal to the length of the unit's source. | |
1214 * | |
1215 * @return the offset of the character just past the node's source range | |
1216 */ | |
1217 int get end => offset + length; | |
1218 | |
1219 /** | |
1220 * Return the last token included in this node's source range. | |
1221 * | |
1222 * @return the last token or `null` if none | |
1223 */ | |
1224 Token get endToken; | |
1225 | |
1226 /** | |
1227 * Return the number of characters in the node's source range. | |
1228 * | |
1229 * @return the number of characters in the node's source range | |
1230 */ | |
1231 int get length { | |
1232 Token beginToken = this.beginToken; | |
1233 Token endToken = this.endToken; | |
1234 if (beginToken == null || endToken == null) { | |
1235 return -1; | |
1236 } | |
1237 return endToken.offset + endToken.length - beginToken.offset; | |
1238 } | |
1239 | |
1240 /** | |
1241 * Return the offset from the beginning of the file to the first character in
the node's source | |
1242 * range. | |
1243 * | |
1244 * @return the offset from the beginning of the file to the first character in
the node's source | |
1245 * range | |
1246 */ | |
1247 int get offset { | |
1248 Token beginToken = this.beginToken; | |
1249 if (beginToken == null) { | |
1250 return -1; | |
1251 } | |
1252 return this.beginToken.offset; | |
1253 } | |
1254 | |
1255 /** | |
1256 * Return this node's parent node, or `null` if this node is the root of an AS
T structure. | |
1257 * | |
1258 * Note that the relationship between an AST node and its parent node may chan
ge over the lifetime | |
1259 * of a node. | |
1260 * | |
1261 * @return the parent of this node, or `null` if none | |
1262 */ | |
1263 XmlNode get parent => _parent; | |
1264 | |
1265 /** | |
1266 * Set the parent of this node to the given node. | |
1267 * | |
1268 * @param newParent the node that is to be made the parent of this node | |
1269 */ | |
1270 void set parent(XmlNode newParent) { | |
1271 XmlNode current = newParent; | |
1272 while (current != null) { | |
1273 if (identical(current, this)) { | |
1274 AnalysisEngine.instance.logger.logError( | |
1275 "Circular structure while setting an XML node's parent", | |
1276 new CaughtException( | |
1277 new ArgumentError(_buildRecursiveStructureMessage(newParent)), | |
1278 null)); | |
1279 return; | |
1280 } | |
1281 current = current.parent; | |
1282 } | |
1283 _parent = newParent; | |
1284 } | |
1285 | |
1286 /** | |
1287 * Use the given visitor to visit this node. | |
1288 * | |
1289 * @param visitor the visitor that will visit this node | |
1290 * @return the value returned by the visitor as a result of visiting this node | |
1291 */ | |
1292 accept(XmlVisitor visitor); | |
1293 | |
1294 /** | |
1295 * Make this node the parent of the given child node. | |
1296 * | |
1297 * @param child the node that will become a child of this node | |
1298 * @return the node that was made a child of this node | |
1299 */ | |
1300 XmlNode becomeParentOf(XmlNode child) { | |
1301 if (child != null) { | |
1302 XmlNode node = child; | |
1303 node.parent = this; | |
1304 } | |
1305 return child; | |
1306 } | |
1307 | |
1308 /** | |
1309 * Make this node the parent of the given child nodes. | |
1310 * | |
1311 * @param children the nodes that will become the children of this node | |
1312 * @param ifEmpty the (empty) nodes to return if "children" is empty | |
1313 * @return the nodes that were made children of this node | |
1314 */ | |
1315 List becomeParentOfAll(List children, {List ifEmpty}) { | |
1316 if (children == null || children.isEmpty) { | |
1317 if (ifEmpty != null) { | |
1318 return ifEmpty; | |
1319 } | |
1320 } | |
1321 if (children != null) { | |
1322 children.forEach((XmlNode node) { | |
1323 node.parent = this; | |
1324 }); | |
1325 } | |
1326 return children; | |
1327 } | |
1328 | |
1329 @override | |
1330 String toString() { | |
1331 PrintStringWriter writer = new PrintStringWriter(); | |
1332 accept(new ToSourceVisitor(writer)); | |
1333 return writer.toString(); | |
1334 } | |
1335 | |
1336 /** | |
1337 * Use the given visitor to visit all of the children of this node. The childr
en will be visited | |
1338 * in source order. | |
1339 * | |
1340 * @param visitor the visitor that will be used to visit the children of this
node | |
1341 */ | |
1342 void visitChildren(XmlVisitor visitor); | |
1343 | |
1344 /** | |
1345 * This method exists for debugging purposes only. | |
1346 */ | |
1347 void _appendIdentifier(StringBuffer buffer, XmlNode node) { | |
1348 if (node is XmlTagNode) { | |
1349 buffer.write(node.tag); | |
1350 } else if (node is XmlAttributeNode) { | |
1351 buffer.write(node.name); | |
1352 } else { | |
1353 buffer.write("htmlUnit"); | |
1354 } | |
1355 } | |
1356 | |
1357 /** | |
1358 * This method exists for debugging purposes only. | |
1359 */ | |
1360 String _buildRecursiveStructureMessage(XmlNode newParent) { | |
1361 StringBuffer buffer = new StringBuffer(); | |
1362 buffer.write("Attempt to create recursive structure: "); | |
1363 XmlNode current = newParent; | |
1364 while (current != null) { | |
1365 if (!identical(current, newParent)) { | |
1366 buffer.write(" -> "); | |
1367 } | |
1368 if (identical(current, this)) { | |
1369 buffer.writeCharCode(0x2A); | |
1370 _appendIdentifier(buffer, current); | |
1371 buffer.writeCharCode(0x2A); | |
1372 } else { | |
1373 _appendIdentifier(buffer, current); | |
1374 } | |
1375 current = current.parent; | |
1376 } | |
1377 return buffer.toString(); | |
1378 } | |
1379 } | |
1380 | |
1381 /** | |
1382 * Instances of the class `XmlParser` are used to parse tokens into a AST struct
ure comprised | |
1383 * of [XmlNode]s. | |
1384 */ | |
1385 class XmlParser { | |
1386 /** | |
1387 * The source being parsed. | |
1388 */ | |
1389 final Source source; | |
1390 | |
1391 /** | |
1392 * The next token to be parsed. | |
1393 */ | |
1394 Token _currentToken; | |
1395 | |
1396 /** | |
1397 * Construct a parser for the specified source. | |
1398 * | |
1399 * @param source the source being parsed | |
1400 */ | |
1401 XmlParser(this.source); | |
1402 | |
1403 /** | |
1404 * Answer the current token. | |
1405 * | |
1406 * @return the current token | |
1407 */ | |
1408 Token get currentToken => _currentToken; | |
1409 | |
1410 /** | |
1411 * Create a node representing an attribute. | |
1412 * | |
1413 * @param name the name of the attribute | |
1414 * @param equals the equals sign, or `null` if there is no value | |
1415 * @param value the value of the attribute | |
1416 * @return the node that was created | |
1417 */ | |
1418 XmlAttributeNode createAttributeNode(Token name, Token equals, Token value) => | |
1419 new XmlAttributeNode(name, equals, value); | |
1420 | |
1421 /** | |
1422 * Create a node representing a tag. | |
1423 * | |
1424 * @param nodeStart the token marking the beginning of the tag | |
1425 * @param tag the name of the tag | |
1426 * @param attributes the attributes in the tag | |
1427 * @param attributeEnd the token terminating the region where attributes can b
e | |
1428 * @param tagNodes the children of the tag | |
1429 * @param contentEnd the token that starts the closing tag | |
1430 * @param closingTag the name of the tag that occurs in the closing tag | |
1431 * @param nodeEnd the last token in the tag | |
1432 * @return the node that was created | |
1433 */ | |
1434 XmlTagNode createTagNode( | |
1435 Token nodeStart, | |
1436 Token tag, | |
1437 List<XmlAttributeNode> attributes, | |
1438 Token attributeEnd, | |
1439 List<XmlTagNode> tagNodes, | |
1440 Token contentEnd, | |
1441 Token closingTag, | |
1442 Token nodeEnd) => | |
1443 new XmlTagNode(nodeStart, tag, attributes, attributeEnd, tagNodes, | |
1444 contentEnd, closingTag, nodeEnd); | |
1445 | |
1446 /** | |
1447 * Answer `true` if the specified tag is self closing and thus should never ha
ve content or | |
1448 * child tag nodes. | |
1449 * | |
1450 * @param tag the tag (not `null`) | |
1451 * @return `true` if self closing | |
1452 */ | |
1453 bool isSelfClosing(Token tag) => false; | |
1454 | |
1455 /** | |
1456 * Parse the entire token stream and in the process, advance the current token
to the end of the | |
1457 * token stream. | |
1458 * | |
1459 * @return the list of tag nodes found (not `null`, contains no `null`) | |
1460 */ | |
1461 List<XmlTagNode> parseTopTagNodes(Token firstToken) { | |
1462 _currentToken = firstToken; | |
1463 List<XmlTagNode> tagNodes = new List<XmlTagNode>(); | |
1464 TokenType type = _currentToken.type; | |
1465 while (type != TokenType.EOF) { | |
1466 if (type == TokenType.LT) { | |
1467 tagNodes.add(_parseTagNode()); | |
1468 } else if (type == TokenType.DECLARATION || | |
1469 type == TokenType.DIRECTIVE || | |
1470 type == TokenType.COMMENT) { | |
1471 // ignored tokens | |
1472 _currentToken = _currentToken.next; | |
1473 } else { | |
1474 _reportUnexpectedToken(); | |
1475 _currentToken = _currentToken.next; | |
1476 } | |
1477 type = _currentToken.type; | |
1478 } | |
1479 return tagNodes; | |
1480 } | |
1481 | |
1482 /** | |
1483 * Insert a synthetic token of the specified type before the current token | |
1484 * | |
1485 * @param type the type of token to be inserted (not `null`) | |
1486 * @return the synthetic token that was inserted (not `null`) | |
1487 */ | |
1488 Token _insertSyntheticToken(TokenType type) { | |
1489 Token token = new Token.con2(type, _currentToken.offset, ""); | |
1490 _currentToken.previous.setNext(token); | |
1491 token.setNext(_currentToken); | |
1492 return token; | |
1493 } | |
1494 | |
1495 /** | |
1496 * Parse the token stream for an attribute. This method advances the current t
oken over the | |
1497 * attribute, but should not be called if the [currentToken] is not [TokenType
.TAG]. | |
1498 * | |
1499 * @return the attribute (not `null`) | |
1500 */ | |
1501 XmlAttributeNode _parseAttribute() { | |
1502 // Assume the current token is a tag | |
1503 Token name = _currentToken; | |
1504 _currentToken = _currentToken.next; | |
1505 // Equals sign | |
1506 Token equals; | |
1507 if (_currentToken.type == TokenType.EQ) { | |
1508 equals = _currentToken; | |
1509 _currentToken = _currentToken.next; | |
1510 } else { | |
1511 _reportUnexpectedToken(); | |
1512 equals = _insertSyntheticToken(TokenType.EQ); | |
1513 } | |
1514 // String value | |
1515 Token value; | |
1516 if (_currentToken.type == TokenType.STRING) { | |
1517 value = _currentToken; | |
1518 _currentToken = _currentToken.next; | |
1519 } else { | |
1520 _reportUnexpectedToken(); | |
1521 value = _insertSyntheticToken(TokenType.STRING); | |
1522 } | |
1523 return createAttributeNode(name, equals, value); | |
1524 } | |
1525 | |
1526 /** | |
1527 * Parse the stream for a sequence of attributes. This method advances the cur
rent token to the | |
1528 * next [TokenType.GT], [TokenType.SLASH_GT], or [TokenType.EOF]. | |
1529 * | |
1530 * @return a collection of zero or more attributes (not `null`, contains no `n
ull`s) | |
1531 */ | |
1532 List<XmlAttributeNode> _parseAttributes() { | |
1533 TokenType type = _currentToken.type; | |
1534 if (type == TokenType.GT || | |
1535 type == TokenType.SLASH_GT || | |
1536 type == TokenType.EOF) { | |
1537 return XmlTagNode.NO_ATTRIBUTES; | |
1538 } | |
1539 List<XmlAttributeNode> attributes = new List<XmlAttributeNode>(); | |
1540 while (type != TokenType.GT && | |
1541 type != TokenType.SLASH_GT && | |
1542 type != TokenType.EOF) { | |
1543 if (type == TokenType.TAG) { | |
1544 attributes.add(_parseAttribute()); | |
1545 } else { | |
1546 _reportUnexpectedToken(); | |
1547 _currentToken = _currentToken.next; | |
1548 } | |
1549 type = _currentToken.type; | |
1550 } | |
1551 return attributes; | |
1552 } | |
1553 | |
1554 /** | |
1555 * Parse the stream for a sequence of tag nodes existing within a parent tag n
ode. This method | |
1556 * advances the current token to the next [TokenType.LT_SLASH] or [TokenType.E
OF]. | |
1557 * | |
1558 * @return a list of nodes (not `null`, contains no `null`s) | |
1559 */ | |
1560 List<XmlTagNode> _parseChildTagNodes() { | |
1561 TokenType type = _currentToken.type; | |
1562 if (type == TokenType.LT_SLASH || type == TokenType.EOF) { | |
1563 return XmlTagNode.NO_TAG_NODES; | |
1564 } | |
1565 List<XmlTagNode> nodes = new List<XmlTagNode>(); | |
1566 while (type != TokenType.LT_SLASH && type != TokenType.EOF) { | |
1567 if (type == TokenType.LT) { | |
1568 nodes.add(_parseTagNode()); | |
1569 } else if (type == TokenType.COMMENT) { | |
1570 // ignored token | |
1571 _currentToken = _currentToken.next; | |
1572 } else { | |
1573 _reportUnexpectedToken(); | |
1574 _currentToken = _currentToken.next; | |
1575 } | |
1576 type = _currentToken.type; | |
1577 } | |
1578 return nodes; | |
1579 } | |
1580 | |
1581 /** | |
1582 * Parse the token stream for the next tag node. This method advances current
token over the | |
1583 * parsed tag node, but should only be called if the current token is [TokenTy
pe.LT] | |
1584 * | |
1585 * @return the tag node or `null` if none found | |
1586 */ | |
1587 XmlTagNode _parseTagNode() { | |
1588 // Assume that the current node is a tag node start TokenType.LT | |
1589 Token nodeStart = _currentToken; | |
1590 _currentToken = _currentToken.next; | |
1591 // Get the tag or create a synthetic tag and report an error | |
1592 Token tag; | |
1593 if (_currentToken.type == TokenType.TAG) { | |
1594 tag = _currentToken; | |
1595 _currentToken = _currentToken.next; | |
1596 } else { | |
1597 _reportUnexpectedToken(); | |
1598 tag = _insertSyntheticToken(TokenType.TAG); | |
1599 } | |
1600 // Parse the attributes | |
1601 List<XmlAttributeNode> attributes = _parseAttributes(); | |
1602 // Token ending attribute list | |
1603 Token attributeEnd; | |
1604 if (_currentToken.type == TokenType.GT || | |
1605 _currentToken.type == TokenType.SLASH_GT) { | |
1606 attributeEnd = _currentToken; | |
1607 _currentToken = _currentToken.next; | |
1608 } else { | |
1609 _reportUnexpectedToken(); | |
1610 attributeEnd = _insertSyntheticToken(TokenType.SLASH_GT); | |
1611 } | |
1612 // If the node has no children, then return the node | |
1613 if (attributeEnd.type == TokenType.SLASH_GT || isSelfClosing(tag)) { | |
1614 return createTagNode(nodeStart, tag, attributes, attributeEnd, | |
1615 XmlTagNode.NO_TAG_NODES, _currentToken, null, attributeEnd); | |
1616 } | |
1617 // Parse the child tag nodes | |
1618 List<XmlTagNode> tagNodes = _parseChildTagNodes(); | |
1619 // Token ending child tag nodes | |
1620 Token contentEnd; | |
1621 if (_currentToken.type == TokenType.LT_SLASH) { | |
1622 contentEnd = _currentToken; | |
1623 _currentToken = _currentToken.next; | |
1624 } else { | |
1625 // TODO (danrubel): handle self closing HTML elements by inserting | |
1626 // synthetic tokens but not reporting an error | |
1627 _reportUnexpectedToken(); | |
1628 contentEnd = _insertSyntheticToken(TokenType.LT_SLASH); | |
1629 } | |
1630 // Closing tag | |
1631 Token closingTag; | |
1632 if (_currentToken.type == TokenType.TAG) { | |
1633 closingTag = _currentToken; | |
1634 _currentToken = _currentToken.next; | |
1635 } else { | |
1636 _reportUnexpectedToken(); | |
1637 closingTag = _insertSyntheticToken(TokenType.TAG); | |
1638 } | |
1639 // Token ending node | |
1640 Token nodeEnd; | |
1641 if (_currentToken.type == TokenType.GT) { | |
1642 nodeEnd = _currentToken; | |
1643 _currentToken = _currentToken.next; | |
1644 } else { | |
1645 _reportUnexpectedToken(); | |
1646 nodeEnd = _insertSyntheticToken(TokenType.GT); | |
1647 } | |
1648 return createTagNode(nodeStart, tag, attributes, attributeEnd, tagNodes, | |
1649 contentEnd, closingTag, nodeEnd); | |
1650 } | |
1651 | |
1652 /** | |
1653 * Report the current token as unexpected | |
1654 */ | |
1655 void _reportUnexpectedToken() { | |
1656 // TODO (danrubel): report unexpected token | |
1657 } | |
1658 } | |
1659 | |
1660 /** | |
1661 * Instances of `XmlTagNode` represent XML or HTML elements such as `` and | |
1662 * `<body foo="bar"> ... </body>`. | |
1663 */ | |
1664 class XmlTagNode extends XmlNode { | |
1665 /** | |
1666 * Constant representing empty list of attributes. | |
1667 */ | |
1668 static List<XmlAttributeNode> NO_ATTRIBUTES = | |
1669 new UnmodifiableListView(new List<XmlAttributeNode>()); | |
1670 | |
1671 /** | |
1672 * Constant representing empty list of tag nodes. | |
1673 */ | |
1674 static List<XmlTagNode> NO_TAG_NODES = | |
1675 new UnmodifiableListView(new List<XmlTagNode>()); | |
1676 | |
1677 /** | |
1678 * The starting [TokenType.LT] token (not `null`). | |
1679 */ | |
1680 final Token nodeStart; | |
1681 | |
1682 /** | |
1683 * The [TokenType.TAG] token after the starting '<' (not `null`). | |
1684 */ | |
1685 final Token _tag; | |
1686 | |
1687 /** | |
1688 * The attributes contained by the receiver (not `null`, contains no `null`s). | |
1689 */ | |
1690 List<XmlAttributeNode> _attributes; | |
1691 | |
1692 /** | |
1693 * The [TokenType.GT] or [TokenType.SLASH_GT] token after the attributes (not | |
1694 * `null`). The token may be the same token as [nodeEnd] if there are no child | |
1695 * [tagNodes]. | |
1696 */ | |
1697 final Token attributeEnd; | |
1698 | |
1699 /** | |
1700 * The tag nodes contained in the receiver (not `null`, contains no `null`s). | |
1701 */ | |
1702 List<XmlTagNode> _tagNodes; | |
1703 | |
1704 /** | |
1705 * The token (not `null`) after the content, which may be | |
1706 * * (1) [TokenType.LT_SLASH] for nodes with open and close tags, or | |
1707 * * (2) the [TokenType.LT] nodeStart of the next sibling node if this node is
self | |
1708 * closing or the attributeEnd is [TokenType.SLASH_GT], or | |
1709 * * (3) [TokenType.EOF] if the node does not have a closing tag and is the la
st node in | |
1710 * the stream [TokenType.LT_SLASH] token after the content, or `null` if there
is no | |
1711 * content and the attributes ended with [TokenType.SLASH_GT]. | |
1712 */ | |
1713 final Token contentEnd; | |
1714 | |
1715 /** | |
1716 * The closing [TokenType.TAG] after the child elements or `null` if there is
no | |
1717 * content and the attributes ended with [TokenType.SLASH_GT] | |
1718 */ | |
1719 final Token closingTag; | |
1720 | |
1721 /** | |
1722 * The ending [TokenType.GT] or [TokenType.SLASH_GT] token (not `null`). | |
1723 */ | |
1724 final Token nodeEnd; | |
1725 | |
1726 /** | |
1727 * The expressions that are embedded in the tag's content. | |
1728 */ | |
1729 List<XmlExpression> expressions = XmlExpression.EMPTY_ARRAY; | |
1730 | |
1731 /** | |
1732 * Construct a new instance representing an XML or HTML element | |
1733 * | |
1734 * @param nodeStart the starting [TokenType.LT] token (not `null`) | |
1735 * @param tag the [TokenType.TAG] token after the starting '<' (not `null`)
. | |
1736 * @param attributes the attributes associated with this element or [NO_ATTRIB
UTES] (not | |
1737 * `null`, contains no `null`s) | |
1738 * @param attributeEnd The [TokenType.GT] or [TokenType.SLASH_GT] token after
the | |
1739 * attributes (not `null`). The token may be the same token as [nodeE
nd] if | |
1740 * there are no child [tagNodes]. | |
1741 * @param tagNodes child tag nodes of the receiver or [NO_TAG_NODES] (not `nul
l`, | |
1742 * contains no `null`s) | |
1743 * @param contentEnd the token (not `null`) after the content, which may be | |
1744 * * (1) [TokenType.LT_SLASH] for nodes with open and close tags, or | |
1745 * * (2) the [TokenType.LT] nodeStart of the next sibling node if thi
s node is | |
1746 * self closing or the attributeEnd is [TokenType.SLASH_GT], or | |
1747 * * (3) [TokenType.EOF] if the node does not have a closing tag and
is the last | |
1748 * node in the stream [TokenType.LT_SLASH] token after the content, o
r `null` | |
1749 * if there is no content and the attributes ended with [TokenType.SL
ASH_GT]. | |
1750 * @param closingTag the closing [TokenType.TAG] after the child elements or `
null` if | |
1751 * there is no content and the attributes ended with [TokenType.SLASH
_GT] | |
1752 * @param nodeEnd the ending [TokenType.GT] or [TokenType.SLASH_GT] token (not | |
1753 * `null`) | |
1754 */ | |
1755 XmlTagNode( | |
1756 this.nodeStart, | |
1757 this._tag, | |
1758 List<XmlAttributeNode> attributes, | |
1759 this.attributeEnd, | |
1760 List<XmlTagNode> tagNodes, | |
1761 this.contentEnd, | |
1762 this.closingTag, | |
1763 this.nodeEnd) { | |
1764 this._attributes = becomeParentOfAll(attributes, ifEmpty: NO_ATTRIBUTES); | |
1765 this._tagNodes = becomeParentOfAll(tagNodes, ifEmpty: NO_TAG_NODES); | |
1766 } | |
1767 | |
1768 /** | |
1769 * Answer the receiver's attributes. Callers should not manipulate the returne
d list to edit the | |
1770 * AST structure. | |
1771 * | |
1772 * @return the attributes (not `null`, contains no `null`s) | |
1773 */ | |
1774 List<XmlAttributeNode> get attributes => _attributes; | |
1775 | |
1776 @override | |
1777 Token get beginToken => nodeStart; | |
1778 | |
1779 /** | |
1780 * Return a string representing the content contained in the receiver. This | |
1781 * includes the textual representation of any child tag nodes ([getTagNodes]). | |
1782 * Whitespace between '<', '</', and '>', '/>' is discarded, but all | |
1783 * other whitespace is preserved. | |
1784 */ | |
1785 String get content { | |
1786 Token token = attributeEnd.next; | |
1787 if (identical(token, contentEnd)) { | |
1788 return ""; | |
1789 } | |
1790 // TODO(danrubel) Handle CDATA and replace HTML character encodings with | |
1791 // the actual characters. | |
1792 String content = token.lexeme; | |
1793 token = token.next; | |
1794 if (identical(token, contentEnd)) { | |
1795 return content; | |
1796 } | |
1797 StringBuffer buffer = new StringBuffer(); | |
1798 buffer.write(content); | |
1799 while (!identical(token, contentEnd)) { | |
1800 buffer.write(token.lexeme); | |
1801 token = token.next; | |
1802 } | |
1803 return buffer.toString(); | |
1804 } | |
1805 | |
1806 @override | |
1807 Token get endToken { | |
1808 if (nodeEnd != null) { | |
1809 return nodeEnd; | |
1810 } | |
1811 if (closingTag != null) { | |
1812 return closingTag; | |
1813 } | |
1814 if (contentEnd != null) { | |
1815 return contentEnd; | |
1816 } | |
1817 if (!_tagNodes.isEmpty) { | |
1818 return _tagNodes[_tagNodes.length - 1].endToken; | |
1819 } | |
1820 if (attributeEnd != null) { | |
1821 return attributeEnd; | |
1822 } | |
1823 if (!_attributes.isEmpty) { | |
1824 return _attributes[_attributes.length - 1].endToken; | |
1825 } | |
1826 return _tag; | |
1827 } | |
1828 | |
1829 /** | |
1830 * Answer the tag name after the starting '<'. | |
1831 * | |
1832 * @return the tag name (not `null`) | |
1833 */ | |
1834 String get tag => _tag.lexeme; | |
1835 | |
1836 /** | |
1837 * Answer the tag nodes contained in the receiver. Callers should not manipula
te the returned list | |
1838 * to edit the AST structure. | |
1839 * | |
1840 * @return the children (not `null`, contains no `null`s) | |
1841 */ | |
1842 List<XmlTagNode> get tagNodes => _tagNodes; | |
1843 | |
1844 /** | |
1845 * Answer the [TokenType.TAG] token after the starting '<'. | |
1846 * | |
1847 * @return the token (not `null`) | |
1848 */ | |
1849 Token get tagToken => _tag; | |
1850 | |
1851 @override | |
1852 accept(XmlVisitor visitor) => visitor.visitXmlTagNode(this); | |
1853 | |
1854 /** | |
1855 * Answer the attribute with the specified name. | |
1856 * | |
1857 * @param name the attribute name | |
1858 * @return the attribute or `null` if no matching attribute is found | |
1859 */ | |
1860 XmlAttributeNode getAttribute(String name) { | |
1861 for (XmlAttributeNode attribute in _attributes) { | |
1862 if (attribute.name == name) { | |
1863 return attribute; | |
1864 } | |
1865 } | |
1866 return null; | |
1867 } | |
1868 | |
1869 /** | |
1870 * Find the attribute with the given name (see [getAttribute] and answer the l
exeme | |
1871 * for the attribute's value token without the leading and trailing quotes (se
e | |
1872 * [XmlAttributeNode.getText]). | |
1873 * | |
1874 * @param name the attribute name | |
1875 * @return the attribute text or `null` if no matching attribute is found | |
1876 */ | |
1877 String getAttributeText(String name) { | |
1878 XmlAttributeNode attribute = getAttribute(name); | |
1879 return attribute != null ? attribute.text : null; | |
1880 } | |
1881 | |
1882 @override | |
1883 void visitChildren(XmlVisitor visitor) { | |
1884 for (XmlAttributeNode node in _attributes) { | |
1885 node.accept(visitor); | |
1886 } | |
1887 for (XmlTagNode node in _tagNodes) { | |
1888 node.accept(visitor); | |
1889 } | |
1890 } | |
1891 } | |
1892 | |
1893 /** | |
1894 * The interface `XmlVisitor` defines the behavior of objects that can be used t
o visit an | |
1895 * [XmlNode] structure. | |
1896 */ | |
1897 abstract class XmlVisitor<R> { | |
1898 R visitHtmlScriptTagNode(HtmlScriptTagNode node); | |
1899 | |
1900 R visitHtmlUnit(HtmlUnit htmlUnit); | |
1901 | |
1902 R visitXmlAttributeNode(XmlAttributeNode xmlAttributeNode); | |
1903 | |
1904 R visitXmlTagNode(XmlTagNode xmlTagNode); | |
1905 } | |
OLD | NEW |