| // Copyright (c) 2012, 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. | |
| part of '../parser.dart'; | |
| // TODO: We should update the tokenization to follow what's described in the | |
| // spec: https://www.w3.org/TR/css-syntax-3/#tokenization. | |
| class Tokenizer extends TokenizerBase { | |
| /// U+ prefix for unicode characters. | |
| // ignore: non_constant_identifier_names | |
| final UNICODE_U = 'U'.codeUnitAt(0); | |
| // ignore: non_constant_identifier_names | |
| final UNICODE_LOWER_U = 'u'.codeUnitAt(0); | |
| // ignore: non_constant_identifier_names | |
| final UNICODE_PLUS = '+'.codeUnitAt(0); | |
| // ignore: non_constant_identifier_names | |
| final QUESTION_MARK = '?'.codeUnitAt(0); | |
| /// CDATA keyword. | |
| // ignore: non_constant_identifier_names | |
| final List<int> CDATA_NAME = 'CDATA'.codeUnits; | |
| Tokenizer(super.file, super.text, super.skipWhitespace, [super.index]); | |
| @override | |
| Token next({bool unicodeRange = false}) { | |
| // keep track of our starting position | |
| _startIndex = _index; | |
| int ch; | |
| ch = _nextChar(); | |
| switch (ch) { | |
| case TokenChar.NEWLINE: | |
| case TokenChar.RETURN: | |
| case TokenChar.SPACE: | |
| case TokenChar.TAB: | |
| return finishWhitespace(); | |
| case TokenChar.END_OF_FILE: | |
| return _finishToken(TokenKind.END_OF_FILE); | |
| case TokenChar.AT: | |
| var peekCh = _peekChar(); | |
| if (TokenizerHelpers.isIdentifierStart(peekCh)) { | |
| var oldIndex = _index; | |
| var oldStartIndex = _startIndex; | |
| _startIndex = _index; | |
| ch = _nextChar(); | |
| finishIdentifier(); | |
| // Is it a directive? | |
| var tokId = TokenKind.matchDirectives( | |
| _text, _startIndex, _index - _startIndex); | |
| if (tokId == -1) { | |
| // No, is it a margin directive? | |
| tokId = TokenKind.matchMarginDirectives( | |
| _text, _startIndex, _index - _startIndex); | |
| } | |
| if (tokId != -1) { | |
| return _finishToken(tokId); | |
| } else { | |
| // Didn't find a CSS directive or margin directive so the @name is | |
| // probably the Less definition '@name: value_variable_definition'. | |
| _startIndex = oldStartIndex; | |
| _index = oldIndex; | |
| } | |
| } | |
| return _finishToken(TokenKind.AT); | |
| case TokenChar.DOT: | |
| var start = _startIndex; // Start where the dot started. | |
| if (maybeEatDigit()) { | |
| // looks like a number dot followed by digit(s). | |
| var number = finishNumber(); | |
| if (number.kind == TokenKind.INTEGER) { | |
| // It's a number but it's preceded by a dot, so make it a double. | |
| _startIndex = start; | |
| return _finishToken(TokenKind.DOUBLE); | |
| } else { | |
| // Don't allow dot followed by a double (e.g, '..1'). | |
| return _errorToken(); | |
| } | |
| } | |
| // It's really a dot. | |
| return _finishToken(TokenKind.DOT); | |
| case TokenChar.LPAREN: | |
| return _finishToken(TokenKind.LPAREN); | |
| case TokenChar.RPAREN: | |
| return _finishToken(TokenKind.RPAREN); | |
| case TokenChar.LBRACE: | |
| return _finishToken(TokenKind.LBRACE); | |
| case TokenChar.RBRACE: | |
| return _finishToken(TokenKind.RBRACE); | |
| case TokenChar.LBRACK: | |
| return _finishToken(TokenKind.LBRACK); | |
| case TokenChar.RBRACK: | |
| if (_maybeEatChar(TokenChar.RBRACK) && | |
| _maybeEatChar(TokenChar.GREATER)) { | |
| // ]]> | |
| return next(); | |
| } | |
| return _finishToken(TokenKind.RBRACK); | |
| case TokenChar.HASH: | |
| return _finishToken(TokenKind.HASH); | |
| case TokenChar.PLUS: | |
| if (_nextCharsAreNumber(ch)) return finishNumber(); | |
| return _finishToken(TokenKind.PLUS); | |
| case TokenChar.MINUS: | |
| if (inSelectorExpression || unicodeRange) { | |
| // If parsing in pseudo function expression then minus is an operator | |
| // not part of identifier e.g., interval value range (e.g. U+400-4ff) | |
| // or minus operator in selector expression. | |
| return _finishToken(TokenKind.MINUS); | |
| } else if (_nextCharsAreNumber(ch)) { | |
| return finishNumber(); | |
| } else if (TokenizerHelpers.isIdentifierStart(ch)) { | |
| return finishIdentifier(); | |
| } | |
| return _finishToken(TokenKind.MINUS); | |
| case TokenChar.GREATER: | |
| return _finishToken(TokenKind.GREATER); | |
| case TokenChar.TILDE: | |
| if (_maybeEatChar(TokenChar.EQUALS)) { | |
| return _finishToken(TokenKind.INCLUDES); // ~= | |
| } | |
| return _finishToken(TokenKind.TILDE); | |
| case TokenChar.ASTERISK: | |
| if (_maybeEatChar(TokenChar.EQUALS)) { | |
| return _finishToken(TokenKind.SUBSTRING_MATCH); // *= | |
| } | |
| return _finishToken(TokenKind.ASTERISK); | |
| case TokenChar.AMPERSAND: | |
| return _finishToken(TokenKind.AMPERSAND); | |
| case TokenChar.NAMESPACE: | |
| if (_maybeEatChar(TokenChar.EQUALS)) { | |
| return _finishToken(TokenKind.DASH_MATCH); // |= | |
| } | |
| return _finishToken(TokenKind.NAMESPACE); | |
| case TokenChar.COLON: | |
| return _finishToken(TokenKind.COLON); | |
| case TokenChar.COMMA: | |
| return _finishToken(TokenKind.COMMA); | |
| case TokenChar.SEMICOLON: | |
| return _finishToken(TokenKind.SEMICOLON); | |
| case TokenChar.PERCENT: | |
| return _finishToken(TokenKind.PERCENT); | |
| case TokenChar.SINGLE_QUOTE: | |
| return _finishToken(TokenKind.SINGLE_QUOTE); | |
| case TokenChar.DOUBLE_QUOTE: | |
| return _finishToken(TokenKind.DOUBLE_QUOTE); | |
| case TokenChar.SLASH: | |
| if (_maybeEatChar(TokenChar.ASTERISK)) return finishMultiLineComment(); | |
| return _finishToken(TokenKind.SLASH); | |
| case TokenChar.LESS: // <!-- | |
| if (_maybeEatChar(TokenChar.BANG)) { | |
| if (_maybeEatChar(TokenChar.MINUS) && | |
| _maybeEatChar(TokenChar.MINUS)) { | |
| return finishHtmlComment(); | |
| } else if (_maybeEatChar(TokenChar.LBRACK) && | |
| _maybeEatChar(CDATA_NAME[0]) && | |
| _maybeEatChar(CDATA_NAME[1]) && | |
| _maybeEatChar(CDATA_NAME[2]) && | |
| _maybeEatChar(CDATA_NAME[3]) && | |
| _maybeEatChar(CDATA_NAME[4]) && | |
| _maybeEatChar(TokenChar.LBRACK)) { | |
| // <![CDATA[ | |
| return next(); | |
| } | |
| } | |
| return _finishToken(TokenKind.LESS); | |
| case TokenChar.EQUALS: | |
| return _finishToken(TokenKind.EQUALS); | |
| case TokenChar.CARET: | |
| if (_maybeEatChar(TokenChar.EQUALS)) { | |
| return _finishToken(TokenKind.PREFIX_MATCH); // ^= | |
| } | |
| return _finishToken(TokenKind.CARET); | |
| case TokenChar.DOLLAR: | |
| if (_maybeEatChar(TokenChar.EQUALS)) { | |
| return _finishToken(TokenKind.SUFFIX_MATCH); // $= | |
| } | |
| return _finishToken(TokenKind.DOLLAR); | |
| case TokenChar.BANG: | |
| return finishIdentifier(); | |
| default: | |
| // TODO(jmesserly): this is used for IE8 detection; I'm not sure it's | |
| // appropriate outside of a few specific places; certainly shouldn't | |
| // be parsed in selectors. | |
| if (!inSelector && ch == TokenChar.BACKSLASH) { | |
| return _finishToken(TokenKind.BACKSLASH); | |
| } | |
| if (unicodeRange) { | |
| // Three types of unicode ranges: | |
| // - single code point (e.g. U+416) | |
| // - interval value range (e.g. U+400-4ff) | |
| // - range where trailing ‘?’ characters imply ‘any digit value’ | |
| // (e.g. U+4??) | |
| if (maybeEatHexDigit()) { | |
| var t = finishHexNumber(); | |
| // Any question marks then it's a HEX_RANGE not HEX_NUMBER. | |
| if (maybeEatQuestionMark()) finishUnicodeRange(); | |
| return t; | |
| } else if (maybeEatQuestionMark()) { | |
| // HEX_RANGE U+N??? | |
| return finishUnicodeRange(); | |
| } else { | |
| return _errorToken(); | |
| } | |
| } else if (_inString && | |
| (ch == UNICODE_U || ch == UNICODE_LOWER_U) && | |
| (_peekChar() == UNICODE_PLUS)) { | |
| // `_inString` is misleading. We actually DON'T want to enter this | |
| // block while tokenizing a string, but the parser sets this value to | |
| // false while it IS consuming tokens within a string. | |
| // | |
| // Unicode range: U+uNumber[-U+uNumber] | |
| // uNumber = 0..10FFFF | |
| _nextChar(); // Skip + | |
| _startIndex = _index; // Starts at the number | |
| return _finishToken(TokenKind.UNICODE_RANGE); | |
| } else if (varDef(ch)) { | |
| return _finishToken(TokenKind.VAR_DEFINITION); | |
| } else if (varUsage(ch)) { | |
| return _finishToken(TokenKind.VAR_USAGE); | |
| } else if (TokenizerHelpers.isIdentifierStart(ch)) { | |
| return finishIdentifier(); | |
| } else if (TokenizerHelpers.isDigit(ch)) { | |
| return finishNumber(); | |
| } | |
| return _errorToken(); | |
| } | |
| } | |
| bool varDef(int ch) => | |
| ch == 'v'.codeUnitAt(0) && | |
| _maybeEatChar('a'.codeUnitAt(0)) && | |
| _maybeEatChar('r'.codeUnitAt(0)) && | |
| _maybeEatChar('-'.codeUnitAt(0)); | |
| bool varUsage(int ch) => | |
| ch == 'v'.codeUnitAt(0) && | |
| _maybeEatChar('a'.codeUnitAt(0)) && | |
| _maybeEatChar('r'.codeUnitAt(0)) && | |
| (_peekChar() == '-'.codeUnitAt(0)); | |
| @override | |
| Token _errorToken([String? message]) => _finishToken(TokenKind.ERROR); | |
| @override | |
| int getIdentifierKind() { | |
| // Is the identifier a unit type? | |
| var tokId = -1; | |
| // Don't match units in selectors or selector expressions. | |
| if (!inSelectorExpression && !inSelector) { | |
| tokId = TokenKind.matchUnits(_text, _startIndex, _index - _startIndex); | |
| } | |
| if (tokId == -1) { | |
| tokId = (_text.substring(_startIndex, _index) == '!important') | |
| ? TokenKind.IMPORTANT | |
| : -1; | |
| } | |
| return tokId >= 0 ? tokId : TokenKind.IDENTIFIER; | |
| } | |
| Token finishIdentifier() { | |
| // If we encounter an escape sequence, remember it so we can post-process | |
| // to unescape. | |
| var chars = <int>[]; | |
| // backup so we can start with the first character | |
| var validateFrom = _index; | |
| _index = _startIndex; | |
| while (_index < _text.length) { | |
| var ch = _text.codeUnitAt(_index); | |
| // If the previous character was "\" we need to escape. T | |
| // http://www.w3.org/TR/CSS21/syndata.html#characters | |
| // if followed by hexadecimal digits, create the appropriate character. | |
| // otherwise, include the character in the identifier and don't treat it | |
| // specially. | |
| if (ch == 92 /*\*/ && _inString) { | |
| var startHex = ++_index; | |
| eatHexDigits(startHex + 6); | |
| if (_index != startHex) { | |
| // Parse the hex digits and add that character. | |
| chars.add(int.parse('0x${_text.substring(startHex, _index)}')); | |
| if (_index == _text.length) break; | |
| // if we stopped the hex because of a whitespace char, skip it | |
| ch = _text.codeUnitAt(_index); | |
| if (_index - startHex != 6 && | |
| (ch == TokenChar.SPACE || | |
| ch == TokenChar.TAB || | |
| ch == TokenChar.RETURN || | |
| ch == TokenChar.NEWLINE)) { | |
| _index++; | |
| } | |
| } else { | |
| // not a digit, just add the next character literally | |
| if (_index == _text.length) break; | |
| chars.add(_text.codeUnitAt(_index++)); | |
| } | |
| } else if (_index < validateFrom || | |
| (inSelectorExpression | |
| ? TokenizerHelpers.isIdentifierPartExpr(ch) | |
| : TokenizerHelpers.isIdentifierPart(ch))) { | |
| chars.add(ch); | |
| _index++; | |
| } else { | |
| // Not an identifier or escaped character. | |
| break; | |
| } | |
| } | |
| var span = _file.span(_startIndex, _index); | |
| var text = String.fromCharCodes(chars); | |
| return IdentifierToken(text, getIdentifierKind(), span); | |
| } | |
| @override | |
| Token finishNumber() { | |
| eatDigits(); | |
| if (_peekChar() == 46 /*.*/) { | |
| // Handle the case of 1.toString(). | |
| _nextChar(); | |
| if (TokenizerHelpers.isDigit(_peekChar())) { | |
| eatDigits(); | |
| return _finishToken(TokenKind.DOUBLE); | |
| } else { | |
| _index -= 1; | |
| } | |
| } | |
| return _finishToken(TokenKind.INTEGER); | |
| } | |
| bool maybeEatDigit() { | |
| if (_index < _text.length && | |
| TokenizerHelpers.isDigit(_text.codeUnitAt(_index))) { | |
| _index += 1; | |
| return true; | |
| } | |
| return false; | |
| } | |
| Token finishHexNumber() { | |
| eatHexDigits(_text.length); | |
| return _finishToken(TokenKind.HEX_INTEGER); | |
| } | |
| void eatHexDigits(int end) { | |
| end = math.min(end, _text.length); | |
| while (_index < end) { | |
| if (TokenizerHelpers.isHexDigit(_text.codeUnitAt(_index))) { | |
| _index += 1; | |
| } else { | |
| return; | |
| } | |
| } | |
| } | |
| bool maybeEatHexDigit() { | |
| if (_index < _text.length && | |
| TokenizerHelpers.isHexDigit(_text.codeUnitAt(_index))) { | |
| _index += 1; | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool maybeEatQuestionMark() { | |
| if (_index < _text.length && _text.codeUnitAt(_index) == QUESTION_MARK) { | |
| _index += 1; | |
| return true; | |
| } | |
| return false; | |
| } | |
| void eatQuestionMarks() { | |
| while (_index < _text.length) { | |
| if (_text.codeUnitAt(_index) == QUESTION_MARK) { | |
| _index += 1; | |
| } else { | |
| return; | |
| } | |
| } | |
| } | |
| Token finishUnicodeRange() { | |
| eatQuestionMarks(); | |
| return _finishToken(TokenKind.HEX_RANGE); | |
| } | |
| Token finishHtmlComment() { | |
| while (true) { | |
| var ch = _nextChar(); | |
| if (ch == 0) { | |
| return _finishToken(TokenKind.INCOMPLETE_COMMENT); | |
| } else if (ch == TokenChar.MINUS) { | |
| /* Check if close part of Comment Definition --> (CDC). */ | |
| if (_maybeEatChar(TokenChar.MINUS)) { | |
| if (_maybeEatChar(TokenChar.GREATER)) { | |
| if (_inString) { | |
| return next(); | |
| } else { | |
| return _finishToken(TokenKind.HTML_COMMENT); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| @override | |
| Token finishMultiLineComment() { | |
| while (true) { | |
| var ch = _nextChar(); | |
| if (ch == 0) { | |
| return _finishToken(TokenKind.INCOMPLETE_COMMENT); | |
| } else if (ch == 42 /*'*'*/) { | |
| if (_maybeEatChar(47 /*'/'*/)) { | |
| if (_inString) { | |
| return next(); | |
| } else { | |
| return _finishToken(TokenKind.COMMENT); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// Static helper methods. | |
| class TokenizerHelpers { | |
| static bool isIdentifierStart(int c) => | |
| isIdentifierStartExpr(c) || c == 45 /*-*/; | |
| static bool isDigit(int c) => c >= 48 /*0*/ && c <= 57 /*9*/; | |
| static bool isHexDigit(int c) => | |
| isDigit(c) || | |
| (c >= 97 /*a*/ && c <= 102 /*f*/) || | |
| (c >= 65 /*A*/ && c <= 70 /*F*/); | |
| static bool isIdentifierPart(int c) => | |
| isIdentifierPartExpr(c) || c == 45 /*-*/; | |
| /// Pseudo function expressions identifiers can't have a minus sign. | |
| static bool isIdentifierStartExpr(int c) => | |
| (c >= 97 /*a*/ && c <= 122 /*z*/) || | |
| (c >= 65 /*A*/ && c <= 90 /*Z*/) || | |
| // Note: Unicode 10646 chars U+00A0 or higher are allowed, see: | |
| // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier | |
| // http://www.w3.org/TR/CSS21/syndata.html#characters | |
| // Also, escaped character should be allowed. | |
| c == 95 /*_*/ || | |
| c >= 0xA0 || | |
| c == 92 /*\*/; | |
| /// Pseudo function expressions identifiers can't have a minus sign. | |
| static bool isIdentifierPartExpr(int c) => | |
| isIdentifierStartExpr(c) || isDigit(c); | |
| } |