1+ //
2+ // Copyright (C) Microsoft. All rights reserved.
3+ //
4+
5+ /// <reference path="Interfaces.d.ts"/>
6+
7+ module Proxy {
8+ "use strict" ;
9+
10+ /**
11+ * Class that synchronously iterates through a CSS document and constructs a list of ICssMediaQuery and ICssRuleset objects.
12+ * The private members in this class store state used during the parse loop and that state is modified from
13+ * the various subroutines executed in the loop.
14+ */
15+ export class CssParser {
16+ // A list that contains any number of ICssMediaQuery and ICssRuleset objects
17+ private _rootNodes : any [ ] ;
18+ private _text : string ;
19+
20+ // Holds the index where text was last extracted from the source text
21+ private _lastCheckpoint : number ;
22+
23+ // Holds a string representing the current type of CSS construct that text is being extracted for
24+ private _state : CssToken ;
25+
26+ // Holds the index into the source text that the parser is currently inspecting at
27+ private _index : number ;
28+
29+ // Holds components of the CSS AST that are still being constructed
30+ private _currentRuleset : ICssRuleset ;
31+ private _currentDeclaration : ICssDeclaration ;
32+ private _currentMediaQuery : ICssMediaQuery ;
33+
34+ // Stores state about whether the loop has passed open quotes or open comments
35+ private _inComment : boolean ;
36+ private _currentQuotationMark : string ;
37+ private _nextCharIsEscaped : boolean ;
38+
39+ constructor ( text : string ) {
40+ this . _rootNodes = [ ] ;
41+ this . _text = text ;
42+ }
43+
44+ /** Returns an array containing ICssRuleset and ICssMediaQuery objects */
45+ public parseCss ( ) : any [ ] {
46+ // Statement control
47+ this . _inComment = false ;
48+ this . _currentQuotationMark = "" ;
49+ this . _nextCharIsEscaped = false ;
50+
51+ // Search maintenance state
52+ this . _lastCheckpoint = 0 ;
53+ this . _state = CssToken . Selector ;
54+
55+ // Storage for under-construction nodes
56+ this . _currentRuleset = null ;
57+ this . _currentDeclaration = null ;
58+ this . _currentMediaQuery = null ;
59+
60+ for ( this . _index = 0 ; this . _index < this . _text . length ; this . _index ++ ) {
61+ if ( this . handleQuoteCharacter ( ) ) {
62+ } else if ( this . handleCommentCharacter ( ) ) {
63+ } else if ( this . handleLeadingWhitespace ( ) ) {
64+ } else if ( this . handleMediaQueryStart ( ) ) {
65+ } else if ( this . handleMediaQueryOpenBracket ( ) ) {
66+ } else if ( this . handleMediaQueryCloseBracket ( ) ) {
67+ } else if ( this . handleSelectorOpenBracket ( ) ) {
68+ } else if ( this . handlePropertyColon ( ) ) {
69+ } else if ( this . handleValueSemicolon ( ) ) {
70+ } else if ( this . handleSelectorCloseBracket ( ) ) {
71+ }
72+ }
73+
74+ // Put any text that wasn't valid CSS into it's own node at the end of the file
75+ this . handleIncompleteBlocks ( ) ;
76+
77+ return this . _rootNodes ;
78+ }
79+
80+ private handleMediaQueryStart ( ) : boolean {
81+ if ( this . _state === CssToken . Selector && ! this . _currentMediaQuery &&
82+ this . _lastCheckpoint >= this . _index &&
83+ this . _text [ this . _index ] === "@" && this . _text . substr ( this . _index , 7 ) . toLowerCase ( ) === "@media " ) {
84+ this . _state = CssToken . Media ;
85+ return true ;
86+ }
87+
88+ return false ;
89+ }
90+
91+ private handleMediaQueryOpenBracket ( ) : boolean {
92+ if ( this . _state === CssToken . Media && this . _text [ this . _index ] === "{" ) {
93+ var mediaText = this . _text . substring ( this . _lastCheckpoint , this . _index ) ;
94+ this . _currentMediaQuery = { originalOffset : this . _lastCheckpoint , query : mediaText , rulesets : [ ] } ;
95+
96+ this . _lastCheckpoint = this . _index + 1 ;
97+ this . _state = CssToken . Selector ;
98+
99+ return true ;
100+ }
101+
102+ return false ;
103+ }
104+
105+ private handleMediaQueryCloseBracket ( ) : boolean {
106+ if ( this . _state === CssToken . Selector && this . _text [ this . _index ] === "}" && this . _currentMediaQuery ) {
107+ this . _lastCheckpoint = this . _index + 1 ;
108+ this . _state = CssToken . Selector ;
109+ this . _currentMediaQuery . endOffset = this . _index + 1 ;
110+ this . _rootNodes . push ( this . _currentMediaQuery ) ;
111+ this . _currentMediaQuery = null ;
112+
113+ return true ;
114+ }
115+
116+ return false ;
117+ }
118+
119+ private handleSelectorOpenBracket ( ) : boolean {
120+ if ( this . _state === CssToken . Selector && this . _text [ this . _index ] === "{" ) {
121+ var selectorText = this . _text . substring ( this . _lastCheckpoint , this . _index ) ;
122+ this . _currentRuleset = { originalOffset : this . _lastCheckpoint , selector : selectorText , declarations : [ ] } ;
123+
124+ this . _lastCheckpoint = this . _index + 1 ;
125+ this . _state = CssToken . Property ;
126+
127+ return true ;
128+ }
129+
130+ return false ;
131+ }
132+
133+ private handlePropertyColon ( ) : boolean {
134+ if ( this . _state === CssToken . Property && this . _text [ this . _index ] === ":" ) {
135+ var propertyText = this . _text . substring ( this . _lastCheckpoint , this . _index ) ;
136+ this . _currentDeclaration = { originalOffset : this . _lastCheckpoint , property : propertyText , value : "" } ;
137+
138+ this . _lastCheckpoint = this . _index + 1 ;
139+ this . _state = CssToken . Value ;
140+
141+ return true ;
142+ }
143+
144+ return false ;
145+ }
146+
147+ private handleValueSemicolon ( ) : boolean {
148+ if ( this . _state === CssToken . Value && this . _text [ this . _index ] === ";" ) {
149+ var valueText = this . _text . substring ( this . _lastCheckpoint , this . _index ) ;
150+ this . _currentDeclaration . value = valueText ;
151+ this . _currentDeclaration . endOffset = this . _index + 1 ;
152+ this . _currentRuleset . declarations . push ( this . _currentDeclaration ) ;
153+ this . _currentDeclaration = null ;
154+
155+ this . _lastCheckpoint = this . _index + 1 ;
156+ this . _state = CssToken . Property ;
157+
158+ return true ;
159+ }
160+
161+ return false ;
162+ }
163+
164+ private handleSelectorCloseBracket ( ) : boolean {
165+ if ( this . _text [ this . _index ] === "}" ) {
166+ if ( this . _state === CssToken . Property ) {
167+ var incompleteDeclaration : ICssDeclaration = { originalOffset : this . _lastCheckpoint , endOffset : this . _index , property : this . _text . substring ( this . _lastCheckpoint , this . _index ) , value : null } ;
168+ if ( incompleteDeclaration . property . trim ( ) ) {
169+ this . _currentRuleset . declarations . push ( incompleteDeclaration ) ;
170+ }
171+
172+ this . _lastCheckpoint = this . _index + 1 ;
173+ this . _state = CssToken . Selector ;
174+
175+ if ( this . _currentMediaQuery ) {
176+ this . _currentMediaQuery . endOffset = this . _index ;
177+ this . _currentMediaQuery . rulesets . push ( this . _currentRuleset ) ;
178+ } else {
179+ this . _currentRuleset . endOffset = this . _index ;
180+ this . _rootNodes . push ( this . _currentRuleset ) ;
181+ }
182+
183+ this . _currentRuleset = null ;
184+ return true ;
185+ }
186+
187+ if ( this . _state === CssToken . Value ) { // No closing semicolon, which is valid syntax
188+ var valueText = this . _text . substring ( this . _lastCheckpoint , this . _index ) ;
189+ this . _currentDeclaration . value = valueText ;
190+ this . _currentDeclaration . isMissingSemicolon = true ;
191+ this . _currentDeclaration . endOffset = this . _index ;
192+ this . _currentRuleset . declarations . push ( this . _currentDeclaration ) ;
193+ this . _currentRuleset . endOffset = this . _index ;
194+ this . _currentDeclaration = null ;
195+
196+ this . _lastCheckpoint = this . _index + 1 ;
197+ this . _state = CssToken . Selector ;
198+
199+ if ( this . _currentMediaQuery ) {
200+ this . _currentMediaQuery . rulesets . push ( this . _currentRuleset ) ;
201+ } else {
202+ this . _rootNodes . push ( this . _currentRuleset ) ;
203+ }
204+
205+ this . _currentRuleset = null ;
206+ return true ;
207+ }
208+ }
209+
210+ return false ;
211+ }
212+
213+ private handleIncompleteBlocks ( ) : void {
214+ if ( this . _currentMediaQuery ) {
215+ this . _lastCheckpoint = this . _currentMediaQuery . originalOffset ;
216+ } else if ( this . _currentRuleset ) {
217+ this . _lastCheckpoint = this . _currentRuleset . originalOffset ;
218+ }
219+
220+ if ( this . _lastCheckpoint < this . _text . length - 1 ) {
221+ var textNode : ICssRuleset = { selector : this . _text . substr ( this . _lastCheckpoint ) , originalOffset : this . _lastCheckpoint , declarations : null , endOffset : this . _index + 1 } ;
222+ this . _rootNodes . push ( textNode ) ;
223+ }
224+ }
225+
226+ private handleCommentCharacter ( ) : boolean {
227+ if ( this . _text . substr ( this . _index , 2 ) === "/*" ) {
228+ var endOfCommentIndex = this . _text . indexOf ( "*/" , this . _index ) ;
229+ if ( endOfCommentIndex === - 1 ) {
230+ endOfCommentIndex = this . _text . length ;
231+ }
232+
233+ if ( this . _state === CssToken . Property && ! this . _text . substring ( this . _lastCheckpoint , this . _index ) . trim ( ) ) {
234+ // this case is a disabled property
235+ var colonIndex = this . _text . indexOf ( ":" , this . _index ) ;
236+ if ( colonIndex === - 1 || colonIndex > endOfCommentIndex ) {
237+ Assert . fail ( "this is not a disabled property, hanlde this case later" ) ;
238+ }
239+
240+ var propertyText = this . _text . substring ( this . _index + 2 , colonIndex ) ;
241+
242+ var semiColonIndex = this . _text . indexOf ( ";" , this . _index ) ;
243+ if ( semiColonIndex === - 1 || semiColonIndex >= endOfCommentIndex ) {
244+ var valueText = this . _text . substring ( colonIndex + 1 , endOfCommentIndex ) ;
245+ } else {
246+ var valueText = this . _text . substring ( colonIndex + 1 , semiColonIndex ) ;
247+ }
248+
249+ this . _currentDeclaration = { originalOffset : this . _lastCheckpoint , property : propertyText , value : valueText } ;
250+
251+ this . _currentDeclaration . isDisabled = true ;
252+ this . _currentDeclaration . disabledFullText = this . _text . substring ( this . _index , endOfCommentIndex + 2 ) ;
253+
254+ this . _index = endOfCommentIndex + "*/" . length - 1 ; // Adjust -1 because the loop will increment index by 1
255+ this . _currentDeclaration . endOffset = this . _index + 1 ;
256+
257+ this . _currentRuleset . declarations . push ( this . _currentDeclaration ) ;
258+ this . _currentDeclaration = null ;
259+
260+ this . _lastCheckpoint = this . _index + 1 ;
261+ } else {
262+ // This case is for normal comments
263+ this . _index = endOfCommentIndex + "*/" . length - 1 ; // Adjust -1 because the loop will increment index by 1
264+ }
265+
266+ return true ;
267+ }
268+
269+ return false ;
270+ }
271+
272+ private handleQuoteCharacter ( ) : boolean {
273+ if ( this . _currentQuotationMark ) {
274+ if ( this . _nextCharIsEscaped ) {
275+ this . _nextCharIsEscaped = false ;
276+ } else if ( this . _text [ this . _index ] === this . _currentQuotationMark ) {
277+ this . _currentQuotationMark = "" ;
278+ } else if ( this . _text [ this . _index ] === "\\" ) {
279+ this . _nextCharIsEscaped = true ;
280+ }
281+
282+ return true ;
283+ }
284+
285+ if ( this . _text [ this . _index ] === "\"" || this . _text [ this . _index ] === "'" ) {
286+ this . _currentQuotationMark = this . _text [ this . _index ] ;
287+ return true ;
288+ }
289+
290+ return false ;
291+ }
292+
293+ private handleLeadingWhitespace ( ) : boolean {
294+ if ( this . _lastCheckpoint === this . _index && this . _text [ this . _index ] . trim ( ) . length === 0 ) {
295+ this . _lastCheckpoint ++ ;
296+ return true ;
297+ }
298+
299+ return false ;
300+ }
301+ }
302+
303+ enum CssToken {
304+ Selector ,
305+ Media ,
306+ Property ,
307+ Value
308+ } ;
309+ }
0 commit comments