Index: mojo/public/dart/third_party/analyzer/lib/src/generated/html.dart |
diff --git a/mojo/public/dart/third_party/analyzer/lib/src/generated/html.dart b/mojo/public/dart/third_party/analyzer/lib/src/generated/html.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a72a1c8064d3d54dc351057af1b01f7c88c87c18 |
--- /dev/null |
+++ b/mojo/public/dart/third_party/analyzer/lib/src/generated/html.dart |
@@ -0,0 +1,1883 @@ |
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library engine.html; |
+ |
+import 'dart:collection'; |
+ |
+import 'ast.dart'; |
+import 'element.dart'; |
+import 'engine.dart' show AnalysisOptions, AnalysisEngine; |
+import 'error.dart' show AnalysisErrorListener; |
+import 'java_core.dart'; |
+import 'java_engine.dart'; |
+import 'parser.dart' show Parser; |
+import 'scanner.dart' as sc show Scanner, SubSequenceReader, Token; |
+import 'source.dart'; |
+ |
+/** |
+ * The abstract class `AbstractScanner` implements a scanner for HTML code. Subclasses are |
+ * required to implement the interface used to access the characters being scanned. |
+ */ |
+abstract class AbstractScanner { |
+ static List<String> _NO_PASS_THROUGH_ELEMENTS = <String>[]; |
+ |
+ /** |
+ * The source being scanned. |
+ */ |
+ final Source source; |
+ |
+ /** |
+ * The token pointing to the head of the linked list of tokens. |
+ */ |
+ Token _tokens; |
+ |
+ /** |
+ * The last token that was scanned. |
+ */ |
+ Token _tail; |
+ |
+ /** |
+ * A list containing the offsets of the first character of each line in the source code. |
+ */ |
+ List<int> _lineStarts = new List<int>(); |
+ |
+ /** |
+ * An array of element tags for which the content between tags should be consider a single token. |
+ */ |
+ List<String> _passThroughElements = _NO_PASS_THROUGH_ELEMENTS; |
+ |
+ /** |
+ * Initialize a newly created scanner. |
+ * |
+ * @param source the source being scanned |
+ */ |
+ AbstractScanner(this.source) { |
+ _tokens = new Token.con1(TokenType.EOF, -1); |
+ _tokens.setNext(_tokens); |
+ _tail = _tokens; |
+ recordStartOfLine(); |
+ } |
+ |
+ /** |
+ * Return an array containing the offsets of the first character of each line in the source code. |
+ * |
+ * @return an array containing the offsets of the first character of each line in the source code |
+ */ |
+ List<int> get lineStarts => _lineStarts; |
+ |
+ /** |
+ * Return the current offset relative to the beginning of the file. Return the initial offset if |
+ * the scanner has not yet scanned the source code, and one (1) past the end of the source code if |
+ * the source code has been scanned. |
+ * |
+ * @return the current offset of the scanner in the source |
+ */ |
+ int get offset; |
+ |
+ /** |
+ * Set array of element tags for which the content between tags should be consider a single token. |
+ */ |
+ void set passThroughElements(List<String> passThroughElements) { |
+ this._passThroughElements = passThroughElements != null |
+ ? passThroughElements |
+ : _NO_PASS_THROUGH_ELEMENTS; |
+ } |
+ |
+ /** |
+ * Advance the current position and return the character at the new current position. |
+ * |
+ * @return the character at the new current position |
+ */ |
+ int advance(); |
+ |
+ /** |
+ * Return the substring of the source code between the start offset and the modified current |
+ * position. The current position is modified by adding the end delta. |
+ * |
+ * @param start the offset to the beginning of the string, relative to the start of the file |
+ * @param endDelta the number of character after the current location to be included in the |
+ * string, or the number of characters before the current location to be excluded if the |
+ * offset is negative |
+ * @return the specified substring of the source code |
+ */ |
+ String getString(int start, int endDelta); |
+ |
+ /** |
+ * Return the character at the current position without changing the current position. |
+ * |
+ * @return the character at the current position |
+ */ |
+ int peek(); |
+ |
+ /** |
+ * Record the fact that we are at the beginning of a new line in the source. |
+ */ |
+ void recordStartOfLine() { |
+ _lineStarts.add(offset); |
+ } |
+ |
+ /** |
+ * Scan the source code to produce a list of tokens representing the source. |
+ * |
+ * @return the first token in the list of tokens that were produced |
+ */ |
+ Token tokenize() { |
+ _scan(); |
+ _appendEofToken(); |
+ return _firstToken(); |
+ } |
+ |
+ void _appendEofToken() { |
+ Token eofToken = new Token.con1(TokenType.EOF, offset); |
+ // The EOF token points to itself so that there is always infinite |
+ // look-ahead. |
+ eofToken.setNext(eofToken); |
+ _tail = _tail.setNext(eofToken); |
+ } |
+ |
+ Token _emit(Token token) { |
+ _tail.setNext(token); |
+ _tail = token; |
+ return token; |
+ } |
+ |
+ Token _emitWithOffset(TokenType type, int start) => |
+ _emit(new Token.con1(type, start)); |
+ |
+ Token _emitWithOffsetAndLength(TokenType type, int start, int count) => |
+ _emit(new Token.con2(type, start, getString(start, count))); |
+ |
+ Token _firstToken() => _tokens.next; |
+ |
+ int _recordStartOfLineAndAdvance(int c) { |
+ if (c == 0xD) { |
+ c = advance(); |
+ if (c == 0xA) { |
+ c = advance(); |
+ } |
+ recordStartOfLine(); |
+ } else if (c == 0xA) { |
+ c = advance(); |
+ recordStartOfLine(); |
+ } else { |
+ c = advance(); |
+ } |
+ return c; |
+ } |
+ |
+ void _scan() { |
+ bool inBrackets = false; |
+ String endPassThrough = null; |
+ int c = advance(); |
+ while (c >= 0) { |
+ int start = offset; |
+ if (c == 0x3C) { |
+ c = advance(); |
+ if (c == 0x21) { |
+ c = advance(); |
+ if (c == 0x2D && peek() == 0x2D) { |
+ // handle a comment |
+ c = advance(); |
+ int dashCount = 1; |
+ while (c >= 0) { |
+ if (c == 0x2D) { |
+ dashCount++; |
+ } else if (c == 0x3E && dashCount >= 2) { |
+ c = advance(); |
+ break; |
+ } else { |
+ dashCount = 0; |
+ } |
+ c = _recordStartOfLineAndAdvance(c); |
+ } |
+ _emitWithOffsetAndLength(TokenType.COMMENT, start, -1); |
+ // Capture <!--> and <!---> as tokens but report an error |
+ if (_tail.length < 7) { |
+ // TODO (danrubel): Report invalid HTML comment |
+ } |
+ } else { |
+ // handle a declaration |
+ while (c >= 0) { |
+ if (c == 0x3E) { |
+ c = advance(); |
+ break; |
+ } |
+ c = _recordStartOfLineAndAdvance(c); |
+ } |
+ _emitWithOffsetAndLength(TokenType.DECLARATION, start, -1); |
+ if (!StringUtilities.endsWithChar(_tail.lexeme, 0x3E)) { |
+ // TODO (danrubel): Report missing '>' in directive |
+ } |
+ } |
+ } else if (c == 0x3F) { |
+ // handle a directive |
+ while (c >= 0) { |
+ if (c == 0x3F) { |
+ c = advance(); |
+ if (c == 0x3E) { |
+ c = advance(); |
+ break; |
+ } |
+ } else { |
+ c = _recordStartOfLineAndAdvance(c); |
+ } |
+ } |
+ _emitWithOffsetAndLength(TokenType.DIRECTIVE, start, -1); |
+ if (_tail.length < 4) { |
+ // TODO (danrubel): Report invalid directive |
+ } |
+ } else if (c == 0x2F) { |
+ _emitWithOffset(TokenType.LT_SLASH, start); |
+ inBrackets = true; |
+ c = advance(); |
+ } else { |
+ inBrackets = true; |
+ _emitWithOffset(TokenType.LT, start); |
+ // ignore whitespace in braces |
+ while (Character.isWhitespace(c)) { |
+ c = _recordStartOfLineAndAdvance(c); |
+ } |
+ // get tag |
+ if (Character.isLetterOrDigit(c)) { |
+ int tagStart = offset; |
+ c = advance(); |
+ while (Character.isLetterOrDigit(c) || c == 0x2D || c == 0x5F) { |
+ c = advance(); |
+ } |
+ _emitWithOffsetAndLength(TokenType.TAG, tagStart, -1); |
+ // check tag against passThrough elements |
+ String tag = _tail.lexeme; |
+ for (String str in _passThroughElements) { |
+ if (str == tag) { |
+ endPassThrough = "</$str>"; |
+ break; |
+ } |
+ } |
+ } |
+ } |
+ } else if (c == 0x3E) { |
+ _emitWithOffset(TokenType.GT, start); |
+ inBrackets = false; |
+ c = advance(); |
+ // if passThrough != null, read until we match it |
+ if (endPassThrough != null) { |
+ bool endFound = false; |
+ int len = endPassThrough.length; |
+ int firstC = endPassThrough.codeUnitAt(0); |
+ int index = 0; |
+ int nextC = firstC; |
+ while (c >= 0) { |
+ if (c == nextC) { |
+ index++; |
+ if (index == len) { |
+ endFound = true; |
+ break; |
+ } |
+ nextC = endPassThrough.codeUnitAt(index); |
+ } else if (c == firstC) { |
+ index = 1; |
+ nextC = endPassThrough.codeUnitAt(1); |
+ } else { |
+ index = 0; |
+ nextC = firstC; |
+ } |
+ c = _recordStartOfLineAndAdvance(c); |
+ } |
+ if (start + 1 < offset) { |
+ if (endFound) { |
+ _emitWithOffsetAndLength(TokenType.TEXT, start + 1, -len); |
+ _emitWithOffset(TokenType.LT_SLASH, offset - len + 1); |
+ _emitWithOffsetAndLength(TokenType.TAG, offset - len + 3, -1); |
+ } else { |
+ _emitWithOffsetAndLength(TokenType.TEXT, start + 1, -1); |
+ } |
+ } |
+ endPassThrough = null; |
+ } |
+ } else if (c == 0x2F && peek() == 0x3E) { |
+ advance(); |
+ _emitWithOffset(TokenType.SLASH_GT, start); |
+ inBrackets = false; |
+ c = advance(); |
+ } else if (!inBrackets) { |
+ c = _recordStartOfLineAndAdvance(c); |
+ while (c != 0x3C && c >= 0) { |
+ c = _recordStartOfLineAndAdvance(c); |
+ } |
+ _emitWithOffsetAndLength(TokenType.TEXT, start, -1); |
+ } else if (c == 0x22 || c == 0x27) { |
+ // read a string |
+ int endQuote = c; |
+ c = advance(); |
+ while (c >= 0) { |
+ if (c == endQuote) { |
+ c = advance(); |
+ break; |
+ } |
+ c = _recordStartOfLineAndAdvance(c); |
+ } |
+ _emitWithOffsetAndLength(TokenType.STRING, start, -1); |
+ } else if (c == 0x3D) { |
+ // a non-char token |
+ _emitWithOffset(TokenType.EQ, start); |
+ c = advance(); |
+ } else if (Character.isWhitespace(c)) { |
+ // ignore whitespace in braces |
+ do { |
+ c = _recordStartOfLineAndAdvance(c); |
+ } while (Character.isWhitespace(c)); |
+ } else if (Character.isLetterOrDigit(c)) { |
+ c = advance(); |
+ while (Character.isLetterOrDigit(c) || c == 0x2D || c == 0x5F) { |
+ c = advance(); |
+ } |
+ _emitWithOffsetAndLength(TokenType.TAG, start, -1); |
+ } else { |
+ // a non-char token |
+ _emitWithOffsetAndLength(TokenType.TEXT, start, 0); |
+ c = advance(); |
+ } |
+ } |
+ } |
+} |
+ |
+/** |
+ * Instances of the class `HtmlParser` are used to parse tokens into a AST structure comprised |
+ * of [XmlNode]s. |
+ */ |
+class HtmlParser extends XmlParser { |
+ static String _APPLICATION_DART_IN_DOUBLE_QUOTES = "\"application/dart\""; |
+ |
+ static String _APPLICATION_DART_IN_SINGLE_QUOTES = "'application/dart'"; |
+ |
+ static String _SCRIPT = "script"; |
+ |
+ static String _TYPE = "type"; |
+ |
+ /** |
+ * A set containing the names of tags that do not have a closing tag. |
+ */ |
+ static Set<String> SELF_CLOSING = new HashSet<String>.from(<String>[ |
+ "area", |
+ "base", |
+ "basefont", |
+ "br", |
+ "col", |
+ "frame", |
+ "hr", |
+ "img", |
+ "input", |
+ "link", |
+ "meta", |
+ "param", |
+ "!" |
+ ]); |
+ |
+ /** |
+ * The line information associated with the source being parsed. |
+ */ |
+ LineInfo _lineInfo; |
+ |
+ /** |
+ * The error listener to which errors will be reported. |
+ */ |
+ final AnalysisErrorListener _errorListener; |
+ |
+ final AnalysisOptions _options; |
+ |
+ /** |
+ * Construct a parser for the specified source. |
+ * |
+ * [source] is the source being parsed. [_errorListener] is the error |
+ * listener to which errors will be reported. [_options] is the analysis |
+ * options which should be used for parsing. |
+ */ |
+ HtmlParser(Source source, this._errorListener, this._options) : super(source); |
+ |
+ @override |
+ XmlAttributeNode createAttributeNode(Token name, Token equals, Token value) => |
+ new XmlAttributeNode(name, equals, value); |
+ |
+ @override |
+ XmlTagNode createTagNode(Token nodeStart, Token tag, |
+ List<XmlAttributeNode> attributes, Token attributeEnd, |
+ List<XmlTagNode> tagNodes, Token contentEnd, Token closingTag, |
+ Token nodeEnd) { |
+ if (_isScriptNode(tag, attributes, tagNodes)) { |
+ HtmlScriptTagNode tagNode = new HtmlScriptTagNode(nodeStart, tag, |
+ attributes, attributeEnd, tagNodes, contentEnd, closingTag, nodeEnd); |
+ String contents = tagNode.content; |
+ int contentOffset = attributeEnd.end; |
+ LineInfo_Location location = _lineInfo.getLocation(contentOffset); |
+ sc.Scanner scanner = new sc.Scanner(source, |
+ new sc.SubSequenceReader(contents, contentOffset), _errorListener); |
+ scanner.setSourceStart(location.lineNumber, location.columnNumber); |
+ sc.Token firstToken = scanner.tokenize(); |
+ Parser parser = new Parser(source, _errorListener); |
+ CompilationUnit unit = parser.parseCompilationUnit(firstToken); |
+ unit.lineInfo = _lineInfo; |
+ tagNode.script = unit; |
+ return tagNode; |
+ } |
+ return new XmlTagNode(nodeStart, tag, attributes, attributeEnd, tagNodes, |
+ contentEnd, closingTag, nodeEnd); |
+ } |
+ |
+ @override |
+ bool isSelfClosing(Token tag) => SELF_CLOSING.contains(tag.lexeme); |
+ |
+ /** |
+ * Parse the given tokens. |
+ * |
+ * @param token the first token in the stream of tokens to be parsed |
+ * @param lineInfo the line information created by the scanner |
+ * @return the parse result (not `null`) |
+ */ |
+ HtmlUnit parse(Token token, LineInfo lineInfo) { |
+ this._lineInfo = lineInfo; |
+ List<XmlTagNode> tagNodes = parseTopTagNodes(token); |
+ return new HtmlUnit(token, tagNodes, currentToken); |
+ } |
+ |
+ /** |
+ * Determine if the specified node is a Dart script. |
+ * |
+ * @param node the node to be tested (not `null`) |
+ * @return `true` if the node is a Dart script |
+ */ |
+ bool _isScriptNode( |
+ Token tag, List<XmlAttributeNode> attributes, List<XmlTagNode> tagNodes) { |
+ if (tagNodes.length != 0 || tag.lexeme != _SCRIPT) { |
+ return false; |
+ } |
+ for (XmlAttributeNode attribute in attributes) { |
+ if (attribute.name == _TYPE) { |
+ Token valueToken = attribute.valueToken; |
+ if (valueToken != null) { |
+ String value = valueToken.lexeme; |
+ if (value == _APPLICATION_DART_IN_DOUBLE_QUOTES || |
+ value == _APPLICATION_DART_IN_SINGLE_QUOTES) { |
+ return true; |
+ } |
+ } |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * Given the contents of an embedded expression that occurs at the given offset, parse it as a |
+ * Dart expression. The contents should not include the expression's delimiters. |
+ * |
+ * @param source the source that contains that given token |
+ * @param token the token to start parsing from |
+ * @return the Dart expression that was parsed |
+ */ |
+ static Expression parseEmbeddedExpression( |
+ Source source, sc.Token token, AnalysisErrorListener errorListener) { |
+ Parser parser = new Parser(source, errorListener); |
+ return parser.parseExpression(token); |
+ } |
+ |
+ /** |
+ * Given the contents of an embedded expression that occurs at the given offset, scans it as a |
+ * Dart code. |
+ * |
+ * @param source the source of that contains the given contents |
+ * @param contents the contents to scan |
+ * @param contentOffset the offset of the contents in the larger file |
+ * @return the first Dart token |
+ */ |
+ static sc.Token scanDartSource(Source source, LineInfo lineInfo, |
+ String contents, int contentOffset, AnalysisErrorListener errorListener) { |
+ LineInfo_Location location = lineInfo.getLocation(contentOffset); |
+ sc.Scanner scanner = new sc.Scanner(source, |
+ new sc.SubSequenceReader(contents, contentOffset), errorListener); |
+ scanner.setSourceStart(location.lineNumber, location.columnNumber); |
+ return scanner.tokenize(); |
+ } |
+} |
+ |
+/** |
+ * Instances of the class `HtmlScriptTagNode` represent a script tag within an HTML file that |
+ * references a Dart script. |
+ */ |
+@deprecated |
+class HtmlScriptTagNode extends XmlTagNode { |
+ /** |
+ * The AST structure representing the Dart code within this tag. |
+ */ |
+ CompilationUnit _script; |
+ |
+ /** |
+ * The element representing this script. |
+ */ |
+ HtmlScriptElement scriptElement; |
+ |
+ /** |
+ * Initialize a newly created node to represent a script tag within an HTML file that references a |
+ * Dart script. |
+ * |
+ * @param nodeStart the token marking the beginning of the tag |
+ * @param tag the name of the tag |
+ * @param attributes the attributes in the tag |
+ * @param attributeEnd the token terminating the region where attributes can be |
+ * @param tagNodes the children of the tag |
+ * @param contentEnd the token that starts the closing tag |
+ * @param closingTag the name of the tag that occurs in the closing tag |
+ * @param nodeEnd the last token in the tag |
+ */ |
+ HtmlScriptTagNode(Token nodeStart, Token tag, |
+ List<XmlAttributeNode> attributes, Token attributeEnd, |
+ List<XmlTagNode> tagNodes, Token contentEnd, Token closingTag, |
+ Token nodeEnd) |
+ : super(nodeStart, tag, attributes, attributeEnd, tagNodes, contentEnd, |
+ closingTag, nodeEnd); |
+ |
+ /** |
+ * Return the AST structure representing the Dart code within this tag, or `null` if this |
+ * tag references an external script. |
+ * |
+ * @return the AST structure representing the Dart code within this tag |
+ */ |
+ CompilationUnit get script => _script; |
+ |
+ /** |
+ * Set the AST structure representing the Dart code within this tag to the given compilation unit. |
+ * |
+ * @param unit the AST structure representing the Dart code within this tag |
+ */ |
+ void set script(CompilationUnit unit) { |
+ _script = unit; |
+ } |
+ |
+ @override |
+ accept(XmlVisitor visitor) => visitor.visitHtmlScriptTagNode(this); |
+} |
+ |
+/** |
+ * Instances of the class `HtmlUnit` represent the contents of an HTML file. |
+ */ |
+@deprecated |
+class HtmlUnit extends XmlNode { |
+ /** |
+ * The first token in the token stream that was parsed to form this HTML unit. |
+ */ |
+ final Token beginToken; |
+ |
+ /** |
+ * The last token in the token stream that was parsed to form this compilation unit. This token |
+ * should always have a type of [TokenType.EOF]. |
+ */ |
+ final Token endToken; |
+ |
+ /** |
+ * The tag nodes contained in the receiver (not `null`, contains no `null`s). |
+ */ |
+ List<XmlTagNode> _tagNodes; |
+ |
+ /** |
+ * Construct a new instance representing the content of an HTML file. |
+ * |
+ * @param beginToken the first token in the file (not `null`) |
+ * @param tagNodes child tag nodes of the receiver (not `null`, contains no `null`s) |
+ * @param endToken the last token in the token stream which should be of type |
+ * [TokenType.EOF] |
+ */ |
+ HtmlUnit(this.beginToken, List<XmlTagNode> tagNodes, this.endToken) { |
+ this._tagNodes = becomeParentOfAll(tagNodes); |
+ } |
+ |
+ /** |
+ * Return the element associated with this HTML unit. |
+ * |
+ * @return the element or `null` if the receiver is not resolved |
+ */ |
+ @override |
+ HtmlElement get element => super.element as HtmlElement; |
+ |
+ @override |
+ void set element(Element element) { |
+ if (element != null && element is! HtmlElement) { |
+ throw new IllegalArgumentException( |
+ "HtmlElement expected, but ${element.runtimeType} given"); |
+ } |
+ super.element = element; |
+ } |
+ |
+ /** |
+ * Answer the tag nodes contained in the receiver. Callers should not manipulate the returned list |
+ * to edit the AST structure. |
+ * |
+ * @return the children (not `null`, contains no `null`s) |
+ */ |
+ List<XmlTagNode> get tagNodes => _tagNodes; |
+ |
+ @override |
+ accept(XmlVisitor visitor) => visitor.visitHtmlUnit(this); |
+ |
+ @override |
+ void visitChildren(XmlVisitor visitor) { |
+ for (XmlTagNode node in _tagNodes) { |
+ node.accept(visitor); |
+ } |
+ } |
+} |
+ |
+/** |
+ * Instances of the class `RecursiveXmlVisitor` implement an XML visitor that will recursively |
+ * visit all of the nodes in an XML structure. For example, using an instance of this class to visit |
+ * a [XmlTagNode] will also cause all of the contained [XmlAttributeNode]s and |
+ * [XmlTagNode]s to be visited. |
+ * |
+ * Subclasses that override a visit method must either invoke the overridden visit method or must |
+ * explicitly ask the visited node to visit its children. Failure to do so will cause the children |
+ * of the visited node to not be visited. |
+ */ |
+class RecursiveXmlVisitor<R> implements XmlVisitor<R> { |
+ @override |
+ R visitHtmlScriptTagNode(HtmlScriptTagNode node) { |
+ node.visitChildren(this); |
+ return null; |
+ } |
+ |
+ @override |
+ R visitHtmlUnit(HtmlUnit node) { |
+ node.visitChildren(this); |
+ return null; |
+ } |
+ |
+ @override |
+ R visitXmlAttributeNode(XmlAttributeNode node) { |
+ node.visitChildren(this); |
+ return null; |
+ } |
+ |
+ @override |
+ R visitXmlTagNode(XmlTagNode node) { |
+ node.visitChildren(this); |
+ return null; |
+ } |
+} |
+ |
+/** |
+ * Instances of the class `SimpleXmlVisitor` implement an AST visitor that will do nothing |
+ * when visiting an AST node. It is intended to be a superclass for classes that use the visitor |
+ * pattern primarily as a dispatch mechanism (and hence don't need to recursively visit a whole |
+ * structure) and that only need to visit a small number of node types. |
+ */ |
+class SimpleXmlVisitor<R> implements XmlVisitor<R> { |
+ @override |
+ R visitHtmlScriptTagNode(HtmlScriptTagNode node) => null; |
+ |
+ @override |
+ R visitHtmlUnit(HtmlUnit htmlUnit) => null; |
+ |
+ @override |
+ R visitXmlAttributeNode(XmlAttributeNode xmlAttributeNode) => null; |
+ |
+ @override |
+ R visitXmlTagNode(XmlTagNode xmlTagNode) => null; |
+} |
+ |
+/** |
+ * Instances of the class `StringScanner` implement a scanner that reads from a string. The |
+ * scanning logic is in the superclass. |
+ */ |
+class StringScanner extends AbstractScanner { |
+ /** |
+ * The string from which characters will be read. |
+ */ |
+ final String _string; |
+ |
+ /** |
+ * The number of characters in the string. |
+ */ |
+ int _stringLength = 0; |
+ |
+ /** |
+ * The index, relative to the string, of the last character that was read. |
+ */ |
+ int _charOffset = 0; |
+ |
+ /** |
+ * Initialize a newly created scanner to scan the characters in the given string. |
+ * |
+ * @param source the source being scanned |
+ * @param string the string from which characters will be read |
+ */ |
+ StringScanner(Source source, this._string) : super(source) { |
+ this._stringLength = _string.length; |
+ this._charOffset = -1; |
+ } |
+ |
+ @override |
+ int get offset => _charOffset; |
+ |
+ void set offset(int offset) { |
+ _charOffset = offset; |
+ } |
+ |
+ @override |
+ int advance() { |
+ if (++_charOffset < _stringLength) { |
+ return _string.codeUnitAt(_charOffset); |
+ } |
+ _charOffset = _stringLength; |
+ return -1; |
+ } |
+ |
+ @override |
+ String getString(int start, int endDelta) => |
+ _string.substring(start, _charOffset + 1 + endDelta).toString(); |
+ |
+ @override |
+ int peek() { |
+ if (_charOffset + 1 < _stringLength) { |
+ return _string.codeUnitAt(_charOffset + 1); |
+ } |
+ return -1; |
+ } |
+} |
+ |
+/** |
+ * Instances of the class `Token` represent a token that was scanned from the input. Each |
+ * token knows which token follows it, acting as the head of a linked list of tokens. |
+ */ |
+class Token { |
+ /** |
+ * The offset from the beginning of the file to the first character in the token. |
+ */ |
+ final int offset; |
+ |
+ /** |
+ * The previous token in the token stream. |
+ */ |
+ Token previous; |
+ |
+ /** |
+ * The next token in the token stream. |
+ */ |
+ Token _next; |
+ |
+ /** |
+ * The type of the token. |
+ */ |
+ final TokenType type; |
+ |
+ /** |
+ * The lexeme represented by this token. |
+ */ |
+ String _value; |
+ |
+ /** |
+ * Initialize a newly created token. |
+ * |
+ * @param type the token type (not `null`) |
+ * @param offset the offset from the beginning of the file to the first character in the token |
+ */ |
+ Token.con1(TokenType type, int offset) : this.con2(type, offset, type.lexeme); |
+ |
+ /** |
+ * Initialize a newly created token. |
+ * |
+ * @param type the token type (not `null`) |
+ * @param offset the offset from the beginning of the file to the first character in the token |
+ * @param value the lexeme represented by this token (not `null`) |
+ */ |
+ Token.con2(this.type, this.offset, String value) { |
+ this._value = StringUtilities.intern(value); |
+ } |
+ |
+ /** |
+ * Return the offset from the beginning of the file to the character after last character of the |
+ * token. |
+ * |
+ * @return the offset from the beginning of the file to the first character after last character |
+ * of the token |
+ */ |
+ int get end => offset + length; |
+ |
+ /** |
+ * Return `true` if this token is a synthetic token. A synthetic token is a token that was |
+ * introduced by the parser in order to recover from an error in the code. Synthetic tokens always |
+ * have a length of zero (`0`). |
+ * |
+ * @return `true` if this token is a synthetic token |
+ */ |
+ bool get isSynthetic => length == 0; |
+ |
+ /** |
+ * Return the number of characters in the node's source range. |
+ * |
+ * @return the number of characters in the node's source range |
+ */ |
+ int get length => lexeme.length; |
+ |
+ /** |
+ * Return the lexeme that represents this token. |
+ * |
+ * @return the lexeme (not `null`) |
+ */ |
+ String get lexeme => _value; |
+ |
+ /** |
+ * Return the next token in the token stream. |
+ * |
+ * @return the next token in the token stream |
+ */ |
+ Token get next => _next; |
+ |
+ /** |
+ * Set the next token in the token stream to the given token. This has the side-effect of setting |
+ * this token to be the previous token for the given token. |
+ * |
+ * @param token the next token in the token stream |
+ * @return the token that was passed in |
+ */ |
+ Token setNext(Token token) { |
+ _next = token; |
+ token.previous = this; |
+ return token; |
+ } |
+ |
+ @override |
+ String toString() => lexeme; |
+} |
+ |
+/** |
+ * The enumeration `TokenType` defines the types of tokens that can be returned by the |
+ * scanner. |
+ */ |
+class TokenType extends Enum<TokenType> { |
+ /** |
+ * The type of the token that marks the end of the input. |
+ */ |
+ static const TokenType EOF = const TokenType_EOF('EOF', 0, ""); |
+ |
+ static const TokenType EQ = const TokenType('EQ', 1, "="); |
+ |
+ static const TokenType GT = const TokenType('GT', 2, ">"); |
+ |
+ static const TokenType LT_SLASH = const TokenType('LT_SLASH', 3, "</"); |
+ |
+ static const TokenType LT = const TokenType('LT', 4, "<"); |
+ |
+ static const TokenType SLASH_GT = const TokenType('SLASH_GT', 5, "/>"); |
+ |
+ static const TokenType COMMENT = const TokenType('COMMENT', 6, null); |
+ |
+ static const TokenType DECLARATION = const TokenType('DECLARATION', 7, null); |
+ |
+ static const TokenType DIRECTIVE = const TokenType('DIRECTIVE', 8, null); |
+ |
+ static const TokenType STRING = const TokenType('STRING', 9, null); |
+ |
+ static const TokenType TAG = const TokenType('TAG', 10, null); |
+ |
+ static const TokenType TEXT = const TokenType('TEXT', 11, null); |
+ |
+ static const List<TokenType> values = const [ |
+ EOF, |
+ EQ, |
+ GT, |
+ LT_SLASH, |
+ LT, |
+ SLASH_GT, |
+ COMMENT, |
+ DECLARATION, |
+ DIRECTIVE, |
+ STRING, |
+ TAG, |
+ TEXT |
+ ]; |
+ |
+ /** |
+ * The lexeme that defines this type of token, or `null` if there is more than one possible |
+ * lexeme for this type of token. |
+ */ |
+ final String lexeme; |
+ |
+ const TokenType(String name, int ordinal, this.lexeme) : super(name, ordinal); |
+} |
+ |
+class TokenType_EOF extends TokenType { |
+ const TokenType_EOF(String name, int ordinal, String arg0) |
+ : super(name, ordinal, arg0); |
+ |
+ @override |
+ String toString() => "-eof-"; |
+} |
+ |
+/** |
+ * Instances of the class `ToSourceVisitor` write a source representation of a visited XML |
+ * node (and all of it's children) to a writer. |
+ */ |
+class ToSourceVisitor implements XmlVisitor<Object> { |
+ /** |
+ * The writer to which the source is to be written. |
+ */ |
+ final PrintWriter _writer; |
+ |
+ /** |
+ * Initialize a newly created visitor to write source code representing the visited nodes to the |
+ * given writer. |
+ * |
+ * @param writer the writer to which the source is to be written |
+ */ |
+ ToSourceVisitor(this._writer); |
+ |
+ @override |
+ Object visitHtmlScriptTagNode(HtmlScriptTagNode node) => |
+ visitXmlTagNode(node); |
+ |
+ @override |
+ Object visitHtmlUnit(HtmlUnit node) { |
+ for (XmlTagNode child in node.tagNodes) { |
+ _visit(child); |
+ } |
+ return null; |
+ } |
+ |
+ @override |
+ Object visitXmlAttributeNode(XmlAttributeNode node) { |
+ String name = node.name; |
+ Token value = node.valueToken; |
+ if (name.length == 0) { |
+ _writer.print("__"); |
+ } else { |
+ _writer.print(name); |
+ } |
+ _writer.print("="); |
+ if (value == null) { |
+ _writer.print("__"); |
+ } else { |
+ _writer.print(value.lexeme); |
+ } |
+ return null; |
+ } |
+ |
+ @override |
+ Object visitXmlTagNode(XmlTagNode node) { |
+ _writer.print("<"); |
+ String tagName = node.tag; |
+ _writer.print(tagName); |
+ for (XmlAttributeNode attribute in node.attributes) { |
+ _writer.print(" "); |
+ _visit(attribute); |
+ } |
+ _writer.print(node.attributeEnd.lexeme); |
+ if (node.closingTag != null) { |
+ for (XmlTagNode child in node.tagNodes) { |
+ _visit(child); |
+ } |
+ _writer.print("</"); |
+ _writer.print(tagName); |
+ _writer.print(">"); |
+ } |
+ return null; |
+ } |
+ |
+ /** |
+ * Safely visit the given node. |
+ * |
+ * @param node the node to be visited |
+ */ |
+ void _visit(XmlNode node) { |
+ if (node != null) { |
+ node.accept(this); |
+ } |
+ } |
+} |
+ |
+/** |
+ * Instances of `XmlAttributeNode` represent name/value pairs owned by an [XmlTagNode]. |
+ */ |
+class XmlAttributeNode extends XmlNode { |
+ /** |
+ * An empty list of XML attribute nodes. |
+ */ |
+ static const List<XmlAttributeNode> EMPTY_LIST = const <XmlAttributeNode>[]; |
+ |
+ final Token _name; |
+ |
+ final Token equals; |
+ |
+ final Token _value; |
+ |
+ List<XmlExpression> expressions = XmlExpression.EMPTY_ARRAY; |
+ |
+ /** |
+ * Construct a new instance representing an XML attribute. |
+ * |
+ * @param name the name token (not `null`). This may be a zero length token if the attribute |
+ * is badly formed. |
+ * @param equals the equals sign or `null` if none |
+ * @param value the value token (not `null`) |
+ */ |
+ XmlAttributeNode(this._name, this.equals, this._value); |
+ |
+ @override |
+ Token get beginToken => _name; |
+ |
+ @override |
+ Token get endToken => _value; |
+ |
+ /** |
+ * Answer the attribute name. This may be a zero length string if the attribute is badly formed. |
+ * |
+ * @return the name (not `null`) |
+ */ |
+ String get name => _name.lexeme; |
+ |
+ /** |
+ * Answer the attribute name token. This may be a zero length token if the attribute is badly |
+ * formed. |
+ * |
+ * @return the name token (not `null`) |
+ */ |
+ Token get nameToken => _name; |
+ |
+ /** |
+ * Answer the lexeme for the value token without the leading and trailing quotes. |
+ * |
+ * @return the text or `null` if the value is not specified |
+ */ |
+ String get text { |
+ if (_value == null) { |
+ return null; |
+ } |
+ //TODO (danrubel): replace HTML character encodings with the actual |
+ // characters |
+ String text = _value.lexeme; |
+ int len = text.length; |
+ if (len > 0) { |
+ if (text.codeUnitAt(0) == 0x22) { |
+ if (len > 1 && text.codeUnitAt(len - 1) == 0x22) { |
+ return text.substring(1, len - 1); |
+ } else { |
+ return text.substring(1); |
+ } |
+ } else if (text.codeUnitAt(0) == 0x27) { |
+ if (len > 1 && text.codeUnitAt(len - 1) == 0x27) { |
+ return text.substring(1, len - 1); |
+ } else { |
+ return text.substring(1); |
+ } |
+ } |
+ } |
+ return text; |
+ } |
+ |
+ /** |
+ * Answer the offset of the value after the leading quote. |
+ * |
+ * @return the offset of the value, or `-1` if the value is not specified |
+ */ |
+ int get textOffset { |
+ if (_value == null) { |
+ return -1; |
+ } |
+ String text = _value.lexeme; |
+ if (StringUtilities.startsWithChar(text, 0x22) || |
+ StringUtilities.startsWithChar(text, 0x27)) { |
+ return _value.offset + 1; |
+ } |
+ return _value.offset; |
+ } |
+ |
+ /** |
+ * Answer the attribute value token. A properly formed value will start and end with matching |
+ * quote characters, but the value returned may not be properly formed. |
+ * |
+ * @return the value token or `null` if this represents a badly formed attribute |
+ */ |
+ Token get valueToken => _value; |
+ |
+ @override |
+ accept(XmlVisitor visitor) => visitor.visitXmlAttributeNode(this); |
+ |
+ @override |
+ void visitChildren(XmlVisitor visitor) { |
+ // no children to visit |
+ } |
+} |
+ |
+/** |
+ * Instances of the class `XmlExpression` represent an abstract expression embedded into |
+ * [XmlNode]. |
+ */ |
+abstract class XmlExpression { |
+ /** |
+ * An empty list of expressions. |
+ */ |
+ static const List<XmlExpression> EMPTY_ARRAY = const <XmlExpression>[]; |
+ |
+ /** |
+ * Return the offset of the character immediately following the last character of this |
+ * expression's source range. This is equivalent to `getOffset() + getLength()`. |
+ * |
+ * @return the offset of the character just past the expression's source range |
+ */ |
+ int get end; |
+ |
+ /** |
+ * Return the number of characters in the expression's source range. |
+ */ |
+ int get length; |
+ |
+ /** |
+ * Return the offset of the first character in the expression's source range. |
+ */ |
+ int get offset; |
+ |
+ /** |
+ * Check if the given offset belongs to the expression's source range. |
+ */ |
+ bool contains(int offset) => this.offset <= offset && offset < end; |
+ |
+ /** |
+ * Return the [Reference] at the given offset. |
+ * |
+ * @param offset the offset from the beginning of the file |
+ * @return the [Reference] at the given offset, maybe `null` |
+ */ |
+ XmlExpression_Reference getReference(int offset); |
+} |
+ |
+/** |
+ * The reference to the [Element]. |
+ */ |
+class XmlExpression_Reference { |
+ Element element; |
+ |
+ int offset = 0; |
+ |
+ int length = 0; |
+ |
+ XmlExpression_Reference(this.element, this.offset, this.length); |
+} |
+ |
+/** |
+ * The abstract class `XmlNode` defines behavior common to all XML/HTML nodes. |
+ */ |
+abstract class XmlNode { |
+ /** |
+ * The parent of the node, or `null` if the node is the root of an AST structure. |
+ */ |
+ XmlNode _parent; |
+ |
+ /** |
+ * The element associated with this node or `null` if the receiver is not resolved. |
+ */ |
+ Element _element; |
+ |
+ /** |
+ * Return the first token included in this node's source range. |
+ * |
+ * @return the first token or `null` if none |
+ */ |
+ Token get beginToken; |
+ |
+ /** |
+ * Return the element associated with this node. |
+ * |
+ * @return the element or `null` if the receiver is not resolved |
+ */ |
+ Element get element => _element; |
+ |
+ /** |
+ * Set the element associated with this node. |
+ * |
+ * @param element the element |
+ */ |
+ void set element(Element element) { |
+ this._element = element; |
+ } |
+ |
+ /** |
+ * Return the offset of the character immediately following the last character of this node's |
+ * source range. This is equivalent to `node.getOffset() + node.getLength()`. For an html |
+ * unit this will be equal to the length of the unit's source. |
+ * |
+ * @return the offset of the character just past the node's source range |
+ */ |
+ int get end => offset + length; |
+ |
+ /** |
+ * Return the last token included in this node's source range. |
+ * |
+ * @return the last token or `null` if none |
+ */ |
+ Token get endToken; |
+ |
+ /** |
+ * Return the number of characters in the node's source range. |
+ * |
+ * @return the number of characters in the node's source range |
+ */ |
+ int get length { |
+ Token beginToken = this.beginToken; |
+ Token endToken = this.endToken; |
+ if (beginToken == null || endToken == null) { |
+ return -1; |
+ } |
+ return endToken.offset + endToken.length - beginToken.offset; |
+ } |
+ |
+ /** |
+ * Return the offset from the beginning of the file to the first character in the node's source |
+ * range. |
+ * |
+ * @return the offset from the beginning of the file to the first character in the node's source |
+ * range |
+ */ |
+ int get offset { |
+ Token beginToken = this.beginToken; |
+ if (beginToken == null) { |
+ return -1; |
+ } |
+ return this.beginToken.offset; |
+ } |
+ |
+ /** |
+ * Return this node's parent node, or `null` if this node is the root of an AST structure. |
+ * |
+ * Note that the relationship between an AST node and its parent node may change over the lifetime |
+ * of a node. |
+ * |
+ * @return the parent of this node, or `null` if none |
+ */ |
+ XmlNode get parent => _parent; |
+ |
+ /** |
+ * Set the parent of this node to the given node. |
+ * |
+ * @param newParent the node that is to be made the parent of this node |
+ */ |
+ void set parent(XmlNode newParent) { |
+ XmlNode current = newParent; |
+ while (current != null) { |
+ if (identical(current, this)) { |
+ AnalysisEngine.instance.logger.logError( |
+ "Circular structure while setting an XML node's parent", |
+ new CaughtException( |
+ new ArgumentError(_buildRecursiveStructureMessage(newParent)), |
+ null)); |
+ return; |
+ } |
+ current = current.parent; |
+ } |
+ _parent = newParent; |
+ } |
+ |
+ /** |
+ * Use the given visitor to visit this node. |
+ * |
+ * @param visitor the visitor that will visit this node |
+ * @return the value returned by the visitor as a result of visiting this node |
+ */ |
+ accept(XmlVisitor visitor); |
+ |
+ /** |
+ * Make this node the parent of the given child node. |
+ * |
+ * @param child the node that will become a child of this node |
+ * @return the node that was made a child of this node |
+ */ |
+ XmlNode becomeParentOf(XmlNode child) { |
+ if (child != null) { |
+ XmlNode node = child; |
+ node.parent = this; |
+ } |
+ return child; |
+ } |
+ |
+ /** |
+ * Make this node the parent of the given child nodes. |
+ * |
+ * @param children the nodes that will become the children of this node |
+ * @param ifEmpty the (empty) nodes to return if "children" is empty |
+ * @return the nodes that were made children of this node |
+ */ |
+ List becomeParentOfAll(List children, {List ifEmpty}) { |
+ if (children == null || children.isEmpty) { |
+ if (ifEmpty != null) { |
+ return ifEmpty; |
+ } |
+ } |
+ if (children != null) { |
+ children.forEach((XmlNode node) { |
+ node.parent = this; |
+ }); |
+ } |
+ return children; |
+ } |
+ |
+ @override |
+ String toString() { |
+ PrintStringWriter writer = new PrintStringWriter(); |
+ accept(new ToSourceVisitor(writer)); |
+ return writer.toString(); |
+ } |
+ |
+ /** |
+ * Use the given visitor to visit all of the children of this node. The children will be visited |
+ * in source order. |
+ * |
+ * @param visitor the visitor that will be used to visit the children of this node |
+ */ |
+ void visitChildren(XmlVisitor visitor); |
+ |
+ /** |
+ * This method exists for debugging purposes only. |
+ */ |
+ void _appendIdentifier(StringBuffer buffer, XmlNode node) { |
+ if (node is XmlTagNode) { |
+ buffer.write(node.tag); |
+ } else if (node is XmlAttributeNode) { |
+ buffer.write(node.name); |
+ } else { |
+ buffer.write("htmlUnit"); |
+ } |
+ } |
+ |
+ /** |
+ * This method exists for debugging purposes only. |
+ */ |
+ String _buildRecursiveStructureMessage(XmlNode newParent) { |
+ StringBuffer buffer = new StringBuffer(); |
+ buffer.write("Attempt to create recursive structure: "); |
+ XmlNode current = newParent; |
+ while (current != null) { |
+ if (!identical(current, newParent)) { |
+ buffer.write(" -> "); |
+ } |
+ if (identical(current, this)) { |
+ buffer.writeCharCode(0x2A); |
+ _appendIdentifier(buffer, current); |
+ buffer.writeCharCode(0x2A); |
+ } else { |
+ _appendIdentifier(buffer, current); |
+ } |
+ current = current.parent; |
+ } |
+ return buffer.toString(); |
+ } |
+} |
+ |
+/** |
+ * Instances of the class `XmlParser` are used to parse tokens into a AST structure comprised |
+ * of [XmlNode]s. |
+ */ |
+class XmlParser { |
+ /** |
+ * The source being parsed. |
+ */ |
+ final Source source; |
+ |
+ /** |
+ * The next token to be parsed. |
+ */ |
+ Token _currentToken; |
+ |
+ /** |
+ * Construct a parser for the specified source. |
+ * |
+ * @param source the source being parsed |
+ */ |
+ XmlParser(this.source); |
+ |
+ /** |
+ * Answer the current token. |
+ * |
+ * @return the current token |
+ */ |
+ Token get currentToken => _currentToken; |
+ |
+ /** |
+ * Create a node representing an attribute. |
+ * |
+ * @param name the name of the attribute |
+ * @param equals the equals sign, or `null` if there is no value |
+ * @param value the value of the attribute |
+ * @return the node that was created |
+ */ |
+ XmlAttributeNode createAttributeNode(Token name, Token equals, Token value) => |
+ new XmlAttributeNode(name, equals, value); |
+ |
+ /** |
+ * Create a node representing a tag. |
+ * |
+ * @param nodeStart the token marking the beginning of the tag |
+ * @param tag the name of the tag |
+ * @param attributes the attributes in the tag |
+ * @param attributeEnd the token terminating the region where attributes can be |
+ * @param tagNodes the children of the tag |
+ * @param contentEnd the token that starts the closing tag |
+ * @param closingTag the name of the tag that occurs in the closing tag |
+ * @param nodeEnd the last token in the tag |
+ * @return the node that was created |
+ */ |
+ XmlTagNode createTagNode(Token nodeStart, Token tag, |
+ List<XmlAttributeNode> attributes, Token attributeEnd, |
+ List<XmlTagNode> tagNodes, Token contentEnd, Token closingTag, |
+ Token nodeEnd) => new XmlTagNode(nodeStart, tag, attributes, attributeEnd, |
+ tagNodes, contentEnd, closingTag, nodeEnd); |
+ |
+ /** |
+ * Answer `true` if the specified tag is self closing and thus should never have content or |
+ * child tag nodes. |
+ * |
+ * @param tag the tag (not `null`) |
+ * @return `true` if self closing |
+ */ |
+ bool isSelfClosing(Token tag) => false; |
+ |
+ /** |
+ * Parse the entire token stream and in the process, advance the current token to the end of the |
+ * token stream. |
+ * |
+ * @return the list of tag nodes found (not `null`, contains no `null`) |
+ */ |
+ List<XmlTagNode> parseTopTagNodes(Token firstToken) { |
+ _currentToken = firstToken; |
+ List<XmlTagNode> tagNodes = new List<XmlTagNode>(); |
+ TokenType type = _currentToken.type; |
+ while (type != TokenType.EOF) { |
+ if (type == TokenType.LT) { |
+ tagNodes.add(_parseTagNode()); |
+ } else if (type == TokenType.DECLARATION || |
+ type == TokenType.DIRECTIVE || |
+ type == TokenType.COMMENT) { |
+ // ignored tokens |
+ _currentToken = _currentToken.next; |
+ } else { |
+ _reportUnexpectedToken(); |
+ _currentToken = _currentToken.next; |
+ } |
+ type = _currentToken.type; |
+ } |
+ return tagNodes; |
+ } |
+ |
+ /** |
+ * Insert a synthetic token of the specified type before the current token |
+ * |
+ * @param type the type of token to be inserted (not `null`) |
+ * @return the synthetic token that was inserted (not `null`) |
+ */ |
+ Token _insertSyntheticToken(TokenType type) { |
+ Token token = new Token.con2(type, _currentToken.offset, ""); |
+ _currentToken.previous.setNext(token); |
+ token.setNext(_currentToken); |
+ return token; |
+ } |
+ |
+ /** |
+ * Parse the token stream for an attribute. This method advances the current token over the |
+ * attribute, but should not be called if the [currentToken] is not [TokenType.TAG]. |
+ * |
+ * @return the attribute (not `null`) |
+ */ |
+ XmlAttributeNode _parseAttribute() { |
+ // Assume the current token is a tag |
+ Token name = _currentToken; |
+ _currentToken = _currentToken.next; |
+ // Equals sign |
+ Token equals; |
+ if (_currentToken.type == TokenType.EQ) { |
+ equals = _currentToken; |
+ _currentToken = _currentToken.next; |
+ } else { |
+ _reportUnexpectedToken(); |
+ equals = _insertSyntheticToken(TokenType.EQ); |
+ } |
+ // String value |
+ Token value; |
+ if (_currentToken.type == TokenType.STRING) { |
+ value = _currentToken; |
+ _currentToken = _currentToken.next; |
+ } else { |
+ _reportUnexpectedToken(); |
+ value = _insertSyntheticToken(TokenType.STRING); |
+ } |
+ return createAttributeNode(name, equals, value); |
+ } |
+ |
+ /** |
+ * Parse the stream for a sequence of attributes. This method advances the current token to the |
+ * next [TokenType.GT], [TokenType.SLASH_GT], or [TokenType.EOF]. |
+ * |
+ * @return a collection of zero or more attributes (not `null`, contains no `null`s) |
+ */ |
+ List<XmlAttributeNode> _parseAttributes() { |
+ TokenType type = _currentToken.type; |
+ if (type == TokenType.GT || |
+ type == TokenType.SLASH_GT || |
+ type == TokenType.EOF) { |
+ return XmlTagNode.NO_ATTRIBUTES; |
+ } |
+ List<XmlAttributeNode> attributes = new List<XmlAttributeNode>(); |
+ while (type != TokenType.GT && |
+ type != TokenType.SLASH_GT && |
+ type != TokenType.EOF) { |
+ if (type == TokenType.TAG) { |
+ attributes.add(_parseAttribute()); |
+ } else { |
+ _reportUnexpectedToken(); |
+ _currentToken = _currentToken.next; |
+ } |
+ type = _currentToken.type; |
+ } |
+ return attributes; |
+ } |
+ |
+ /** |
+ * Parse the stream for a sequence of tag nodes existing within a parent tag node. This method |
+ * advances the current token to the next [TokenType.LT_SLASH] or [TokenType.EOF]. |
+ * |
+ * @return a list of nodes (not `null`, contains no `null`s) |
+ */ |
+ List<XmlTagNode> _parseChildTagNodes() { |
+ TokenType type = _currentToken.type; |
+ if (type == TokenType.LT_SLASH || type == TokenType.EOF) { |
+ return XmlTagNode.NO_TAG_NODES; |
+ } |
+ List<XmlTagNode> nodes = new List<XmlTagNode>(); |
+ while (type != TokenType.LT_SLASH && type != TokenType.EOF) { |
+ if (type == TokenType.LT) { |
+ nodes.add(_parseTagNode()); |
+ } else if (type == TokenType.COMMENT) { |
+ // ignored token |
+ _currentToken = _currentToken.next; |
+ } else { |
+ _reportUnexpectedToken(); |
+ _currentToken = _currentToken.next; |
+ } |
+ type = _currentToken.type; |
+ } |
+ return nodes; |
+ } |
+ |
+ /** |
+ * Parse the token stream for the next tag node. This method advances current token over the |
+ * parsed tag node, but should only be called if the current token is [TokenType.LT] |
+ * |
+ * @return the tag node or `null` if none found |
+ */ |
+ XmlTagNode _parseTagNode() { |
+ // Assume that the current node is a tag node start TokenType.LT |
+ Token nodeStart = _currentToken; |
+ _currentToken = _currentToken.next; |
+ // Get the tag or create a synthetic tag and report an error |
+ Token tag; |
+ if (_currentToken.type == TokenType.TAG) { |
+ tag = _currentToken; |
+ _currentToken = _currentToken.next; |
+ } else { |
+ _reportUnexpectedToken(); |
+ tag = _insertSyntheticToken(TokenType.TAG); |
+ } |
+ // Parse the attributes |
+ List<XmlAttributeNode> attributes = _parseAttributes(); |
+ // Token ending attribute list |
+ Token attributeEnd; |
+ if (_currentToken.type == TokenType.GT || |
+ _currentToken.type == TokenType.SLASH_GT) { |
+ attributeEnd = _currentToken; |
+ _currentToken = _currentToken.next; |
+ } else { |
+ _reportUnexpectedToken(); |
+ attributeEnd = _insertSyntheticToken(TokenType.SLASH_GT); |
+ } |
+ // If the node has no children, then return the node |
+ if (attributeEnd.type == TokenType.SLASH_GT || isSelfClosing(tag)) { |
+ return createTagNode(nodeStart, tag, attributes, attributeEnd, |
+ XmlTagNode.NO_TAG_NODES, _currentToken, null, attributeEnd); |
+ } |
+ // Parse the child tag nodes |
+ List<XmlTagNode> tagNodes = _parseChildTagNodes(); |
+ // Token ending child tag nodes |
+ Token contentEnd; |
+ if (_currentToken.type == TokenType.LT_SLASH) { |
+ contentEnd = _currentToken; |
+ _currentToken = _currentToken.next; |
+ } else { |
+ // TODO (danrubel): handle self closing HTML elements by inserting |
+ // synthetic tokens but not reporting an error |
+ _reportUnexpectedToken(); |
+ contentEnd = _insertSyntheticToken(TokenType.LT_SLASH); |
+ } |
+ // Closing tag |
+ Token closingTag; |
+ if (_currentToken.type == TokenType.TAG) { |
+ closingTag = _currentToken; |
+ _currentToken = _currentToken.next; |
+ } else { |
+ _reportUnexpectedToken(); |
+ closingTag = _insertSyntheticToken(TokenType.TAG); |
+ } |
+ // Token ending node |
+ Token nodeEnd; |
+ if (_currentToken.type == TokenType.GT) { |
+ nodeEnd = _currentToken; |
+ _currentToken = _currentToken.next; |
+ } else { |
+ _reportUnexpectedToken(); |
+ nodeEnd = _insertSyntheticToken(TokenType.GT); |
+ } |
+ return createTagNode(nodeStart, tag, attributes, attributeEnd, tagNodes, |
+ contentEnd, closingTag, nodeEnd); |
+ } |
+ |
+ /** |
+ * Report the current token as unexpected |
+ */ |
+ void _reportUnexpectedToken() { |
+ // TODO (danrubel): report unexpected token |
+ } |
+} |
+ |
+/** |
+ * Instances of `XmlTagNode` represent XML or HTML elements such as `` and |
+ * `<body foo="bar"> ... </body>`. |
+ */ |
+class XmlTagNode extends XmlNode { |
+ /** |
+ * Constant representing empty list of attributes. |
+ */ |
+ static List<XmlAttributeNode> NO_ATTRIBUTES = |
+ new UnmodifiableListView(new List<XmlAttributeNode>()); |
+ |
+ /** |
+ * Constant representing empty list of tag nodes. |
+ */ |
+ static List<XmlTagNode> NO_TAG_NODES = |
+ new UnmodifiableListView(new List<XmlTagNode>()); |
+ |
+ /** |
+ * The starting [TokenType.LT] token (not `null`). |
+ */ |
+ final Token nodeStart; |
+ |
+ /** |
+ * The [TokenType.TAG] token after the starting '<' (not `null`). |
+ */ |
+ final Token _tag; |
+ |
+ /** |
+ * The attributes contained by the receiver (not `null`, contains no `null`s). |
+ */ |
+ List<XmlAttributeNode> _attributes; |
+ |
+ /** |
+ * The [TokenType.GT] or [TokenType.SLASH_GT] token after the attributes (not |
+ * `null`). The token may be the same token as [nodeEnd] if there are no child |
+ * [tagNodes]. |
+ */ |
+ final Token attributeEnd; |
+ |
+ /** |
+ * The tag nodes contained in the receiver (not `null`, contains no `null`s). |
+ */ |
+ List<XmlTagNode> _tagNodes; |
+ |
+ /** |
+ * The token (not `null`) after the content, which may be |
+ * * (1) [TokenType.LT_SLASH] for nodes with open and close tags, or |
+ * * (2) the [TokenType.LT] nodeStart of the next sibling node if this node is self |
+ * closing or the attributeEnd is [TokenType.SLASH_GT], or |
+ * * (3) [TokenType.EOF] if the node does not have a closing tag and is the last node in |
+ * the stream [TokenType.LT_SLASH] token after the content, or `null` if there is no |
+ * content and the attributes ended with [TokenType.SLASH_GT]. |
+ */ |
+ final Token contentEnd; |
+ |
+ /** |
+ * The closing [TokenType.TAG] after the child elements or `null` if there is no |
+ * content and the attributes ended with [TokenType.SLASH_GT] |
+ */ |
+ final Token closingTag; |
+ |
+ /** |
+ * The ending [TokenType.GT] or [TokenType.SLASH_GT] token (not `null`). |
+ */ |
+ final Token nodeEnd; |
+ |
+ /** |
+ * The expressions that are embedded in the tag's content. |
+ */ |
+ List<XmlExpression> expressions = XmlExpression.EMPTY_ARRAY; |
+ |
+ /** |
+ * Construct a new instance representing an XML or HTML element |
+ * |
+ * @param nodeStart the starting [TokenType.LT] token (not `null`) |
+ * @param tag the [TokenType.TAG] token after the starting '<' (not `null`). |
+ * @param attributes the attributes associated with this element or [NO_ATTRIBUTES] (not |
+ * `null`, contains no `null`s) |
+ * @param attributeEnd The [TokenType.GT] or [TokenType.SLASH_GT] token after the |
+ * attributes (not `null`). The token may be the same token as [nodeEnd] if |
+ * there are no child [tagNodes]. |
+ * @param tagNodes child tag nodes of the receiver or [NO_TAG_NODES] (not `null`, |
+ * contains no `null`s) |
+ * @param contentEnd the token (not `null`) after the content, which may be |
+ * * (1) [TokenType.LT_SLASH] for nodes with open and close tags, or |
+ * * (2) the [TokenType.LT] nodeStart of the next sibling node if this node is |
+ * self closing or the attributeEnd is [TokenType.SLASH_GT], or |
+ * * (3) [TokenType.EOF] if the node does not have a closing tag and is the last |
+ * node in the stream [TokenType.LT_SLASH] token after the content, or `null` |
+ * if there is no content and the attributes ended with [TokenType.SLASH_GT]. |
+ * @param closingTag the closing [TokenType.TAG] after the child elements or `null` if |
+ * there is no content and the attributes ended with [TokenType.SLASH_GT] |
+ * @param nodeEnd the ending [TokenType.GT] or [TokenType.SLASH_GT] token (not |
+ * `null`) |
+ */ |
+ XmlTagNode(this.nodeStart, this._tag, List<XmlAttributeNode> attributes, |
+ this.attributeEnd, List<XmlTagNode> tagNodes, this.contentEnd, |
+ this.closingTag, this.nodeEnd) { |
+ this._attributes = becomeParentOfAll(attributes, ifEmpty: NO_ATTRIBUTES); |
+ this._tagNodes = becomeParentOfAll(tagNodes, ifEmpty: NO_TAG_NODES); |
+ } |
+ |
+ /** |
+ * Answer the receiver's attributes. Callers should not manipulate the returned list to edit the |
+ * AST structure. |
+ * |
+ * @return the attributes (not `null`, contains no `null`s) |
+ */ |
+ List<XmlAttributeNode> get attributes => _attributes; |
+ |
+ @override |
+ Token get beginToken => nodeStart; |
+ |
+ /** |
+ * Return a string representing the content contained in the receiver. This |
+ * includes the textual representation of any child tag nodes ([getTagNodes]). |
+ * Whitespace between '<', '</', and '>', '/>' is discarded, but all |
+ * other whitespace is preserved. |
+ */ |
+ String get content { |
+ Token token = attributeEnd.next; |
+ if (identical(token, contentEnd)) { |
+ return ""; |
+ } |
+ // TODO(danrubel) Handle CDATA and replace HTML character encodings with |
+ // the actual characters. |
+ String content = token.lexeme; |
+ token = token.next; |
+ if (identical(token, contentEnd)) { |
+ return content; |
+ } |
+ StringBuffer buffer = new StringBuffer(); |
+ buffer.write(content); |
+ while (!identical(token, contentEnd)) { |
+ buffer.write(token.lexeme); |
+ token = token.next; |
+ } |
+ return buffer.toString(); |
+ } |
+ |
+ @override |
+ Token get endToken { |
+ if (nodeEnd != null) { |
+ return nodeEnd; |
+ } |
+ if (closingTag != null) { |
+ return closingTag; |
+ } |
+ if (contentEnd != null) { |
+ return contentEnd; |
+ } |
+ if (!_tagNodes.isEmpty) { |
+ return _tagNodes[_tagNodes.length - 1].endToken; |
+ } |
+ if (attributeEnd != null) { |
+ return attributeEnd; |
+ } |
+ if (!_attributes.isEmpty) { |
+ return _attributes[_attributes.length - 1].endToken; |
+ } |
+ return _tag; |
+ } |
+ |
+ /** |
+ * Answer the tag name after the starting '<'. |
+ * |
+ * @return the tag name (not `null`) |
+ */ |
+ String get tag => _tag.lexeme; |
+ |
+ /** |
+ * Answer the tag nodes contained in the receiver. Callers should not manipulate the returned list |
+ * to edit the AST structure. |
+ * |
+ * @return the children (not `null`, contains no `null`s) |
+ */ |
+ List<XmlTagNode> get tagNodes => _tagNodes; |
+ |
+ /** |
+ * Answer the [TokenType.TAG] token after the starting '<'. |
+ * |
+ * @return the token (not `null`) |
+ */ |
+ Token get tagToken => _tag; |
+ |
+ @override |
+ accept(XmlVisitor visitor) => visitor.visitXmlTagNode(this); |
+ |
+ /** |
+ * Answer the attribute with the specified name. |
+ * |
+ * @param name the attribute name |
+ * @return the attribute or `null` if no matching attribute is found |
+ */ |
+ XmlAttributeNode getAttribute(String name) { |
+ for (XmlAttributeNode attribute in _attributes) { |
+ if (attribute.name == name) { |
+ return attribute; |
+ } |
+ } |
+ return null; |
+ } |
+ |
+ /** |
+ * Find the attribute with the given name (see [getAttribute] and answer the lexeme |
+ * for the attribute's value token without the leading and trailing quotes (see |
+ * [XmlAttributeNode.getText]). |
+ * |
+ * @param name the attribute name |
+ * @return the attribute text or `null` if no matching attribute is found |
+ */ |
+ String getAttributeText(String name) { |
+ XmlAttributeNode attribute = getAttribute(name); |
+ return attribute != null ? attribute.text : null; |
+ } |
+ |
+ @override |
+ void visitChildren(XmlVisitor visitor) { |
+ for (XmlAttributeNode node in _attributes) { |
+ node.accept(visitor); |
+ } |
+ for (XmlTagNode node in _tagNodes) { |
+ node.accept(visitor); |
+ } |
+ } |
+} |
+ |
+/** |
+ * The interface `XmlVisitor` defines the behavior of objects that can be used to visit an |
+ * [XmlNode] structure. |
+ */ |
+abstract class XmlVisitor<R> { |
+ R visitHtmlScriptTagNode(HtmlScriptTagNode node); |
+ |
+ R visitHtmlUnit(HtmlUnit htmlUnit); |
+ |
+ R visitXmlAttributeNode(XmlAttributeNode xmlAttributeNode); |
+ |
+ R visitXmlTagNode(XmlTagNode xmlTagNode); |
+} |