Skip to content

Cleaner match() implementation #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 8, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 75 additions & 235 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,85 @@ See the accompanying LICENSE file for terms.
*/

'use strict';
module.exports = matchMQ;

// -----------------------------------------------------------------------------

function matchMQ(mediaQuery, values) {
//return parseQuery(mediaQuery);
return doesMQMatch(mediaQuery, values);
}
exports.match = matchQuery;
exports.parse = parseQuery;

// -- Utilities ----------------------------------------------------------------
// -----------------------------------------------------------------------------

var RE_MEDIA_QUERY = /(?:(only|not)?\s*([^\s\(\)]+)\s*and\s*)?(.+)?/i,
RE_MQ_EXPRESSION = /\(\s*([^\s\:\)]+)\s*(?:\:\s*([^\s\)]+))?\s*\)/,
RE_MQ_FEATURE = /^(?:(min|max)-)?(.+)/,
RE_LENGTH_UNIT = /(em|rem|px|cm|mm|in|pt|pc)?$/,
RE_RESOLUTION_UNIT = /(dpi|dpcm|dppx)?$/;

function matchQuery(mediaQuery, values) {
return parseQuery(mediaQuery).some(function (query) {
var inverse = query.inverse;

// Either the parsed or specified `type` is "all", or the types must be
// equal for a match.
var typeMatch = query.type === 'all' || values.type === 'all' ||
values.type === query.type;

// Quit early when `type` doesn't match, but take "not" into account.
if ((typeMatch && inverse) || !(typeMatch || inverse)) {
return false;
}

var expressionsMatch = query.expressions.every(function (expression) {
var feature = expression.feature,
modifier = expression.modifier,
expValue = expression.value,
value = values[feature];

// Missing or falsy values don't match.
if (!value) { return false; }

switch (feature) {
case 'orientation':
case 'scan':
return value.toLowerCase() === expValue.toLowerCase();

case 'width':
case 'height':
case 'device-width':
case 'device-height':
expValue = toPx(expValue);
value = toPx(value);
break;

case 'resolution':
expValue = toDpi(expValue);
value = toDpi(value);
break;

case 'aspect-ratio':
case 'device-aspect-ratio':
expValue = toDecimal(expValue);
value = toDecimal(value);
break;

case 'grid':
case 'color':
case 'color-index':
case 'monochrome':
expValue = parseInt(expValue, 10) || 1;
value = parseInt(value, 10) || 0;
break;
}

switch (modifier) {
case 'min': return value >= expValue;
case 'max': return value <= expValue;
default : return value === expValue;
}
});

return (expressionsMatch && !inverse) || (!expressionsMatch && inverse);
});
}

function parseQuery(mediaQuery) {
return mediaQuery.split(',').map(function (query) {
var captures = query.match(RE_MEDIA_QUERY),
Expand All @@ -30,9 +92,8 @@ function parseQuery(mediaQuery) {
expressions = captures[3],
parsed = {};

parsed.only = !!modifier && modifier.toLowerCase() === 'only';
parsed.not = !!modifier && modifier.toLowerCase() === 'not';
parsed.type = type ? type.toLowerCase() : 'all';
parsed.inverse = !!modifier && modifier.toLowerCase() === 'not';
parsed.type = type ? type.toLowerCase() : 'all';

// Split expressions into a list.
expressions = expressions.match(/\([^\)]+\)/g);
Expand All @@ -42,9 +103,8 @@ function parseQuery(mediaQuery) {
feature = captures[1].toLowerCase().match(RE_MQ_FEATURE);

return {
feature : feature[0],
modifier: feature[1],
property: feature[2],
feature : feature[2],
value : captures[2]
};
});
Expand All @@ -53,6 +113,8 @@ function parseQuery(mediaQuery) {
});
}

// -- Utilities ----------------------------------------------------------------

function toDecimal(ratio) {
var decimal = Number(ratio),
numbers;
Expand Down Expand Up @@ -91,225 +153,3 @@ function toPx(length) {
default : return value;
}
}

function toDecimal(ratio) {
var numbers = ratio.match(/^(\d+)\s*\/\s*(\d+)$/);
return numbers[1] / numbers[2];
}


/* Couple of array utility methods inspired by UnderscoreJS */

//http://underscorejs.org/#pluck
function pluck (o, key) {
return o.map(function (o) {
return o[key];
});
};

//http://underscorejs.org/#difference
function difference (array) {
var rest = Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(arguments, 1));
return array.filter(function(value){ return !(rest.indexOf(value) != -1) });
};

//http://underscorejs.org/#flatten
function flatten (input) {
return Array.prototype.concat.apply([], input);
}


