Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support generalized @supports conditions #1134

Merged
merged 2 commits into from Nov 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,12 @@
## 1.29.0

* Support a broader syntax for `@supports` conditions, based on the latest
[Editor's Draft of CSS Conditional Rules 3]. Almost all syntax will be allowed
(with interpolation) in the conditions' parentheses, as well as function
syntax such as `@supports selector(...)`.

[Editor's Draft of CSS Conditional Rules 3]: https://drafts.csswg.org/css-conditional-3/#at-supports

## 1.28.0

* Add a [`color.hwb()`] function to `sass:color` that can express colors in [HWB] format.
Expand Down
2 changes: 2 additions & 0 deletions lib/src/ast/sass.dart
Expand Up @@ -60,7 +60,9 @@ export 'sass/statement/variable_declaration.dart';
export 'sass/statement/warn_rule.dart';
export 'sass/statement/while_rule.dart';
export 'sass/supports_condition.dart';
export 'sass/supports_condition/anything.dart';
export 'sass/supports_condition/declaration.dart';
export 'sass/supports_condition/function.dart';
export 'sass/supports_condition/interpolation.dart';
export 'sass/supports_condition/negation.dart';
export 'sass/supports_condition/operation.dart';
21 changes: 21 additions & 0 deletions lib/src/ast/sass/supports_condition/anything.dart
@@ -0,0 +1,21 @@
// Copyright 2020 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 '../interpolation.dart';
import '../supports_condition.dart';

/// A supports condition that represents the forwards-compatible
/// `<general-enclosed>` production.
class SupportsAnything implements SupportsCondition {
/// The contents of the condition.
final Interpolation contents;

final FileSpan span;

SupportsAnything(this.contents, this.span);

String toString() => "($contents)";
}
23 changes: 23 additions & 0 deletions lib/src/ast/sass/supports_condition/function.dart
@@ -0,0 +1,23 @@
// Copyright 2020 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 '../interpolation.dart';
import '../supports_condition.dart';

/// A function-syntax condition.
class SupportsFunction implements SupportsCondition {
/// The name of the function.
final Interpolation name;

/// The arguments to the function.
final Interpolation arguments;

final FileSpan span;

SupportsFunction(this.name, this.arguments, this.span);

String toString() => "$name($arguments)";
}
170 changes: 126 additions & 44 deletions lib/src/parse/stylesheet.dart
Expand Up @@ -372,7 +372,7 @@ abstract class StylesheetParser extends Parser {
// Parse custom properties as declarations no matter what.
var name = nameBuffer.interpolation(scanner.spanFrom(start, beforeColon));
if (name.initialPlain.startsWith('--')) {
var value = _interpolatedDeclarationValue();
var value = StringExpression(_interpolatedDeclarationValue());
expectStatementSeparator("custom property");
return Declaration(name, scanner.spanFrom(start), value: value);
}
Expand Down Expand Up @@ -538,7 +538,7 @@ abstract class StylesheetParser extends Parser {
scanner.expectChar($colon);

if (parseCustomProperties && name.initialPlain.startsWith('--')) {
var value = _interpolatedDeclarationValue();
var value = StringExpression(_interpolatedDeclarationValue());
expectStatementSeparator("custom property");
return Declaration(name, scanner.spanFrom(start), value: value);
}
Expand Down Expand Up @@ -2681,8 +2681,7 @@ relase. For details, see http://bit.ly/moz-document.
return null;
}

buffer
.addInterpolation(_interpolatedDeclarationValue(allowEmpty: true).text);
buffer.addInterpolation(_interpolatedDeclarationValue(allowEmpty: true));
scanner.expectChar($rparen);
buffer.writeCharCode($rparen);

Expand Down Expand Up @@ -2808,8 +2807,7 @@ relase. For details, see http://bit.ly/moz-document.
buffer
..write(name)
..writeCharCode($lparen)
..addInterpolation(
_interpolatedDeclarationValue(allowEmpty: true).asInterpolation())
..addInterpolation(_interpolatedDeclarationValue(allowEmpty: true))
..writeCharCode($rparen);
if (!scanner.scanChar($rparen)) return false;
return true;
Expand Down Expand Up @@ -2984,10 +2982,18 @@ relase. For details, see http://bit.ly/moz-document.
///
/// If [allowEmpty] is `false` (the default), this requires at least one token.
///
/// If [allowSemicolon] is `true`, this doesn't stop at semicolons and instead
/// includes them in the interpolated output.
///
/// If [allowColon] is `false`, this stops at top-level colons.
///
/// Unlike [declarationValue], this allows interpolation.
StringExpression _interpolatedDeclarationValue({bool allowEmpty = false}) {
// NOTE: this logic is largely duplicated in Parser.declarationValue and
// isIdentifier in utils.dart. Most changes here should be mirrored there.
Interpolation _interpolatedDeclarationValue(
{bool allowEmpty = false,
bool allowSemicolon = false,
bool allowColon = true}) {
// NOTE: this logic is largely duplicated in Parser.declarationValue. Most
// changes here should be mirrored there.

var start = scanner.state;
var buffer = InterpolationBuffer();
Expand Down Expand Up @@ -3065,8 +3071,15 @@ relase. For details, see http://bit.ly/moz-document.
break;

case $semicolon:
if (brackets.isEmpty) break loop;
if (!allowSemicolon && brackets.isEmpty) break loop;
buffer.writeCharCode(scanner.readChar());
wroteNewline = false;
break;

case $colon:
if (!allowColon && brackets.isEmpty) break loop;
buffer.writeCharCode(scanner.readChar());
wroteNewline = false;
break;

case $u:
Expand Down Expand Up @@ -3103,7 +3116,7 @@ relase. For details, see http://bit.ly/moz-document.

if (brackets.isNotEmpty) scanner.expectChar(brackets.last);
if (!allowEmpty && buffer.isEmpty) scanner.error("Expected token.");
return StringExpression(buffer.interpolation(scanner.spanFrom(start)));
return buffer.interpolation(scanner.spanFrom(start));
}

/// Consumes an identifier that may contain interpolation.
Expand Down Expand Up @@ -3301,20 +3314,19 @@ relase. For details, see http://bit.ly/moz-document.
/// Consumes a `@supports` condition.
SupportsCondition _supportsCondition() {
var start = scanner.state;
var first = scanner.peekChar();
if (first != $lparen && first != $hash) {
var start = scanner.state;
expectIdentifier("not");
if (scanIdentifier("not")) {
whitespace();
return SupportsNegation(
_supportsConditionInParens(), scanner.spanFrom(start));
}

var condition = _supportsConditionInParens();
whitespace();
String operator;
while (lookingAtIdentifier()) {
String operator;
if (scanIdentifier("or")) {
if (operator != null) {
expectIdentifier(operator);
} else if (scanIdentifier("or")) {
operator = "or";
} else {
expectIdentifier("and");
Expand All @@ -3333,56 +3345,126 @@ relase. For details, see http://bit.ly/moz-document.
/// Consumes a parenthesized supports condition, or an interpolation.
SupportsCondition _supportsConditionInParens() {
var start = scanner.state;
if (scanner.peekChar() == $hash) {
return SupportsInterpolation(
singleInterpolation(), scanner.spanFrom(start));

if (_lookingAtInterpolatedIdentifier()) {
var identifier = interpolatedIdentifier();
if (identifier.asPlain?.toLowerCase() == "not") {
error('"not" is not a valid identifier here.', identifier.span);
}

if (scanner.scanChar($lparen)) {
var arguments = _interpolatedDeclarationValue(
allowEmpty: true, allowSemicolon: true);
scanner.expectChar($rparen);
return SupportsFunction(identifier, arguments, scanner.spanFrom(start));
} else if (identifier.contents.length != 1 ||
identifier.contents.first is! Expression) {
error("Expected @supports condition.", identifier.span);
} else {
return SupportsInterpolation(
identifier.contents.first as Expression, scanner.spanFrom(start));
}
}

scanner.expectChar($lparen);
whitespace();
var next = scanner.peekChar();
if (next == $lparen || next == $hash) {
var condition = _supportsCondition();
if (scanIdentifier("not")) {
whitespace();
var condition = _supportsConditionInParens();
scanner.expectChar($rparen);
return SupportsNegation(condition, scanner.spanFrom(start));
} else if (scanner.peekChar() == $lparen) {
var condition = _supportsCondition();
scanner.expectChar($rparen);
return condition;
}

if (next == $n || next == $N) {
var negation = _trySupportsNegation();
if (negation != null) {
// Unfortunately, we may have to backtrack here. The grammar is:
//
// Expression ":" Expression
// | InterpolatedIdentifier InterpolatedAnyValue?
//
// These aren't ambiguous because this `InterpolatedAnyValue` is forbidden
// from containing a top-level colon, but we still have to parse the full
// expression to figure out if there's a colon after it.
//
// We could avoid the overhead of a full expression parse by looking ahead
// for a colon (outside of balanced brackets), but in practice we expect the
// vast majority of real uses to be `Expression ":" Expression`, so it makes
// sense to parse that case faster in exchange for less code complexity and
// a slower backtracking case.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a a really nice explanation.

Expression name;
var nameStart = scanner.state;
var wasInParentheses = _inParentheses;
try {
name = expression();
scanner.expectChar($colon);
} on FormatException catch (_) {
scanner.state = nameStart;
_inParentheses = wasInParentheses;

var identifier = interpolatedIdentifier();
var operation = _trySupportsOperation(identifier, nameStart);
if (operation != null) {
scanner.expectChar($rparen);
return negation;
return operation;
}

// If parsing an expression fails, try to parse an
// `InterpolatedAnyValue` instead. But if that value runs into a
// top-level colon, then this is probably intended to be a declaration
// after all, so we rethrow the declaration-parsing error.
var contents = (InterpolationBuffer()
..addInterpolation(identifier)
..addInterpolation(_interpolatedDeclarationValue(
allowEmpty: true, allowSemicolon: true, allowColon: false)))
.interpolation(scanner.spanFrom(nameStart));
if (scanner.peekChar() == $colon) rethrow;

scanner.expectChar($rparen);
return SupportsAnything(contents, scanner.spanFrom(start));
}

var name = expression();
scanner.expectChar($colon);
whitespace();
var value = expression();
scanner.expectChar($rparen);
return SupportsDeclaration(name, value, scanner.spanFrom(start));
}

/// Tries to consume a negated supports condition.
/// If [interpolation] is followed by `"and"` or `"or"`, parse it as a supports operation.
///
/// Returns `null` if it fails.
SupportsNegation _trySupportsNegation() {
var start = scanner.state;
if (!scanIdentifier("not") || scanner.isDone) {
scanner.state = start;
return null;
}
/// Otherwise, return `null` without moving the scanner position.
SupportsOperation _trySupportsOperation(Interpolation interpolation, LineScannerState start) {
if (interpolation.contents.length != 1) return null;
var expression = interpolation.contents.first;
if (expression is! Expression) return null;

var next = scanner.peekChar();
if (!isWhitespace(next) && next != $lparen) {
scanner.state = start;
return null;
var beforeWhitespace = scanner.state;
whitespace();

SupportsOperation operation;
String operator;
while (lookingAtIdentifier()) {
if (operator != null) {
expectIdentifier(operator);
} else if (scanIdentifier("and")) {
operator = "and";
} else if (scanIdentifier("or")) {
operator = "or";
} else {
scanner.state = beforeWhitespace;
return null;
}

whitespace();
var right = _supportsConditionInParens();
operation = SupportsOperation(
operation ?? SupportsInterpolation(
expression as Expression, interpolation.span), right, operator, scanner.spanFrom(start));
whitespace();
}

whitespace();
return SupportsNegation(
_supportsConditionInParens(), scanner.spanFrom(start));
return operation;
}

// ## Characters
Expand Down
5 changes: 5 additions & 0 deletions lib/src/visitor/async_evaluate.dart
Expand Up @@ -1806,6 +1806,11 @@ class _EvaluateVisitor
} else if (condition is SupportsDeclaration) {
return "(${await _evaluateToCss(condition.name)}: "
"${await _evaluateToCss(condition.value)})";
} else if (condition is SupportsFunction) {
return "${await _performInterpolation(condition.name)}("
"${await _performInterpolation(condition.arguments)})";
} else if (condition is SupportsAnything) {
return "(${await _performInterpolation(condition.contents)})";
} else {
return null;
}
Expand Down
7 changes: 6 additions & 1 deletion lib/src/visitor/evaluate.dart
Expand Up @@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: 485fce53ba9f381973c25a69b193a681891be098
// Checksum: ae80942bc5f7f9f4b92e7e4d46903578ea3b9a58
//
// ignore_for_file: unused_import

Expand Down Expand Up @@ -1797,6 +1797,11 @@ class _EvaluateVisitor
} else if (condition is SupportsDeclaration) {
return "(${_evaluateToCss(condition.name)}: "
"${_evaluateToCss(condition.value)})";
} else if (condition is SupportsFunction) {
return "${_performInterpolation(condition.name)}("
"${_performInterpolation(condition.arguments)})";
} else if (condition is SupportsAnything) {
return "(${_performInterpolation(condition.contents)})";
} else {
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
@@ -1,5 +1,5 @@
name: sass
version: 1.28.0
version: 1.29.0
description: A Sass implementation in Dart.
author: Sass Team
homepage: https://github.com/sass/dart-sass
Expand Down