Skip to content

Commit

Permalink
Add support for arbitrary modifiers after @import (#1695)
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed May 19, 2022
1 parent b19b3b1 commit fa0d2fb
Show file tree
Hide file tree
Showing 19 changed files with 211 additions and 129 deletions.
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)) {
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

0 comments on commit fa0d2fb

Please sign in to comment.