refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
248
packages/compiler/src/css_parser/css_ast.ts
Normal file
248
packages/compiler/src/css_parser/css_ast.ts
Normal file
@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @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 {ParseLocation, ParseSourceSpan} from '../parse_util';
|
||||
|
||||
import {CssToken, CssTokenType} from './css_lexer';
|
||||
|
||||
export enum BlockType {
|
||||
Import,
|
||||
Charset,
|
||||
Namespace,
|
||||
Supports,
|
||||
Keyframes,
|
||||
MediaQuery,
|
||||
Selector,
|
||||
FontFace,
|
||||
Page,
|
||||
Document,
|
||||
Viewport,
|
||||
Unsupported
|
||||
}
|
||||
|
||||
export interface CssAstVisitor {
|
||||
visitCssValue(ast: CssStyleValueAst, context?: any): any;
|
||||
visitCssInlineRule(ast: CssInlineRuleAst, context?: any): any;
|
||||
visitCssAtRulePredicate(ast: CssAtRulePredicateAst, context?: any): any;
|
||||
visitCssKeyframeRule(ast: CssKeyframeRuleAst, context?: any): any;
|
||||
visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAst, context?: any): any;
|
||||
visitCssMediaQueryRule(ast: CssMediaQueryRuleAst, context?: any): any;
|
||||
visitCssSelectorRule(ast: CssSelectorRuleAst, context?: any): any;
|
||||
visitCssSelector(ast: CssSelectorAst, context?: any): any;
|
||||
visitCssSimpleSelector(ast: CssSimpleSelectorAst, context?: any): any;
|
||||
visitCssPseudoSelector(ast: CssPseudoSelectorAst, context?: any): any;
|
||||
visitCssDefinition(ast: CssDefinitionAst, context?: any): any;
|
||||
visitCssBlock(ast: CssBlockAst, context?: any): any;
|
||||
visitCssStylesBlock(ast: CssStylesBlockAst, context?: any): any;
|
||||
visitCssStyleSheet(ast: CssStyleSheetAst, context?: any): any;
|
||||
visitCssUnknownRule(ast: CssUnknownRuleAst, context?: any): any;
|
||||
visitCssUnknownTokenList(ast: CssUnknownTokenListAst, context?: any): any;
|
||||
}
|
||||
|
||||
export abstract class CssAst {
|
||||
constructor(public location: ParseSourceSpan) {}
|
||||
get start(): ParseLocation { return this.location.start; }
|
||||
get end(): ParseLocation { return this.location.end; }
|
||||
abstract visit(visitor: CssAstVisitor, context?: any): any;
|
||||
}
|
||||
|
||||
export class CssStyleValueAst extends CssAst {
|
||||
constructor(location: ParseSourceSpan, public tokens: CssToken[], public strValue: string) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any { return visitor.visitCssValue(this); }
|
||||
}
|
||||
|
||||
export abstract class CssRuleAst extends CssAst {
|
||||
constructor(location: ParseSourceSpan) { super(location); }
|
||||
}
|
||||
|
||||
export class CssBlockRuleAst extends CssRuleAst {
|
||||
constructor(
|
||||
public location: ParseSourceSpan, public type: BlockType, public block: CssBlockAst,
|
||||
public name: CssToken = null) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssBlock(this.block, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssKeyframeRuleAst extends CssBlockRuleAst {
|
||||
constructor(location: ParseSourceSpan, name: CssToken, block: CssBlockAst) {
|
||||
super(location, BlockType.Keyframes, block, name);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssKeyframeRule(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssKeyframeDefinitionAst extends CssBlockRuleAst {
|
||||
constructor(location: ParseSourceSpan, public steps: CssToken[], block: CssBlockAst) {
|
||||
super(location, BlockType.Keyframes, block, mergeTokens(steps, ','));
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssKeyframeDefinition(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssBlockDefinitionRuleAst extends CssBlockRuleAst {
|
||||
constructor(
|
||||
location: ParseSourceSpan, public strValue: string, type: BlockType,
|
||||
public query: CssAtRulePredicateAst, block: CssBlockAst) {
|
||||
super(location, type, block);
|
||||
const firstCssToken: CssToken = query.tokens[0];
|
||||
this.name = new CssToken(
|
||||
firstCssToken.index, firstCssToken.column, firstCssToken.line, CssTokenType.Identifier,
|
||||
this.strValue);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssBlock(this.block, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssMediaQueryRuleAst extends CssBlockDefinitionRuleAst {
|
||||
constructor(
|
||||
location: ParseSourceSpan, strValue: string, query: CssAtRulePredicateAst,
|
||||
block: CssBlockAst) {
|
||||
super(location, strValue, BlockType.MediaQuery, query, block);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssMediaQueryRule(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssAtRulePredicateAst extends CssAst {
|
||||
constructor(location: ParseSourceSpan, public strValue: string, public tokens: CssToken[]) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssAtRulePredicate(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssInlineRuleAst extends CssRuleAst {
|
||||
constructor(location: ParseSourceSpan, public type: BlockType, public value: CssStyleValueAst) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssInlineRule(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssSelectorRuleAst extends CssBlockRuleAst {
|
||||
public strValue: string;
|
||||
|
||||
constructor(location: ParseSourceSpan, public selectors: CssSelectorAst[], block: CssBlockAst) {
|
||||
super(location, BlockType.Selector, block);
|
||||
this.strValue = selectors.map(selector => selector.strValue).join(',');
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssSelectorRule(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssDefinitionAst extends CssAst {
|
||||
constructor(
|
||||
location: ParseSourceSpan, public property: CssToken, public value: CssStyleValueAst) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssDefinition(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class CssSelectorPartAst extends CssAst {
|
||||
constructor(location: ParseSourceSpan) { super(location); }
|
||||
}
|
||||
|
||||
export class CssSelectorAst extends CssSelectorPartAst {
|
||||
public strValue: string;
|
||||
constructor(location: ParseSourceSpan, public selectorParts: CssSimpleSelectorAst[]) {
|
||||
super(location);
|
||||
this.strValue = selectorParts.map(part => part.strValue).join('');
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssSelector(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssSimpleSelectorAst extends CssSelectorPartAst {
|
||||
constructor(
|
||||
location: ParseSourceSpan, public tokens: CssToken[], public strValue: string,
|
||||
public pseudoSelectors: CssPseudoSelectorAst[], public operator: CssToken) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssSimpleSelector(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssPseudoSelectorAst extends CssSelectorPartAst {
|
||||
constructor(
|
||||
location: ParseSourceSpan, public strValue: string, public name: string,
|
||||
public tokens: CssToken[], public innerSelectors: CssSelectorAst[]) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssPseudoSelector(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssBlockAst extends CssAst {
|
||||
constructor(location: ParseSourceSpan, public entries: CssAst[]) { super(location); }
|
||||
visit(visitor: CssAstVisitor, context?: any): any { return visitor.visitCssBlock(this, context); }
|
||||
}
|
||||
|
||||
/*
|
||||
a style block is different from a standard block because it contains
|
||||
css prop:value definitions. A regular block can contain a list of Ast entries.
|
||||
*/
|
||||
export class CssStylesBlockAst extends CssBlockAst {
|
||||
constructor(location: ParseSourceSpan, public definitions: CssDefinitionAst[]) {
|
||||
super(location, definitions);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssStylesBlock(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssStyleSheetAst extends CssAst {
|
||||
constructor(location: ParseSourceSpan, public rules: CssAst[]) { super(location); }
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssStyleSheet(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssUnknownRuleAst extends CssRuleAst {
|
||||
constructor(location: ParseSourceSpan, public ruleName: string, public tokens: CssToken[]) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssUnknownRule(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssUnknownTokenListAst extends CssRuleAst {
|
||||
constructor(location: ParseSourceSpan, public name: string, public tokens: CssToken[]) {
|
||||
super(location);
|
||||
}
|
||||
visit(visitor: CssAstVisitor, context?: any): any {
|
||||
return visitor.visitCssUnknownTokenList(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeTokens(tokens: CssToken[], separator: string = ''): CssToken {
|
||||
const mainToken = tokens[0];
|
||||
let str = mainToken.strValue;
|
||||
for (let i = 1; i < tokens.length; i++) {
|
||||
str += separator + tokens[i].strValue;
|
||||
}
|
||||
|
||||
return new CssToken(mainToken.index, mainToken.column, mainToken.line, mainToken.type, str);
|
||||
}
|
720
packages/compiler/src/css_parser/css_lexer.ts
Normal file
720
packages/compiler/src/css_parser/css_lexer.ts
Normal file
@ -0,0 +1,720 @@
|
||||
/**
|
||||
* @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 * as chars from '../chars';
|
||||
|
||||
export enum CssTokenType {
|
||||
EOF,
|
||||
String,
|
||||
Comment,
|
||||
Identifier,
|
||||
Number,
|
||||
IdentifierOrNumber,
|
||||
AtKeyword,
|
||||
Character,
|
||||
Whitespace,
|
||||
Invalid
|
||||
}
|
||||
|
||||
export enum CssLexerMode {
|
||||
ALL,
|
||||
ALL_TRACK_WS,
|
||||
SELECTOR,
|
||||
PSEUDO_SELECTOR,
|
||||
PSEUDO_SELECTOR_WITH_ARGUMENTS,
|
||||
ATTRIBUTE_SELECTOR,
|
||||
AT_RULE_QUERY,
|
||||
MEDIA_QUERY,
|
||||
BLOCK,
|
||||
KEYFRAME_BLOCK,
|
||||
STYLE_BLOCK,
|
||||
STYLE_VALUE,
|
||||
STYLE_VALUE_FUNCTION,
|
||||
STYLE_CALC_FUNCTION
|
||||
}
|
||||
|
||||
export class LexedCssResult {
|
||||
constructor(public error: Error, public token: CssToken) {}
|
||||
}
|
||||
|
||||
export function generateErrorMessage(
|
||||
input: string, message: string, errorValue: string, index: number, row: number,
|
||||
column: number): string {
|
||||
return `${message} at column ${row}:${column} in expression [` +
|
||||
findProblemCode(input, errorValue, index, column) + ']';
|
||||
}
|
||||
|
||||
export function findProblemCode(
|
||||
input: string, errorValue: string, index: number, column: number): string {
|
||||
let endOfProblemLine = index;
|
||||
let current = charCode(input, index);
|
||||
while (current > 0 && !isNewline(current)) {
|
||||
current = charCode(input, ++endOfProblemLine);
|
||||
}
|
||||
const choppedString = input.substring(0, endOfProblemLine);
|
||||
let pointerPadding = '';
|
||||
for (let i = 0; i < column; i++) {
|
||||
pointerPadding += ' ';
|
||||
}
|
||||
let pointerString = '';
|
||||
for (let i = 0; i < errorValue.length; i++) {
|
||||
pointerString += '^';
|
||||
}
|
||||
return choppedString + '\n' + pointerPadding + pointerString + '\n';
|
||||
}
|
||||
|
||||
export class CssToken {
|
||||
numValue: number;
|
||||
constructor(
|
||||
public index: number, public column: number, public line: number, public type: CssTokenType,
|
||||
public strValue: string) {
|
||||
this.numValue = charCode(strValue, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssLexer {
|
||||
scan(text: string, trackComments: boolean = false): CssScanner {
|
||||
return new CssScanner(text, trackComments);
|
||||
}
|
||||
}
|
||||
|
||||
export function cssScannerError(token: CssToken, message: string): Error {
|
||||
const error = Error('CssParseError: ' + message);
|
||||
(error as any)[ERROR_RAW_MESSAGE] = message;
|
||||
(error as any)[ERROR_TOKEN] = token;
|
||||
return error;
|
||||
}
|
||||
|
||||
const ERROR_TOKEN = 'ngToken';
|
||||
const ERROR_RAW_MESSAGE = 'ngRawMessage';
|
||||
|
||||
export function getRawMessage(error: Error): string {
|
||||
return (error as any)[ERROR_RAW_MESSAGE];
|
||||
}
|
||||
|
||||
export function getToken(error: Error): CssToken {
|
||||
return (error as any)[ERROR_TOKEN];
|
||||
}
|
||||
|
||||
function _trackWhitespace(mode: CssLexerMode) {
|
||||
switch (mode) {
|
||||
case CssLexerMode.SELECTOR:
|
||||
case CssLexerMode.PSEUDO_SELECTOR:
|
||||
case CssLexerMode.ALL_TRACK_WS:
|
||||
case CssLexerMode.STYLE_VALUE:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class CssScanner {
|
||||
peek: number;
|
||||
peekPeek: number;
|
||||
length: number = 0;
|
||||
index: number = -1;
|
||||
column: number = -1;
|
||||
line: number = 0;
|
||||
|
||||
/** @internal */
|
||||
_currentMode: CssLexerMode = CssLexerMode.BLOCK;
|
||||
/** @internal */
|
||||
_currentError: Error = null;
|
||||
|
||||
constructor(public input: string, private _trackComments: boolean = false) {
|
||||
this.length = this.input.length;
|
||||
this.peekPeek = this.peekAt(0);
|
||||
this.advance();
|
||||
}
|
||||
|
||||
getMode(): CssLexerMode { return this._currentMode; }
|
||||
|
||||
setMode(mode: CssLexerMode) {
|
||||
if (this._currentMode != mode) {
|
||||
if (_trackWhitespace(this._currentMode) && !_trackWhitespace(mode)) {
|
||||
this.consumeWhitespace();
|
||||
}
|
||||
this._currentMode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
advance(): void {
|
||||
if (isNewline(this.peek)) {
|
||||
this.column = 0;
|
||||
this.line++;
|
||||
} else {
|
||||
this.column++;
|
||||
}
|
||||
|
||||
this.index++;
|
||||
this.peek = this.peekPeek;
|
||||
this.peekPeek = this.peekAt(this.index + 1);
|
||||
}
|
||||
|
||||
peekAt(index: number): number {
|
||||
return index >= this.length ? chars.$EOF : this.input.charCodeAt(index);
|
||||
}
|
||||
|
||||
consumeEmptyStatements(): void {
|
||||
this.consumeWhitespace();
|
||||
while (this.peek == chars.$SEMICOLON) {
|
||||
this.advance();
|
||||
this.consumeWhitespace();
|
||||
}
|
||||
}
|
||||
|
||||
consumeWhitespace(): void {
|
||||
while (chars.isWhitespace(this.peek) || isNewline(this.peek)) {
|
||||
this.advance();
|
||||
if (!this._trackComments && isCommentStart(this.peek, this.peekPeek)) {
|
||||
this.advance(); // /
|
||||
this.advance(); // *
|
||||
while (!isCommentEnd(this.peek, this.peekPeek)) {
|
||||
if (this.peek == chars.$EOF) {
|
||||
this.error('Unterminated comment');
|
||||
}
|
||||
this.advance();
|
||||
}
|
||||
this.advance(); // *
|
||||
this.advance(); // /
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
consume(type: CssTokenType, value: string = null): LexedCssResult {
|
||||
const mode = this._currentMode;
|
||||
|
||||
this.setMode(_trackWhitespace(mode) ? CssLexerMode.ALL_TRACK_WS : CssLexerMode.ALL);
|
||||
|
||||
const previousIndex = this.index;
|
||||
const previousLine = this.line;
|
||||
const previousColumn = this.column;
|
||||
|
||||
let next: CssToken;
|
||||
const output = this.scan();
|
||||
if (output != null) {
|
||||
// just incase the inner scan method returned an error
|
||||
if (output.error != null) {
|
||||
this.setMode(mode);
|
||||
return output;
|
||||
}
|
||||
|
||||
next = output.token;
|
||||
}
|
||||
|
||||
if (next == null) {
|
||||
next = new CssToken(this.index, this.column, this.line, CssTokenType.EOF, 'end of file');
|
||||
}
|
||||
|
||||
let isMatchingType: boolean = false;
|
||||
if (type == CssTokenType.IdentifierOrNumber) {
|
||||
// TODO (matsko): implement array traversal for lookup here
|
||||
isMatchingType = next.type == CssTokenType.Number || next.type == CssTokenType.Identifier;
|
||||
} else {
|
||||
isMatchingType = next.type == type;
|
||||
}
|
||||
|
||||
// before throwing the error we need to bring back the former
|
||||
// mode so that the parser can recover...
|
||||
this.setMode(mode);
|
||||
|
||||
let error: Error = null;
|
||||
if (!isMatchingType || (value != null && value != next.strValue)) {
|
||||
let errorMessage =
|
||||
CssTokenType[next.type] + ' does not match expected ' + CssTokenType[type] + ' value';
|
||||
|
||||
if (value != null) {
|
||||
errorMessage += ' ("' + next.strValue + '" should match "' + value + '")';
|
||||
}
|
||||
|
||||
error = cssScannerError(
|
||||
next, generateErrorMessage(
|
||||
this.input, errorMessage, next.strValue, previousIndex, previousLine,
|
||||
previousColumn));
|
||||
}
|
||||
|
||||
return new LexedCssResult(error, next);
|
||||
}
|
||||
|
||||
|
||||
scan(): LexedCssResult {
|
||||
const trackWS = _trackWhitespace(this._currentMode);
|
||||
if (this.index == 0 && !trackWS) { // first scan
|
||||
this.consumeWhitespace();
|
||||
}
|
||||
|
||||
const token = this._scan();
|
||||
if (token == null) return null;
|
||||
|
||||
const error = this._currentError;
|
||||
this._currentError = null;
|
||||
|
||||
if (!trackWS) {
|
||||
this.consumeWhitespace();
|
||||
}
|
||||
return new LexedCssResult(error, token);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_scan(): CssToken {
|
||||
let peek = this.peek;
|
||||
let peekPeek = this.peekPeek;
|
||||
if (peek == chars.$EOF) return null;
|
||||
|
||||
if (isCommentStart(peek, peekPeek)) {
|
||||
// even if comments are not tracked we still lex the
|
||||
// comment so we can move the pointer forward
|
||||
const commentToken = this.scanComment();
|
||||
if (this._trackComments) {
|
||||
return commentToken;
|
||||
}
|
||||
}
|
||||
|
||||
if (_trackWhitespace(this._currentMode) && (chars.isWhitespace(peek) || isNewline(peek))) {
|
||||
return this.scanWhitespace();
|
||||
}
|
||||
|
||||
peek = this.peek;
|
||||
peekPeek = this.peekPeek;
|
||||
if (peek == chars.$EOF) return null;
|
||||
|
||||
if (isStringStart(peek, peekPeek)) {
|
||||
return this.scanString();
|
||||
}
|
||||
|
||||
// something like url(cool)
|
||||
if (this._currentMode == CssLexerMode.STYLE_VALUE_FUNCTION) {
|
||||
return this.scanCssValueFunction();
|
||||
}
|
||||
|
||||
const isModifier = peek == chars.$PLUS || peek == chars.$MINUS;
|
||||
const digitA = isModifier ? false : chars.isDigit(peek);
|
||||
const digitB = chars.isDigit(peekPeek);
|
||||
if (digitA || (isModifier && (peekPeek == chars.$PERIOD || digitB)) ||
|
||||
(peek == chars.$PERIOD && digitB)) {
|
||||
return this.scanNumber();
|
||||
}
|
||||
|
||||
if (peek == chars.$AT) {
|
||||
return this.scanAtExpression();
|
||||
}
|
||||
|
||||
if (isIdentifierStart(peek, peekPeek)) {
|
||||
return this.scanIdentifier();
|
||||
}
|
||||
|
||||
if (isValidCssCharacter(peek, this._currentMode)) {
|
||||
return this.scanCharacter();
|
||||
}
|
||||
|
||||
return this.error(`Unexpected character [${String.fromCharCode(peek)}]`);
|
||||
}
|
||||
|
||||
scanComment(): CssToken {
|
||||
if (this.assertCondition(
|
||||
isCommentStart(this.peek, this.peekPeek), 'Expected comment start value')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const start = this.index;
|
||||
const startingColumn = this.column;
|
||||
const startingLine = this.line;
|
||||
|
||||
this.advance(); // /
|
||||
this.advance(); // *
|
||||
|
||||
while (!isCommentEnd(this.peek, this.peekPeek)) {
|
||||
if (this.peek == chars.$EOF) {
|
||||
this.error('Unterminated comment');
|
||||
}
|
||||
this.advance();
|
||||
}
|
||||
|
||||
this.advance(); // *
|
||||
this.advance(); // /
|
||||
|
||||
const str = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, startingLine, CssTokenType.Comment, str);
|
||||
}
|
||||
|
||||
scanWhitespace(): CssToken {
|
||||
const start = this.index;
|
||||
const startingColumn = this.column;
|
||||
const startingLine = this.line;
|
||||
while (chars.isWhitespace(this.peek) && this.peek != chars.$EOF) {
|
||||
this.advance();
|
||||
}
|
||||
const str = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, startingLine, CssTokenType.Whitespace, str);
|
||||
}
|
||||
|
||||
scanString(): CssToken {
|
||||
if (this.assertCondition(
|
||||
isStringStart(this.peek, this.peekPeek), 'Unexpected non-string starting value')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = this.peek;
|
||||
const start = this.index;
|
||||
const startingColumn = this.column;
|
||||
const startingLine = this.line;
|
||||
let previous = target;
|
||||
this.advance();
|
||||
|
||||
while (!isCharMatch(target, previous, this.peek)) {
|
||||
if (this.peek == chars.$EOF || isNewline(this.peek)) {
|
||||
this.error('Unterminated quote');
|
||||
}
|
||||
previous = this.peek;
|
||||
this.advance();
|
||||
}
|
||||
|
||||
if (this.assertCondition(this.peek == target, 'Unterminated quote')) {
|
||||
return null;
|
||||
}
|
||||
this.advance();
|
||||
|
||||
const str = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, startingLine, CssTokenType.String, str);
|
||||
}
|
||||
|
||||
scanNumber(): CssToken {
|
||||
const start = this.index;
|
||||
const startingColumn = this.column;
|
||||
if (this.peek == chars.$PLUS || this.peek == chars.$MINUS) {
|
||||
this.advance();
|
||||
}
|
||||
let periodUsed = false;
|
||||
while (chars.isDigit(this.peek) || this.peek == chars.$PERIOD) {
|
||||
if (this.peek == chars.$PERIOD) {
|
||||
if (periodUsed) {
|
||||
this.error('Unexpected use of a second period value');
|
||||
}
|
||||
periodUsed = true;
|
||||
}
|
||||
this.advance();
|
||||
}
|
||||
const strValue = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.Number, strValue);
|
||||
}
|
||||
|
||||
scanIdentifier(): CssToken {
|
||||
if (this.assertCondition(
|
||||
isIdentifierStart(this.peek, this.peekPeek), 'Expected identifier starting value')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const start = this.index;
|
||||
const startingColumn = this.column;
|
||||
while (isIdentifierPart(this.peek)) {
|
||||
this.advance();
|
||||
}
|
||||
const strValue = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue);
|
||||
}
|
||||
|
||||
scanCssValueFunction(): CssToken {
|
||||
const start = this.index;
|
||||
const startingColumn = this.column;
|
||||
let parenBalance = 1;
|
||||
while (this.peek != chars.$EOF && parenBalance > 0) {
|
||||
this.advance();
|
||||
if (this.peek == chars.$LPAREN) {
|
||||
parenBalance++;
|
||||
} else if (this.peek == chars.$RPAREN) {
|
||||
parenBalance--;
|
||||
}
|
||||
}
|
||||
const strValue = this.input.substring(start, this.index);
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue);
|
||||
}
|
||||
|
||||
scanCharacter(): CssToken {
|
||||
const start = this.index;
|
||||
const startingColumn = this.column;
|
||||
if (this.assertCondition(
|
||||
isValidCssCharacter(this.peek, this._currentMode),
|
||||
charStr(this.peek) + ' is not a valid CSS character')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const c = this.input.substring(start, start + 1);
|
||||
this.advance();
|
||||
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.Character, c);
|
||||
}
|
||||
|
||||
scanAtExpression(): CssToken {
|
||||
if (this.assertCondition(this.peek == chars.$AT, 'Expected @ value')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const start = this.index;
|
||||
const startingColumn = this.column;
|
||||
this.advance();
|
||||
if (isIdentifierStart(this.peek, this.peekPeek)) {
|
||||
const ident = this.scanIdentifier();
|
||||
const strValue = '@' + ident.strValue;
|
||||
return new CssToken(start, startingColumn, this.line, CssTokenType.AtKeyword, strValue);
|
||||
} else {
|
||||
return this.scanCharacter();
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(status: boolean, errorMessage: string): boolean {
|
||||
if (!status) {
|
||||
this.error(errorMessage);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
error(message: string, errorTokenValue: string = null, doNotAdvance: boolean = false): CssToken {
|
||||
const index: number = this.index;
|
||||
const column: number = this.column;
|
||||
const line: number = this.line;
|
||||
errorTokenValue = errorTokenValue || String.fromCharCode(this.peek);
|
||||
const invalidToken = new CssToken(index, column, line, CssTokenType.Invalid, errorTokenValue);
|
||||
const errorMessage =
|
||||
generateErrorMessage(this.input, message, errorTokenValue, index, line, column);
|
||||
if (!doNotAdvance) {
|
||||
this.advance();
|
||||
}
|
||||
this._currentError = cssScannerError(invalidToken, errorMessage);
|
||||
return invalidToken;
|
||||
}
|
||||
}
|
||||
|
||||
function isCharMatch(target: number, previous: number, code: number): boolean {
|
||||
return code == target && previous != chars.$BACKSLASH;
|
||||
}
|
||||
|
||||
function isCommentStart(code: number, next: number): boolean {
|
||||
return code == chars.$SLASH && next == chars.$STAR;
|
||||
}
|
||||
|
||||
function isCommentEnd(code: number, next: number): boolean {
|
||||
return code == chars.$STAR && next == chars.$SLASH;
|
||||
}
|
||||
|
||||
function isStringStart(code: number, next: number): boolean {
|
||||
let target = code;
|
||||
if (target == chars.$BACKSLASH) {
|
||||
target = next;
|
||||
}
|
||||
return target == chars.$DQ || target == chars.$SQ;
|
||||
}
|
||||
|
||||
function isIdentifierStart(code: number, next: number): boolean {
|
||||
let target = code;
|
||||
if (target == chars.$MINUS) {
|
||||
target = next;
|
||||
}
|
||||
|
||||
return chars.isAsciiLetter(target) || target == chars.$BACKSLASH || target == chars.$MINUS ||
|
||||
target == chars.$_;
|
||||
}
|
||||
|
||||
function isIdentifierPart(target: number): boolean {
|
||||
return chars.isAsciiLetter(target) || target == chars.$BACKSLASH || target == chars.$MINUS ||
|
||||
target == chars.$_ || chars.isDigit(target);
|
||||
}
|
||||
|
||||
function isValidPseudoSelectorCharacter(code: number): boolean {
|
||||
switch (code) {
|
||||
case chars.$LPAREN:
|
||||
case chars.$RPAREN:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidKeyframeBlockCharacter(code: number): boolean {
|
||||
return code == chars.$PERCENT;
|
||||
}
|
||||
|
||||
function isValidAttributeSelectorCharacter(code: number): boolean {
|
||||
// value^*|$~=something
|
||||
switch (code) {
|
||||
case chars.$$:
|
||||
case chars.$PIPE:
|
||||
case chars.$CARET:
|
||||
case chars.$TILDA:
|
||||
case chars.$STAR:
|
||||
case chars.$EQ:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidSelectorCharacter(code: number): boolean {
|
||||
// selector [ key = value ]
|
||||
// IDENT C IDENT C IDENT C
|
||||
// #id, .class, *+~>
|
||||
// tag:PSEUDO
|
||||
switch (code) {
|
||||
case chars.$HASH:
|
||||
case chars.$PERIOD:
|
||||
case chars.$TILDA:
|
||||
case chars.$STAR:
|
||||
case chars.$PLUS:
|
||||
case chars.$GT:
|
||||
case chars.$COLON:
|
||||
case chars.$PIPE:
|
||||
case chars.$COMMA:
|
||||
case chars.$LBRACKET:
|
||||
case chars.$RBRACKET:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidStyleBlockCharacter(code: number): boolean {
|
||||
// key:value;
|
||||
// key:calc(something ... )
|
||||
switch (code) {
|
||||
case chars.$HASH:
|
||||
case chars.$SEMICOLON:
|
||||
case chars.$COLON:
|
||||
case chars.$PERCENT:
|
||||
case chars.$SLASH:
|
||||
case chars.$BACKSLASH:
|
||||
case chars.$BANG:
|
||||
case chars.$PERIOD:
|
||||
case chars.$LPAREN:
|
||||
case chars.$RPAREN:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidMediaQueryRuleCharacter(code: number): boolean {
|
||||
// (min-width: 7.5em) and (orientation: landscape)
|
||||
switch (code) {
|
||||
case chars.$LPAREN:
|
||||
case chars.$RPAREN:
|
||||
case chars.$COLON:
|
||||
case chars.$PERCENT:
|
||||
case chars.$PERIOD:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidAtRuleCharacter(code: number): boolean {
|
||||
// @document url(http://www.w3.org/page?something=on#hash),
|
||||
switch (code) {
|
||||
case chars.$LPAREN:
|
||||
case chars.$RPAREN:
|
||||
case chars.$COLON:
|
||||
case chars.$PERCENT:
|
||||
case chars.$PERIOD:
|
||||
case chars.$SLASH:
|
||||
case chars.$BACKSLASH:
|
||||
case chars.$HASH:
|
||||
case chars.$EQ:
|
||||
case chars.$QUESTION:
|
||||
case chars.$AMPERSAND:
|
||||
case chars.$STAR:
|
||||
case chars.$COMMA:
|
||||
case chars.$MINUS:
|
||||
case chars.$PLUS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidStyleFunctionCharacter(code: number): boolean {
|
||||
switch (code) {
|
||||
case chars.$PERIOD:
|
||||
case chars.$MINUS:
|
||||
case chars.$PLUS:
|
||||
case chars.$STAR:
|
||||
case chars.$SLASH:
|
||||
case chars.$LPAREN:
|
||||
case chars.$RPAREN:
|
||||
case chars.$COMMA:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidBlockCharacter(code: number): boolean {
|
||||
// @something { }
|
||||
// IDENT
|
||||
return code == chars.$AT;
|
||||
}
|
||||
|
||||
function isValidCssCharacter(code: number, mode: CssLexerMode): boolean {
|
||||
switch (mode) {
|
||||
case CssLexerMode.ALL:
|
||||
case CssLexerMode.ALL_TRACK_WS:
|
||||
return true;
|
||||
|
||||
case CssLexerMode.SELECTOR:
|
||||
return isValidSelectorCharacter(code);
|
||||
|
||||
case CssLexerMode.PSEUDO_SELECTOR_WITH_ARGUMENTS:
|
||||
return isValidPseudoSelectorCharacter(code);
|
||||
|
||||
case CssLexerMode.ATTRIBUTE_SELECTOR:
|
||||
return isValidAttributeSelectorCharacter(code);
|
||||
|
||||
case CssLexerMode.MEDIA_QUERY:
|
||||
return isValidMediaQueryRuleCharacter(code);
|
||||
|
||||
case CssLexerMode.AT_RULE_QUERY:
|
||||
return isValidAtRuleCharacter(code);
|
||||
|
||||
case CssLexerMode.KEYFRAME_BLOCK:
|
||||
return isValidKeyframeBlockCharacter(code);
|
||||
|
||||
case CssLexerMode.STYLE_BLOCK:
|
||||
case CssLexerMode.STYLE_VALUE:
|
||||
return isValidStyleBlockCharacter(code);
|
||||
|
||||
case CssLexerMode.STYLE_CALC_FUNCTION:
|
||||
return isValidStyleFunctionCharacter(code);
|
||||
|
||||
case CssLexerMode.BLOCK:
|
||||
return isValidBlockCharacter(code);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function charCode(input: string, index: number): number {
|
||||
return index >= input.length ? chars.$EOF : input.charCodeAt(index);
|
||||
}
|
||||
|
||||
function charStr(code: number): string {
|
||||
return String.fromCharCode(code);
|
||||
}
|
||||
|
||||
export function isNewline(code: number): boolean {
|
||||
switch (code) {
|
||||
case chars.$FF:
|
||||
case chars.$CR:
|
||||
case chars.$LF:
|
||||
case chars.$VTAB:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
900
packages/compiler/src/css_parser/css_parser.ts
Normal file
900
packages/compiler/src/css_parser/css_parser.ts
Normal file
@ -0,0 +1,900 @@
|
||||
/**
|
||||
* @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 * as chars from '../chars';
|
||||
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../parse_util';
|
||||
|
||||
import {BlockType, CssAst, CssAtRulePredicateAst, CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssPseudoSelectorAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssSimpleSelectorAst, CssStyleSheetAst, CssStyleValueAst, CssStylesBlockAst, CssUnknownRuleAst, CssUnknownTokenListAst, mergeTokens} from './css_ast';
|
||||
import {CssLexer, CssLexerMode, CssScanner, CssToken, CssTokenType, generateErrorMessage, getRawMessage, isNewline} from './css_lexer';
|
||||
|
||||
const SPACE_OPERATOR = ' ';
|
||||
|
||||
export {CssToken} from './css_lexer';
|
||||
export {BlockType} from './css_ast';
|
||||
|
||||
const SLASH_CHARACTER = '/';
|
||||
const GT_CHARACTER = '>';
|
||||
const TRIPLE_GT_OPERATOR_STR = '>>>';
|
||||
const DEEP_OPERATOR_STR = '/deep/';
|
||||
|
||||
const EOF_DELIM_FLAG = 1;
|
||||
const RBRACE_DELIM_FLAG = 2;
|
||||
const LBRACE_DELIM_FLAG = 4;
|
||||
const COMMA_DELIM_FLAG = 8;
|
||||
const COLON_DELIM_FLAG = 16;
|
||||
const SEMICOLON_DELIM_FLAG = 32;
|
||||
const NEWLINE_DELIM_FLAG = 64;
|
||||
const RPAREN_DELIM_FLAG = 128;
|
||||
const LPAREN_DELIM_FLAG = 256;
|
||||
const SPACE_DELIM_FLAG = 512;
|
||||
|
||||
function _pseudoSelectorSupportsInnerSelectors(name: string): boolean {
|
||||
return ['not', 'host', 'host-context'].indexOf(name) >= 0;
|
||||
}
|
||||
|
||||
function isSelectorOperatorCharacter(code: number): boolean {
|
||||
switch (code) {
|
||||
case chars.$SLASH:
|
||||
case chars.$TILDA:
|
||||
case chars.$PLUS:
|
||||
case chars.$GT:
|
||||
return true;
|
||||
default:
|
||||
return chars.isWhitespace(code);
|
||||
}
|
||||
}
|
||||
|
||||
function getDelimFromCharacter(code: number): number {
|
||||
switch (code) {
|
||||
case chars.$EOF:
|
||||
return EOF_DELIM_FLAG;
|
||||
case chars.$COMMA:
|
||||
return COMMA_DELIM_FLAG;
|
||||
case chars.$COLON:
|
||||
return COLON_DELIM_FLAG;
|
||||
case chars.$SEMICOLON:
|
||||
return SEMICOLON_DELIM_FLAG;
|
||||
case chars.$RBRACE:
|
||||
return RBRACE_DELIM_FLAG;
|
||||
case chars.$LBRACE:
|
||||
return LBRACE_DELIM_FLAG;
|
||||
case chars.$RPAREN:
|
||||
return RPAREN_DELIM_FLAG;
|
||||
case chars.$SPACE:
|
||||
case chars.$TAB:
|
||||
return SPACE_DELIM_FLAG;
|
||||
default:
|
||||
return isNewline(code) ? NEWLINE_DELIM_FLAG : 0;
|
||||
}
|
||||
}
|
||||
|
||||
function characterContainsDelimiter(code: number, delimiters: number): boolean {
|
||||
return (getDelimFromCharacter(code) & delimiters) > 0;
|
||||
}
|
||||
|
||||
export class ParsedCssResult {
|
||||
constructor(public errors: CssParseError[], public ast: CssStyleSheetAst) {}
|
||||
}
|
||||
|
||||
export class CssParser {
|
||||
private _errors: CssParseError[] = [];
|
||||
private _file: ParseSourceFile;
|
||||
private _scanner: CssScanner;
|
||||
private _lastToken: CssToken;
|
||||
|
||||
/**
|
||||
* @param css the CSS code that will be parsed
|
||||
* @param url the name of the CSS file containing the CSS source code
|
||||
*/
|
||||
parse(css: string, url: string): ParsedCssResult {
|
||||
const lexer = new CssLexer();
|
||||
this._file = new ParseSourceFile(css, url);
|
||||
this._scanner = lexer.scan(css, false);
|
||||
|
||||
const ast = this._parseStyleSheet(EOF_DELIM_FLAG);
|
||||
|
||||
const errors = this._errors;
|
||||
this._errors = [];
|
||||
|
||||
const result = new ParsedCssResult(errors, ast);
|
||||
this._file = null;
|
||||
this._scanner = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseStyleSheet(delimiters: number): CssStyleSheetAst {
|
||||
const results: CssRuleAst[] = [];
|
||||
this._scanner.consumeEmptyStatements();
|
||||
while (this._scanner.peek != chars.$EOF) {
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
results.push(this._parseRule(delimiters));
|
||||
}
|
||||
let span: ParseSourceSpan = null;
|
||||
if (results.length > 0) {
|
||||
const firstRule = results[0];
|
||||
// we collect the last token like so incase there was an
|
||||
// EOF token that was emitted sometime during the lexing
|
||||
span = this._generateSourceSpan(firstRule, this._lastToken);
|
||||
}
|
||||
return new CssStyleSheetAst(span, results);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_getSourceContent(): string { return this._scanner != null ? this._scanner.input : ''; }
|
||||
|
||||
/** @internal */
|
||||
_extractSourceContent(start: number, end: number): string {
|
||||
return this._getSourceContent().substring(start, end + 1);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_generateSourceSpan(start: CssToken|CssAst, end: CssToken|CssAst = null): ParseSourceSpan {
|
||||
let startLoc: ParseLocation;
|
||||
if (start instanceof CssAst) {
|
||||
startLoc = start.location.start;
|
||||
} else {
|
||||
let token = start;
|
||||
if (token == null) {
|
||||
// the data here is invalid, however, if and when this does
|
||||
// occur, any other errors associated with this will be collected
|
||||
token = this._lastToken;
|
||||
}
|
||||
startLoc = new ParseLocation(this._file, token.index, token.line, token.column);
|
||||
}
|
||||
|
||||
if (end == null) {
|
||||
end = this._lastToken;
|
||||
}
|
||||
|
||||
let endLine: number;
|
||||
let endColumn: number;
|
||||
let 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;
|
||||
}
|
||||
|
||||
const endLoc = new ParseLocation(this._file, endIndex, endLine, endColumn);
|
||||
return new ParseSourceSpan(startLoc, endLoc);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_resolveBlockType(token: CssToken): BlockType {
|
||||
switch (token.strValue) {
|
||||
case '@-o-keyframes':
|
||||
case '@-moz-keyframes':
|
||||
case '@-webkit-keyframes':
|
||||
case '@keyframes':
|
||||
return BlockType.Keyframes;
|
||||
|
||||
case '@charset':
|
||||
return BlockType.Charset;
|
||||
|
||||
case '@import':
|
||||
return BlockType.Import;
|
||||
|
||||
case '@namespace':
|
||||
return BlockType.Namespace;
|
||||
|
||||
case '@page':
|
||||
return BlockType.Page;
|
||||
|
||||
case '@document':
|
||||
return BlockType.Document;
|
||||
|
||||
case '@media':
|
||||
return BlockType.MediaQuery;
|
||||
|
||||
case '@font-face':
|
||||
return BlockType.FontFace;
|
||||
|
||||
case '@viewport':
|
||||
return BlockType.Viewport;
|
||||
|
||||
case '@supports':
|
||||
return BlockType.Supports;
|
||||
|
||||
default:
|
||||
return BlockType.Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseRule(delimiters: number): CssRuleAst {
|
||||
if (this._scanner.peek == chars.$AT) {
|
||||
return this._parseAtRule(delimiters);
|
||||
}
|
||||
return this._parseSelectorRule(delimiters);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseAtRule(delimiters: number): CssRuleAst {
|
||||
const start = this._getScannerIndex();
|
||||
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
const token = this._scan();
|
||||
const startToken = token;
|
||||
|
||||
this._assertCondition(
|
||||
token.type == CssTokenType.AtKeyword,
|
||||
`The CSS Rule ${token.strValue} is not a valid [@] rule.`, token);
|
||||
|
||||
let block: CssBlockAst;
|
||||
const type = this._resolveBlockType(token);
|
||||
let span: ParseSourceSpan;
|
||||
let tokens: CssToken[];
|
||||
let endToken: CssToken;
|
||||
let end: number;
|
||||
let strValue: string;
|
||||
let query: CssAtRulePredicateAst;
|
||||
switch (type) {
|
||||
case BlockType.Charset:
|
||||
case BlockType.Namespace:
|
||||
case BlockType.Import:
|
||||
let value = this._parseValue(delimiters);
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
this._scanner.consumeEmptyStatements();
|
||||
span = this._generateSourceSpan(startToken, value);
|
||||
return new CssInlineRuleAst(span, type, value);
|
||||
|
||||
case BlockType.Viewport:
|
||||
case BlockType.FontFace:
|
||||
block = this._parseStyleBlock(delimiters);
|
||||
span = this._generateSourceSpan(startToken, block);
|
||||
return new CssBlockRuleAst(span, type, block);
|
||||
|
||||
case BlockType.Keyframes:
|
||||
tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
|
||||
// keyframes only have one identifier name
|
||||
let name = tokens[0];
|
||||
block = this._parseKeyframeBlock(delimiters);
|
||||
span = this._generateSourceSpan(startToken, block);
|
||||
return new CssKeyframeRuleAst(span, name, block);
|
||||
|
||||
case BlockType.MediaQuery:
|
||||
this._scanner.setMode(CssLexerMode.MEDIA_QUERY);
|
||||
tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
|
||||
endToken = tokens[tokens.length - 1];
|
||||
// we do not track the whitespace after the mediaQuery predicate ends
|
||||
// so we have to calculate the end string value on our own
|
||||
end = endToken.index + endToken.strValue.length - 1;
|
||||
strValue = this._extractSourceContent(start, end);
|
||||
span = this._generateSourceSpan(startToken, endToken);
|
||||
query = new CssAtRulePredicateAst(span, strValue, tokens);
|
||||
block = this._parseBlock(delimiters);
|
||||
strValue = this._extractSourceContent(start, this._getScannerIndex() - 1);
|
||||
span = this._generateSourceSpan(startToken, block);
|
||||
return new CssMediaQueryRuleAst(span, strValue, query, block);
|
||||
|
||||
case BlockType.Document:
|
||||
case BlockType.Supports:
|
||||
case BlockType.Page:
|
||||
this._scanner.setMode(CssLexerMode.AT_RULE_QUERY);
|
||||
tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
|
||||
endToken = tokens[tokens.length - 1];
|
||||
// we do not track the whitespace after this block rule predicate ends
|
||||
// so we have to calculate the end string value on our own
|
||||
end = endToken.index + endToken.strValue.length - 1;
|
||||
strValue = this._extractSourceContent(start, end);
|
||||
span = this._generateSourceSpan(startToken, tokens[tokens.length - 1]);
|
||||
query = new CssAtRulePredicateAst(span, strValue, tokens);
|
||||
block = this._parseBlock(delimiters);
|
||||
strValue = this._extractSourceContent(start, block.end.offset);
|
||||
span = this._generateSourceSpan(startToken, block);
|
||||
return new CssBlockDefinitionRuleAst(span, strValue, type, query, block);
|
||||
|
||||
// if a custom @rule { ... } is used it should still tokenize the insides
|
||||
default:
|
||||
let listOfTokens: CssToken[] = [];
|
||||
let tokenName = token.strValue;
|
||||
this._scanner.setMode(CssLexerMode.ALL);
|
||||
this._error(
|
||||
generateErrorMessage(
|
||||
this._getSourceContent(),
|
||||
`The CSS "at" rule "${tokenName}" is not allowed to used here`, token.strValue,
|
||||
token.index, token.line, token.column),
|
||||
token);
|
||||
|
||||
this._collectUntilDelim(delimiters | LBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG)
|
||||
.forEach((token) => { listOfTokens.push(token); });
|
||||
if (this._scanner.peek == chars.$LBRACE) {
|
||||
listOfTokens.push(this._consume(CssTokenType.Character, '{'));
|
||||
this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG)
|
||||
.forEach((token) => { listOfTokens.push(token); });
|
||||
listOfTokens.push(this._consume(CssTokenType.Character, '}'));
|
||||
}
|
||||
endToken = listOfTokens[listOfTokens.length - 1];
|
||||
span = this._generateSourceSpan(startToken, endToken);
|
||||
return new CssUnknownRuleAst(span, tokenName, listOfTokens);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseSelectorRule(delimiters: number): CssRuleAst {
|
||||
const start = this._getScannerIndex();
|
||||
const selectors = this._parseSelectors(delimiters);
|
||||
const block = this._parseStyleBlock(delimiters);
|
||||
let ruleAst: CssRuleAst;
|
||||
let span: ParseSourceSpan;
|
||||
const startSelector = selectors[0];
|
||||
if (block != null) {
|
||||
span = this._generateSourceSpan(startSelector, block);
|
||||
ruleAst = new CssSelectorRuleAst(span, selectors, block);
|
||||
} else {
|
||||
const name = this._extractSourceContent(start, this._getScannerIndex() - 1);
|
||||
const innerTokens: CssToken[] = [];
|
||||
selectors.forEach((selector: CssSelectorAst) => {
|
||||
selector.selectorParts.forEach((part: CssSimpleSelectorAst) => {
|
||||
part.tokens.forEach((token: CssToken) => { innerTokens.push(token); });
|
||||
});
|
||||
});
|
||||
const endToken = innerTokens[innerTokens.length - 1];
|
||||
span = this._generateSourceSpan(startSelector, endToken);
|
||||
ruleAst = new CssUnknownTokenListAst(span, name, innerTokens);
|
||||
}
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
this._scanner.consumeEmptyStatements();
|
||||
return ruleAst;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseSelectors(delimiters: number): CssSelectorAst[] {
|
||||
delimiters |= LBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG;
|
||||
|
||||
const selectors: CssSelectorAst[] = [];
|
||||
let isParsingSelectors = true;
|
||||
while (isParsingSelectors) {
|
||||
selectors.push(this._parseSelector(delimiters));
|
||||
|
||||
isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters);
|
||||
|
||||
if (isParsingSelectors) {
|
||||
this._consume(CssTokenType.Character, ',');
|
||||
isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters);
|
||||
if (isParsingSelectors) {
|
||||
this._scanner.consumeWhitespace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectors;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_scan(): CssToken {
|
||||
const output = this._scanner.scan();
|
||||
const token = output.token;
|
||||
const error = output.error;
|
||||
if (error != null) {
|
||||
this._error(getRawMessage(error), token);
|
||||
}
|
||||
this._lastToken = token;
|
||||
return token;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_getScannerIndex(): number { return this._scanner.index; }
|
||||
|
||||
/** @internal */
|
||||
_consume(type: CssTokenType, value: string = null): CssToken {
|
||||
const output = this._scanner.consume(type, value);
|
||||
const token = output.token;
|
||||
const error = output.error;
|
||||
if (error != null) {
|
||||
this._error(getRawMessage(error), token);
|
||||
}
|
||||
this._lastToken = token;
|
||||
return token;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseKeyframeBlock(delimiters: number): CssBlockAst {
|
||||
delimiters |= RBRACE_DELIM_FLAG;
|
||||
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
|
||||
|
||||
const startToken = this._consume(CssTokenType.Character, '{');
|
||||
|
||||
const definitions: CssKeyframeDefinitionAst[] = [];
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
definitions.push(this._parseKeyframeDefinition(delimiters));
|
||||
}
|
||||
|
||||
const endToken = this._consume(CssTokenType.Character, '}');
|
||||
|
||||
const span = this._generateSourceSpan(startToken, endToken);
|
||||
return new CssBlockAst(span, definitions);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseKeyframeDefinition(delimiters: number): CssKeyframeDefinitionAst {
|
||||
const start = this._getScannerIndex();
|
||||
const stepTokens: CssToken[] = [];
|
||||
delimiters |= LBRACE_DELIM_FLAG;
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
stepTokens.push(this._parseKeyframeLabel(delimiters | COMMA_DELIM_FLAG));
|
||||
if (this._scanner.peek != chars.$LBRACE) {
|
||||
this._consume(CssTokenType.Character, ',');
|
||||
}
|
||||
}
|
||||
const stylesBlock = this._parseStyleBlock(delimiters | RBRACE_DELIM_FLAG);
|
||||
const span = this._generateSourceSpan(stepTokens[0], stylesBlock);
|
||||
const ast = new CssKeyframeDefinitionAst(span, stepTokens, stylesBlock);
|
||||
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
return ast;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseKeyframeLabel(delimiters: number): CssToken {
|
||||
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
|
||||
return mergeTokens(this._collectUntilDelim(delimiters));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parsePseudoSelector(delimiters: number): CssPseudoSelectorAst {
|
||||
const start = this._getScannerIndex();
|
||||
|
||||
delimiters &= ~COMMA_DELIM_FLAG;
|
||||
|
||||
// we keep the original value since we may use it to recurse when :not, :host are used
|
||||
const startingDelims = delimiters;
|
||||
|
||||
const startToken = this._consume(CssTokenType.Character, ':');
|
||||
const tokens = [startToken];
|
||||
|
||||
if (this._scanner.peek == chars.$COLON) { // ::something
|
||||
tokens.push(this._consume(CssTokenType.Character, ':'));
|
||||
}
|
||||
|
||||
const innerSelectors: CssSelectorAst[] = [];
|
||||
|
||||
this._scanner.setMode(CssLexerMode.PSEUDO_SELECTOR);
|
||||
|
||||
// host, host-context, lang, not, nth-child are all identifiers
|
||||
const pseudoSelectorToken = this._consume(CssTokenType.Identifier);
|
||||
const pseudoSelectorName = pseudoSelectorToken.strValue;
|
||||
tokens.push(pseudoSelectorToken);
|
||||
|
||||
// host(), lang(), nth-child(), etc...
|
||||
if (this._scanner.peek == chars.$LPAREN) {
|
||||
this._scanner.setMode(CssLexerMode.PSEUDO_SELECTOR_WITH_ARGUMENTS);
|
||||
|
||||
const openParenToken = this._consume(CssTokenType.Character, '(');
|
||||
tokens.push(openParenToken);
|
||||
|
||||
// :host(innerSelector(s)), :not(selector), etc...
|
||||
if (_pseudoSelectorSupportsInnerSelectors(pseudoSelectorName)) {
|
||||
let innerDelims = startingDelims | LPAREN_DELIM_FLAG | RPAREN_DELIM_FLAG;
|
||||
if (pseudoSelectorName == 'not') {
|
||||
// the inner selector inside of :not(...) can only be one
|
||||
// CSS selector (no commas allowed) ... This is according
|
||||
// to the CSS specification
|
||||
innerDelims |= COMMA_DELIM_FLAG;
|
||||
}
|
||||
|
||||
// :host(a, b, c) {
|
||||
this._parseSelectors(innerDelims).forEach((selector, index) => {
|
||||
innerSelectors.push(selector);
|
||||
});
|
||||
} else {
|
||||
// this branch is for things like "en-us, 2k + 1, etc..."
|
||||
// which all end up in pseudoSelectors like :lang, :nth-child, etc..
|
||||
const innerValueDelims = delimiters | LBRACE_DELIM_FLAG | COLON_DELIM_FLAG |
|
||||
RPAREN_DELIM_FLAG | LPAREN_DELIM_FLAG;
|
||||
while (!characterContainsDelimiter(this._scanner.peek, innerValueDelims)) {
|
||||
const token = this._scan();
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
const closeParenToken = this._consume(CssTokenType.Character, ')');
|
||||
tokens.push(closeParenToken);
|
||||
}
|
||||
|
||||
const end = this._getScannerIndex() - 1;
|
||||
const strValue = this._extractSourceContent(start, end);
|
||||
|
||||
const endToken = tokens[tokens.length - 1];
|
||||
const span = this._generateSourceSpan(startToken, endToken);
|
||||
return new CssPseudoSelectorAst(span, strValue, pseudoSelectorName, tokens, innerSelectors);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseSimpleSelector(delimiters: number): CssSimpleSelectorAst {
|
||||
const start = this._getScannerIndex();
|
||||
|
||||
delimiters |= COMMA_DELIM_FLAG;
|
||||
|
||||
this._scanner.setMode(CssLexerMode.SELECTOR);
|
||||
const selectorCssTokens: CssToken[] = [];
|
||||
const pseudoSelectors: CssPseudoSelectorAst[] = [];
|
||||
|
||||
let previousToken: CssToken;
|
||||
|
||||
const selectorPartDelimiters = delimiters | SPACE_DELIM_FLAG;
|
||||
let loopOverSelector = !characterContainsDelimiter(this._scanner.peek, selectorPartDelimiters);
|
||||
|
||||
let hasAttributeError = false;
|
||||
while (loopOverSelector) {
|
||||
const peek = this._scanner.peek;
|
||||
|
||||
switch (peek) {
|
||||
case chars.$COLON:
|
||||
let innerPseudo = this._parsePseudoSelector(delimiters);
|
||||
pseudoSelectors.push(innerPseudo);
|
||||
this._scanner.setMode(CssLexerMode.SELECTOR);
|
||||
break;
|
||||
|
||||
case chars.$LBRACKET:
|
||||
// we set the mode after the scan because attribute mode does not
|
||||
// allow attribute [] values. And this also will catch any errors
|
||||
// if an extra "[" is used inside.
|
||||
selectorCssTokens.push(this._scan());
|
||||
this._scanner.setMode(CssLexerMode.ATTRIBUTE_SELECTOR);
|
||||
break;
|
||||
|
||||
case chars.$RBRACKET:
|
||||
if (this._scanner.getMode() != CssLexerMode.ATTRIBUTE_SELECTOR) {
|
||||
hasAttributeError = true;
|
||||
}
|
||||
// we set the mode early because attribute mode does not
|
||||
// allow attribute [] values
|
||||
this._scanner.setMode(CssLexerMode.SELECTOR);
|
||||
selectorCssTokens.push(this._scan());
|
||||
break;
|
||||
|
||||
default:
|
||||
if (isSelectorOperatorCharacter(peek)) {
|
||||
loopOverSelector = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let token = this._scan();
|
||||
previousToken = token;
|
||||
selectorCssTokens.push(token);
|
||||
break;
|
||||
}
|
||||
|
||||
loopOverSelector = !characterContainsDelimiter(this._scanner.peek, selectorPartDelimiters);
|
||||
}
|
||||
|
||||
hasAttributeError =
|
||||
hasAttributeError || this._scanner.getMode() == CssLexerMode.ATTRIBUTE_SELECTOR;
|
||||
if (hasAttributeError) {
|
||||
this._error(
|
||||
`Unbalanced CSS attribute selector at column ${previousToken.line}:${previousToken.column}`,
|
||||
previousToken);
|
||||
}
|
||||
|
||||
let end = this._getScannerIndex() - 1;
|
||||
|
||||
// this happens if the selector is not directly followed by
|
||||
// a comma or curly brace without a space in between
|
||||
let operator: CssToken = null;
|
||||
let operatorScanCount = 0;
|
||||
let lastOperatorToken: CssToken = null;
|
||||
if (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
while (operator == null && !characterContainsDelimiter(this._scanner.peek, delimiters) &&
|
||||
isSelectorOperatorCharacter(this._scanner.peek)) {
|
||||
let token = this._scan();
|
||||
const tokenOperator = token.strValue;
|
||||
operatorScanCount++;
|
||||
lastOperatorToken = token;
|
||||
if (tokenOperator != SPACE_OPERATOR) {
|
||||
switch (tokenOperator) {
|
||||
case SLASH_CHARACTER:
|
||||
// /deep/ operator
|
||||
let deepToken = this._consume(CssTokenType.Identifier);
|
||||
let deepSlash = this._consume(CssTokenType.Character);
|
||||
let index = lastOperatorToken.index;
|
||||
let line = lastOperatorToken.line;
|
||||
let column = lastOperatorToken.column;
|
||||
if (deepToken != null && deepToken.strValue.toLowerCase() == 'deep' &&
|
||||
deepSlash.strValue == SLASH_CHARACTER) {
|
||||
token = new CssToken(
|
||||
lastOperatorToken.index, lastOperatorToken.column, lastOperatorToken.line,
|
||||
CssTokenType.Identifier, DEEP_OPERATOR_STR);
|
||||
} else {
|
||||
const text = SLASH_CHARACTER + deepToken.strValue + deepSlash.strValue;
|
||||
this._error(
|
||||
generateErrorMessage(
|
||||
this._getSourceContent(), `${text} is an invalid CSS operator`, text, index,
|
||||
line, column),
|
||||
lastOperatorToken);
|
||||
token = new CssToken(index, column, line, CssTokenType.Invalid, text);
|
||||
}
|
||||
break;
|
||||
|
||||
case GT_CHARACTER:
|
||||
// >>> operator
|
||||
if (this._scanner.peek == chars.$GT && this._scanner.peekPeek == chars.$GT) {
|
||||
this._consume(CssTokenType.Character, GT_CHARACTER);
|
||||
this._consume(CssTokenType.Character, GT_CHARACTER);
|
||||
token = new CssToken(
|
||||
lastOperatorToken.index, lastOperatorToken.column, lastOperatorToken.line,
|
||||
CssTokenType.Identifier, TRIPLE_GT_OPERATOR_STR);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
operator = token;
|
||||
}
|
||||
}
|
||||
|
||||
// so long as there is an operator then we can have an
|
||||
// ending value that is beyond the selector value ...
|
||||
// otherwise it's just a bunch of trailing whitespace
|
||||
if (operator != null) {
|
||||
end = operator.index;
|
||||
}
|
||||
}
|
||||
|
||||
this._scanner.consumeWhitespace();
|
||||
|
||||
const strValue = this._extractSourceContent(start, end);
|
||||
|
||||
// if we do come across one or more spaces inside of
|
||||
// the operators loop then an empty space is still a
|
||||
// valid operator to use if something else was not found
|
||||
if (operator == null && operatorScanCount > 0 && this._scanner.peek != chars.$LBRACE) {
|
||||
operator = lastOperatorToken;
|
||||
}
|
||||
|
||||
// please note that `endToken` is reassigned multiple times below
|
||||
// so please do not optimize the if statements into if/elseif
|
||||
let startTokenOrAst: CssToken|CssAst = null;
|
||||
let 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 (operator != null) {
|
||||
startTokenOrAst = startTokenOrAst || operator;
|
||||
endTokenOrAst = operator;
|
||||
}
|
||||
|
||||
const span = this._generateSourceSpan(startTokenOrAst, endTokenOrAst);
|
||||
return new CssSimpleSelectorAst(span, selectorCssTokens, strValue, pseudoSelectors, operator);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseSelector(delimiters: number): CssSelectorAst {
|
||||
delimiters |= COMMA_DELIM_FLAG;
|
||||
this._scanner.setMode(CssLexerMode.SELECTOR);
|
||||
|
||||
const simpleSelectors: CssSimpleSelectorAst[] = [];
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
simpleSelectors.push(this._parseSimpleSelector(delimiters));
|
||||
this._scanner.consumeWhitespace();
|
||||
}
|
||||
|
||||
const firstSelector = simpleSelectors[0];
|
||||
const lastSelector = simpleSelectors[simpleSelectors.length - 1];
|
||||
const span = this._generateSourceSpan(firstSelector, lastSelector);
|
||||
return new CssSelectorAst(span, simpleSelectors);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseValue(delimiters: number): CssStyleValueAst {
|
||||
delimiters |= RBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG | NEWLINE_DELIM_FLAG;
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_VALUE);
|
||||
const start = this._getScannerIndex();
|
||||
|
||||
const tokens: CssToken[] = [];
|
||||
let wsStr = '';
|
||||
let previous: CssToken;
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
let token: CssToken;
|
||||
if (previous != null && previous.type == CssTokenType.Identifier &&
|
||||
this._scanner.peek == chars.$LPAREN) {
|
||||
token = this._consume(CssTokenType.Character, '(');
|
||||
tokens.push(token);
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_VALUE_FUNCTION);
|
||||
|
||||
token = this._scan();
|
||||
tokens.push(token);
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_VALUE);
|
||||
|
||||
token = this._consume(CssTokenType.Character, ')');
|
||||
tokens.push(token);
|
||||
} else {
|
||||
token = this._scan();
|
||||
if (token.type == CssTokenType.Whitespace) {
|
||||
wsStr += token.strValue;
|
||||
} else {
|
||||
wsStr = '';
|
||||
tokens.push(token);
|
||||
}
|
||||
}
|
||||
previous = token;
|
||||
}
|
||||
|
||||
const end = this._getScannerIndex() - 1;
|
||||
this._scanner.consumeWhitespace();
|
||||
|
||||
const code = this._scanner.peek;
|
||||
if (code == chars.$SEMICOLON) {
|
||||
this._consume(CssTokenType.Character, ';');
|
||||
} else if (code != chars.$RBRACE) {
|
||||
this._error(
|
||||
generateErrorMessage(
|
||||
this._getSourceContent(), `The CSS key/value definition did not end with a semicolon`,
|
||||
previous.strValue, previous.index, previous.line, previous.column),
|
||||
previous);
|
||||
}
|
||||
|
||||
const strValue = this._extractSourceContent(start, end);
|
||||
const startToken = tokens[0];
|
||||
const endToken = tokens[tokens.length - 1];
|
||||
const span = this._generateSourceSpan(startToken, endToken);
|
||||
return new CssStyleValueAst(span, tokens, strValue);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_collectUntilDelim(delimiters: number, assertType: CssTokenType = null): CssToken[] {
|
||||
const tokens: CssToken[] = [];
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
const val = assertType != null ? this._consume(assertType) : this._scan();
|
||||
tokens.push(val);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseBlock(delimiters: number): CssBlockAst {
|
||||
delimiters |= RBRACE_DELIM_FLAG;
|
||||
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
|
||||
const startToken = this._consume(CssTokenType.Character, '{');
|
||||
this._scanner.consumeEmptyStatements();
|
||||
|
||||
const results: CssRuleAst[] = [];
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
results.push(this._parseRule(delimiters));
|
||||
}
|
||||
|
||||
const endToken = this._consume(CssTokenType.Character, '}');
|
||||
|
||||
this._scanner.setMode(CssLexerMode.BLOCK);
|
||||
this._scanner.consumeEmptyStatements();
|
||||
|
||||
const span = this._generateSourceSpan(startToken, endToken);
|
||||
return new CssBlockAst(span, results);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseStyleBlock(delimiters: number): CssStylesBlockAst {
|
||||
delimiters |= RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG;
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
|
||||
|
||||
const startToken = this._consume(CssTokenType.Character, '{');
|
||||
if (startToken.numValue != chars.$LBRACE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definitions: CssDefinitionAst[] = [];
|
||||
this._scanner.consumeEmptyStatements();
|
||||
|
||||
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
|
||||
definitions.push(this._parseDefinition(delimiters));
|
||||
this._scanner.consumeEmptyStatements();
|
||||
}
|
||||
|
||||
const endToken = this._consume(CssTokenType.Character, '}');
|
||||
|
||||
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
|
||||
this._scanner.consumeEmptyStatements();
|
||||
|
||||
const span = this._generateSourceSpan(startToken, endToken);
|
||||
return new CssStylesBlockAst(span, definitions);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_parseDefinition(delimiters: number): CssDefinitionAst {
|
||||
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
|
||||
|
||||
let prop = this._consume(CssTokenType.Identifier);
|
||||
let parseValue: boolean = false;
|
||||
let value: CssStyleValueAst = null;
|
||||
let endToken: CssToken|CssStyleValueAst = prop;
|
||||
|
||||
// the colon value separates the prop from the style.
|
||||
// there are a few cases as to what could happen if it
|
||||
// is missing
|
||||
switch (this._scanner.peek) {
|
||||
case chars.$SEMICOLON:
|
||||
case chars.$RBRACE:
|
||||
case chars.$EOF:
|
||||
parseValue = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
let propStr = [prop.strValue];
|
||||
if (this._scanner.peek != chars.$COLON) {
|
||||
// this will throw the error
|
||||
const nextValue = this._consume(CssTokenType.Character, ':');
|
||||
propStr.push(nextValue.strValue);
|
||||
|
||||
const remainingTokens = this._collectUntilDelim(
|
||||
delimiters | COLON_DELIM_FLAG | SEMICOLON_DELIM_FLAG, CssTokenType.Identifier);
|
||||
if (remainingTokens.length > 0) {
|
||||
remainingTokens.forEach((token) => { propStr.push(token.strValue); });
|
||||
}
|
||||
|
||||
endToken = prop =
|
||||
new CssToken(prop.index, prop.column, prop.line, prop.type, propStr.join(' '));
|
||||
}
|
||||
|
||||
// this means we've reached the end of the definition and/or block
|
||||
if (this._scanner.peek == chars.$COLON) {
|
||||
this._consume(CssTokenType.Character, ':');
|
||||
parseValue = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (parseValue) {
|
||||
value = this._parseValue(delimiters);
|
||||
endToken = value;
|
||||
} else {
|
||||
this._error(
|
||||
generateErrorMessage(
|
||||
this._getSourceContent(), `The CSS property was not paired with a style value`,
|
||||
prop.strValue, prop.index, prop.line, prop.column),
|
||||
prop);
|
||||
}
|
||||
|
||||
const span = this._generateSourceSpan(prop, endToken);
|
||||
return new CssDefinitionAst(span, prop, value);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_assertCondition(status: boolean, errorMessage: string, problemToken: CssToken): boolean {
|
||||
if (!status) {
|
||||
this._error(errorMessage, problemToken);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_error(message: string, problemToken: CssToken) {
|
||||
const length = problemToken.strValue.length;
|
||||
const error = CssParseError.create(
|
||||
this._file, 0, problemToken.line, problemToken.column, length, message);
|
||||
this._errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
export class CssParseError extends ParseError {
|
||||
static create(
|
||||
file: ParseSourceFile, offset: number, line: number, col: number, length: number,
|
||||
errMsg: string): CssParseError {
|
||||
const start = new ParseLocation(file, offset, line, col);
|
||||
const end = new ParseLocation(file, offset, line, col + length);
|
||||
const span = new ParseSourceSpan(start, end);
|
||||
return new CssParseError(span, 'CSS Parse Error: ' + errMsg);
|
||||
}
|
||||
|
||||
constructor(span: ParseSourceSpan, message: string) { super(span, message); }
|
||||
}
|
Reference in New Issue
Block a user