function doesMQMatch(mq, query) {

var parsed = parseQuery(mq),
matches = [];

parsed.forEach(function (p) {

var diff,
keys,
q = query,
expressionKeys = [],
didMQMatch = true;

//Early check to make sure we have the correct type. No point proceeding if we don't.
if (doesTypePass(p.type, q.type)) {

//we delete the type property from the query object so we are just left with media features
//TODO: should the payload be broken up into { type: 'foo', features: { width: 'bar' }}
delete q.type;

//Make an array out of the remaining properties
keys = Object.keys(q);


// We do a quick check to make sure all keys in the query are reflected in the media query features.
// If there are keys in the query which are not present in the media query features, its a false match.
//Get all the property values from the expressions and flatten it into an array.
expressionKeys.push(pluck(p.expressions, 'property'));
expressionKeys = flatten(expressionKeys);

//Compare the diff between the keys and the collected property values.
diff = difference(keys, expressionKeys);

//If there are missing keys, then it's a false match.
if (diff.length) {
didMQMatch = false;
}

//If there is no difference, then all keys are in media query. Now we loop through the features to see it's a positive match.
else {
p.expressions.forEach(function (e) {
//if the query contains this property, then we need to do a check to see if it passes the threshold. If any of the matches are false, then the media query does not pass.
var match = checkForMatch(e, q);
if (!match) {
didMQMatch = false;
}
});
}

}
else {
didMQMatch = false;
}

//If there was a `not` in front of the media query, we need to invert the match.
didMQMatch = (p.not) ? !didMQMatch : didMQMatch;

//For each parsed mq, add a `true` or a `false` to the matches array.
matches.push(didMQMatch);
});

//if the `matches` array contains any truthy value, return true. Else, return false.
return matches.indexOf(true) != -1;
}

function checkForMatch (exp, query) {
var val = query[exp.property],
isMatch = false;

//if there's a value for this property, then we need to see if it is within the threshold
//doing an explicit undefined check here so that `0` goes through.
if (val !== undefined) {
switch (exp.property) {
case 'device-width':
case 'device-height':
case 'width':
case 'height':
isMatch = doesLengthPass(exp, val);
break;

case 'color':
case 'color-index':
case 'monochrome':
isMatch = doesColorPass(exp, val);
break;

case 'resolution':
isMatch = doesResolutionPass(exp, val);
break;

case 'aspect-ratio':
isMatch = doesAspectRatioPass(exp, val);
break;

case 'orientation':
case 'scan':
isMatch = doesScanPass(exp, val);
break;

case 'grid':
isMatch = doesGridPass(exp, val);
break;

}

return isMatch;
}

//if there is not a value for the property, then we can return true.
else {
return true;
}
}

function checkMinMax (expVal, queryVal, modifier) {
switch (modifier) {
case 'min':
//if the value we want is greater than the minimum required, then it's true.
if (expVal <= queryVal) {
return true;
}
break;
case 'max':
//if the value we want is less than or equal to the maximum required, then it's true.
if (expVal >= queryVal) {
return true;
}
break;
default:
//sometimes we may not have a modifier. in this case, the value has to be an exact match.
if (expVal === queryVal) {
return true;
}
break;
}

return false;
}

function doesTypePass (parsed, value) {
if (!value || value === 'all' || parsed === value) {
return true;
}

return false;
}

function doesLengthPass (exp, val) {
var expToPx = toPx(exp.value),
valToPx = toPx(val);

return checkMinMax(expToPx, valToPx, exp.modifier);
}

function doesColorPass (exp, val) {
var expInt;

//this is the (min-width: foo) and (color) use case, which means "any colored device"
if (!exp.value) {
if (val === 0) return false;
else return true;
}

//assigning after exp.value `undefined` check.
expInt = parseInt(exp.value);
return checkMinMax(expInt, val, exp.modifier);

}

function doesResolutionPass (exp, val) {
var expDpi = toDpi(exp.value),
valDpi = (typeof val === 'string') ? toDpi(val) : val;

return checkMinMax(expDpi, valDpi, exp.modifier);
}

function doesAspectRatioPass (exp, val) {
var expDec = toDecimal(exp.value),
valDec = (typeof val === 'string') ? toDecimal(val) : val;
return checkMinMax(expDec, valDec, exp.modifier);
}

function doesScanPass (exp, val) {
if (exp.value === val) {
return true;
}
return false;
}

function doesGridPass (exp, val) {
//the only way grid would return false is if we explicitly had {grid: <falsy val>} in our query object.
return !!val;
}

2 changes: 1 addition & 1 deletion test/unit-tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var assert = require('chai').assert,
doesMQMatch = require('../');
doesMQMatch = require('../').match;

describe('#doesMQMatch() media `type`', function () {
describe('Type', function(){
Expand Down