diff --git a/HISTORY.md b/HISTORY.md index 73ef943..b1268f0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,22 @@ CSS Media Query Change History ============================== +NEXT +---- + +* Made `parse()` sticter; it will now thorw a `SyntaxError` if a media query is + invalid. This makes it behave more like `JSON.parse()`. + + +0.1.2 (2014-01-21) +------------------ + +* Fixed issue with parsing media queries that do not have expressions. ([#5][]) + + +[#5]: https://github.com/ericf/css-mediaquery/issues/5 + + 0.1.1 (2014-01-08) ------------------ diff --git a/index.js b/index.js index 6cd4fad..0783e84 100644 --- a/index.js +++ b/index.js @@ -11,11 +11,11 @@ exports.parse = parseQuery; // ----------------------------------------------------------------------------- -var RE_MEDIA_QUERY = /(?:(only|not)?\s*([^\s\(\)]+)\s*and\s*)?(.+)?/i, - RE_MQ_EXPRESSION = /\(\s*([^\s\:\)]+)\s*(?:\:\s*([^\s\)]+))?\s*\)/, +var RE_MEDIA_QUERY = /^(?:(only|not)?\s*([_a-z][_a-z0-9-]*)|(\([^\)]+\)))(?:\s*and\s*(.*))?$/i, + RE_MQ_EXPRESSION = /^\(\s*([_a-z-][_a-z0-9-]*)\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)?$/; + RE_LENGTH_UNIT = /(em|rem|px|cm|mm|in|pt|pc)?\s*$/, + RE_RESOLUTION_UNIT = /(dpi|dpcm|dppx)?\s*$/; function matchQuery(mediaQuery, values) { return parseQuery(mediaQuery).some(function (query) { @@ -86,21 +86,46 @@ function matchQuery(mediaQuery, values) { function parseQuery(mediaQuery) { return mediaQuery.split(',').map(function (query) { - var captures = query.match(RE_MEDIA_QUERY), - modifier = captures[1], + query = query.trim(); + + var captures = query.match(RE_MEDIA_QUERY); + + // Media Query must be valid. + if (!captures) { + throw new SyntaxError('Invalid CSS media query: "' + query + '"'); + } + + var modifier = captures[1], type = captures[2], - expressions = captures[3], + expressions = ((captures[3] || '') + (captures[4] || '')).trim(), parsed = {}; parsed.inverse = !!modifier && modifier.toLowerCase() === 'not'; parsed.type = type ? type.toLowerCase() : 'all'; + // Check for media query expressions. + if (!expressions) { + parsed.expressions = []; + return parsed; + } + // Split expressions into a list. - expressions = expressions.match(/\([^\)]+\)/g) || []; + expressions = expressions.match(/\([^\)]+\)/g); + + // Media Query must be valid. + if (!expressions) { + throw new SyntaxError('Invalid CSS media query: "' + query + '"'); + } parsed.expressions = expressions.map(function (expression) { - var captures = expression.match(RE_MQ_EXPRESSION), - feature = captures[1].toLowerCase().match(RE_MQ_FEATURE); + var captures = expression.match(RE_MQ_EXPRESSION); + + // Media Query must be valid. + if (!captures) { + throw new SyntaxError('Invalid CSS media query: "' + query + '"'); + } + + var feature = captures[1].toLowerCase().match(RE_MQ_FEATURE); return { modifier: feature[1], diff --git a/package.json b/package.json index 5bbc663..56a8970 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "css-mediaquery", - "version": "0.1.1", + "version": "0.1.2", "description": "Parses and determines if a given CSS Media Query matches a set of values.", "main": "index.js", "scripts": { @@ -15,6 +15,7 @@ "media", "query", "mediaquery", + "media-query", "mobile", "parse", "match" @@ -23,7 +24,7 @@ "contributors": [ "Tilo Mitra " ], - "license": "BSD", + "license": "BSD-3-Clause", "bugs": { "url": "https://github.com/ericf/css-mediaquery/issues" }, diff --git a/test/unit-tests.js b/test/unit-tests.js index 95a81e9..2483df5 100644 --- a/test/unit-tests.js +++ b/test/unit-tests.js @@ -5,6 +5,57 @@ var expect = require('chai').expect, mediaQuery = require('../'); +describe('mediaQuery.parse()', function () { + it('should parse media queries without expressions', function () { + expect(mediaQuery.parse('screen')).to.eql([ + { + inverse : false, + type : 'screen', + expressions: [] + } + ]); + + expect(mediaQuery.parse('not screen')).to.eql([ + { + inverse : true, + type : 'screen', + expressions: [] + } + ]); + }); + + it('should parse common retina media query list', function () { + var parsed = mediaQuery.parse( + 'only screen and (-webkit-min-device-pixel-ratio: 2),\n' + + 'only screen and ( min--moz-device-pixel-ratio: 2),\n' + + 'only screen and ( -o-min-device-pixel-ratio: 2/1),\n' + + 'only screen and ( min-device-pixel-ratio: 2),\n' + + 'only screen and ( min-resolution: 192dpi),\n' + + 'only screen and ( min-resolution: 2dppx)' + ); + + expect(parsed).to.be.an.array; + expect(parsed).to.have.length(6); + expect(parsed[0].expressions[0].feature).to.equal('-webkit-min-device-pixel-ratio'); + expect(parsed[1].expressions[0].modifier).to.equal('min'); + }); + + it('should throw a SyntaxError when a media query is invalid', function () { + function parse(query) { + return function () { mediaQuery.parse(query); }; + } + + expect(parse('some crap')).to.throw(SyntaxError); + expect(parse('48em')).to.throw(SyntaxError); + expect(parse('screen and crap')).to.throw(SyntaxError); + expect(parse('screen and (48em)')).to.throw(SyntaxError); + expect(parse('screen and (foo:)')).to.throw(SyntaxError); + expect(parse('()')).to.throw(SyntaxError); + expect(parse('(foo) (bar)')).to.throw(SyntaxError); + expect(parse('(foo:) and (bar)')).to.throw(SyntaxError); + }); +}); + describe('mediaQuery.match()', function () { describe('Equality Check', function () { it('Orientation: should return true for a correct match (===)', function () { @@ -279,7 +330,7 @@ describe('mediaQuery.match()', function () { }); }); - describe('#mediaQuery.match() Integration Tests', function () { + describe('mediaQuery.match() Integration Tests', function () { describe('Real World Use Cases (mostly AND)', function () { it('should return true because of width and type match', function () { expect(mediaQuery.match(