Skip to content

Commit 3c195d6

Browse files
committed
Merge pull request #1 from ericf/match-impl
Cleaner match() implementation
2 parents fa2bd00 + b6524f7 commit 3c195d6

File tree

2 files changed

+76
-236
lines changed

2 files changed

+76
-236
lines changed

index.js

+75-235
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,85 @@ See the accompanying LICENSE file for terms.
55
*/
66

77
'use strict';
8-
module.exports = matchMQ;
98

10-
// -----------------------------------------------------------------------------
11-
12-
function matchMQ(mediaQuery, values) {
13-
//return parseQuery(mediaQuery);
14-
return doesMQMatch(mediaQuery, values);
15-
}
9+
exports.match = matchQuery;
10+
exports.parse = parseQuery;
1611

17-
// -- Utilities ----------------------------------------------------------------
12+
// -----------------------------------------------------------------------------
1813

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

20+
function matchQuery(mediaQuery, values) {
21+
return parseQuery(mediaQuery).some(function (query) {
22+
var inverse = query.inverse;
23+
24+
// Either the parsed or specified `type` is "all", or the types must be
25+
// equal for a match.
26+
var typeMatch = query.type === 'all' || values.type === 'all' ||
27+
values.type === query.type;
28+
29+
// Quit early when `type` doesn't match, but take "not" into account.
30+
if ((typeMatch && inverse) || !(typeMatch || inverse)) {
31+
return false;
32+
}
33+
34+
var expressionsMatch = query.expressions.every(function (expression) {
35+
var feature = expression.feature,
36+
modifier = expression.modifier,
37+
expValue = expression.value,
38+
value = values[feature];
39+
40+
// Missing or falsy values don't match.
41+
if (!value) { return false; }
42+
43+
switch (feature) {
44+
case 'orientation':
45+
case 'scan':
46+
return value.toLowerCase() === expValue.toLowerCase();
47+
48+
case 'width':
49+
case 'height':
50+
case 'device-width':
51+
case 'device-height':
52+
expValue = toPx(expValue);
53+
value = toPx(value);
54+
break;
55+
56+
case 'resolution':
57+
expValue = toDpi(expValue);
58+
value = toDpi(value);
59+
break;
60+
61+
case 'aspect-ratio':
62+
case 'device-aspect-ratio':
63+
expValue = toDecimal(expValue);
64+
value = toDecimal(value);
65+
break;
66+
67+
case 'grid':
68+
case 'color':
69+
case 'color-index':
70+
case 'monochrome':
71+
expValue = parseInt(expValue, 10) || 1;
72+
value = parseInt(value, 10) || 0;
73+
break;
74+
}
75+
76+
switch (modifier) {
77+
case 'min': return value >= expValue;
78+
case 'max': return value <= expValue;
79+
default : return value === expValue;
80+
}
81+
});
82+
83+
return (expressionsMatch && !inverse) || (!expressionsMatch && inverse);
84+
});
85+
}
86+
2587
function parseQuery(mediaQuery) {
2688
return mediaQuery.split(',').map(function (query) {
2789
var captures = query.match(RE_MEDIA_QUERY),
@@ -30,9 +92,8 @@ function parseQuery(mediaQuery) {
3092
expressions = captures[3],
3193
parsed = {};
3294

33-
parsed.only = !!modifier && modifier.toLowerCase() === 'only';
34-
parsed.not = !!modifier && modifier.toLowerCase() === 'not';
35-
parsed.type = type ? type.toLowerCase() : 'all';
95+
parsed.inverse = !!modifier && modifier.toLowerCase() === 'not';
96+
parsed.type = type ? type.toLowerCase() : 'all';
3697

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

44105
return {
45-
feature : feature[0],
46106
modifier: feature[1],
47-
property: feature[2],
107+
feature : feature[2],
48108
value : captures[2]
49109
};
50110
});
@@ -53,6 +113,8 @@ function parseQuery(mediaQuery) {
53113
});
54114
}
55115

