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