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

Add support for arbitrary modifiers after @import #1695

Merged
merged 3 commits into from May 19, 2022
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Expand Up @@ -183,11 +183,11 @@ jobs:
- uses: dart-lang/setup-dart@v1
- run: dart pub get
- name: dartdoc sass
run: dartdoc --quiet --no-generate-docs
run: dart run dartdoc --quiet --no-generate-docs
--errors ambiguous-doc-reference,broken-link,deprecated
--errors unknown-directive,unknown-macro,unresolved-doc-reference
- name: dartdoc sass_api
run: cd pkg/sass_api && dartdoc --quiet --no-generate-docs
run: cd pkg/sass_api && dart run dartdoc --quiet --no-generate-docs
--errors ambiguous-doc-reference,broken-link,deprecated
--errors unknown-directive,unknown-macro,unresolved-doc-reference

Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
@@ -1,7 +1,13 @@
## 1.51.1
## 1.52.0

* Add support for arbitrary modifiers at the end of plain CSS imports, in
addition to the existing `supports()` and media queries. Sass now allows any
sequence of identifiers of functions after the URL of an import for forwards
compatibility with future additions to the CSS spec.

* Fix an issue where source locations tracked through variable references could
potentially become incorrect.

* Fix a bug where a loud comment in the source can break the source map when
embedding the sources, when using the command-line interface or the legacy JS
API.
Expand Down
8 changes: 2 additions & 6 deletions lib/src/ast/css/import.dart
Expand Up @@ -3,7 +3,6 @@
// https://opensource.org/licenses/MIT.

import '../../visitor/interface/css.dart';
import 'media_query.dart';
import 'node.dart';
import 'value.dart';

Expand All @@ -14,11 +13,8 @@ abstract class CssImport extends CssNode {
/// This includes quotes.
CssValue<String> get url;

/// The supports condition attached to this import.
CssValue<String>? get supports;

/// The media query attached to this import.
List<CssMediaQuery>? get media;
/// The modifiers (such as media or supports queries) attached to this import.
CssValue<String>? get modifiers;

T accept<T>(CssVisitor<T> visitor) => visitor.visitCssImport(this);
}
11 changes: 2 additions & 9 deletions lib/src/ast/css/modifiable/import.dart
Expand Up @@ -6,7 +6,6 @@ import 'package:source_span/source_span.dart';

import '../../../visitor/interface/modifiable_css.dart';
import '../import.dart';
import '../media_query.dart';
import '../value.dart';
import 'node.dart';

Expand All @@ -17,17 +16,11 @@ class ModifiableCssImport extends ModifiableCssNode implements CssImport {
/// This includes quotes.
final CssValue<String> url;

/// The supports condition attached to this import.
final CssValue<String>? supports;

/// The media query attached to this import.
final List<CssMediaQuery>? media;
final CssValue<String>? modifiers;

final FileSpan span;

ModifiableCssImport(this.url, this.span,
{this.supports, Iterable<CssMediaQuery>? media})
: media = media == null ? null : List.unmodifiable(media);
ModifiableCssImport(this.url, this.span, {this.modifiers});

T accept<T>(ModifiableCssVisitor<T> visitor) => visitor.visitCssImport(this);
}
1 change: 1 addition & 0 deletions lib/src/ast/sass.dart
Expand Up @@ -25,6 +25,7 @@ export 'sass/expression/number.dart';
export 'sass/expression/parenthesized.dart';
export 'sass/expression/selector.dart';
export 'sass/expression/string.dart';
export 'sass/expression/supports.dart';
export 'sass/expression/unary_operation.dart';
export 'sass/expression/value.dart';
export 'sass/expression/variable.dart';
Expand Down
31 changes: 31 additions & 0 deletions lib/src/ast/sass/expression/supports.dart
@@ -0,0 +1,31 @@
// Copyright 2022 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:meta/meta.dart';
import 'package:source_span/source_span.dart';

import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../supports_condition.dart';

/// An expression-level `@supports` condition.
///
/// This appears only in the modifiers that come after a plain-CSS `@import`. It
/// doesn't include the function name wrapping the condition.
///
/// {@category AST}
@sealed
class SupportsExpression implements Expression {
/// The condition itself.
final SupportsCondition condition;

FileSpan get span => condition.span;

SupportsExpression(this.condition);

T accept<T>(ExpressionVisitor<T> visitor) =>
visitor.visitSupportsExpression(this);

String toString() => condition.toString();
}
20 changes: 5 additions & 15 deletions lib/src/ast/sass/import/static.dart
Expand Up @@ -7,7 +7,6 @@ import 'package:source_span/source_span.dart';

