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