Adds support for `@-moz-document`.
Also fixes emitted CSS formatting for `@media` and `@host`.
diff --git a/lib/parser.dart b/lib/parser.dart
index 168c9da..2bdaad1 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -485,7 +485,11 @@
* mixin: '@mixin name [(args,...)] '{' declarations/ruleset '}'
* include: '@include name [(@arg,@arg1)]
* '@include name [(@arg...)]
- * content '@content'
+ * content: '@content'
+ * -moz-document: '@-moz-document' [ <url> | url-prefix(<string>) |
+ * domain(<string>) | regexp(<string) ]# '{'
+ * declarations
+ * '}'
*/
processDirective() {
var start = _peekToken.span;
@@ -762,11 +766,12 @@
case TokenKind.DIRECTIVE_INCLUDE:
return processInclude(_makeSpan(start));
-
case TokenKind.DIRECTIVE_CONTENT:
// TODO(terry): TBD
_warning("@content not implemented.", _makeSpan(start));
return null;
+ case TokenKind.DIRECTIVE_MOZ_DOCUMENT:
+ return processDocumentDirective(start);
}
return null;
}
@@ -987,6 +992,51 @@
return new IncludeDirective(name.name, params, span);
}
+ /// Assumes '@' has already been consumed.
+ DocumentDirective processDocumentDirective(SourceSpan start) {
+ _next(); // '-moz-document'
+ var functions = <LiteralTerm>[];
+ do {
+ var function;
+
+ // Consume function token: IDENT '('
+ var ident = identifier();
+ _eat(TokenKind.LPAREN);
+
+ // Consume function arguments.
+ if (ident.name == 'url-prefix' || ident.name == 'domain') {
+ // @-moz-document allows the 'url-prefix' and 'domain' functions to
+ // omit quotations around their argument, contrary to the standard
+ // in which they must be strings. To support this we consume a
+ // string with optional quotation marks, then reapply quotation
+ // marks so they're present in the emitted CSS.
+ var argumentStart = _peekToken.span;
+ var value = processQuotedString(true);
+ // Don't quote the argument if it's empty. '@-moz-document url-prefix()'
+ // is a common pattern used for browser detection.
+ var argument = value.isNotEmpty ? '"$value"' : '';
+ var argumentSpan = _makeSpan(argumentStart);
+
+ _eat(TokenKind.RPAREN);
+
+ var arguments = new Expressions(_makeSpan(argumentSpan))
+ ..add(new LiteralTerm(argument, argument, argumentSpan));
+ function = new FunctionTerm(
+ ident.name, ident.name, arguments, _makeSpan(ident.span));
+ } else {
+ function = processFunction(ident);
+ }
+
+ functions.add(function);
+ } while (_maybeEat(TokenKind.COMMA));
+
+ _eat(TokenKind.LBRACE);
+ var groupRuleBody = processGroupRuleBody();
+ _eat(TokenKind.RBRACE);
+ return new DocumentDirective(
+ functions, groupRuleBody, _makeSpan(start));
+ }
+
RuleSet processRuleSet([SelectorGroup selectorGroup]) {
if (selectorGroup == null) {
selectorGroup = processSelectorGroup();
@@ -998,6 +1048,24 @@
return null;
}
+ List<TreeNode> processGroupRuleBody() {
+ var nodes = <TreeNode>[];
+ while (!(_peekKind(TokenKind.RBRACE) || _peekKind(TokenKind.END_OF_FILE))) {
+ var directive = processDirective();
+ if (directive != null) {
+ nodes.add(directive);
+ continue;
+ }
+ var ruleSet = processRuleSet();
+ if (ruleSet != null) {
+ nodes.add(ruleSet);
+ continue;
+ }
+ break;
+ }
+ return nodes;
+ }
+
/**
* Look ahead to see if what should be a declaration is really a selector.
* If it's a selector than it's a nested selector. This support's Less'
diff --git a/lib/src/css_printer.dart b/lib/src/css_printer.dart
index d3b62d0..2ae6b66 100644
--- a/lib/src/css_printer.dart
+++ b/lib/src/css_printer.dart
@@ -77,22 +77,36 @@
}
}
+ void visitDocumentDirective(DocumentDirective node) {
+ emit('$_newLine@-moz-document ');
+ node.functions.first.visit(this);
+ for (var function in node.functions.skip(1)) {
+ emit(',$_sp');
+ function.visit(this);
+ }
+ emit('$_sp{');
+ for (var ruleSet in node.groupRuleBody) {
+ ruleSet.visit(this);
+ }
+ emit('$_newLine}');
+ }
+
void visitMediaDirective(MediaDirective node) {
- emit(' @media');
+ emit('$_newLine@media');
emitMediaQueries(node.mediaQueries);
- emit(' {');
+ emit('$_sp{');
for (var ruleset in node.rulesets) {
ruleset.visit(this);
}
- emit('$_newLine\}');
+ emit('$_newLine}');
}
void visitHostDirective(HostDirective node) {
- emit('\n@host {');
+ emit('$_newLine@host$_sp{');
for (var ruleset in node.rulesets) {
ruleset.visit(this);
}
- emit('$_newLine\}');
+ emit('$_newLine}');
}
/**
diff --git a/lib/src/tokenkind.dart b/lib/src/tokenkind.dart
index 85e6d40..f6a6c8c 100644
--- a/lib/src/tokenkind.dart
+++ b/lib/src/tokenkind.dart
@@ -161,6 +161,7 @@
static const int DIRECTIVE_INCLUDE = 655;
static const int DIRECTIVE_CONTENT = 656;
static const int DIRECTIVE_EXTEND = 657;
+ static const int DIRECTIVE_MOZ_DOCUMENT = 658;
// Media query operators
static const int MEDIA_OP_ONLY = 665; // Unary.
@@ -218,6 +219,7 @@
const {'type': TokenKind.DIRECTIVE_INCLUDE, 'value': 'include'},
const {'type': TokenKind.DIRECTIVE_CONTENT, 'value': 'content'},
const {'type': TokenKind.DIRECTIVE_EXTEND, 'value': 'extend'},
+ const {'type': TokenKind.DIRECTIVE_MOZ_DOCUMENT, 'value': '-moz-document'},
];
static const List<Map<String, dynamic>> MEDIA_OPERATORS = const [
diff --git a/lib/src/tree.dart b/lib/src/tree.dart
index c3709d2..6321807 100644
--- a/lib/src/tree.dart
+++ b/lib/src/tree.dart
@@ -435,6 +435,20 @@
visit(VisitorBase visitor) => visitor.visitDirective(this);
}
+class DocumentDirective extends Directive {
+ final List<LiteralTerm> functions;
+ final List<TreeNode> groupRuleBody;
+
+ DocumentDirective(this.functions, this.groupRuleBody, SourceSpan span)
+ : super(span);
+
+ DocumentDirective clone() {
+ return new DocumentDirective(this.functions, this.groupRuleBody, span);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitDocumentDirective(this);
+}
+
class ImportDirective extends Directive {
/** import name specified. */
final String import;
diff --git a/lib/src/tree_printer.dart b/lib/src/tree_printer.dart
index ac4512c..97403dc 100644
--- a/lib/src/tree_printer.dart
+++ b/lib/src/tree_printer.dart
@@ -90,6 +90,15 @@
output.depth--;
}
+ void visitDocumentDirective(DocumentDirective node) {
+ heading('DocumentDirective', node);
+ output.depth++;
+ output.writeNodeList('functions', node.functions);
+ output.writeNodeList('group rule body', node.groupRuleBody);
+ super.visitDocumentDirective(node);
+ output.depth--;
+ }
+
void visitPageDirective(PageDirective node) {
heading('PageDirective', node);
output.depth++;
diff --git a/lib/visitor.dart b/lib/visitor.dart
index 1123493..016f20e 100644
--- a/lib/visitor.dart
+++ b/lib/visitor.dart
@@ -20,6 +20,7 @@
visitNoOp(NoOp node);
visitTopLevelProduction(TopLevelProduction node);
visitDirective(Directive node);
+ visitDocumentDirective(DocumentDirective node);
visitMediaExpression(MediaExpression node);
visitMediaQuery(MediaQuery node);
visitMediaDirective(MediaDirective node);
@@ -152,6 +153,11 @@
}
}
+ visitDocumentDirective(DocumentDirective node) {
+ _visitNodeList(node.functions);
+ _visitNodeList(node.groupRuleBody);
+ }
+
visitMediaDirective(MediaDirective node) {
for (var mediaQuery in node.mediaQueries) {
visitMediaQuery(mediaQuery);
diff --git a/test/declaration_test.dart b/test/declaration_test.dart
index 96600d8..0e7cf07 100644
--- a/test/declaration_test.dart
+++ b/test/declaration_test.dart
@@ -380,11 +380,13 @@
.myclass {
height: 20px;
}
-} @media print AND (min-resolution:300dpi) {
+}
+@media print AND (min-resolution:300dpi) {
#anotherId {
color: #fff;
}
-} @media print AND (min-resolution:280dpcm) {
+}
+@media print AND (min-resolution:280dpcm) {
#finalId {
color: #aaa;
}
@@ -445,6 +447,49 @@
expect(prettyPrint(stylesheet), generated);
}
+void testMozDocument() {
+ var errors = <Message>[];
+ // Test empty url-prefix, commonly used for browser detection.
+ var css = '@-moz-document url-prefix() {}';
+ var expected = '@-moz-document url-prefix() {\n}';
+ var styleSheet = parseCss(css, errors: errors);
+ expect(styleSheet, isNotNull);
+ expect(errors, isEmpty);
+ expect(prettyPrint(styleSheet), expected);
+
+ // Test url-prefix with unquoted parameter
+ css = '@-moz-document url-prefix(http://www.w3.org/Style/) {}';
+ expected = '@-moz-document url-prefix("http://www.w3.org/Style/") {\n}';
+ styleSheet = parseCss(css, errors: errors);
+ expect(styleSheet, isNotNull);
+ expect(errors, isEmpty);
+ expect(prettyPrint(styleSheet), expected);
+
+ // Test domain with unquoted parameter
+ css = '@-moz-document domain(google.com) {}';
+ expected = '@-moz-document domain("google.com") {\n}';
+ styleSheet = parseCss(css, errors: errors);
+ expect(styleSheet, isNotNull);
+ expect(errors, isEmpty);
+ expect(prettyPrint(styleSheet), expected);
+
+ // Test all document functions combined.
+ css = '@-moz-document ' +
+ 'url(http://www.w3.org/), ' +
+ "url-prefix('http://www.w3.org/Style/'), " +
+ 'domain("google.com"), ' +
+ 'regexp("https:.*") {} ';
+ expected = '@-moz-document ' +
+ 'url("http://www.w3.org/"), ' +
+ 'url-prefix("http://www.w3.org/Style/"), ' +
+ 'domain("google.com"), ' +
+ 'regexp("https:.*") {\n}';
+ styleSheet = parseCss(css, errors: errors);
+ expect(styleSheet, isNotNull);
+ expect(errors, isEmpty);
+ expect(prettyPrint(styleSheet), expected);
+}
+
void testFontFace() {
var errors = <Message>[];
@@ -1111,6 +1156,7 @@
test('Unicode', testUnicode);
test('Newer CSS', testNewerCss);
test('Media Queries', testMediaQueries);
+ test('Document', testMozDocument);
test('Font-Face', testFontFace);
test('CSS file', testCssFile);
test('Compact Emitter', testCompactEmitter);