//////////////////////////////////////////////////////////////////////////////// // // Licensed to the Apache Software Foundation (ASF) under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // The ASF licenses this file to You under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// package mx.utils { import flash.system.Capabilities; import mx.core.IFlexModuleFactory; import mx.core.mx_internal; use namespace mx_internal; [ExcludeClass] /** * @private * Parser for CSS Media Query syntax. Not a full-fledged parser. * Doesn't report syntax errors, assumes you have your attributes * and identifiers spelled correctly, etc. * * @langversion 3.0 * @playerversion Flash 10.2 * @playerversion AIR 2.6 * @productversion Flex 4.5 */ public class MediaQueryParser { /** * @private * Table of known media types * * @langversion 3.0 * @playerversion Flash 10.2 * @playerversion AIR 2.6 * @productversion Flex 4.5 */ public static var platformMap:Object = { AND: "android", IOS: "ios", MAC: "macintosh", WIN: "windows", LNX: "linux", QNX: "qnx" } /** * @private */ private static var _instance:MediaQueryParser; /** * Single shared instance of the parser * * @langversion 3.0 * @playerversion Flash 10.2 * @playerversion AIR 2.6 * @productversion Flex 4.5 */ public static function get instance():MediaQueryParser { return _instance; } /** * @private */ public static function set instance(value:MediaQueryParser):void { if (!_instance) _instance = value; } /** * @private * Constructor * * @langversion 3.0 * @playerversion Flash 10.2 * @playerversion AIR 2.6 * @productversion Flex 4.5 */ public function MediaQueryParser(moduleFactory:IFlexModuleFactory = null) { applicationDpi = DensityUtil.getRuntimeDPI(); if (moduleFactory) { if (moduleFactory.info()["applicationDPI"] != null) applicationDpi = moduleFactory.info()["applicationDPI"]; } osPlatform = getPlatform(); } /** * Queries that were true */ mx_internal var goodQueries:Object = {}; /** * Queries that were false */ mx_internal var badQueries:Object = {}; /** * @private * Main entry point. * * @param expression A syntactically correct CSS Media Query * @returns true if valid for this media, false otherwise * * @langversion 3.0 * @playerversion Flash 10.2 * @playerversion AIR 2.6 * @productversion Flex 4.5 */ public function parse(expression:String):Boolean { // remove whitespace expression = StringUtil.trim(expression); // force to lower case cuz case-insensitive expression = expression.toLowerCase(); // degenerate expressions if (expression == "") return true; if (expression == "all") return true; if (goodQueries[expression]) return true; if (badQueries[expression]) return false; // get a list of queries. If any pass then // we're good var mediaQueries:Array = expression.split(", "); var n:int = mediaQueries.length; for (var i:int = 0; i < n; i++) { var result:Boolean; var mediaQuery:String = mediaQueries[i]; var notFlag:Boolean = false; // eat only if (mediaQuery.indexOf("only ") == 0) mediaQuery = mediaQuery.substr(5); // remember if this is a "not" expression if (mediaQuery.indexOf("not ") == 0) { notFlag = true; mediaQuery = mediaQuery.substr(4); } // get a list of the parts of the query. // it should be media type, optionally // followed by "and" followed by // optional media feature expressions var expressions:Array = tokenizeMediaQuery(mediaQuery); var numExpressions:int = expressions.length; if (expressions[0] == "all" || expressions[0] == type) { if (numExpressions == 1 && !notFlag) { goodQueries[expression] = true; return true; } // bail if "and" and no media features (invalid query) if (numExpressions == 2) return false; // kick off the type and "and" expressions.shift(); expressions.shift(); // see if the media features match result = evalExpressions(expressions); // early exit if it returned true; if ((result && !notFlag) || (!result && notFlag)) { goodQueries[expression] = true; return true; } } // if we didn't match on media type and we have a notFlag // then we match else if (notFlag) { goodQueries[expression] = true; return true; } } badQueries[expression] = true; return false; } // break up the expression into pieces private function tokenizeMediaQuery(mediaQuery:String):Array { var tokens:Array = []; // if leading off with "(" then // "all and" is implied var pos:int = mediaQuery.indexOf("("); if (pos == 0) { tokens.push("all"); tokens.push("and"); } else if (pos == -1) { // no parens means the whole thing should // be the media type return [ mediaQuery ]; } var parenLevel:int = 0; var inComment:Boolean = false; var n:int = mediaQuery.length; var expression:Array = []; // walk through each character looking for the pieces for (var i:int = 0; i < n; i++) { var c:String = mediaQuery.charAt(i); if (StringUtil.isWhitespace(c) && expression.length == 0) { // eat extra whitespace between tokens continue; } else { // this piece should be the media type if (c == '/' && i < n - 1 && mediaQuery.charAt(i + 1) == '*') { inComment = true; i++; continue; } if (inComment) { if (c == '*' && i < n - 1 && mediaQuery.charAt(i + 1) == '/') { inComment = false; i++; } continue; } else if (c == "(") // Not sure whether these should be in the “else” here? parenLevel++; else if (c == ")") parenLevel--; else { expression.push(c); } // If we found whitespace and not in a paren, or just closed a paren, // then that's the end of an expression if (parenLevel == 0 && (StringUtil.isWhitespace(c) || (c == ")"))) { if (c != ")") expression.length--; tokens.push(expression.join("")); expression.length = 0; // reset } } } return tokens; } // take a media feature expression and evaluate it private function evalExpressions(expressions:Array):Boolean { var n:int = expressions.length; for (var i:int = 0; i < n; i++) { var expr:String = expressions[i]; // skip over "and" if (expr == "and") continue; // break into two pieces var parts:Array = expr.split(":"); var min:Boolean = false; var max:Boolean = false; // look for min if (parts[0].indexOf("min-") == 0) { min = true; parts[0] = parts[0].substr(4); } // look for max else if (parts[0].indexOf("max-") == 0) { max = true; parts[0] = parts[0].substr(4); } // collapse hypens into camelcase if (parts[0].indexOf("-") > 0) parts[0] = deHyphenate(parts[0]); // if only one part, then it only matters that this property exists if (parts.length == 1) { if (!(parts[0] in this)) return false; } // if two parts, then make sure the property exists and value matches if (parts.length == 2) { // if property doesn't exist, then bail if (!(parts[0] in this)) return false; // handle min (we don't check if min is allowed for this property) if (min) { if (this[parts[0]] < normalize(parts[1], typeof(this[parts[0]]))) return false; } // handle max (we don't check if min is allowed for this property) else if (max) { if (this[parts[0]] > normalize(parts[1], typeof(this[parts[0]]))) return false; } // bail if the value doesn't match else if (this[parts[0]] != normalize(parts[1], typeof(this[parts[0]]))) { return false; } } } // all parts matched so return true return true; } // strip off metrics (maybe convert metrics some day) private function normalize(s:String, type:String):Object { var index:int; // strip leading white space if (s.charAt(0) == " ") s = s.substr(1); // for the numbers we currently handle, we // might find dpi or ppi on it, that we just strip off. // We don't handle dpcm yet. if (type == "number") { index = s.indexOf("dpi"); if (index != -1) { s = s.substr(0, index); } return Number(s); } else if (type == "int") { return int(s); } else if (type == "string") { // strip quotes of strings if (s.indexOf('"') == 0) { if (s.lastIndexOf('"') == s.length - 1) s = s.substr(1, s.length - 2); else s = s.substr(1); } } return s; } // collapse "-" to camelCase private function deHyphenate(s:String):String { var i:int = s.indexOf("-"); while (i > 0) { var part:String = s.substr(i + 1); s = s.substr(0, i); var c:String = part.charAt(0); c = c.toUpperCase(); s += c + part.substr(1); i = s.indexOf("-"); } return s; } private function getPlatform():String { var s:String = Capabilities.version.substr(0, 3); // if there is a friendly name, then use it if (platformMap.hasOwnProperty(s)) return platformMap[s] as String; // otherwise match against the 3 characters. // use lower case because match are case // insensitive and we lower case the entire // expression return s.toLowerCase(); } // the type of the media public var type:String = "screen"; // the resolution of the media public var applicationDpi:Number; // the platform of the media public var osPlatform:String; } }