refactor(core): ensure CSS parser uses ParseSourceSpan to track ast locations

This commit also fixes up any remaining TODO comments.

Closes #9778
This commit is contained in:
Matias Niemelä 2016-06-16 13:02:18 -07:00
parent 0ed7773223
commit 3fe1cb0253
5 changed files with 618 additions and 386 deletions

View File

@ -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);
}

View File

@ -205,10 +205,10 @@ export class CssScanner {
} }
if (!isPresent(next)) { 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) { if (type == CssTokenType.IdentifierOrNumber) {
// TODO (matsko): implement array traversal for lookup here // TODO (matsko): implement array traversal for lookup here
isMatchingType = next.type == CssTokenType.Number || next.type == CssTokenType.Identifier; isMatchingType = next.type == CssTokenType.Number || next.type == CssTokenType.Identifier;

View File

@ -7,28 +7,15 @@
*/ */
import * as chars from './chars'; 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 {isPresent} from './facade/lang';
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from './parse_util'; import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from './parse_util';
const SPACE_OPERATOR = ' '; const SPACE_OPERATOR = ' ';
export {CssToken} from './css_lexer'; export {CssToken} from './css_lexer';
export {BlockType} from './css_ast';
export enum BlockType {
Import,
Charset,
Namespace,
Supports,
Keyframes,
MediaQuery,
Selector,
FontFace,
Page,
Document,
Viewport,
Unsupported
}
const SLASH_CHARACTER = '/'; const SLASH_CHARACTER = '/';
const GT_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 { function getDelimFromToken(token: CssToken): number {
return getDelimFromCharacter(token.numValue); return getDelimFromCharacter(token.numValue);
} }
@ -104,30 +81,6 @@ function characterContainsDelimiter(code: number, delimiters: number): boolean {
return (getDelimFromCharacter(code) & delimiters) > 0; 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 { export class ParsedCssResult {
constructor(public errors: CssParseError[], public ast: CssStyleSheetAst) {} constructor(public errors: CssParseError[], public ast: CssStyleSheetAst) {}
} }
@ -135,9 +88,89 @@ export class ParsedCssResult {
export class CssParser { export class CssParser {
private _errors: CssParseError[] = []; private _errors: CssParseError[] = [];
private _file: ParseSourceFile; 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 */ /** @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 */ /** @internal */
_parseRule(delimiters: number): CssRuleAst { _parseRule(delimiters: number): CssRuleAst {
if (this._scanner.peek == chars.$AT) { if (this._scanner.peek == chars.$AT) {
@ -215,10 +225,10 @@ export class CssParser {
/** @internal */ /** @internal */
_parseAtRule(delimiters: number): CssRuleAst { _parseAtRule(delimiters: number): CssRuleAst {
const start = this._getScannerIndex(); const start = this._getScannerIndex();
var end: number;
this._scanner.setMode(CssLexerMode.BLOCK); this._scanner.setMode(CssLexerMode.BLOCK);
var token = this._scan(); var token = this._scan();
var startToken = token;
this._assertCondition( this._assertCondition(
token.type == CssTokenType.AtKeyword, token.type == CssTokenType.AtKeyword,
@ -232,46 +242,55 @@ export class CssParser {
case BlockType.Import: case BlockType.Import:
var value = this._parseValue(delimiters); var value = this._parseValue(delimiters);
this._scanner.setMode(CssLexerMode.BLOCK); this._scanner.setMode(CssLexerMode.BLOCK);
end = value.end;
this._scanner.consumeEmptyStatements(); 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.Viewport:
case BlockType.FontFace: case BlockType.FontFace:
block = this._parseStyleBlock(delimiters); block = this._parseStyleBlock(delimiters);
end = this._getScannerIndex() - 1; var span = this._generateSourceSpan(startToken, block);
return new CssBlockRuleAst(start, end, type, block); return new CssBlockRuleAst(span, type, block);
case BlockType.Keyframes: case BlockType.Keyframes:
var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
// keyframes only have one identifier name // keyframes only have one identifier name
var name = tokens[0]; var name = tokens[0];
end = this._getScannerIndex() - 1; var block = this._parseKeyframeBlock(delimiters);
return new CssKeyframeRuleAst(start, end, name, this._parseKeyframeBlock(delimiters)); var span = this._generateSourceSpan(startToken, block);
return new CssKeyframeRuleAst(span, name, block);
case BlockType.MediaQuery: case BlockType.MediaQuery:
this._scanner.setMode(CssLexerMode.MEDIA_QUERY); this._scanner.setMode(CssLexerMode.MEDIA_QUERY);
var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
end = this._getScannerIndex() - 1; var endToken = tokens[tokens.length - 1];
var strValue = this._scanner.input.substring(start, end); // we do not track the whitespace after the mediaQuery predicate ends
var query = new CssAtRulePredicateAst(start, end, strValue, tokens); // 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); block = this._parseBlock(delimiters);
end = this._getScannerIndex() - 1; strValue = this._extractSourceContent(start, this._getScannerIndex() - 1);
strValue = this._scanner.input.substring(start, end); span = this._generateSourceSpan(startToken, block);
return new CssMediaQueryRuleAst(start, end, strValue, query, block); return new CssMediaQueryRuleAst(span, strValue, query, block);
case BlockType.Document: case BlockType.Document:
case BlockType.Supports: case BlockType.Supports:
case BlockType.Page: case BlockType.Page:
this._scanner.setMode(CssLexerMode.AT_RULE_QUERY); this._scanner.setMode(CssLexerMode.AT_RULE_QUERY);
var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG); var tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
end = this._getScannerIndex() - 1; var endToken = tokens[tokens.length - 1];
var strValue = this._scanner.input.substring(start, end); // we do not track the whitespace after this block rule predicate ends
var query = new CssAtRulePredicateAst(start, end, strValue, tokens); // 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); block = this._parseBlock(delimiters);
end = this._getScannerIndex() - 1; strValue = this._extractSourceContent(start, block.end.offset);
strValue = this._scanner.input.substring(start, end); span = this._generateSourceSpan(startToken, block);
return new CssBlockDefinitionRuleAst(start, end, strValue, type, query, block); return new CssBlockDefinitionRuleAst(span, strValue, type, query, block);
// if a custom @rule { ... } is used it should still tokenize the insides // if a custom @rule { ... } is used it should still tokenize the insides
default: default:
@ -280,8 +299,9 @@ export class CssParser {
this._scanner.setMode(CssLexerMode.ALL); this._scanner.setMode(CssLexerMode.ALL);
this._error( this._error(
generateErrorMessage( generateErrorMessage(
this._scanner.input, `The CSS "at" rule "${tokenName}" is not allowed to used here`, this._getSourceContent(),
token.strValue, token.index, token.line, token.column), `The CSS "at" rule "${tokenName}" is not allowed to used here`, token.strValue,
token.index, token.line, token.column),
token); token);
this._collectUntilDelim(delimiters | LBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG) this._collectUntilDelim(delimiters | LBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG)
@ -292,8 +312,9 @@ export class CssParser {
.forEach((token) => { listOfTokens.push(token); }); .forEach((token) => { listOfTokens.push(token); });
listOfTokens.push(this._consume(CssTokenType.Character, '}')); listOfTokens.push(this._consume(CssTokenType.Character, '}'));
} }
end = this._getScannerIndex() - 1; var endToken = listOfTokens[listOfTokens.length - 1];
return new CssUnknownRuleAst(start, end, tokenName, listOfTokens); var span = this._generateSourceSpan(startToken, endToken);
return new CssUnknownRuleAst(span, tokenName, listOfTokens);
} }
} }
@ -302,23 +323,27 @@ export class CssParser {
const start = this._getScannerIndex(); const start = this._getScannerIndex();
var selectors = this._parseSelectors(delimiters); var selectors = this._parseSelectors(delimiters);
var block = this._parseStyleBlock(delimiters); var block = this._parseStyleBlock(delimiters);
const end = this._getScannerIndex() - 1; var ruleAst: CssRuleAst;
var token: CssRuleAst; var span: ParseSourceSpan;
var startSelector = selectors[0];
if (isPresent(block)) { if (isPresent(block)) {
token = new CssSelectorRuleAst(start, end, selectors, block); var span = this._generateSourceSpan(startSelector, block);
ruleAst = new CssSelectorRuleAst(span, selectors, block);
} else { } else {
var name = this._scanner.input.substring(start, end); var name = this._extractSourceContent(start, this._getScannerIndex() - 1);
var innerTokens: CssToken[] = []; var innerTokens: CssToken[] = [];
selectors.forEach((selector: CssSelectorAst) => { selectors.forEach((selector: CssSelectorAst) => {
selector.selectorParts.forEach((part: CssSimpleSelectorAst) => { selector.selectorParts.forEach((part: CssSimpleSelectorAst) => {
part.tokens.forEach((token: CssToken) => { innerTokens.push(token); }); 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.setMode(CssLexerMode.BLOCK);
this._scanner.consumeEmptyStatements(); this._scanner.consumeEmptyStatements();
return token; return ruleAst;
} }
/** @internal */ /** @internal */
@ -352,6 +377,7 @@ export class CssParser {
if (isPresent(error)) { if (isPresent(error)) {
this._error(error.rawMessage, token); this._error(error.rawMessage, token);
} }
this._lastToken = token;
return token; return token;
} }
@ -366,27 +392,26 @@ export class CssParser {
if (isPresent(error)) { if (isPresent(error)) {
this._error(error.rawMessage, token); this._error(error.rawMessage, token);
} }
this._lastToken = token;
return token; return token;
} }
/** @internal */ /** @internal */
_parseKeyframeBlock(delimiters: number): CssBlockAst { _parseKeyframeBlock(delimiters: number): CssBlockAst {
const start = this._getScannerIndex();
delimiters |= RBRACE_DELIM_FLAG; delimiters |= RBRACE_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK); this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
this._consume(CssTokenType.Character, '{'); var startToken = this._consume(CssTokenType.Character, '{');
var definitions: CssKeyframeDefinitionAst[] = []; var definitions: CssKeyframeDefinitionAst[] = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
definitions.push(this._parseKeyframeDefinition(delimiters)); definitions.push(this._parseKeyframeDefinition(delimiters));
} }
this._consume(CssTokenType.Character, '}'); var endToken = this._consume(CssTokenType.Character, '}');
const end = this._getScannerIndex() - 1; var span = this._generateSourceSpan(startToken, endToken);
return new CssBlockAst(start, end, definitions); return new CssBlockAst(span, definitions);
} }
/** @internal */ /** @internal */
@ -400,10 +425,12 @@ export class CssParser {
this._consume(CssTokenType.Character, ','); 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); this._scanner.setMode(CssLexerMode.BLOCK);
const end = this._getScannerIndex() - 1; return ast;
return new CssKeyframeDefinitionAst(start, end, stepTokens, styles);
} }
/** @internal */ /** @internal */
@ -425,8 +452,7 @@ export class CssParser {
var tokens = [startToken]; var tokens = [startToken];
if (this._scanner.peek == chars.$COLON) { // ::something if (this._scanner.peek == chars.$COLON) { // ::something
startToken = this._consume(CssTokenType.Character, ':'); tokens.push(this._consume(CssTokenType.Character, ':'));
tokens.push(startToken);
} }
var innerSelectors: CssSelectorAst[] = []; var innerSelectors: CssSelectorAst[] = [];
@ -475,9 +501,11 @@ export class CssParser {
} }
const end = this._getScannerIndex() - 1; const end = this._getScannerIndex() - 1;
var strValue = this._scanner.input.substring(start, end); var strValue = this._extractSourceContent(start, end);
return new CssPseudoSelectorAst(
start, end, strValue, pseudoSelectorName, tokens, innerSelectors); var endToken = tokens[tokens.length - 1];
var span = this._generateSourceSpan(startToken, endToken);
return new CssPseudoSelectorAst(span, strValue, pseudoSelectorName, tokens, innerSelectors);
} }
/** @internal */ /** @internal */
@ -547,12 +575,11 @@ export class CssParser {
previousToken); previousToken);
} }
var end = this._getScannerIndex(); var end = this._getScannerIndex() - 1;
if (characterContainsDelimiter(this._scanner.peek, delimiters)) {
// this happens if the selector is followed by a comma or curly // this happens if the selector is not directly followed by
// brace without a space in between // a comma or curly brace without a space in between
end--; if (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
} else {
var operator: CssToken = null; var operator: CssToken = null;
var operatorScanCount = 0; var operatorScanCount = 0;
var lastOperatorToken: CssToken = null; var lastOperatorToken: CssToken = null;
@ -580,7 +607,7 @@ export class CssParser {
let text = SLASH_CHARACTER + deepToken.strValue + deepSlash.strValue; let text = SLASH_CHARACTER + deepToken.strValue + deepSlash.strValue;
this._error( this._error(
generateErrorMessage( generateErrorMessage(
this._scanner.input, `${text} is an invalid CSS operator`, text, index, this._getSourceContent(), `${text} is an invalid CSS operator`, text, index,
line, column), line, column),
lastOperatorToken); lastOperatorToken);
token = new CssToken(index, column, line, CssTokenType.Invalid, text); token = new CssToken(index, column, line, CssTokenType.Invalid, text);
@ -600,13 +627,21 @@ export class CssParser {
} }
operator = token; 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(); this._scanner.consumeWhitespace();
var strValue = this._extractSourceContent(start, end);
// if we do come across one or more spaces inside of // if we do come across one or more spaces inside of
// the operators loop then an empty space is still a // the operators loop then an empty space is still a
// valid operator to use if something else was not found // valid operator to use if something else was not found
@ -614,33 +649,42 @@ export class CssParser {
operator = lastOperatorToken; operator = lastOperatorToken;
} }
var strValue = this._scanner.input.substring(start, end); // please note that `endToken` is reassigned multiple times below
return new CssSimpleSelectorAst( // so please do not optimize the if statements into if/elseif
start, end, selectorCssTokens, strValue, pseudoSelectors, operator); 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 */ /** @internal */
_parseSelector(delimiters: number): CssSelectorAst { _parseSelector(delimiters: number): CssSelectorAst {
const start = this._getScannerIndex();
delimiters |= COMMA_DELIM_FLAG; delimiters |= COMMA_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.SELECTOR); this._scanner.setMode(CssLexerMode.SELECTOR);
var simpleSelectors: CssSimpleSelectorAst[] = []; var simpleSelectors: CssSimpleSelectorAst[] = [];
var end = this._getScannerIndex() - 1;
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
simpleSelectors.push(this._parseSimpleSelector(delimiters)); simpleSelectors.push(this._parseSimpleSelector(delimiters));
this._scanner.consumeWhitespace(); this._scanner.consumeWhitespace();
} }
// we do this to avoid any trailing whitespace that is processed var firstSelector = simpleSelectors[0];
// in order to determine the final operator value var lastSelector = simpleSelectors[simpleSelectors.length - 1];
var limit = simpleSelectors.length - 1; var span = this._generateSourceSpan(firstSelector, lastSelector);
if (limit >= 0) { return new CssSelectorAst(span, simpleSelectors);
end = simpleSelectors[limit].end;
}
return new CssSelectorAst(start, end, simpleSelectors);
} }
/** @internal */ /** @internal */
@ -690,13 +734,16 @@ export class CssParser {
} else if (code != chars.$RBRACE) { } else if (code != chars.$RBRACE) {
this._error( this._error(
generateErrorMessage( 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.strValue, previous.index, previous.line, previous.column),
previous); previous);
} }
var strValue = this._scanner.input.substring(start, end); var strValue = this._extractSourceContent(start, end);
return new CssStyleValueAst(start, end, tokens, strValue); var startToken = tokens[0];
var endToken = tokens[tokens.length - 1];
var span = this._generateSourceSpan(startToken, endToken);
return new CssStyleValueAst(span, tokens, strValue);
} }
/** @internal */ /** @internal */
@ -711,39 +758,35 @@ export class CssParser {
/** @internal */ /** @internal */
_parseBlock(delimiters: number): CssBlockAst { _parseBlock(delimiters: number): CssBlockAst {
const start = this._getScannerIndex();
delimiters |= RBRACE_DELIM_FLAG; delimiters |= RBRACE_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.BLOCK); this._scanner.setMode(CssLexerMode.BLOCK);
this._consume(CssTokenType.Character, '{'); var startToken = this._consume(CssTokenType.Character, '{');
this._scanner.consumeEmptyStatements(); this._scanner.consumeEmptyStatements();
var results: CssAst[] = []; var results: CssRuleAst[] = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) { while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
results.push(this._parseRule(delimiters)); results.push(this._parseRule(delimiters));
} }
this._consume(CssTokenType.Character, '}'); var endToken = this._consume(CssTokenType.Character, '}');
this._scanner.setMode(CssLexerMode.BLOCK); this._scanner.setMode(CssLexerMode.BLOCK);
this._scanner.consumeEmptyStatements(); this._scanner.consumeEmptyStatements();
const end = this._getScannerIndex() - 1; var span = this._generateSourceSpan(startToken, endToken);
return new CssBlockAst(start, end, results); return new CssBlockAst(span, results);
} }
/** @internal */ /** @internal */
_parseStyleBlock(delimiters: number): CssStylesBlockAst { _parseStyleBlock(delimiters: number): CssStylesBlockAst {
const start = this._getScannerIndex();
delimiters |= RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG; delimiters |= RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.STYLE_BLOCK); this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
var result = this._consume(CssTokenType.Character, '{'); var startToken = this._consume(CssTokenType.Character, '{');
if (result.numValue != chars.$LBRACE) { if (startToken.numValue != chars.$LBRACE) {
return null; return null;
} }
@ -755,32 +798,28 @@ export class CssParser {
this._scanner.consumeEmptyStatements(); this._scanner.consumeEmptyStatements();
} }
this._consume(CssTokenType.Character, '}'); var endToken = this._consume(CssTokenType.Character, '}');
this._scanner.setMode(CssLexerMode.STYLE_BLOCK); this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
this._scanner.consumeEmptyStatements(); this._scanner.consumeEmptyStatements();
const end = this._getScannerIndex() - 1; var span = this._generateSourceSpan(startToken, endToken);
return new CssStylesBlockAst(start, end, definitions); return new CssStylesBlockAst(span, definitions);
} }
/** @internal */ /** @internal */
_parseDefinition(delimiters: number): CssDefinitionAst { _parseDefinition(delimiters: number): CssDefinitionAst {
const start = this._getScannerIndex();
this._scanner.setMode(CssLexerMode.STYLE_BLOCK); this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
var prop = this._consume(CssTokenType.Identifier); 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. // the colon value separates the prop from the style.
// there are a few cases as to what could happen if it // there are a few cases as to what could happen if it
// is missing // is missing
switch (this._scanner.peek) { switch (this._scanner.peek) {
case chars.$COLON:
this._consume(CssTokenType.Character, ':');
parseValue = true;
break;
case chars.$SEMICOLON: case chars.$SEMICOLON:
case chars.$RBRACE: case chars.$RBRACE:
case chars.$EOF: case chars.$EOF:
@ -800,31 +839,31 @@ export class CssParser {
remainingTokens.forEach((token) => { propStr.push(token.strValue); }); 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 // this means we've reached the end of the definition and/or block
if (this._scanner.peek == chars.$COLON) { if (this._scanner.peek == chars.$COLON) {
this._consume(CssTokenType.Character, ':'); this._consume(CssTokenType.Character, ':');
parseValue = true; parseValue = true;
} else {
parseValue = false;
} }
break; break;
} }
if (parseValue) { if (parseValue) {
value = this._parseValue(delimiters); value = this._parseValue(delimiters);
endToken = value;
} else { } else {
this._error( this._error(
generateErrorMessage( 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.strValue, prop.index, prop.line, prop.column),
prop); prop);
} }
const end = this._getScannerIndex() - 1; var span = this._generateSourceSpan(prop, endToken);
return new CssDefinitionAst(start, end, prop, value); return new CssDefinitionAst(span, prop, value);
} }
/** @internal */ /** @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 { export class CssParseError extends ParseError {
static create( static create(
file: ParseSourceFile, offset: number, line: number, col: number, length: number, file: ParseSourceFile, offset: number, line: number, col: number, length: number,
errMsg: string): CssParseError { errMsg: string): CssParseError {
var start = new ParseLocation(file, offset, line, col); 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); var span = new ParseSourceSpan(start, end);
return new CssParseError(span, 'CSS Parse Error: ' + errMsg); return new CssParseError(span, 'CSS Parse Error: ' + errMsg);
} }
constructor(span: ParseSourceSpan, message: string) { super(span, message); } 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);
}
}

View File

@ -7,11 +7,12 @@
*/ */
import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../core/testing/testing_internal'; import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../core/testing/testing_internal';
import {CssLexer} from '../src/css_lexer'; import {CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssStyleSheetAst, CssStyleValueAst} from '../src/css_ast';
import {BlockType, CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssParseError, CssParser, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssStyleSheetAst, CssStyleValueAst, ParsedCssResult} from '../src/css_parser'; import {BlockType, CssParseError, CssParser, CssToken, ParsedCssResult} from '../src/css_parser';
import {BaseException} from '../src/facade/exceptions'; 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++) { for (var i = 0; i < tokens.length; i++) {
expect(tokens[i].strValue == valuesArr[i]); expect(tokens[i].strValue == valuesArr[i]);
} }
@ -19,14 +20,11 @@ export function assertTokens(tokens: any /** TODO #9100 */, valuesArr: any /** T
export function main() { export function main() {
describe('CssParser', () => { describe('CssParser', () => {
function parse(css: any /** TODO #9100 */): ParsedCssResult { function parse(css: string): ParsedCssResult {
var lexer = new CssLexer(); return new CssParser().parse(css, 'some-fake-css-file.css');
var scanner = lexer.scan(css);
var parser = new CssParser(scanner, 'some-fake-file-name.css');
return parser.parse();
} }
function makeAst(css: any /** TODO #9100 */): CssStyleSheetAst { function makeAst(css: string): CssStyleSheetAst {
var output = parse(css); var output = parse(css);
var errors = output.errors; var errors = output.errors;
if (errors.length > 0) { if (errors.length > 0) {
@ -36,11 +34,7 @@ export function main() {
} }
it('should parse CSS into a stylesheet Ast', () => { it('should parse CSS into a stylesheet Ast', () => {
var styles = ` var styles = '.selector { prop: value123; }';
.selector {
prop: value123;
}
`;
var ast = makeAst(styles); var ast = makeAst(styles);
expect(ast.rules.length).toEqual(1); expect(ast.rules.length).toEqual(1);
@ -155,7 +149,7 @@ export function main() {
expect(ast.rules.length).toEqual(1); expect(ast.rules.length).toEqual(1);
var rule = <CssMediaQueryRuleAst>ast.rules[0]; var rule = <CssMediaQueryRuleAst>ast.rules[0];
assertTokens(rule.query, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']); assertTokens(rule.query.tokens, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']);
var block = <CssBlockAst>rule.block; var block = <CssBlockAst>rule.block;
expect(block.entries.length).toEqual(1); expect(block.entries.length).toEqual(1);
@ -268,7 +262,8 @@ export function main() {
expect(fontFaceRule.block.entries.length).toEqual(2); expect(fontFaceRule.block.entries.length).toEqual(2);
var mediaQueryRule = <CssMediaQueryRuleAst>ast.rules[2]; var mediaQueryRule = <CssMediaQueryRuleAst>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); expect(mediaQueryRule.block.entries.length).toEqual(2);
}); });
@ -372,7 +367,7 @@ export function main() {
var rules = ast.rules; var rules = ast.rules;
var supportsRule = <CssBlockDefinitionRuleAst>rules[0]; var supportsRule = <CssBlockDefinitionRuleAst>rules[0];
assertTokens(supportsRule.query, ['(', 'animation-name', ':', 'rotate', ')']); assertTokens(supportsRule.query.tokens, ['(', 'animation-name', ':', 'rotate', ')']);
expect(supportsRule.type).toEqual(BlockType.Supports); expect(supportsRule.type).toEqual(BlockType.Supports);
var selectorOne = <CssSelectorRuleAst>supportsRule.block.entries[0]; var selectorOne = <CssSelectorRuleAst>supportsRule.block.entries[0];
@ -562,6 +557,148 @@ export function main() {
assertTokens(style2.value.tokens, ['white']); 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 = <CssSelectorRuleAst>ast.rules[0];
assertMatchesOffsetAndChar(rule1.location.start, 0, '.');
assertMatchesOffsetAndChar(rule1.location.end, 54, '}');
var rule2 = <CssSelectorRuleAst>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 = <CssDefinitionAst>block1.entries[0];
assertMatchesOffsetAndChar(block1def1.location.start, 17, 'b'); // border-top-right
assertMatchesOffsetAndChar(block1def1.location.end, 36, 'p'); // px
var block1def2 = <CssDefinitionAst>block1.entries[1];
assertMatchesOffsetAndChar(block1def2.location.start, 40, 'c'); // color
assertMatchesOffsetAndChar(block1def2.location.end, 47, 'w'); // white
var block2def1 = <CssDefinitionAst>block2.entries[0];
assertMatchesOffsetAndChar(block2def1.location.start, 74, 'b'); // background-color
assertMatchesOffsetAndChar(block2def1.location.end, 93, 'f'); // fe4
var block2def2 = <CssDefinitionAst>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 = <CssMediaQueryRuleAst>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 = <CssSelectorRuleAst>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 = <CssKeyframeRuleAst>ast.rules[0];
assertMatchesOffsetAndChar(keyframes.location.start, 0, '@');
assertMatchesOffsetAndChar(keyframes.location.end, 108, '}');
var step1 = <CssKeyframeDefinitionAst>keyframes.block.entries[0];
assertMatchesOffsetAndChar(step1.location.start, 30, 'f');
assertMatchesOffsetAndChar(step1.location.end, 62, '}');
var step2 = <CssKeyframeDefinitionAst>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 = <CssInlineRuleAst>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', () => { it('should parse minified CSS content properly', () => {
// this code was taken from the angular.io webpage's CSS code // this code was taken from the angular.io webpage's CSS code
var styles = ` var styles = `

View File

@ -7,10 +7,10 @@
*/ */
import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../core/testing/testing_internal'; import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../core/testing/testing_internal';
import {CssLexer} from '../src/css_lexer'; 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, 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 {BlockType, CssParseError, CssParser, CssToken} from '../src/css_parser';
import {BaseException} from '../src/facade/exceptions'; 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 { function _assertTokens(tokens: CssToken[], valuesArr: string[]): void {
expect(tokens.length).toEqual(valuesArr.length); expect(tokens.length).toEqual(valuesArr.length);
@ -115,10 +115,7 @@ function _getCaptureAst(capture: any[], index = 0): CssAst {
export function main() { export function main() {
function parse(cssCode: string, ignoreErrors: boolean = false) { function parse(cssCode: string, ignoreErrors: boolean = false) {
var lexer = new CssLexer(); var output = new CssParser().parse(cssCode, 'some-fake-css-file.css');
var scanner = lexer.scan(cssCode);
var parser = new CssParser(scanner, 'some-fake-file-name.css');
var output = parser.parse();
var errors = output.errors; var errors = output.errors;
if (errors.length > 0 && !ignoreErrors) { if (errors.length > 0 && !ignoreErrors) {
throw new BaseException(errors.map((error: CssParseError) => error.msg).join(', ')); throw new BaseException(errors.map((error: CssParseError) => error.msg).join(', '));
@ -127,7 +124,7 @@ export function main() {
} }
describe('CSS parsing and visiting', () => { describe('CSS parsing and visiting', () => {
var ast: any /** TODO #9100 */; var ast: CssStyleSheetAst;
var context = {}; var context = {};
beforeEach(() => { beforeEach(() => {