2016-06-23 12:47:54 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2016-06-21 14:10:25 -04:00
|
|
|
import * as chars from './chars';
|
2016-06-16 14:39:29 -04:00
|
|
|
import {BaseException} from './facade/exceptions';
|
|
|
|
import {StringWrapper, isPresent, resolveEnumToken} from './facade/lang';
|
2016-06-08 19:38:52 -04:00
|
|
|
|
2016-02-02 03:37:08 -05:00
|
|
|
export enum CssTokenType {
|
|
|
|
EOF,
|
|
|
|
String,
|
|
|
|
Comment,
|
|
|
|
Identifier,
|
|
|
|
Number,
|
|
|
|
IdentifierOrNumber,
|
|
|
|
AtKeyword,
|
|
|
|
Character,
|
|
|
|
Whitespace,
|
|
|
|
Invalid
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum CssLexerMode {
|
|
|
|
ALL,
|
|
|
|
ALL_TRACK_WS,
|
|
|
|
SELECTOR,
|
|
|
|
PSEUDO_SELECTOR,
|
2016-06-14 19:26:57 -04:00
|
|
|
PSEUDO_SELECTOR_WITH_ARGUMENTS,
|
2016-02-02 03:37:08 -05:00
|
|
|
ATTRIBUTE_SELECTOR,
|
|
|
|
AT_RULE_QUERY,
|
|
|
|
MEDIA_QUERY,
|
|
|
|
BLOCK,
|
|
|
|
KEYFRAME_BLOCK,
|
|
|
|
STYLE_BLOCK,
|
|
|
|
STYLE_VALUE,
|
|
|
|
STYLE_VALUE_FUNCTION,
|
|
|
|
STYLE_CALC_FUNCTION
|
|
|
|
}
|
|
|
|
|
|
|
|
export class LexedCssResult {
|
|
|
|
constructor(public error: CssScannerError, public token: CssToken) {}
|
|
|
|
}
|
|
|
|
|
2016-06-08 19:38:52 -04:00
|
|
|
export function generateErrorMessage(
|
|
|
|
input: string, message: string, errorValue: string, index: number, row: number,
|
|
|
|
column: number): string {
|
2016-02-02 03:37:08 -05:00
|
|
|
return `${message} at column ${row}:${column} in expression [` +
|
2016-06-08 19:38:52 -04:00
|
|
|
findProblemCode(input, errorValue, index, column) + ']';
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
2016-06-08 19:38:52 -04:00
|
|
|
export function findProblemCode(
|
|
|
|
input: string, errorValue: string, index: number, column: number): string {
|
2016-02-02 03:37:08 -05:00
|
|
|
var endOfProblemLine = index;
|
|
|
|
var current = charCode(input, index);
|
|
|
|
while (current > 0 && !isNewline(current)) {
|
|
|
|
current = charCode(input, ++endOfProblemLine);
|
|
|
|
}
|
|
|
|
var choppedString = input.substring(0, endOfProblemLine);
|
2016-06-08 19:38:52 -04:00
|
|
|
var pointerPadding = '';
|
2016-02-02 03:37:08 -05:00
|
|
|
for (var i = 0; i < column; i++) {
|
2016-06-08 19:38:52 -04:00
|
|
|
pointerPadding += ' ';
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
2016-06-08 19:38:52 -04:00
|
|
|
var pointerString = '';
|
2016-02-02 03:37:08 -05:00
|
|
|
for (var i = 0; i < errorValue.length; i++) {
|
2016-06-08 19:38:52 -04:00
|
|
|
pointerString += '^';
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
2016-06-08 19:38:52 -04:00
|
|
|
return choppedString + '\n' + pointerPadding + pointerString + '\n';
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export class CssToken {
|
|
|
|
numValue: number;
|
2016-06-08 19:38:52 -04:00
|
|
|
constructor(
|
|
|
|
public index: number, public column: number, public line: number, public type: CssTokenType,
|
|
|
|
public strValue: string) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.numValue = charCode(strValue, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class CssLexer {
|
|
|
|
scan(text: string, trackComments: boolean = false): CssScanner {
|
|
|
|
return new CssScanner(text, trackComments);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class CssScannerError extends BaseException {
|
|
|
|
public rawMessage: string;
|
|
|
|
public message: string;
|
|
|
|
|
2016-06-28 14:35:59 -04:00
|
|
|
constructor(public token: CssToken, message: string) {
|
2016-02-02 03:37:08 -05:00
|
|
|
super('Css Parse Error: ' + message);
|
|
|
|
this.rawMessage = message;
|
|
|
|
}
|
|
|
|
|
|
|
|
toString(): string { return this.message; }
|
|
|
|
}
|
|
|
|
|
|
|
|
function _trackWhitespace(mode: CssLexerMode) {
|
|
|
|
switch (mode) {
|
|
|
|
case CssLexerMode.SELECTOR:
|
2016-06-14 19:26:57 -04:00
|
|
|
case CssLexerMode.PSEUDO_SELECTOR:
|
2016-02-02 03:37:08 -05:00
|
|
|
case CssLexerMode.ALL_TRACK_WS:
|
|
|
|
case CssLexerMode.STYLE_VALUE:
|
|
|
|
return true;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class CssScanner {
|
|
|
|
peek: number;
|
|
|
|
peekPeek: number;
|
|
|
|
length: number = 0;
|
|
|
|
index: number = -1;
|
|
|
|
column: number = -1;
|
|
|
|
line: number = 0;
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
/** @internal */
|
2016-02-02 03:37:08 -05:00
|
|
|
_currentMode: CssLexerMode = CssLexerMode.BLOCK;
|
2016-03-28 17:25:22 -04:00
|
|
|
/** @internal */
|
2016-02-02 03:37:08 -05:00
|
|
|
_currentError: CssScannerError = null;
|
|
|
|
|
|
|
|
constructor(public input: string, private _trackComments: boolean = false) {
|
|
|
|
this.length = this.input.length;
|
|
|
|
this.peekPeek = this.peekAt(0);
|
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
|
|
|
|
getMode(): CssLexerMode { return this._currentMode; }
|
|
|
|
|
|
|
|
setMode(mode: CssLexerMode) {
|
|
|
|
if (this._currentMode != mode) {
|
2016-06-14 19:26:57 -04:00
|
|
|
if (_trackWhitespace(this._currentMode) && !_trackWhitespace(mode)) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.consumeWhitespace();
|
|
|
|
}
|
|
|
|
this._currentMode = mode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
advance(): void {
|
|
|
|
if (isNewline(this.peek)) {
|
|
|
|
this.column = 0;
|
|
|
|
this.line++;
|
|
|
|
} else {
|
|
|
|
this.column++;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.index++;
|
|
|
|
this.peek = this.peekPeek;
|
|
|
|
this.peekPeek = this.peekAt(this.index + 1);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
peekAt(index: number): number {
|
2016-06-21 14:10:25 -04:00
|
|
|
return index >= this.length ? chars.$EOF : StringWrapper.charCodeAt(this.input, index);
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
consumeEmptyStatements(): void {
|
|
|
|
this.consumeWhitespace();
|
2016-06-21 14:10:25 -04:00
|
|
|
while (this.peek == chars.$SEMICOLON) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.advance();
|
|
|
|
this.consumeWhitespace();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
consumeWhitespace(): void {
|
2016-06-21 14:10:25 -04:00
|
|
|
while (chars.isWhitespace(this.peek) || isNewline(this.peek)) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.advance();
|
|
|
|
if (!this._trackComments && isCommentStart(this.peek, this.peekPeek)) {
|
|
|
|
this.advance(); // /
|
|
|
|
this.advance(); // *
|
|
|
|
while (!isCommentEnd(this.peek, this.peekPeek)) {
|
2016-06-21 14:10:25 -04:00
|
|
|
if (this.peek == chars.$EOF) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.error('Unterminated comment');
|
|
|
|
}
|
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
this.advance(); // *
|
|
|
|
this.advance(); // /
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
consume(type: CssTokenType, value: string = null): LexedCssResult {
|
|
|
|
var mode = this._currentMode;
|
2016-06-14 19:26:57 -04:00
|
|
|
|
|
|
|
this.setMode(_trackWhitespace(mode) ? CssLexerMode.ALL_TRACK_WS : CssLexerMode.ALL);
|
2016-02-02 03:37:08 -05:00
|
|
|
|
|
|
|
var previousIndex = this.index;
|
|
|
|
var previousLine = this.line;
|
|
|
|
var previousColumn = this.column;
|
|
|
|
|
2016-06-14 19:26:57 -04:00
|
|
|
var next: CssToken;
|
2016-02-02 03:37:08 -05:00
|
|
|
var output = this.scan();
|
2016-06-14 19:26:57 -04:00
|
|
|
if (isPresent(output)) {
|
|
|
|
// just incase the inner scan method returned an error
|
|
|
|
if (isPresent(output.error)) {
|
|
|
|
this.setMode(mode);
|
|
|
|
return output;
|
|
|
|
}
|
2016-02-02 03:37:08 -05:00
|
|
|
|
2016-06-14 19:26:57 -04:00
|
|
|
next = output.token;
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!isPresent(next)) {
|
2016-06-16 16:02:18 -04:00
|
|
|
next = new CssToken(this.index, this.column, this.line, CssTokenType.EOF, 'end of file');
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
2016-06-16 16:02:18 -04:00
|
|
|
var isMatchingType: boolean = false;
|
2016-02-02 03:37:08 -05:00
|
|
|
if (type == CssTokenType.IdentifierOrNumber) {
|
|
|
|
// TODO (matsko): implement array traversal for lookup here
|
|
|
|
isMatchingType = next.type == CssTokenType.Number || next.type == CssTokenType.Identifier;
|
|
|
|
} else {
|
|
|
|
isMatchingType = next.type == type;
|
|
|
|
}
|
|
|
|
|
|
|
|
// before throwing the error we need to bring back the former
|
|
|
|
// mode so that the parser can recover...
|
|
|
|
this.setMode(mode);
|
|
|
|
|
2016-06-28 14:35:59 -04:00
|
|
|
var error: CssScannerError = null;
|
2016-02-02 03:37:08 -05:00
|
|
|
if (!isMatchingType || (isPresent(value) && value != next.strValue)) {
|
2016-06-08 19:38:52 -04:00
|
|
|
var errorMessage = resolveEnumToken(CssTokenType, next.type) + ' does not match expected ' +
|
|
|
|
resolveEnumToken(CssTokenType, type) + ' value';
|
2016-02-02 03:37:08 -05:00
|
|
|
|
|
|
|
if (isPresent(value)) {
|
|
|
|
errorMessage += ' ("' + next.strValue + '" should match "' + value + '")';
|
|
|
|
}
|
|
|
|
|
|
|
|
error = new CssScannerError(
|
2016-06-08 19:38:52 -04:00
|
|
|
next, generateErrorMessage(
|
|
|
|
this.input, errorMessage, next.strValue, previousIndex, previousLine,
|
|
|
|
previousColumn));
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return new LexedCssResult(error, next);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
scan(): LexedCssResult {
|
|
|
|
var trackWS = _trackWhitespace(this._currentMode);
|
|
|
|
if (this.index == 0 && !trackWS) { // first scan
|
|
|
|
this.consumeWhitespace();
|
|
|
|
}
|
|
|
|
|
|
|
|
var token = this._scan();
|
|
|
|
if (token == null) return null;
|
|
|
|
|
|
|
|
var error = this._currentError;
|
|
|
|
this._currentError = null;
|
|
|
|
|
|
|
|
if (!trackWS) {
|
|
|
|
this.consumeWhitespace();
|
|
|
|
}
|
|
|
|
return new LexedCssResult(error, token);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
/** @internal */
|
2016-02-02 03:37:08 -05:00
|
|
|
_scan(): CssToken {
|
|
|
|
var peek = this.peek;
|
|
|
|
var peekPeek = this.peekPeek;
|
2016-06-21 14:10:25 -04:00
|
|
|
if (peek == chars.$EOF) return null;
|
2016-02-02 03:37:08 -05:00
|
|
|
|
|
|
|
if (isCommentStart(peek, peekPeek)) {
|
|
|
|
// even if comments are not tracked we still lex the
|
|
|
|
// comment so we can move the pointer forward
|
|
|
|
var commentToken = this.scanComment();
|
|
|
|
if (this._trackComments) {
|
|
|
|
return commentToken;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-21 14:10:25 -04:00
|
|
|
if (_trackWhitespace(this._currentMode) && (chars.isWhitespace(peek) || isNewline(peek))) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return this.scanWhitespace();
|
|
|
|
}
|
|
|
|
|
|
|
|
peek = this.peek;
|
|
|
|
peekPeek = this.peekPeek;
|
2016-06-21 14:10:25 -04:00
|
|
|
if (peek == chars.$EOF) return null;
|
2016-02-02 03:37:08 -05:00
|
|
|
|
|
|
|
if (isStringStart(peek, peekPeek)) {
|
|
|
|
return this.scanString();
|
|
|
|
}
|
|
|
|
|
|
|
|
// something like url(cool)
|
|
|
|
if (this._currentMode == CssLexerMode.STYLE_VALUE_FUNCTION) {
|
|
|
|
return this.scanCssValueFunction();
|
|
|
|
}
|
|
|
|
|
2016-06-21 14:10:25 -04:00
|
|
|
var isModifier = peek == chars.$PLUS || peek == chars.$MINUS;
|
|
|
|
var digitA = isModifier ? false : chars.isDigit(peek);
|
|
|
|
var digitB = chars.isDigit(peekPeek);
|
|
|
|
if (digitA || (isModifier && (peekPeek == chars.$PERIOD || digitB)) ||
|
|
|
|
(peek == chars.$PERIOD && digitB)) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return this.scanNumber();
|
|
|
|
}
|
|
|
|
|
2016-06-21 14:10:25 -04:00
|
|
|
if (peek == chars.$AT) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return this.scanAtExpression();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isIdentifierStart(peek, peekPeek)) {
|
|
|
|
return this.scanIdentifier();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isValidCssCharacter(peek, this._currentMode)) {
|
|
|
|
return this.scanCharacter();
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
scanComment(): CssToken {
|
2016-06-08 19:38:52 -04:00
|
|
|
if (this.assertCondition(
|
|
|
|
isCommentStart(this.peek, this.peekPeek), 'Expected comment start value')) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
var start = this.index;
|
|
|
|
var startingColumn = this.column;
|
|
|
|
var startingLine = this.line;
|
|
|
|
|
|
|
|
this.advance(); // /
|
|
|
|
this.advance(); // *
|
|
|
|
|
|
|
|
while (!isCommentEnd(this.peek, this.peekPeek)) {
|
2016-06-21 14:10:25 -04:00
|
|
|
if (this.peek == chars.$EOF) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.error('Unterminated comment');
|
|
|
|
}
|
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.advance(); // *
|
|
|
|
this.advance(); // /
|
|
|
|
|
|
|
|
var str = this.input.substring(start, this.index);
|
|
|
|
return new CssToken(start, startingColumn, startingLine, CssTokenType.Comment, str);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
scanWhitespace(): CssToken {
|
2016-02-02 03:37:08 -05:00
|
|
|
var start = this.index;
|
|
|
|
var startingColumn = this.column;
|
|
|
|
var startingLine = this.line;
|
2016-06-21 14:10:25 -04:00
|
|
|
while (chars.isWhitespace(this.peek) && this.peek != chars.$EOF) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
var str = this.input.substring(start, this.index);
|
|
|
|
return new CssToken(start, startingColumn, startingLine, CssTokenType.Whitespace, str);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
scanString(): CssToken {
|
2016-06-08 19:38:52 -04:00
|
|
|
if (this.assertCondition(
|
|
|
|
isStringStart(this.peek, this.peekPeek), 'Unexpected non-string starting value')) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
var target = this.peek;
|
|
|
|
var start = this.index;
|
|
|
|
var startingColumn = this.column;
|
|
|
|
var startingLine = this.line;
|
|
|
|
var previous = target;
|
|
|
|
this.advance();
|
|
|
|
|
|
|
|
while (!isCharMatch(target, previous, this.peek)) {
|
2016-06-21 14:10:25 -04:00
|
|
|
if (this.peek == chars.$EOF || isNewline(this.peek)) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.error('Unterminated quote');
|
|
|
|
}
|
|
|
|
previous = this.peek;
|
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
|
2016-06-08 19:38:52 -04:00
|
|
|
if (this.assertCondition(this.peek == target, 'Unterminated quote')) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
this.advance();
|
|
|
|
|
|
|
|
var str = this.input.substring(start, this.index);
|
|
|
|
return new CssToken(start, startingColumn, startingLine, CssTokenType.String, str);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
scanNumber(): CssToken {
|
2016-02-02 03:37:08 -05:00
|
|
|
var start = this.index;
|
|
|
|
var startingColumn = this.column;
|
2016-06-21 14:10:25 -04:00
|
|
|
if (this.peek == chars.$PLUS || this.peek == chars.$MINUS) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
var periodUsed = false;
|
2016-06-21 14:10:25 -04:00
|
|
|
while (chars.isDigit(this.peek) || this.peek == chars.$PERIOD) {
|
|
|
|
if (this.peek == chars.$PERIOD) {
|
2016-02-02 03:37:08 -05:00
|
|
|
if (periodUsed) {
|
|
|
|
this.error('Unexpected use of a second period value');
|
|
|
|
}
|
|
|
|
periodUsed = true;
|
|
|
|
}
|
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
var strValue = this.input.substring(start, this.index);
|
|
|
|
return new CssToken(start, startingColumn, this.line, CssTokenType.Number, strValue);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
scanIdentifier(): CssToken {
|
2016-06-08 19:38:52 -04:00
|
|
|
if (this.assertCondition(
|
|
|
|
isIdentifierStart(this.peek, this.peekPeek), 'Expected identifier starting value')) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
var start = this.index;
|
|
|
|
var startingColumn = this.column;
|
|
|
|
while (isIdentifierPart(this.peek)) {
|
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
var strValue = this.input.substring(start, this.index);
|
|
|
|
return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
scanCssValueFunction(): CssToken {
|
2016-02-02 03:37:08 -05:00
|
|
|
var start = this.index;
|
|
|
|
var startingColumn = this.column;
|
2016-03-13 04:56:20 -04:00
|
|
|
var parenBalance = 1;
|
2016-06-21 14:10:25 -04:00
|
|
|
while (this.peek != chars.$EOF && parenBalance > 0) {
|
2016-02-02 03:37:08 -05:00
|
|
|
this.advance();
|
2016-06-21 14:10:25 -04:00
|
|
|
if (this.peek == chars.$LPAREN) {
|
2016-03-13 04:56:20 -04:00
|
|
|
parenBalance++;
|
2016-06-21 14:10:25 -04:00
|
|
|
} else if (this.peek == chars.$RPAREN) {
|
2016-03-13 04:56:20 -04:00
|
|
|
parenBalance--;
|
|
|
|
}
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
var strValue = this.input.substring(start, this.index);
|
|
|
|
return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
scanCharacter(): CssToken {
|
2016-02-02 03:37:08 -05:00
|
|
|
var start = this.index;
|
|
|
|
var startingColumn = this.column;
|
2016-06-08 19:38:52 -04:00
|
|
|
if (this.assertCondition(
|
|
|
|
isValidCssCharacter(this.peek, this._currentMode),
|
|
|
|
charStr(this.peek) + ' is not a valid CSS character')) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
var c = this.input.substring(start, start + 1);
|
|
|
|
this.advance();
|
|
|
|
|
|
|
|
return new CssToken(start, startingColumn, this.line, CssTokenType.Character, c);
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
scanAtExpression(): CssToken {
|
2016-06-21 14:10:25 -04:00
|
|
|
if (this.assertCondition(this.peek == chars.$AT, 'Expected @ value')) {
|
2016-02-02 03:37:08 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
var start = this.index;
|
|
|
|
var startingColumn = this.column;
|
|
|
|
this.advance();
|
|
|
|
if (isIdentifierStart(this.peek, this.peekPeek)) {
|
|
|
|
var ident = this.scanIdentifier();
|
|
|
|
var strValue = '@' + ident.strValue;
|
|
|
|
return new CssToken(start, startingColumn, this.line, CssTokenType.AtKeyword, strValue);
|
|
|
|
} else {
|
|
|
|
return this.scanCharacter();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
assertCondition(status: boolean, errorMessage: string): boolean {
|
|
|
|
if (!status) {
|
|
|
|
this.error(errorMessage);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
error(message: string, errorTokenValue: string = null, doNotAdvance: boolean = false): CssToken {
|
|
|
|
var index: number = this.index;
|
|
|
|
var column: number = this.column;
|
|
|
|
var line: number = this.line;
|
|
|
|
errorTokenValue =
|
|
|
|
isPresent(errorTokenValue) ? errorTokenValue : StringWrapper.fromCharCode(this.peek);
|
|
|
|
var invalidToken = new CssToken(index, column, line, CssTokenType.Invalid, errorTokenValue);
|
|
|
|
var errorMessage =
|
|
|
|
generateErrorMessage(this.input, message, errorTokenValue, index, line, column);
|
|
|
|
if (!doNotAdvance) {
|
|
|
|
this.advance();
|
|
|
|
}
|
|
|
|
this._currentError = new CssScannerError(invalidToken, errorMessage);
|
|
|
|
return invalidToken;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isCharMatch(target: number, previous: number, code: number): boolean {
|
2016-06-21 14:10:25 -04:00
|
|
|
return code == target && previous != chars.$BACKSLASH;
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isCommentStart(code: number, next: number): boolean {
|
2016-06-21 14:10:25 -04:00
|
|
|
return code == chars.$SLASH && next == chars.$STAR;
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isCommentEnd(code: number, next: number): boolean {
|
2016-06-21 14:10:25 -04:00
|
|
|
return code == chars.$STAR && next == chars.$SLASH;
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function isStringStart(code: number, next: number): boolean {
|
|
|
|
var target = code;
|
2016-06-21 14:10:25 -04:00
|
|
|
if (target == chars.$BACKSLASH) {
|
2016-02-02 03:37:08 -05:00
|
|
|
target = next;
|
|
|
|
}
|
2016-06-21 14:10:25 -04:00
|
|
|
return target == chars.$DQ || target == chars.$SQ;
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function isIdentifierStart(code: number, next: number): boolean {
|
|
|
|
var target = code;
|
2016-06-21 14:10:25 -04:00
|
|
|
if (target == chars.$MINUS) {
|
2016-02-02 03:37:08 -05:00
|
|
|
target = next;
|
|
|
|
}
|
|
|
|
|
2016-06-21 14:10:25 -04:00
|
|
|
return chars.isAsciiLetter(target) || target == chars.$BACKSLASH || target == chars.$MINUS ||
|
|
|
|
target == chars.$_;
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isIdentifierPart(target: number): boolean {
|
2016-06-21 14:10:25 -04:00
|
|
|
return chars.isAsciiLetter(target) || target == chars.$BACKSLASH || target == chars.$MINUS ||
|
|
|
|
target == chars.$_ || chars.isDigit(target);
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidPseudoSelectorCharacter(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
switch (code) {
|
2016-06-21 14:10:25 -04:00
|
|
|
case chars.$LPAREN:
|
|
|
|
case chars.$RPAREN:
|
2016-02-02 03:37:08 -05:00
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidKeyframeBlockCharacter(code: number): boolean {
|
2016-06-21 14:10:25 -04:00
|
|
|
return code == chars.$PERCENT;
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidAttributeSelectorCharacter(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
// value^*|$~=something
|
|
|
|
switch (code) {
|
2016-06-21 14:10:25 -04:00
|
|
|
case chars.$$:
|
|
|
|
case chars.$PIPE:
|
|
|
|
case chars.$CARET:
|
|
|
|
case chars.$TILDA:
|
|
|
|
case chars.$STAR:
|
|
|
|
case chars.$EQ:
|
2016-02-02 03:37:08 -05:00
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidSelectorCharacter(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
// selector [ key = value ]
|
|
|
|
// IDENT C IDENT C IDENT C
|
|
|
|
// #id, .class, *+~>
|
|
|
|
// tag:PSEUDO
|
|
|
|
switch (code) {
|
2016-06-21 14:10:25 -04:00
|
|
|
case chars.$HASH:
|
|
|
|
case chars.$PERIOD:
|
|
|
|
case chars.$TILDA:
|
|
|
|
case chars.$STAR:
|
|
|
|
case chars.$PLUS:
|
|
|
|
case chars.$GT:
|
|
|
|
case chars.$COLON:
|
|
|
|
case chars.$PIPE:
|
|
|
|
case chars.$COMMA:
|
|
|
|
case chars.$LBRACKET:
|
|
|
|
case chars.$RBRACKET:
|
2016-02-02 03:37:08 -05:00
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidStyleBlockCharacter(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
// key:value;
|
|
|
|
// key:calc(something ... )
|
|
|
|
switch (code) {
|
2016-06-21 14:10:25 -04:00
|
|
|
case chars.$HASH:
|
|
|
|
case chars.$SEMICOLON:
|
|
|
|
case chars.$COLON:
|
|
|
|
case chars.$PERCENT:
|
|
|
|
case chars.$SLASH:
|
|
|
|
case chars.$BACKSLASH:
|
|
|
|
case chars.$BANG:
|
|
|
|
case chars.$PERIOD:
|
|
|
|
case chars.$LPAREN:
|
|
|
|
case chars.$RPAREN:
|
2016-02-02 03:37:08 -05:00
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidMediaQueryRuleCharacter(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
// (min-width: 7.5em) and (orientation: landscape)
|
|
|
|
switch (code) {
|
2016-06-21 14:10:25 -04:00
|
|
|
case chars.$LPAREN:
|
|
|
|
case chars.$RPAREN:
|
|
|
|
case chars.$COLON:
|
|
|
|
case chars.$PERCENT:
|
|
|
|
case chars.$PERIOD:
|
2016-02-02 03:37:08 -05:00
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidAtRuleCharacter(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
// @document url(http://www.w3.org/page?something=on#hash),
|
|
|
|
switch (code) {
|
2016-06-21 14:10:25 -04:00
|
|
|
case chars.$LPAREN:
|
|
|
|
case chars.$RPAREN:
|
|
|
|
case chars.$COLON:
|
|
|
|
case chars.$PERCENT:
|
|
|
|
case chars.$PERIOD:
|
|
|
|
case chars.$SLASH:
|
|
|
|
case chars.$BACKSLASH:
|
|
|
|
case chars.$HASH:
|
|
|
|
case chars.$EQ:
|
|
|
|
case chars.$QUESTION:
|
|
|
|
case chars.$AMPERSAND:
|
|
|
|
case chars.$STAR:
|
|
|
|
case chars.$COMMA:
|
|
|
|
case chars.$MINUS:
|
|
|
|
case chars.$PLUS:
|
2016-02-02 03:37:08 -05:00
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidStyleFunctionCharacter(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
switch (code) {
|
2016-06-21 14:10:25 -04:00
|
|
|
case chars.$PERIOD:
|
|
|
|
case chars.$MINUS:
|
|
|
|
case chars.$PLUS:
|
|
|
|
case chars.$STAR:
|
|
|
|
case chars.$SLASH:
|
|
|
|
case chars.$LPAREN:
|
|
|
|
case chars.$RPAREN:
|
|
|
|
case chars.$COMMA:
|
2016-02-02 03:37:08 -05:00
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-28 17:25:22 -04:00
|
|
|
function isValidBlockCharacter(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
// @something { }
|
|
|
|
// IDENT
|
2016-06-21 14:10:25 -04:00
|
|
|
return code == chars.$AT;
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function isValidCssCharacter(code: number, mode: CssLexerMode): boolean {
|
|
|
|
switch (mode) {
|
|
|
|
case CssLexerMode.ALL:
|
|
|
|
case CssLexerMode.ALL_TRACK_WS:
|
|
|
|
return true;
|
|
|
|
|
|
|
|
case CssLexerMode.SELECTOR:
|
|
|
|
return isValidSelectorCharacter(code);
|
|
|
|
|
2016-06-14 19:26:57 -04:00
|
|
|
case CssLexerMode.PSEUDO_SELECTOR_WITH_ARGUMENTS:
|
2016-02-02 03:37:08 -05:00
|
|
|
return isValidPseudoSelectorCharacter(code);
|
|
|
|
|
|
|
|
case CssLexerMode.ATTRIBUTE_SELECTOR:
|
|
|
|
return isValidAttributeSelectorCharacter(code);
|
|
|
|
|
|
|
|
case CssLexerMode.MEDIA_QUERY:
|
|
|
|
return isValidMediaQueryRuleCharacter(code);
|
|
|
|
|
|
|
|
case CssLexerMode.AT_RULE_QUERY:
|
|
|
|
return isValidAtRuleCharacter(code);
|
|
|
|
|
|
|
|
case CssLexerMode.KEYFRAME_BLOCK:
|
|
|
|
return isValidKeyframeBlockCharacter(code);
|
|
|
|
|
|
|
|
case CssLexerMode.STYLE_BLOCK:
|
|
|
|
case CssLexerMode.STYLE_VALUE:
|
|
|
|
return isValidStyleBlockCharacter(code);
|
|
|
|
|
|
|
|
case CssLexerMode.STYLE_CALC_FUNCTION:
|
|
|
|
return isValidStyleFunctionCharacter(code);
|
|
|
|
|
|
|
|
case CssLexerMode.BLOCK:
|
|
|
|
return isValidBlockCharacter(code);
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-21 14:10:25 -04:00
|
|
|
function charCode(input: string, index: number): number {
|
|
|
|
return index >= input.length ? chars.$EOF : StringWrapper.charCodeAt(input, index);
|
2016-02-02 03:37:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function charStr(code: number): string {
|
|
|
|
return StringWrapper.fromCharCode(code);
|
|
|
|
}
|
|
|
|
|
2016-06-21 14:10:25 -04:00
|
|
|
export function isNewline(code: number): boolean {
|
2016-02-02 03:37:08 -05:00
|
|
|
switch (code) {
|
2016-06-21 14:10:25 -04:00
|
|
|
case chars.$FF:
|
|
|
|
case chars.$CR:
|
|
|
|
case chars.$LF:
|
|
|
|
case chars.$VTAB:
|
2016-02-02 03:37:08 -05:00
|
|
|
return true;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|