Fixes @media and invalid test cases (#47)
* Fixes @media and invalid test cases
The parser now supports media queries with expressionless media features (no
trailing `: <expr>`). For example:
@media all and (transform-3d) {}
Also adds missing AND operators to test cases which had expressions immediately
following the media type.
Fixes #44.
* Adds more tests and fixes query list parsing
Changing how media queries are parsed introduced a regression where parsing a
malformed media query list would consume tokens past the initial point of
failure.
diff --git a/lib/parser.dart b/lib/parser.dart
index 35f9133..098079f 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -375,27 +375,19 @@
List<MediaQuery> processMediaQueryList() {
var mediaQueries = <MediaQuery>[];
- bool firstTime = true;
- var mediaQuery;
do {
- mediaQuery = processMediaQuery(firstTime == true);
+ var mediaQuery = processMediaQuery();
if (mediaQuery != null) {
mediaQueries.add(mediaQuery);
- firstTime = false;
- continue;
+ } else {
+ break;
}
-
- // Any more more media types separated by comma.
- if (!_maybeEat(TokenKind.COMMA)) break;
-
- // Yep more media types start again.
- firstTime = true;
- } while ((!firstTime && mediaQuery != null) || firstTime);
+ } while (_maybeEat(TokenKind.COMMA));
return mediaQueries;
}
- MediaQuery processMediaQuery([bool startQuery = true]) {
+ MediaQuery processMediaQuery() {
// Grammar: [ONLY | NOT]? S* media_type S*
// [ AND S* MediaExpr ]* | MediaExpr [ AND S* MediaExpr ]*
@@ -407,41 +399,39 @@
var unaryOp = TokenKind.matchMediaOperator(op, 0, opLen);
if (unaryOp != -1) {
if (isChecked) {
- if (startQuery && unaryOp != TokenKind.MEDIA_OP_NOT ||
+ if (unaryOp != TokenKind.MEDIA_OP_NOT ||
unaryOp != TokenKind.MEDIA_OP_ONLY) {
_warning("Only the unary operators NOT and ONLY allowed",
_makeSpan(start));
}
- if (!startQuery && unaryOp != TokenKind.MEDIA_OP_AND) {
- _warning("Only the binary AND operator allowed", _makeSpan(start));
- }
}
_next();
start = _peekToken.span;
}
var type;
- if (startQuery && unaryOp != TokenKind.MEDIA_OP_AND) {
- // Get the media type.
- if (_peekIdentifier()) type = identifier();
- }
+ // Get the media type.
+ if (_peekIdentifier()) type = identifier();
var exprs = <MediaExpression>[];
- if (unaryOp == -1 || unaryOp == TokenKind.MEDIA_OP_AND) {
- var andOp = false;
- while (true) {
- var expr = processMediaExpression(andOp);
- if (expr == null) break;
-
- exprs.add(expr);
+ while (true) {
+ // Parse AND if query has a media_type or previous expression.
+ var andOp = exprs.isNotEmpty || type != null;
+ if (andOp) {
op = _peekToken.text;
opLen = op.length;
- andOp = TokenKind.matchMediaOperator(op, 0, opLen) ==
- TokenKind.MEDIA_OP_AND;
- if (!andOp) break;
+ if (TokenKind.matchMediaOperator(op, 0, opLen) !=
+ TokenKind.MEDIA_OP_AND) {
+ break;
+ }
_next();
}
+
+ var expr = processMediaExpression(andOp);
+ if (expr == null) break;
+
+ exprs.add(expr);
}
if (unaryOp != -1 || type != null || exprs.length > 0) {
@@ -457,17 +447,16 @@
if (_maybeEat(TokenKind.LPAREN)) {
if (_peekIdentifier()) {
var feature = identifier(); // Media feature.
- while (_maybeEat(TokenKind.COLON)) {
- var startExpr = _peekToken.span;
- var exprs = processExpr();
- if (_maybeEat(TokenKind.RPAREN)) {
- return new MediaExpression(
- andOperator, feature, exprs, _makeSpan(startExpr));
- } else if (isChecked) {
- _warning("Missing parenthesis around media expression",
- _makeSpan(start));
- return null;
- }
+ var exprs = _maybeEat(TokenKind.COLON)
+ ? processExpr()
+ : new Expressions(_makeSpan(_peekToken.span));
+ if (_maybeEat(TokenKind.RPAREN)) {
+ return new MediaExpression(
+ andOperator, feature, exprs, _makeSpan(start));
+ } else if (isChecked) {
+ _warning(
+ "Missing parenthesis around media expression", _makeSpan(start));
+ return null;
}
} else if (isChecked) {
_warning("Missing media feature in media expression", _makeSpan(start));
diff --git a/lib/src/css_printer.dart b/lib/src/css_printer.dart
index 79bcaed..ac26acb 100644
--- a/lib/src/css_printer.dart
+++ b/lib/src/css_printer.dart
@@ -54,8 +54,11 @@
void visitMediaExpression(MediaExpression node) {
emit(node.andOperator ? ' AND ' : ' ');
- emit('(${node.mediaFeature}:');
- visitExpressions(node.exprs);
+ emit('(${node.mediaFeature}');
+ if (node.exprs.expressions.isNotEmpty) {
+ emit(':');
+ visitExpressions(node.exprs);
+ }
emit(')');
}
@@ -68,11 +71,11 @@
}
}
- void emitMediaQueries(queries) {
+ void emitMediaQueries(List<MediaQuery> queries) {
var queriesLen = queries.length;
for (var i = 0; i < queriesLen; i++) {
var query = queries[i];
- if (query.hasMediaType && i > 0) emit(',');
+ if (i > 0) emit(',');
visitMediaQuery(query);
}
}
diff --git a/test/declaration_test.dart b/test/declaration_test.dart
index 9236b12..1cec21e 100644
--- a/test/declaration_test.dart
+++ b/test/declaration_test.dart
@@ -411,13 +411,13 @@
input = '''
@media only screen and (min-device-width: 4000px) and
- (min-device-height: 2000px), screen (another: 100px) {
+ (min-device-height: 2000px), screen AND (another: 100px) {
html {
font-size: 10em;
}
}''';
generated = '@media ONLY screen AND (min-device-width:4000px) '
- 'AND (min-device-height:2000px), screen (another:100px) {\n'
+ 'AND (min-device-height:2000px), screen AND (another:100px) {\n'
'html {\n font-size: 10em;\n}\n}';
stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
@@ -427,14 +427,14 @@
expect(prettyPrint(stylesheet), generated);
input = '''
-@media screen,print (min-device-width: 4000px) and
- (min-device-height: 2000px), screen (another: 100px) {
+@media screen,print AND (min-device-width: 4000px) and
+ (min-device-height: 2000px), screen AND (another: 100px) {
html {
font-size: 10em;
}
}''';
- generated = '@media screen, print (min-device-width:4000px) AND '
- '(min-device-height:2000px), screen (another:100px) {\n'
+ generated = '@media screen, print AND (min-device-width:4000px) AND '
+ '(min-device-height:2000px), screen AND (another:100px) {\n'
'html {\n font-size: 10em;\n}\n}';
stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
@@ -444,15 +444,29 @@
expect(prettyPrint(stylesheet), generated);
input = '''
-@import "test.css" ONLY screen, NOT print (min-device-width: 4000px);''';
- generated =
- '@import "test.css" ONLY screen, NOT print (min-device-width:4000px);';
+@import "test.css" ONLY screen, NOT print AND (min-device-width: 4000px);''';
+ generated = '@import "test.css" ONLY screen, '
+ 'NOT print AND (min-device-width:4000px);';
stylesheet = parseCss(input, errors: errors..clear(), opts: simpleOptions);
expect(stylesheet != null, true);
expect(errors.isEmpty, true, reason: errors.toString());
expect(prettyPrint(stylesheet), generated);
+
+ var css = '@media (min-device-width:400px) {\n}';
+ expectCss(css, css);
+
+ css = '@media all AND (tranform-3d), (-webkit-transform-3d) {\n}';
+ expectCss(css, css);
+
+ // Test that AND operator is required between media type and expressions.
+ css = '@media screen (min-device-width:400px';
+ stylesheet = parseCss(css, errors: errors..clear(), opts: simpleOptions);
+ expect(errors, isNotEmpty);
+ expect(
+ errors.first.message, contains('expected { after media before ruleset'));
+ expect(errors.first.span.text, '(');
}
void testMozDocument() {