2016-06-13 18:58:04 +02:00

1020 lines
30 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {TokenType, Scanner, IToken} from './cssScanner';
import * as nodes from './cssNodes';
import {ParseError, CSSIssueType} from './cssErrors';
import * as languageFacts from '../services/languageFacts';
import {TextDocument} from 'vscode-languageserver';
export interface IMark {
prev: IToken;
curr: IToken;
pos: number;
}
/// <summary>
/// A parser for the css core specification. See for reference:
/// http://www.w3.org/TR/CSS21/syndata.html#tokenization
/// </summary>
export class Parser {
public scanner: Scanner;
public token: IToken;
public prevToken: IToken;
private lastErrorToken: IToken;
constructor(scnr: Scanner = new Scanner()) {
this.scanner = scnr;
this.token = null;
this.prevToken = null;
}
public peek(type: TokenType, text?: string, ignoreCase: boolean = true): boolean {
if (type !== this.token.type) {
return false;
}
if (typeof text !== 'undefined') {
if (ignoreCase) {
return text.toLowerCase() === this.token.text.toLowerCase();
} else {
return text === this.token.text;
}
}
return true;
}
public peekRegExp(type: TokenType, regEx: RegExp): boolean {
if (type !== this.token.type) {
return false;
}
return regEx.test(this.token.text);
}
public hasWhitespace(): boolean {
return this.prevToken && (this.prevToken.offset + this.prevToken.len !== this.token.offset);
}
public consumeToken(): void {
this.prevToken = this.token;
this.token = this.scanner.scan();
}
public mark(): IMark {
return {
prev: this.prevToken,
curr: this.token,
pos: this.scanner.pos()
};
}
public restoreAtMark(mark: IMark): void {
this.prevToken = mark.prev;
this.token = mark.curr;
this.scanner.goBackTo(mark.pos);
}
public acceptOne(type: TokenType, text?: string[], ignoreCase: boolean = true): boolean {
for (let i = 0; i < text.length; i++) {
if (this.peek(type, text[i], ignoreCase)) {
this.consumeToken();
return true;
}
}
return false;
}
public accept(type: TokenType, text?: string, ignoreCase: boolean = true): boolean {
if (this.peek(type, text, ignoreCase)) {
this.consumeToken();
return true;
}
return false;
}
public resync(resyncTokens: TokenType[], resyncStopTokens: TokenType[]): boolean {
while (true) {
if (resyncTokens && resyncTokens.indexOf(this.token.type) !== -1) {
this.consumeToken();
return true;
} else if (resyncStopTokens && resyncStopTokens.indexOf(this.token.type) !== -1) {
return true;
} else {
if (this.token.type === TokenType.EOF) {
return false;
}
this.token = this.scanner.scan();
}
}
}
public createNode(nodeType: nodes.NodeType): nodes.Node {
return new nodes.Node(this.token.offset, this.token.len, nodeType);
}
public create(ctor: any): nodes.Node {
let obj = Object.create(ctor.prototype);
ctor.apply(obj, [this.token.offset, this.token.len]);
return obj;
}
public finish<T extends nodes.Node>(node: T, error?: CSSIssueType, resyncTokens?: TokenType[], resyncStopTokens?: TokenType[]): T {
// parseNumeric misuses error for boolean flagging (however the real error mustn't be a false)
// + nodelist offsets mustn't be modified, because there is a offset hack in rulesets for smartselection
if (!(node instanceof nodes.Nodelist)) {
if (error) {
this.markError(node, error, resyncTokens, resyncStopTokens);
}
// set the node end position
if (this.prevToken !== null) {
// length with more elements belonging together
let prevEnd = this.prevToken.offset + this.prevToken.len;
node.length = prevEnd > node.offset ? prevEnd - node.offset : 0; // offset is taken from current token, end from previous: Use 0 for empty nodes
}
}
return node;
}
public markError<T extends nodes.Node>(node: T, error: CSSIssueType, resyncTokens?: TokenType[], resyncStopTokens?: TokenType[]): void {
if (this.token !== this.lastErrorToken) { // do not report twice on the same token
node.addIssue(new nodes.Marker(node, error, nodes.Level.Error, null, this.token.offset, this.token.len));
this.lastErrorToken = this.token;
}
if (resyncTokens || resyncStopTokens) {
this.resync(resyncTokens, resyncStopTokens);
}
}
public parseStylesheet(textDocument: TextDocument): nodes.Stylesheet {
let versionId = textDocument.version;
let textProvider = (offset: number, length: number) => {
if (textDocument.version !== versionId) {
throw new Error('Underlying model has changed, AST is no longer valid');
}
return textDocument.getText().substr(offset, length);
};
return this.internalParse(textDocument.getText(), this._parseStylesheet, textProvider);
}
public internalParse<T extends nodes.Node>(input: string, parseFunc: () => T, textProvider?: nodes.ITextProvider): T {
this.scanner.setSource(input);
this.token = this.scanner.scan();
let node = parseFunc.bind(this)();
if (node) {
if (textProvider) {
node.textProvider = textProvider;
} else {
node.textProvider = (offset: number, length: number) => { return input.substr(offset, length); };
}
}
return node;
}
public _parseStylesheet(): nodes.Stylesheet {
let node = <nodes.Stylesheet>this.create(nodes.Stylesheet);
node.addChild(this._parseCharset());
let inRecovery = false;
do {
let hasMatch = false;
do {
hasMatch = false;
let statement = this._parseStylesheetStatement();
if (statement) {
node.addChild(statement);
hasMatch = true;
inRecovery = false;
if (!this.peek(TokenType.EOF) && this._needsSemicolonAfter(statement) && !this.accept(TokenType.SemiColon)) {
this.markError(node, ParseError.SemiColonExpected);
}
}
while (this.accept(TokenType.SemiColon) || this.accept(TokenType.CDO) || this.accept(TokenType.CDC)) {
// accept empty statements
hasMatch = true;
inRecovery = false;
}
} while (hasMatch);
if (this.peek(TokenType.EOF)) {
break;
}
if (!inRecovery) {
if (this.peek(TokenType.AtKeyword)) {
this.markError(node, ParseError.UnknownAtRule);
} else {
this.markError(node, ParseError.RuleOrSelectorExpected);
}
inRecovery = true;
}
this.consumeToken();
} while (!this.peek(TokenType.EOF));
return this.finish(node);
}
public _parseStylesheetStatement(): nodes.Node {
return this._parseRuleset(false)
|| this._parseImport()
|| this._parseMedia()
|| this._parsePage()
|| this._parseFontFace()
|| this._parseKeyframe()
|| this._parseViewPort()
|| this._parseNamespace()
|| this._parseDocument();
}
public _tryParseRuleset(isNested: boolean): nodes.RuleSet {
let mark = this.mark();
if (this._parseSelector(isNested)) {
while (this.accept(TokenType.Comma) && this._parseSelector(isNested)) {
// loop
}
if (this.accept(TokenType.CurlyL)) {
this.restoreAtMark(mark);
return this._parseRuleset(isNested);
}
}
this.restoreAtMark(mark);
return null;
}
public _parseRuleset(isNested: boolean = false): nodes.RuleSet {
let node = <nodes.RuleSet>this.create(nodes.RuleSet);
if (!node.getSelectors().addChild(this._parseSelector(isNested))) {
return null;
}
while (this.accept(TokenType.Comma) && node.getSelectors().addChild(this._parseSelector(isNested))) {
// loop
}
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parseRuleSetDeclaration(): nodes.Node {
return this._parseDeclaration();
}
public _needsSemicolonAfter(node: nodes.Node): boolean {
switch (node.type) {
case nodes.NodeType.Keyframe:
case nodes.NodeType.ViewPort:
case nodes.NodeType.Media:
case nodes.NodeType.Ruleset:
case nodes.NodeType.Namespace:
case nodes.NodeType.If:
case nodes.NodeType.For:
case nodes.NodeType.Each:
case nodes.NodeType.While:
case nodes.NodeType.MixinDeclaration:
case nodes.NodeType.FunctionDeclaration:
return false;
case nodes.NodeType.VariableDeclaration:
case nodes.NodeType.ExtendsReference:
case nodes.NodeType.MixinContent:
case nodes.NodeType.ReturnStatement:
case nodes.NodeType.MediaQuery:
case nodes.NodeType.Debug:
case nodes.NodeType.Import:
return true;
case nodes.NodeType.MixinReference:
return !(<nodes.MixinReference>node).getContent();
case nodes.NodeType.Declaration:
return !(<nodes.Declaration>node).getNestedProperties();
}
return false;
}
public _parseDeclarations(parseDeclaration: () => nodes.Node): nodes.Declarations {
let node = <nodes.Declarations>this.create(nodes.Declarations);
if (!this.accept(TokenType.CurlyL)) {
return null;
}
let decl = parseDeclaration();
while (node.addChild(decl)) {
if (this.peek(TokenType.CurlyR)) {
break;
}
if (this._needsSemicolonAfter(decl) && !this.accept(TokenType.SemiColon)) {
return this.finish(node, ParseError.SemiColonExpected, [TokenType.SemiColon, TokenType.CurlyR]);
}
while (this.accept(TokenType.SemiColon)) {
// accept empty statements
}
decl = parseDeclaration();
}
if (!this.accept(TokenType.CurlyR)) {
return this.finish(node, ParseError.RightCurlyExpected, [TokenType.CurlyR, TokenType.SemiColon]);
}
return this.finish(node);
}
public _parseBody<T extends nodes.BodyDeclaration>(node: T, parseDeclaration: () => nodes.Node): T {
if (!node.setDeclarations(this._parseDeclarations(parseDeclaration))) {
return this.finish(node, ParseError.LeftCurlyExpected, [TokenType.CurlyR, TokenType.SemiColon]);
}
return this.finish(node);
}
public _parseSelector(isNested: boolean): nodes.Selector {
let node = <nodes.Selector>this.create(nodes.Selector);
let hasContent = false;
if (isNested) {
// nested selectors can start with a combinator
hasContent = node.addChild(this._parseCombinator());
}
while (node.addChild(this._parseSimpleSelector())) {
hasContent = true;
node.addChild(this._parseCombinator()); // optional
}
return hasContent ? this.finish(node) : null;
}
public _parseDeclaration(resyncStopTokens?: TokenType[]): nodes.Declaration {
let node = <nodes.Declaration>this.create(nodes.Declaration);
if (!node.setProperty(this._parseProperty())) {
return null;
}
if (!this.accept(TokenType.Colon)) {
return <nodes.Declaration>this.finish(node, ParseError.ColonExpected, [TokenType.Colon], resyncStopTokens);
}
node.colonPosition = this.prevToken.offset;
if (!node.setValue(this._parseExpr())) {
return this.finish(node, ParseError.PropertyValueExpected);
}
node.addChild(this._parsePrio());
if (this.peek(TokenType.SemiColon)) {
node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist
}
return this.finish(node);
}
public _tryToParseDeclaration(): nodes.Declaration {
let mark = this.mark();
if (this._parseProperty() && this.accept(TokenType.Colon)) {
// looks like a declaration, go ahead
this.restoreAtMark(mark);
return this._parseDeclaration();
}
this.restoreAtMark(mark);
return null;
}
public _parseProperty(): nodes.Property {
let node = <nodes.Property>this.create(nodes.Property);
let mark = this.mark();
if (this.accept(TokenType.Delim, '*') || this.accept(TokenType.Delim, '_')) {
// support for IE 5.x, 6 and 7 star hack: see http://en.wikipedia.org/wiki/CSS_filter#Star_hack
if (this.hasWhitespace()) {
this.restoreAtMark(mark);
return null;
}
}
if (node.setIdentifier(this._parseIdent())) {
return <nodes.Property>this.finish(node);
}
return null;
}
public _parseCharset(): nodes.Node {
let node = this.create(nodes.Node);
if (!this.accept(TokenType.Charset)) {
return null;
}
if (!this.accept(TokenType.String)) {
return this.finish(node, ParseError.IdentifierExpected);
}
if (!this.accept(TokenType.SemiColon)) {
return this.finish(node, ParseError.SemiColonExpected);
}
return this.finish(node);
}
public _parseImport(): nodes.Node {
let node = <nodes.Import>this.create(nodes.Import);
if (!this.accept(TokenType.AtKeyword, '@import')) {
return null;
}
if (!this.accept(TokenType.URI) && !this.accept(TokenType.String)) {
return this.finish(node, ParseError.URIOrStringExpected);
}
node.setMedialist(this._parseMediaList());
return this.finish(node);
}
public _parseNamespace(): nodes.Node {
// http://www.w3.org/TR/css3-namespace/
// namespace : NAMESPACE_SYM S* [IDENT S*]? [STRING|URI] S* ';' S*
let node = <nodes.Namespace>this.create(nodes.Namespace);
if (!this.accept(TokenType.AtKeyword, '@namespace')) {
return null;
}
node.addChild(this._parseIdent()); // optional prefix
if (!this.accept(TokenType.URI) && !this.accept(TokenType.String)) {
return this.finish(node, ParseError.URIExpected, [TokenType.SemiColon]);
}
if (!this.accept(TokenType.SemiColon)) {
return this.finish(node, ParseError.SemiColonExpected);
}
return this.finish(node);
}
public _parseFontFace(): nodes.Node {
if (!this.peek(TokenType.AtKeyword, '@font-face')) {
return null;
}
let node = <nodes.FontFace>this.create(nodes.FontFace);
this.consumeToken(); // @font-face
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parseViewPort(): nodes.Node {
if (!this.peek(TokenType.AtKeyword, '@-ms-viewport') &&
!this.peek(TokenType.AtKeyword, '@-o-viewport') &&
!this.peek(TokenType.AtKeyword, '@viewport')
) {
return null;
}
let node = <nodes.ViewPort>this.create(nodes.ViewPort);
this.consumeToken(); // @-ms-viewport
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parseKeyframe(): nodes.Node {
let node = <nodes.Keyframe>this.create(nodes.Keyframe);
let atNode = this.create(nodes.Node);
if (!this.accept(TokenType.AtKeyword, '@keyframes') &&
!this.accept(TokenType.AtKeyword, '@-webkit-keyframes') &&
!this.accept(TokenType.AtKeyword, '@-ms-keyframes') &&
!this.accept(TokenType.AtKeyword, '@-moz-keyframes') &&
!this.accept(TokenType.AtKeyword, '@-o-keyframes')) {
return null;
}
node.setKeyword(this.finish(atNode));
if (atNode.getText() === '@-ms-keyframes') { // -ms-keyframes never existed
this.markError(atNode, ParseError.UnknownKeyword);
}
if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Keyframe]))) {
return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]);
}
return this._parseBody(node, this._parseKeyframeSelector.bind(this));
}
public _parseKeyframeSelector(): nodes.Node {
let node = <nodes.KeyframeSelector>this.create(nodes.KeyframeSelector);
if (!node.addChild(this._parseIdent()) && !this.accept(TokenType.Percentage)) {
return null;
}
while (this.accept(TokenType.Comma)) {
if (!node.addChild(this._parseIdent()) && !this.accept(TokenType.Percentage)) {
return this.finish(node, ParseError.PercentageExpected);
}
}
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parseMediaDeclaration(): nodes.Node {
return this._tryParseRuleset(false) || this._tryToParseDeclaration() || this._parseStylesheetStatement();
}
public _parseMedia(): nodes.Node {
// MEDIA_SYM S* media_query_list '{' S* ruleset* '}' S*
// media_query_list : S* [media_query [ ',' S* media_query ]* ]?
let node = <nodes.Media>this.create(nodes.Media);
if (!this.accept(TokenType.AtKeyword, '@media')) {
return null;
}
if (!node.addChild(this._parseMediaQuery([TokenType.CurlyL]))) {
return this.finish(node, ParseError.IdentifierExpected);
}
while (this.accept(TokenType.Comma)) {
if (!node.addChild(this._parseMediaQuery([TokenType.CurlyL]))) {
return this.finish(node, ParseError.IdentifierExpected);
}
}
return this._parseBody(node, this._parseMediaDeclaration.bind(this));
}
public _parseMediaQuery(resyncStopToken: TokenType[]): nodes.Node {
// http://www.w3.org/TR/css3-mediaqueries/
// media_query : [ONLY | NOT]? S* IDENT S* [ AND S* expression ]* | expression [ AND S* expression ]*
// expression : '(' S* IDENT S* [ ':' S* expr ]? ')' S*
let node = <nodes.MediaQuery>this.create(nodes.MediaQuery);
let parseExpression = true;
let hasContent = false;
if (!this.peek(TokenType.ParenthesisL)) {
if (this.accept(TokenType.Ident, 'only', true) || this.accept(TokenType.Ident, 'not', true)) {
// optional
}
if (!node.addChild(this._parseIdent())) {
return null;
}
hasContent = true;
parseExpression = this.accept(TokenType.Ident, 'and', true);
}
while (parseExpression) {
if (!this.accept(TokenType.ParenthesisL)) {
if (hasContent) {
return this.finish(node, ParseError.LeftParenthesisExpected, [], resyncStopToken);
}
return null;
}
if (!node.addChild(this._parseMediaFeatureName())) {
return this.finish(node, ParseError.IdentifierExpected, [], resyncStopToken);
}
if (this.accept(TokenType.Colon)) {
if (!node.addChild(this._parseExpr())) {
return this.finish(node, ParseError.TermExpected, [], resyncStopToken);
}
}
if (!this.accept(TokenType.ParenthesisR)) {
return this.finish(node, ParseError.RightParenthesisExpected, [], resyncStopToken);
}
parseExpression = this.accept(TokenType.Ident, 'and', true);
}
return node;
}
public _parseMediaFeatureName(): nodes.Node {
return this._parseIdent();
}
public _parseMediaList(): nodes.Medialist {
let node = <nodes.Medialist>this.create(nodes.Medialist);
if (node.getMediums().addChild(this._parseMedium())) {
while (this.accept(TokenType.Comma)) {
if (!node.getMediums().addChild(this._parseMedium())) {
return this.finish(node, ParseError.IdentifierExpected);
}
}
return <nodes.Medialist>this.finish(node);
}
return null;
}
public _parseMedium(): nodes.Node {
let node = this.create(nodes.Node);
if (node.addChild(this._parseIdent())) {
return this.finish(node);
} else {
return null;
}
}
public _parsePageDeclaration(): nodes.Node {
return this._parsePageMarginBox() || this._parseRuleSetDeclaration();
}
public _parsePage(): nodes.Node {
// http://www.w3.org/TR/css3-page/
// page_rule : PAGE_SYM S* page_selector_list '{' S* page_body '}' S*
// page_body : /* Can be empty */ declaration? [ ';' S* page_body ]? | page_margin_box page_body
let node = <nodes.Page>this.create(nodes.Page);
if (!this.accept(TokenType.AtKeyword, '@Page')) {
return null;
}
if (node.addChild(this._parsePageSelector())) {
while (this.accept(TokenType.Comma)) {
if (!node.addChild(this._parsePageSelector())) {
return this.finish(node, ParseError.IdentifierExpected);
}
}
}
return this._parseBody(node, this._parsePageDeclaration.bind(this));
}
public _parsePageMarginBox(): nodes.Node {
// page_margin_box : margin_sym S* '{' S* declaration? [ ';' S* declaration? ]* '}' S*
let node = <nodes.PageBoxMarginBox>this.create(nodes.PageBoxMarginBox);
if (!this.peek(TokenType.AtKeyword)) {
return null;
}
if (!this.acceptOne(TokenType.AtKeyword, languageFacts.getPageBoxDirectives())) {
this.markError(node, ParseError.UnknownAtRule, [], [TokenType.CurlyL]);
}
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parsePageSelector(): nodes.Node {
// page_selector : pseudo_page+ | IDENT pseudo_page*
// pseudo_page : ':' [ "left" | "right" | "first" | "blank" ];
let node = this.create(nodes.Node);
if (!this.peek(TokenType.Ident) && !this.peek(TokenType.Colon)) {
return null;
}
node.addChild(this._parseIdent()); // optional ident
if (this.accept(TokenType.Colon)) {
if (!node.addChild(this._parseIdent())) { // optional ident
return this.finish(node, ParseError.IdentifierExpected);
}
}
return this.finish(node);
}
public _parseDocument(): nodes.Node {
// -moz-document is experimental but has been pushed to css4
let node = <nodes.Document>this.create(nodes.Document);
if (!this.accept(TokenType.AtKeyword, '@-moz-document')) {
return null;
}
this.resync([], [TokenType.CurlyL]); // ignore all the rules
return this._parseBody(node, this._parseStylesheetStatement.bind(this));
}
public _parseOperator(): nodes.Node {
// these are operators for binary expressions
let node = this.createNode(nodes.NodeType.Operator);
if (this.accept(TokenType.Delim, '/') ||
this.accept(TokenType.Delim, '*') ||
this.accept(TokenType.Delim, '+') ||
this.accept(TokenType.Delim, '-') ||
this.accept(TokenType.Dashmatch) ||
this.accept(TokenType.Includes) ||
this.accept(TokenType.SubstringOperator) ||
this.accept(TokenType.PrefixOperator) ||
this.accept(TokenType.SuffixOperator) ||
this.accept(TokenType.Delim, '=')) { // doesn't stick to the standard here
return this.finish(node);
} else {
return null;
}
}
public _parseUnaryOperator(): nodes.Node {
let node = this.create(nodes.Node);
if (this.accept(TokenType.Delim, '+') || this.accept(TokenType.Delim, '-')) {
return this.finish(node);
} else {
return null;
}
}
public _parseCombinator(): nodes.Node {
let node = this.create(nodes.Node);
if (this.accept(TokenType.Delim, '>')) {
node.type = nodes.NodeType.SelectorCombinatorParent;
return this.finish(node);
} else if (this.accept(TokenType.Delim, '+')) {
node.type = nodes.NodeType.SelectorCombinatorSibling;
return this.finish(node);
} else if (this.accept(TokenType.Delim, '~')) {
node.type = nodes.NodeType.SelectorCombinatorAllSiblings;
return this.finish(node);
} else {
return null;
}
}
public _parseSimpleSelector(): nodes.Node {
// simple_selector
// : element_name [ HASH | class | attrib | pseudo ]* | [ HASH | class | attrib | pseudo ]+ ;
let node = <nodes.SimpleSelector>this.create(nodes.SimpleSelector);
let c = 0;
if (node.addChild(this._parseElementName())) {
c++;
}
while ((c === 0 || !this.hasWhitespace()) && node.addChild(this._parseSimpleSelectorBody())) {
c++;
}
return c > 0 ? this.finish(node) : null;
}
public _parseSimpleSelectorBody(): nodes.Node {
return this._parsePseudo() || this._parseHash() || this._parseClass() || this._parseAttrib();
}
public _parseSelectorIdent(): nodes.Node {
return this._parseIdent();
}
public _parseHash(): nodes.Node {
if (!this.peek(TokenType.Hash) && !this.peek(TokenType.Delim, '#')) {
return null;
}
let node = this.createNode(nodes.NodeType.IdentifierSelector);
if (this.accept(TokenType.Delim, '#')) {
if (this.hasWhitespace() || !node.addChild(this._parseSelectorIdent())) {
return this.finish(node, ParseError.IdentifierExpected);
}
} else {
this.consumeToken(); // TokenType.Hash
}
return this.finish(node);
}
public _parseClass(): nodes.Node {
// class: '.' IDENT ;
if (!this.peek(TokenType.Delim, '.')) {
return null;
}
let node = this.createNode(nodes.NodeType.ClassSelector);
this.consumeToken(); // '.'
if (this.hasWhitespace() || !node.addChild(this._parseSelectorIdent())) {
return this.finish(node, ParseError.IdentifierExpected);
}
return this.finish(node);
}
public _parseElementName(): nodes.Node {
// element_name: IDENT | '*';
let node = this.createNode(nodes.NodeType.ElementNameSelector);
if (node.addChild(this._parseSelectorIdent()) || this.accept(TokenType.Delim, '*')) {
return this.finish(node);
}
return null;
}
public _parseAttrib(): nodes.Node {
// attrib : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S* [ IDENT | STRING ] S* ]? ']'
if (!this.peek(TokenType.BracketL)) {
return null;
}
let node = this.createNode(nodes.NodeType.AttributeSelector);
this.consumeToken(); // BracketL
if (!node.addChild(this._parseBinaryExpr())) {
// is this bad?
}
if (!this.accept(TokenType.BracketR)) {
return this.finish(node, ParseError.RightSquareBracketExpected);
}
return this.finish(node);
}
public _parsePseudo(): nodes.Node {
// pseudo: ':' [ IDENT | FUNCTION S* [IDENT S*]? ')' ]
if (!this.peek(TokenType.Colon)) {
return null;
}
let pos = this.mark();
let node = this.createNode(nodes.NodeType.PseudoSelector);
this.consumeToken(); // Colon
if (!this.hasWhitespace() && this.accept(TokenType.Colon)) {
// optional, support ::
}
if (!this.hasWhitespace()) {
if (!node.addChild(this._parseIdent())) {
return this.finish(node, ParseError.IdentifierExpected);
}
if (!this.hasWhitespace() && this.accept(TokenType.ParenthesisL)) {
node.addChild(this._parseBinaryExpr() || this._parseSimpleSelector());
if (!this.accept(TokenType.ParenthesisR)) {
return this.finish(node, ParseError.RightParenthesisExpected);
}
}
return this.finish(node);
}
this.restoreAtMark(pos);
return null;
}
public _parsePrio(): nodes.Node {
if (!this.peek(TokenType.Exclamation)) {
return null;
}
let node = this.createNode(nodes.NodeType.Prio);
if (this.accept(TokenType.Exclamation) && this.accept(TokenType.Ident, 'important', true)) {
return this.finish(node);
}
return null;
}
public _parseExpr(stopOnComma: boolean = false): nodes.Expression {
let node = <nodes.Expression>this.create(nodes.Expression);
if (!node.addChild(this._parseBinaryExpr())) {
return null;
}
while (true) {
if (this.peek(TokenType.Comma)) { // optional
if (stopOnComma) {
return this.finish(node);
}
this.consumeToken();
}
if (!node.addChild(this._parseBinaryExpr())) {
break;
}
}
return this.finish(node);
}
public _parseBinaryExpr(preparsedLeft?: nodes.BinaryExpression, preparsedOper?: nodes.Node): nodes.Node {
let node = <nodes.BinaryExpression>this.create(nodes.BinaryExpression);
if (!node.setLeft((<nodes.Node>preparsedLeft || this._parseTerm()))) {
return null;
}
if (!node.setOperator(preparsedOper || this._parseOperator())) {
return this.finish(node);
}
if (!node.setRight(this._parseTerm())) {
return this.finish(node, ParseError.TermExpected);
}
// things needed for multiple binary expressions
node = <nodes.BinaryExpression>this.finish(node);
let operator = this._parseOperator();
if (operator) {
node = <nodes.BinaryExpression>this._parseBinaryExpr(node, operator);
}
return this.finish(node);
}
public _parseTerm(): nodes.Term {
let node = <nodes.Term>this.create(nodes.Term);
node.setOperator(this._parseUnaryOperator()); // optional
if (node.setExpression(this._parseFunction()) || // first function then ident
node.setExpression(this._parseIdent()) ||
node.setExpression(this._parseURILiteral()) ||
node.setExpression(this._parseStringLiteral()) ||
node.setExpression(this._parseNumeric()) ||
node.setExpression(this._parseHexColor()) ||
node.setExpression(this._parseOperation())
) {
return <nodes.Term>this.finish(node);
}
return null;
}
public _parseOperation(): nodes.Node {
let node = this.create(nodes.Node);
if (!this.accept(TokenType.ParenthesisL)) {
return null;
}
node.addChild(this._parseExpr());
if (!this.accept(TokenType.ParenthesisR)) {
return this.finish(node, ParseError.RightParenthesisExpected);
}
return this.finish(node);
}
public _parseNumeric(): nodes.NumericValue {
let node = <nodes.NumericValue>this.create(nodes.NumericValue);
if (this.accept(TokenType.Num) ||
this.accept(TokenType.Percentage) ||
this.accept(TokenType.Resolution) ||
this.accept(TokenType.Length) ||
this.accept(TokenType.EMS) ||
this.accept(TokenType.EXS) ||
this.accept(TokenType.Angle) ||
this.accept(TokenType.Time) ||
this.accept(TokenType.Dimension) ||
this.accept(TokenType.Freq)) {
return <nodes.NumericValue>this.finish(node);
}
return null;
}
public _parseStringLiteral(): nodes.Node {
let node = this.createNode(nodes.NodeType.StringLiteral);
if (this.accept(TokenType.String) || this.accept(TokenType.BadString)) {
return this.finish(node);
}
return null;
}
public _parseURILiteral(): nodes.Node {
let node = this.createNode(nodes.NodeType.URILiteral);
if (this.accept(TokenType.URI) || this.accept(TokenType.BadUri)) {
return this.finish(node);
}
return null;
}
public _parseIdent(referenceTypes?: nodes.ReferenceType[]): nodes.Identifier {
let node = <nodes.Identifier>this.create(nodes.Identifier);
if (referenceTypes) {
node.referenceTypes = referenceTypes;
}
if (this.accept(TokenType.Ident)) {
return this.finish(node);
}
return null;
}
public _parseFunction(): nodes.Function {
let pos = this.mark();
let node = <nodes.Function>this.create(nodes.Function);
if (!node.setIdentifier(this._parseFunctionIdentifier())) {
return null;
}
if (this.hasWhitespace() || !this.accept(TokenType.ParenthesisL)) {
this.restoreAtMark(pos);
return null;
}
if (node.getArguments().addChild(this._parseFunctionArgument())) {
while (this.accept(TokenType.Comma)) {
if (!node.getArguments().addChild(this._parseFunctionArgument())) {
this.markError(node, ParseError.ExpressionExpected);
}
}
}
if (!this.accept(TokenType.ParenthesisR)) {
return <nodes.Function>this.finish(node, ParseError.RightParenthesisExpected);
}
return <nodes.Function>this.finish(node);
}
public _parseFunctionIdentifier(): nodes.Identifier {
let node = <nodes.Identifier>this.create(nodes.Identifier);
node.referenceTypes = [nodes.ReferenceType.Function];
if (this.accept(TokenType.Ident, 'progid')) {
// support for IE7 specific filters: 'progid:DXImageTransform.Microsoft.MotionBlur(strength=13, direction=310)'
if (this.accept(TokenType.Colon)) {
while (this.accept(TokenType.Ident) && this.accept(TokenType.Delim, '.')) {
// loop
}
}
return this.finish(node);
} else if (this.accept(TokenType.Ident)) {
return this.finish(node);
}
return null;
}
public _parseFunctionArgument(): nodes.Node {
let node = <nodes.FunctionArgument>this.create(nodes.FunctionArgument);
if (node.setValue(this._parseExpr(true))) {
return this.finish(node);
}
return null;
}
public _parseHexColor(): nodes.Node {
if (this.peekRegExp(TokenType.Hash, /^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/g)) {
let node = this.create(nodes.HexColorValue);
this.consumeToken();
return this.finish(node);
} else {
return null;
}
}
}