326 строки
13 KiB
TypeScript
326 строки
13 KiB
TypeScript
//
|
|
// Copyright (C) Microsoft. All rights reserved.
|
|
//
|
|
|
|
/// <reference path="Interfaces.d.ts"/>
|
|
|
|
module IEDiagnosticsAdapter {
|
|
"use strict";
|
|
|
|
/**
|
|
* Class that synchronously iterates through a CSS document and constructs a list of ICssMediaQuery and ICssRuleset objects.
|
|
* The private members in this class store state used during the parse loop and that state is modified from
|
|
* the various subroutines executed in the loop.
|
|
*/
|
|
export class CssParser {
|
|
// A list that contains any number of ICssMediaQuery and ICssRuleset objects
|
|
private _rootNodes: any[];
|
|
private _text: string;
|
|
|
|
// Holds the index where text was last extracted from the source text
|
|
private _lastCheckpoint: number;
|
|
|
|
// Holds a string representing the current type of CSS construct that text is being extracted for
|
|
private _state: CssToken;
|
|
|
|
// Holds the index into the source text that the parser is currently inspecting at
|
|
private _index: number;
|
|
|
|
// Holds components of the CSS AST that are still being constructed
|
|
private _currentRuleset: ICssRuleset;
|
|
private _currentDeclaration: ICssDeclaration;
|
|
private _currentMediaQuery: ICssMediaQuery;
|
|
|
|
// Stores state about whether the loop has passed open quotes or open comments
|
|
private _inComment: boolean;
|
|
private _currentQuotationMark: string;
|
|
private _nextCharIsEscaped: boolean;
|
|
|
|
constructor(text: string) {
|
|
this._rootNodes = [];
|
|
this._text = text;
|
|
|
|
// Statement control
|
|
this._inComment = false;
|
|
this._currentQuotationMark = "";
|
|
this._nextCharIsEscaped = false;
|
|
|
|
// Search maintenance state
|
|
this._lastCheckpoint = 0;
|
|
this._state = CssToken.Selector;
|
|
|
|
// Storage for under-construction nodes
|
|
this._currentRuleset = null;
|
|
this._currentDeclaration = null;
|
|
this._currentMediaQuery = null;
|
|
}
|
|
|
|
/** Returns an array containing ICssRuleset and ICssMediaQuery objects */
|
|
public parseCss(): any[] {
|
|
this.parseText();
|
|
|
|
// Put any text that wasn't valid CSS into it's own node at the end of the file
|
|
this.handleIncompleteBlocks();
|
|
return this._rootNodes;
|
|
}
|
|
|
|
/** Returns an array containing a single rule, ICssRuleset and ICssMediaQuery objects */
|
|
public parseInlineCss(): ICssRuleset {
|
|
// inline CSS is just a list of properties. Set up the parser state to read them correctly.
|
|
this._currentRuleset = { originalOffset: this._lastCheckpoint, selector: "DoesNotMatter", declarations: [] };
|
|
this._state = CssToken.Property;
|
|
|
|
this.parseText();
|
|
|
|
Assert.isTrue(this._currentRuleset && this._rootNodes.length === 0, "Text was not valid inline CSS");
|
|
this._currentRuleset.endOffset = this._text.length;
|
|
|
|
return this._currentRuleset;
|
|
}
|
|
|
|
private parseText(): void {
|
|
for (this._index = 0; this._index < this._text.length; this._index++) {
|
|
if (this.handleQuoteCharacter()) {
|
|
} else if (this.handleCommentCharacter()) {
|
|
} else if (this.handleLeadingWhitespace()) {
|
|
} else if (this.handleMediaQueryStart()) {
|
|
} else if (this.handleMediaQueryOpenBracket()) {
|
|
} else if (this.handleMediaQueryCloseBracket()) {
|
|
} else if (this.handleSelectorOpenBracket()) {
|
|
} else if (this.handlePropertyColon()) {
|
|
} else if (this.handleValueSemicolon()) {
|
|
} else if (this.handleSelectorCloseBracket()) {
|
|
}
|
|
}
|
|
}
|
|
|
|
private handleMediaQueryStart(): boolean {
|
|
if (this._state === CssToken.Selector && !this._currentMediaQuery &&
|
|
this._lastCheckpoint >= this._index &&
|
|
this._text[this._index] === "@" && this._text.substr(this._index, 7).toLowerCase() === "@media ") {
|
|
this._state = CssToken.Media;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handleMediaQueryOpenBracket(): boolean {
|
|
if (this._state === CssToken.Media && this._text[this._index] === "{") {
|
|
var mediaText = this._text.substring(this._lastCheckpoint, this._index);
|
|
this._currentMediaQuery = { originalOffset: this._lastCheckpoint, query: mediaText, rulesets: [] };
|
|
|
|
this._lastCheckpoint = this._index + 1;
|
|
this._state = CssToken.Selector;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handleMediaQueryCloseBracket(): boolean {
|
|
if (this._state === CssToken.Selector && this._text[this._index] === "}" && this._currentMediaQuery) {
|
|
this._lastCheckpoint = this._index + 1;
|
|
this._state = CssToken.Selector;
|
|
this._currentMediaQuery.endOffset = this._index + 1;
|
|
this._rootNodes.push(this._currentMediaQuery);
|
|
this._currentMediaQuery = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handleSelectorOpenBracket(): boolean {
|
|
if (this._state === CssToken.Selector && this._text[this._index] === "{") {
|
|
var selectorText = this._text.substring(this._lastCheckpoint, this._index);
|
|
this._currentRuleset = { originalOffset: this._lastCheckpoint, selector: selectorText, declarations: [] };
|
|
|
|
this._lastCheckpoint = this._index + 1;
|
|
this._state = CssToken.Property;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handlePropertyColon(): boolean {
|
|
if (this._state === CssToken.Property && this._text[this._index] === ":") {
|
|
var propertyText = this._text.substring(this._lastCheckpoint, this._index);
|
|
this._currentDeclaration = { originalOffset: this._lastCheckpoint, property: propertyText, value: "" };
|
|
|
|
this._lastCheckpoint = this._index + 1;
|
|
this._state = CssToken.Value;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handleValueSemicolon(): boolean {
|
|
if (this._state === CssToken.Value && this._text[this._index] === ";") {
|
|
var valueText = this._text.substring(this._lastCheckpoint, this._index);
|
|
this._currentDeclaration.value = valueText;
|
|
this._currentDeclaration.endOffset = this._index + 1;
|
|
this._currentRuleset.declarations.push(this._currentDeclaration);
|
|
this._currentDeclaration = null;
|
|
|
|
this._lastCheckpoint = this._index + 1;
|
|
this._state = CssToken.Property;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handleSelectorCloseBracket(): boolean {
|
|
if (this._text[this._index] === "}") {
|
|
if (this._state === CssToken.Property) {
|
|
var incompleteDeclaration: ICssDeclaration = { originalOffset: this._lastCheckpoint, endOffset: this._index, property: this._text.substring(this._lastCheckpoint, this._index), value: null };
|
|
if (incompleteDeclaration.property.trim()) {
|
|
this._currentRuleset.declarations.push(incompleteDeclaration);
|
|
}
|
|
|
|
this._lastCheckpoint = this._index + 1;
|
|
this._state = CssToken.Selector;
|
|
|
|
if (this._currentMediaQuery) {
|
|
this._currentMediaQuery.endOffset = this._index;
|
|
this._currentMediaQuery.rulesets.push(this._currentRuleset);
|
|
} else {
|
|
this._currentRuleset.endOffset = this._index;
|
|
this._rootNodes.push(this._currentRuleset);
|
|
}
|
|
|
|
this._currentRuleset = null;
|
|
return true;
|
|
}
|
|
|
|
if (this._state === CssToken.Value) { // No closing semicolon, which is valid syntax
|
|
var valueText = this._text.substring(this._lastCheckpoint, this._index);
|
|
this._currentDeclaration.value = valueText;
|
|
this._currentDeclaration.isMissingSemicolon = true;
|
|
this._currentDeclaration.endOffset = this._index;
|
|
this._currentRuleset.declarations.push(this._currentDeclaration);
|
|
this._currentRuleset.endOffset = this._index;
|
|
this._currentDeclaration = null;
|
|
|
|
this._lastCheckpoint = this._index + 1;
|
|
this._state = CssToken.Selector;
|
|
|
|
if (this._currentMediaQuery) {
|
|
this._currentMediaQuery.rulesets.push(this._currentRuleset);
|
|
} else {
|
|
this._rootNodes.push(this._currentRuleset);
|
|
}
|
|
|
|
this._currentRuleset = null;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handleIncompleteBlocks(): void {
|
|
if (this._currentMediaQuery) {
|
|
this._lastCheckpoint = this._currentMediaQuery.originalOffset;
|
|
} else if (this._currentRuleset) {
|
|
this._lastCheckpoint = this._currentRuleset.originalOffset;
|
|
}
|
|
|
|
if (this._lastCheckpoint < this._text.length - 1) {
|
|
var textNode: ICssRuleset = { selector: this._text.substr(this._lastCheckpoint), originalOffset: this._lastCheckpoint, declarations: null, endOffset: this._index + 1 };
|
|
this._rootNodes.push(textNode);
|
|
}
|
|
}
|
|
|
|
private handleCommentCharacter(): boolean {
|
|
if (this._text.substr(this._index, 2) === "/*") {
|
|
var endOfCommentIndex = this._text.indexOf("*/", this._index);
|
|
if (endOfCommentIndex === -1) {
|
|
endOfCommentIndex = this._text.length;
|
|
}
|
|
|
|
if (this._state === CssToken.Property && !this._text.substring(this._lastCheckpoint, this._index).trim()) {
|
|
// this case is a disabled property
|
|
var colonIndex = this._text.indexOf(":", this._index);
|
|
if (colonIndex === -1 || colonIndex > endOfCommentIndex) {
|
|
Assert.fail("this is not a disabled property, hanlde this case later");
|
|
}
|
|
|
|
var propertyText = this._text.substring(this._index + 2, colonIndex);
|
|
|
|
var semiColonIndex = this._text.indexOf(";", this._index);
|
|
if (semiColonIndex === -1 || semiColonIndex >= endOfCommentIndex) {
|
|
var valueText = this._text.substring(colonIndex + 1, endOfCommentIndex);
|
|
} else {
|
|
var valueText = this._text.substring(colonIndex + 1, semiColonIndex);
|
|
}
|
|
|
|
this._currentDeclaration = { originalOffset: this._lastCheckpoint, property: propertyText, value: valueText };
|
|
|
|
this._currentDeclaration.isDisabled = true;
|
|
this._currentDeclaration.disabledFullText = this._text.substring(this._index, endOfCommentIndex + 2);
|
|
|
|
this._index = endOfCommentIndex + "*/".length - 1; // Adjust -1 because the loop will increment index by 1
|
|
this._currentDeclaration.endOffset = this._index + 1;
|
|
|
|
this._currentRuleset.declarations.push(this._currentDeclaration);
|
|
this._currentDeclaration = null;
|
|
|
|
this._lastCheckpoint = this._index + 1;
|
|
} else {
|
|
// This case is for normal comments
|
|
this._index = endOfCommentIndex + "*/".length - 1; // Adjust -1 because the loop will increment index by 1
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handleQuoteCharacter(): boolean {
|
|
if (this._currentQuotationMark) {
|
|
if (this._nextCharIsEscaped) {
|
|
this._nextCharIsEscaped = false;
|
|
} else if (this._text[this._index] === this._currentQuotationMark) {
|
|
this._currentQuotationMark = "";
|
|
} else if (this._text[this._index] === "\\") {
|
|
this._nextCharIsEscaped = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (this._text[this._index] === "\"" || this._text[this._index] === "'") {
|
|
this._currentQuotationMark = this._text[this._index];
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private handleLeadingWhitespace(): boolean {
|
|
if (this._lastCheckpoint === this._index && this._text[this._index].trim().length === 0) {
|
|
this._lastCheckpoint++;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
enum CssToken {
|
|
Selector,
|
|
Media,
|
|
Property,
|
|
Value
|
|
};
|
|
} |