From 1dc4f1b49efeca21e9030c2e3d1de1ffad96b546 Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Sun, 28 Apr 2013 17:29:52 -0700 Subject: [PATCH] VASTLY improved parser. --- TODO | 8 ++ lib/element-query.js | 10 +- lib/parser-old.js | 101 ++++++++++++++++ lib/parser.js | 278 +++++++++++++++++++++++++++++++++---------- test/test.css | 96 +++++++++------ 5 files changed, 389 insertions(+), 104 deletions(-) create mode 100644 TODO create mode 100644 lib/parser-old.js diff --git a/TODO b/TODO new file mode 100644 index 0000000..a34724f --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ +- Not/or/only for media queries +- Support alternative syntax (use the .query-property-value class name) so we don't need to (re)load via XHR +- Unit tests +- Packaging support +- Other query types +- Plugin hooks for queries +- Explanation of how/why it differs from MediaClass (especially w/r/t "available" space) +- Support html:media() by synthesizing actual @media(){} rules diff --git a/lib/element-query.js b/lib/element-query.js index f046e8e..cf44090 100644 --- a/lib/element-query.js +++ b/lib/element-query.js @@ -88,8 +88,9 @@ var elementQuery = (function() { // parse a single CSSStyleSheet object for element queries loadStyleSheet: function(sheet, callback) { if (sheet.ownerNode.nodeName === "STYLE") { - var newStyles = elementQuery.parser.parseStyleText(sheet.ownerNode.innerHTML); - sheet.ownerNode.innerHTML += newStyles; + var result = elementQuery.parser.parseStyleText(sheet.ownerNode.innerHTML); + sheet.ownerNode.innerHTML += result.newCss; + elementQuery.queries = elementQuery.queries.concat(result.queries); callback && callback(); } else if (sheet.href) { @@ -98,9 +99,10 @@ var elementQuery = (function() { xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { - var newStyles = elementQuery.parser.parseStyleText(xhr.responseText); + var result = elementQuery.parser.parseStyleText(xhr.responseText); + elementQuery.queries = elementQuery.queries.concat(result.queries); var style = document.createElement("style"); - style.innerHTML = newStyles; + style.innerHTML = result.newCss; document.body.appendChild(style); } else if (window.console) { diff --git a/lib/parser-old.js b/lib/parser-old.js new file mode 100644 index 0000000..87037bf --- /dev/null +++ b/lib/parser-old.js @@ -0,0 +1,101 @@ +(function(elementQuery) { + // Regexes for parsing + var COMMENT_PATTERN = /(\/\*)[\s\S]*?(\*\/)/g; + var CSS_RULE_PATTERN = /\s*([^{]+)\{([^}]*)\}\s*/g; + var QUERY_PATTERN = /:media\s*\(([^)]*)\)/g; + var QUERY_RULES_PATTERN = /\(?([^\s:]+):\s*(\d+(?:\.\d+)?)(px|em|rem|vw|vh|vmin|vmax)\)?/g; + var MEDIA_PATTERN = /(@media[^{]*)\{((?:\s*([^{]+)\{([^}]*)\}\s*)*)\}/g; + + /** + * el-cheapo parser. It at least screws up nested @media rules + * and puts things that were in @media rules out-of-order + */ + elementQuery.parser = { + // parse the raw text of a style sheet for element queries + parseStyleText: function(sheet) { + // new stylesheet content to add (replacing rules with `:media()` in them) + var newRules = ""; + + // remove comments + sheet = sheet.replace(COMMENT_PATTERN, ""); + + // manage vanilla media queries + var mediaRules = ""; + var parser = this; + sheet = sheet.replace(MEDIA_PATTERN, function(mediaString, query, content) { + var newMediaRules = parser.parseStyleText(content); + if (!/^\s*$/.test(newMediaRules)) { + mediaRules += query + "{\n" + newMediaRules + "\n}\n"; + } + return ""; + }); + + var ruleMatch; + while (ruleMatch = CSS_RULE_PATTERN.exec(sheet)) { + var results = this.queriesForSelector(ruleMatch[1]); + if (results.queries.length) { + newRules += results.selector + "{" + ruleMatch[2] + "}\n"; + elementQuery.queries.push.apply(elementQuery.queries, results.queries); + } + } + return newRules + "\n" + mediaRules; + }, + + // find all the queries in a selector + queriesForSelector: function(selectorString) { + var selectors = selectorString.split(","); + var selectorResults = []; + var queryResults = []; + for (var i = 0, len = selectors.length; i < len; i++) { + var result = this.queriesForSingleSelector(selectors[i]); + selectorResults.push(result.selector); + queryResults = queryResults.concat(result.queries); + } + return { + selector: selectorResults.join(","), + queries: queryResults + }; + }, + + // find all the queries in a *single* selector (i.e. no commas) + queriesForSingleSelector: function(selectorString) { + var queries = []; + var newSelector = ""; + var lastIndex = 0; + var queryMatch; + while (queryMatch = QUERY_PATTERN.exec(selectorString)) { + var querySelector = selectorString.slice(0, queryMatch.index); + var queryRules = this.parseQuery(queryMatch[1]); + var className = elementQuery.classNameForRules(queryRules); + newSelector += selectorString.slice(lastIndex, queryMatch.index); + lastIndex = queryMatch.index + queryMatch[0].length; + queries.push({ + selector: newSelector, + className: className, + rules: queryRules + }); + newSelector += "." + className; + } + newSelector += selectorString.slice(lastIndex); + return { + selector: newSelector, + queries: queries + }; + }, + + // find the actual queried properties in an element query + parseQuery: function(queryString) { + var rules = []; + var ruleMatch; + while (ruleMatch = QUERY_RULES_PATTERN.exec(queryString)) { + rules.push({ + property: ruleMatch[1], + value: parseFloat(ruleMatch[2]), + units: ruleMatch[3] + }); + } + return rules; + } + }; + +}(elementQuery)); \ No newline at end of file diff --git a/lib/parser.js b/lib/parser.js index 87037bf..6a5621b 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,89 +1,237 @@ (function(elementQuery) { - // Regexes for parsing + + // Identifies comments in CSS var COMMENT_PATTERN = /(\/\*)[\s\S]*?(\*\/)/g; - var CSS_RULE_PATTERN = /\s*([^{]+)\{([^}]*)\}\s*/g; + // $1 is the end of a block ("}") + // $2 is all of a simple @rule ("@something ...;") + // $3 is the start of a rule with a block, excluding the opening { + // this could be an @media rule or a simple style rule. + var STATEMENT_END_OR_START_PATTERN = /\s*(?:(\})|(@\S+\s+[^;{]+;)|(?:([^{}]+)\{))/g; + // element queries look like: + // `:media(property: value)` or `:media((property: value) and (property: value))` var QUERY_PATTERN = /:media\s*\(([^)]*)\)/g; var QUERY_RULES_PATTERN = /\(?([^\s:]+):\s*(\d+(?:\.\d+)?)(px|em|rem|vw|vh|vmin|vmax)\)?/g; - var MEDIA_PATTERN = /(@media[^{]*)\{((?:\s*([^{]+)\{([^}]*)\}\s*)*)\}/g; + var WHITESPACE_PATTERN = /^\s*$/; - /** - * el-cheapo parser. It at least screws up nested @media rules - * and puts things that were in @media rules out-of-order - */ + // Parse CSS content for element queries elementQuery.parser = { - // parse the raw text of a style sheet for element queries - parseStyleText: function(sheet) { - // new stylesheet content to add (replacing rules with `:media()` in them) - var newRules = ""; + /** + * This the main entry point for parsing some CSS. Pass it a string of + * CSS content and you'll get back an object like: + * { + * queries: [array of query objects] + * newCss: [string of new CSS] + * } + * The `newCss` property contains new CSS rules to use in place of the + * rules that have element queries -- they replace the queries in selectors + * with classes you can turn on and off on the relevant elements. You'll + * want to insert the new CSS content into a