| // Copyright (c) project authors. All rights reserved. | |
| // Licensed under the MIT license. | |
| // See LICENSE file in the project root for details. | |
| import 'dart:collection'; | |
| import '../parser.dart' show HtmlParser; | |
| import 'constants.dart'; | |
| import 'html_input_stream.dart'; | |
| import 'token.dart'; | |
| import 'trie.dart'; | |
| import 'utils.dart'; | |
| // Group entities by their first character, for faster lookups | |
| // TODO(jmesserly): lots of ways to make this faster: | |
| // - use switch instead of contains, indexOf | |
| // - use switch instead of the sequential if tests | |
| // - avoid string concat | |
| /// This class takes care of tokenizing HTML. | |
| class HtmlTokenizer implements Iterator<Token> { | |
| // TODO(jmesserly): a lot of these could be made private | |
| final HtmlInputStream stream; | |
| final bool lowercaseElementName; | |
| final bool lowercaseAttrName; | |
| /// True to generate spans in for [Token.span]. | |
| final bool generateSpans; | |
| /// True to generate spans for attributes. | |
| final bool attributeSpans; | |
| /// This reference to the parser is used for correct CDATA handling. | |
| /// The [HtmlParser] will set this at construction time. | |
| HtmlParser? parser; | |
| final Queue<Token?> tokenQueue; | |
| /// Holds the token that is currently being processed. | |
| Token? currentToken; | |
| /// Holds a reference to the method to be invoked for the next parser state. | |
| late bool Function() state; | |
| final StringBuffer _buffer = StringBuffer(); | |
| late int _lastOffset; | |
| // TODO(jmesserly): ideally this would be a LinkedHashMap and we wouldn't add | |
| // an item until it's ready. But the code doesn't have a clear notion of when | |
| // it's "done" with the attribute. | |
| List<TagAttribute>? _attributes; | |
| Set<String>? _attributeNames; | |
| HtmlTokenizer(dynamic doc, | |
| {String? encoding, | |
| bool parseMeta = true, | |
| this.lowercaseElementName = true, | |
| this.lowercaseAttrName = true, | |
| this.generateSpans = false, | |
| String? sourceUrl, | |
| this.attributeSpans = false}) | |
| : stream = | |
| HtmlInputStream(doc, encoding, parseMeta, generateSpans, sourceUrl), | |
| tokenQueue = Queue() { | |
| reset(); | |
| } | |
| TagToken get currentTagToken => currentToken as TagToken; | |
| DoctypeToken get currentDoctypeToken => currentToken as DoctypeToken; | |
| StringToken get currentStringToken => currentToken as StringToken; | |
| Token? _current; | |
| @override | |
| Token get current => _current!; | |
| final StringBuffer _attributeName = StringBuffer(); | |
| final StringBuffer _attributeValue = StringBuffer(); | |
| void _markAttributeEnd(int offset) { | |
| _attributes!.last.value = '$_attributeValue'; | |
| if (attributeSpans) _attributes!.last.end = stream.position + offset; | |
| } | |
| void _markAttributeValueStart(int offset) { | |
| if (attributeSpans) _attributes!.last.startValue = stream.position + offset; | |
| } | |
| void _markAttributeValueEnd(int offset) { | |
| if (attributeSpans) _attributes!.last.endValue = stream.position + offset; | |
| _markAttributeEnd(offset); | |
| } | |
| // Note: we could track the name span here, if we need it. | |
| void _markAttributeNameEnd(int offset) => _markAttributeEnd(offset); | |
| void _addAttribute(String name) { | |
| _attributes ??= []; | |
| _attributeName.clear(); | |
| _attributeName.write(name); | |
| _attributeValue.clear(); | |
| final attr = TagAttribute(); | |
| _attributes!.add(attr); | |
| if (attributeSpans) attr.start = stream.position - name.length; | |
| } | |
| /// This is where the magic happens. | |
| /// | |
| /// We do our usually processing through the states and when we have a token | |
| /// to return we yield the token which pauses processing until the next token | |
| /// is requested. | |
| @override | |
| bool moveNext() { | |
| // Start processing. When EOF is reached state will return false; | |
| // instead of true and the loop will terminate. | |
| while (stream.errors.isEmpty && tokenQueue.isEmpty) { | |
| if (!state()) { | |
| _current = null; | |
| return false; | |
| } | |
| } | |
| if (stream.errors.isNotEmpty) { | |
| _current = ParseErrorToken(stream.errors.removeFirst()); | |
| } else { | |
| assert(tokenQueue.isNotEmpty); | |
| _current = tokenQueue.removeFirst(); | |
| } | |
| return true; | |
| } | |
| /// Resets the tokenizer state. Calling this does not reset the [stream] or | |
| /// the [parser]. | |
| void reset() { | |
| _lastOffset = 0; | |
| tokenQueue.clear(); | |
| currentToken = null; | |
| _buffer.clear(); | |
| _attributes = null; | |
| _attributeNames = null; | |
| state = dataState; | |
| } | |
| /// Adds a token to the queue. Sets the span if needed. | |
| void _addToken(Token token) { | |
| if (generateSpans && token.span == null) { | |
| final offset = stream.position; | |
| token.span = stream.fileInfo!.span(_lastOffset, offset); | |
| if (token is! ParseErrorToken) { | |
| _lastOffset = offset; | |
| } | |
| } | |
| tokenQueue.add(token); | |
| } | |
| /// This function returns either U+FFFD or the character based on the | |
| /// decimal or hexadecimal representation. It also discards ";" if present. | |
| /// If not present it will add a [ParseErrorToken]. | |
| String consumeNumberEntity(bool isHex) { | |
| var allowed = isDigit; | |
| var radix = 10; | |
| if (isHex) { | |
| allowed = isHexDigit; | |
| radix = 16; | |
| } | |
| final charStack = <String?>[]; | |
| // Consume all the characters that are in range while making sure we | |
| // don't hit an EOF. | |
| var c = stream.char(); | |
| while (allowed(c) && c != eof) { | |
| charStack.add(c); | |
| c = stream.char(); | |
| } | |
| // Convert the set of characters consumed to an int. | |
| final charAsInt = int.parse(charStack.join(), radix: radix); | |
| // Certain characters get replaced with others | |
| var char = replacementCharacters[charAsInt]; | |
| if (char != null) { | |
| _addToken(ParseErrorToken('illegal-codepoint-for-numeric-entity', | |
| messageParams: {'charAsInt': charAsInt})); | |
| } else if ((0xD800 <= charAsInt && charAsInt <= 0xDFFF) || | |
| (charAsInt > 0x10FFFF)) { | |
| char = '\uFFFD'; | |
| _addToken(ParseErrorToken('illegal-codepoint-for-numeric-entity', | |
| messageParams: {'charAsInt': charAsInt})); | |
| } else { | |
| // Should speed up this check somehow (e.g. move the set to a constant) | |
| if ((0x0001 <= charAsInt && charAsInt <= 0x0008) || | |
| (0x000E <= charAsInt && charAsInt <= 0x001F) || | |
| (0x007F <= charAsInt && charAsInt <= 0x009F) || | |
| (0xFDD0 <= charAsInt && charAsInt <= 0xFDEF) || | |
| const [ | |
| 0x000B, | |
| 0xFFFE, | |
| 0xFFFF, | |
| 0x1FFFE, | |
| 0x1FFFF, | |
| 0x2FFFE, | |
| 0x2FFFF, | |
| 0x3FFFE, | |
| 0x3FFFF, | |
| 0x4FFFE, | |
| 0x4FFFF, | |
| 0x5FFFE, | |
| 0x5FFFF, | |
| 0x6FFFE, | |
| 0x6FFFF, | |
| 0x7FFFE, | |
| 0x7FFFF, | |
| 0x8FFFE, | |
| 0x8FFFF, | |
| 0x9FFFE, | |
| 0x9FFFF, | |
| 0xAFFFE, | |
| 0xAFFFF, | |
| 0xBFFFE, | |
| 0xBFFFF, | |
| 0xCFFFE, | |
| 0xCFFFF, | |
| 0xDFFFE, | |
| 0xDFFFF, | |
| 0xEFFFE, | |
| 0xEFFFF, | |
| 0xFFFFE, | |
| 0xFFFFF, | |
| 0x10FFFE, | |
| 0x10FFFF | |
| ].contains(charAsInt)) { | |
| _addToken(ParseErrorToken('illegal-codepoint-for-numeric-entity', | |
| messageParams: {'charAsInt': charAsInt})); | |
| } | |
| char = String.fromCharCodes([charAsInt]); | |
| } | |
| // Discard the ; if present. Otherwise, put it back on the queue and | |
| // invoke parseError on parser. | |
| if (c != ';') { | |
| _addToken(ParseErrorToken('numeric-entity-without-semicolon')); | |
| stream.unget(c); | |
| } | |
| return char; | |
| } | |
| void consumeEntity({String? allowedChar, bool fromAttribute = false}) { | |
| // Initialise to the default output for when no entity is matched | |
| String? output = '&'; | |
| final charStack = [stream.char()]; | |
| if (isWhitespace(charStack[0]) || | |
| charStack[0] == '<' || | |
| charStack[0] == '&' || | |
| charStack[0] == eof || | |
| allowedChar == charStack[0]) { | |
| stream.unget(charStack[0]); | |
| } else if (charStack[0] == '#') { | |
| // Read the next character to see if it's hex or decimal | |
| var hex = false; | |
| charStack.add(stream.char()); | |
| if (charStack.last == 'x' || charStack.last == 'X') { | |
| hex = true; | |
| charStack.add(stream.char()); | |
| } | |
| // charStack.last should be the first digit | |
| if (hex && isHexDigit(charStack.last) || | |
| (!hex && isDigit(charStack.last))) { | |
| // At least one digit found, so consume the whole number | |
| stream.unget(charStack.last); | |
| output = consumeNumberEntity(hex); | |
| } else { | |
| // No digits found | |
| _addToken(ParseErrorToken('expected-numeric-entity')); | |
| stream.unget(charStack.removeLast()); | |
| output = '&${charStack.join()}'; | |
| } | |
| } else { | |
| // At this point in the process might have named entity. Entities | |
| // are stored in the global variable "entities". | |
| // | |
| // Consume characters and compare to these to a substring of the | |
| // entity names in the list until the substring no longer matches. | |
| dynamic node = entitiesTrieRoot[charStack.last?.codeUnitAt(0)]; | |
| while (node != null && charStack.last != eof) { | |
| charStack.add(stream.char()); | |
| node = (node as Map)[charStack.last?.codeUnitAt(0)]; | |
| } | |
| // At this point we have a string that starts with some characters | |
| // that may match an entity | |
| String? entityName; | |
| // Try to find the longest entity the string will match to take care | |
| // of ¬i for instance. | |
| int entityLen; | |
| for (entityLen = charStack.length - 1; entityLen > 1; entityLen--) { | |
| final possibleEntityName = charStack.sublist(0, entityLen).join(); | |
| if (entities.containsKey(possibleEntityName)) { | |
| entityName = possibleEntityName; | |
| break; | |
| } | |
| } | |
| if (entityName != null) { | |
| final lastChar = entityName[entityName.length - 1]; | |
| if (lastChar != ';') { | |
| _addToken(ParseErrorToken('named-entity-without-semicolon')); | |
| } | |
| if (lastChar != ';' && | |
| fromAttribute && | |
| (isLetterOrDigit(charStack[entityLen]) || | |
| charStack[entityLen] == '=')) { | |
| stream.unget(charStack.removeLast()); | |
| output = '&${charStack.join()}'; | |
| } else { | |
| output = entities[entityName]; | |
| stream.unget(charStack.removeLast()); | |
| output = '$output${slice(charStack, entityLen).join()}'; | |
| } | |
| } else { | |
| if (!fromAttribute) { | |
| // Only emit this error token when we're consuming this NOT as part of an attribute. | |
| // See: https://html.spec.whatwg.org/multipage/parsing.html#ambiguous-ampersand-state | |
| _addToken(ParseErrorToken('expected-named-entity')); | |
| } | |
| stream.unget(charStack.removeLast()); | |
| output = '&${charStack.join()}'; | |
| } | |
| } | |
| if (fromAttribute) { | |
| _attributeValue.write(output); | |
| } else { | |
| Token token; | |
| if (isWhitespace(output)) { | |
| token = SpaceCharactersToken(output); | |
| } else { | |
| token = CharactersToken(output); | |
| } | |
| _addToken(token); | |
| } | |
| } | |
| /// This method replaces the need for "entityInAttributeValueState". | |
| void processEntityInAttribute(String allowedChar) { | |
| consumeEntity(allowedChar: allowedChar, fromAttribute: true); | |
| } | |
| /// This method is a generic handler for emitting the tags. It also sets | |
| /// the state to "data" because that's what's needed after a token has been | |
| /// emitted. | |
| void emitCurrentToken() { | |
| final token = currentToken!; | |
| // Add token to the queue to be yielded | |
| if (token is TagToken) { | |
| if (lowercaseElementName) { | |
| token.name = token.name?.toAsciiLowerCase(); | |
| } | |
| if (token is EndTagToken) { | |
| if (_attributes != null) { | |
| _addToken(ParseErrorToken('attributes-in-end-tag')); | |
| } | |
| if (token.selfClosing) { | |
| _addToken(ParseErrorToken('this-closing-flag-on-end-tag')); | |
| } | |
| } else if (token is StartTagToken) { | |
| // HTML5 specific normalizations to the token stream. | |
| // Convert the list into a map where first key wins. | |
| token.data = LinkedHashMap<Object, String>(); | |
| if (_attributes != null) { | |
| for (var attr in _attributes!) { | |
| token.data.putIfAbsent(attr.name!, () => attr.value); | |
| } | |
| if (attributeSpans) token.attributeSpans = _attributes; | |
| } | |
| } | |
| _attributes = null; | |
| _attributeNames = null; | |
| } | |
| _addToken(token); | |
| state = dataState; | |
| } | |
| // Below are the various tokenizer states worked out. | |
| bool dataState() { | |
| final data = stream.char(); | |
| if (data == '&') { | |
| state = entityDataState; | |
| } else if (data == '<') { | |
| state = tagOpenState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\u0000')); | |
| } else if (data == eof) { | |
| // Tokenization ends. | |
| return false; | |
| } else if (isWhitespace(data)) { | |
| // Directly after emitting a token you switch back to the "data | |
| // state". At that point spaceCharacters are important so they are | |
| // emitted separately. | |
| _addToken(SpaceCharactersToken('$data${stream.charsUntilSpace(true)}')); | |
| // No need to update lastFourChars here, since the first space will | |
| // have already been appended to lastFourChars and will have broken | |
| // any <!-- or --> sequences | |
| } else { | |
| final chars = stream.charsUntil3( | |
| Charcode.ampersand, Charcode.lessThan, Charcode.nul); | |
| _addToken(CharactersToken('$data$chars')); | |
| } | |
| return true; | |
| } | |
| bool entityDataState() { | |
| consumeEntity(); | |
| state = dataState; | |
| return true; | |
| } | |
| bool rcdataState() { | |
| final data = stream.char(); | |
| if (data == '&') { | |
| state = characterReferenceInRcdata; | |
| } else if (data == '<') { | |
| state = rcdataLessThanSignState; | |
| } else if (data == eof) { | |
| // Tokenization ends. | |
| return false; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| } else if (isWhitespace(data)) { | |
| // Directly after emitting a token you switch back to the "data | |
| // state". At that point spaceCharacters are important so they are | |
| // emitted separately. | |
| _addToken(SpaceCharactersToken('$data${stream.charsUntilSpace(true)}')); | |
| } else { | |
| final chars = stream.charsUntil2(Charcode.ampersand, Charcode.lessThan); | |
| _addToken(CharactersToken('$data$chars')); | |
| } | |
| return true; | |
| } | |
| bool characterReferenceInRcdata() { | |
| consumeEntity(); | |
| state = rcdataState; | |
| return true; | |
| } | |
| bool rawtextState() { | |
| final data = stream.char(); | |
| if (data == '<') { | |
| state = rawtextLessThanSignState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| } else if (data == eof) { | |
| // Tokenization ends. | |
| return false; | |
| } else { | |
| final chars = stream.charsUntil2(Charcode.lessThan, Charcode.nul); | |
| _addToken(CharactersToken('$data$chars')); | |
| } | |
| return true; | |
| } | |
| bool scriptDataState() { | |
| final data = stream.char(); | |
| if (data == '<') { | |
| state = scriptDataLessThanSignState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| } else if (data == eof) { | |
| // Tokenization ends. | |
| return false; | |
| } else { | |
| final chars = stream.charsUntil2(Charcode.lessThan, Charcode.nul); | |
| _addToken(CharactersToken('$data$chars')); | |
| } | |
| return true; | |
| } | |
| bool plaintextState() { | |
| final data = stream.char(); | |
| if (data == eof) { | |
| // Tokenization ends. | |
| return false; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| } else { | |
| _addToken(CharactersToken('$data${stream.charsUntil1(Charcode.nul)}')); | |
| } | |
| return true; | |
| } | |
| bool tagOpenState() { | |
| final data = stream.char(); | |
| if (data == '!') { | |
| state = markupDeclarationOpenState; | |
| } else if (data == '/') { | |
| state = closeTagOpenState; | |
| } else if (isLetter(data)) { | |
| currentToken = StartTagToken(data); | |
| state = tagNameState; | |
| } else if (data == '>') { | |
| // XXX In theory it could be something besides a tag name. But | |
| // do we really care? | |
| _addToken(ParseErrorToken('expected-tag-name-but-got-right-bracket')); | |
| _addToken(CharactersToken('<>')); | |
| state = dataState; | |
| } else if (data == '?') { | |
| // XXX In theory it could be something besides a tag name. But | |
| // do we really care? | |
| _addToken(ParseErrorToken('expected-tag-name-but-got-question-mark')); | |
| stream.unget(data); | |
| state = bogusCommentState; | |
| } else { | |
| // XXX | |
| _addToken(ParseErrorToken('expected-tag-name')); | |
| _addToken(CharactersToken('<')); | |
| stream.unget(data); | |
| state = dataState; | |
| } | |
| return true; | |
| } | |
| bool closeTagOpenState() { | |
| final data = stream.char(); | |
| if (isLetter(data)) { | |
| currentToken = EndTagToken(data); | |
| state = tagNameState; | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('expected-closing-tag-but-got-right-bracket')); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('expected-closing-tag-but-got-eof')); | |
| _addToken(CharactersToken('</')); | |
| state = dataState; | |
| } else { | |
| // XXX data can be _'_... | |
| _addToken(ParseErrorToken('expected-closing-tag-but-got-char', | |
| messageParams: {'data': data})); | |
| stream.unget(data); | |
| state = bogusCommentState; | |
| } | |
| return true; | |
| } | |
| bool tagNameState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| state = beforeAttributeNameState; | |
| } else if (data == '>') { | |
| emitCurrentToken(); | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-tag-name')); | |
| state = dataState; | |
| } else if (data == '/') { | |
| state = selfClosingStartTagState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentTagToken.name = '${currentTagToken.name}\uFFFD'; | |
| } else { | |
| currentTagToken.name = '${currentTagToken.name}$data'; | |
| // (Don't use charsUntil here, because tag names are | |
| // very short and it's faster to not do anything fancy) | |
| } | |
| return true; | |
| } | |
| bool rcdataLessThanSignState() { | |
| final data = stream.char(); | |
| if (data == '/') { | |
| _buffer.clear(); | |
| state = rcdataEndTagOpenState; | |
| } else { | |
| _addToken(CharactersToken('<')); | |
| stream.unget(data); | |
| state = rcdataState; | |
| } | |
| return true; | |
| } | |
| bool rcdataEndTagOpenState() { | |
| final data = stream.char(); | |
| if (isLetter(data)) { | |
| _buffer.write(data); | |
| state = rcdataEndTagNameState; | |
| } else { | |
| _addToken(CharactersToken('</')); | |
| stream.unget(data); | |
| state = rcdataState; | |
| } | |
| return true; | |
| } | |
| bool _tokenIsAppropriate() { | |
| // TODO(jmesserly): this should use case insensitive compare instead. | |
| return currentToken is TagToken && | |
| currentTagToken.name!.toLowerCase() == '$_buffer'.toLowerCase(); | |
| } | |
| bool rcdataEndTagNameState() { | |
| final appropriate = _tokenIsAppropriate(); | |
| final data = stream.char(); | |
| if (isWhitespace(data) && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| state = beforeAttributeNameState; | |
| } else if (data == '/' && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| state = selfClosingStartTagState; | |
| } else if (data == '>' && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| emitCurrentToken(); | |
| state = dataState; | |
| } else if (isLetter(data)) { | |
| _buffer.write(data); | |
| } else { | |
| _addToken(CharactersToken('</$_buffer')); | |
| stream.unget(data); | |
| state = rcdataState; | |
| } | |
| return true; | |
| } | |
| bool rawtextLessThanSignState() { | |
| final data = stream.char(); | |
| if (data == '/') { | |
| _buffer.clear(); | |
| state = rawtextEndTagOpenState; | |
| } else { | |
| _addToken(CharactersToken('<')); | |
| stream.unget(data); | |
| state = rawtextState; | |
| } | |
| return true; | |
| } | |
| bool rawtextEndTagOpenState() { | |
| final data = stream.char(); | |
| if (isLetter(data)) { | |
| _buffer.write(data); | |
| state = rawtextEndTagNameState; | |
| } else { | |
| _addToken(CharactersToken('</')); | |
| stream.unget(data); | |
| state = rawtextState; | |
| } | |
| return true; | |
| } | |
| bool rawtextEndTagNameState() { | |
| final appropriate = _tokenIsAppropriate(); | |
| final data = stream.char(); | |
| if (isWhitespace(data) && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| state = beforeAttributeNameState; | |
| } else if (data == '/' && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| state = selfClosingStartTagState; | |
| } else if (data == '>' && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| emitCurrentToken(); | |
| state = dataState; | |
| } else if (isLetter(data)) { | |
| _buffer.write(data); | |
| } else { | |
| _addToken(CharactersToken('</$_buffer')); | |
| stream.unget(data); | |
| state = rawtextState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataLessThanSignState() { | |
| final data = stream.char(); | |
| if (data == '/') { | |
| _buffer.clear(); | |
| state = scriptDataEndTagOpenState; | |
| } else if (data == '!') { | |
| _addToken(CharactersToken('<!')); | |
| state = scriptDataEscapeStartState; | |
| } else { | |
| _addToken(CharactersToken('<')); | |
| stream.unget(data); | |
| state = scriptDataState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEndTagOpenState() { | |
| final data = stream.char(); | |
| if (isLetter(data)) { | |
| _buffer.write(data); | |
| state = scriptDataEndTagNameState; | |
| } else { | |
| _addToken(CharactersToken('</')); | |
| stream.unget(data); | |
| state = scriptDataState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEndTagNameState() { | |
| final appropriate = _tokenIsAppropriate(); | |
| final data = stream.char(); | |
| if (isWhitespace(data) && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| state = beforeAttributeNameState; | |
| } else if (data == '/' && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| state = selfClosingStartTagState; | |
| } else if (data == '>' && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| emitCurrentToken(); | |
| state = dataState; | |
| } else if (isLetter(data)) { | |
| _buffer.write(data); | |
| } else { | |
| _addToken(CharactersToken('</$_buffer')); | |
| stream.unget(data); | |
| state = scriptDataState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEscapeStartState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| _addToken(CharactersToken('-')); | |
| state = scriptDataEscapeStartDashState; | |
| } else { | |
| stream.unget(data); | |
| state = scriptDataState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEscapeStartDashState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| _addToken(CharactersToken('-')); | |
| state = scriptDataEscapedDashDashState; | |
| } else { | |
| stream.unget(data); | |
| state = scriptDataState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEscapedState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| _addToken(CharactersToken('-')); | |
| state = scriptDataEscapedDashState; | |
| } else if (data == '<') { | |
| state = scriptDataEscapedLessThanSignState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| } else if (data == eof) { | |
| state = dataState; | |
| } else { | |
| final chars = | |
| stream.charsUntil3(Charcode.lessThan, Charcode.hyphen, Charcode.nul); | |
| _addToken(CharactersToken('$data$chars')); | |
| } | |
| return true; | |
| } | |
| bool scriptDataEscapedDashState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| _addToken(CharactersToken('-')); | |
| state = scriptDataEscapedDashDashState; | |
| } else if (data == '<') { | |
| state = scriptDataEscapedLessThanSignState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| state = scriptDataEscapedState; | |
| } else if (data == eof) { | |
| state = dataState; | |
| } else { | |
| _addToken(CharactersToken(data)); | |
| state = scriptDataEscapedState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEscapedDashDashState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| _addToken(CharactersToken('-')); | |
| } else if (data == '<') { | |
| state = scriptDataEscapedLessThanSignState; | |
| } else if (data == '>') { | |
| _addToken(CharactersToken('>')); | |
| state = scriptDataState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| state = scriptDataEscapedState; | |
| } else if (data == eof) { | |
| state = dataState; | |
| } else { | |
| _addToken(CharactersToken(data)); | |
| state = scriptDataEscapedState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEscapedLessThanSignState() { | |
| final data = stream.char(); | |
| if (data == '/') { | |
| _buffer.clear(); | |
| state = scriptDataEscapedEndTagOpenState; | |
| } else if (isLetter(data)) { | |
| _addToken(CharactersToken('<$data')); | |
| _buffer.clear(); | |
| _buffer.write(data); | |
| state = scriptDataDoubleEscapeStartState; | |
| } else { | |
| _addToken(CharactersToken('<')); | |
| stream.unget(data); | |
| state = scriptDataEscapedState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEscapedEndTagOpenState() { | |
| final data = stream.char(); | |
| if (isLetter(data)) { | |
| _buffer.clear(); | |
| _buffer.write(data); | |
| state = scriptDataEscapedEndTagNameState; | |
| } else { | |
| _addToken(CharactersToken('</')); | |
| stream.unget(data); | |
| state = scriptDataEscapedState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataEscapedEndTagNameState() { | |
| final appropriate = _tokenIsAppropriate(); | |
| final data = stream.char(); | |
| if (isWhitespace(data) && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| state = beforeAttributeNameState; | |
| } else if (data == '/' && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| state = selfClosingStartTagState; | |
| } else if (data == '>' && appropriate) { | |
| currentToken = EndTagToken('$_buffer'); | |
| emitCurrentToken(); | |
| state = dataState; | |
| } else if (isLetter(data)) { | |
| _buffer.write(data); | |
| } else { | |
| _addToken(CharactersToken('</$_buffer')); | |
| stream.unget(data); | |
| state = scriptDataEscapedState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataDoubleEscapeStartState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data) || data == '/' || data == '>') { | |
| _addToken(CharactersToken(data)); | |
| if ('$_buffer'.toLowerCase() == 'script') { | |
| state = scriptDataDoubleEscapedState; | |
| } else { | |
| state = scriptDataEscapedState; | |
| } | |
| } else if (isLetter(data)) { | |
| _addToken(CharactersToken(data)); | |
| _buffer.write(data); | |
| } else { | |
| stream.unget(data); | |
| state = scriptDataEscapedState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataDoubleEscapedState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| _addToken(CharactersToken('-')); | |
| state = scriptDataDoubleEscapedDashState; | |
| } else if (data == '<') { | |
| _addToken(CharactersToken('<')); | |
| state = scriptDataDoubleEscapedLessThanSignState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-script-in-script')); | |
| state = dataState; | |
| } else { | |
| _addToken(CharactersToken(data)); | |
| } | |
| return true; | |
| } | |
| bool scriptDataDoubleEscapedDashState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| _addToken(CharactersToken('-')); | |
| state = scriptDataDoubleEscapedDashDashState; | |
| } else if (data == '<') { | |
| _addToken(CharactersToken('<')); | |
| state = scriptDataDoubleEscapedLessThanSignState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| state = scriptDataDoubleEscapedState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-script-in-script')); | |
| state = dataState; | |
| } else { | |
| _addToken(CharactersToken(data)); | |
| state = scriptDataDoubleEscapedState; | |
| } | |
| return true; | |
| } | |
| // TODO(jmesserly): report bug in original code | |
| // (was "Dash" instead of "DashDash") | |
| bool scriptDataDoubleEscapedDashDashState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| _addToken(CharactersToken('-')); | |
| } else if (data == '<') { | |
| _addToken(CharactersToken('<')); | |
| state = scriptDataDoubleEscapedLessThanSignState; | |
| } else if (data == '>') { | |
| _addToken(CharactersToken('>')); | |
| state = scriptDataState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addToken(CharactersToken('\uFFFD')); | |
| state = scriptDataDoubleEscapedState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-script-in-script')); | |
| state = dataState; | |
| } else { | |
| _addToken(CharactersToken(data)); | |
| state = scriptDataDoubleEscapedState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataDoubleEscapedLessThanSignState() { | |
| final data = stream.char(); | |
| if (data == '/') { | |
| _addToken(CharactersToken('/')); | |
| _buffer.clear(); | |
| state = scriptDataDoubleEscapeEndState; | |
| } else { | |
| stream.unget(data); | |
| state = scriptDataDoubleEscapedState; | |
| } | |
| return true; | |
| } | |
| bool scriptDataDoubleEscapeEndState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data) || data == '/' || data == '>') { | |
| _addToken(CharactersToken(data)); | |
| if ('$_buffer'.toLowerCase() == 'script') { | |
| state = scriptDataEscapedState; | |
| } else { | |
| state = scriptDataDoubleEscapedState; | |
| } | |
| } else if (isLetter(data)) { | |
| _addToken(CharactersToken(data)); | |
| _buffer.write(data); | |
| } else { | |
| stream.unget(data); | |
| state = scriptDataDoubleEscapedState; | |
| } | |
| return true; | |
| } | |
| bool beforeAttributeNameState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| stream.charsUntilSpace(true); | |
| } else if (data != null && isLetter(data)) { | |
| _addAttribute(data); | |
| state = attributeNameState; | |
| } else if (data == '>') { | |
| emitCurrentToken(); | |
| } else if (data == '/') { | |
| state = selfClosingStartTagState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('expected-attribute-name-but-got-eof')); | |
| state = dataState; | |
| } else if ("'\"=<".contains(data!)) { | |
| _addToken(ParseErrorToken('invalid-character-in-attribute-name')); | |
| _addAttribute(data); | |
| state = attributeNameState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addAttribute('\uFFFD'); | |
| state = attributeNameState; | |
| } else { | |
| _addAttribute(data); | |
| state = attributeNameState; | |
| } | |
| return true; | |
| } | |
| bool attributeNameState() { | |
| final data = stream.char(); | |
| var leavingThisState = true; | |
| var emitToken = false; | |
| if (data == '=') { | |
| state = beforeAttributeValueState; | |
| } else if (isLetter(data)) { | |
| _attributeName.write(data); | |
| _attributeName.write(stream.charsUntilAsciiLetter(true)); | |
| leavingThisState = false; | |
| } else if (data == '>') { | |
| // XXX If we emit here the attributes are converted to a dict | |
| // without being checked and when the code below runs we error | |
| // because data is a dict not a list | |
| emitToken = true; | |
| } else if (isWhitespace(data)) { | |
| state = afterAttributeNameState; | |
| } else if (data == '/') { | |
| state = selfClosingStartTagState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _attributeName.write('\uFFFD'); | |
| leavingThisState = false; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-attribute-name')); | |
| state = dataState; | |
| } else if ("'\"<".contains(data!)) { | |
| _addToken(ParseErrorToken('invalid-character-in-attribute-name')); | |
| _attributeName.write(data); | |
| leavingThisState = false; | |
| } else { | |
| _attributeName.write(data); | |
| leavingThisState = false; | |
| } | |
| if (leavingThisState) { | |
| _markAttributeNameEnd(-1); | |
| // Attributes are not dropped at this stage. That happens when the | |
| // start tag token is emitted so values can still be safely appended | |
| // to attributes, but we do want to report the parse error in time. | |
| var attrName = _attributeName.toString(); | |
| if (lowercaseAttrName) { | |
| attrName = attrName.toAsciiLowerCase(); | |
| } | |
| _attributes!.last.name = attrName; | |
| _attributeNames ??= {}; | |
| if (_attributeNames!.contains(attrName)) { | |
| _addToken(ParseErrorToken('duplicate-attribute')); | |
| } | |
| _attributeNames!.add(attrName); | |
| // XXX Fix for above XXX | |
| if (emitToken) { | |
| emitCurrentToken(); | |
| } | |
| } | |
| return true; | |
| } | |
| bool afterAttributeNameState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| stream.charsUntilSpace(true); | |
| } else if (data == '=') { | |
| state = beforeAttributeValueState; | |
| } else if (data == '>') { | |
| emitCurrentToken(); | |
| } else if (data != null && isLetter(data)) { | |
| _addAttribute(data); | |
| state = attributeNameState; | |
| } else if (data == '/') { | |
| state = selfClosingStartTagState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _addAttribute('\uFFFD'); | |
| state = attributeNameState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('expected-end-of-tag-but-got-eof')); | |
| state = dataState; | |
| } else if ("'\"<".contains(data!)) { | |
| _addToken(ParseErrorToken('invalid-character-after-attribute-name')); | |
| _addAttribute(data); | |
| state = attributeNameState; | |
| } else { | |
| _addAttribute(data); | |
| state = attributeNameState; | |
| } | |
| return true; | |
| } | |
| bool beforeAttributeValueState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| stream.charsUntilSpace(true); | |
| } else if (data == '"') { | |
| _markAttributeValueStart(0); | |
| state = attributeValueDoubleQuotedState; | |
| } else if (data == '&') { | |
| state = attributeValueUnQuotedState; | |
| stream.unget(data); | |
| _markAttributeValueStart(0); | |
| } else if (data == "'") { | |
| _markAttributeValueStart(0); | |
| state = attributeValueSingleQuotedState; | |
| } else if (data == '>') { | |
| _addToken( | |
| ParseErrorToken('expected-attribute-value-but-got-right-bracket')); | |
| emitCurrentToken(); | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _markAttributeValueStart(-1); | |
| _attributeValue.write('\uFFFD'); | |
| state = attributeValueUnQuotedState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('expected-attribute-value-but-got-eof')); | |
| state = dataState; | |
| } else if ('=<`'.contains(data!)) { | |
| _addToken(ParseErrorToken('equals-in-unquoted-attribute-value')); | |
| _markAttributeValueStart(-1); | |
| _attributeValue.write(data); | |
| state = attributeValueUnQuotedState; | |
| } else { | |
| _markAttributeValueStart(-1); | |
| _attributeValue.write(data); | |
| state = attributeValueUnQuotedState; | |
| } | |
| return true; | |
| } | |
| bool attributeValueDoubleQuotedState() { | |
| final data = stream.char(); | |
| if (data == '"') { | |
| _markAttributeValueEnd(-1); | |
| _markAttributeEnd(0); | |
| state = afterAttributeValueState; | |
| } else if (data == '&') { | |
| processEntityInAttribute('"'); | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _attributeValue.write('\uFFFD'); | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-attribute-value-double-quote')); | |
| _markAttributeValueEnd(-1); | |
| state = dataState; | |
| } else { | |
| _attributeValue.write(data); | |
| _attributeValue | |
| .write(stream.charsUntil2(Charcode.doubleQuote, Charcode.ampersand)); | |
| } | |
| return true; | |
| } | |
| bool attributeValueSingleQuotedState() { | |
| final data = stream.char(); | |
| if (data == "'") { | |
| _markAttributeValueEnd(-1); | |
| _markAttributeEnd(0); | |
| state = afterAttributeValueState; | |
| } else if (data == '&') { | |
| processEntityInAttribute("'"); | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _attributeValue.write('\uFFFD'); | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-attribute-value-single-quote')); | |
| _markAttributeValueEnd(-1); | |
| state = dataState; | |
| } else { | |
| _attributeValue.write(data); | |
| _attributeValue | |
| .write(stream.charsUntil2(Charcode.singleQuote, Charcode.ampersand)); | |
| } | |
| return true; | |
| } | |
| bool attributeValueUnQuotedState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| _markAttributeValueEnd(-1); | |
| state = beforeAttributeNameState; | |
| } else if (data == '&') { | |
| processEntityInAttribute('>'); | |
| } else if (data == '>') { | |
| _markAttributeValueEnd(-1); | |
| emitCurrentToken(); | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-attribute-value-no-quotes')); | |
| _markAttributeValueEnd(-1); | |
| state = dataState; | |
| } else if ('"\'=<`'.contains(data!)) { | |
| _addToken( | |
| ParseErrorToken('unexpected-character-in-unquoted-attribute-value')); | |
| _attributeValue.write(data); | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| _attributeValue.write('\uFFFD'); | |
| } else { | |
| _attributeValue.write(data); | |
| _attributeValue.write(stream.charsUntil(const { | |
| Charcode.ampersand, | |
| Charcode.greaterThan, | |
| Charcode.doubleQuote, | |
| Charcode.singleQuote, | |
| Charcode.equals, | |
| Charcode.lessThan, | |
| Charcode.graveAccent, | |
| ...spaceCharacters | |
| })); | |
| } | |
| return true; | |
| } | |
| bool afterAttributeValueState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| state = beforeAttributeNameState; | |
| } else if (data == '>') { | |
| emitCurrentToken(); | |
| } else if (data == '/') { | |
| state = selfClosingStartTagState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('unexpected-EOF-after-attribute-value')); | |
| stream.unget(data); | |
| state = dataState; | |
| } else { | |
| _addToken(ParseErrorToken('unexpected-character-after-attribute-value')); | |
| stream.unget(data); | |
| state = beforeAttributeNameState; | |
| } | |
| return true; | |
| } | |
| bool selfClosingStartTagState() { | |
| final data = stream.char(); | |
| if (data == '>') { | |
| currentTagToken.selfClosing = true; | |
| emitCurrentToken(); | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('unexpected-EOF-after-solidus-in-tag')); | |
| stream.unget(data); | |
| state = dataState; | |
| } else { | |
| _addToken(ParseErrorToken('unexpected-character-after-soldius-in-tag')); | |
| stream.unget(data); | |
| state = beforeAttributeNameState; | |
| } | |
| return true; | |
| } | |
| bool bogusCommentState() { | |
| // Make a new comment token and give it as value all the characters | |
| // until the first > or EOF (charsUntil checks for EOF automatically) | |
| // and emit it. | |
| var data = stream.charsUntil1(Charcode.greaterThan); | |
| data = data.replaceAll('\u0000', '\uFFFD'); | |
| _addToken(CommentToken(data)); | |
| // Eat the character directly after the bogus comment which is either a | |
| // ">" or an EOF. | |
| stream.char(); | |
| state = dataState; | |
| return true; | |
| } | |
| bool markupDeclarationOpenState() { | |
| final charStack = [stream.char()]; | |
| if (charStack.last == '-') { | |
| charStack.add(stream.char()); | |
| if (charStack.last == '-') { | |
| currentToken = CommentToken(); | |
| state = commentStartState; | |
| return true; | |
| } | |
| } else if (charStack.last == 'd' || charStack.last == 'D') { | |
| var matched = true; | |
| for (var expected in const ['oO', 'cC', 'tT', 'yY', 'pP', 'eE']) { | |
| final char = stream.char(); | |
| charStack.add(char); | |
| if (char == eof || !expected.contains(char!)) { | |
| matched = false; | |
| break; | |
| } | |
| } | |
| if (matched) { | |
| currentToken = DoctypeToken(correct: true); | |
| state = doctypeState; | |
| return true; | |
| } | |
| } else if (charStack.last == '[' && | |
| parser != null && | |
| parser!.tree.openElements.isNotEmpty && | |
| parser!.tree.openElements.last.namespaceUri != | |
| parser!.tree.defaultNamespace) { | |
| var matched = true; | |
| for (var expected in const ['C', 'D', 'A', 'T', 'A', '[']) { | |
| charStack.add(stream.char()); | |
| if (charStack.last != expected) { | |
| matched = false; | |
| break; | |
| } | |
| } | |
| if (matched) { | |
| state = cdataSectionState; | |
| return true; | |
| } | |
| } | |
| _addToken(ParseErrorToken('expected-dashes-or-doctype')); | |
| while (charStack.isNotEmpty) { | |
| stream.unget(charStack.removeLast()); | |
| } | |
| state = bogusCommentState; | |
| return true; | |
| } | |
| bool commentStartState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| state = commentStartDashState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentStringToken.add('\uFFFD'); | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('incorrect-comment')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-comment')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentStringToken.add(data!); | |
| state = commentState; | |
| } | |
| return true; | |
| } | |
| bool commentStartDashState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| state = commentEndState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentStringToken.add('-\uFFFD'); | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('incorrect-comment')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-comment')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentStringToken.add('-').add(data!); | |
| state = commentState; | |
| } | |
| return true; | |
| } | |
| bool commentState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| state = commentEndDashState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentStringToken.add('\uFFFD'); | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-comment')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentStringToken | |
| .add(data!) | |
| .add(stream.charsUntil2(Charcode.hyphen, Charcode.nul)); | |
| } | |
| return true; | |
| } | |
| bool commentEndDashState() { | |
| final data = stream.char(); | |
| if (data == '-') { | |
| state = commentEndState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentStringToken.add('-\uFFFD'); | |
| state = commentState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-comment-end-dash')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentStringToken.add('-').add(data!); | |
| state = commentState; | |
| } | |
| return true; | |
| } | |
| bool commentEndState() { | |
| final data = stream.char(); | |
| if (data == '>') { | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentStringToken.add('--\uFFFD'); | |
| state = commentState; | |
| } else if (data == '!') { | |
| _addToken( | |
| ParseErrorToken('unexpected-bang-after-double-dash-in-comment')); | |
| state = commentEndBangState; | |
| } else if (data == '-') { | |
| _addToken( | |
| ParseErrorToken('unexpected-dash-after-double-dash-in-comment')); | |
| currentStringToken.add(data!); | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-comment-double-dash')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| // XXX | |
| _addToken(ParseErrorToken('unexpected-char-in-comment')); | |
| currentStringToken.add('--').add(data!); | |
| state = commentState; | |
| } | |
| return true; | |
| } | |
| bool commentEndBangState() { | |
| final data = stream.char(); | |
| if (data == '>') { | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == '-') { | |
| currentStringToken.add('--!'); | |
| state = commentEndDashState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentStringToken.add('--!\uFFFD'); | |
| state = commentState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-comment-end-bang-state')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentStringToken.add('--!').add(data!); | |
| state = commentState; | |
| } | |
| return true; | |
| } | |
| bool doctypeState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| state = beforeDoctypeNameState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('expected-doctype-name-but-got-eof')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| _addToken(ParseErrorToken('need-space-after-doctype')); | |
| stream.unget(data); | |
| state = beforeDoctypeNameState; | |
| } | |
| return true; | |
| } | |
| bool beforeDoctypeNameState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| return true; | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('expected-doctype-name-but-got-right-bracket')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentDoctypeToken.name = '\uFFFD'; | |
| state = doctypeNameState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('expected-doctype-name-but-got-eof')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentDoctypeToken.name = data; | |
| state = doctypeNameState; | |
| } | |
| return true; | |
| } | |
| bool doctypeNameState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| currentDoctypeToken.name = currentDoctypeToken.name?.toAsciiLowerCase(); | |
| state = afterDoctypeNameState; | |
| } else if (data == '>') { | |
| currentDoctypeToken.name = currentDoctypeToken.name?.toAsciiLowerCase(); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentDoctypeToken.name = '${currentDoctypeToken.name}\uFFFD'; | |
| state = doctypeNameState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype-name')); | |
| currentDoctypeToken.correct = false; | |
| currentDoctypeToken.name = currentDoctypeToken.name?.toAsciiLowerCase(); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentDoctypeToken.name = '${currentDoctypeToken.name}$data'; | |
| } | |
| return true; | |
| } | |
| bool afterDoctypeNameState() { | |
| var data = stream.char(); | |
| if (isWhitespace(data)) { | |
| return true; | |
| } else if (data == '>') { | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| currentDoctypeToken.correct = false; | |
| stream.unget(data); | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| if (data == 'p' || data == 'P') { | |
| // TODO(jmesserly): would be nice to have a helper for this. | |
| var matched = true; | |
| for (var expected in const ['uU', 'bB', 'lL', 'iI', 'cC']) { | |
| data = stream.char(); | |
| if (data == eof || !expected.contains(data!)) { | |
| matched = false; | |
| break; | |
| } | |
| } | |
| if (matched) { | |
| state = afterDoctypePublicKeywordState; | |
| return true; | |
| } | |
| } else if (data == 's' || data == 'S') { | |
| var matched = true; | |
| for (var expected in const ['yY', 'sS', 'tT', 'eE', 'mM']) { | |
| data = stream.char(); | |
| if (data == eof || !expected.contains(data!)) { | |
| matched = false; | |
| break; | |
| } | |
| } | |
| if (matched) { | |
| state = afterDoctypeSystemKeywordState; | |
| return true; | |
| } | |
| } | |
| // All the characters read before the current 'data' will be | |
| // [a-zA-Z], so they're garbage in the bogus doctype and can be | |
| // discarded; only the latest character might be '>' or EOF | |
| // and needs to be ungetted | |
| stream.unget(data); | |
| _addToken(ParseErrorToken('expected-space-or-right-bracket-in-doctype', | |
| messageParams: {'data': data})); | |
| currentDoctypeToken.correct = false; | |
| state = bogusDoctypeState; | |
| } | |
| return true; | |
| } | |
| bool afterDoctypePublicKeywordState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| state = beforeDoctypePublicIdentifierState; | |
| } else if (data == "'" || data == '"') { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| stream.unget(data); | |
| state = beforeDoctypePublicIdentifierState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| stream.unget(data); | |
| state = beforeDoctypePublicIdentifierState; | |
| } | |
| return true; | |
| } | |
| bool beforeDoctypePublicIdentifierState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| return true; | |
| } else if (data == '"') { | |
| currentDoctypeToken.publicId = ''; | |
| state = doctypePublicIdentifierDoubleQuotedState; | |
| } else if (data == "'") { | |
| currentDoctypeToken.publicId = ''; | |
| state = doctypePublicIdentifierSingleQuotedState; | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('unexpected-end-of-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| state = bogusDoctypeState; | |
| } | |
| return true; | |
| } | |
| bool doctypePublicIdentifierDoubleQuotedState() { | |
| final data = stream.char(); | |
| if (data == '"') { | |
| state = afterDoctypePublicIdentifierState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentDoctypeToken.publicId = '${currentDoctypeToken.publicId}\uFFFD'; | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('unexpected-end-of-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentDoctypeToken.publicId = '${currentDoctypeToken.publicId}$data'; | |
| } | |
| return true; | |
| } | |
| bool doctypePublicIdentifierSingleQuotedState() { | |
| final data = stream.char(); | |
| if (data == "'") { | |
| state = afterDoctypePublicIdentifierState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentDoctypeToken.publicId = '${currentDoctypeToken.publicId}\uFFFD'; | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('unexpected-end-of-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentDoctypeToken.publicId = '${currentDoctypeToken.publicId}$data'; | |
| } | |
| return true; | |
| } | |
| bool afterDoctypePublicIdentifierState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| state = betweenDoctypePublicAndSystemIdentifiersState; | |
| } else if (data == '>') { | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == '"') { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| currentDoctypeToken.systemId = ''; | |
| state = doctypeSystemIdentifierDoubleQuotedState; | |
| } else if (data == "'") { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| currentDoctypeToken.systemId = ''; | |
| state = doctypeSystemIdentifierSingleQuotedState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| state = bogusDoctypeState; | |
| } | |
| return true; | |
| } | |
| bool betweenDoctypePublicAndSystemIdentifiersState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| return true; | |
| } else if (data == '>') { | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == '"') { | |
| currentDoctypeToken.systemId = ''; | |
| state = doctypeSystemIdentifierDoubleQuotedState; | |
| } else if (data == "'") { | |
| currentDoctypeToken.systemId = ''; | |
| state = doctypeSystemIdentifierSingleQuotedState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| state = bogusDoctypeState; | |
| } | |
| return true; | |
| } | |
| bool afterDoctypeSystemKeywordState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| state = beforeDoctypeSystemIdentifierState; | |
| } else if (data == "'" || data == '"') { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| stream.unget(data); | |
| state = beforeDoctypeSystemIdentifierState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| stream.unget(data); | |
| state = beforeDoctypeSystemIdentifierState; | |
| } | |
| return true; | |
| } | |
| bool beforeDoctypeSystemIdentifierState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| return true; | |
| } else if (data == '"') { | |
| currentDoctypeToken.systemId = ''; | |
| state = doctypeSystemIdentifierDoubleQuotedState; | |
| } else if (data == "'") { | |
| currentDoctypeToken.systemId = ''; | |
| state = doctypeSystemIdentifierSingleQuotedState; | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| state = bogusDoctypeState; | |
| } | |
| return true; | |
| } | |
| bool doctypeSystemIdentifierDoubleQuotedState() { | |
| final data = stream.char(); | |
| if (data == '"') { | |
| state = afterDoctypeSystemIdentifierState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentDoctypeToken.systemId = '${currentDoctypeToken.systemId}\uFFFD'; | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('unexpected-end-of-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentDoctypeToken.systemId = '${currentDoctypeToken.systemId}$data'; | |
| } | |
| return true; | |
| } | |
| bool doctypeSystemIdentifierSingleQuotedState() { | |
| final data = stream.char(); | |
| if (data == "'") { | |
| state = afterDoctypeSystemIdentifierState; | |
| } else if (data == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| currentDoctypeToken.systemId = '${currentDoctypeToken.systemId}\uFFFD'; | |
| } else if (data == '>') { | |
| _addToken(ParseErrorToken('unexpected-end-of-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| currentDoctypeToken.systemId = '${currentDoctypeToken.systemId}$data'; | |
| } | |
| return true; | |
| } | |
| bool afterDoctypeSystemIdentifierState() { | |
| final data = stream.char(); | |
| if (isWhitespace(data)) { | |
| return true; | |
| } else if (data == '>') { | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| _addToken(ParseErrorToken('eof-in-doctype')); | |
| currentDoctypeToken.correct = false; | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else { | |
| _addToken(ParseErrorToken('unexpected-char-in-doctype')); | |
| state = bogusDoctypeState; | |
| } | |
| return true; | |
| } | |
| bool bogusDoctypeState() { | |
| final data = stream.char(); | |
| if (data == '>') { | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } else if (data == eof) { | |
| // XXX EMIT | |
| stream.unget(data); | |
| _addToken(currentToken!); | |
| state = dataState; | |
| } | |
| return true; | |
| } | |
| bool cdataSectionState() { | |
| final data = <String>[]; | |
| var matchedEnd = 0; | |
| while (true) { | |
| var ch = stream.char(); | |
| if (ch == null) { | |
| break; | |
| } | |
| // Deal with null here rather than in the parser | |
| if (ch == '\u0000') { | |
| _addToken(ParseErrorToken('invalid-codepoint')); | |
| ch = '\uFFFD'; | |
| } | |
| data.add(ch); | |
| // TODO(jmesserly): it'd be nice if we had an easier way to match the end, | |
| // perhaps with a "peek" API. | |
| if (ch == ']') { | |
| if (matchedEnd < 2) { | |
| matchedEnd++; | |
| } | |
| } else if (ch == '>' && matchedEnd == 2) { | |
| // Remove "]]>" from the end. | |
| data.removeLast(); | |
| data.removeLast(); | |
| data.removeLast(); | |
| break; | |
| } else { | |
| matchedEnd = 0; | |
| } | |
| } | |
| if (data.isNotEmpty) { | |
| _addToken(CharactersToken(data.join())); | |
| } | |
| state = dataState; | |
| return true; | |
| } | |
| } |