Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Media query parser plugin #23

Closed
wants to merge 9 commits into from

3 participants

@pifantastic

For people who would like to use Respond without adding comment delimiters to their CSS.

@paulirish
Collaborator
var qs = (window.parseMQs
    ? parseMQs(styles)

along with the function rename. (but i dropped one window. )

@scottjehl
Owner

This looks interesting. Nice work. Planning to review further when I get some time.

@pifantastic

Unfortunately my testing has shown the parser to be slower than the comment approach (except, weirdly, in Firefox). For that reason I think it makes sense to implement it as a plugin until I can get performance to match.

Additionally, I can think of one case where the parser will break, escaped quotes in strings. For example, p { content: "\""; }.

@scottjehl
Owner

Thanks!! I may have time to look at this on Sunday

@pifantastic

Here is the jsperf I used for my tests if anyone is interested: http://jsperf.com/respond-js-comment-regex-vs-parser

@pifantastic

Thanks @scottjehl

Also, the parser plugin is live here: http://fourkitchens.com/

@scottjehl
Owner

Great, thanks again. I didn't have time today but I hope to pull this in this week.
If you have any interest in helping on the unit tests, I'd like to get those passing in IE 6-8 before changing the parser. Any experience in that area? I started one up but didn't finish it yet.

@pifantastic

Cool. I've a touch of qunit experience. I'll try to find some time to play around with the tests.

@scottjehl
Owner

Hey - quick update.

Tests are passing in IE 6-8 now. I need to write more of them, but it's better than nothing. Those tests are running here if you're interested http://scottjehl.github.com/Respond/test/unit/

I've updated your branch here so it's parallel with master (unit tests are running).

I also started an experimental branch that pulls your media query parser into the main codebase: https://github.com/scottjehl/Respond/tree/parser-integration

I'd like to do a little more testing to see how noticeable the performance hit is without comments before deciding whether it's a good idea to keep this as an optional plugin or bring it into the main codebase by default. The code weight is light enough that it might be worth considering keeping it in there anyway and turning it on through configuration defaults. Much easier for people to use it that way.

I briefly dropped the minified version with the parser into a complex client site and unfortunately, there was a pretty noticeable delay in IE8 at page load, where it showed the mobile layout for 4-5 seconds before jogging into place (flash of un-media'd content?)

More testing to do tomorrow.

Thanks again!

@scottjehl
Owner

Also: I started a comment-free branch that uses a modified regexp to remove the need for comment tokens.

It seems to work now, but it's probably a bit too greedy.
https://github.com/scottjehl/Respond/blob/comment-free/respond.src.js

@scottjehl scottjehl closed this
@scottjehl
Owner

Closing this out, as it's been integrated into the parser-integration branch, and optimized quite a bit further. thanks so much for the help on this. We're real close now :)

@scottjehl
Owner

Hi all,
I have put together a jsperf test this morning to compare the regexp and loop parsing implementations and see which makes more sense to use for the Respond.js comment-free implementation.

The main goal is to simply test the speed of each method of media query isolation, so the actual queries being used are somewhat irrelevant as far as I can tell. The CSS passed to the parser is intentionally large to expose differences in performance. Some of the queries include a large amount of CSS to see how the number of enclosed rules affect the performance. There are more notes on this at the top.

I'd very much appreciate feedback on whether the test itself should be edited to cover situations I hadn't thought of.

It should be noted that the only browsers that really matter in this test are non-media-query-supporting browsers, such as IE6-8. Other browser results are in there purely for curiosity's sake.

Here's the test suite:
http://jsperf.com/respond-js-parser-regexp-vs-split-loop

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
78 respond.parser.js
@@ -0,0 +1,78 @@
+
+(function(win, doc, undefined) {
+
+ var parseMQs = function(str) {
+ var index = 0,
+ len = str.length,
+ stack = [],
+ media = [],
+ queries = [],
+ inMediaQuery = false;
+
+ while (index < len) {
+ switch (str.charAt(index)) {
+ // Start of a block.
+ case '{':
+ stack.push('{');
+ if (stack.length === 2) {
+ inMediaQuery = true;
+ }
+ break;
+
+ // End of a block.
+ case '}':
+ stack.pop();
+ if (stack.length === 0 && inMediaQuery) {
+ if (media.length) {
+ queries.push(str.substring(media.pop(), index));
+ }
+ inMediaQuery = false;
+ }
+ break;
+
+ // @media queries.
+ case '@':
+ if (str.substring(index, index + 7) === '@media ') {
+ var start = index;
+ // Zip forward to the start of the media query.
+ while (++index < len && str.charAt(index) !== '{');
+
+ // Save the location of this media query. If we hit the end of the file
+ // just fucking, i don't know.
+ if (str.charAt(index) === '{') {
+ media.push(start);
+ index--;
+ }
+ }
+ break;
+
+ // Doubley quoted strings.
+ case '"':
+ while (++index < len && str.charAt(index) !== '"');
+ break;
+
+ // Singley quoted strings.
+ case "'":
+ while (++index < len && str.charAt(index) !== "'");
+ break;
+
+ // Comments.
+ case "/":
+ if (str.charAt(index + 1) == '*') {
+ index += 2;
+ // Zip to the end of this comment block.
+ while (++index < len && str.charAt(index) !== '/' && str.charAt(index - 1) !== '*');
+ }
+ break;
+ };
+
+ index++;
+ }
+
+ return queries;
+ };
+
+ win.respond = win.respond || {};
+ win.respond.parseMQs = parseMQs;
+
+})(window, document);
View
8 respond.src.js
@@ -6,7 +6,7 @@
*/
(function( win, mqSupported ){
//exposed namespace
- win.respond = {};
+ win.respond = win.respond || {};
//define update even in native-mq-supporting browsers, to avoid errors
respond.update = function(){};
@@ -73,7 +73,9 @@
//find media blocks in css text, convert to style blocks
translate = function( styles, href, media ){
- var qs = styles.match( /@media ([^\{]+)\{((?!@media)[\s\S])*(?=\}[\s]*\/\*\/mediaquery\*\/)/gmi ),
+ var qs = (respond.parseMQs
+ ? respond.parseMQs(styles)
+ : styles.match( /@media ([^\{]+)\{((?!@media)[\s\S])*(?=\}[\s]*\/\*\/mediaquery\*\/)/gmi )),
ql = qs && qs.length || 0,
//try to get CSS path
href = href.substring( 0, href.lastIndexOf( "/" )),
@@ -251,7 +253,7 @@
})(
this,
(function( win ){
-
+
//for speed, flag browsers with window.matchMedia support and IE 9 as supported
if( win.matchMedia ){ return true; }
View
2  test/test.html
@@ -5,6 +5,8 @@
<title>Respond JS Test Page</title>
<link href="test.css" rel="stylesheet"/>
<link href="test2.css" media="screen and (min-width: 600px)" rel="stylesheet"/>
+ <link href="test3.css" rel="stylesheet"/>
+ <script src="../respond.parser.js"></script>
<script src="../respond.src.js"></script>
</head>
<body>
View
25 test/test3.css
@@ -0,0 +1,25 @@
+/* Try to trick the parser. */
+
+a:before { content: "}'☺'{" }
+a:after { content: '{""}' }
+
+a:before { content: '' }
+a:after { content: '' }
+
+/*/** /*
+@media screen and (min-width: 1300px) {
+ body {
+ background: navy;
+ }
+}
+*/
+
+@media screen and (min-width: 50000px) {
+ body {
+ background: blanchedalmond;
+ }
+
+ h1 {
+ text-decoration: underline;
+ }
+}
View
28 test/unit/index.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Respond.js Test Suite</title>
+ <link rel="stylesheet" href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" media="screen">
+ <script src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script>
+ <script src="../../respond.src.js"></script>
+ <link href="test.css" rel="stylesheet" />
+ <link href="test2.css" media="all and (min-width: 1200px)" rel="stylesheet" />
+ <script src="tests.js"></script>
+
+</head>
+<body>
+ <h1 id="qunit-header">Respond.js Test Suite</h1>
+ <h2 id="qunit-banner"></h2>
+ <div id="qunit-testrunner-toolbar"></div>
+ <h2 id="qunit-userAgent"></h2>
+ <ol id="qunit-tests"></ol>
+
+ <!-- tests must run in new window -->
+ <div id="launcher"></div>
+
+ <!-- elem for applying css via queries -->
+ <div id="testelem"></div>
+
+</body>
+</html>
View
56 test/unit/test.css
@@ -0,0 +1,56 @@
+
+
+.launcher #qunit-testrunner-toolbar,
+.launcher #qunit-userAgent,
+.launcher #qunit-tests,
+.launcher #qunit-testresult {
+ display: none;
+}
+.launcher #launcher {
+
+ font: 1.5em/1 bold Helvetica, sans-serif;
+}
+
+#testelem {
+ width: 50px;
+}
+
+/*styles for 480px and up */
+@media screen and (min-width: 480px) {
+ #testelem {
+ width: 150px;
+ }
+}/*/mediaquery*/
+
+/*styles for 500px and under*/
+@media screen and (max-width: 480px) {
+ #testelem {
+ height: 150px;
+ }
+}/*/mediaquery*/
+
+
+
+
+/*styles for 620px and up */
+@media only screen and (min-width: 620px) {
+ #testelem {
+ width: 250px;
+ }
+}/*/mediaquery*/
+
+
+/*styles for 760px and up */
+@media only print, only screen and (min-width: 760px) {
+ #testelem {
+ width: 350px;
+ }
+}/*/mediaquery*/
+
+
+/*styles for print that shouldn't apply */
+@media only print and (min-width: 800px) {
+ #testelem {
+ width: 500px;
+ }
+}/*/mediaquery*/
View
5 test/unit/test2.css
@@ -0,0 +1,5 @@
+/* this stylesheet was referenced via a link that had a media attr defined
+it should only apply on screen > 1200px */
+#testelem {
+ width: 16px;
+}
View
95 test/unit/tests.js
@@ -0,0 +1,95 @@
+/*
+ Respond.js unit tests - based on qUnit
+*/
+
+window.onload = function(){
+
+ if( !window.opener ){
+
+ document.documentElement.className = "launcher";
+
+ document.getElementById("launcher").innerHTML = '<p>Tests must run in a popup window. <a href="suite.html" id="suitelink">Open test suite</a></p>';
+
+ document.getElementById( "suitelink" ).onclick = function(){
+ window.open( location.href + "?" + Math.random(), 'win', 'width=800,height=600,scrollbars=1,resizable=1' );
+ return false;
+ };
+
+ }
+ else {
+
+ var testElem = document.getElementById("testelem");
+
+ //check if a particular style has applied properly
+ function widthApplied( val ){
+ return testElem.offsetWidth === val;
+ }
+ function heightApplied( val ){
+ return testElem.offsetHeight === val;
+ }
+
+
+
+ /* TESTS HERE */
+ asyncTest( 'Styles not nested in media queries apply as expected', function() {
+ window.resizeTo(200,600);
+ setTimeout(function(){
+ ok( widthApplied( 50 ), "testelem is 50px wide when window is 200px wide" );
+ start();
+ }, 50);
+ });
+
+ asyncTest( 'styles within min-width media queries apply properly', function() {
+ window.resizeTo(500,600);
+ setTimeout(function(){
+ ok( widthApplied( 150 ), 'testelem is 150px wide when window is 500px wide' );
+ start();
+ }, 50);
+ });
+
+ asyncTest( 'styles within max-width media queries apply properly', function() {
+ window.resizeTo(300,600);
+ setTimeout(function(){
+ ok( heightApplied( 150 ), 'testelem is 150px tall when window is under 480px wide' );
+ start();
+ }, 50);
+ });
+
+
+
+ asyncTest( "styles within a min-width media query with an \"only\" keyword apply properly", function() {
+ window.resizeTo(650,600);
+ setTimeout(function(){
+ ok( widthApplied( 250 ), "testelem is 250px wide when window is 650px wide" );
+ start();
+ }, 50);
+ });
+
+ asyncTest( "styles within a media query with a one true query among other false queries apply properly", function() {
+ window.resizeTo(800,600);
+ setTimeout(function(){
+ ok( widthApplied( 350 ), "testelem is 350px wide when window is 750px wide" );
+ start();
+ }, 50);
+ });
+
+ asyncTest( "stylesheets with a media query in a media attribute apply when they should", function() {
+ window.resizeTo(1300,600);
+ setTimeout(function(){
+ ok( widthApplied( 16 ), "testelem is 16px wide when window is 1300px wide" );
+ start();
+ }, 50);
+ });
+
+ asyncTest( "Styles within a false media query do not apply", function() {
+ window.resizeTo(800,600);
+ setTimeout(function(){
+ ok( !widthApplied( 500 ), "testelem is not 500px wide when window is 1300px wide" );
+ start();
+
+ }, 50);
+ });
+
+ }
+
+};
Something went wrong with that request. Please try again.