diff --git a/modules/@angular/compiler/src/css_ast.ts b/modules/@angular/compiler/src/css_ast.ts new file mode 100644 index 0000000000..7e2b1a135a --- /dev/null +++ b/modules/@angular/compiler/src/css_ast.ts @@ -0,0 +1,247 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CssToken, CssTokenType} from './css_lexer'; +import {ParseLocation, ParseSourceSpan} from './parse_util'; + +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) { + 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); + var 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 { + var mainToken = tokens[0]; + var str = mainToken.strValue; + for (var 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/modules/@angular/compiler/src/css_lexer.ts b/modules/@angular/compiler/src/css_lexer.ts index d6b9b0a7e1..86c3f7e7a6 100644 --- a/modules/@angular/compiler/src/css_lexer.ts +++ b/modules/@angular/compiler/src/css_lexer.ts @@ -205,10 +205,10 @@ export class CssScanner { } if (!isPresent(next)) { - next = new CssToken(0, 0, 0, CssTokenType.EOF, 'end of file'); + next = new CssToken(this.index, this.column, this.line, CssTokenType.EOF, 'end of file'); } - var isMatchingType: boolean; + var isMatchingType: boolean = false; if (type == CssTokenType.IdentifierOrNumber) { // TODO (matsko): implement array traversal for lookup here isMatchingType = next.type == CssTokenType.Number || next.type == CssTokenType.Identifier; diff --git a/modules/@angular/compiler/src/css_parser.ts b/modules/@angular/compiler/src/css_parser.ts index 35a8f7c33d..30e1e3a936 100644 --- a/modules/@angular/compiler/src/css_parser.ts +++ b/modules/@angular/compiler/src/css_parser.ts @@ -7,28 +7,15 @@ */ import * as chars from './chars'; -import {CssLexerMode, CssScanner, CssToken, CssTokenType, generateErrorMessage, isNewline} from './css_lexer'; +import {BlockType, CssAst, CssAtRulePredicateAst, CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssPseudoSelectorAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssSimpleSelectorAst, CssStyleSheetAst, CssStyleValueAst, CssStylesBlockAst, CssUnknownRuleAst, CssUnknownTokenListAst, mergeTokens} from './css_ast'; +import {CssLexer, CssLexerMode, CssScanner, CssToken, CssTokenType, generateErrorMessage, isNewline} from './css_lexer'; import {isPresent} from './facade/lang'; import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from './parse_util'; const SPACE_OPERATOR = ' '; export {CssToken} from './css_lexer'; - -export enum BlockType { - Import, - Charset, - Namespace, - Supports, - Keyframes, - MediaQuery, - Selector, - FontFace, - Page, - Document, - Viewport, - Unsupported -} +export {BlockType} from './css_ast'; const SLASH_CHARACTER = '/'; const GT_CHARACTER = '>'; @@ -62,16 +49,6 @@ function isSelectorOperatorCharacter(code: number): boolean { } } -function mergeTokens(tokens: CssToken[], separator: string = ''): CssToken { - var mainToken = tokens[0]; - var str = mainToken.strValue; - for (var i = 1; i < tokens.length; i++) { - str += separator + tokens[i].strValue; - } - - return new CssToken(mainToken.index, mainToken.column, mainToken.line, mainToken.type, str); -} - function getDelimFromToken(token: CssToken): number { return getDelimFromCharacter(token.numValue); } @@ -104,30 +81,6 @@ function characterContainsDelimiter(code: number, delimiters: number): boolean { return (getDelimFromCharacter(code) & delimiters) > 0; } -export abstract class CssAst { - constructor(public start: number, public end: number) {} - abstract visit(visitor: CssAstVisitor, context?: any): any; -} - -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 class ParsedCssResult { constructor(public errors: CssParseError[], public ast: CssStyleSheetAst) {} } @@ -135,9 +88,89 @@ export class ParsedCssResult { export class CssParser { private _errors: CssParseError[] = []; private _file: ParseSourceFile; + private _scanner: CssScanner; + private _lastToken: CssToken; - constructor(private _scanner: CssScanner, private _fileName: string) { - this._file = new ParseSourceFile(this._scanner.input, _fileName); + /** + * @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 { + var lexer = new CssLexer(); + this._file = new ParseSourceFile(css, url); + this._scanner = lexer.scan(css, false); + + var ast = this._parseStyleSheet(EOF_DELIM_FLAG); + + var errors = this._errors; + this._errors = []; + + var result = new ParsedCssResult(errors, ast); + this._file = null; + this._scanner = null; + return result; + } + + /** @internal */ + _parseStyleSheet(delimiters: number): CssStyleSheetAst { + var results: CssRuleAst[] = []; + this._scanner.consumeEmptyStatements(); + while (this._scanner.peek != chars.$EOF) { + this._scanner.setMode(CssLexerMode.BLOCK); + results.push(this._parseRule(delimiters)); + } + var span: ParseSourceSpan = null; + if (results.length > 0) { + var 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 isPresent(this._scanner) ? 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): ParseSourceSpan { + var startLoc: ParseLocation; + if (start instanceof CssAst) { + startLoc = start.location.start; + } else { + var token = start; + if (!isPresent(token)) { + // 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 (!isPresent(end)) { + end = this._lastToken; + } + + var endLine: number; + var endColumn: number; + var endIndex: number; + 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; + } + + var endLoc = new ParseLocation(this._file, endIndex, endLine, endColumn); + return new ParseSourceSpan(startLoc, endLoc); } /** @internal */ @@ -181,29 +214,6 @@ export class CssParser { } } - parse(): ParsedCssResult { - var delimiters: number = EOF_DELIM_FLAG; - var ast = this._parseStyleSheet(delimiters); - - var errors = this._errors; - this._errors = []; - - return new ParsedCssResult(errors, ast); - } - - /** @internal */ - _parseStyleSheet(delimiters: number): CssStyleSheetAst { - const start = this._getScannerIndex(); - var results: CssAst[] = []; - this._scanner.consumeEmptyStatements(); - while (this._scanner.peek != chars.$EOF) { - this._scanner.setMode(CssLexerMode.BLOCK); - results.push(this._parseRule(delimiters)); - } - const end = this._getScannerIndex() - 1; - return new CssStyleSheetAst(start, end, results); - } - /** @internal */ _parseRule(delimiters: number): CssRuleAst { if (this._scanner.peek == chars.$AT) { @@ -215,10 +225,10 @@ export class CssParser { /** @internal */ _parseAtRule(delimiters: number): CssRuleAst { const start = this._getScannerIndex(); - var end: number; this._scanner.setMode(CssLexerMode.BLOCK); var token = this._scan(); + var startToken = token; this._assertCondition( token.type == CssTokenType.AtKeyword, @@ -232,46 +242,55 @@ export class CssParser { case BlockType.Import: var value = this._parseValue(delimiters); this._scanner.setMode(CssLexerMode.BLOCK); - end = value.end; this._scanner.consumeEmptyStatements(); - return new CssInlineRuleAst(start, end, type, value); + var span = this._generateSourceSpan(startToken, value); + return new CssInlineRuleAst(span, type, value); case BlockType.Viewport: case BlockType.FontFace: block = this._parseStyleBlock(delimiters); - end = this._getScannerIndex() - 1; - return new CssBlockRuleAst(start, end, type, block); + var span = this._generateSourceSpan(startToken, block); + return new CssBlockRuleAst(span, type, block); case BlockType.Keyframes: var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); // keyframes only have one identifier name var name = tokens[0]; - end = this._getScannerIndex() - 1; - return new CssKeyframeRuleAst(start, end, name, this._parseKeyframeBlock(delimiters)); + var block = this._parseKeyframeBlock(delimiters); + var span = this._generateSourceSpan(startToken, block); + return new CssKeyframeRuleAst(span, name, block); case BlockType.MediaQuery: this._scanner.setMode(CssLexerMode.MEDIA_QUERY); var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); - end = this._getScannerIndex() - 1; - var strValue = this._scanner.input.substring(start, end); - var query = new CssAtRulePredicateAst(start, end, strValue, tokens); + var 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 + var end = endToken.index + endToken.strValue.length - 1; + var strValue = this._extractSourceContent(start, end); + var span = this._generateSourceSpan(startToken, endToken); + var query = new CssAtRulePredicateAst(span, strValue, tokens); block = this._parseBlock(delimiters); - end = this._getScannerIndex() - 1; - strValue = this._scanner.input.substring(start, end); - return new CssMediaQueryRuleAst(start, end, strValue, query, block); + 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); var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); - end = this._getScannerIndex() - 1; - var strValue = this._scanner.input.substring(start, end); - var query = new CssAtRulePredicateAst(start, end, strValue, tokens); + var 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 + var end = endToken.index + endToken.strValue.length - 1; + var strValue = this._extractSourceContent(start, end); + var span = this._generateSourceSpan(startToken, tokens[tokens.length - 1]); + var query = new CssAtRulePredicateAst(span, strValue, tokens); block = this._parseBlock(delimiters); - end = this._getScannerIndex() - 1; - strValue = this._scanner.input.substring(start, end); - return new CssBlockDefinitionRuleAst(start, end, strValue, type, query, block); + 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: @@ -280,8 +299,9 @@ export class CssParser { this._scanner.setMode(CssLexerMode.ALL); this._error( generateErrorMessage( - this._scanner.input, `The CSS "at" rule "${tokenName}" is not allowed to used here`, - token.strValue, token.index, token.line, token.column), + 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) @@ -292,8 +312,9 @@ export class CssParser { .forEach((token) => { listOfTokens.push(token); }); listOfTokens.push(this._consume(CssTokenType.Character, '}')); } - end = this._getScannerIndex() - 1; - return new CssUnknownRuleAst(start, end, tokenName, listOfTokens); + var endToken = listOfTokens[listOfTokens.length - 1]; + var span = this._generateSourceSpan(startToken, endToken); + return new CssUnknownRuleAst(span, tokenName, listOfTokens); } } @@ -302,23 +323,27 @@ export class CssParser { const start = this._getScannerIndex(); var selectors = this._parseSelectors(delimiters); var block = this._parseStyleBlock(delimiters); - const end = this._getScannerIndex() - 1; - var token: CssRuleAst; + var ruleAst: CssRuleAst; + var span: ParseSourceSpan; + var startSelector = selectors[0]; if (isPresent(block)) { - token = new CssSelectorRuleAst(start, end, selectors, block); + var span = this._generateSourceSpan(startSelector, block); + ruleAst = new CssSelectorRuleAst(span, selectors, block); } else { - var name = this._scanner.input.substring(start, end); + var name = this._extractSourceContent(start, this._getScannerIndex() - 1); var innerTokens: CssToken[] = []; selectors.forEach((selector: CssSelectorAst) => { selector.selectorParts.forEach((part: CssSimpleSelectorAst) => { part.tokens.forEach((token: CssToken) => { innerTokens.push(token); }); }); }); - token = new CssUnknownTokenListAst(start, end, name, innerTokens); + var 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 token; + return ruleAst; } /** @internal */ @@ -352,6 +377,7 @@ export class CssParser { if (isPresent(error)) { this._error(error.rawMessage, token); } + this._lastToken = token; return token; } @@ -366,27 +392,26 @@ export class CssParser { if (isPresent(error)) { this._error(error.rawMessage, token); } + this._lastToken = token; return token; } /** @internal */ _parseKeyframeBlock(delimiters: number): CssBlockAst { - const start = this._getScannerIndex(); - delimiters |= RBRACE_DELIM_FLAG; this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK); - this._consume(CssTokenType.Character, '{'); + var startToken = this._consume(CssTokenType.Character, '{'); var definitions: CssKeyframeDefinitionAst[] = []; while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { definitions.push(this._parseKeyframeDefinition(delimiters)); } - this._consume(CssTokenType.Character, '}'); + var endToken = this._consume(CssTokenType.Character, '}'); - const end = this._getScannerIndex() - 1; - return new CssBlockAst(start, end, definitions); + var span = this._generateSourceSpan(startToken, endToken); + return new CssBlockAst(span, definitions); } /** @internal */ @@ -400,10 +425,12 @@ export class CssParser { this._consume(CssTokenType.Character, ','); } } - var styles = this._parseStyleBlock(delimiters | RBRACE_DELIM_FLAG); + var stylesBlock = this._parseStyleBlock(delimiters | RBRACE_DELIM_FLAG); + var span = this._generateSourceSpan(stepTokens[0], stylesBlock); + var ast = new CssKeyframeDefinitionAst(span, stepTokens, stylesBlock); + this._scanner.setMode(CssLexerMode.BLOCK); - const end = this._getScannerIndex() - 1; - return new CssKeyframeDefinitionAst(start, end, stepTokens, styles); + return ast; } /** @internal */ @@ -425,8 +452,7 @@ export class CssParser { var tokens = [startToken]; if (this._scanner.peek == chars.$COLON) { // ::something - startToken = this._consume(CssTokenType.Character, ':'); - tokens.push(startToken); + tokens.push(this._consume(CssTokenType.Character, ':')); } var innerSelectors: CssSelectorAst[] = []; @@ -475,9 +501,11 @@ export class CssParser { } const end = this._getScannerIndex() - 1; - var strValue = this._scanner.input.substring(start, end); - return new CssPseudoSelectorAst( - start, end, strValue, pseudoSelectorName, tokens, innerSelectors); + var strValue = this._extractSourceContent(start, end); + + var endToken = tokens[tokens.length - 1]; + var span = this._generateSourceSpan(startToken, endToken); + return new CssPseudoSelectorAst(span, strValue, pseudoSelectorName, tokens, innerSelectors); } /** @internal */ @@ -547,12 +575,11 @@ export class CssParser { previousToken); } - var end = this._getScannerIndex(); - if (characterContainsDelimiter(this._scanner.peek, delimiters)) { - // this happens if the selector is followed by a comma or curly - // brace without a space in between - end--; - } else { + var end = this._getScannerIndex() - 1; + + // this happens if the selector is not directly followed by + // a comma or curly brace without a space in between + if (!characterContainsDelimiter(this._scanner.peek, delimiters)) { var operator: CssToken = null; var operatorScanCount = 0; var lastOperatorToken: CssToken = null; @@ -580,7 +607,7 @@ export class CssParser { let text = SLASH_CHARACTER + deepToken.strValue + deepSlash.strValue; this._error( generateErrorMessage( - this._scanner.input, `${text} is an invalid CSS operator`, text, index, + this._getSourceContent(), `${text} is an invalid CSS operator`, text, index, line, column), lastOperatorToken); token = new CssToken(index, column, line, CssTokenType.Invalid, text); @@ -600,13 +627,21 @@ export class CssParser { } operator = token; - end = this._getScannerIndex() - 1; } } + + // 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 (isPresent(operator)) { + end = operator.index; + } } this._scanner.consumeWhitespace(); + var 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 @@ -614,33 +649,42 @@ export class CssParser { operator = lastOperatorToken; } - var strValue = this._scanner.input.substring(start, end); - return new CssSimpleSelectorAst( - start, end, selectorCssTokens, strValue, pseudoSelectors, operator); + // please note that `endToken` is reassigned multiple times below + // so please do not optimize the if statements into if/elseif + var startTokenOrAst: CssToken|CssAst = null; + var endTokenOrAst: CssToken|CssAst = 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 (isPresent(operator)) { + startTokenOrAst = startTokenOrAst || operator; + endTokenOrAst = operator; + } + + var span = this._generateSourceSpan(startTokenOrAst, endTokenOrAst); + return new CssSimpleSelectorAst(span, selectorCssTokens, strValue, pseudoSelectors, operator); } /** @internal */ _parseSelector(delimiters: number): CssSelectorAst { - const start = this._getScannerIndex(); - delimiters |= COMMA_DELIM_FLAG; this._scanner.setMode(CssLexerMode.SELECTOR); var simpleSelectors: CssSimpleSelectorAst[] = []; - var end = this._getScannerIndex() - 1; while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { simpleSelectors.push(this._parseSimpleSelector(delimiters)); this._scanner.consumeWhitespace(); } - // we do this to avoid any trailing whitespace that is processed - // in order to determine the final operator value - var limit = simpleSelectors.length - 1; - if (limit >= 0) { - end = simpleSelectors[limit].end; - } - - return new CssSelectorAst(start, end, simpleSelectors); + var firstSelector = simpleSelectors[0]; + var lastSelector = simpleSelectors[simpleSelectors.length - 1]; + var span = this._generateSourceSpan(firstSelector, lastSelector); + return new CssSelectorAst(span, simpleSelectors); } /** @internal */ @@ -690,13 +734,16 @@ export class CssParser { } else if (code != chars.$RBRACE) { this._error( generateErrorMessage( - this._scanner.input, `The CSS key/value definition did not end with a semicolon`, + this._getSourceContent(), `The CSS key/value definition did not end with a semicolon`, previous.strValue, previous.index, previous.line, previous.column), previous); } - var strValue = this._scanner.input.substring(start, end); - return new CssStyleValueAst(start, end, tokens, strValue); + var strValue = this._extractSourceContent(start, end); + var startToken = tokens[0]; + var endToken = tokens[tokens.length - 1]; + var span = this._generateSourceSpan(startToken, endToken); + return new CssStyleValueAst(span, tokens, strValue); } /** @internal */ @@ -711,39 +758,35 @@ export class CssParser { /** @internal */ _parseBlock(delimiters: number): CssBlockAst { - const start = this._getScannerIndex(); - delimiters |= RBRACE_DELIM_FLAG; this._scanner.setMode(CssLexerMode.BLOCK); - this._consume(CssTokenType.Character, '{'); + var startToken = this._consume(CssTokenType.Character, '{'); this._scanner.consumeEmptyStatements(); - var results: CssAst[] = []; + var results: CssRuleAst[] = []; while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { results.push(this._parseRule(delimiters)); } - this._consume(CssTokenType.Character, '}'); + var endToken = this._consume(CssTokenType.Character, '}'); this._scanner.setMode(CssLexerMode.BLOCK); this._scanner.consumeEmptyStatements(); - const end = this._getScannerIndex() - 1; - return new CssBlockAst(start, end, results); + var span = this._generateSourceSpan(startToken, endToken); + return new CssBlockAst(span, results); } /** @internal */ _parseStyleBlock(delimiters: number): CssStylesBlockAst { - const start = this._getScannerIndex(); - delimiters |= RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG; this._scanner.setMode(CssLexerMode.STYLE_BLOCK); - var result = this._consume(CssTokenType.Character, '{'); - if (result.numValue != chars.$LBRACE) { + var startToken = this._consume(CssTokenType.Character, '{'); + if (startToken.numValue != chars.$LBRACE) { return null; } @@ -755,32 +798,28 @@ export class CssParser { this._scanner.consumeEmptyStatements(); } - this._consume(CssTokenType.Character, '}'); + var endToken = this._consume(CssTokenType.Character, '}'); this._scanner.setMode(CssLexerMode.STYLE_BLOCK); this._scanner.consumeEmptyStatements(); - const end = this._getScannerIndex() - 1; - return new CssStylesBlockAst(start, end, definitions); + var span = this._generateSourceSpan(startToken, endToken); + return new CssStylesBlockAst(span, definitions); } /** @internal */ _parseDefinition(delimiters: number): CssDefinitionAst { - const start = this._getScannerIndex(); this._scanner.setMode(CssLexerMode.STYLE_BLOCK); var prop = this._consume(CssTokenType.Identifier); - var parseValue: boolean, value: CssStyleValueAst = null; + var parseValue: boolean = false; + var value: CssStyleValueAst = null; + var 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.$COLON: - this._consume(CssTokenType.Character, ':'); - parseValue = true; - break; - case chars.$SEMICOLON: case chars.$RBRACE: case chars.$EOF: @@ -800,31 +839,31 @@ export class CssParser { remainingTokens.forEach((token) => { propStr.push(token.strValue); }); } - prop = new CssToken(prop.index, prop.column, prop.line, prop.type, propStr.join(' ')); + 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; - } else { - parseValue = false; } break; } if (parseValue) { value = this._parseValue(delimiters); + endToken = value; } else { this._error( generateErrorMessage( - this._scanner.input, `The CSS property was not paired with a style value`, + this._getSourceContent(), `The CSS property was not paired with a style value`, prop.strValue, prop.index, prop.line, prop.column), prop); } - const end = this._getScannerIndex() - 1; - return new CssDefinitionAst(start, end, prop, value); + var span = this._generateSourceSpan(prop, endToken); + return new CssDefinitionAst(span, prop, value); } /** @internal */ @@ -845,203 +884,15 @@ export class CssParser { } } -export class CssStyleValueAst extends CssAst { - constructor(start: number, end: number, public tokens: CssToken[], public strValue: string) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { return visitor.visitCssValue(this); } -} - -export abstract class CssRuleAst extends CssAst { - constructor(start: number, end: number) { super(start, end); } -} - -export class CssBlockRuleAst extends CssRuleAst { - constructor( - start: number, end: number, public type: BlockType, public block: CssBlockAst, - public name: CssToken = null) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssBlock(this.block, context); - } -} - -export class CssKeyframeRuleAst extends CssBlockRuleAst { - constructor(start: number, end: number, name: CssToken, block: CssBlockAst) { - super(start, end, BlockType.Keyframes, block, name); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssKeyframeRule(this, context); - } -} - -export class CssKeyframeDefinitionAst extends CssBlockRuleAst { - public steps: CssToken[]; - constructor(start: number, end: number, _steps: CssToken[], block: CssBlockAst) { - super(start, end, BlockType.Keyframes, block, mergeTokens(_steps, ',')); - this.steps = _steps; - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssKeyframeDefinition(this, context); - } -} - -export class CssBlockDefinitionRuleAst extends CssBlockRuleAst { - constructor( - start: number, end: number, public strValue: string, type: BlockType, - public query: CssAtRulePredicateAst, block: CssBlockAst) { - super(start, end, type, block); - var 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( - start: number, end: number, strValue: string, query: CssAtRulePredicateAst, - block: CssBlockAst) { - super(start, end, strValue, BlockType.MediaQuery, query, block); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssMediaQueryRule(this, context); - } -} - -export class CssAtRulePredicateAst extends CssAst { - constructor(start: number, end: number, public strValue: string, public tokens: CssToken[]) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssAtRulePredicate(this, context); - } -} - -export class CssInlineRuleAst extends CssRuleAst { - constructor(start: number, end: number, public type: BlockType, public value: CssStyleValueAst) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssInlineRule(this, context); - } -} - -export class CssSelectorRuleAst extends CssBlockRuleAst { - public strValue: string; - - constructor(start: number, end: number, public selectors: CssSelectorAst[], block: CssBlockAst) { - super(start, end, 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( - start: number, end: number, public property: CssToken, public value: CssStyleValueAst) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssDefinition(this, context); - } -} - -export abstract class CssSelectorPartAst extends CssAst { - constructor(start: number, end: number) { super(start, end); } -} - -export class CssSelectorAst extends CssSelectorPartAst { - public strValue: string; - constructor(start: number, end: number, public selectorParts: CssSimpleSelectorAst[]) { - super(start, end); - 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( - start: number, end: number, public tokens: CssToken[], public strValue: string, - public pseudoSelectors: CssPseudoSelectorAst[], public operator: CssToken) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssSimpleSelector(this, context); - } -} - -export class CssPseudoSelectorAst extends CssSelectorPartAst { - constructor( - start: number, end: number, public strValue: string, public name: string, - public tokens: CssToken[], public innerSelectors: CssSelectorAst[]) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssPseudoSelector(this, context); - } -} - -export class CssBlockAst extends CssAst { - constructor(start: number, end: number, public entries: CssAst[]) { super(start, end); } - 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(start: number, end: number, public definitions: CssDefinitionAst[]) { - super(start, end, definitions); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssStylesBlock(this, context); - } -} - -export class CssStyleSheetAst extends CssAst { - constructor(start: number, end: number, public rules: CssAst[]) { super(start, end); } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssStyleSheet(this, context); - } -} - export class CssParseError extends ParseError { static create( file: ParseSourceFile, offset: number, line: number, col: number, length: number, errMsg: string): CssParseError { var start = new ParseLocation(file, offset, line, col); - const end = new ParseLocation(file, offset, line, col + length); + var end = new ParseLocation(file, offset, line, col + length); var span = new ParseSourceSpan(start, end); return new CssParseError(span, 'CSS Parse Error: ' + errMsg); } constructor(span: ParseSourceSpan, message: string) { super(span, message); } } - -export class CssUnknownRuleAst extends CssRuleAst { - constructor(start: number, end: number, public ruleName: string, public tokens: CssToken[]) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssUnknownRule(this, context); - } -} - -export class CssUnknownTokenListAst extends CssRuleAst { - constructor(start: number, end: number, public name: string, public tokens: CssToken[]) { - super(start, end); - } - visit(visitor: CssAstVisitor, context?: any): any { - return visitor.visitCssUnknownTokenList(this, context); - } -} diff --git a/modules/@angular/compiler/test/css_parser_spec.ts b/modules/@angular/compiler/test/css_parser_spec.ts index 4dacf6ddcc..1dc15fa2cf 100644 --- a/modules/@angular/compiler/test/css_parser_spec.ts +++ b/modules/@angular/compiler/test/css_parser_spec.ts @@ -7,11 +7,12 @@ */ import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../core/testing/testing_internal'; -import {CssLexer} from '../src/css_lexer'; -import {BlockType, CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssParseError, CssParser, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssStyleSheetAst, CssStyleValueAst, ParsedCssResult} from '../src/css_parser'; +import {CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssStyleSheetAst, CssStyleValueAst} from '../src/css_ast'; +import {BlockType, CssParseError, CssParser, CssToken, ParsedCssResult} from '../src/css_parser'; import {BaseException} from '../src/facade/exceptions'; +import {ParseLocation} from '../src/parse_util'; -export function assertTokens(tokens: any /** TODO #9100 */, valuesArr: any /** TODO #9100 */) { +export function assertTokens(tokens: CssToken[], valuesArr: string[]) { for (var i = 0; i < tokens.length; i++) { expect(tokens[i].strValue == valuesArr[i]); } @@ -19,14 +20,11 @@ export function assertTokens(tokens: any /** TODO #9100 */, valuesArr: any /** T export function main() { describe('CssParser', () => { - function parse(css: any /** TODO #9100 */): ParsedCssResult { - var lexer = new CssLexer(); - var scanner = lexer.scan(css); - var parser = new CssParser(scanner, 'some-fake-file-name.css'); - return parser.parse(); + function parse(css: string): ParsedCssResult { + return new CssParser().parse(css, 'some-fake-css-file.css'); } - function makeAst(css: any /** TODO #9100 */): CssStyleSheetAst { + function makeAst(css: string): CssStyleSheetAst { var output = parse(css); var errors = output.errors; if (errors.length > 0) { @@ -36,11 +34,7 @@ export function main() { } it('should parse CSS into a stylesheet Ast', () => { - var styles = ` - .selector { - prop: value123; - } - `; + var styles = '.selector { prop: value123; }'; var ast = makeAst(styles); expect(ast.rules.length).toEqual(1); @@ -155,7 +149,7 @@ export function main() { expect(ast.rules.length).toEqual(1); var rule = ast.rules[0]; - assertTokens(rule.query, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']); + assertTokens(rule.query.tokens, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']); var block = rule.block; expect(block.entries.length).toEqual(1); @@ -268,7 +262,8 @@ export function main() { expect(fontFaceRule.block.entries.length).toEqual(2); var mediaQueryRule = ast.rules[2]; - assertTokens(mediaQueryRule.query, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']); + assertTokens( + mediaQueryRule.query.tokens, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']); expect(mediaQueryRule.block.entries.length).toEqual(2); }); @@ -372,7 +367,7 @@ export function main() { var rules = ast.rules; var supportsRule = rules[0]; - assertTokens(supportsRule.query, ['(', 'animation-name', ':', 'rotate', ')']); + assertTokens(supportsRule.query.tokens, ['(', 'animation-name', ':', 'rotate', ')']); expect(supportsRule.type).toEqual(BlockType.Supports); var selectorOne = supportsRule.block.entries[0]; @@ -562,6 +557,148 @@ export function main() { assertTokens(style2.value.tokens, ['white']); }); + describe('location offsets', () => { + var 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; }'; + + var output = parse(styles); + var ast = output.ast; + assertMatchesOffsetAndChar(ast.location.start, 0, '.'); + assertMatchesOffsetAndChar(ast.location.end, 111, '}'); + + var rule1 = ast.rules[0]; + assertMatchesOffsetAndChar(rule1.location.start, 0, '.'); + assertMatchesOffsetAndChar(rule1.location.end, 54, '}'); + + var rule2 = ast.rules[1]; + assertMatchesOffsetAndChar(rule2.location.start, 56, '#'); + assertMatchesOffsetAndChar(rule2.location.end, 111, '}'); + + var selector1 = rule1.selectors[0]; + assertMatchesOffsetAndChar(selector1.location.start, 0, '.'); + assertMatchesOffsetAndChar(selector1.location.end, 1, 'p'); // problem-class + + var selector2 = rule2.selectors[0]; + assertMatchesOffsetAndChar(selector2.location.start, 56, '#'); + assertMatchesOffsetAndChar(selector2.location.end, 57, 'g'); // good-boy-rule_ + + var block1 = rule1.block; + assertMatchesOffsetAndChar(block1.location.start, 15, '{'); + assertMatchesOffsetAndChar(block1.location.end, 54, '}'); + + var block2 = rule2.block; + assertMatchesOffsetAndChar(block2.location.start, 72, '{'); + assertMatchesOffsetAndChar(block2.location.end, 111, '}'); + + var block1def1 = block1.entries[0]; + assertMatchesOffsetAndChar(block1def1.location.start, 17, 'b'); // border-top-right + assertMatchesOffsetAndChar(block1def1.location.end, 36, 'p'); // px + + var block1def2 = block1.entries[1]; + assertMatchesOffsetAndChar(block1def2.location.start, 40, 'c'); // color + assertMatchesOffsetAndChar(block1def2.location.end, 47, 'w'); // white + + var block2def1 = block2.entries[0]; + assertMatchesOffsetAndChar(block2def1.location.start, 74, 'b'); // background-color + assertMatchesOffsetAndChar(block2def1.location.end, 93, 'f'); // fe4 + + var block2def2 = block2.entries[1]; + assertMatchesOffsetAndChar(block2def2.location.start, 98, 'c'); // color + assertMatchesOffsetAndChar(block2def2.location.end, 105, 't'); // teal + + var block1value1 = block1def1.value; + assertMatchesOffsetAndChar(block1value1.location.start, 35, '1'); + assertMatchesOffsetAndChar(block1value1.location.end, 36, 'p'); + + var block1value2 = block1def2.value; + assertMatchesOffsetAndChar(block1value2.location.start, 47, 'w'); + assertMatchesOffsetAndChar(block1value2.location.end, 47, 'w'); + + var block2value1 = block2def1.value; + assertMatchesOffsetAndChar(block2value1.location.start, 92, '#'); + assertMatchesOffsetAndChar(block2value1.location.end, 93, 'f'); + + var 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; } }'; + + var output = parse(styles); + var ast = output.ast; + + var mediaQuery = ast.rules[0]; + assertMatchesOffsetAndChar(mediaQuery.location.start, 0, '@'); + assertMatchesOffsetAndChar(mediaQuery.location.end, 56, '}'); + + var predicate = mediaQuery.query; + assertMatchesOffsetAndChar(predicate.location.start, 0, '@'); + assertMatchesOffsetAndChar(predicate.location.end, 32, ')'); + + var 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 += '}'; + + var output = parse(styles); + var ast = output.ast; + + var keyframes = ast.rules[0]; + assertMatchesOffsetAndChar(keyframes.location.start, 0, '@'); + assertMatchesOffsetAndChar(keyframes.location.end, 108, '}'); + + var step1 = keyframes.block.entries[0]; + assertMatchesOffsetAndChar(step1.location.start, 30, 'f'); + assertMatchesOffsetAndChar(step1.location.end, 62, '}'); + + var 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)'; + + var output = parse(styles); + var ast = output.ast; + + var rule = ast.rules[0]; + assertMatchesOffsetAndChar(rule.location.start, 0, '@'); + assertMatchesOffsetAndChar(rule.location.end, 25, ')'); + + var 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'; + + var output = parse(styles); + var 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 var styles = ` diff --git a/modules/@angular/compiler/test/css_visitor_spec.ts b/modules/@angular/compiler/test/css_visitor_spec.ts index 8ee43bc29a..f2b630e71b 100644 --- a/modules/@angular/compiler/test/css_visitor_spec.ts +++ b/modules/@angular/compiler/test/css_visitor_spec.ts @@ -7,10 +7,10 @@ */ import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../core/testing/testing_internal'; -import {CssLexer} from '../src/css_lexer'; -import {BlockType, CssAst, CssAstVisitor, CssAtRulePredicateAst, CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssParseError, CssParser, CssPseudoSelectorAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssSimpleSelectorAst, CssStyleSheetAst, CssStyleValueAst, CssStylesBlockAst, CssToken, CssUnknownRuleAst, CssUnknownTokenListAst} from '../src/css_parser'; +import {CssAst, CssAstVisitor, CssAtRulePredicateAst, CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssPseudoSelectorAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssSimpleSelectorAst, CssStyleSheetAst, CssStyleValueAst, CssStylesBlockAst, CssUnknownRuleAst, CssUnknownTokenListAst} from '../src/css_ast'; +import {BlockType, CssParseError, CssParser, CssToken} from '../src/css_parser'; import {BaseException} from '../src/facade/exceptions'; -import {NumberWrapper, StringWrapper, isPresent} from '../src/facade/lang'; +import {isPresent} from '../src/facade/lang'; function _assertTokens(tokens: CssToken[], valuesArr: string[]): void { expect(tokens.length).toEqual(valuesArr.length); @@ -115,10 +115,7 @@ function _getCaptureAst(capture: any[], index = 0): CssAst { export function main() { function parse(cssCode: string, ignoreErrors: boolean = false) { - var lexer = new CssLexer(); - var scanner = lexer.scan(cssCode); - var parser = new CssParser(scanner, 'some-fake-file-name.css'); - var output = parser.parse(); + var output = new CssParser().parse(cssCode, 'some-fake-css-file.css'); var errors = output.errors; if (errors.length > 0 && !ignoreErrors) { throw new BaseException(errors.map((error: CssParseError) => error.msg).join(', ')); @@ -127,7 +124,7 @@ export function main() { } describe('CSS parsing and visiting', () => { - var ast: any /** TODO #9100 */; + var ast: CssStyleSheetAst; var context = {}; beforeEach(() => {