import '../import.dart';
import '../interpolation.dart';
import '../supports_condition.dart';

/// An import that produces a plain CSS `@import` rule.
///
Expand All @@ -19,22 +18,13 @@ class StaticImport implements Import {
/// This already contains quotes.
final Interpolation url;

/// The supports condition attached to this import, or `null` if no condition
/// is attached.
final SupportsCondition? supports;

/// The media query attached to this import, or `null` if no condition is
/// attached.
final Interpolation? media;
/// The modifiers (such as media or supports queries) attached to this import,
/// or `null` if none are attached.
final Interpolation? modifiers;

final FileSpan span;

StaticImport(this.url, this.span, {this.supports, this.media});
StaticImport(this.url, this.span, {this.modifiers});

String toString() {
var buffer = StringBuffer(url);
if (supports != null) buffer.write(" supports($supports)");
if (media != null) buffer.write(" $media");
return buffer.toString();
}
String toString() => "$url${modifiers == null ? '' : ' $modifiers'}";
}
23 changes: 23 additions & 0 deletions lib/src/ast/sass/interpolation.dart
Expand Up @@ -5,6 +5,7 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';

import '../../interpolation_buffer.dart';
import 'expression.dart';
import 'node.dart';

Expand Down Expand Up @@ -40,6 +41,28 @@ class Interpolation implements SassNode {
return first is String ? first : '';
}

/// Creates a new [Interpolation] by concatenating a sequence of [String]s,
/// [Expression]s, or nested [Interpolation]s.
static Interpolation concat(
Iterable<Object /* String | Expression | Interpolation */ > contents,
FileSpan span) {
var buffer = InterpolationBuffer();
for (var element in contents) {
if (element is String) {
buffer.write(element);
} else if (element is Expression) {
buffer.add(element);
} else if (element is Interpolation) {
buffer.addInterpolation(element);
} else {
throw ArgumentError.value(contents, "contents",
"May only contains Strings, Expressions, or Interpolations.");
}
}

return buffer.interpolation(span);
}