116+
// -- Utilities ----------------------------------------------------------------
117+
56118
function toDecimal(ratio) {
57119
var decimal = Number(ratio),
58120
numbers;
@@ -91,225 +153,3 @@ function toPx(length) {
91153
default : return value;
92154
}
93155
}
94-
95-
function toDecimal(ratio) {
96-
var numbers = ratio.match(/^(\d+)\s*\/\s*(\d+)$/);
97-
return numbers[1] / numbers[2];
98-
}
99-
100-
101-
/* Couple of array utility methods inspired by UnderscoreJS */
102-
103-
//http://underscorejs.org/#pluck
104-
function pluck (o, key) {
105-
return o.map(function (o) {
106-
return o[key];
107-
});
108-
};
109-
110-
//http://underscorejs.org/#difference
111-
function difference (array) {
112-
var rest = Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(arguments, 1));
113-
return array.filter(function(value){ return !(rest.indexOf(value) != -1) });
114-
};
115-
116-
//http://underscorejs.org/#flatten
117-
function flatten (input) {
118-
return Array.prototype.concat.apply([], input);
119-
}
120-
121-
122-
function doesMQMatch(mq, query) {
123-
124-
var parsed = parseQuery(mq),
125-
matches = [];
126-
127-
parsed.forEach(function (p) {
128-
129-
var diff,
130-
keys,
131-
q = query,
132-
expressionKeys = [],
133-
didMQMatch = true;
134-
135-
//Early check to make sure we have the correct type. No point proceeding if we don't.
136-
if (doesTypePass(p.type, q.type)) {
137-
138-
//we delete the type property from the query object so we are just left with media features
139-
//TODO: should the payload be broken up into { type: 'foo', features: { width: 'bar' }}
140-
delete q.type;
141-
142-
//Make an array out of the remaining properties
143-
keys = Object.keys(q);
144-
145-
146-
// We do a quick check to make sure all keys in the query are reflected in the media query features.
147-
// If there are keys in the query which are not present in the media query features, its a false match.
148-
//Get all the property values from the expressions and flatten it into an array.
149-
expressionKeys.push(pluck(p.expressions, 'property'));
150-
expressionKeys = flatten(expressionKeys);
151-
152-
//Compare the diff between the keys and the collected property values.
153-
diff = difference(keys, expressionKeys);
154-
155-
//If there are missing keys, then it's a false match.
156-
if (diff.length) {
157-
didMQMatch = false;
158-
}
159-
160-
//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.
161-
else {
162-
p.expressions.forEach(function (e) {
163-
//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.
164-
var match = checkForMatch(e, q);
165-
if (!match) {
166-
didMQMatch = false;
167-
}
168-
});
169-
}
170-
171-
}
172-
else {
173-
didMQMatch = false;
174-
}
175-
176-
//If there was a `not` in front of the media query, we need to invert the match.
177-
didMQMatch = (p.not) ? !didMQMatch : didMQMatch;
178-
179-
//For each parsed mq, add a `true` or a `false` to the matches array.
180-
matches.push(didMQMatch);
181-
});
182-
183-
//if the `matches` array contains any truthy value, return true. Else, return false.
184-
return matches.indexOf(true) != -1;
185-
}
186-
187-
function checkForMatch (exp, query) {
188-
var val = query[exp.property],
189-
isMatch = false;
190-
191-
//if there's a value for this property, then we need to see if it is within the threshold
192-
//doing an explicit undefined check here so that `0` goes through.
193-
if (val !== undefined) {
194-
switch (exp.property) {
195-
case 'device-width':
196-
case 'device-height':
197-
case 'width':
198-
case 'height':
199-
isMatch = doesLengthPass(exp, val);
200-
break;
201-
202-
case 'color':
203-
case 'color-index':
204-
case 'monochrome':
205-
isMatch = doesColorPass(exp, val);
206-
break;
207-
208-
case 'resolution':
209-
isMatch = doesResolutionPass(exp, val);
210-
break;
211-
212-
case 'aspect-ratio':
213-
isMatch = doesAspectRatioPass(exp, val);
214-
break;
215-
216-
case 'orientation':
217-
case 'scan':
218-
isMatch = doesScanPass(exp, val);
219-
break;
220-
221-
case 'grid':
222-
isMatch = doesGridPass(exp, val);
223-
break;
224-
225-
}
226-
227-
return isMatch;
228-
}
229-
230-
//if there is not a value for the property, then we can return true.
231-
else {
232-
return true;
233-
}
234-
}
235-
236-
function checkMinMax (expVal, queryVal, modifier) {
237-
switch (modifier) {
238-
case 'min':
239-
//if the value we want is greater than the minimum required, then it's true.
240-
if (expVal <= queryVal) {
241-
return true;
242-
}
243-
break;
244-
case 'max':
245-
//if the value we want is less than or equal to the maximum required, then it's true.
246-
if (expVal >= queryVal) {
247-
return true;
248-
}
249-
break;
250-
default:
251-
//sometimes we may not have a modifier. in this case, the value has to be an exact match.
252-
if (expVal === queryVal) {
253-
return true;
254-
}
255-
break;
256-
}
257-
258-
return false;
259-
}
260-
261-
function doesTypePass (parsed, value) {
262-
if (!value || value === 'all' || parsed === value) {
263-
return true;
264-
}
265-
266-
return false;
267-
}
268-
269-
function doesLengthPass (exp, val) {
270-
var expToPx = toPx(exp.value),
271-
valToPx = toPx(val);
272-
273-
return checkMinMax(expToPx, valToPx, exp.modifier);
274-
}
275-
276-
function doesColorPass (exp, val) {
277-
var expInt;
278-
279-
//this is the (min-width: foo) and (color) use case, which means "any colored device"
280-
if (!exp.value) {
281-
if (val === 0) return false;
282-
else return true;
283-
}
284-
285-
//assigning after exp.value `undefined` check.
286-
expInt = parseInt(exp.value);
287-
return checkMinMax(expInt, val, exp.modifier);
288-
289-
}
290-
291-
function doesResolutionPass (exp, val) {
292-
var expDpi = toDpi(exp.value),
293-
valDpi = (typeof val === 'string') ? toDpi(val) : val;
294-
295-
return checkMinMax(expDpi, valDpi, exp.modifier);
296-
}
297-
298-
function doesAspectRatioPass (exp, val) {
299-
var expDec = toDecimal(exp.value),
300-
valDec = (typeof val === 'string') ? toDecimal(val) : val;
301-
return checkMinMax(expDec, valDec, exp.modifier);
302-
}
303-
304-
function doesScanPass (exp, val) {
305-
if (exp.value === val) {
306-
return true;
307-
}
308-
return false;
309-
}
310-
311-
function doesGridPass (exp, val) {
312-
//the only way grid would return false is if we explicitly had {grid: <falsy val>} in our query object.
313-
return !!val;
314-
}
315-

test/unit-tests.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
var assert = require('chai').assert,
2-
doesMQMatch = require('../');
2+
doesMQMatch = require('../').match;
33

44
describe('#doesMQMatch() media `type`', function () {
55
describe('Type', function(){

0 commit comments

Comments
 (0)