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:
parent
0ed7773223
commit
3fe1cb0253
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 = `
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
Loading…
Reference in New Issue