blob: d670281e2f4e2acbda7520cf9fd2fd9a2c3cbfb8 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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;
}
}