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(Token nodeStart, Token tag, |
| 405 List<XmlAttributeNode> attributes, Token attributeEnd, |
| 406 List<XmlTagNode> tagNodes, Token contentEnd, Token closingTag, |
| 407 Token nodeEnd) { |
| 408 if (_isScriptNode(tag, attributes, tagNodes)) { |
| 409 HtmlScriptTagNode tagNode = new HtmlScriptTagNode(nodeStart, tag, |
| 410 attributes, attributeEnd, tagNodes, contentEnd, closingTag, nodeEnd); |
| 411 String contents = tagNode.content; |
| 412 int contentOffset = attributeEnd.end; |
| 413 LineInfo_Location location = _lineInfo.getLocation(contentOffset); |
| 414 sc.Scanner scanner = new sc.Scanner(source, |
| 415 new sc.SubSequenceReader(contents, contentOffset), _errorListener); |
| 416 scanner.setSourceStart(location.lineNumber, location.columnNumber); |
| 417 sc.Token firstToken = scanner.tokenize(); |
| 418 Parser parser = new Parser(source, _errorListener); |
| 419 CompilationUnit unit = parser.parseCompilationUnit(firstToken); |
| 420 unit.lineInfo = _lineInfo; |
| 421 tagNode.script = unit; |
| 422 return tagNode; |
| 423 } |
| 424 return new XmlTagNode(nodeStart, tag, attributes, attributeEnd, tagNodes, |
| 425 contentEnd, closingTag, nodeEnd); |
| 426 } |
| 427 |
| 428 @override |
| 429 bool isSelfClosing(Token tag) => SELF_CLOSING.contains(tag.lexeme); |
| 430 |
| 431 /** |
| 432 * Parse the given tokens. |
| 433 * |
| 434 * @param token the first token in the stream of tokens to be parsed |
| 435 * @param lineInfo the line information created by the scanner |
| 436 * @return the parse result (not `null`) |
| 437 */ |
| 438 HtmlUnit parse(Token token, LineInfo lineInfo) { |
| 439 this._lineInfo = lineInfo; |
| 440 List<XmlTagNode> tagNodes = parseTopTagNodes(token); |
| 441 return new HtmlUnit(token, tagNodes, currentToken); |
| 442 } |
| 443 |
| 444 /** |
| 445 * Determine if the specified node is a Dart script. |
| 446 * |
| 447 * @param node the node to be tested (not `null`) |
| 448 * @return `true` if the node is a Dart script |
| 449 */ |
| 450 bool _isScriptNode( |
| 451 Token tag, List<XmlAttributeNode> attributes, List<XmlTagNode> tagNodes) { |
| 452 if (tagNodes.length != 0 || tag.lexeme != _SCRIPT) { |
| 453 return false; |
| 454 } |
| 455 for (XmlAttributeNode attribute in attributes) { |
| 456 if (attribute.name == _TYPE) { |
| 457 Token valueToken = attribute.valueToken; |
| 458 if (valueToken != null) { |
| 459 String value = valueToken.lexeme; |
| 460 if (value == _APPLICATION_DART_IN_DOUBLE_QUOTES || |
| 461 value == _APPLICATION_DART_IN_SINGLE_QUOTES) { |
| 462 return true; |
| 463 } |
| 464 } |
| 465 } |
| 466 } |
| 467 return false; |
| 468 } |
| 469 |
| 470 /** |
| 471 * Given the contents of an embedded expression that occurs at the given offse
t, parse it as a |
| 472 * Dart expression. The contents should not include the expression's delimiter
s. |
| 473 * |
| 474 * @param source the source that contains that given token |
| 475 * @param token the token to start parsing from |
| 476 * @return the Dart expression that was parsed |
| 477 */ |
| 478 static Expression parseEmbeddedExpression( |
| 479 Source source, sc.Token token, AnalysisErrorListener errorListener) { |
| 480 Parser parser = new Parser(source, errorListener); |
| 481 return parser.parseExpression(token); |
| 482 } |
| 483 |
| 484 /** |
| 485 * Given the contents of an embedded expression that occurs at the given offse
t, scans it as a |
| 486 * Dart code. |
| 487 * |
| 488 * @param source the source of that contains the given contents |
| 489 * @param contents the contents to scan |
| 490 * @param contentOffset the offset of the contents in the larger file |
| 491 * @return the first Dart token |
| 492 */ |
| 493 static sc.Token scanDartSource(Source source, LineInfo lineInfo, |
| 494 String contents, int contentOffset, AnalysisErrorListener errorListener) { |
| 495 LineInfo_Location location = lineInfo.getLocation(contentOffset); |
| 496 sc.Scanner scanner = new sc.Scanner(source, |
| 497 new sc.SubSequenceReader(contents, contentOffset), errorListener); |
| 498 scanner.setSourceStart(location.lineNumber, location.columnNumber); |
| 499 return scanner.tokenize(); |
| 500 } |
| 501 } |
| 502 |
| 503 /** |
| 504 * Instances of the class `HtmlScriptTagNode` represent a script tag within an H
TML file that |
| 505 * references a Dart script. |
| 506 */ |
| 507 @deprecated |
| 508 class HtmlScriptTagNode extends XmlTagNode { |
| 509 /** |
| 510 * The AST structure representing the Dart code within this tag. |
| 511 */ |
| 512 CompilationUnit _script; |
| 513 |
| 514 /** |
| 515 * The element representing this script. |
| 516 */ |
| 517 HtmlScriptElement scriptElement; |
| 518 |
| 519 /** |
| 520 * Initialize a newly created node to represent a script tag within an HTML fi
le that references a |
| 521 * Dart script. |
| 522 * |
| 523 * @param nodeStart the token marking the beginning of the tag |
| 524 * @param tag the name of the tag |
| 525 * @param attributes the attributes in the tag |
| 526 * @param attributeEnd the token terminating the region where attributes can b
e |
| 527 * @param tagNodes the children of the tag |
| 528 * @param contentEnd the token that starts the closing tag |
| 529 * @param closingTag the name of the tag that occurs in the closing tag |
| 530 * @param nodeEnd the last token in the tag |
| 531 */ |
| 532 HtmlScriptTagNode(Token nodeStart, Token tag, |
| 533 List<XmlAttributeNode> attributes, Token attributeEnd, |
| 534 List<XmlTagNode> tagNodes, Token contentEnd, Token closingTag, |
| 535 Token nodeEnd) |
| 536 : super(nodeStart, tag, attributes, attributeEnd, tagNodes, contentEnd, |
| 537 closingTag, nodeEnd); |
| 538 |
| 539 /** |
| 540 * Return the AST structure representing the Dart code within this tag, or `nu
ll` if this |
| 541 * tag references an external script. |
| 542 * |
| 543 * @return the AST structure representing the Dart code within this tag |
| 544 */ |
| 545 CompilationUnit get script => _script; |
| 546 |
| 547 /** |
| 548 * Set the AST structure representing the Dart code within this tag to the giv
en compilation unit. |
| 549 * |
| 550 * @param unit the AST structure representing the Dart code within this tag |
| 551 */ |
| 552 void set script(CompilationUnit unit) { |
| 553 _script = unit; |
| 554 } |
| 555 |
| 556 @override |
| 557 accept(XmlVisitor visitor) => visitor.visitHtmlScriptTagNode(this); |
| 558 } |
| 559 |
| 560 /** |
| 561 * Instances of the class `HtmlUnit` represent the contents of an HTML file. |
| 562 */ |
| 563 @deprecated |
| 564 class HtmlUnit extends XmlNode { |
| 565 /** |
| 566 * The first token in the token stream that was parsed to form this HTML unit. |
| 567 */ |
| 568 final Token beginToken; |
| 569 |
| 570 /** |
| 571 * The last token in the token stream that was parsed to form this compilation
unit. This token |
| 572 * should always have a type of [TokenType.EOF]. |
| 573 */ |
| 574 final Token endToken; |
| 575 |
| 576 /** |
| 577 * The tag nodes contained in the receiver (not `null`, contains no `null`s). |
| 578 */ |
| 579 List<XmlTagNode> _tagNodes; |
| 580 |
| 581 /** |
| 582 * Construct a new instance representing the content of an HTML file. |
| 583 * |
| 584 * @param beginToken the first token in the file (not `null`) |
| 585 * @param tagNodes child tag nodes of the receiver (not `null`, contains no `n
ull`s) |
| 586 * @param endToken the last token in the token stream which should be of type |
| 587 * [TokenType.EOF] |
| 588 */ |
| 589 HtmlUnit(this.beginToken, List<XmlTagNode> tagNodes, this.endToken) { |
| 590 this._tagNodes = becomeParentOfAll(tagNodes); |
| 591 } |
| 592 |
| 593 /** |
| 594 * Return the element associated with this HTML unit. |
| 595 * |
| 596 * @return the element or `null` if the receiver is not resolved |
| 597 */ |
| 598 @override |
| 599 HtmlElement get element => super.element as HtmlElement; |
| 600 |
| 601 @override |
| 602 void set element(Element element) { |
| 603 if (element != null && element is! HtmlElement) { |
| 604 throw new IllegalArgumentException( |
| 605 "HtmlElement expected, but ${element.runtimeType} given"); |
| 606 } |
| 607 super.element = element; |
| 608 } |
| 609 |
| 610 /** |
| 611 * Answer the tag nodes contained in the receiver. Callers should not manipula
te the returned list |
| 612 * to edit the AST structure. |
| 613 * |
| 614 * @return the children (not `null`, contains no `null`s) |
| 615 */ |
| 616 List<XmlTagNode> get tagNodes => _tagNodes; |
| 617 |
| 618 @override |
| 619 accept(XmlVisitor visitor) => visitor.visitHtmlUnit(this); |
| 620 |
| 621 @override |
| 622 void visitChildren(XmlVisitor visitor) { |
| 623 for (XmlTagNode node in _tagNodes) { |
| 624 node.accept(visitor); |
| 625 } |
| 626 } |
| 627 } |
| 628 |
| 629 /** |
| 630 * Instances of the class `RecursiveXmlVisitor` implement an XML visitor that wi
ll recursively |
| 631 * visit all of the nodes in an XML structure. For example, using an instance of
this class to visit |
| 632 * a [XmlTagNode] will also cause all of the contained [XmlAttributeNode]s and |
| 633 * [XmlTagNode]s to be visited. |
| 634 * |
| 635 * Subclasses that override a visit method must either invoke the overridden vis
it method or must |
| 636 * explicitly ask the visited node to visit its children. Failure to do so will
cause the children |
| 637 * of the visited node to not be visited. |
| 638 */ |
| 639 class RecursiveXmlVisitor<R> implements XmlVisitor<R> { |
| 640 @override |
| 641 R visitHtmlScriptTagNode(HtmlScriptTagNode node) { |
| 642 node.visitChildren(this); |
| 643 return null; |
| 644 } |
| 645 |
| 646 @override |
| 647 R visitHtmlUnit(HtmlUnit node) { |
| 648 node.visitChildren(this); |
| 649 return null; |
| 650 } |
| 651 |
| 652 @override |
| 653 R visitXmlAttributeNode(XmlAttributeNode node) { |
| 654 node.visitChildren(this); |
| 655 return null; |
| 656 } |
| 657 |
| 658 @override |
| 659 R visitXmlTagNode(XmlTagNode node) { |
| 660 node.visitChildren(this); |
| 661 return null; |
| 662 } |
| 663 } |
| 664 |
| 665 /** |
| 666 * Instances of the class `SimpleXmlVisitor` implement an AST visitor that will
do nothing |
| 667 * when visiting an AST node. It is intended to be a superclass for classes that
use the visitor |
| 668 * pattern primarily as a dispatch mechanism (and hence don't need to recursivel
y visit a whole |
| 669 * structure) and that only need to visit a small number of node types. |
| 670 */ |
| 671 class SimpleXmlVisitor<R> implements XmlVisitor<R> { |
| 672 @override |
| 673 R visitHtmlScriptTagNode(HtmlScriptTagNode node) => null; |
| 674 |
| 675 @override |
| 676 R visitHtmlUnit(HtmlUnit htmlUnit) => null; |
| 677 |
| 678 @override |
| 679 R visitXmlAttributeNode(XmlAttributeNode xmlAttributeNode) => null; |
| 680 |
| 681 @override |
| 682 R visitXmlTagNode(XmlTagNode xmlTagNode) => null; |
| 683 } |
| 684 |
| 685 /** |
| 686 * Instances of the class `StringScanner` implement a scanner that reads from a
string. The |
| 687 * scanning logic is in the superclass. |
| 688 */ |
| 689 class StringScanner extends AbstractScanner { |
| 690 /** |
| 691 * The string from which characters will be read. |
| 692 */ |
| 693 final String _string; |
| 694 |
| 695 /** |
| 696 * The number of characters in the string. |
| 697 */ |
| 698 int _stringLength = 0; |
| 699 |
| 700 /** |
| 701 * The index, relative to the string, of the last character that was read. |
| 702 */ |
| 703 int _charOffset = 0; |
| 704 |
| 705 /** |
| 706 * Initialize a newly created scanner to scan the characters in the given stri
ng. |
| 707 * |
| 708 * @param source the source being scanned |
| 709 * @param string the string from which characters will be read |
| 710 */ |
| 711 StringScanner(Source source, this._string) : super(source) { |
| 712 this._stringLength = _string.length; |
| 713 this._charOffset = -1; |
| 714 } |
| 715 |
| 716 @override |
| 717 int get offset => _charOffset; |
| 718 |
| 719 void set offset(int offset) { |
| 720 _charOffset = offset; |
| 721 } |
| 722 |
| 723 @override |
| 724 int advance() { |
| 725 if (++_charOffset < _stringLength) { |
| 726 return _string.codeUnitAt(_charOffset); |
| 727 } |
| 728 _charOffset = _stringLength; |
| 729 return -1; |
| 730 } |
| 731 |
| 732 @override |
| 733 String getString(int start, int endDelta) => |
| 734 _string.substring(start, _charOffset + 1 + endDelta).toString(); |
| 735 |
| 736 @override |
| 737 int peek() { |
| 738 if (_charOffset + 1 < _stringLength) { |
| 739 return _string.codeUnitAt(_charOffset + 1); |
| 740 } |
| 741 return -1; |
| 742 } |
| 743 } |
| 744 |
| 745 /** |
| 746 * Instances of the class `Token` represent a token that was scanned from the in
put. Each |
| 747 * token knows which token follows it, acting as the head of a linked list of to
kens. |
| 748 */ |
| 749 class Token { |
| 750 /** |
| 751 * The offset from the beginning of the file to the first character in the tok
en. |
| 752 */ |
| 753 final int offset; |
| 754 |
| 755 /** |
| 756 * The previous token in the token stream. |
| 757 */ |
| 758 Token previous; |
| 759 |
| 760 /** |
| 761 * The next token in the token stream. |
| 762 */ |
| 763 Token _next; |
| 764 |
| 765 /** |
| 766 * The type of the token. |
| 767 */ |
| 768 final TokenType type; |
| 769 |
| 770 /** |
| 771 * The lexeme represented by this token. |
| 772 */ |
| 773 String _value; |
| 774 |
| 775 /** |
| 776 * Initialize a newly created token. |
| 777 * |
| 778 * @param type the token type (not `null`) |
| 779 * @param offset the offset from the beginning of the file to the first charac
ter in the token |
| 780 */ |
| 781 Token.con1(TokenType type, int offset) : this.con2(type, offset, type.lexeme); |
| 782 |
| 783 /** |
| 784 * Initialize a newly created token. |
| 785 * |
| 786 * @param type the token type (not `null`) |
| 787 * @param offset the offset from the beginning of the file to the first charac
ter in the token |
| 788 * @param value the lexeme represented by this token (not `null`) |
| 789 */ |
| 790 Token.con2(this.type, this.offset, String value) { |
| 791 this._value = StringUtilities.intern(value); |
| 792 } |
| 793 |
| 794 /** |
| 795 * Return the offset from the beginning of the file to the character after las
t character of the |
| 796 * token. |
| 797 * |
| 798 * @return the offset from the beginning of the file to the first character af
ter last character |
| 799 * of the token |
| 800 */ |
| 801 int get end => offset + length; |
| 802 |
| 803 /** |
| 804 * Return `true` if this token is a synthetic token. A synthetic token is a to
ken that was |
| 805 * introduced by the parser in order to recover from an error in the code. Syn
thetic tokens always |
| 806 * have a length of zero (`0`). |
| 807 * |
| 808 * @return `true` if this token is a synthetic token |
| 809 */ |
| 810 bool get isSynthetic => length == 0; |
| 811 |
| 812 /** |
| 813 * Return the number of characters in the node's source range. |
| 814 * |
| 815 * @return the number of characters in the node's source range |
| 816 */ |
| 817 int get length => lexeme.length; |
| 818 |
| 819 /** |
| 820 * Return the lexeme that represents this token. |
| 821 * |
| 822 * @return the lexeme (not `null`) |
| 823 */ |
| 824 String get lexeme => _value; |
| 825 |
| 826 /** |
| 827 * Return the next token in the token stream. |
| 828 * |
| 829 * @return the next token in the token stream |
| 830 */ |
| 831 Token get next => _next; |
| 832 |
| 833 /** |
| 834 * Set the next token in the token stream to the given token. This has the sid
e-effect of setting |
| 835 * this token to be the previous token for the given token. |
| 836 * |
| 837 * @param token the next token in the token stream |
| 838 * @return the token that was passed in |
| 839 */ |
| 840 Token setNext(Token token) { |
| 841 _next = token; |
| 842 token.previous = this; |
| 843 return token; |
| 844 } |
| 845 |
| 846 @override |
| 847 String toString() => lexeme; |
| 848 } |
| 849 |
| 850 /** |
| 851 * The enumeration `TokenType` defines the types of tokens that can be returned
by the |
| 852 * scanner. |
| 853 */ |
| 854 class TokenType extends Enum<TokenType> { |
| 855 /** |
| 856 * The type of the token that marks the end of the input. |
| 857 */ |
| 858 static const TokenType EOF = const TokenType_EOF('EOF', 0, ""); |
| 859 |
| 860 static const TokenType EQ = const TokenType('EQ', 1, "="); |
| 861 |
| 862 static const TokenType GT = const TokenType('GT', 2, ">"); |
| 863 |
| 864 static const TokenType LT_SLASH = const TokenType('LT_SLASH', 3, "</"); |
| 865 |
| 866 static const TokenType LT = const TokenType('LT', 4, "<"); |
| 867 |
| 868 static const TokenType SLASH_GT = const TokenType('SLASH_GT', 5, "/>"); |
| 869 |
| 870 static const TokenType COMMENT = const TokenType('COMMENT', 6, null); |
| 871 |
| 872 static const TokenType DECLARATION = const TokenType('DECLARATION', 7, null); |
| 873 |
| 874 static const TokenType DIRECTIVE = const TokenType('DIRECTIVE', 8, null); |
| 875 |
| 876 static const TokenType STRING = const TokenType('STRING', 9, null); |
| 877 |
| 878 static const TokenType TAG = const TokenType('TAG', 10, null); |
| 879 |
| 880 static const TokenType TEXT = const TokenType('TEXT', 11, null); |
| 881 |
| 882 static const List<TokenType> values = const [ |
| 883 EOF, |
| 884 EQ, |
| 885 GT, |
| 886 LT_SLASH, |
| 887 LT, |
| 888 SLASH_GT, |
| 889 COMMENT, |
| 890 DECLARATION, |
| 891 DIRECTIVE, |
| 892 STRING, |
| 893 TAG, |
| 894 TEXT |
| 895 ]; |
| 896 |
| 897 /** |
| 898 * The lexeme that defines this type of token, or `null` if there is more than
one possible |
| 899 * lexeme for this type of token. |
| 900 */ |
| 901 final String lexeme; |
| 902 |
| 903 const TokenType(String name, int ordinal, this.lexeme) : super(name, ordinal); |
| 904 } |
| 905 |
| 906 class TokenType_EOF extends TokenType { |
| 907 const TokenType_EOF(String name, int ordinal, String arg0) |
| 908 : super(name, ordinal, arg0); |
| 909 |
| 910 @override |
| 911 String toString() => "-eof-"; |
| 912 } |
| 913 |
| 914 /** |
| 915 * Instances of the class `ToSourceVisitor` write a source representation of a v
isited XML |
| 916 * node (and all of it's children) to a writer. |
| 917 */ |
| 918 class ToSourceVisitor implements XmlVisitor<Object> { |
| 919 /** |
| 920 * The writer to which the source is to be written. |
| 921 */ |
| 922 final PrintWriter _writer; |
| 923 |
| 924 /** |
| 925 * Initialize a newly created visitor to write source code representing the vi
sited nodes to the |
| 926 * given writer. |
| 927 * |
| 928 * @param writer the writer to which the source is to be written |
| 929 */ |
| 930 ToSourceVisitor(this._writer); |
| 931 |
| 932 @override |
| 933 Object visitHtmlScriptTagNode(HtmlScriptTagNode node) => |
| 934 visitXmlTagNode(node); |
| 935 |
| 936 @override |
| 937 Object visitHtmlUnit(HtmlUnit node) { |
| 938 for (XmlTagNode child in node.tagNodes) { |
| 939 _visit(child); |
| 940 } |
| 941 return null; |
| 942 } |
| 943 |
| 944 @override |
| 945 Object visitXmlAttributeNode(XmlAttributeNode node) { |
| 946 String name = node.name; |
| 947 Token value = node.valueToken; |
| 948 if (name.length == 0) { |
| 949 _writer.print("__"); |
| 950 } else { |
| 951 _writer.print(name); |
| 952 } |
| 953 _writer.print("="); |
| 954 if (value == null) { |
| 955 _writer.print("__"); |
| 956 } else { |
| 957 _writer.print(value.lexeme); |
| 958 } |
| 959 return null; |
| 960 } |
| 961 |
| 962 @override |
| 963 Object visitXmlTagNode(XmlTagNode node) { |
| 964 _writer.print("<"); |
| 965 String tagName = node.tag; |
| 966 _writer.print(tagName); |
| 967 for (XmlAttributeNode attribute in node.attributes) { |
| 968 _writer.print(" "); |
| 969 _visit(attribute); |
| 970 } |
| 971 _writer.print(node.attributeEnd.lexeme); |
| 972 if (node.closingTag != null) { |
| 973 for (XmlTagNode child in node.tagNodes) { |
| 974 _visit(child); |
| 975 } |
| 976 _writer.print("</"); |
| 977 _writer.print(tagName); |
| 978 _writer.print(">"); |
| 979 } |
| 980 return null; |
| 981 } |
| 982 |
| 983 /** |
| 984 * Safely visit the given node. |
| 985 * |
| 986 * @param node the node to be visited |
| 987 */ |
| 988 void _visit(XmlNode node) { |
| 989 if (node != null) { |
| 990 node.accept(this); |
| 991 } |
| 992 } |
| 993 } |
| 994 |
| 995 /** |
| 996 * Instances of `XmlAttributeNode` represent name/value pairs owned by an [XmlTa
gNode]. |
| 997 */ |
| 998 class XmlAttributeNode extends XmlNode { |
| 999 /** |
| 1000 * An empty list of XML attribute nodes. |
| 1001 */ |
| 1002 static const List<XmlAttributeNode> EMPTY_LIST = const <XmlAttributeNode>[]; |
| 1003 |
| 1004 final Token _name; |
| 1005 |
| 1006 final Token equals; |
| 1007 |
| 1008 final Token _value; |
| 1009 |
| 1010 List<XmlExpression> expressions = XmlExpression.EMPTY_ARRAY; |
| 1011 |
| 1012 /** |
| 1013 * Construct a new instance representing an XML attribute. |
| 1014 * |
| 1015 * @param name the name token (not `null`). This may be a zero length token if
the attribute |
| 1016 * is badly formed. |
| 1017 * @param equals the equals sign or `null` if none |
| 1018 * @param value the value token (not `null`) |
| 1019 */ |
| 1020 XmlAttributeNode(this._name, this.equals, this._value); |
| 1021 |
| 1022 @override |
| 1023 Token get beginToken => _name; |
| 1024 |
| 1025 @override |
| 1026 Token get endToken => _value; |
| 1027 |
| 1028 /** |
| 1029 * Answer the attribute name. This may be a zero length string if the attribut
e is badly formed. |
| 1030 * |
| 1031 * @return the name (not `null`) |
| 1032 */ |
| 1033 String get name => _name.lexeme; |
| 1034 |
| 1035 /** |
| 1036 * Answer the attribute name token. This may be a zero length token if the att
ribute is badly |
| 1037 * formed. |
| 1038 * |
| 1039 * @return the name token (not `null`) |
| 1040 */ |
| 1041 Token get nameToken => _name; |
| 1042 |
| 1043 /** |
| 1044 * Answer the lexeme for the value token without the leading and trailing quot
es. |
| 1045 * |
| 1046 * @return the text or `null` if the value is not specified |
| 1047 */ |
| 1048 String get text { |
| 1049 if (_value == null) { |
| 1050 return null; |
| 1051 } |
| 1052 //TODO (danrubel): replace HTML character encodings with the actual |
| 1053 // characters |
| 1054 String text = _value.lexeme; |
| 1055 int len = text.length; |
| 1056 if (len > 0) { |
| 1057 if (text.codeUnitAt(0) == 0x22) { |
| 1058 if (len > 1 && text.codeUnitAt(len - 1) == 0x22) { |
| 1059 return text.substring(1, len - 1); |
| 1060 } else { |
| 1061 return text.substring(1); |
| 1062 } |
| 1063 } else if (text.codeUnitAt(0) == 0x27) { |
| 1064 if (len > 1 && text.codeUnitAt(len - 1) == 0x27) { |
| 1065 return text.substring(1, len - 1); |
| 1066 } else { |
| 1067 return text.substring(1); |
| 1068 } |
| 1069 } |
| 1070 } |
| 1071 return text; |
| 1072 } |
| 1073 |
| 1074 /** |
| 1075 * Answer the offset of the value after the leading quote. |
| 1076 * |
| 1077 * @return the offset of the value, or `-1` if the value is not specified |
| 1078 */ |
| 1079 int get textOffset { |
| 1080 if (_value == null) { |
| 1081 return -1; |
| 1082 } |
| 1083 String text = _value.lexeme; |
| 1084 if (StringUtilities.startsWithChar(text, 0x22) || |
| 1085 StringUtilities.startsWithChar(text, 0x27)) { |
| 1086 return _value.offset + 1; |
| 1087 } |
| 1088 return _value.offset; |
| 1089 } |
| 1090 |
| 1091 /** |
| 1092 * Answer the attribute value token. A properly formed value will start and en
d with matching |
| 1093 * quote characters, but the value returned may not be properly formed. |
| 1094 * |
| 1095 * @return the value token or `null` if this represents a badly formed attribu
te |
| 1096 */ |
| 1097 Token get valueToken => _value; |
| 1098 |
| 1099 @override |
| 1100 accept(XmlVisitor visitor) => visitor.visitXmlAttributeNode(this); |
| 1101 |
| 1102 @override |
| 1103 void visitChildren(XmlVisitor visitor) { |
| 1104 // no children to visit |
| 1105 } |
| 1106 } |
| 1107 |
| 1108 /** |
| 1109 * Instances of the class `XmlExpression` represent an abstract expression embed
ded into |
| 1110 * [XmlNode]. |
| 1111 */ |
| 1112 abstract class XmlExpression { |
| 1113 /** |
| 1114 * An empty list of expressions. |
| 1115 */ |
| 1116 static const List<XmlExpression> EMPTY_ARRAY = const <XmlExpression>[]; |
| 1117 |
| 1118 /** |
| 1119 * Return the offset of the character immediately following the last character
of this |
| 1120 * expression's source range. This is equivalent to `getOffset() + getLength()
`. |
| 1121 * |
| 1122 * @return the offset of the character just past the expression's source range |
| 1123 */ |
| 1124 int get end; |
| 1125 |
| 1126 /** |
| 1127 * Return the number of characters in the expression's source range. |
| 1128 */ |
| 1129 int get length; |
| 1130 |
| 1131 /** |
| 1132 * Return the offset of the first character in the expression's source range. |
| 1133 */ |
| 1134 int get offset; |
| 1135 |
| 1136 /** |
| 1137 * Check if the given offset belongs to the expression's source range. |
| 1138 */ |
| 1139 bool contains(int offset) => this.offset <= offset && offset < end; |
| 1140 |
| 1141 /** |
| 1142 * Return the [Reference] at the given offset. |
| 1143 * |
| 1144 * @param offset the offset from the beginning of the file |
| 1145 * @return the [Reference] at the given offset, maybe `null` |
| 1146 */ |
| 1147 XmlExpression_Reference getReference(int offset); |
| 1148 } |
| 1149 |
| 1150 /** |
| 1151 * The reference to the [Element]. |
| 1152 */ |
| 1153 class XmlExpression_Reference { |
| 1154 Element element; |
| 1155 |
| 1156 int offset = 0; |
| 1157 |
| 1158 int length = 0; |
| 1159 |
| 1160 XmlExpression_Reference(this.element, this.offset, this.length); |
| 1161 } |
| 1162 |
| 1163 /** |
| 1164 * The abstract class `XmlNode` defines behavior common to all XML/HTML nodes. |
| 1165 */ |
| 1166 abstract class XmlNode { |
| 1167 /** |
| 1168 * The parent of the node, or `null` if the node is the root of an AST structu
re. |
| 1169 */ |
| 1170 XmlNode _parent; |
| 1171 |
| 1172 /** |
| 1173 * The element associated with this node or `null` if the receiver is not reso
lved. |
| 1174 */ |
| 1175 Element _element; |
| 1176 |
| 1177 /** |
| 1178 * Return the first token included in this node's source range. |
| 1179 * |
| 1180 * @return the first token or `null` if none |
| 1181 */ |
| 1182 Token get beginToken; |
| 1183 |
| 1184 /** |
| 1185 * Return the element associated with this node. |
| 1186 * |
| 1187 * @return the element or `null` if the receiver is not resolved |
| 1188 */ |
| 1189 Element get element => _element; |
| 1190 |
| 1191 /** |
| 1192 * Set the element associated with this node. |
| 1193 * |
| 1194 * @param element the element |
| 1195 */ |
| 1196 void set element(Element element) { |
| 1197 this._element = element; |
| 1198 } |
| 1199 |
| 1200 /** |
| 1201 * Return the offset of the character immediately following the last character
of this node's |
| 1202 * source range. This is equivalent to `node.getOffset() + node.getLength()`.
For an html |
| 1203 * unit this will be equal to the length of the unit's source. |
| 1204 * |
| 1205 * @return the offset of the character just past the node's source range |
| 1206 */ |
| 1207 int get end => offset + length; |
| 1208 |
| 1209 /** |
| 1210 * Return the last token included in this node's source range. |
| 1211 * |
| 1212 * @return the last token or `null` if none |
| 1213 */ |
| 1214 Token get endToken; |
| 1215 |
| 1216 /** |
| 1217 * Return the number of characters in the node's source range. |
| 1218 * |
| 1219 * @return the number of characters in the node's source range |
| 1220 */ |
| 1221 int get length { |
| 1222 Token beginToken = this.beginToken; |
| 1223 Token endToken = this.endToken; |
| 1224 if (beginToken == null || endToken == null) { |
| 1225 return -1; |
| 1226 } |
| 1227 return endToken.offset + endToken.length - beginToken.offset; |
| 1228 } |
| 1229 |
| 1230 /** |
| 1231 * Return the offset from the beginning of the file to the first character in
the node's source |
| 1232 * range. |
| 1233 * |
| 1234 * @return the offset from the beginning of the file to the first character in
the node's source |
| 1235 * range |
| 1236 */ |
| 1237 int get offset { |
| 1238 Token beginToken = this.beginToken; |
| 1239 if (beginToken == null) { |
| 1240 return -1; |
| 1241 } |
| 1242 return this.beginToken.offset; |
| 1243 } |
| 1244 |
| 1245 /** |
| 1246 * Return this node's parent node, or `null` if this node is the root of an AS
T structure. |
| 1247 * |
| 1248 * Note that the relationship between an AST node and its parent node may chan
ge over the lifetime |
| 1249 * of a node. |
| 1250 * |
| 1251 * @return the parent of this node, or `null` if none |
| 1252 */ |
| 1253 XmlNode get parent => _parent; |
| 1254 |
| 1255 /** |
| 1256 * Set the parent of this node to the given node. |
| 1257 * |
| 1258 * @param newParent the node that is to be made the parent of this node |
| 1259 */ |
| 1260 void set parent(XmlNode newParent) { |
| 1261 XmlNode current = newParent; |
| 1262 while (current != null) { |
| 1263 if (identical(current, this)) { |
| 1264 AnalysisEngine.instance.logger.logError( |
| 1265 "Circular structure while setting an XML node's parent", |
| 1266 new CaughtException( |
| 1267 new ArgumentError(_buildRecursiveStructureMessage(newParent)), |
| 1268 null)); |
| 1269 return; |
| 1270 } |
| 1271 current = current.parent; |
| 1272 } |
| 1273 _parent = newParent; |
| 1274 } |
| 1275 |
| 1276 /** |
| 1277 * Use the given visitor to visit this node. |
| 1278 * |
| 1279 * @param visitor the visitor that will visit this node |
| 1280 * @return the value returned by the visitor as a result of visiting this node |
| 1281 */ |
| 1282 accept(XmlVisitor visitor); |
| 1283 |
| 1284 /** |
| 1285 * Make this node the parent of the given child node. |
| 1286 * |
| 1287 * @param child the node that will become a child of this node |
| 1288 * @return the node that was made a child of this node |
| 1289 */ |
| 1290 XmlNode becomeParentOf(XmlNode child) { |
| 1291 if (child != null) { |
| 1292 XmlNode node = child; |
| 1293 node.parent = this; |
| 1294 } |
| 1295 return child; |
| 1296 } |
| 1297 |
| 1298 /** |
| 1299 * Make this node the parent of the given child nodes. |
| 1300 * |
| 1301 * @param children the nodes that will become the children of this node |
| 1302 * @param ifEmpty the (empty) nodes to return if "children" is empty |
| 1303 * @return the nodes that were made children of this node |
| 1304 */ |
| 1305 List becomeParentOfAll(List children, {List ifEmpty}) { |
| 1306 if (children == null || children.isEmpty) { |
| 1307 if (ifEmpty != null) { |
| 1308 return ifEmpty; |
| 1309 } |
| 1310 } |
| 1311 if (children != null) { |
| 1312 children.forEach((XmlNode node) { |
| 1313 node.parent = this; |
| 1314 }); |
| 1315 } |
| 1316 return children; |
| 1317 } |
| 1318 |
| 1319 @override |
| 1320 String toString() { |
| 1321 PrintStringWriter writer = new PrintStringWriter(); |
| 1322 accept(new ToSourceVisitor(writer)); |
| 1323 return writer.toString(); |
| 1324 } |
| 1325 |
| 1326 /** |
| 1327 * Use the given visitor to visit all of the children of this node. The childr
en will be visited |
| 1328 * in source order. |
| 1329 * |
| 1330 * @param visitor the visitor that will be used to visit the children of this
node |
| 1331 */ |
| 1332 void visitChildren(XmlVisitor visitor); |
| 1333 |
| 1334 /** |
| 1335 * This method exists for debugging purposes only. |
| 1336 */ |
| 1337 void _appendIdentifier(StringBuffer buffer, XmlNode node) { |
| 1338 if (node is XmlTagNode) { |
| 1339 buffer.write(node.tag); |
| 1340 } else if (node is XmlAttributeNode) { |
| 1341 buffer.write(node.name); |
| 1342 } else { |
| 1343 buffer.write("htmlUnit"); |
| 1344 } |
| 1345 } |
| 1346 |
| 1347 /** |
| 1348 * This method exists for debugging purposes only. |
| 1349 */ |
| 1350 String _buildRecursiveStructureMessage(XmlNode newParent) { |
| 1351 StringBuffer buffer = new StringBuffer(); |
| 1352 buffer.write("Attempt to create recursive structure: "); |
| 1353 XmlNode current = newParent; |
| 1354 while (current != null) { |
| 1355 if (!identical(current, newParent)) { |
| 1356 buffer.write(" -> "); |
| 1357 } |
| 1358 if (identical(current, this)) { |
| 1359 buffer.writeCharCode(0x2A); |
| 1360 _appendIdentifier(buffer, current); |
| 1361 buffer.writeCharCode(0x2A); |
| 1362 } else { |
| 1363 _appendIdentifier(buffer, current); |
| 1364 } |
| 1365 current = current.parent; |
| 1366 } |
| 1367 return buffer.toString(); |
| 1368 } |
| 1369 } |
| 1370 |
| 1371 /** |
| 1372 * Instances of the class `XmlParser` are used to parse tokens into a AST struct
ure comprised |
| 1373 * of [XmlNode]s. |
| 1374 */ |
| 1375 class XmlParser { |
| 1376 /** |
| 1377 * The source being parsed. |
| 1378 */ |
| 1379 final Source source; |
| 1380 |
| 1381 /** |
| 1382 * The next token to be parsed. |
| 1383 */ |
| 1384 Token _currentToken; |
| 1385 |
| 1386 /** |
| 1387 * Construct a parser for the specified source. |
| 1388 * |
| 1389 * @param source the source being parsed |
| 1390 */ |
| 1391 XmlParser(this.source); |
| 1392 |
| 1393 /** |
| 1394 * Answer the current token. |
| 1395 * |
| 1396 * @return the current token |
| 1397 */ |
| 1398 Token get currentToken => _currentToken; |
| 1399 |
| 1400 /** |
| 1401 * Create a node representing an attribute. |
| 1402 * |
| 1403 * @param name the name of the attribute |
| 1404 * @param equals the equals sign, or `null` if there is no value |
| 1405 * @param value the value of the attribute |
| 1406 * @return the node that was created |
| 1407 */ |
| 1408 XmlAttributeNode createAttributeNode(Token name, Token equals, Token value) => |
| 1409 new XmlAttributeNode(name, equals, value); |
| 1410 |
| 1411 /** |
| 1412 * Create a node representing a tag. |
| 1413 * |
| 1414 * @param nodeStart the token marking the beginning of the tag |
| 1415 * @param tag the name of the tag |
| 1416 * @param attributes the attributes in the tag |
| 1417 * @param attributeEnd the token terminating the region where attributes can b
e |
| 1418 * @param tagNodes the children of the tag |
| 1419 * @param contentEnd the token that starts the closing tag |
| 1420 * @param closingTag the name of the tag that occurs in the closing tag |
| 1421 * @param nodeEnd the last token in the tag |
| 1422 * @return the node that was created |
| 1423 */ |
| 1424 XmlTagNode createTagNode(Token nodeStart, Token tag, |
| 1425 List<XmlAttributeNode> attributes, Token attributeEnd, |
| 1426 List<XmlTagNode> tagNodes, Token contentEnd, Token closingTag, |
| 1427 Token nodeEnd) => new XmlTagNode(nodeStart, tag, attributes, attributeEnd, |
| 1428 tagNodes, contentEnd, closingTag, nodeEnd); |
| 1429 |
| 1430 /** |
| 1431 * Answer `true` if the specified tag is self closing and thus should never ha
ve content or |
| 1432 * child tag nodes. |
| 1433 * |
| 1434 * @param tag the tag (not `null`) |
| 1435 * @return `true` if self closing |
| 1436 */ |
| 1437 bool isSelfClosing(Token tag) => false; |
| 1438 |
| 1439 /** |
| 1440 * Parse the entire token stream and in the process, advance the current token
to the end of the |
| 1441 * token stream. |
| 1442 * |
| 1443 * @return the list of tag nodes found (not `null`, contains no `null`) |
| 1444 */ |
| 1445 List<XmlTagNode> parseTopTagNodes(Token firstToken) { |
| 1446 _currentToken = firstToken; |
| 1447 List<XmlTagNode> tagNodes = new List<XmlTagNode>(); |
| 1448 TokenType type = _currentToken.type; |
| 1449 while (type != TokenType.EOF) { |
| 1450 if (type == TokenType.LT) { |
| 1451 tagNodes.add(_parseTagNode()); |
| 1452 } else if (type == TokenType.DECLARATION || |
| 1453 type == TokenType.DIRECTIVE || |
| 1454 type == TokenType.COMMENT) { |
| 1455 // ignored tokens |
| 1456 _currentToken = _currentToken.next; |
| 1457 } else { |
| 1458 _reportUnexpectedToken(); |
| 1459 _currentToken = _currentToken.next; |
| 1460 } |
| 1461 type = _currentToken.type; |
| 1462 } |
| 1463 return tagNodes; |
| 1464 } |
| 1465 |
| 1466 /** |
| 1467 * Insert a synthetic token of the specified type before the current token |
| 1468 * |
| 1469 * @param type the type of token to be inserted (not `null`) |
| 1470 * @return the synthetic token that was inserted (not `null`) |
| 1471 */ |
| 1472 Token _insertSyntheticToken(TokenType type) { |
| 1473 Token token = new Token.con2(type, _currentToken.offset, ""); |
| 1474 _currentToken.previous.setNext(token); |
| 1475 token.setNext(_currentToken); |
| 1476 return token; |
| 1477 } |
| 1478 |
| 1479 /** |
| 1480 * Parse the token stream for an attribute. This method advances the current t
oken over the |
| 1481 * attribute, but should not be called if the [currentToken] is not [TokenType
.TAG]. |
| 1482 * |
| 1483 * @return the attribute (not `null`) |
| 1484 */ |
| 1485 XmlAttributeNode _parseAttribute() { |
| 1486 // Assume the current token is a tag |
| 1487 Token name = _currentToken; |
| 1488 _currentToken = _currentToken.next; |
| 1489 // Equals sign |
| 1490 Token equals; |
| 1491 if (_currentToken.type == TokenType.EQ) { |
| 1492 equals = _currentToken; |
| 1493 _currentToken = _currentToken.next; |
| 1494 } else { |
| 1495 _reportUnexpectedToken(); |
| 1496 equals = _insertSyntheticToken(TokenType.EQ); |
| 1497 } |
| 1498 // String value |
| 1499 Token value; |
| 1500 if (_currentToken.type == TokenType.STRING) { |
| 1501 value = _currentToken; |
| 1502 _currentToken = _currentToken.next; |
| 1503 } else { |
| 1504 _reportUnexpectedToken(); |
| 1505 value = _insertSyntheticToken(TokenType.STRING); |
| 1506 } |
| 1507 return createAttributeNode(name, equals, value); |
| 1508 } |
| 1509 |
| 1510 /** |
| 1511 * Parse the stream for a sequence of attributes. This method advances the cur
rent token to the |
| 1512 * next [TokenType.GT], [TokenType.SLASH_GT], or [TokenType.EOF]. |
| 1513 * |
| 1514 * @return a collection of zero or more attributes (not `null`, contains no `n
ull`s) |
| 1515 */ |
| 1516 List<XmlAttributeNode> _parseAttributes() { |
| 1517 TokenType type = _currentToken.type; |
| 1518 if (type == TokenType.GT || |
| 1519 type == TokenType.SLASH_GT || |
| 1520 type == TokenType.EOF) { |
| 1521 return XmlTagNode.NO_ATTRIBUTES; |
| 1522 } |
| 1523 List<XmlAttributeNode> attributes = new List<XmlAttributeNode>(); |
| 1524 while (type != TokenType.GT && |
| 1525 type != TokenType.SLASH_GT && |
| 1526 type != TokenType.EOF) { |
| 1527 if (type == TokenType.TAG) { |
| 1528 attributes.add(_parseAttribute()); |
| 1529 } else { |
| 1530 _reportUnexpectedToken(); |
| 1531 _currentToken = _currentToken.next; |
| 1532 } |
| 1533 type = _currentToken.type; |
| 1534 } |
| 1535 return attributes; |
| 1536 } |
| 1537 |
| 1538 /** |
| 1539 * Parse the stream for a sequence of tag nodes existing within a parent tag n
ode. This method |
| 1540 * advances the current token to the next [TokenType.LT_SLASH] or [TokenType.E
OF]. |
| 1541 * |
| 1542 * @return a list of nodes (not `null`, contains no `null`s) |
| 1543 */ |
| 1544 List<XmlTagNode> _parseChildTagNodes() { |
| 1545 TokenType type = _currentToken.type; |
| 1546 if (type == TokenType.LT_SLASH || type == TokenType.EOF) { |
| 1547 return XmlTagNode.NO_TAG_NODES; |
| 1548 } |
| 1549 List<XmlTagNode> nodes = new List<XmlTagNode>(); |
| 1550 while (type != TokenType.LT_SLASH && type != TokenType.EOF) { |
| 1551 if (type == TokenType.LT) { |
| 1552 nodes.add(_parseTagNode()); |
| 1553 } else if (type == TokenType.COMMENT) { |
| 1554 // ignored token |
| 1555 _currentToken = _currentToken.next; |
| 1556 } else { |
| 1557 _reportUnexpectedToken(); |
| 1558 _currentToken = _currentToken.next; |
| 1559 } |
| 1560 type = _currentToken.type; |
| 1561 } |
| 1562 return nodes; |
| 1563 } |
| 1564 |
| 1565 /** |
| 1566 * Parse the token stream for the next tag node. This method advances current
token over the |
| 1567 * parsed tag node, but should only be called if the current token is [TokenTy
pe.LT] |
| 1568 * |
| 1569 * @return the tag node or `null` if none found |
| 1570 */ |
| 1571 XmlTagNode _parseTagNode() { |
| 1572 // Assume that the current node is a tag node start TokenType.LT |
| 1573 Token nodeStart = _currentToken; |
| 1574 _currentToken = _currentToken.next; |
| 1575 // Get the tag or create a synthetic tag and report an error |
| 1576 Token tag; |
| 1577 if (_currentToken.type == TokenType.TAG) { |
| 1578 tag = _currentToken; |
| 1579 _currentToken = _currentToken.next; |
| 1580 } else { |
| 1581 _reportUnexpectedToken(); |
| 1582 tag = _insertSyntheticToken(TokenType.TAG); |
| 1583 } |
| 1584 // Parse the attributes |
| 1585 List<XmlAttributeNode> attributes = _parseAttributes(); |
| 1586 // Token ending attribute list |
| 1587 Token attributeEnd; |
| 1588 if (_currentToken.type == TokenType.GT || |
| 1589 _currentToken.type == TokenType.SLASH_GT) { |
| 1590 attributeEnd = _currentToken; |
| 1591 _currentToken = _currentToken.next; |
| 1592 } else { |
| 1593 _reportUnexpectedToken(); |
| 1594 attributeEnd = _insertSyntheticToken(TokenType.SLASH_GT); |
| 1595 } |
| 1596 // If the node has no children, then return the node |
| 1597 if (attributeEnd.type == TokenType.SLASH_GT || isSelfClosing(tag)) { |
| 1598 return createTagNode(nodeStart, tag, attributes, attributeEnd, |
| 1599 XmlTagNode.NO_TAG_NODES, _currentToken, null, attributeEnd); |
| 1600 } |
| 1601 // Parse the child tag nodes |
| 1602 List<XmlTagNode> tagNodes = _parseChildTagNodes(); |
| 1603 // Token ending child tag nodes |
| 1604 Token contentEnd; |
| 1605 if (_currentToken.type == TokenType.LT_SLASH) { |
| 1606 contentEnd = _currentToken; |
| 1607 _currentToken = _currentToken.next; |
| 1608 } else { |
| 1609 // TODO (danrubel): handle self closing HTML elements by inserting |
| 1610 // synthetic tokens but not reporting an error |
| 1611 _reportUnexpectedToken(); |
| 1612 contentEnd = _insertSyntheticToken(TokenType.LT_SLASH); |
| 1613 } |
| 1614 // Closing tag |
| 1615 Token closingTag; |
| 1616 if (_currentToken.type == TokenType.TAG) { |
| 1617 closingTag = _currentToken; |
| 1618 _currentToken = _currentToken.next; |
| 1619 } else { |
| 1620 _reportUnexpectedToken(); |
| 1621 closingTag = _insertSyntheticToken(TokenType.TAG); |
| 1622 } |
| 1623 // Token ending node |
| 1624 Token nodeEnd; |
| 1625 if (_currentToken.type == TokenType.GT) { |
| 1626 nodeEnd = _currentToken; |
| 1627 _currentToken = _currentToken.next; |
| 1628 } else { |
| 1629 _reportUnexpectedToken(); |
| 1630 nodeEnd = _insertSyntheticToken(TokenType.GT); |
| 1631 } |
| 1632 return createTagNode(nodeStart, tag, attributes, attributeEnd, tagNodes, |
| 1633 contentEnd, closingTag, nodeEnd); |
| 1634 } |
| 1635 |
| 1636 /** |
| 1637 * Report the current token as unexpected |
| 1638 */ |
| 1639 void _reportUnexpectedToken() { |
| 1640 // TODO (danrubel): report unexpected token |
| 1641 } |
| 1642 } |
| 1643 |
| 1644 /** |
| 1645 * Instances of `XmlTagNode` represent XML or HTML elements such as `` and |
| 1646 * `<body foo="bar"> ... </body>`. |
| 1647 */ |
| 1648 class XmlTagNode extends XmlNode { |
| 1649 /** |
| 1650 * Constant representing empty list of attributes. |
| 1651 */ |
| 1652 static List<XmlAttributeNode> NO_ATTRIBUTES = |
| 1653 new UnmodifiableListView(new List<XmlAttributeNode>()); |
| 1654 |
| 1655 /** |
| 1656 * Constant representing empty list of tag nodes. |
| 1657 */ |
| 1658 static List<XmlTagNode> NO_TAG_NODES = |
| 1659 new UnmodifiableListView(new List<XmlTagNode>()); |
| 1660 |
| 1661 /** |
| 1662 * The starting [TokenType.LT] token (not `null`). |
| 1663 */ |
| 1664 final Token nodeStart; |
| 1665 |
| 1666 /** |
| 1667 * The [TokenType.TAG] token after the starting '<' (not `null`). |
| 1668 */ |
| 1669 final Token _tag; |
| 1670 |
| 1671 /** |
| 1672 * The attributes contained by the receiver (not `null`, contains no `null`s). |
| 1673 */ |
| 1674 List<XmlAttributeNode> _attributes; |
| 1675 |
| 1676 /** |
| 1677 * The [TokenType.GT] or [TokenType.SLASH_GT] token after the attributes (not |
| 1678 * `null`). The token may be the same token as [nodeEnd] if there are no child |
| 1679 * [tagNodes]. |
| 1680 */ |
| 1681 final Token attributeEnd; |
| 1682 |
| 1683 /** |
| 1684 * The tag nodes contained in the receiver (not `null`, contains no `null`s). |
| 1685 */ |
| 1686 List<XmlTagNode> _tagNodes; |
| 1687 |
| 1688 /** |
| 1689 * The token (not `null`) after the content, which may be |
| 1690 * * (1) [TokenType.LT_SLASH] for nodes with open and close tags, or |
| 1691 * * (2) the [TokenType.LT] nodeStart of the next sibling node if this node is
self |
| 1692 * closing or the attributeEnd is [TokenType.SLASH_GT], or |
| 1693 * * (3) [TokenType.EOF] if the node does not have a closing tag and is the la
st node in |
| 1694 * the stream [TokenType.LT_SLASH] token after the content, or `null` if there
is no |
| 1695 * content and the attributes ended with [TokenType.SLASH_GT]. |
| 1696 */ |
| 1697 final Token contentEnd; |
| 1698 |
| 1699 /** |
| 1700 * The closing [TokenType.TAG] after the child elements or `null` if there is
no |
| 1701 * content and the attributes ended with [TokenType.SLASH_GT] |
| 1702 */ |
| 1703 final Token closingTag; |
| 1704 |
| 1705 /** |
| 1706 * The ending [TokenType.GT] or [TokenType.SLASH_GT] token (not `null`). |
| 1707 */ |
| 1708 final Token nodeEnd; |
| 1709 |
| 1710 /** |
| 1711 * The expressions that are embedded in the tag's content. |
| 1712 */ |
| 1713 List<XmlExpression> expressions = XmlExpression.EMPTY_ARRAY; |
| 1714 |
| 1715 /** |
| 1716 * Construct a new instance representing an XML or HTML element |
| 1717 * |
| 1718 * @param nodeStart the starting [TokenType.LT] token (not `null`) |
| 1719 * @param tag the [TokenType.TAG] token after the starting '<' (not `null`)
. |
| 1720 * @param attributes the attributes associated with this element or [NO_ATTRIB
UTES] (not |
| 1721 * `null`, contains no `null`s) |
| 1722 * @param attributeEnd The [TokenType.GT] or [TokenType.SLASH_GT] token after
the |
| 1723 * attributes (not `null`). The token may be the same token as [nodeE
nd] if |
| 1724 * there are no child [tagNodes]. |
| 1725 * @param tagNodes child tag nodes of the receiver or [NO_TAG_NODES] (not `nul
l`, |
| 1726 * contains no `null`s) |
| 1727 * @param contentEnd the token (not `null`) after the content, which may be |
| 1728 * * (1) [TokenType.LT_SLASH] for nodes with open and close tags, or |
| 1729 * * (2) the [TokenType.LT] nodeStart of the next sibling node if thi
s node is |
| 1730 * self closing or the attributeEnd is [TokenType.SLASH_GT], or |
| 1731 * * (3) [TokenType.EOF] if the node does not have a closing tag and
is the last |
| 1732 * node in the stream [TokenType.LT_SLASH] token after the content, o
r `null` |
| 1733 * if there is no content and the attributes ended with [TokenType.SL
ASH_GT]. |
| 1734 * @param closingTag the closing [TokenType.TAG] after the child elements or `
null` if |
| 1735 * there is no content and the attributes ended with [TokenType.SLASH
_GT] |
| 1736 * @param nodeEnd the ending [TokenType.GT] or [TokenType.SLASH_GT] token (not |
| 1737 * `null`) |
| 1738 */ |
| 1739 XmlTagNode(this.nodeStart, this._tag, List<XmlAttributeNode> attributes, |
| 1740 this.attributeEnd, List<XmlTagNode> tagNodes, this.contentEnd, |
| 1741 this.closingTag, this.nodeEnd) { |
| 1742 this._attributes = becomeParentOfAll(attributes, ifEmpty: NO_ATTRIBUTES); |
| 1743 this._tagNodes = becomeParentOfAll(tagNodes, ifEmpty: NO_TAG_NODES); |
| 1744 } |
| 1745 |
| 1746 /** |
| 1747 * Answer the receiver's attributes. Callers should not manipulate the returne
d list to edit the |
| 1748 * AST structure. |
| 1749 * |
| 1750 * @return the attributes (not `null`, contains no `null`s) |
| 1751 */ |
| 1752 List<XmlAttributeNode> get attributes => _attributes; |
| 1753 |
| 1754 @override |
| 1755 Token get beginToken => nodeStart; |
| 1756 |
| 1757 /** |
| 1758 * Return a string representing the content contained in the receiver. This |
| 1759 * includes the textual representation of any child tag nodes ([getTagNodes]). |
| 1760 * Whitespace between '<', '</', and '>', '/>' is discarded, but all |
| 1761 * other whitespace is preserved. |
| 1762 */ |
| 1763 String get content { |
| 1764 Token token = attributeEnd.next; |
| 1765 if (identical(token, contentEnd)) { |
| 1766 return ""; |
| 1767 } |
| 1768 // TODO(danrubel) Handle CDATA and replace HTML character encodings with |
| 1769 // the actual characters. |
| 1770 String content = token.lexeme; |
| 1771 token = token.next; |
| 1772 if (identical(token, contentEnd)) { |
| 1773 return content; |
| 1774 } |
| 1775 StringBuffer buffer = new StringBuffer(); |
| 1776 buffer.write(content); |
| 1777 while (!identical(token, contentEnd)) { |
| 1778 buffer.write(token.lexeme); |
| 1779 token = token.next; |
| 1780 } |
| 1781 return buffer.toString(); |
| 1782 } |
| 1783 |
| 1784 @override |
| 1785 Token get endToken { |
| 1786 if (nodeEnd != null) { |
| 1787 return nodeEnd; |
| 1788 } |
| 1789 if (closingTag != null) { |
| 1790 return closingTag; |
| 1791 } |
| 1792 if (contentEnd != null) { |
| 1793 return contentEnd; |
| 1794 } |
| 1795 if (!_tagNodes.isEmpty) { |
| 1796 return _tagNodes[_tagNodes.length - 1].endToken; |
| 1797 } |
| 1798 if (attributeEnd != null) { |
| 1799 return attributeEnd; |
| 1800 } |
| 1801 if (!_attributes.isEmpty) { |
| 1802 return _attributes[_attributes.length - 1].endToken; |
| 1803 } |
| 1804 return _tag; |
| 1805 } |
| 1806 |
| 1807 /** |
| 1808 * Answer the tag name after the starting '<'. |
| 1809 * |
| 1810 * @return the tag name (not `null`) |
| 1811 */ |
| 1812 String get tag => _tag.lexeme; |
| 1813 |
| 1814 /** |
| 1815 * Answer the tag nodes contained in the receiver. Callers should not manipula
te the returned list |
| 1816 * to edit the AST structure. |
| 1817 * |
| 1818 * @return the children (not `null`, contains no `null`s) |
| 1819 */ |
| 1820 List<XmlTagNode> get tagNodes => _tagNodes; |
| 1821 |
| 1822 /** |
| 1823 * Answer the [TokenType.TAG] token after the starting '<'. |
| 1824 * |
| 1825 * @return the token (not `null`) |
| 1826 */ |
| 1827 Token get tagToken => _tag; |
| 1828 |
| 1829 @override |
| 1830 accept(XmlVisitor visitor) => visitor.visitXmlTagNode(this); |
| 1831 |
| 1832 /** |
| 1833 * Answer the attribute with the specified name. |
| 1834 * |
| 1835 * @param name the attribute name |
| 1836 * @return the attribute or `null` if no matching attribute is found |
| 1837 */ |
| 1838 XmlAttributeNode getAttribute(String name) { |
| 1839 for (XmlAttributeNode attribute in _attributes) { |
| 1840 if (attribute.name == name) { |
| 1841 return attribute; |
| 1842 } |
| 1843 } |
| 1844 return null; |
| 1845 } |
| 1846 |
| 1847 /** |
| 1848 * Find the attribute with the given name (see [getAttribute] and answer the l
exeme |
| 1849 * for the attribute's value token without the leading and trailing quotes (se
e |
| 1850 * [XmlAttributeNode.getText]). |
| 1851 * |
| 1852 * @param name the attribute name |
| 1853 * @return the attribute text or `null` if no matching attribute is found |
| 1854 */ |
| 1855 String getAttributeText(String name) { |
| 1856 XmlAttributeNode attribute = getAttribute(name); |
| 1857 return attribute != null ? attribute.text : null; |
| 1858 } |
| 1859 |
| 1860 @override |
| 1861 void visitChildren(XmlVisitor visitor) { |
| 1862 for (XmlAttributeNode node in _attributes) { |
| 1863 node.accept(visitor); |
| 1864 } |
| 1865 for (XmlTagNode node in _tagNodes) { |
| 1866 node.accept(visitor); |
| 1867 } |
| 1868 } |
| 1869 } |
| 1870 |
| 1871 /** |
| 1872 * The interface `XmlVisitor` defines the behavior of objects that can be used t
o visit an |
| 1873 * [XmlNode] structure. |
| 1874 */ |
| 1875 abstract class XmlVisitor<R> { |
| 1876 R visitHtmlScriptTagNode(HtmlScriptTagNode node); |
| 1877 |
| 1878 R visitHtmlUnit(HtmlUnit htmlUnit); |
| 1879 |
| 1880 R visitXmlAttributeNode(XmlAttributeNode xmlAttributeNode); |
| 1881 |
| 1882 R visitXmlTagNode(XmlTagNode xmlTagNode); |
| 1883 } |
OLD | NEW |