Interpolation(Iterable<Object /* String | Expression */ > contents, this.span)
: contents = List.unmodifiable(contents) {
for (var i = 0; i < this.contents.length; i++) {
Expand Down
4 changes: 2 additions & 2 deletions lib/src/parse/css.dart
Expand Up @@ -94,11 +94,11 @@ class CssParser extends ScssParser {
var urlSpan = scanner.spanFrom(urlStart);

whitespace();
var queries = tryImportQueries();
var modifiers = tryImportModifiers();
expectStatementSeparator("@import rule");
return ImportRule([
StaticImport(Interpolation([url], urlSpan), scanner.spanFrom(urlStart),
supports: queries?.item1, media: queries?.item2)
modifiers: modifiers)
], scanner.spanFrom(start));
}

Expand Down
134 changes: 90 additions & 44 deletions lib/src/parse/stylesheet.dart
Expand Up @@ -1086,20 +1086,20 @@ abstract class StylesheetParser extends Parser {
if (next == $u || next == $U) {
var url = dynamicUrl();
whitespace();
var queries = tryImportQueries();
var modifiers = tryImportModifiers();
return StaticImport(Interpolation([url], scanner.spanFrom(start)),
scanner.spanFrom(start),
supports: queries?.item1, media: queries?.item2);
modifiers: modifiers);
}

var url = string();
var urlSpan = scanner.spanFrom(start);
whitespace();
var queries = tryImportQueries();
if (isPlainImportUrl(url) || queries != null) {
var modifiers = tryImportModifiers();
if (isPlainImportUrl(url) || modifiers != null) {
return StaticImport(
Interpolation([urlSpan.text], urlSpan), scanner.spanFrom(start),
supports: queries?.item1, media: queries?.item2);
modifiers: modifiers);
} else {
try {
return DynamicImport(parseImportUrl(url), urlSpan);
Expand Down Expand Up @@ -1135,54 +1135,100 @@ abstract class StylesheetParser extends Parser {
return url.startsWith("http://") || url.startsWith("https://");
}

/// Consumes a supports condition and/or a media query after an `@import`.
/// Consumes a sequence of modifiers (such as media or supports queries)
/// after an import argument.
///
/// Returns `null` if neither type of query can be found.
Tuple2<SupportsCondition?, Interpolation?>? tryImportQueries() {
SupportsCondition? supports;
if (scanIdentifier("supports")) {
scanner.expectChar($lparen);
var start = scanner.state;
if (scanIdentifier("not")) {
whitespace();
supports = SupportsNegation(
_supportsConditionInParens(), scanner.spanFrom(start));
} else if (scanner.peekChar() == $lparen) {
supports = _supportsCondition();
} else {
if (_lookingAtInterpolatedIdentifier()) {
var identifier = interpolatedIdentifier();
if (identifier.asPlain?.toLowerCase() == "not") {
error('"not" is not a valid identifier here.', identifier.span);
}
/// Returns `null` if there are no modifiers.
Interpolation? tryImportModifiers() {
// Exit before allocating anything if we're not looking at any modifiers, as
// is the most common case.
if (!_lookingAtInterpolatedIdentifier() && scanner.peekChar() != $lparen) {
return null;
}

if (scanner.scanChar($lparen)) {
var arguments = _interpolatedDeclarationValue(
allowEmpty: true, allowSemicolon: true);
scanner.expectChar($rparen);
supports = SupportsFunction(
identifier, arguments, scanner.spanFrom(start));
var start = scanner.state;
var buffer = InterpolationBuffer();
while (true) {
if (_lookingAtInterpolatedIdentifier()) {
if (!buffer.isEmpty) buffer.writeCharCode($space);

var identifier = interpolatedIdentifier();
buffer.addInterpolation(identifier);

var name = identifier.asPlain?.toLowerCase();
if (name != "and" && scanner.scanChar($lparen)) {
if (name == "supports") {
var query = _importSupportsQuery();
if (query is! SupportsDeclaration) buffer.writeCharCode($lparen);
buffer.add(SupportsExpression(query));
if (query is! SupportsDeclaration) buffer.writeCharCode($rparen);
} else {
// Backtrack to parse a variable declaration
scanner.state = start;
buffer.writeCharCode($lparen);
buffer.addInterpolation(_interpolatedDeclarationValue(
allowEmpty: true, allowSemicolon: true));
buffer.writeCharCode($rparen);
}

scanner.expectChar($rparen);
whitespace();
} else {
whitespace();
if (scanner.scanChar($comma)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This support for comma looks incompatible with using the comma as separator between multiple ImportArgument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This actually exists to match the previous behavior: because MediaQueryList can contain commas at nearly any point, you could already write @import "..." foo, bar, baz. This just makes it more explicit.

buffer.write(", ");
buffer.addInterpolation(_mediaQueryList());
return buffer.interpolation(scanner.spanFrom(start));
}
}
if (supports == null) {
var name = expression();
scanner.expectChar($colon);
supports = _supportsDeclarationValue(name, start);
}
} else if (scanner.peekChar() == $lparen) {
if (!buffer.isEmpty) buffer.writeCharCode($space);
buffer.addInterpolation(_mediaQueryList());
return buffer.interpolation(scanner.spanFrom(start));
} else {
return buffer.interpolation(scanner.spanFrom(start));
}
scanner.expectChar($rparen);
}
}

/// Consumes the contents of a `supports()` function after an `@import` rule
/// (but not the function name or parentheses).
SupportsCondition _importSupportsQuery() {
if (scanIdentifier("not")) {
whitespace();
var start = scanner.state;
return SupportsNegation(
_supportsConditionInParens(), scanner.spanFrom(start));
} else if (scanner.peekChar() == $lparen) {
return _supportsCondition();
} else {
var function = _tryImportSupportsFunction();
if (function != null) return function;

var start = scanner.state;
var name = expression();
scanner.expectChar($colon);
return _supportsDeclarationValue(name, start);
}
}

/// Consumes a function call within a `supports()` function after an
/// `@import` if available.
SupportsFunction? _tryImportSupportsFunction() {
if (!_lookingAtInterpolatedIdentifier()) return null;

var start = scanner.state;
var name = interpolatedIdentifier();
assert(name.asPlain != "not");

if (!scanner.scanChar($lparen)) {
scanner.state = start;
return null;
}

var media =
_lookingAtInterpolatedIdentifier() || scanner.peekChar() == $lparen
? _mediaQueryList()
: null;
if (supports == null && media == null) return null;
return Tuple2(supports, media);
var value =
_interpolatedDeclarationValue(allowEmpty: true, allowSemicolon: true);
scanner.expectChar($rparen);

return SupportsFunction(name, value, scanner.spanFrom(start));
}

/// Consumes an `@include` rule.
Expand Down