From 0fe62a04a6e2fdfdd048f87c5f7baaddb9a12a0c Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 30 Aug 2016 01:00:49 -0700 Subject: [PATCH] @supports support. --- lib/src/ast/css.dart | 1 + lib/src/ast/css/media_rule.dart | 2 - lib/src/ast/css/supports_rule.dart | 20 ++++ lib/src/ast/sass.dart | 6 ++ lib/src/ast/sass/statement/supports_rule.dart | 25 +++++ lib/src/ast/sass/supports_condition.dart | 7 ++ .../sass/supports_condition/declaration.dart | 20 ++++ .../supports_condition/interpolation.dart | 18 ++++ .../ast/sass/supports_condition/negation.dart | 24 +++++ .../sass/supports_condition/operation.dart | 29 ++++++ lib/src/parser.dart | 92 ++++++++++++++++++- lib/src/visitor/interface/css.dart | 1 + lib/src/visitor/interface/statement.dart | 1 + lib/src/visitor/perform.dart | 54 +++++++++++ lib/src/visitor/serialize.dart | 16 +++- 15 files changed, 308 insertions(+), 8 deletions(-) create mode 100644 lib/src/ast/css/supports_rule.dart create mode 100644 lib/src/ast/sass/statement/supports_rule.dart create mode 100644 lib/src/ast/sass/supports_condition.dart create mode 100644 lib/src/ast/sass/supports_condition/declaration.dart create mode 100644 lib/src/ast/sass/supports_condition/interpolation.dart create mode 100644 lib/src/ast/sass/supports_condition/negation.dart create mode 100644 lib/src/ast/sass/supports_condition/operation.dart diff --git a/lib/src/ast/css.dart b/lib/src/ast/css.dart index b463388a..5eb32dd1 100644 --- a/lib/src/ast/css.dart +++ b/lib/src/ast/css.dart @@ -11,4 +11,5 @@ export 'css/media_rule.dart'; export 'css/node.dart'; export 'css/style_rule.dart'; export 'css/stylesheet.dart'; +export 'css/supports_rule.dart'; export 'css/value.dart'; diff --git a/lib/src/ast/css/media_rule.dart b/lib/src/ast/css/media_rule.dart index d9bbd4a8..387ae518 100644 --- a/lib/src/ast/css/media_rule.dart +++ b/lib/src/ast/css/media_rule.dart @@ -17,6 +17,4 @@ class CssMediaRule extends CssParentNode { /*=T*/ accept/**/(CssVisitor/**/ visitor) => visitor.visitMediaRule(this); - - String toString() => "@media ${queries.join(", ")} {${children.join(" ")}}"; } diff --git a/lib/src/ast/css/supports_rule.dart b/lib/src/ast/css/supports_rule.dart new file mode 100644 index 00000000..867267ff --- /dev/null +++ b/lib/src/ast/css/supports_rule.dart @@ -0,0 +1,20 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../../visitor/interface/css.dart'; +import 'node.dart'; +import 'value.dart'; + +class CssSupportsRule extends CssParentNode { + final CssValue condition; + + final FileSpan span; + + CssSupportsRule(this.condition, this.span); + + /*=T*/ accept/**/(CssVisitor/**/ visitor) => + visitor.visitSupportsRule(this); +} diff --git a/lib/src/ast/sass.dart b/lib/src/ast/sass.dart index 2775d120..4ce2b1f8 100644 --- a/lib/src/ast/sass.dart +++ b/lib/src/ast/sass.dart @@ -37,4 +37,10 @@ export 'sass/statement/plain_import.dart'; export 'sass/statement/return.dart'; export 'sass/statement/style_rule.dart'; export 'sass/statement/stylesheet.dart'; +export 'sass/statement/supports_rule.dart'; export 'sass/statement/variable_declaration.dart'; +export 'sass/supports_condition.dart'; +export 'sass/supports_condition/declaration.dart'; +export 'sass/supports_condition/interpolation.dart'; +export 'sass/supports_condition/negation.dart'; +export 'sass/supports_condition/operation.dart'; diff --git a/lib/src/ast/sass/statement/supports_rule.dart b/lib/src/ast/sass/statement/supports_rule.dart new file mode 100644 index 00000000..108334e1 --- /dev/null +++ b/lib/src/ast/sass/statement/supports_rule.dart @@ -0,0 +1,25 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../../../visitor/interface/statement.dart'; +import '../statement.dart'; +import '../supports_condition.dart'; + +class SupportsRule implements Statement { + final SupportsCondition condition; + + final List children; + + final FileSpan span; + + SupportsRule(this.condition, Iterable children, this.span) + : children = new List.from(children); + + /*=T*/ accept/**/(StatementVisitor/**/ visitor) => + visitor.visitSupportsRule(this); + + String toString() => "@supports $condition {${children.join(' ')}}"; +} diff --git a/lib/src/ast/sass/supports_condition.dart b/lib/src/ast/sass/supports_condition.dart new file mode 100644 index 00000000..e7801311 --- /dev/null +++ b/lib/src/ast/sass/supports_condition.dart @@ -0,0 +1,7 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'node.dart'; + +abstract class SupportsCondition extends SassNode {} diff --git a/lib/src/ast/sass/supports_condition/declaration.dart b/lib/src/ast/sass/supports_condition/declaration.dart new file mode 100644 index 00000000..8c328e46 --- /dev/null +++ b/lib/src/ast/sass/supports_condition/declaration.dart @@ -0,0 +1,20 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../expression.dart'; +import '../supports_condition.dart'; + +class SupportsDeclaration implements SupportsCondition { + final Expression name; + + final Expression value; + + final FileSpan span; + + SupportsDeclaration(this.name, this.value, this.span); + + String toString() => "($name: $value)"; +} diff --git a/lib/src/ast/sass/supports_condition/interpolation.dart b/lib/src/ast/sass/supports_condition/interpolation.dart new file mode 100644 index 00000000..6a449147 --- /dev/null +++ b/lib/src/ast/sass/supports_condition/interpolation.dart @@ -0,0 +1,18 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../expression.dart'; +import '../supports_condition.dart'; + +class SupportsInterpolation implements SupportsCondition { + final Expression expression; + + final FileSpan span; + + SupportsInterpolation(this.expression, this.span); + + String toString() => "#{$expression}"; +} diff --git a/lib/src/ast/sass/supports_condition/negation.dart b/lib/src/ast/sass/supports_condition/negation.dart new file mode 100644 index 00000000..c7ec12e1 --- /dev/null +++ b/lib/src/ast/sass/supports_condition/negation.dart @@ -0,0 +1,24 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../supports_condition.dart'; +import 'operation.dart'; + +class SupportsNegation implements SupportsCondition { + final SupportsCondition condition; + + final FileSpan span; + + SupportsNegation(this.condition, this.span); + + String toString() { + if (condition is SupportsNegation || condition is SupportsOperation) { + return "not ($condition)"; + } else { + return "not $condition"; + } + } +} diff --git a/lib/src/ast/sass/supports_condition/operation.dart b/lib/src/ast/sass/supports_condition/operation.dart new file mode 100644 index 00000000..87242c50 --- /dev/null +++ b/lib/src/ast/sass/supports_condition/operation.dart @@ -0,0 +1,29 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import '../supports_condition.dart'; +import 'negation.dart'; + +class SupportsOperation implements SupportsCondition { + final SupportsCondition left; + + final SupportsCondition right; + + final String operator; + + final FileSpan span; + + SupportsOperation(this.left, this.right, this.operator, this.span); + + String toString() => + "${_parenthesize(left)} ${operator} ${_parenthesize(right)}"; + + String _parenthesize(SupportsCondition condition) => + condition is SupportsNegation || + (condition is SupportsOperation && condition.operator == operator) + ? "($condition)" + : condition.toString(); +} diff --git a/lib/src/parser.dart b/lib/src/parser.dart index b166642f..ba5ba8fe 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -312,6 +312,8 @@ class Parser { return _mixinDeclaration(start); case "return": return _disallowedAtRule(start); + case "supports": + return _supportsRule(start); default: return _unknownAtRule(start, name); } @@ -468,9 +470,14 @@ class Parser { hasContent: _mixinHasContent); } - Return _return(LineScannerState start) { + Return _return(LineScannerState start) => + new Return(_expression(), _scanner.spanFrom(start)); + + SupportsRule _supportsRule(LineScannerState start) { + var condition = _supportsCondition(); _ignoreComments(); - return new Return(_expression(), _scanner.spanFrom(start)); + return new SupportsRule( + condition, _children(_ruleChild), _scanner.spanFrom(start)); } AtRule _unknownAtRule(LineScannerState start, String name) { @@ -1513,6 +1520,87 @@ class Parser { return buffer.interpolation(_scanner.spanFrom(start)); } + // ## Supports Conditions + + SupportsCondition _supportsCondition() { + var start = _scanner.state; + var first = _scanner.peekChar(); + if (first != $lparen && first != $hash) { + var start = _scanner.state; + _expectCaseInsensitive("not"); + _ignoreComments(); + return new SupportsNegation( + _supportsConditionInParens(), _scanner.spanFrom(start)); + } + + var condition = _supportsConditionInParens(); + _ignoreComments(); + while (_lookingAtInterpolatedIdentifier()) { + String operator; + if (_scanCaseInsensitive("or")) { + operator = "or"; + } else { + _expectCaseInsensitive("and"); + operator = "and"; + } + + _ignoreComments(); + var right = _supportsConditionInParens(); + condition = new SupportsOperation( + condition, right, operator, _scanner.spanFrom(start)); + _ignoreComments(); + } + return condition; + } + + SupportsCondition _supportsConditionInParens() { + var start = _scanner.state; + if (_scanner.peekChar() == $hash) { + return new SupportsInterpolation( + _singleInterpolation(), _scanner.spanFrom(start)); + } + + _scanner.expectChar($lparen); + _ignoreComments(); + var next = _scanner.peekChar(); + if (next == $lparen || next == $hash) { + var condition = _supportsCondition(); + _ignoreComments(); + _scanner.expectChar($rparen); + return condition; + } + + if (next == $n || next == $N) { + var negation = _trySupportsNegation(); + if (negation != null) return negation; + } + + var name = _expression(); + _scanner.expectChar($colon); + _ignoreComments(); + var value = _expression(); + _scanner.expectChar($rparen); + return new SupportsDeclaration(name, value, _scanner.spanFrom(start)); + } + + // If this fails, it puts the cursor back at the beginning. + SupportsNegation _trySupportsNegation() { + var start = _scanner.state; + if (!_scanCaseInsensitive("not") || _scanner.isDone) { + _scanner.state = start; + return null; + } + + var next = _scanner.peekChar(); + if (!isWhitespace(next) && next != $lparen) { + _scanner.state = start; + return null; + } + + return new SupportsNegation( + _supportsConditionInParens(), _scanner.spanFrom(start)); + } + // ## Tokens String _commentText() => _rawText(_ignoreComments); diff --git a/lib/src/visitor/interface/css.dart b/lib/src/visitor/interface/css.dart index c228a199..39bdec3c 100644 --- a/lib/src/visitor/interface/css.dart +++ b/lib/src/visitor/interface/css.dart @@ -12,4 +12,5 @@ abstract class CssVisitor { T visitMediaRule(CssMediaRule node); T visitStyleRule(CssStyleRule node); T visitStylesheet(CssStylesheet node); + T visitSupportsRule(CssSupportsRule node); } diff --git a/lib/src/visitor/interface/statement.dart b/lib/src/visitor/interface/statement.dart index 68bd5055..4a332d8d 100644 --- a/lib/src/visitor/interface/statement.dart +++ b/lib/src/visitor/interface/statement.dart @@ -20,5 +20,6 @@ abstract class StatementVisitor { T visitReturn(Return node); T visitStyleRule(StyleRule node); T visitStylesheet(Stylesheet node); + T visitSupportsRule(SupportsRule node); T visitVariableDeclaration(VariableDeclaration node); } diff --git a/lib/src/visitor/perform.dart b/lib/src/visitor/perform.dart index c49c25e7..308e9e6e 100644 --- a/lib/src/visitor/perform.dart +++ b/lib/src/visitor/perform.dart @@ -337,6 +337,60 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor { }, through: (node) => node is CssStyleRule, removeIfEmpty: true); } + void visitSupportsRule(SupportsRule node) { + if (_declarationName != null) { + throw node.span.message( + "Supports rules may not be used within nested declarations."); + } + + var condition = new CssValue( + _visitSupportsCondition(node.condition), node.condition.span); + _withParent(new CssSupportsRule(condition, node.span), () { + if (_selector == null) { + for (var child in node.children) { + child.accept(this); + } + } else { + // If we're in a style rule, copy it into the supports rule so that + // declarations immediately inside @supports have somewhere to go. + // + // For example, "a {@supports (a: b) {b: c}}" should produce "@supports + // (a: b) {a {b: c}}". + _withParent(new CssStyleRule(_selector, _selector.span), () { + for (var child in node.children) { + child.accept(this); + } + }, removeIfEmpty: true); + } + }, through: (node) => node is CssStyleRule); + } + + String _visitSupportsCondition(SupportsCondition condition) { + if (condition is SupportsOperation) { + return "${_parenthesize(condition.left, condition.operator)} " + "${condition.operator} " + "${_parenthesize(condition.right, condition.operator)}"; + } else if (condition is SupportsNegation) { + return "not ${_parenthesize(condition.condition)}"; + } else if (condition is SupportsInterpolation) { + return condition.expression.accept(this); + } else if (condition is SupportsDeclaration) { + return "(${condition.name.accept(this)}: ${condition.value.accept(this)})"; + } else { + return null; + } + } + + String _parenthesize(SupportsCondition condition, [String operator]) { + if ((condition is SupportsNegation) || + (condition is SupportsOperation && + (operator == null || operator != condition.operator))) { + return "(${_visitSupportsCondition(condition)})"; + } else { + return _visitSupportsCondition(condition); + } + } + void visitVariableDeclaration(VariableDeclaration node) { _environment.setVariable(node.name, node.expression.accept(this), global: node.isGlobal); diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index a4f43087..10c6027f 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -42,14 +42,11 @@ String selectorToCss(Selector selector) { class _SerializeCssVisitor implements CssVisitor, ValueVisitor, SelectorVisitor { - final OutputStyle _style; - final _buffer = new StringBuffer(); var _indentation = 0; - _SerializeCssVisitor({OutputStyle style}) - : _style = style ?? OutputStyle.expanded; + _SerializeCssVisitor({OutputStyle style}); void visitStylesheet(CssStylesheet node) { for (var child in node.children) { @@ -134,6 +131,17 @@ class _SerializeCssVisitor _buffer.writeln(); } + void visitSupportsRule(CssSupportsRule node) { + _writeIndentation(); + _buffer.write("@supports "); + _buffer.write(node.condition.value); + _buffer.writeCharCode($space); + _visitChildren(node.children); + + // TODO: only add an extra newline if this is a group end + _buffer.writeln(); + } + void visitDeclaration(CssDeclaration node) { _writeIndentation(); _buffer.write(node.name.value);