parse.js
  1  //.CommonJS
  2  var CSSOM = {};
  3  ///CommonJS
  4  
  5  
  6  /**
  7   * @param {string} token
  8   */
  9  CSSOM.parse = function parse(token) {
 10  
 11  	var i = 0;
 12  
 13  	/**
 14  		"before-selector" or
 15  		"selector" or
 16  		"atRule" or
 17  		"atBlock" or
 18  		"conditionBlock" or
 19  		"before-name" or
 20  		"name" or
 21  		"before-value" or
 22  		"value"
 23  	*/
 24  	var state = "before-selector";
 25  
 26  	var index;
 27  	var buffer = "";
 28  	var valueParenthesisDepth = 0;
 29  
 30  	var SIGNIFICANT_WHITESPACE = {
 31  		"selector": true,
 32  		"value": true,
 33  		"value-parenthesis": true,
 34  		"atRule": true,
 35  		"importRule-begin": true,
 36  		"importRule": true,
 37  		"atBlock": true,
 38  		"conditionBlock": true,
 39  		'documentRule-begin': true
 40  	};
 41  
 42  	var styleSheet = new CSSOM.CSSStyleSheet();
 43  
 44  	// @type CSSStyleSheet|CSSMediaRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
 45  	var currentScope = styleSheet;
 46  
 47  	// @type CSSMediaRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
 48  	var parentRule;
 49  
 50  	var ancestorRules = [];
 51  	var hasAncestors = false;
 52  	var prevScope;
 53  
 54  	var name, priority="", styleRule, mediaRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule;
 55  
 56  	var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g;
 57  
 58  	var parseError = function(message) {
 59  		var lines = token.substring(0, i).split('\n');
 60  		var lineCount = lines.length;
 61  		var charCount = lines.pop().length + 1;
 62  		var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
 63  		error.line = lineCount;
 64  		/* jshint sub : true */
 65  		error['char'] = charCount;
 66  		error.styleSheet = styleSheet;
 67  		throw error;
 68  	};
 69  
 70  	for (var character; (character = token.charAt(i)); i++) {
 71  
 72  		switch (character) {
 73  
 74  		case " ":
 75  		case "\t":
 76  		case "\r":
 77  		case "\n":
 78  		case "\f":
 79  			if (SIGNIFICANT_WHITESPACE[state]) {
 80  				buffer += character;
 81  			}
 82  			break;
 83  
 84  		// String
 85  		case '"':
 86  			index = i + 1;
 87  			do {
 88  				index = token.indexOf('"', index) + 1;
 89  				if (!index) {
 90  					parseError('Unmatched "');
 91  				}
 92  			} while (token[index - 2] === '\\');
 93  			buffer += token.slice(i, index);
 94  			i = index - 1;
 95  			switch (state) {
 96  				case 'before-value':
 97  					state = 'value';
 98  					break;
 99  				case 'importRule-begin':
100  					state = 'importRule';
101  					break;
102  			}
103  			break;
104  
105  		case "'":
106  			index = i + 1;
107  			do {
108  				index = token.indexOf("'", index) + 1;
109  				if (!index) {
110  					parseError("Unmatched '");
111  				}
112  			} while (token[index - 2] === '\\');
113  			buffer += token.slice(i, index);
114  			i = index - 1;
115  			switch (state) {
116  				case 'before-value':
117  					state = 'value';
118  					break;
119  				case 'importRule-begin':
120  					state = 'importRule';
121  					break;
122  			}
123  			break;
124  
125  		// Comment
126  		case "/":
127  			if (token.charAt(i + 1) === "*") {
128  				i += 2;
129  				index = token.indexOf("*/", i);
130  				if (index === -1) {
131  					parseError("Missing */");
132  				} else {
133  					i = index + 1;
134  				}
135  			} else {
136  				buffer += character;
137  			}
138  			if (state === "importRule-begin") {
139  				buffer += " ";
140  				state = "importRule";
141  			}
142  			break;
143  
144  		// At-rule
145  		case "@":
146  			if (token.indexOf("@-moz-document", i) === i) {
147  				state = "documentRule-begin";
148  				documentRule = new CSSOM.CSSDocumentRule();
149  				documentRule.__starts = i;
150  				i += "-moz-document".length;
151  				buffer = "";
152  				break;
153  			} else if (token.indexOf("@media", i) === i) {
154  				state = "atBlock";
155  				mediaRule = new CSSOM.CSSMediaRule();
156  				mediaRule.__starts = i;
157  				i += "media".length;
158  				buffer = "";
159  				break;
160  			} else if (token.indexOf("@supports", i) === i) {
161  				state = "conditionBlock";
162  				supportsRule = new CSSOM.CSSSupportsRule();
163  				supportsRule.__starts = i;
164  				i += "supports".length;
165  				buffer = "";
166  				break;
167  			} else if (token.indexOf("@host", i) === i) {
168  				state = "hostRule-begin";
169  				i += "host".length;
170  				hostRule = new CSSOM.CSSHostRule();
171  				hostRule.__starts = i;
172  				buffer = "";
173  				break;
174  			} else if (token.indexOf("@import", i) === i) {
175  				state = "importRule-begin";
176  				i += "import".length;
177  				buffer += "@import";
178  				break;
179  			} else if (token.indexOf("@font-face", i) === i) {
180  				state = "fontFaceRule-begin";
181  				i += "font-face".length;
182  				fontFaceRule = new CSSOM.CSSFontFaceRule();
183  				fontFaceRule.__starts = i;
184  				buffer = "";
185  				break;
186  			} else {
187  				atKeyframesRegExp.lastIndex = i;
188  				var matchKeyframes = atKeyframesRegExp.exec(token);
189  				if (matchKeyframes && matchKeyframes.index === i) {
190  					state = "keyframesRule-begin";
191  					keyframesRule = new CSSOM.CSSKeyframesRule();
192  					keyframesRule.__starts = i;
193  					keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found
194  					i += matchKeyframes[0].length - 1;
195  					buffer = "";
196  					break;
197  				} else if (state === "selector") {
198  					state = "atRule";
199  				}
200  			}
201  			buffer += character;
202  			break;
203  
204  		case "{":
205  			if (state === "selector" || state === "atRule") {
206  				styleRule.selectorText = buffer.trim();
207  				styleRule.style.__starts = i;
208  				buffer = "";
209  				state = "before-name";
210  			} else if (state === "atBlock") {
211  				mediaRule.media.mediaText = buffer.trim();
212  
213  				if (parentRule) {
214  					ancestorRules.push(parentRule);
215  				}
216  
217  				currentScope = parentRule = mediaRule;
218  				mediaRule.parentStyleSheet = styleSheet;
219  				buffer = "";
220  				state = "before-selector";
221  			} else if (state === "conditionBlock") {
222  				supportsRule.conditionText = buffer.trim();
223  
224  				if (parentRule) {
225  					ancestorRules.push(parentRule);
226  				}
227  
228  				currentScope = parentRule = supportsRule;
229  				supportsRule.parentStyleSheet = styleSheet;
230  				buffer = "";
231  				state = "before-selector";
232  			} else if (state === "hostRule-begin") {
233  				if (parentRule) {
234  					ancestorRules.push(parentRule);
235  				}
236  
237  				currentScope = parentRule = hostRule;
238  				hostRule.parentStyleSheet = styleSheet;
239  				buffer = "";
240  				state = "before-selector";
241  			} else if (state === "fontFaceRule-begin") {
242  				if (parentRule) {
243  					fontFaceRule.parentRule = parentRule;
244  				}
245  				fontFaceRule.parentStyleSheet = styleSheet;
246  				styleRule = fontFaceRule;
247  				buffer = "";
248  				state = "before-name";
249  			} else if (state === "keyframesRule-begin") {
250  				keyframesRule.name = buffer.trim();
251  				if (parentRule) {
252  					ancestorRules.push(parentRule);
253  					keyframesRule.parentRule = parentRule;
254  				}
255  				keyframesRule.parentStyleSheet = styleSheet;
256  				currentScope = parentRule = keyframesRule;
257  				buffer = "";
258  				state = "keyframeRule-begin";
259  			} else if (state === "keyframeRule-begin") {
260  				styleRule = new CSSOM.CSSKeyframeRule();
261  				styleRule.keyText = buffer.trim();
262  				styleRule.__starts = i;
263  				buffer = "";
264  				state = "before-name";
265  			} else if (state === "documentRule-begin") {
266  				// FIXME: what if this '{' is in the url text of the match function?
267  				documentRule.matcher.matcherText = buffer.trim();
268  				if (parentRule) {
269  					ancestorRules.push(parentRule);
270  					documentRule.parentRule = parentRule;
271  				}
272  				currentScope = parentRule = documentRule;
273  				documentRule.parentStyleSheet = styleSheet;
274  				buffer = "";
275  				state = "before-selector";
276  			}
277  			break;
278  
279  		case ":":
280  			if (state === "name") {
281  				name = buffer.trim();
282  				buffer = "";
283  				state = "before-value";
284  			} else {
285  				buffer += character;
286  			}
287  			break;
288  
289  		case "(":
290  			if (state === 'value') {
291  				// ie css expression mode
292  				if (buffer.trim() === 'expression') {
293  					var info = (new CSSOM.CSSValueExpression(token, i)).parse();
294  
295  					if (info.error) {
296  						parseError(info.error);
297  					} else {
298  						buffer += info.expression;
299  						i = info.idx;
300  					}
301  				} else {
302  					state = 'value-parenthesis';
303  					//always ensure this is reset to 1 on transition
304  					//from value to value-parenthesis
305  					valueParenthesisDepth = 1;
306  					buffer += character;
307  				}
308  			} else if (state === 'value-parenthesis') {
309  				valueParenthesisDepth++;
310  				buffer += character;
311  			} else {
312  				buffer += character;
313  			}
314  			break;
315  
316  		case ")":
317  			if (state === 'value-parenthesis') {
318  				valueParenthesisDepth--;
319  				if (valueParenthesisDepth === 0) state = 'value';
320  			}
321  			buffer += character;
322  			break;
323  
324  		case "!":
325  			if (state === "value" && token.indexOf("!important", i) === i) {
326  				priority = "important";
327  				i += "important".length;
328  			} else {
329  				buffer += character;
330  			}
331  			break;
332  
333  		case ";":
334  			switch (state) {
335  				case "value":
336  					styleRule.style.setProperty(name, buffer.trim(), priority);
337  					priority = "";
338  					buffer = "";
339  					state = "before-name";
340  					break;
341  				case "atRule":
342  					buffer = "";
343  					state = "before-selector";
344  					break;
345  				case "importRule":
346  					importRule = new CSSOM.CSSImportRule();
347  					importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet;
348  					importRule.cssText = buffer + character;
349  					styleSheet.cssRules.push(importRule);
350  					buffer = "";
351  					state = "before-selector";
352  					break;
353  				default:
354  					buffer += character;
355  					break;
356  			}
357  			break;
358  
359  		case "}":
360  			switch (state) {
361  				case "value":
362  					styleRule.style.setProperty(name, buffer.trim(), priority);
363  					priority = "";
364  					/* falls through */
365  				case "before-name":
366  				case "name":
367  					styleRule.__ends = i + 1;
368  					if (parentRule) {
369  						styleRule.parentRule = parentRule;
370  					}
371  					styleRule.parentStyleSheet = styleSheet;
372  					currentScope.cssRules.push(styleRule);
373  					buffer = "";
374  					if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
375  						state = "keyframeRule-begin";
376  					} else {
377  						state = "before-selector";
378  					}
379  					break;
380  				case "keyframeRule-begin":
381  				case "before-selector":
382  				case "selector":
383  					// End of media/supports/document rule.
384  					if (!parentRule) {
385  						parseError("Unexpected }");
386  					}
387  
388  					// Handle rules nested in @media or @supports
389  					hasAncestors = ancestorRules.length > 0;
390  
391  					while (ancestorRules.length > 0) {
392  						parentRule = ancestorRules.pop();
393  
394  						if (
395  							parentRule.constructor.name === "CSSMediaRule"
396  							|| parentRule.constructor.name === "CSSSupportsRule"
397  						) {
398  							prevScope = currentScope;
399  							currentScope = parentRule;
400  							currentScope.cssRules.push(prevScope);
401  							break;
402  						}
403  
404  						if (ancestorRules.length === 0) {
405  							hasAncestors = false;
406  						}
407  					}
408  					
409  					if (!hasAncestors) {
410  						currentScope.__ends = i + 1;
411  						styleSheet.cssRules.push(currentScope);
412  						currentScope = styleSheet;
413  						parentRule = null;
414  					}
415  
416  					buffer = "";
417  					state = "before-selector";
418  					break;
419  			}
420  			break;
421  
422  		default:
423  			switch (state) {
424  				case "before-selector":
425  					state = "selector";
426  					styleRule = new CSSOM.CSSStyleRule();
427  					styleRule.__starts = i;
428  					break;
429  				case "before-name":
430  					state = "name";
431  					break;
432  				case "before-value":
433  					state = "value";
434  					break;
435  				case "importRule-begin":
436  					state = "importRule";
437  					break;
438  			}
439  			buffer += character;
440  			break;
441  		}
442  	}
443  
444  	return styleSheet;
445  };
446  
447  
448  //.CommonJS
449  exports.parse = CSSOM.parse;
450  // The following modules cannot be included sooner due to the mutual dependency with parse.js
451  CSSOM.CSSStyleSheet = require("./CSSStyleSheet").CSSStyleSheet;
452  CSSOM.CSSStyleRule = require("./CSSStyleRule").CSSStyleRule;
453  CSSOM.CSSImportRule = require("./CSSImportRule").CSSImportRule;
454  CSSOM.CSSMediaRule = require("./CSSMediaRule").CSSMediaRule;
455  CSSOM.CSSSupportsRule = require("./CSSSupportsRule").CSSSupportsRule;
456  CSSOM.CSSFontFaceRule = require("./CSSFontFaceRule").CSSFontFaceRule;
457  CSSOM.CSSHostRule = require("./CSSHostRule").CSSHostRule;
458  CSSOM.CSSStyleDeclaration = require('./CSSStyleDeclaration').CSSStyleDeclaration;
459  CSSOM.CSSKeyframeRule = require('./CSSKeyframeRule').CSSKeyframeRule;
460  CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
461  CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
462  CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
463  ///CommonJS