UNPKG

cssom-papandreou

Version:

CSS Object Model implementation and CSS parser

1,692 lines (1,460 loc) 39.5 kB
(function(root) { 'use strict'; var CSSOM = root.CSSOM = {}; /** * @constructor * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration */ CSSOM.CSSStyleDeclaration = function CSSStyleDeclaration(){ this.length = 0; this.parentRule = null; // NON-STANDARD this._importants = {}; }; CSSOM.CSSStyleDeclaration.prototype = { constructor: CSSOM.CSSStyleDeclaration, /** * * @param {string} name * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-getPropertyValue * @return {string} the value of the property if it has been explicitly set for this declaration block. * Returns the empty string if the property has not been set. */ getPropertyValue: function(name) { return this[name] || ""; }, /** * * @param {string} name * @param {string} value * @param {string} [priority=null] "important" or null * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty */ setProperty: function(name, value, priority) { if (this[name]) { // Property already exist. Overwrite it. var index = Array.prototype.indexOf.call(this, name); if (index < 0) { this[this.length] = name; this.length++; } } else { // New property. this[this.length] = name; this.length++; } this[name] = value; this._importants[name] = priority; }, /** * * @param {string} name * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-removeProperty * @return {string} the value of the property if it has been explicitly set for this declaration block. * Returns the empty string if the property has not been set or the property name does not correspond to a known CSS property. */ removeProperty: function(name) { if (!(name in this)) { return ""; } var index = Array.prototype.indexOf.call(this, name); if (index < 0) { return ""; } var prevValue = this[name]; this[name] = ""; // That's what WebKit and Opera do Array.prototype.splice.call(this, index, 1); // That's what Firefox does //this[index] = "" return prevValue; }, getPropertyCSSValue: function() { //FIXME }, /** * * @param {String} name */ getPropertyPriority: function(name) { return this._importants[name] || ""; }, /** * element.style.overflow = "auto" * element.style.getPropertyShorthand("overflow-x") * -> "overflow" */ getPropertyShorthand: function() { //FIXME }, isPropertyImplicit: function() { //FIXME }, // Doesn't work in IE < 9 get cssText(){ var properties = []; for (var i=0, length=this.length; i < length; ++i) { var name = this[i]; var value = this.getPropertyValue(name); var priority = this.getPropertyPriority(name); if (priority) { priority = " !" + priority; } properties[i] = name + ": " + value + priority + ";"; } return properties.join(" "); }, set cssText(text){ var i, name; for (i = this.length; i--;) { name = this[i]; this[name] = ""; } Array.prototype.splice.call(this, 0, this.length); this._importants = {}; var dummyRule = CSSOM.parse('#bogus{' + text + '}').cssRules[0].style; var length = dummyRule.length; for (i = 0; i < length; ++i) { name = dummyRule[i]; this.setProperty(dummyRule[i], dummyRule.getPropertyValue(name), dummyRule.getPropertyPriority(name)); } } }; /** * @constructor * @see http://dev.w3.org/csswg/cssom/#the-cssrule-interface * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSRule */ CSSOM.CSSRule = function CSSRule() { this.parentRule = null; this.parentStyleSheet = null; }; CSSOM.CSSRule.STYLE_RULE = 1; CSSOM.CSSRule.IMPORT_RULE = 3; CSSOM.CSSRule.MEDIA_RULE = 4; CSSOM.CSSRule.FONT_FACE_RULE = 5; CSSOM.CSSRule.PAGE_RULE = 6; CSSOM.CSSRule.WEBKIT_KEYFRAMES_RULE = 8; CSSOM.CSSRule.WEBKIT_KEYFRAME_RULE = 9; // Obsolete in CSSOM http://dev.w3.org/csswg/cssom/ //CSSOM.CSSRule.UNKNOWN_RULE = 0; //CSSOM.CSSRule.CHARSET_RULE = 2; // Never implemented //CSSOM.CSSRule.VARIABLES_RULE = 7; CSSOM.CSSRule.prototype = { constructor: CSSOM.CSSRule //FIXME }; /** * @constructor * @see http://dev.w3.org/csswg/cssom/#cssstylerule * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleRule */ CSSOM.CSSStyleRule = function CSSStyleRule() { CSSOM.CSSRule.call(this); this.selectorText = ""; this.style = new CSSOM.CSSStyleDeclaration(); this.style.parentRule = this; }; CSSOM.CSSStyleRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSStyleRule.prototype.constructor = CSSOM.CSSStyleRule; CSSOM.CSSStyleRule.prototype.type = 1; Object.defineProperty(CSSOM.CSSStyleRule.prototype, "cssText", { get: function() { var text; if (this.selectorText) { text = this.selectorText + " {" + this.style.cssText + "}"; } else { text = ""; } return text; }, set: function(cssText) { var rule = CSSOM.CSSStyleRule.parse(cssText); this.style = rule.style; this.selectorText = rule.selectorText; } }); /** * NON-STANDARD * lightweight version of parse.js. * @param {string} ruleText * @return CSSStyleRule */ CSSOM.CSSStyleRule.parse = function(ruleText) { var i = 0; var state = "selector"; var index; var j = i; var buffer = ""; var SIGNIFICANT_WHITESPACE = { "selector": true, "value": true }; var styleRule = new CSSOM.CSSStyleRule(); var name, priority=""; for (var character; (character = ruleText.charAt(i)); i++) { switch (character) { case " ": case "\t": case "\r": case "\n": case "\f": if (SIGNIFICANT_WHITESPACE[state]) { // Squash 2 or more white-spaces in the row into 1 switch (ruleText.charAt(i - 1)) { case " ": case "\t": case "\r": case "\n": case "\f": break; default: buffer += " "; break; } } break; // String case '"': j = i + 1; index = ruleText.indexOf('"', j) + 1; if (!index) { throw '" is missing'; } buffer += ruleText.slice(i, index); i = index - 1; break; case "'": j = i + 1; index = ruleText.indexOf("'", j) + 1; if (!index) { throw "' is missing"; } buffer += ruleText.slice(i, index); i = index - 1; break; // Comment case "/": if (ruleText.charAt(i + 1) === "*") { i += 2; index = ruleText.indexOf("*/", i); if (index === -1) { throw new SyntaxError("Missing */"); } else { i = index + 1; } } else { buffer += character; } break; case "{": if (state === "selector") { styleRule.selectorText = buffer.trim(); buffer = ""; state = "name"; } break; case ":": if (state === "name") { name = buffer.trim(); buffer = ""; state = "value"; } else { buffer += character; } break; case "!": if (state === "value" && ruleText.indexOf("!important", i) === i) { priority = "important"; i += "important".length; } else { buffer += character; } break; case ";": if (state === "value") { styleRule.style.setProperty(name, buffer.trim(), priority); priority = ""; buffer = ""; state = "name"; } else { buffer += character; } break; case "}": if (state === "value") { styleRule.style.setProperty(name, buffer.trim(), priority); priority = ""; buffer = ""; } else if (state === "name") { break; } else { buffer += character; } state = "selector"; break; default: buffer += character; break; } } return styleRule; }; /** * @constructor * @see http://dev.w3.org/csswg/cssom/#the-medialist-interface */ CSSOM.MediaList = function MediaList(){ this.length = 0; }; CSSOM.MediaList.prototype = { constructor: CSSOM.MediaList, /** * @return {string} */ get mediaText() { return Array.prototype.join.call(this, ", "); }, /** * @param {string} value */ set mediaText(value) { var values = value.split(","); var length = this.length = values.length; for (var i=0; i<length; i++) { this[i] = values[i].trim(); } }, /** * @param {string} medium */ appendMedium: function(medium) { if (Array.prototype.indexOf.call(this, medium) === -1) { this[this.length] = medium; this.length++; } }, /** * @param {string} medium */ deleteMedium: function(medium) { var index = Array.prototype.indexOf.call(this, medium); if (index !== -1) { Array.prototype.splice.call(this, index, 1); } } }; /** * @constructor * @see http://dev.w3.org/csswg/cssom/#cssmediarule * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSMediaRule */ CSSOM.CSSMediaRule = function CSSMediaRule() { CSSOM.CSSRule.call(this); this.media = new CSSOM.MediaList(); this.cssRules = []; }; CSSOM.CSSMediaRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSMediaRule.prototype.constructor = CSSOM.CSSMediaRule; CSSOM.CSSMediaRule.prototype.type = 4; //FIXME //CSSOM.CSSMediaRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSMediaRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; // http://opensource.apple.com/source/WebCore/WebCore-658.28/css/CSSMediaRule.cpp Object.defineProperty(CSSOM.CSSMediaRule.prototype, "cssText", { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@media " + this.media.mediaText + " {" + cssTexts.join("") + "}"; } }); /** * @constructor * @see http://dev.w3.org/csswg/cssom/#cssimportrule * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSImportRule */ CSSOM.CSSImportRule = function CSSImportRule() { CSSOM.CSSRule.call(this); this.href = ""; this.media = new CSSOM.MediaList(); this.styleSheet = new CSSOM.CSSStyleSheet(); }; CSSOM.CSSImportRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSImportRule.prototype.constructor = CSSOM.CSSImportRule; CSSOM.CSSImportRule.prototype.type = 3; Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", { get: function() { var mediaText = this.media.mediaText; return "@import url(" + this.href + ")" + (mediaText ? " " + mediaText : "") + ";"; }, set: function(cssText) { var i = 0; /** * @import url(partial.css) screen, handheld; * || | * after-import media * | * url */ var state = ''; var buffer = ''; var index; for (var character; (character = cssText.charAt(i)); i++) { switch (character) { case ' ': case '\t': case '\r': case '\n': case '\f': if (state === 'after-import') { state = 'url'; } else { buffer += character; } break; case '@': if (!state && cssText.indexOf('@import', i) === i) { state = 'after-import'; i += 'import'.length; buffer = ''; } break; case 'u': if (state === 'url' && cssText.indexOf('url(', i) === i) { index = cssText.indexOf(')', i + 1); if (index === -1) { throw i + ': ")" not found'; } i += 'url('.length; var url = cssText.slice(i, index); if (url[0] === url[url.length - 1]) { if (url[0] === '"' || url[0] === "'") { url = url.slice(1, -1); } } this.href = url; i = index; state = 'media'; } break; case '"': if (state === 'url') { index = cssText.indexOf('"', i + 1); if (!index) { throw i + ": '\"' not found"; } this.href = cssText.slice(i + 1, index); i = index; state = 'media'; } break; case "'": if (state === 'url') { index = cssText.indexOf("'", i + 1); if (!index) { throw i + ': "\'" not found'; } this.href = cssText.slice(i + 1, index); i = index; state = 'media'; } break; case ';': if (state === 'media') { if (buffer) { this.media.mediaText = buffer.trim(); } } break; default: if (state === 'media') { buffer += character; } break; } } } }); /** * @constructor * @see http://dev.w3.org/csswg/cssom/#css-font-face-rule */ CSSOM.CSSFontFaceRule = function CSSFontFaceRule() { CSSOM.CSSRule.call(this); this.style = new CSSOM.CSSStyleDeclaration(); this.style.parentRule = this; }; CSSOM.CSSFontFaceRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSFontFaceRule.prototype.constructor = CSSOM.CSSFontFaceRule; CSSOM.CSSFontFaceRule.prototype.type = 5; //FIXME //CSSOM.CSSFontFaceRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSFontFaceRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSFontFaceRule.cpp Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", { get: function() { return "@font-face {" + this.style.cssText + "}"; } }); /** * @constructor * @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface */ CSSOM.StyleSheet = function StyleSheet() { this.parentStyleSheet = null; }; /** * @constructor * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet */ CSSOM.CSSStyleSheet = function CSSStyleSheet() { CSSOM.StyleSheet.call(this); this.cssRules = []; }; CSSOM.CSSStyleSheet.prototype = new CSSOM.StyleSheet(); CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet; /** * Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade. * * sheet = new Sheet("body {margin: 0}") * sheet.toString() * -> "body{margin:0;}" * sheet.insertRule("img {border: none}", 0) * -> 0 * sheet.toString() * -> "img{border:none;}body{margin:0;}" * * @param {string} rule * @param {number} index * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-insertRule * @return {number} The index within the style sheet's rule collection of the newly inserted rule. */ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) { if (index < 0 || index > this.cssRules.length) { throw new RangeError("INDEX_SIZE_ERR"); } var cssRule = CSSOM.parse(rule).cssRules[0]; cssRule.parentStyleSheet = this; this.cssRules.splice(index, 0, cssRule); return index; }; /** * Used to delete a rule from the style sheet. * * sheet = new Sheet("img{border:none} body{margin:0}") * sheet.toString() * -> "img{border:none;}body{margin:0;}" * sheet.deleteRule(0) * sheet.toString() * -> "body{margin:0;}" * * @param {number} index within the style sheet's rule list of the rule to remove. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-deleteRule */ CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) { if (index < 0 || index >= this.cssRules.length) { throw new RangeError("INDEX_SIZE_ERR"); } this.cssRules.splice(index, 1); }; /** * NON-STANDARD * @return {string} serialize stylesheet */ CSSOM.CSSStyleSheet.prototype.toString = function() { var result = ""; var rules = this.cssRules; function serializeRuleBodyWithoutIsContinuation(cssRule) { var isContinuation = cssRule.style.getPropertyValue('is-continuation'); result; if (isContinuation) { rules[i].style.removeProperty('is-continuation'); } result = cssRule.style.cssText; if (isContinuation) { rules[i].style.setProperty('is-continuation', isContinuation); } return result; } for (var i=0; i<rules.length; i++) { if (rules[i].type === CSSOM.CSSRule.STYLE_RULE) { // STYLE_RULE result += rules[i].selectorText + " {"; if (rules[i].style) { result += serializeRuleBodyWithoutIsContinuation(rules[i]); } while (i + 1 < rules.length && rules[i + 1].type === CSSOM.CSSRule.STYLE_RULE && rules[i + 1].selectorText === rules[i].selectorText && rules[i + 1].style && rules[i + 1].style.getPropertyValue('is-continuation') === 'yes') { i += 1; result += " " + serializeRuleBodyWithoutIsContinuation(rules[i]); } result += "}\n"; } else if (rules[i].type === CSSOM.CSSRule.FONT_FACE_RULE) { result += "@font-face {"; if (rules[i].style) { result += serializeRuleBodyWithoutIsContinuation(rules[i]); } var fontFamily = rules[i].style.getPropertyValue('font-family'); while (fontFamily && i + 1 < rules.length && rules[i + 1].type === CSSOM.CSSRule.FONT_FACE_RULE && rules[i + 1].style && rules[i + 1].style.getPropertyValue('is-continuation') === 'yes' && rules[i + 1].style.getPropertyValue('font-family') === fontFamily) { i += 1; if (rules[i].style) { rules[i].style.removeProperty('font-family'); result += " " + serializeRuleBodyWithoutIsContinuation(rules[i]); rules[i].style.setProperty('font-family', fontFamily); } } result += "}\n"; } else { result += rules[i].cssText + "\n"; } } return result; }; /** * @constructor * @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframesRule */ CSSOM.CSSKeyframesRule = function CSSKeyframesRule() { CSSOM.CSSRule.call(this); this.name = ''; this.cssRules = []; }; CSSOM.CSSKeyframesRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSKeyframesRule.prototype.constructor = CSSOM.CSSKeyframesRule; CSSOM.CSSKeyframesRule.prototype.type = 8; //FIXME //CSSOM.CSSKeyframesRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSKeyframesRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframesRule.cpp Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "cssText", { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(" " + this.cssRules[i].cssText); } return "@" + (this._vendorPrefix || '') + "keyframes " + this.name + " { \n" + cssTexts.join("\n") + "\n}"; } }); /** * @constructor * @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframeRule */ CSSOM.CSSKeyframeRule = function CSSKeyframeRule() { CSSOM.CSSRule.call(this); this.keyText = ''; this.style = new CSSOM.CSSStyleDeclaration(); this.style.parentRule = this; }; CSSOM.CSSKeyframeRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSKeyframeRule.prototype.constructor = CSSOM.CSSKeyframeRule; CSSOM.CSSKeyframeRule.prototype.type = 9; //FIXME //CSSOM.CSSKeyframeRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSKeyframeRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframeRule.cpp Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "cssText", { get: function() { return this.keyText + " {" + this.style.cssText + "} "; } }); /** * @constructor * @see https://developer.mozilla.org/en/CSS/@-moz-document */ CSSOM.MatcherList = function MatcherList(){ this.length = 0; }; CSSOM.MatcherList.prototype = { constructor: CSSOM.MatcherList, /** * @return {string} */ get matcherText() { return Array.prototype.join.call(this, ", "); }, /** * @param {string} value */ set matcherText(value) { // just a temporary solution, actually it may be wrong by just split the value with ',', because a url can include ','. var values = value.split(","); var length = this.length = values.length; for (var i=0; i<length; i++) { this[i] = values[i].trim(); } }, /** * @param {string} matcher */ appendMatcher: function(matcher) { if (Array.prototype.indexOf.call(this, matcher) === -1) { this[this.length] = matcher; this.length++; } }, /** * @param {string} matcher */ deleteMatcher: function(matcher) { var index = Array.prototype.indexOf.call(this, matcher); if (index !== -1) { Array.prototype.splice.call(this, index, 1); } } }; /** * @constructor * @see https://developer.mozilla.org/en/CSS/@-moz-document */ CSSOM.CSSDocumentRule = function CSSDocumentRule() { CSSOM.CSSRule.call(this); this.matcher = new CSSOM.MatcherList(); this.cssRules = []; }; CSSOM.CSSDocumentRule.prototype = new CSSOM.CSSRule(); CSSOM.CSSDocumentRule.prototype.constructor = CSSOM.CSSDocumentRule; CSSOM.CSSDocumentRule.prototype.type = 10; //FIXME //CSSOM.CSSDocumentRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule; //CSSOM.CSSDocumentRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule; Object.defineProperty(CSSOM.CSSDocumentRule.prototype, "cssText", { get: function() { var cssTexts = []; for (var i=0, length=this.cssRules.length; i < length; i++) { cssTexts.push(this.cssRules[i].cssText); } return "@-moz-document " + this.matcher.matcherText + " {" + cssTexts.join("") + "}"; } }); /** * @constructor * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue * * TODO: add if needed */ CSSOM.CSSValue = function CSSValue() { }; CSSOM.CSSValue.prototype = { constructor: CSSOM.CSSValue, // @see: http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue set cssText(text) { var name = this._getConstructorName(); throw new Error('DOMException: property "cssText" of "' + name + '" is readonly and can not be replaced with "' + text + '"!'); }, get cssText() { var name = this._getConstructorName(); throw new Error('getter "cssText" of "' + name + '" is not implemented!'); }, _getConstructorName: function() { var s = this.constructor.toString(), c = s.match(/function\s([^\(]+)/), name = c[1]; return name; } }; /** * @constructor * @see http://msdn.microsoft.com/en-us/library/ms537634(v=vs.85).aspx * */ CSSOM.CSSValueExpression = function CSSValueExpression(token, idx) { this._token = token; this._idx = idx; }; CSSOM.CSSValueExpression.prototype = new CSSOM.CSSValue(); CSSOM.CSSValueExpression.prototype.constructor = CSSOM.CSSValueExpression; /** * parse css expression() value * * @return {Object} * - error: * or * - idx: * - expression: * * Example: * * .selector { * zoom: expression(documentElement.clientWidth > 1000 ? '1000px' : 'auto'); * } */ CSSOM.CSSValueExpression.prototype.parse = function() { var token = this._token, idx = this._idx; var character = '', expression = '', error = '', info, paren = []; for (; ; ++idx) { character = token.charAt(idx); // end of token if (character === '') { error = 'css expression error: unfinished expression!'; break; } switch(character) { case '(': paren.push(character); expression += character; break; case ')': paren.pop(character); expression += character; break; case '/': if ((info = this._parseJSComment(token, idx))) { // comment? if (info.error) { error = 'css expression error: unfinished comment in expression!'; } else { idx = info.idx; // ignore the comment } } else if ((info = this._parseJSRexExp(token, idx))) { // regexp idx = info.idx; expression += info.text; } else { // other expression += character; } break; case "'": case '"': info = this._parseJSString(token, idx, character); if (info) { // string idx = info.idx; expression += info.text; } else { expression += character; } break; default: expression += character; break; } if (error) { break; } // end of expression if (paren.length === 0) { break; } } var ret; if (error) { ret = { error: error }; } else { ret = { idx: idx, expression: expression }; } return ret; }; /** * * @return {Object|false} * - idx: * - text: * or * - error: * or * false * */ CSSOM.CSSValueExpression.prototype._parseJSComment = function(token, idx) { var nextChar = token.charAt(idx + 1), text; if (nextChar === '/' || nextChar === '*') { var startIdx = idx, endIdx, commentEndChar; if (nextChar === '/') { // line comment commentEndChar = '\n'; } else if (nextChar === '*') { // block comment commentEndChar = '*/'; } endIdx = token.indexOf(commentEndChar, startIdx + 1 + 1); if (endIdx !== -1) { endIdx = endIdx + commentEndChar.length - 1; text = token.substring(idx, endIdx + 1); return { idx: endIdx, text: text }; } else { var error = 'css expression error: unfinished comment in expression!'; return { error: error }; } } else { return false; } }; /** * * @return {Object|false} * - idx: * - text: * or * false * */ CSSOM.CSSValueExpression.prototype._parseJSString = function(token, idx, sep) { var endIdx = this._findMatchedIdx(token, idx, sep), text; if (endIdx === -1) { return false; } else { text = token.substring(idx, endIdx + sep.length); return { idx: endIdx, text: text }; } }; /** * parse regexp in css expression * * @return {Object|false} * - idx: * - regExp: * or * false */ /* all legal RegExp /a/ (/a/) [/a/] [12, /a/] !/a/ +/a/ -/a/ * /a/ / /a/ %/a/ ===/a/ !==/a/ ==/a/ !=/a/ >/a/ >=/a/ </a/ <=/a/ &/a/ |/a/ ^/a/ ~/a/ <</a/ >>/a/ >>>/a/ &&/a/ ||/a/ ?/a/ =/a/ ,/a/ delete /a/ in /a/ instanceof /a/ new /a/ typeof /a/ void /a/ */ CSSOM.CSSValueExpression.prototype._parseJSRexExp = function(token, idx) { var before = token.substring(0, idx).replace(/\s+$/, ""), legalRegx = [ /^$/, /\($/, /\[$/, /\!$/, /\+$/, /\-$/, /\*$/, /\/\s+/, /\%$/, /\=$/, /\>$/, /<$/, /\&$/, /\|$/, /\^$/, /\~$/, /\?$/, /\,$/, /delete$/, /in$/, /instanceof$/, /new$/, /typeof$/, /void$/ ]; var isLegal = legalRegx.some(function(reg) { return reg.test(before); }); if (!isLegal) { return false; } else { var sep = '/'; // same logic as string return this._parseJSString(token, idx, sep); } }; /** * * find next sep(same line) index in `token` * * @return {Number} * */ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) { var startIdx = idx, endIdx; var NOT_FOUND = -1; while(true) { endIdx = token.indexOf(sep, startIdx + 1); if (endIdx === -1) { // not found endIdx = NOT_FOUND; break; } else { var text = token.substring(idx + 1, endIdx), matched = text.match(/\\+$/); if (!matched || matched[0] % 2 === 0) { // not escaped break; } else { startIdx = endIdx; } } } // boundary must be in the same line(js sting or regexp) var nextNewLineIdx = token.indexOf('\n', idx + 1); if (nextNewLineIdx < endIdx) { endIdx = NOT_FOUND; } return endIdx; }; /** * @param {string} token */ CSSOM.parse = function parse(token) { var i = 0; /** "before-selector" or "selector" or "atRule" or "atBlock" or "before-name" or "name" or "before-value" or "value" */ var state = "before-selector"; var index; var buffer = ""; var SIGNIFICANT_WHITESPACE = { "selector": true, "value": true, "atRule": true, "importRule-begin": true, "importRule": true, "atBlock": true, 'documentRule-begin': true }; var styleSheet = new CSSOM.CSSStyleSheet(); // @type CSSStyleSheet|CSSMediaRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule var currentScope = styleSheet; // @type CSSMediaRule|CSSKeyframesRule|CSSDocumentRule var parentRule; var name, priority="", styleRule, mediaRule, importRule, fontFaceRule, keyframesRule, documentRule; var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; var parseError = function(message) { var lines = token.substring(0, i).split('\n'); var lineCount = lines.length; var charCount = lines.pop().length + 1; var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')'); error.line = lineCount; /* jshint sub : true */ error['char'] = charCount; error.styleSheet = styleSheet; throw error; }; for (var character; (character = token.charAt(i)); i++) { switch (character) { case " ": case "\t": case "\r": case "\n": case "\f": if (SIGNIFICANT_WHITESPACE[state]) { buffer += character; } break; // String case '"': index = i + 1; do { index = token.indexOf('"', index) + 1; if (!index) { parseError('Unmatched "'); } } while (token[index - 2] === '\\'); buffer += token.slice(i, index); i = index - 1; switch (state) { case 'before-value': state = 'value'; break; case 'importRule-begin': state = 'importRule'; break; } break; case "'": index = i + 1; do { index = token.indexOf("'", index) + 1; if (!index) { parseError("Unmatched '"); } } while (token[index - 2] === '\\'); buffer += token.slice(i, index); i = index - 1; switch (state) { case 'before-value': state = 'value'; break; case 'importRule-begin': state = 'importRule'; break; } break; // Comment case "/": if (token.charAt(i + 1) === "*") { i += 2; index = token.indexOf("*/", i); if (index === -1) { parseError("Missing */"); } else { i = index + 1; } } else { buffer += character; } if (state === "importRule-begin") { buffer += " "; state = "importRule"; } break; // At-rule case "@": if (token.indexOf("@-moz-document", i) === i) { state = "documentRule-begin"; documentRule = new CSSOM.CSSDocumentRule(); documentRule.__starts = i; i += "-moz-document".length; buffer = ""; break; } else if (token.indexOf("@media", i) === i) { state = "atBlock"; mediaRule = new CSSOM.CSSMediaRule(); mediaRule.__starts = i; i += "media".length; buffer = ""; break; } else if (token.indexOf("@import", i) === i) { state = "importRule-begin"; i += "import".length; buffer += "@import"; break; } else if (token.indexOf("@font-face", i) === i) { state = "fontFaceRule-begin"; i += "font-face".length; fontFaceRule = new CSSOM.CSSFontFaceRule(); fontFaceRule.__starts = i; buffer = ""; break; } else { atKeyframesRegExp.lastIndex = i; var matchKeyframes = atKeyframesRegExp.exec(token); if (matchKeyframes && matchKeyframes.index === i) { state = "keyframesRule-begin"; keyframesRule = new CSSOM.CSSKeyframesRule(); keyframesRule.__starts = i; keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found i += matchKeyframes[0].length - 1; buffer = ""; break; } else if (state === "selector") { state = "atRule"; } } buffer += character; break; case "{": if (state === "selector" || state === "atRule") { styleRule.selectorText = buffer.trim(); styleRule.style.__starts = i; buffer = ""; state = "before-name"; } else if (state === "atBlock") { mediaRule.media.mediaText = buffer.trim(); currentScope = parentRule = mediaRule; mediaRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "fontFaceRule-begin") { if (parentRule) { fontFaceRule.parentRule = parentRule; } fontFaceRule.parentStyleSheet = styleSheet; styleRule = fontFaceRule; buffer = ""; state = "before-name"; } else if (state === "keyframesRule-begin") { keyframesRule.name = buffer.trim(); if (parentRule) { keyframesRule.parentRule = parentRule; } keyframesRule.parentStyleSheet = styleSheet; currentScope = parentRule = keyframesRule; buffer = ""; state = "keyframeRule-begin"; } else if (state === "keyframeRule-begin") { styleRule = new CSSOM.CSSKeyframeRule(); styleRule.keyText = buffer.trim(); styleRule.__starts = i; buffer = ""; state = "before-name"; } else if (state === "documentRule-begin") { // FIXME: what if this '{' is in the url text of the match function? documentRule.matcher.matcherText = buffer.trim(); if (parentRule) { documentRule.parentRule = parentRule; } currentScope = parentRule = documentRule; documentRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } break; case ":": if (state === "name") { name = buffer.trim(); buffer = ""; state = "before-value"; } else { buffer += character; } break; case '(': if (state === 'value') { // ie css expression mode if (buffer.trim() === 'expression') { var info = (new CSSOM.CSSValueExpression(token, i)).parse(); if (info.error) { parseError(info.error); } else { buffer += info.expression; i = info.idx; } } else { index = token.indexOf(')', i + 1); if (index === -1) { parseError('Unmatched "("'); } buffer += token.slice(i, index + 1); i = index; } } else { buffer += character; } break; case "!": if (state === "value" && token.indexOf("!important", i) === i) { priority = "important"; i += "important".length; } else { buffer += character; } break; case ";": switch (state) { case "value": // Hack: Start a new CSSStyleRule if a property gets redefined. // Temporary workaround until https://github.com/NV/CSSOM/issues/16 gets resolved // Remember to keep this block of code in sync with the equivalent hack 50 lines below. if (styleRule.style.getPropertyValue(name)) { styleRule.__ends = i; currentScope.cssRules.push(styleRule); var newStyleRule = new styleRule.constructor(); newStyleRule.__starts = i; console.log("CSSOM", CSSOM); if (styleRule.type === CSSOM.CSSRule.STYLE_RULE) { newStyleRule.style.setProperty('is-continuation', 'yes'); newStyleRule.selectorText = currentScope.cssRules[currentScope.cssRules.length - 1].selectorText; } else if (styleRule.type === CSSOM.CSSRule.FONT_FACE_RULE) { newStyleRule.style.setProperty('is-continuation', 'yes'); var fontFamily = styleRule.style.getPropertyValue('font-family'); if (fontFamily) { newStyleRule.style.setProperty('font-family', fontFamily); } } styleRule = newStyleRule; } styleRule.style.setProperty(name, buffer.trim(), priority); priority = ""; buffer = ""; state = "before-name"; break; case "atRule": buffer = ""; state = "before-selector"; break; case "importRule": importRule = new CSSOM.CSSImportRule(); importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet; importRule.cssText = buffer + character; styleSheet.cssRules.push(importRule); buffer = ""; state = "before-selector"; break; default: buffer += character; break; } break; case "}": switch (state) { case "value": // Hack: Start a new CSSStyleRule if a property gets redefined. // Temporary workaround until https://github.com/NV/CSSOM/issues/16 gets resolved // Remember to keep this block of code in sync with the equivalent hack 50 lines above. if (styleRule.style.getPropertyValue(name)) { styleRule.__ends = i; currentScope.cssRules.push(styleRule); var newStyleRule = new styleRule.constructor(); newStyleRule.__starts = i; if (styleRule.type === CSSOM.CSSRule.STYLE_RULE) { newStyleRule.style.setProperty('is-continuation', 'yes'); newStyleRule.selectorText = currentScope.cssRules[currentScope.cssRules.length - 1].selectorText; } else if (styleRule.type === CSSOM.CSSRule.FONT_FACE_RULE) { newStyleRule.style.setProperty('is-continuation', 'yes'); var fontFamily = styleRule.style.getPropertyValue('font-family'); if (fontFamily) { newStyleRule.style.setProperty('font-family', fontFamily); } } styleRule = newStyleRule; } styleRule.style.setProperty(name, buffer.trim(), priority); priority = ""; /* falls through */ case "before-name": case "name": styleRule.__ends = i + 1; if (parentRule) { styleRule.parentRule = parentRule; } styleRule.parentStyleSheet = styleSheet; currentScope.cssRules.push(styleRule); buffer = ""; if (currentScope.constructor === CSSOM.CSSKeyframesRule) { state = "keyframeRule-begin"; } else { state = "before-selector"; } break; case "keyframeRule-begin": case "before-selector": case "selector": // End of media/document rule. if (!parentRule) { parseError("Unexpected }"); } currentScope.__ends = i + 1; // Nesting rules aren't supported yet styleSheet.cssRules.push(currentScope); currentScope = styleSheet; parentRule = null; buffer = ""; state = "before-selector"; break; } break; default: switch (state) { case "before-selector": state = "selector"; styleRule = new CSSOM.CSSStyleRule(); styleRule.__starts = i; break; case "before-name": state = "name"; break; case "before-value": state = "value"; break; case "importRule-begin": state = "importRule"; break; } buffer += character; break; } } return styleSheet; }; /** * Produces a deep copy of stylesheet — the instance variables of stylesheet are copied recursively. * @param {CSSStyleSheet|CSSOM.CSSStyleSheet} stylesheet * @nosideeffects * @return {CSSOM.CSSStyleSheet} */ CSSOM.clone = function clone(stylesheet) { var cloned = new CSSOM.CSSStyleSheet(); var rules = stylesheet.cssRules; if (!rules) { return cloned; } var RULE_TYPES = { 1: CSSOM.CSSStyleRule, 4: CSSOM.CSSMediaRule, //3: CSSOM.CSSImportRule, //5: CSSOM.CSSFontFaceRule, //6: CSSOM.CSSPageRule, 8: CSSOM.CSSKeyframesRule, 9: CSSOM.CSSKeyframeRule }; for (var i=0, rulesLength=rules.length; i < rulesLength; i++) { var rule = rules[i]; var ruleClone = cloned.cssRules[i] = new RULE_TYPES[rule.type](); var style = rule.style; if (style) { var styleClone = ruleClone.style = new CSSOM.CSSStyleDeclaration(); for (var j=0, styleLength=style.length; j < styleLength; j++) { var name = styleClone[j] = style[j]; styleClone[name] = style[name]; styleClone._importants[name] = style.getPropertyPriority(name); } styleClone.length = style.length; } if (rule.hasOwnProperty('keyText')) { ruleClone.keyText = rule.keyText; } if (rule.hasOwnProperty('selectorText')) { ruleClone.selectorText = rule.selectorText; } if (rule.hasOwnProperty('mediaText')) { ruleClone.mediaText = rule.mediaText; } if (rule.hasOwnProperty('cssRules')) { ruleClone.cssRules = clone(rule).cssRules; } } return cloned; }; })(this);