From 61792cc6f5e3da6e3538506a193f3a573a7a3ef5 Mon Sep 17 00:00:00 2001 From: Scott Wang Date: Fri, 5 Jun 2020 15:37:56 -0400 Subject: [PATCH] refactor(compiler): remove unused files in css_parser/ and corresponding spec tests (#37463) Reasons for change: - css_parser, css_ast, and css_lexer are not used anywhere and there are no entry points from compiler.ts - tested by building Angular and building/running aio with build-local PR Close #37463 --- packages/compiler/src/css_parser/css_ast.ts | 264 ----- packages/compiler/src/css_parser/css_lexer.ts | 725 -------------- .../compiler/src/css_parser/css_parser.ts | 918 ------------------ packages/compiler/test/css_parser/BUILD.bazel | 30 - .../test/css_parser/css_lexer_spec.ts | 387 -------- .../test/css_parser/css_parser_spec.ts | 802 --------------- .../test/css_parser/css_visitor_spec.ts | 333 ------- 7 files changed, 3459 deletions(-) delete mode 100644 packages/compiler/src/css_parser/css_ast.ts delete mode 100644 packages/compiler/src/css_parser/css_lexer.ts delete mode 100644 packages/compiler/src/css_parser/css_parser.ts delete mode 100644 packages/compiler/test/css_parser/BUILD.bazel delete mode 100644 packages/compiler/test/css_parser/css_lexer_spec.ts delete mode 100644 packages/compiler/test/css_parser/css_parser_spec.ts delete mode 100644 packages/compiler/test/css_parser/css_visitor_spec.ts diff --git a/packages/compiler/src/css_parser/css_ast.ts b/packages/compiler/src/css_parser/css_ast.ts deleted file mode 100644 index b36b8195ab..0000000000 --- a/packages/compiler/src/css_parser/css_ast.ts +++ /dev/null @@ -1,264 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - -import {ParseLocation, ParseSourceSpan} from '../parse_util'; - -import {CssToken, CssTokenType} from './css_lexer'; - -export enum BlockType { - Import, - Charset, - Namespace, - Supports, - Keyframes, - MediaQuery, - Selector, - FontFace, - Page, - Document, - Viewport, - Unsupported -} - -export interface CssAstVisitor { - visitCssValue(ast: CssStyleValueAst, context?: any): any; - visitCssInlineRule(ast: CssInlineRuleAst, context?: any): any; - visitCssAtRulePredicate(ast: CssAtRulePredicateAst, context?: any): any; - visitCssKeyframeRule(ast: CssKeyframeRuleAst, context?: any): any; - visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAst, context?: any): any; - visitCssMediaQueryRule(ast: CssMediaQueryRuleAst, context?: any): any; - visitCssSelectorRule(ast: CssSelectorRuleAst, context?: any): any; - visitCssSelector(ast: CssSelectorAst, context?: any): any; - visitCssSimpleSelector(ast: CssSimpleSelectorAst, context?: any): any; - visitCssPseudoSelector(ast: CssPseudoSelectorAst, context?: any): any; - visitCssDefinition(ast: CssDefinitionAst, context?: any): any; - visitCssBlock(ast: CssBlockAst, context?: any): any; - visitCssStylesBlock(ast: CssStylesBlockAst, context?: any): any; - visitCssStyleSheet(ast: CssStyleSheetAst, context?: any): any; - visitCssUnknownRule(ast: CssUnknownRuleAst, context?: any): any; - visitCssUnknownTokenList(ast: CssUnknownTokenListAst, context?: any): any; -} - -export abstract class CssAst { - constructor(public location: ParseSourceSpan) {} - get start(): ParseLocation { - return this.location.start; - } - get end(): ParseLocation { - return this.location.end; - } - abstract visit(visitor: CssAstVisitor, context?: any): any; -} - -export class CssStyleValueAst extends CssAst { - constructor(location: ParseSourceSpan, public tokens: CssToken[], public strValue: string) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssValue(this); - } -} - -export abstract class CssRuleAst extends CssAst { - constructor(location: ParseSourceSpan) { - super(location); - } -} - -export class CssBlockRuleAst extends CssRuleAst { - constructor( - public location: ParseSourceSpan, public type: BlockType, public block: CssBlockAst, - public name: CssToken|null = null) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssBlock(this.block, context); - } -} - -export class CssKeyframeRuleAst extends CssBlockRuleAst { - constructor(location: ParseSourceSpan, name: CssToken, block: CssBlockAst) { - super(location, BlockType.Keyframes, block, name); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssKeyframeRule(this, context); - } -} - -export class CssKeyframeDefinitionAst extends CssBlockRuleAst { - constructor(location: ParseSourceSpan, public steps: CssToken[], block: CssBlockAst) { - super(location, BlockType.Keyframes, block, mergeTokens(steps, ',')); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssKeyframeDefinition(this, context); - } -} - -export class CssBlockDefinitionRuleAst extends CssBlockRuleAst { - constructor( - location: ParseSourceSpan, public strValue: string, type: BlockType, - public query: CssAtRulePredicateAst, block: CssBlockAst) { - super(location, type, block); - const firstCssToken: CssToken = query.tokens[0]; - this.name = new CssToken( - firstCssToken.index, firstCssToken.column, firstCssToken.line, CssTokenType.Identifier, - this.strValue); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssBlock(this.block, context); - } -} - -export class CssMediaQueryRuleAst extends CssBlockDefinitionRuleAst { - constructor( - location: ParseSourceSpan, strValue: string, query: CssAtRulePredicateAst, - block: CssBlockAst) { - super(location, strValue, BlockType.MediaQuery, query, block); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssMediaQueryRule(this, context); - } -} - -export class CssAtRulePredicateAst extends CssAst { - constructor(location: ParseSourceSpan, public strValue: string, public tokens: CssToken[]) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssAtRulePredicate(this, context); - } -} - -export class CssInlineRuleAst extends CssRuleAst { - constructor(location: ParseSourceSpan, public type: BlockType, public value: CssStyleValueAst) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssInlineRule(this, context); - } -} - -export class CssSelectorRuleAst extends CssBlockRuleAst { - public strValue: string; - - constructor(location: ParseSourceSpan, public selectors: CssSelectorAst[], block: CssBlockAst) { - super(location, BlockType.Selector, block); - this.strValue = selectors.map(selector => selector.strValue).join(','); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssSelectorRule(this, context); - } -} - -export class CssDefinitionAst extends CssAst { - constructor( - location: ParseSourceSpan, public property: CssToken, public value: CssStyleValueAst) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssDefinition(this, context); - } -} - -export abstract class CssSelectorPartAst extends CssAst { - constructor(location: ParseSourceSpan) { - super(location); - } -} - -export class CssSelectorAst extends CssSelectorPartAst { - public strValue: string; - constructor(location: ParseSourceSpan, public selectorParts: CssSimpleSelectorAst[]) { - super(location); - this.strValue = selectorParts.map(part => part.strValue).join(''); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssSelector(this, context); - } -} - -export class CssSimpleSelectorAst extends CssSelectorPartAst { - constructor( - location: ParseSourceSpan, public tokens: CssToken[], public strValue: string, - public pseudoSelectors: CssPseudoSelectorAst[], public operator: CssToken) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssSimpleSelector(this, context); - } -} - -export class CssPseudoSelectorAst extends CssSelectorPartAst { - constructor( - location: ParseSourceSpan, public strValue: string, public name: string, - public tokens: CssToken[], public innerSelectors: CssSelectorAst[]) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssPseudoSelector(this, context); - } -} - -export class CssBlockAst extends CssAst { - constructor(location: ParseSourceSpan, public entries: CssAst[]) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssBlock(this, context); - } -} - -/* - a style block is different from a standard block because it contains - css prop:value definitions. A regular block can contain a list of Ast entries. - */ -export class CssStylesBlockAst extends CssBlockAst { - constructor(location: ParseSourceSpan, public definitions: CssDefinitionAst[]) { - super(location, definitions); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssStylesBlock(this, context); - } -} - -export class CssStyleSheetAst extends CssAst { - constructor(location: ParseSourceSpan, public rules: CssAst[]) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssStyleSheet(this, context); - } -} - -export class CssUnknownRuleAst extends CssRuleAst { - constructor(location: ParseSourceSpan, public ruleName: string, public tokens: CssToken[]) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssUnknownRule(this, context); - } -} - -export class CssUnknownTokenListAst extends CssRuleAst { - constructor(location: ParseSourceSpan, public name: string, public tokens: CssToken[]) { - super(location); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssUnknownTokenList(this, context); - } -} - -export function mergeTokens(tokens: CssToken[], separator: string = ''): CssToken { - const mainToken = tokens[0]; - let str = mainToken.strValue; - for (let i = 1; i < tokens.length; i++) { - str += separator + tokens[i].strValue; - } - - return new CssToken(mainToken.index, mainToken.column, mainToken.line, mainToken.type, str); -} diff --git a/packages/compiler/src/css_parser/css_lexer.ts b/packages/compiler/src/css_parser/css_lexer.ts deleted file mode 100644 index c9142e8143..0000000000 --- a/packages/compiler/src/css_parser/css_lexer.ts +++ /dev/null @@ -1,725 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - - -import * as chars from '../chars'; - -export enum CssTokenType { - EOF, - String, - Comment, - Identifier, - Number, - IdentifierOrNumber, - AtKeyword, - Character, - Whitespace, - Invalid -} - -export enum CssLexerMode { - ALL, - ALL_TRACK_WS, - SELECTOR, - PSEUDO_SELECTOR, - PSEUDO_SELECTOR_WITH_ARGUMENTS, - 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: Error|null, public token: CssToken) {} -} - -export function generateErrorMessage( - input: string, message: string, errorValue: string, index: number, row: number, - column: number): string { - return `${message} at column ${row}:${column} in expression [` + - findProblemCode(input, errorValue, index, column) + ']'; -} - -export function findProblemCode( - input: string, errorValue: string, index: number, column: number): string { - let endOfProblemLine = index; - let current = charCode(input, index); - while (current > 0 && !isNewline(current)) { - current = charCode(input, ++endOfProblemLine); - } - const choppedString = input.substring(0, endOfProblemLine); - let pointerPadding = ''; - for (let i = 0; i < column; i++) { - pointerPadding += ' '; - } - let pointerString = ''; - for (let i = 0; i < errorValue.length; i++) { - pointerString += '^'; - } - return choppedString + '\n' + pointerPadding + pointerString + '\n'; -} - -export class CssToken { - numValue: number; - constructor( - public index: number, public column: number, public line: number, public type: CssTokenType, - public strValue: string) { - this.numValue = charCode(strValue, 0); - } -} - -export class CssLexer { - scan(text: string, trackComments: boolean = false): CssScanner { - return new CssScanner(text, trackComments); - } -} - -export function cssScannerError(token: CssToken, message: string): Error { - const error = Error('CssParseError: ' + message); - (error as any)[ERROR_RAW_MESSAGE] = message; - (error as any)[ERROR_TOKEN] = token; - return error; -} - -const ERROR_TOKEN = 'ngToken'; -const ERROR_RAW_MESSAGE = 'ngRawMessage'; - -export function getRawMessage(error: Error): string { - return (error as any)[ERROR_RAW_MESSAGE]; -} - -export function getToken(error: Error): CssToken { - return (error as any)[ERROR_TOKEN]; -} - -function _trackWhitespace(mode: CssLexerMode) { - switch (mode) { - case CssLexerMode.SELECTOR: - case CssLexerMode.PSEUDO_SELECTOR: - case CssLexerMode.ALL_TRACK_WS: - case CssLexerMode.STYLE_VALUE: - return true; - - default: - return false; - } -} - -export class CssScanner { - // TODO(issue/24571): remove '!'. - peek!: number; - peekPeek: number; - length: number = 0; - index: number = -1; - column: number = -1; - line: number = 0; - - /** @internal */ - _currentMode: CssLexerMode = CssLexerMode.BLOCK; - /** @internal */ - _currentError: Error|null = 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) { - if (_trackWhitespace(this._currentMode) && !_trackWhitespace(mode)) { - 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); - } - - peekAt(index: number): number { - return index >= this.length ? chars.$EOF : this.input.charCodeAt(index); - } - - consumeEmptyStatements(): void { - this.consumeWhitespace(); - while (this.peek == chars.$SEMICOLON) { - this.advance(); - this.consumeWhitespace(); - } - } - - consumeWhitespace(): void { - while (chars.isWhitespace(this.peek) || isNewline(this.peek)) { - this.advance(); - if (!this._trackComments && isCommentStart(this.peek, this.peekPeek)) { - this.advance(); // / - this.advance(); // * - while (!isCommentEnd(this.peek, this.peekPeek)) { - if (this.peek == chars.$EOF) { - this.error('Unterminated comment'); - } - this.advance(); - } - this.advance(); // * - this.advance(); // / - } - } - } - - consume(type: CssTokenType, value: string|null = null): LexedCssResult { - const mode = this._currentMode; - - this.setMode(_trackWhitespace(mode) ? CssLexerMode.ALL_TRACK_WS : CssLexerMode.ALL); - - const previousIndex = this.index; - const previousLine = this.line; - const previousColumn = this.column; - - let next: CssToken = undefined!; - const output = this.scan(); - if (output != null) { - // just incase the inner scan method returned an error - if (output.error != null) { - this.setMode(mode); - return output; - } - - next = output.token; - } - - if (next == null) { - next = new CssToken(this.index, this.column, this.line, CssTokenType.EOF, 'end of file'); - } - - let isMatchingType: boolean = false; - 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); - - let error: Error|null = null; - if (!isMatchingType || (value != null && value != next.strValue)) { - let errorMessage = - CssTokenType[next.type] + ' does not match expected ' + CssTokenType[type] + ' value'; - - if (value != null) { - errorMessage += ' ("' + next.strValue + '" should match "' + value + '")'; - } - - error = cssScannerError( - next, - generateErrorMessage( - this.input, errorMessage, next.strValue, previousIndex, previousLine, - previousColumn)); - } - - return new LexedCssResult(error, next); - } - - - scan(): LexedCssResult|null { - const trackWS = _trackWhitespace(this._currentMode); - if (this.index == 0 && !trackWS) { // first scan - this.consumeWhitespace(); - } - - const token = this._scan(); - if (token == null) return null; - - const error = this._currentError!; - this._currentError = null; - - if (!trackWS) { - this.consumeWhitespace(); - } - return new LexedCssResult(error, token); - } - - /** @internal */ - _scan(): CssToken|null { - let peek = this.peek; - let peekPeek = this.peekPeek; - if (peek == chars.$EOF) return null; - - if (isCommentStart(peek, peekPeek)) { - // even if comments are not tracked we still lex the - // comment so we can move the pointer forward - const commentToken = this.scanComment(); - if (this._trackComments) { - return commentToken; - } - } - - if (_trackWhitespace(this._currentMode) && (chars.isWhitespace(peek) || isNewline(peek))) { - return this.scanWhitespace(); - } - - peek = this.peek; - peekPeek = this.peekPeek; - if (peek == chars.$EOF) return null; - - if (isStringStart(peek, peekPeek)) { - return this.scanString(); - } - - // something like url(cool) - if (this._currentMode == CssLexerMode.STYLE_VALUE_FUNCTION) { - return this.scanCssValueFunction(); - } - - const isModifier = peek == chars.$PLUS || peek == chars.$MINUS; - const digitA = isModifier ? false : chars.isDigit(peek); - const digitB = chars.isDigit(peekPeek); - if (digitA || (isModifier && (peekPeek == chars.$PERIOD || digitB)) || - (peek == chars.$PERIOD && digitB)) { - return this.scanNumber(); - } - - if (peek == chars.$AT) { - return this.scanAtExpression(); - } - - if (isIdentifierStart(peek, peekPeek)) { - return this.scanIdentifier(); - } - - if (isValidCssCharacter(peek, this._currentMode)) { - return this.scanCharacter(); - } - - return this.error(`Unexpected character [${String.fromCharCode(peek)}]`); - } - - scanComment(): CssToken|null { - if (this.assertCondition( - isCommentStart(this.peek, this.peekPeek), 'Expected comment start value')) { - return null; - } - - const start = this.index; - const startingColumn = this.column; - const startingLine = this.line; - - this.advance(); // / - this.advance(); // * - - while (!isCommentEnd(this.peek, this.peekPeek)) { - if (this.peek == chars.$EOF) { - this.error('Unterminated comment'); - } - this.advance(); - } - - this.advance(); // * - this.advance(); // / - - const str = this.input.substring(start, this.index); - return new CssToken(start, startingColumn, startingLine, CssTokenType.Comment, str); - } - - scanWhitespace(): CssToken { - const start = this.index; - const startingColumn = this.column; - const startingLine = this.line; - while (chars.isWhitespace(this.peek) && this.peek != chars.$EOF) { - this.advance(); - } - const str = this.input.substring(start, this.index); - return new CssToken(start, startingColumn, startingLine, CssTokenType.Whitespace, str); - } - - scanString(): CssToken|null { - if (this.assertCondition( - isStringStart(this.peek, this.peekPeek), 'Unexpected non-string starting value')) { - return null; - } - - const target = this.peek; - const start = this.index; - const startingColumn = this.column; - const startingLine = this.line; - let previous = target; - this.advance(); - - while (!isCharMatch(target, previous, this.peek)) { - if (this.peek == chars.$EOF || isNewline(this.peek)) { - this.error('Unterminated quote'); - } - previous = this.peek; - this.advance(); - } - - if (this.assertCondition(this.peek == target, 'Unterminated quote')) { - return null; - } - this.advance(); - - const str = this.input.substring(start, this.index); - return new CssToken(start, startingColumn, startingLine, CssTokenType.String, str); - } - - scanNumber(): CssToken { - const start = this.index; - const startingColumn = this.column; - if (this.peek == chars.$PLUS || this.peek == chars.$MINUS) { - this.advance(); - } - let periodUsed = false; - while (chars.isDigit(this.peek) || this.peek == chars.$PERIOD) { - if (this.peek == chars.$PERIOD) { - if (periodUsed) { - this.error('Unexpected use of a second period value'); - } - periodUsed = true; - } - this.advance(); - } - const strValue = this.input.substring(start, this.index); - return new CssToken(start, startingColumn, this.line, CssTokenType.Number, strValue); - } - - scanIdentifier(): CssToken|null { - if (this.assertCondition( - isIdentifierStart(this.peek, this.peekPeek), 'Expected identifier starting value')) { - return null; - } - - const start = this.index; - const startingColumn = this.column; - while (isIdentifierPart(this.peek)) { - this.advance(); - } - const strValue = this.input.substring(start, this.index); - return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue); - } - - scanCssValueFunction(): CssToken { - const start = this.index; - const startingColumn = this.column; - let parenBalance = 1; - while (this.peek != chars.$EOF && parenBalance > 0) { - this.advance(); - if (this.peek == chars.$LPAREN) { - parenBalance++; - } else if (this.peek == chars.$RPAREN) { - parenBalance--; - } - } - const strValue = this.input.substring(start, this.index); - return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue); - } - - scanCharacter(): CssToken|null { - const start = this.index; - const startingColumn = this.column; - if (this.assertCondition( - isValidCssCharacter(this.peek, this._currentMode), - charStr(this.peek) + ' is not a valid CSS character')) { - return null; - } - - const c = this.input.substring(start, start + 1); - this.advance(); - - return new CssToken(start, startingColumn, this.line, CssTokenType.Character, c); - } - - scanAtExpression(): CssToken|null { - if (this.assertCondition(this.peek == chars.$AT, 'Expected @ value')) { - return null; - } - - const start = this.index; - const startingColumn = this.column; - this.advance(); - if (isIdentifierStart(this.peek, this.peekPeek)) { - const ident = this.scanIdentifier()!; - const 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 = null, doNotAdvance: boolean = false): - CssToken { - const index: number = this.index; - const column: number = this.column; - const line: number = this.line; - errorTokenValue = errorTokenValue || String.fromCharCode(this.peek); - const invalidToken = new CssToken(index, column, line, CssTokenType.Invalid, errorTokenValue); - const errorMessage = - generateErrorMessage(this.input, message, errorTokenValue, index, line, column); - if (!doNotAdvance) { - this.advance(); - } - this._currentError = cssScannerError(invalidToken, errorMessage); - return invalidToken; - } -} - -function isCharMatch(target: number, previous: number, code: number): boolean { - return code == target && previous != chars.$BACKSLASH; -} - -function isCommentStart(code: number, next: number): boolean { - return code == chars.$SLASH && next == chars.$STAR; -} - -function isCommentEnd(code: number, next: number): boolean { - return code == chars.$STAR && next == chars.$SLASH; -} - -function isStringStart(code: number, next: number): boolean { - let target = code; - if (target == chars.$BACKSLASH) { - target = next; - } - return target == chars.$DQ || target == chars.$SQ; -} - -function isIdentifierStart(code: number, next: number): boolean { - let target = code; - if (target == chars.$MINUS) { - target = next; - } - - return chars.isAsciiLetter(target) || target == chars.$BACKSLASH || target == chars.$MINUS || - target == chars.$_; -} - -function isIdentifierPart(target: number): boolean { - return chars.isAsciiLetter(target) || target == chars.$BACKSLASH || target == chars.$MINUS || - target == chars.$_ || chars.isDigit(target); -} - -function isValidPseudoSelectorCharacter(code: number): boolean { - switch (code) { - case chars.$LPAREN: - case chars.$RPAREN: - return true; - default: - return false; - } -} - -function isValidKeyframeBlockCharacter(code: number): boolean { - return code == chars.$PERCENT; -} - -function isValidAttributeSelectorCharacter(code: number): boolean { - // value^*|$~=something - switch (code) { - case chars.$$: - case chars.$PIPE: - case chars.$CARET: - case chars.$TILDA: - case chars.$STAR: - case chars.$EQ: - return true; - default: - return false; - } -} - -function isValidSelectorCharacter(code: number): boolean { - // selector [ key = value ] - // IDENT C IDENT C IDENT C - // #id, .class, *+~> - // tag:PSEUDO - switch (code) { - 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: - return true; - default: - return false; - } -} - -function isValidStyleBlockCharacter(code: number): boolean { - // key:value; - // key:calc(something ... ) - switch (code) { - 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: - return true; - default: - return false; - } -} - -function isValidMediaQueryRuleCharacter(code: number): boolean { - // (min-width: 7.5em) and (orientation: landscape) - switch (code) { - case chars.$LPAREN: - case chars.$RPAREN: - case chars.$COLON: - case chars.$PERCENT: - case chars.$PERIOD: - return true; - default: - return false; - } -} - -function isValidAtRuleCharacter(code: number): boolean { - // @document url(http://www.w3.org/page?something=on#hash), - switch (code) { - 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: - return true; - default: - return false; - } -} - -function isValidStyleFunctionCharacter(code: number): boolean { - switch (code) { - case chars.$PERIOD: - case chars.$MINUS: - case chars.$PLUS: - case chars.$STAR: - case chars.$SLASH: - case chars.$LPAREN: - case chars.$RPAREN: - case chars.$COMMA: - return true; - default: - return false; - } -} - -function isValidBlockCharacter(code: number): boolean { - // @something { } - // IDENT - return code == chars.$AT; -} - -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); - - case CssLexerMode.PSEUDO_SELECTOR_WITH_ARGUMENTS: - 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; - } -} - -function charCode(input: string, index: number): number { - return index >= input.length ? chars.$EOF : input.charCodeAt(index); -} - -function charStr(code: number): string { - return String.fromCharCode(code); -} - -export function isNewline(code: number): boolean { - switch (code) { - case chars.$FF: - case chars.$CR: - case chars.$LF: - case chars.$VTAB: - return true; - - default: - return false; - } -} diff --git a/packages/compiler/src/css_parser/css_parser.ts b/packages/compiler/src/css_parser/css_parser.ts deleted file mode 100644 index 64ba7752d5..0000000000 --- a/packages/compiler/src/css_parser/css_parser.ts +++ /dev/null @@ -1,918 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - -import * as chars from '../chars'; -import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../parse_util'; - -import {BlockType, CssAst, CssAtRulePredicateAst, CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssPseudoSelectorAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssSimpleSelectorAst, CssStylesBlockAst, CssStyleSheetAst, CssStyleValueAst, CssUnknownRuleAst, CssUnknownTokenListAst, mergeTokens} from './css_ast'; -import {CssLexer, CssLexerMode, CssScanner, CssToken, CssTokenType, generateErrorMessage, getRawMessage, isNewline} from './css_lexer'; - -const SPACE_OPERATOR = ' '; - -export {CssToken} from './css_lexer'; -export {BlockType} from './css_ast'; - -const SLASH_CHARACTER = '/'; -const GT_CHARACTER = '>'; -const TRIPLE_GT_OPERATOR_STR = '>>>'; -const DEEP_OPERATOR_STR = '/deep/'; - -const EOF_DELIM_FLAG = 1; -const RBRACE_DELIM_FLAG = 2; -const LBRACE_DELIM_FLAG = 4; -const COMMA_DELIM_FLAG = 8; -const COLON_DELIM_FLAG = 16; -const SEMICOLON_DELIM_FLAG = 32; -const NEWLINE_DELIM_FLAG = 64; -const RPAREN_DELIM_FLAG = 128; -const LPAREN_DELIM_FLAG = 256; -const SPACE_DELIM_FLAG = 512; - -function _pseudoSelectorSupportsInnerSelectors(name: string): boolean { - return ['not', 'host', 'host-context'].indexOf(name) >= 0; -} - -function isSelectorOperatorCharacter(code: number): boolean { - switch (code) { - case chars.$SLASH: - case chars.$TILDA: - case chars.$PLUS: - case chars.$GT: - return true; - default: - return chars.isWhitespace(code); - } -} - -function getDelimFromCharacter(code: number): number { - switch (code) { - case chars.$EOF: - return EOF_DELIM_FLAG; - case chars.$COMMA: - return COMMA_DELIM_FLAG; - case chars.$COLON: - return COLON_DELIM_FLAG; - case chars.$SEMICOLON: - return SEMICOLON_DELIM_FLAG; - case chars.$RBRACE: - return RBRACE_DELIM_FLAG; - case chars.$LBRACE: - return LBRACE_DELIM_FLAG; - case chars.$RPAREN: - return RPAREN_DELIM_FLAG; - case chars.$SPACE: - case chars.$TAB: - return SPACE_DELIM_FLAG; - default: - return isNewline(code) ? NEWLINE_DELIM_FLAG : 0; - } -} - -function characterContainsDelimiter(code: number, delimiters: number): boolean { - return (getDelimFromCharacter(code) & delimiters) > 0; -} - -export class ParsedCssResult { - constructor(public errors: CssParseError[], public ast: CssStyleSheetAst) {} -} - -export class CssParser { - private _errors: CssParseError[] = []; - // TODO(issue/24571): remove '!'. - private _file!: ParseSourceFile; - // TODO(issue/24571): remove '!'. - private _scanner!: CssScanner; - // TODO(issue/24571): remove '!'. - private _lastToken!: CssToken; - - /** - * @param css the CSS code that will be parsed - * @param url the name of the CSS file containing the CSS source code - */ - parse(css: string, url: string): ParsedCssResult { - const lexer = new CssLexer(); - this._file = new ParseSourceFile(css, url); - this._scanner = lexer.scan(css, false); - - const ast = this._parseStyleSheet(EOF_DELIM_FLAG); - - const errors = this._errors; - this._errors = []; - - const result = new ParsedCssResult(errors, ast); - this._file = null as any; - this._scanner = null as any; - return result; - } - - /** @internal */ - _parseStyleSheet(delimiters: number): CssStyleSheetAst { - const results: CssRuleAst[] = []; - this._scanner.consumeEmptyStatements(); - while (this._scanner.peek != chars.$EOF) { - this._scanner.setMode(CssLexerMode.BLOCK); - results.push(this._parseRule(delimiters)); - } - let span: ParseSourceSpan|null = null; - if (results.length > 0) { - const firstRule = results[0]; - // we collect the last token like so incase there was an - // EOF token that was emitted sometime during the lexing - span = this._generateSourceSpan(firstRule, this._lastToken); - } - return new CssStyleSheetAst(span!, results); - } - - /** @internal */ - _getSourceContent(): string { - return this._scanner != null ? this._scanner.input : ''; - } - - /** @internal */ - _extractSourceContent(start: number, end: number): string { - return this._getSourceContent().substring(start, end + 1); - } - - /** @internal */ - _generateSourceSpan(start: CssToken|CssAst, end: CssToken|CssAst|null = null): ParseSourceSpan { - let startLoc: ParseLocation; - if (start instanceof CssAst) { - startLoc = start.location.start; - } else { - let token = start; - if (token == null) { - // the data here is invalid, however, if and when this does - // occur, any other errors associated with this will be collected - token = this._lastToken; - } - startLoc = new ParseLocation(this._file, token.index, token.line, token.column); - } - - if (end == null) { - end = this._lastToken; - } - - let endLine: number = -1; - let endColumn: number = -1; - let endIndex: number = -1; - if (end instanceof CssAst) { - endLine = end.location.end.line!; - endColumn = end.location.end.col!; - endIndex = end.location.end.offset!; - } else if (end instanceof CssToken) { - endLine = end.line; - endColumn = end.column; - endIndex = end.index; - } - - const endLoc = new ParseLocation(this._file, endIndex, endLine, endColumn); - return new ParseSourceSpan(startLoc, endLoc); - } - - /** @internal */ - _resolveBlockType(token: CssToken): BlockType { - switch (token.strValue) { - case '@-o-keyframes': - case '@-moz-keyframes': - case '@-webkit-keyframes': - case '@keyframes': - return BlockType.Keyframes; - - case '@charset': - return BlockType.Charset; - - case '@import': - return BlockType.Import; - - case '@namespace': - return BlockType.Namespace; - - case '@page': - return BlockType.Page; - - case '@document': - return BlockType.Document; - - case '@media': - return BlockType.MediaQuery; - - case '@font-face': - return BlockType.FontFace; - - case '@viewport': - return BlockType.Viewport; - - case '@supports': - return BlockType.Supports; - - default: - return BlockType.Unsupported; - } - } - - /** @internal */ - _parseRule(delimiters: number): CssRuleAst { - if (this._scanner.peek == chars.$AT) { - return this._parseAtRule(delimiters); - } - return this._parseSelectorRule(delimiters); - } - - /** @internal */ - _parseAtRule(delimiters: number): CssRuleAst { - const start = this._getScannerIndex(); - - this._scanner.setMode(CssLexerMode.BLOCK); - const token = this._scan(); - const startToken = token; - - this._assertCondition( - token.type == CssTokenType.AtKeyword, - `The CSS Rule ${token.strValue} is not a valid [@] rule.`, token); - - let block: CssBlockAst; - const type = this._resolveBlockType(token); - let span: ParseSourceSpan; - let tokens: CssToken[]; - let endToken: CssToken; - let end: number; - let strValue: string; - let query: CssAtRulePredicateAst; - switch (type) { - case BlockType.Charset: - case BlockType.Namespace: - case BlockType.Import: - let value = this._parseValue(delimiters); - this._scanner.setMode(CssLexerMode.BLOCK); - this._scanner.consumeEmptyStatements(); - span = this._generateSourceSpan(startToken, value); - return new CssInlineRuleAst(span, type, value); - - case BlockType.Viewport: - case BlockType.FontFace: - block = this._parseStyleBlock(delimiters)!; - span = this._generateSourceSpan(startToken, block); - return new CssBlockRuleAst(span, type, block); - - case BlockType.Keyframes: - tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); - // keyframes only have one identifier name - let name = tokens[0]; - block = this._parseKeyframeBlock(delimiters); - span = this._generateSourceSpan(startToken, block); - return new CssKeyframeRuleAst(span, name, block); - - case BlockType.MediaQuery: - this._scanner.setMode(CssLexerMode.MEDIA_QUERY); - tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); - endToken = tokens[tokens.length - 1]; - // we do not track the whitespace after the mediaQuery predicate ends - // so we have to calculate the end string value on our own - end = endToken.index + endToken.strValue.length - 1; - strValue = this._extractSourceContent(start, end); - span = this._generateSourceSpan(startToken, endToken); - query = new CssAtRulePredicateAst(span, strValue, tokens); - block = this._parseBlock(delimiters); - strValue = this._extractSourceContent(start, this._getScannerIndex() - 1); - span = this._generateSourceSpan(startToken, block); - return new CssMediaQueryRuleAst(span, strValue, query, block); - - case BlockType.Document: - case BlockType.Supports: - case BlockType.Page: - this._scanner.setMode(CssLexerMode.AT_RULE_QUERY); - tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); - endToken = tokens[tokens.length - 1]; - // we do not track the whitespace after this block rule predicate ends - // so we have to calculate the end string value on our own - end = endToken.index + endToken.strValue.length - 1; - strValue = this._extractSourceContent(start, end); - span = this._generateSourceSpan(startToken, tokens[tokens.length - 1]); - query = new CssAtRulePredicateAst(span, strValue, tokens); - block = this._parseBlock(delimiters); - strValue = this._extractSourceContent(start, block.end.offset!); - span = this._generateSourceSpan(startToken, block); - return new CssBlockDefinitionRuleAst(span, strValue, type, query, block); - - // if a custom @rule { ... } is used it should still tokenize the insides - default: - let listOfTokens: CssToken[] = []; - let tokenName = token.strValue; - this._scanner.setMode(CssLexerMode.ALL); - this._error( - generateErrorMessage( - this._getSourceContent(), - `The CSS "at" rule "${tokenName}" is not allowed to used here`, token.strValue, - token.index, token.line, token.column), - token); - - this._collectUntilDelim(delimiters | LBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG) - .forEach((token) => { - listOfTokens.push(token); - }); - if (this._scanner.peek == chars.$LBRACE) { - listOfTokens.push(this._consume(CssTokenType.Character, '{')); - this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG) - .forEach((token) => { - listOfTokens.push(token); - }); - listOfTokens.push(this._consume(CssTokenType.Character, '}')); - } - endToken = listOfTokens[listOfTokens.length - 1]; - span = this._generateSourceSpan(startToken, endToken); - return new CssUnknownRuleAst(span, tokenName, listOfTokens); - } - } - - /** @internal */ - _parseSelectorRule(delimiters: number): CssRuleAst { - const start = this._getScannerIndex(); - const selectors = this._parseSelectors(delimiters); - const block = this._parseStyleBlock(delimiters); - let ruleAst: CssRuleAst; - let span: ParseSourceSpan; - const startSelector = selectors[0]; - if (block != null) { - span = this._generateSourceSpan(startSelector, block); - ruleAst = new CssSelectorRuleAst(span, selectors, block); - } else { - const name = this._extractSourceContent(start, this._getScannerIndex() - 1); - const innerTokens: CssToken[] = []; - selectors.forEach((selector: CssSelectorAst) => { - selector.selectorParts.forEach((part: CssSimpleSelectorAst) => { - part.tokens.forEach((token: CssToken) => { - innerTokens.push(token); - }); - }); - }); - const endToken = innerTokens[innerTokens.length - 1]; - span = this._generateSourceSpan(startSelector, endToken); - ruleAst = new CssUnknownTokenListAst(span, name, innerTokens); - } - this._scanner.setMode(CssLexerMode.BLOCK); - this._scanner.consumeEmptyStatements(); - return ruleAst; - } - - /** @internal */ - _parseSelectors(delimiters: number): CssSelectorAst[] { - delimiters |= LBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG; - - const selectors: CssSelectorAst[] = []; - let isParsingSelectors = true; - while (isParsingSelectors) { - selectors.push(this._parseSelector(delimiters)); - - isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters); - - if (isParsingSelectors) { - this._consume(CssTokenType.Character, ','); - isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters); - if (isParsingSelectors) { - this._scanner.consumeWhitespace(); - } - } - } - - return selectors; - } - - /** @internal */ - _scan(): CssToken { - const output = this._scanner.scan()!; - const token = output.token; - const error = output.error; - if (error != null) { - this._error(getRawMessage(error), token); - } - this._lastToken = token; - return token; - } - - /** @internal */ - _getScannerIndex(): number { - return this._scanner.index; - } - - /** @internal */ - _consume(type: CssTokenType, value: string|null = null): CssToken { - const output = this._scanner.consume(type, value); - const token = output.token; - const error = output.error; - if (error != null) { - this._error(getRawMessage(error), token); - } - this._lastToken = token; - return token; - } - - /** @internal */ - _parseKeyframeBlock(delimiters: number): CssBlockAst { - delimiters |= RBRACE_DELIM_FLAG; - this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK); - - const startToken = this._consume(CssTokenType.Character, '{'); - - const definitions: CssKeyframeDefinitionAst[] = []; - while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { - definitions.push(this._parseKeyframeDefinition(delimiters)); - } - - const endToken = this._consume(CssTokenType.Character, '}'); - - const span = this._generateSourceSpan(startToken, endToken); - return new CssBlockAst(span, definitions); - } - - /** @internal */ - _parseKeyframeDefinition(delimiters: number): CssKeyframeDefinitionAst { - const start = this._getScannerIndex(); - const stepTokens: CssToken[] = []; - delimiters |= LBRACE_DELIM_FLAG; - while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { - stepTokens.push(this._parseKeyframeLabel(delimiters | COMMA_DELIM_FLAG)); - if (this._scanner.peek != chars.$LBRACE) { - this._consume(CssTokenType.Character, ','); - } - } - const stylesBlock = this._parseStyleBlock(delimiters | RBRACE_DELIM_FLAG); - const span = this._generateSourceSpan(stepTokens[0], stylesBlock); - const ast = new CssKeyframeDefinitionAst(span, stepTokens, stylesBlock!); - - this._scanner.setMode(CssLexerMode.BLOCK); - return ast; - } - - /** @internal */ - _parseKeyframeLabel(delimiters: number): CssToken { - this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK); - return mergeTokens(this._collectUntilDelim(delimiters)); - } - - /** @internal */ - _parsePseudoSelector(delimiters: number): CssPseudoSelectorAst { - const start = this._getScannerIndex(); - - delimiters &= ~COMMA_DELIM_FLAG; - - // we keep the original value since we may use it to recurse when :not, :host are used - const startingDelims = delimiters; - - const startToken = this._consume(CssTokenType.Character, ':'); - const tokens = [startToken]; - - if (this._scanner.peek == chars.$COLON) { // ::something - tokens.push(this._consume(CssTokenType.Character, ':')); - } - - const innerSelectors: CssSelectorAst[] = []; - - this._scanner.setMode(CssLexerMode.PSEUDO_SELECTOR); - - // host, host-context, lang, not, nth-child are all identifiers - const pseudoSelectorToken = this._consume(CssTokenType.Identifier); - const pseudoSelectorName = pseudoSelectorToken.strValue; - tokens.push(pseudoSelectorToken); - - // host(), lang(), nth-child(), etc... - if (this._scanner.peek == chars.$LPAREN) { - this._scanner.setMode(CssLexerMode.PSEUDO_SELECTOR_WITH_ARGUMENTS); - - const openParenToken = this._consume(CssTokenType.Character, '('); - tokens.push(openParenToken); - - // :host(innerSelector(s)), :not(selector), etc... - if (_pseudoSelectorSupportsInnerSelectors(pseudoSelectorName)) { - let innerDelims = startingDelims | LPAREN_DELIM_FLAG | RPAREN_DELIM_FLAG; - if (pseudoSelectorName == 'not') { - // the inner selector inside of :not(...) can only be one - // CSS selector (no commas allowed) ... This is according - // to the CSS specification - innerDelims |= COMMA_DELIM_FLAG; - } - - // :host(a, b, c) { - this._parseSelectors(innerDelims).forEach((selector, index) => { - innerSelectors.push(selector); - }); - } else { - // this branch is for things like "en-us, 2k + 1, etc..." - // which all end up in pseudoSelectors like :lang, :nth-child, etc.. - const innerValueDelims = delimiters | LBRACE_DELIM_FLAG | COLON_DELIM_FLAG | - RPAREN_DELIM_FLAG | LPAREN_DELIM_FLAG; - while (!characterContainsDelimiter(this._scanner.peek, innerValueDelims)) { - const token = this._scan(); - tokens.push(token); - } - } - - const closeParenToken = this._consume(CssTokenType.Character, ')'); - tokens.push(closeParenToken); - } - - const end = this._getScannerIndex() - 1; - const strValue = this._extractSourceContent(start, end); - - const endToken = tokens[tokens.length - 1]; - const span = this._generateSourceSpan(startToken, endToken); - return new CssPseudoSelectorAst(span, strValue, pseudoSelectorName, tokens, innerSelectors); - } - - /** @internal */ - _parseSimpleSelector(delimiters: number): CssSimpleSelectorAst { - const start = this._getScannerIndex(); - - delimiters |= COMMA_DELIM_FLAG; - - this._scanner.setMode(CssLexerMode.SELECTOR); - const selectorCssTokens: CssToken[] = []; - const pseudoSelectors: CssPseudoSelectorAst[] = []; - - let previousToken: CssToken = undefined!; - - const selectorPartDelimiters = delimiters | SPACE_DELIM_FLAG; - let loopOverSelector = !characterContainsDelimiter(this._scanner.peek, selectorPartDelimiters); - - let hasAttributeError = false; - while (loopOverSelector) { - const peek = this._scanner.peek; - - switch (peek) { - case chars.$COLON: - let innerPseudo = this._parsePseudoSelector(delimiters); - pseudoSelectors.push(innerPseudo); - this._scanner.setMode(CssLexerMode.SELECTOR); - break; - - case chars.$LBRACKET: - // we set the mode after the scan because attribute mode does not - // allow attribute [] values. And this also will catch any errors - // if an extra "[" is used inside. - selectorCssTokens.push(this._scan()); - this._scanner.setMode(CssLexerMode.ATTRIBUTE_SELECTOR); - break; - - case chars.$RBRACKET: - if (this._scanner.getMode() != CssLexerMode.ATTRIBUTE_SELECTOR) { - hasAttributeError = true; - } - // we set the mode early because attribute mode does not - // allow attribute [] values - this._scanner.setMode(CssLexerMode.SELECTOR); - selectorCssTokens.push(this._scan()); - break; - - default: - if (isSelectorOperatorCharacter(peek)) { - loopOverSelector = false; - continue; - } - - let token = this._scan(); - previousToken = token; - selectorCssTokens.push(token); - break; - } - - loopOverSelector = !characterContainsDelimiter(this._scanner.peek, selectorPartDelimiters); - } - - hasAttributeError = - hasAttributeError || this._scanner.getMode() == CssLexerMode.ATTRIBUTE_SELECTOR; - if (hasAttributeError) { - this._error( - `Unbalanced CSS attribute selector at column ${previousToken.line}:${ - previousToken.column}`, - previousToken); - } - - let end = this._getScannerIndex() - 1; - - // this happens if the selector is not directly followed by - // a comma or curly brace without a space in between - let operator: CssToken|null = null; - let operatorScanCount = 0; - let lastOperatorToken: CssToken|null = null; - if (!characterContainsDelimiter(this._scanner.peek, delimiters)) { - while (operator == null && !characterContainsDelimiter(this._scanner.peek, delimiters) && - isSelectorOperatorCharacter(this._scanner.peek)) { - let token = this._scan(); - const tokenOperator = token.strValue; - operatorScanCount++; - lastOperatorToken = token; - if (tokenOperator != SPACE_OPERATOR) { - switch (tokenOperator) { - case SLASH_CHARACTER: - // /deep/ operator - let deepToken = this._consume(CssTokenType.Identifier); - let deepSlash = this._consume(CssTokenType.Character); - let index = lastOperatorToken.index; - let line = lastOperatorToken.line; - let column = lastOperatorToken.column; - if (deepToken != null && deepToken.strValue.toLowerCase() == 'deep' && - deepSlash.strValue == SLASH_CHARACTER) { - token = new CssToken( - lastOperatorToken.index, lastOperatorToken.column, lastOperatorToken.line, - CssTokenType.Identifier, DEEP_OPERATOR_STR); - } else { - const text = SLASH_CHARACTER + deepToken.strValue + deepSlash.strValue; - this._error( - generateErrorMessage( - this._getSourceContent(), `${text} is an invalid CSS operator`, text, index, - line, column), - lastOperatorToken); - token = new CssToken(index, column, line, CssTokenType.Invalid, text); - } - break; - - case GT_CHARACTER: - // >>> operator - if (this._scanner.peek == chars.$GT && this._scanner.peekPeek == chars.$GT) { - this._consume(CssTokenType.Character, GT_CHARACTER); - this._consume(CssTokenType.Character, GT_CHARACTER); - token = new CssToken( - lastOperatorToken.index, lastOperatorToken.column, lastOperatorToken.line, - CssTokenType.Identifier, TRIPLE_GT_OPERATOR_STR); - } - break; - } - - operator = token; - } - } - - // so long as there is an operator then we can have an - // ending value that is beyond the selector value ... - // otherwise it's just a bunch of trailing whitespace - if (operator != null) { - end = operator.index; - } - } - - this._scanner.consumeWhitespace(); - - const strValue = this._extractSourceContent(start, end); - - // if we do come across one or more spaces inside of - // the operators loop then an empty space is still a - // valid operator to use if something else was not found - if (operator == null && operatorScanCount > 0 && this._scanner.peek != chars.$LBRACE) { - operator = lastOperatorToken; - } - - // please note that `endToken` is reassigned multiple times below - // so please do not optimize the if statements into if/elseif - let startTokenOrAst: CssToken|CssAst|null = null; - let endTokenOrAst: CssToken|CssAst|null = null; - if (selectorCssTokens.length > 0) { - startTokenOrAst = startTokenOrAst || selectorCssTokens[0]; - endTokenOrAst = selectorCssTokens[selectorCssTokens.length - 1]; - } - if (pseudoSelectors.length > 0) { - startTokenOrAst = startTokenOrAst || pseudoSelectors[0]; - endTokenOrAst = pseudoSelectors[pseudoSelectors.length - 1]; - } - if (operator != null) { - startTokenOrAst = startTokenOrAst || operator; - endTokenOrAst = operator; - } - - const span = this._generateSourceSpan(startTokenOrAst!, endTokenOrAst); - return new CssSimpleSelectorAst(span, selectorCssTokens, strValue, pseudoSelectors, operator!); - } - - /** @internal */ - _parseSelector(delimiters: number): CssSelectorAst { - delimiters |= COMMA_DELIM_FLAG; - this._scanner.setMode(CssLexerMode.SELECTOR); - - const simpleSelectors: CssSimpleSelectorAst[] = []; - while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { - simpleSelectors.push(this._parseSimpleSelector(delimiters)); - this._scanner.consumeWhitespace(); - } - - const firstSelector = simpleSelectors[0]; - const lastSelector = simpleSelectors[simpleSelectors.length - 1]; - const span = this._generateSourceSpan(firstSelector, lastSelector); - return new CssSelectorAst(span, simpleSelectors); - } - - /** @internal */ - _parseValue(delimiters: number): CssStyleValueAst { - delimiters |= RBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG | NEWLINE_DELIM_FLAG; - - this._scanner.setMode(CssLexerMode.STYLE_VALUE); - const start = this._getScannerIndex(); - - const tokens: CssToken[] = []; - let wsStr = ''; - let previous: CssToken = undefined!; - while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { - let token: CssToken; - if (previous != null && previous.type == CssTokenType.Identifier && - this._scanner.peek == chars.$LPAREN) { - token = this._consume(CssTokenType.Character, '('); - tokens.push(token); - - this._scanner.setMode(CssLexerMode.STYLE_VALUE_FUNCTION); - - token = this._scan(); - tokens.push(token); - - this._scanner.setMode(CssLexerMode.STYLE_VALUE); - - token = this._consume(CssTokenType.Character, ')'); - tokens.push(token); - } else { - token = this._scan(); - if (token.type == CssTokenType.Whitespace) { - wsStr += token.strValue; - } else { - wsStr = ''; - tokens.push(token); - } - } - previous = token; - } - - const end = this._getScannerIndex() - 1; - this._scanner.consumeWhitespace(); - - const code = this._scanner.peek; - if (code == chars.$SEMICOLON) { - this._consume(CssTokenType.Character, ';'); - } else if (code != chars.$RBRACE) { - this._error( - generateErrorMessage( - this._getSourceContent(), `The CSS key/value definition did not end with a semicolon`, - previous.strValue, previous.index, previous.line, previous.column), - previous); - } - - const strValue = this._extractSourceContent(start, end); - const startToken = tokens[0]; - const endToken = tokens[tokens.length - 1]; - const span = this._generateSourceSpan(startToken, endToken); - return new CssStyleValueAst(span, tokens, strValue); - } - - /** @internal */ - _collectUntilDelim(delimiters: number, assertType: CssTokenType|null = null): CssToken[] { - const tokens: CssToken[] = []; - while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { - const val = assertType != null ? this._consume(assertType) : this._scan(); - tokens.push(val); - } - return tokens; - } - - /** @internal */ - _parseBlock(delimiters: number): CssBlockAst { - delimiters |= RBRACE_DELIM_FLAG; - - this._scanner.setMode(CssLexerMode.BLOCK); - - const startToken = this._consume(CssTokenType.Character, '{'); - this._scanner.consumeEmptyStatements(); - - const results: CssRuleAst[] = []; - while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { - results.push(this._parseRule(delimiters)); - } - - const endToken = this._consume(CssTokenType.Character, '}'); - - this._scanner.setMode(CssLexerMode.BLOCK); - this._scanner.consumeEmptyStatements(); - - const span = this._generateSourceSpan(startToken, endToken); - return new CssBlockAst(span, results); - } - - /** @internal */ - _parseStyleBlock(delimiters: number): CssStylesBlockAst|null { - delimiters |= RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG; - - this._scanner.setMode(CssLexerMode.STYLE_BLOCK); - - const startToken = this._consume(CssTokenType.Character, '{'); - if (startToken.numValue != chars.$LBRACE) { - return null; - } - - const definitions: CssDefinitionAst[] = []; - this._scanner.consumeEmptyStatements(); - - while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { - definitions.push(this._parseDefinition(delimiters)); - this._scanner.consumeEmptyStatements(); - } - - const endToken = this._consume(CssTokenType.Character, '}'); - - this._scanner.setMode(CssLexerMode.STYLE_BLOCK); - this._scanner.consumeEmptyStatements(); - - const span = this._generateSourceSpan(startToken, endToken); - return new CssStylesBlockAst(span, definitions); - } - - /** @internal */ - _parseDefinition(delimiters: number): CssDefinitionAst { - this._scanner.setMode(CssLexerMode.STYLE_BLOCK); - - let prop = this._consume(CssTokenType.Identifier); - let parseValue: boolean = false; - let value: CssStyleValueAst|null = null; - let endToken: CssToken|CssStyleValueAst = prop; - - // the colon value separates the prop from the style. - // there are a few cases as to what could happen if it - // is missing - switch (this._scanner.peek) { - case chars.$SEMICOLON: - case chars.$RBRACE: - case chars.$EOF: - parseValue = false; - break; - - default: - let propStr = [prop.strValue]; - if (this._scanner.peek != chars.$COLON) { - // this will throw the error - const nextValue = this._consume(CssTokenType.Character, ':'); - propStr.push(nextValue.strValue); - - const remainingTokens = this._collectUntilDelim( - delimiters | COLON_DELIM_FLAG | SEMICOLON_DELIM_FLAG, CssTokenType.Identifier); - if (remainingTokens.length > 0) { - remainingTokens.forEach((token) => { - propStr.push(token.strValue); - }); - } - - endToken = prop = - new CssToken(prop.index, prop.column, prop.line, prop.type, propStr.join(' ')); - } - - // this means we've reached the end of the definition and/or block - if (this._scanner.peek == chars.$COLON) { - this._consume(CssTokenType.Character, ':'); - parseValue = true; - } - break; - } - - if (parseValue) { - value = this._parseValue(delimiters); - endToken = value; - } else { - this._error( - generateErrorMessage( - this._getSourceContent(), `The CSS property was not paired with a style value`, - prop.strValue, prop.index, prop.line, prop.column), - prop); - } - - const span = this._generateSourceSpan(prop, endToken); - return new CssDefinitionAst(span, prop, value!); - } - - /** @internal */ - _assertCondition(status: boolean, errorMessage: string, problemToken: CssToken): boolean { - if (!status) { - this._error(errorMessage, problemToken); - return true; - } - return false; - } - - /** @internal */ - _error(message: string, problemToken: CssToken) { - const length = problemToken.strValue.length; - const error = CssParseError.create( - this._file, 0, problemToken.line, problemToken.column, length, message); - this._errors.push(error); - } -} - -export class CssParseError extends ParseError { - static create( - file: ParseSourceFile, offset: number, line: number, col: number, length: number, - errMsg: string): CssParseError { - const start = new ParseLocation(file, offset, line, col); - const end = new ParseLocation(file, offset, line, col + length); - const span = new ParseSourceSpan(start, end); - return new CssParseError(span, 'CSS Parse Error: ' + errMsg); - } - - constructor(span: ParseSourceSpan, message: string) { - super(span, message); - } -} diff --git a/packages/compiler/test/css_parser/BUILD.bazel b/packages/compiler/test/css_parser/BUILD.bazel deleted file mode 100644 index 13f19b8330..0000000000 --- a/packages/compiler/test/css_parser/BUILD.bazel +++ /dev/null @@ -1,30 +0,0 @@ -load("//tools:defaults.bzl", "jasmine_node_test", "karma_web_test_suite", "ts_library") - -ts_library( - name = "css_parser_lib", - testonly = True, - srcs = glob(["**/*.ts"]), - deps = [ - "//packages:types", - "//packages/compiler", - "//packages/compiler/testing", - "//packages/core/testing", - "//packages/platform-browser", - "//packages/platform-browser/testing", - ], -) - -jasmine_node_test( - name = "css_parser", - bootstrap = ["//tools/testing:node_es5"], - deps = [ - ":css_parser_lib", - ], -) - -karma_web_test_suite( - name = "css_parser_web", - deps = [ - ":css_parser_lib", - ], -) diff --git a/packages/compiler/test/css_parser/css_lexer_spec.ts b/packages/compiler/test/css_parser/css_lexer_spec.ts deleted file mode 100644 index 20879d7a42..0000000000 --- a/packages/compiler/test/css_parser/css_lexer_spec.ts +++ /dev/null @@ -1,387 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - -import {describe, expect, it} from '../../../core/testing/src/testing_internal'; -import {CssLexer, CssLexerMode, cssScannerError, CssToken, CssTokenType, getRawMessage, getToken} from '../../src/css_parser/css_lexer'; - -(function() { -function tokenize( - code: string, trackComments: boolean = false, - mode: CssLexerMode = CssLexerMode.ALL): CssToken[] { - const scanner = new CssLexer().scan(code, trackComments); - scanner.setMode(mode); - - const tokens: CssToken[] = []; - let output = scanner.scan(); - while (output != null) { - const error = output.error; - if (error != null) { - throw cssScannerError(getToken(error), getRawMessage(error)); - } - tokens.push(output.token); - output = scanner.scan(); - } - - return tokens; -} - -describe('CssLexer', () => { - it('should lex newline characters as whitespace when whitespace mode is on', () => { - const newlines = ['\n', '\r\n', '\r', '\f']; - newlines.forEach((line) => { - const token = tokenize(line, false, CssLexerMode.ALL_TRACK_WS)[0]; - expect(token.type).toEqual(CssTokenType.Whitespace); - }); - }); - - it('should combined newline characters as one newline token when whitespace mode is on', () => { - const newlines = ['\n', '\r\n', '\r', '\f'].join(''); - const tokens = tokenize(newlines, false, CssLexerMode.ALL_TRACK_WS); - expect(tokens.length).toEqual(1); - expect(tokens[0].type).toEqual(CssTokenType.Whitespace); - }); - - it('should not consider whitespace or newline values at all when whitespace mode is off', () => { - const newlines = ['\n', '\r\n', '\r', '\f'].join(''); - const tokens = tokenize(newlines); - expect(tokens.length).toEqual(0); - }); - - it('should lex simple selectors and their inner properties', () => { - const cssCode = '\n' + - ' .selector { my-prop: my-value; }\n'; - const tokens = tokenize(cssCode); - - expect(tokens[0].type).toEqual(CssTokenType.Character); - expect(tokens[0].strValue).toEqual('.'); - - expect(tokens[1].type).toEqual(CssTokenType.Identifier); - expect(tokens[1].strValue).toEqual('selector'); - - expect(tokens[2].type).toEqual(CssTokenType.Character); - expect(tokens[2].strValue).toEqual('{'); - - expect(tokens[3].type).toEqual(CssTokenType.Identifier); - expect(tokens[3].strValue).toEqual('my-prop'); - - expect(tokens[4].type).toEqual(CssTokenType.Character); - expect(tokens[4].strValue).toEqual(':'); - - expect(tokens[5].type).toEqual(CssTokenType.Identifier); - expect(tokens[5].strValue).toEqual('my-value'); - - expect(tokens[6].type).toEqual(CssTokenType.Character); - expect(tokens[6].strValue).toEqual(';'); - - expect(tokens[7].type).toEqual(CssTokenType.Character); - expect(tokens[7].strValue).toEqual('}'); - }); - - it('should capture the column and line values for each token', () => { - const cssCode = '#id {\n' + - ' prop:value;\n' + - '}'; - - const tokens = tokenize(cssCode); - - // # - expect(tokens[0].type).toEqual(CssTokenType.Character); - expect(tokens[0].column).toEqual(0); - expect(tokens[0].line).toEqual(0); - - // id - expect(tokens[1].type).toEqual(CssTokenType.Identifier); - expect(tokens[1].column).toEqual(1); - expect(tokens[1].line).toEqual(0); - - // { - expect(tokens[2].type).toEqual(CssTokenType.Character); - expect(tokens[2].column).toEqual(4); - expect(tokens[2].line).toEqual(0); - - // prop - expect(tokens[3].type).toEqual(CssTokenType.Identifier); - expect(tokens[3].column).toEqual(2); - expect(tokens[3].line).toEqual(1); - - // : - expect(tokens[4].type).toEqual(CssTokenType.Character); - expect(tokens[4].column).toEqual(6); - expect(tokens[4].line).toEqual(1); - - // value - expect(tokens[5].type).toEqual(CssTokenType.Identifier); - expect(tokens[5].column).toEqual(7); - expect(tokens[5].line).toEqual(1); - - // ; - expect(tokens[6].type).toEqual(CssTokenType.Character); - expect(tokens[6].column).toEqual(12); - expect(tokens[6].line).toEqual(1); - - // } - expect(tokens[7].type).toEqual(CssTokenType.Character); - expect(tokens[7].column).toEqual(0); - expect(tokens[7].line).toEqual(2); - }); - - it('should lex quoted strings and escape accordingly', () => { - const cssCode = 'prop: \'some { value } \\\' that is quoted\''; - const tokens = tokenize(cssCode); - - expect(tokens[0].type).toEqual(CssTokenType.Identifier); - expect(tokens[1].type).toEqual(CssTokenType.Character); - expect(tokens[2].type).toEqual(CssTokenType.String); - expect(tokens[2].strValue).toEqual('\'some { value } \\\' that is quoted\''); - }); - - it('should treat attribute operators as regular characters', () => { - tokenize('^|~+*').forEach((token) => { - expect(token.type).toEqual(CssTokenType.Character); - }); - }); - - it('should lex numbers properly and set them as numbers', () => { - const cssCode = '0 1 -2 3.0 -4.001'; - const tokens = tokenize(cssCode); - - expect(tokens[0].type).toEqual(CssTokenType.Number); - expect(tokens[0].strValue).toEqual('0'); - - expect(tokens[1].type).toEqual(CssTokenType.Number); - expect(tokens[1].strValue).toEqual('1'); - - expect(tokens[2].type).toEqual(CssTokenType.Number); - expect(tokens[2].strValue).toEqual('-2'); - - expect(tokens[3].type).toEqual(CssTokenType.Number); - expect(tokens[3].strValue).toEqual('3.0'); - - expect(tokens[4].type).toEqual(CssTokenType.Number); - expect(tokens[4].strValue).toEqual('-4.001'); - }); - - it('should lex @keywords', () => { - const cssCode = '@import()@something'; - const tokens = tokenize(cssCode); - - expect(tokens[0].type).toEqual(CssTokenType.AtKeyword); - expect(tokens[0].strValue).toEqual('@import'); - - expect(tokens[1].type).toEqual(CssTokenType.Character); - expect(tokens[1].strValue).toEqual('('); - - expect(tokens[2].type).toEqual(CssTokenType.Character); - expect(tokens[2].strValue).toEqual(')'); - - expect(tokens[3].type).toEqual(CssTokenType.AtKeyword); - expect(tokens[3].strValue).toEqual('@something'); - }); - - it('should still lex a number even if it has a dimension suffix', () => { - const cssCode = '40% is 40 percent'; - const tokens = tokenize(cssCode); - - expect(tokens[0].type).toEqual(CssTokenType.Number); - expect(tokens[0].strValue).toEqual('40'); - - expect(tokens[1].type).toEqual(CssTokenType.Character); - expect(tokens[1].strValue).toEqual('%'); - - expect(tokens[2].type).toEqual(CssTokenType.Identifier); - expect(tokens[2].strValue).toEqual('is'); - - expect(tokens[3].type).toEqual(CssTokenType.Number); - expect(tokens[3].strValue).toEqual('40'); - }); - - it('should allow escaped character and unicode character-strings in CSS selectors', () => { - const cssCode = '\\123456 .some\\thing \{\}'; - const tokens = tokenize(cssCode); - - expect(tokens[0].type).toEqual(CssTokenType.Identifier); - expect(tokens[0].strValue).toEqual('\\123456'); - - expect(tokens[1].type).toEqual(CssTokenType.Character); - expect(tokens[2].type).toEqual(CssTokenType.Identifier); - expect(tokens[2].strValue).toEqual('some\\thing'); - }); - - it('should distinguish identifiers and numbers from special characters', () => { - const cssCode = 'one*two=-4+three-4-equals_value$'; - const tokens = tokenize(cssCode); - - expect(tokens[0].type).toEqual(CssTokenType.Identifier); - expect(tokens[0].strValue).toEqual('one'); - - expect(tokens[1].type).toEqual(CssTokenType.Character); - expect(tokens[1].strValue).toEqual('*'); - - expect(tokens[2].type).toEqual(CssTokenType.Identifier); - expect(tokens[2].strValue).toEqual('two'); - - expect(tokens[3].type).toEqual(CssTokenType.Character); - expect(tokens[3].strValue).toEqual('='); - - expect(tokens[4].type).toEqual(CssTokenType.Number); - expect(tokens[4].strValue).toEqual('-4'); - - expect(tokens[5].type).toEqual(CssTokenType.Character); - expect(tokens[5].strValue).toEqual('+'); - - expect(tokens[6].type).toEqual(CssTokenType.Identifier); - expect(tokens[6].strValue).toEqual('three-4-equals_value'); - - expect(tokens[7].type).toEqual(CssTokenType.Character); - expect(tokens[7].strValue).toEqual('$'); - }); - - it('should filter out comments and whitespace by default', () => { - const cssCode = '.selector /* comment */ { /* value */ }'; - const tokens = tokenize(cssCode); - - expect(tokens[0].strValue).toEqual('.'); - expect(tokens[1].strValue).toEqual('selector'); - expect(tokens[2].strValue).toEqual('{'); - expect(tokens[3].strValue).toEqual('}'); - }); - - it('should track comments when the flag is set to true', () => { - const cssCode = '.selector /* comment */ { /* value */ }'; - const trackComments = true; - const tokens = tokenize(cssCode, trackComments, CssLexerMode.ALL_TRACK_WS); - - expect(tokens[0].strValue).toEqual('.'); - expect(tokens[1].strValue).toEqual('selector'); - expect(tokens[2].strValue).toEqual(' '); - - expect(tokens[3].type).toEqual(CssTokenType.Comment); - expect(tokens[3].strValue).toEqual('/* comment */'); - - expect(tokens[4].strValue).toEqual(' '); - expect(tokens[5].strValue).toEqual('{'); - expect(tokens[6].strValue).toEqual(' '); - - expect(tokens[7].type).toEqual(CssTokenType.Comment); - expect(tokens[7].strValue).toEqual('/* value */'); - }); - - describe('Selector Mode', () => { - it('should throw an error if a selector is being parsed while in the wrong mode', () => { - const cssCode = '.class > tag'; - - let capturedMessage: string|null = null; - try { - tokenize(cssCode, false, CssLexerMode.STYLE_BLOCK); - } catch (e) { - capturedMessage = getRawMessage(e); - } - - expect(capturedMessage).toMatch(/Unexpected character \[\>\] at column 0:7 in expression/g); - - capturedMessage = null; - try { - tokenize(cssCode, false, CssLexerMode.SELECTOR); - } catch (e) { - capturedMessage = getRawMessage(e); - } - - expect(capturedMessage).toEqual(null); - }); - }); - - describe('Attribute Mode', () => { - it('should consider attribute selectors as valid input and throw when an invalid modifier is used', - () => { - function tokenizeAttr(modifier: string) { - const cssCode = 'value' + modifier + '=\'something\''; - return tokenize(cssCode, false, CssLexerMode.ATTRIBUTE_SELECTOR); - } - - expect(tokenizeAttr('*').length).toEqual(4); - expect(tokenizeAttr('|').length).toEqual(4); - expect(tokenizeAttr('^').length).toEqual(4); - expect(tokenizeAttr('$').length).toEqual(4); - expect(tokenizeAttr('~').length).toEqual(4); - expect(tokenizeAttr('').length).toEqual(3); - - expect(() => { - tokenizeAttr('+'); - }).toThrow(); - }); - }); - - describe('Media Query Mode', () => { - it('should validate media queries with a reduced subset of valid characters', () => { - function tokenizeQuery(code: string) { - return tokenize(code, false, CssLexerMode.MEDIA_QUERY); - } - - // the reason why the numbers are so high is because MediaQueries keep - // track of the whitespace values - expect(tokenizeQuery('(prop: value)').length).toEqual(5); - expect(tokenizeQuery('(prop: value) and (prop2: value2)').length).toEqual(11); - expect(tokenizeQuery('tv and (prop: value)').length).toEqual(7); - expect(tokenizeQuery('print and ((prop: value) or (prop2: value2))').length).toEqual(15); - expect(tokenizeQuery('(content: \'something $ crazy inside &\')').length).toEqual(5); - - expect(() => { - tokenizeQuery('(max-height: 10 + 20)'); - }).toThrow(); - - expect(() => { - tokenizeQuery('(max-height: fifty < 100)'); - }).toThrow(); - }); - }); - - describe('Pseudo Selector Mode', () => { - it('should validate pseudo selector identifiers with a reduced subset of valid characters', - () => { - function tokenizePseudo(code: string, withArgs = false): CssToken[] { - const mode = withArgs ? CssLexerMode.PSEUDO_SELECTOR_WITH_ARGUMENTS : - CssLexerMode.PSEUDO_SELECTOR; - return tokenize(code, false, mode); - } - - expect(tokenizePseudo('hover').length).toEqual(1); - expect(tokenizePseudo('focus').length).toEqual(1); - expect(tokenizePseudo('lang(en-us)', true).length).toEqual(4); - - expect(() => { - tokenizePseudo('lang(something:broken)', true); - }).toThrow(); - - expect(() => { - tokenizePseudo('not(.selector)', true); - }).toThrow(); - }); - }); - - describe( - 'Style Block Mode', () => { - it( - 'should style blocks with a reduced subset of valid characters', () => { - function tokenizeStyles(code: string) { - return tokenize(code, false, CssLexerMode.STYLE_BLOCK); - } - - expect(tokenizeStyles(` - key: value; - prop: 100; - style: value3!important; - `).length).toEqual(14); - - expect(() => tokenizeStyles(` key$: value; `)).toThrow(); - expect(() => tokenizeStyles(` key: value$; `)).toThrow(); - expect(() => tokenizeStyles(` key: value + 10; `)).toThrow(); - expect(() => tokenizeStyles(` key: &value; `)).toThrow(); - }); - }); -}); -})(); diff --git a/packages/compiler/test/css_parser/css_parser_spec.ts b/packages/compiler/test/css_parser/css_parser_spec.ts deleted file mode 100644 index 3020ee87ee..0000000000 --- a/packages/compiler/test/css_parser/css_parser_spec.ts +++ /dev/null @@ -1,802 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - - -import {describe, expect, it} from '../../../core/testing/src/testing_internal'; -import {CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssSelectorRuleAst, CssStyleSheetAst, CssStyleValueAst} from '../../src/css_parser/css_ast'; -import {BlockType, CssParseError, CssParser, CssToken, ParsedCssResult} from '../../src/css_parser/css_parser'; -import {ParseLocation} from '../../src/parse_util'; - -export function assertTokens(tokens: CssToken[], valuesArr: string[]) { - for (let i = 0; i < tokens.length; i++) { - expect(tokens[i].strValue == valuesArr[i]); - } -} - -{ - describe('CssParser', () => { - function parse(css: string): ParsedCssResult { - return new CssParser().parse(css, 'some-fake-css-file.css'); - } - - function makeAst(css: string): CssStyleSheetAst { - const output = parse(css); - const errors = output.errors; - if (errors.length > 0) { - throw new Error(errors.map((error: CssParseError) => error.msg).join(', ')); - } - return output.ast; - } - - it('should parse CSS into a stylesheet Ast', () => { - const styles = '.selector { prop: value123; }'; - - const ast = makeAst(styles); - expect(ast.rules.length).toEqual(1); - - const rule = ast.rules[0]; - const selector = rule.selectors[0]; - expect(selector.strValue).toEqual('.selector'); - - const block: CssBlockAst = rule.block; - expect(block.entries.length).toEqual(1); - - const definition = block.entries[0]; - expect(definition.property.strValue).toEqual('prop'); - - const value = definition.value; - expect(value.tokens[0].strValue).toEqual('value123'); - }); - - it('should parse multiple CSS selectors sharing the same set of styles', () => { - const styles = ` - .class, #id, tag, [attr], key + value, * value, :-moz-any-link { - prop: value123; - } - `; - - const ast = makeAst(styles); - expect(ast.rules.length).toEqual(1); - - const rule = ast.rules[0]; - expect(rule.selectors.length).toBe(7); - - const classRule = rule.selectors[0]; - const idRule = rule.selectors[1]; - const tagRule = rule.selectors[2]; - const attrRule = rule.selectors[3]; - const plusOpRule = rule.selectors[4]; - const starOpRule = rule.selectors[5]; - const mozRule = rule.selectors[6]; - - assertTokens(classRule.selectorParts[0].tokens, ['.', 'class']); - assertTokens(idRule.selectorParts[0].tokens, ['.', 'class']); - assertTokens(attrRule.selectorParts[0].tokens, ['[', 'attr', ']']); - - assertTokens(plusOpRule.selectorParts[0].tokens, ['key']); - expect(plusOpRule.selectorParts[0].operator.strValue).toEqual('+'); - assertTokens(plusOpRule.selectorParts[1].tokens, ['value']); - - assertTokens(starOpRule.selectorParts[0].tokens, ['*']); - assertTokens(starOpRule.selectorParts[1].tokens, ['value']); - - assertTokens(mozRule.selectorParts[0].pseudoSelectors[0].tokens, [':', '-moz-any-link']); - - const style1 = rule.block.entries[0]; - expect(style1.property.strValue).toEqual('prop'); - assertTokens(style1.value.tokens, ['value123']); - }); - - it('should parse keyframe rules', () => { - const styles = ` - @keyframes rotateMe { - from { - transform: rotate(-360deg); - } - 50% { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } - `; - - const ast = makeAst(styles); - expect(ast.rules.length).toEqual(1); - - const rule = ast.rules[0]; - expect(rule.name!.strValue).toEqual('rotateMe'); - - const block = rule.block; - const fromRule = block.entries[0]; - - expect(fromRule.name!.strValue).toEqual('from'); - const fromStyle = (fromRule.block).entries[0]; - expect(fromStyle.property.strValue).toEqual('transform'); - assertTokens(fromStyle.value.tokens, ['rotate', '(', '-360', 'deg', ')']); - - const midRule = block.entries[1]; - - expect(midRule.name!.strValue).toEqual('50%'); - const midStyle = (midRule.block).entries[0]; - expect(midStyle.property.strValue).toEqual('transform'); - assertTokens(midStyle.value.tokens, ['rotate', '(', '0', 'deg', ')']); - - const toRule = block.entries[2]; - - expect(toRule.name!.strValue).toEqual('to'); - const toStyle = (toRule.block).entries[0]; - expect(toStyle.property.strValue).toEqual('transform'); - assertTokens(toStyle.value.tokens, ['rotate', '(', '360', 'deg', ')']); - }); - - it('should parse media queries into a stylesheet Ast', () => { - const styles = ` - @media all and (max-width:100px) { - .selector { - prop: value123; - } - } - `; - - const ast = makeAst(styles); - expect(ast.rules.length).toEqual(1); - - const rule = ast.rules[0]; - assertTokens(rule.query.tokens, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']); - - const block = rule.block; - expect(block.entries.length).toEqual(1); - - const rule2 = block.entries[0]; - expect(rule2.selectors[0].strValue).toEqual('.selector'); - - const block2 = rule2.block; - expect(block2.entries.length).toEqual(1); - }); - - it('should parse inline CSS values', () => { - const styles = ` - @import url('remote.css'); - @charset "UTF-8"; - @namespace ng url(http://angular.io/namespace/ng); - `; - - const ast = makeAst(styles); - - const importRule = ast.rules[0]; - expect(importRule.type).toEqual(BlockType.Import); - assertTokens(importRule.value.tokens, ['url', '(', 'remote', '.', 'css', ')']); - - const charsetRule = ast.rules[1]; - expect(charsetRule.type).toEqual(BlockType.Charset); - assertTokens(charsetRule.value.tokens, ['UTF-8']); - - const namespaceRule = ast.rules[2]; - expect(namespaceRule.type).toEqual(BlockType.Namespace); - assertTokens( - namespaceRule.value.tokens, ['ng', 'url', '(', 'http://angular.io/namespace/ng', ')']); - }); - - it('should parse CSS values that contain functions and leave the inner function data untokenized', - () => { - const styles = ` - .class { - background: url(matias.css); - animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); - height: calc(100% - 50px); - background-image: linear-gradient( 45deg, rgba(100, 0, 0, 0.5), black ); - } - `; - - const ast = makeAst(styles); - expect(ast.rules.length).toEqual(1); - - const defs = (ast.rules[0]).block.entries; - expect(defs.length).toEqual(4); - - assertTokens((defs[0]).value.tokens, ['url', '(', 'matias.css', ')']); - assertTokens( - (defs[1]).value.tokens, - ['cubic-bezier', '(', '0.755, 0.050, 0.855, 0.060', ')']); - assertTokens((defs[2]).value.tokens, ['calc', '(', '100% - 50px', ')']); - assertTokens( - (defs[3]).value.tokens, - ['linear-gradient', '(', '45deg, rgba(100, 0, 0, 0.5), black', ')']); - }); - - it('should parse un-named block-level CSS values', () => { - const styles = ` - @font-face { - font-family: "Matias"; - font-weight: bold; - src: url(font-face.ttf); - } - @viewport { - max-width: 100px; - min-height: 1000px; - } - `; - - const ast = makeAst(styles); - - const fontFaceRule = ast.rules[0]; - expect(fontFaceRule.type).toEqual(BlockType.FontFace); - expect(fontFaceRule.block.entries.length).toEqual(3); - - const viewportRule = ast.rules[1]; - expect(viewportRule.type).toEqual(BlockType.Viewport); - expect(viewportRule.block.entries.length).toEqual(2); - }); - - it('should parse multiple levels of semicolons', () => { - const styles = ` - ;;; - @import url('something something') - ;;;;;;;; - ;;;;;;;; - ;@font-face { - ;src : url(font-face.ttf);;;;;;;; - ;;;-webkit-animation:my-animation - };;; - @media all and (max-width:100px) - {; - .selector {prop: value123;}; - ;.selector2{prop:1}} - `; - - const ast = makeAst(styles); - - const importRule = ast.rules[0]; - expect(importRule.type).toEqual(BlockType.Import); - assertTokens(importRule.value.tokens, ['url', '(', 'something something', ')']); - - const fontFaceRule = ast.rules[1]; - expect(fontFaceRule.type).toEqual(BlockType.FontFace); - expect(fontFaceRule.block.entries.length).toEqual(2); - - const mediaQueryRule = ast.rules[2]; - assertTokens( - mediaQueryRule.query.tokens, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']); - expect(mediaQueryRule.block.entries.length).toEqual(2); - }); - - it('should throw an error if an unknown @value block rule is parsed', () => { - const styles = ` - @matias { hello: there; } - `; - - expect(() => { - makeAst(styles); - }).toThrowError(/^CSS Parse Error: The CSS "at" rule "@matias" is not allowed to used here/g); - }); - - it('should parse empty rules', () => { - const styles = ` - .empty-rule { } - .somewhat-empty-rule { /* property: value; */ } - .non-empty-rule { property: value; } - `; - - const ast = makeAst(styles); - - const rules = ast.rules; - expect((rules[0]).block.entries.length).toEqual(0); - expect((rules[1]).block.entries.length).toEqual(0); - expect((rules[2]).block.entries.length).toEqual(1); - }); - - it('should parse the @document rule', () => { - const styles = ` - @document url(http://www.w3.org/), - url-prefix(http://www.w3.org/Style/), - domain(mozilla.org), - regexp("https:.*") - { - /* CSS rules here apply to: - - The page "http://www.w3.org/". - - Any page whose URL begins with "http://www.w3.org/Style/" - - Any page whose URL's host is "mozilla.org" or ends with - ".mozilla.org" - - Any page whose URL starts with "https:" */ - - /* make the above-mentioned pages really ugly */ - body { - color: purple; - background: yellow; - } - } - `; - - const ast = makeAst(styles); - - const rules = ast.rules; - const documentRule = rules[0]; - expect(documentRule.type).toEqual(BlockType.Document); - - const rule = documentRule.block.entries[0]; - expect(rule.strValue).toEqual('body'); - }); - - it('should parse the @page rule', () => { - const styles = ` - @page one { - .selector { prop: value; } - } - @page two { - .selector2 { prop: value2; } - } - `; - - const ast = makeAst(styles); - - const rules = ast.rules; - - const pageRule1 = rules[0]; - expect(pageRule1.query.strValue).toEqual('@page one'); - expect(pageRule1.query.tokens[0].strValue).toEqual('one'); - expect(pageRule1.type).toEqual(BlockType.Page); - - const pageRule2 = rules[1]; - expect(pageRule2.query.strValue).toEqual('@page two'); - expect(pageRule2.query.tokens[0].strValue).toEqual('two'); - expect(pageRule2.type).toEqual(BlockType.Page); - - const selectorOne = pageRule1.block.entries[0]; - expect(selectorOne.strValue).toEqual('.selector'); - - const selectorTwo = pageRule2.block.entries[0]; - expect(selectorTwo.strValue).toEqual('.selector2'); - }); - - it('should parse the @supports rule', () => { - const styles = ` - @supports (animation-name: "rotate") { - a:hover { animation: rotate 1s; } - } - `; - - const ast = makeAst(styles); - - const rules = ast.rules; - - const supportsRule = rules[0]; - assertTokens(supportsRule.query.tokens, ['(', 'animation-name', ':', 'rotate', ')']); - expect(supportsRule.type).toEqual(BlockType.Supports); - - const selectorOne = supportsRule.block.entries[0]; - expect(selectorOne.strValue).toEqual('a:hover'); - }); - - it('should collect multiple errors during parsing', () => { - const styles = ` - .class$value { something: something } - @custom { something: something } - #id { cool^: value } - `; - - const output = parse(styles); - expect(output.errors.length).toEqual(3); - }); - - it('should recover from selector errors and continue parsing', () => { - const styles = ` - tag& { key: value; } - .%tag { key: value; } - #tag$ { key: value; } - `; - - const output = parse(styles); - const errors = output.errors; - const ast = output.ast; - - expect(errors.length).toEqual(3); - - expect(ast.rules.length).toEqual(3); - - const rule1 = ast.rules[0]; - expect(rule1.selectors[0].strValue).toEqual('tag&'); - expect(rule1.block.entries.length).toEqual(1); - - const rule2 = ast.rules[1]; - expect(rule2.selectors[0].strValue).toEqual('.%tag'); - expect(rule2.block.entries.length).toEqual(1); - - const rule3 = ast.rules[2]; - expect(rule3.selectors[0].strValue).toEqual('#tag$'); - expect(rule3.block.entries.length).toEqual(1); - }); - - it('should throw an error when parsing invalid CSS Selectors', () => { - const styles = '.class[[prop%=value}] { style: val; }'; - const output = parse(styles); - const errors = output.errors; - - expect(errors.length).toEqual(3); - - expect(errors[0].msg).toMatch(/Unexpected character \[\[\] at column 0:7/g); - - expect(errors[1].msg).toMatch(/Unexpected character \[%\] at column 0:12/g); - - expect(errors[2].msg).toMatch(/Unexpected character \[}\] at column 0:19/g); - }); - - it('should throw an error if an attribute selector is not closed properly', () => { - const styles = '.class[prop=value { style: val; }'; - const output = parse(styles); - const errors = output.errors; - - expect(errors[0].msg).toMatch(/Unbalanced CSS attribute selector at column 0:12/g); - }); - - it('should throw an error if a pseudo function selector is not closed properly', () => { - const styles = 'body:lang(en { key:value; }'; - const output = parse(styles); - const errors = output.errors; - - expect(errors[0].msg) - .toMatch(/Character does not match expected Character value \("{" should match "\)"\)/); - }); - - it('should raise an error when a semi colon is missing from a CSS style/pair that isn\'t the last entry', - () => { - const styles = `.class { - color: red - background: blue - }`; - - const output = parse(styles); - const errors = output.errors; - - expect(errors.length).toEqual(1); - - expect(errors[0].msg) - .toMatch(/The CSS key\/value definition did not end with a semicolon at column 1:15/g); - }); - - it('should parse the inner value of a :not() pseudo-selector as a CSS selector', () => { - const styles = `div:not(.ignore-this-div) { - prop: value; - }`; - - const output = parse(styles); - const errors = output.errors; - const ast = output.ast; - - expect(errors.length).toEqual(0); - - const rule1 = ast.rules[0]; - expect(rule1.selectors.length).toEqual(1); - - const simpleSelector = rule1.selectors[0].selectorParts[0]; - assertTokens(simpleSelector.tokens, ['div']); - - const pseudoSelector = simpleSelector.pseudoSelectors[0]; - expect(pseudoSelector.name).toEqual('not'); - assertTokens(pseudoSelector.tokens, ['.', 'ignore-this-div']); - }); - - it('should parse the inner selectors of a :host-context selector', () => { - const styles = `body > :host-context(.a, .b, .c:hover) { - prop: value; - }`; - - const output = parse(styles); - const errors = output.errors; - const ast = output.ast; - - expect(errors.length).toEqual(0); - - const rule1 = ast.rules[0]; - expect(rule1.selectors.length).toEqual(1); - - const simpleSelector = rule1.selectors[0].selectorParts[1]; - const innerSelectors = simpleSelector.pseudoSelectors[0].innerSelectors; - - assertTokens(innerSelectors[0].selectorParts[0].tokens, ['.', 'a']); - assertTokens(innerSelectors[1].selectorParts[0].tokens, ['.', 'b']); - - const finalSelector = innerSelectors[2].selectorParts[0]; - assertTokens(finalSelector.tokens, ['.', 'c', ':', 'hover']); - assertTokens(finalSelector.pseudoSelectors[0].tokens, [':', 'hover']); - }); - - it('should raise parse errors when CSS key/value pairs are invalid', () => { - const styles = `.class { - background color: value; - color: value - font-size; - font-weight - }`; - - const output = parse(styles); - const errors = output.errors; - - expect(errors.length).toEqual(4); - - expect(errors[0].msg) - .toMatch( - /Identifier does not match expected Character value \("color" should match ":"\) at column 1:19/g); - - expect(errors[1].msg) - .toMatch(/The CSS key\/value definition did not end with a semicolon at column 2:15/g); - - expect(errors[2].msg) - .toMatch(/The CSS property was not paired with a style value at column 3:8/g); - - expect(errors[3].msg) - .toMatch(/The CSS property was not paired with a style value at column 4:8/g); - }); - - it('should recover from CSS key/value parse errors', () => { - const styles = ` - .problem-class { background color: red; color: white; } - .good-boy-class { background-color: red; color: white; } - `; - - const output = parse(styles); - const ast = output.ast; - - expect(ast.rules.length).toEqual(2); - - const rule1 = ast.rules[0]; - expect(rule1.block.entries.length).toEqual(2); - - const style1 = rule1.block.entries[0]; - expect(style1.property.strValue).toEqual('background color'); - assertTokens(style1.value.tokens, ['red']); - - const style2 = rule1.block.entries[1]; - expect(style2.property.strValue).toEqual('color'); - assertTokens(style2.value.tokens, ['white']); - }); - - describe('location offsets', () => { - let styles: string; - - function assertMatchesOffsetAndChar( - location: ParseLocation, expectedOffset: number, expectedChar: string): void { - expect(location.offset).toEqual(expectedOffset); - expect(styles[expectedOffset]).toEqual(expectedChar); - } - - it('should collect the source span location of each AST node with regular selectors', () => { - styles = '.problem-class { border-top-right: 1px; color: white; }\n'; - styles += '#good-boy-rule_ { background-color: #fe4; color: teal; }'; - - const output = parse(styles); - const ast = output.ast; - assertMatchesOffsetAndChar(ast.location.start, 0, '.'); - assertMatchesOffsetAndChar(ast.location.end, 111, '}'); - - const rule1 = ast.rules[0]; - assertMatchesOffsetAndChar(rule1.location.start, 0, '.'); - assertMatchesOffsetAndChar(rule1.location.end, 54, '}'); - - const rule2 = ast.rules[1]; - assertMatchesOffsetAndChar(rule2.location.start, 56, '#'); - assertMatchesOffsetAndChar(rule2.location.end, 111, '}'); - - const selector1 = rule1.selectors[0]; - assertMatchesOffsetAndChar(selector1.location.start, 0, '.'); - assertMatchesOffsetAndChar(selector1.location.end, 1, 'p'); // problem-class - - const selector2 = rule2.selectors[0]; - assertMatchesOffsetAndChar(selector2.location.start, 56, '#'); - assertMatchesOffsetAndChar(selector2.location.end, 57, 'g'); // good-boy-rule_ - - const block1 = rule1.block; - assertMatchesOffsetAndChar(block1.location.start, 15, '{'); - assertMatchesOffsetAndChar(block1.location.end, 54, '}'); - - const block2 = rule2.block; - assertMatchesOffsetAndChar(block2.location.start, 72, '{'); - assertMatchesOffsetAndChar(block2.location.end, 111, '}'); - - const block1def1 = block1.entries[0]; - assertMatchesOffsetAndChar(block1def1.location.start, 17, 'b'); // border-top-right - assertMatchesOffsetAndChar(block1def1.location.end, 36, 'p'); // px - - const block1def2 = block1.entries[1]; - assertMatchesOffsetAndChar(block1def2.location.start, 40, 'c'); // color - assertMatchesOffsetAndChar(block1def2.location.end, 47, 'w'); // white - - const block2def1 = block2.entries[0]; - assertMatchesOffsetAndChar(block2def1.location.start, 74, 'b'); // background-color - assertMatchesOffsetAndChar(block2def1.location.end, 93, 'f'); // fe4 - - const block2def2 = block2.entries[1]; - assertMatchesOffsetAndChar(block2def2.location.start, 98, 'c'); // color - assertMatchesOffsetAndChar(block2def2.location.end, 105, 't'); // teal - - const block1value1 = block1def1.value; - assertMatchesOffsetAndChar(block1value1.location.start, 35, '1'); - assertMatchesOffsetAndChar(block1value1.location.end, 36, 'p'); - - const block1value2 = block1def2.value; - assertMatchesOffsetAndChar(block1value2.location.start, 47, 'w'); - assertMatchesOffsetAndChar(block1value2.location.end, 47, 'w'); - - const block2value1 = block2def1.value; - assertMatchesOffsetAndChar(block2value1.location.start, 92, '#'); - assertMatchesOffsetAndChar(block2value1.location.end, 93, 'f'); - - const block2value2 = block2def2.value; - assertMatchesOffsetAndChar(block2value2.location.start, 105, 't'); - assertMatchesOffsetAndChar(block2value2.location.end, 105, 't'); - }); - - it('should collect the source span location of each AST node with media query data', () => { - styles = '@media (all and max-width: 100px) { a { display:none; } }'; - - const output = parse(styles); - const ast = output.ast; - - const mediaQuery = ast.rules[0]; - assertMatchesOffsetAndChar(mediaQuery.location.start, 0, '@'); - assertMatchesOffsetAndChar(mediaQuery.location.end, 56, '}'); - - const predicate = mediaQuery.query; - assertMatchesOffsetAndChar(predicate.location.start, 0, '@'); - assertMatchesOffsetAndChar(predicate.location.end, 32, ')'); - - const rule = mediaQuery.block.entries[0]; - assertMatchesOffsetAndChar(rule.location.start, 36, 'a'); - assertMatchesOffsetAndChar(rule.location.end, 54, '}'); - }); - - it('should collect the source span location of each AST node with keyframe data', () => { - styles = '@keyframes rotateAndZoomOut { '; - styles += 'from { transform: rotate(0deg); } '; - styles += '100% { transform: rotate(360deg) scale(2); }'; - styles += '}'; - - const output = parse(styles); - const ast = output.ast; - - const keyframes = ast.rules[0]; - assertMatchesOffsetAndChar(keyframes.location.start, 0, '@'); - assertMatchesOffsetAndChar(keyframes.location.end, 108, '}'); - - const step1 = keyframes.block.entries[0]; - assertMatchesOffsetAndChar(step1.location.start, 30, 'f'); - assertMatchesOffsetAndChar(step1.location.end, 62, '}'); - - const step2 = keyframes.block.entries[1]; - assertMatchesOffsetAndChar(step2.location.start, 64, '1'); - assertMatchesOffsetAndChar(step2.location.end, 107, '}'); - }); - - it('should collect the source span location of each AST node with an inline rule', () => { - styles = '@import url(something.css)'; - - const output = parse(styles); - const ast = output.ast; - - const rule = ast.rules[0]; - assertMatchesOffsetAndChar(rule.location.start, 0, '@'); - assertMatchesOffsetAndChar(rule.location.end, 25, ')'); - - const value = rule.value; - assertMatchesOffsetAndChar(value.location.start, 8, 'u'); - assertMatchesOffsetAndChar(value.location.end, 25, ')'); - }); - - it('should property collect the start/end locations with an invalid stylesheet', () => { - styles = '#id { something: value'; - - const output = parse(styles); - const ast = output.ast; - - assertMatchesOffsetAndChar(ast.location.start, 0, '#'); - assertMatchesOffsetAndChar(ast.location.end, 22, undefined!); - }); - }); - - it('should parse minified CSS content properly', () => { - // this code was taken from the angular.io webpage's CSS code - const styles = ` -.is-hidden{display:none!important} -.is-visible{display:block!important} -.is-visually-hidden{height:1px;width:1px;overflow:hidden;opacity:0.01;position:absolute;bottom:0;right:0;z-index:1} -.grid-fluid,.grid-fixed{margin:0 auto} -.grid-fluid .c1,.grid-fixed .c1,.grid-fluid .c2,.grid-fixed .c2,.grid-fluid .c3,.grid-fixed .c3,.grid-fluid .c4,.grid-fixed .c4,.grid-fluid .c5,.grid-fixed .c5,.grid-fluid .c6,.grid-fixed .c6,.grid-fluid .c7,.grid-fixed .c7,.grid-fluid .c8,.grid-fixed .c8,.grid-fluid .c9,.grid-fixed .c9,.grid-fluid .c10,.grid-fixed .c10,.grid-fluid .c11,.grid-fixed .c11,.grid-fluid .c12,.grid-fixed .c12{display:inline;float:left} -.grid-fluid .c1.grid-right,.grid-fixed .c1.grid-right,.grid-fluid .c2.grid-right,.grid-fixed .c2.grid-right,.grid-fluid .c3.grid-right,.grid-fixed .c3.grid-right,.grid-fluid .c4.grid-right,.grid-fixed .c4.grid-right,.grid-fluid .c5.grid-right,.grid-fixed .c5.grid-right,.grid-fluid .c6.grid-right,.grid-fixed .c6.grid-right,.grid-fluid .c7.grid-right,.grid-fixed .c7.grid-right,.grid-fluid .c8.grid-right,.grid-fixed .c8.grid-right,.grid-fluid .c9.grid-right,.grid-fixed .c9.grid-right,.grid-fluid .c10.grid-right,.grid-fixed .c10.grid-right,.grid-fluid .c11.grid-right,.grid-fixed .c11.grid-right,.grid-fluid .c12.grid-right,.grid-fixed .c12.grid-right{float:right} -.grid-fluid .c1.nb,.grid-fixed .c1.nb,.grid-fluid .c2.nb,.grid-fixed .c2.nb,.grid-fluid .c3.nb,.grid-fixed .c3.nb,.grid-fluid .c4.nb,.grid-fixed .c4.nb,.grid-fluid .c5.nb,.grid-fixed .c5.nb,.grid-fluid .c6.nb,.grid-fixed .c6.nb,.grid-fluid .c7.nb,.grid-fixed .c7.nb,.grid-fluid .c8.nb,.grid-fixed .c8.nb,.grid-fluid .c9.nb,.grid-fixed .c9.nb,.grid-fluid .c10.nb,.grid-fixed .c10.nb,.grid-fluid .c11.nb,.grid-fixed .c11.nb,.grid-fluid .c12.nb,.grid-fixed .c12.nb{margin-left:0} -.grid-fluid .c1.na,.grid-fixed .c1.na,.grid-fluid .c2.na,.grid-fixed .c2.na,.grid-fluid .c3.na,.grid-fixed .c3.na,.grid-fluid .c4.na,.grid-fixed .c4.na,.grid-fluid .c5.na,.grid-fixed .c5.na,.grid-fluid .c6.na,.grid-fixed .c6.na,.grid-fluid .c7.na,.grid-fixed .c7.na,.grid-fluid .c8.na,.grid-fixed .c8.na,.grid-fluid .c9.na,.grid-fixed .c9.na,.grid-fluid .c10.na,.grid-fixed .c10.na,.grid-fluid .c11.na,.grid-fixed .c11.na,.grid-fluid .c12.na,.grid-fixed .c12.na{margin-right:0} - `; - - const output = parse(styles); - const errors = output.errors; - expect(errors.length).toEqual(0); - - const ast = output.ast; - expect(ast.rules.length).toEqual(8); - }); - - it('should parse a snippet of keyframe code from animate.css properly', () => { - // this code was taken from the angular.io webpage's CSS code - const styles = ` -@charset "UTF-8"; - -/*! - * animate.css -http://daneden.me/animate - * Version - 3.5.1 - * Licensed under the MIT license - http://opensource.org/licenses/MIT - * - * Copyright (c) 2016 Daniel Eden - */ - -.animated { - -webkit-animation-duration: 1s; - animation-duration: 1s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} - -.animated.infinite { - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.animated.hinge { - -webkit-animation-duration: 2s; - animation-duration: 2s; -} - -.animated.flipOutX, -.animated.flipOutY, -.animated.bounceIn, -.animated.bounceOut { - -webkit-animation-duration: .75s; - animation-duration: .75s; -} - -@-webkit-keyframes bounce { - from, 20%, 53%, 80%, to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); - animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); - -webkit-transform: translate3d(0,0,0); - transform: translate3d(0,0,0); - } - - 40%, 43% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); - animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); - -webkit-transform: translate3d(0, -30px, 0); - transform: translate3d(0, -30px, 0); - } - - 70% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); - animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); - -webkit-transform: translate3d(0, -15px, 0); - transform: translate3d(0, -15px, 0); - } - - 90% { - -webkit-transform: translate3d(0,-4px,0); - transform: translate3d(0,-4px,0); - } -} - `; - - const output = parse(styles); - const errors = output.errors; - expect(errors.length).toEqual(0); - - const ast = output.ast; - expect(ast.rules.length).toEqual(6); - - const finalRule = ast.rules[ast.rules.length - 1]; - expect(finalRule.type).toEqual(BlockType.Keyframes); - expect(finalRule.block.entries.length).toEqual(4); - }); - }); -} diff --git a/packages/compiler/test/css_parser/css_visitor_spec.ts b/packages/compiler/test/css_parser/css_visitor_spec.ts deleted file mode 100644 index 9c6d485fd0..0000000000 --- a/packages/compiler/test/css_parser/css_visitor_spec.ts +++ /dev/null @@ -1,333 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - - -import {beforeEach, describe, expect, it} from '../../../core/testing/src/testing_internal'; -import {CssAst, CssAstVisitor, CssAtRulePredicateAst, CssBlockAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssPseudoSelectorAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssSimpleSelectorAst, CssStylesBlockAst, CssStyleSheetAst, CssStyleValueAst, CssUnknownRuleAst, CssUnknownTokenListAst} from '../../src/css_parser/css_ast'; -import {BlockType, CssParseError, CssParser, CssToken} from '../../src/css_parser/css_parser'; - -function _assertTokens(tokens: CssToken[], valuesArr: string[]): void { - expect(tokens.length).toEqual(valuesArr.length); - for (let i = 0; i < tokens.length; i++) { - expect(tokens[i].strValue == valuesArr[i]); - } -} - -class MyVisitor implements CssAstVisitor { - captures: {[key: string]: any[]} = {}; - - /** - * @internal - */ - _capture(method: string, ast: CssAst, context: any) { - this.captures[method] = this.captures[method] || []; - this.captures[method].push([ast, context]); - } - - constructor(ast: CssStyleSheetAst, context: any) { - ast.visit(this, context); - } - - visitCssValue(ast: CssStyleValueAst, context: any): void { - this._capture('visitCssValue', ast, context); - } - - visitCssInlineRule(ast: CssInlineRuleAst, context: any): void { - this._capture('visitCssInlineRule', ast, context); - } - - visitCssAtRulePredicate(ast: CssAtRulePredicateAst, context: any): void { - this._capture('visitCssAtRulePredicate', ast, context); - } - - visitCssKeyframeRule(ast: CssKeyframeRuleAst, context: any): void { - this._capture('visitCssKeyframeRule', ast, context); - ast.block.visit(this, context); - } - - visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAst, context: any): void { - this._capture('visitCssKeyframeDefinition', ast, context); - ast.block.visit(this, context); - } - - visitCssMediaQueryRule(ast: CssMediaQueryRuleAst, context: any): void { - this._capture('visitCssMediaQueryRule', ast, context); - ast.query.visit(this, context); - ast.block.visit(this, context); - } - - visitCssSelectorRule(ast: CssSelectorRuleAst, context: any): void { - this._capture('visitCssSelectorRule', ast, context); - ast.selectors.forEach((selAst: CssSelectorAst) => { - selAst.visit(this, context); - }); - ast.block.visit(this, context); - } - - visitCssSelector(ast: CssSelectorAst, context: any): void { - this._capture('visitCssSelector', ast, context); - ast.selectorParts.forEach((simpleAst: CssSimpleSelectorAst) => { - simpleAst.visit(this, context); - }); - } - - visitCssSimpleSelector(ast: CssSimpleSelectorAst, context: any): void { - this._capture('visitCssSimpleSelector', ast, context); - ast.pseudoSelectors.forEach((pseudoAst: CssPseudoSelectorAst) => { - pseudoAst.visit(this, context); - }); - } - - visitCssDefinition(ast: CssDefinitionAst, context: any): void { - this._capture('visitCssDefinition', ast, context); - ast.value.visit(this, context); - } - - visitCssBlock(ast: CssBlockAst, context: any): void { - this._capture('visitCssBlock', ast, context); - ast.entries.forEach((entryAst: CssAst) => { - entryAst.visit(this, context); - }); - } - - visitCssStylesBlock(ast: CssStylesBlockAst, context: any): void { - this._capture('visitCssStylesBlock', ast, context); - ast.definitions.forEach((definitionAst: CssDefinitionAst) => { - definitionAst.visit(this, context); - }); - } - - visitCssStyleSheet(ast: CssStyleSheetAst, context: any): void { - this._capture('visitCssStyleSheet', ast, context); - ast.rules.forEach((ruleAst: CssRuleAst) => { - ruleAst.visit(this, context); - }); - } - - visitCssUnknownRule(ast: CssUnknownRuleAst, context: any): void { - this._capture('visitCssUnknownRule', ast, context); - } - - visitCssUnknownTokenList(ast: CssUnknownTokenListAst, context: any): void { - this._capture('visitCssUnknownTokenList', ast, context); - } - - visitCssPseudoSelector(ast: CssPseudoSelectorAst, context: any): void { - this._capture('visitCssPseudoSelector', ast, context); - } -} - -function _getCaptureAst(capture: any[], index = 0): CssAst { - return capture[index][0]; -} - -(function() { -function parse(cssCode: string, ignoreErrors: boolean = false) { - const output = new CssParser().parse(cssCode, 'some-fake-css-file.css'); - const errors = output.errors; - if (errors.length > 0 && !ignoreErrors) { - throw new Error(errors.map((error: CssParseError) => error.msg).join(', ')); - } - return output.ast; -} - -describe('CSS parsing and visiting', () => { - let ast: CssStyleSheetAst; - const context = {}; - - beforeEach(() => { - const cssCode = ` - .rule1 { prop1: value1 } - .rule2 { prop2: value2 } - - @media all (max-width: 100px) { - #id { prop3 :value3; } - } - - @import url(file.css); - - @keyframes rotate { - from { - prop4: value4; - } - 50%, 100% { - prop5: value5; - } - } - `; - ast = parse(cssCode); - }); - - it('should parse and visit a stylesheet', () => { - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssStyleSheet']; - - expect(captures.length).toEqual(1); - - const capture = captures[0]; - expect(capture[0]).toEqual(ast); - expect(capture[1]).toEqual(context); - }); - - it('should parse and visit each of the stylesheet selectors', () => { - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssSelectorRule']; - - expect(captures.length).toEqual(3); - - const rule1 = _getCaptureAst(captures, 0); - expect(rule1).toEqual(ast.rules[0] as CssSelectorRuleAst); - - const firstSelector = rule1.selectors[0]; - const firstSimpleSelector = firstSelector.selectorParts[0]; - _assertTokens(firstSimpleSelector.tokens, ['.', 'rule1']); - - const rule2 = _getCaptureAst(captures, 1); - expect(rule2).toEqual(ast.rules[1] as CssSelectorRuleAst); - - const secondSelector = rule2.selectors[0]; - const secondSimpleSelector = secondSelector.selectorParts[0]; - _assertTokens(secondSimpleSelector.tokens, ['.', 'rule2']); - - const rule3 = _getCaptureAst(captures, 2); - expect(rule3).toEqual( - (ast.rules[2] as CssSelectorRuleAst).block.entries[0] as CssSelectorRuleAst); - - const thirdSelector = rule3.selectors[0]; - const thirdSimpleSelector = thirdSelector.selectorParts[0]; - _assertTokens(thirdSimpleSelector.tokens, ['#', 'rule3']); - }); - - it('should parse and visit each of the stylesheet style key/value definitions', () => { - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssDefinition']; - - expect(captures.length).toEqual(5); - - const def1 = _getCaptureAst(captures, 0); - expect(def1.property.strValue).toEqual('prop1'); - expect(def1.value.tokens[0].strValue).toEqual('value1'); - - const def2 = _getCaptureAst(captures, 1); - expect(def2.property.strValue).toEqual('prop2'); - expect(def2.value.tokens[0].strValue).toEqual('value2'); - - const def3 = _getCaptureAst(captures, 2); - expect(def3.property.strValue).toEqual('prop3'); - expect(def3.value.tokens[0].strValue).toEqual('value3'); - - const def4 = _getCaptureAst(captures, 3); - expect(def4.property.strValue).toEqual('prop4'); - expect(def4.value.tokens[0].strValue).toEqual('value4'); - - const def5 = _getCaptureAst(captures, 4); - expect(def5.property.strValue).toEqual('prop5'); - expect(def5.value.tokens[0].strValue).toEqual('value5'); - }); - - it('should parse and visit the associated media query values', () => { - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssMediaQueryRule']; - - expect(captures.length).toEqual(1); - - const query1 = _getCaptureAst(captures, 0); - _assertTokens(query1.query.tokens, ['all', 'and', '(', 'max-width', '100', 'px', ')']); - expect(query1.block.entries.length).toEqual(1); - }); - - it('should capture the media query predicate', () => { - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssAtRulePredicate']; - - expect(captures.length).toEqual(1); - - const predicate = _getCaptureAst(captures, 0); - expect(predicate.strValue).toEqual('@media all (max-width: 100px)'); - }); - - it('should parse and visit the associated "@inline" rule values', () => { - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssInlineRule']; - - expect(captures.length).toEqual(1); - - const inline1 = _getCaptureAst(captures, 0); - expect(inline1.type).toEqual(BlockType.Import); - _assertTokens(inline1.value.tokens, ['url', '(', 'file.css', ')']); - }); - - it('should parse and visit the keyframe blocks', () => { - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssKeyframeRule']; - - expect(captures.length).toEqual(1); - - const keyframe1 = _getCaptureAst(captures, 0); - expect(keyframe1.name!.strValue).toEqual('rotate'); - expect(keyframe1.block.entries.length).toEqual(2); - }); - - it('should parse and visit the associated keyframe rules', () => { - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssKeyframeDefinition']; - - expect(captures.length).toEqual(2); - - const def1 = _getCaptureAst(captures, 0); - _assertTokens(def1.steps, ['from']); - expect(def1.block.entries.length).toEqual(1); - - const def2 = _getCaptureAst(captures, 1); - _assertTokens(def2.steps, ['50%', '100%']); - expect(def2.block.entries.length).toEqual(1); - }); - - it('should visit an unknown `@` rule', () => { - const cssCode = ` - @someUnknownRule param { - one two three - } - `; - ast = parse(cssCode, true); - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssUnknownRule']; - - expect(captures.length).toEqual(1); - - const rule = _getCaptureAst(captures, 0); - expect(rule.ruleName).toEqual('@someUnknownRule'); - - _assertTokens(rule.tokens, ['param', '{', 'one', 'two', 'three', '}']); - }); - - it('should collect an invalid list of tokens before a valid selector', () => { - const cssCode = 'one two three four five; selector { }'; - ast = parse(cssCode, true); - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssUnknownTokenList']; - - expect(captures.length).toEqual(1); - - const rule = _getCaptureAst(captures, 0); - _assertTokens(rule.tokens, ['one', 'two', 'three', 'four', 'five']); - }); - - it('should collect an invalid list of tokens after a valid selector', () => { - const cssCode = 'selector { } six seven eight'; - ast = parse(cssCode, true); - const visitor = new MyVisitor(ast, context); - const captures = visitor.captures['visitCssUnknownTokenList']; - - expect(captures.length).toEqual(1); - - const rule = _getCaptureAst(captures, 0); - _assertTokens(rule.tokens, ['six', 'seven', 'eight']); - }); -}); -})();