Skip to content

Commit fa0d2fb

Browse files
authored
Add support for arbitrary modifiers after @import (#1695)
See sass/sass#3285
1 parent b19b3b1 commit fa0d2fb

19 files changed

+211
-129
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,11 @@ jobs:
183183
- uses: dart-lang/setup-dart@v1
184184
- run: dart pub get
185185
- name: dartdoc sass
186-
run: dartdoc --quiet --no-generate-docs
186+
run: dart run dartdoc --quiet --no-generate-docs
187187
--errors ambiguous-doc-reference,broken-link,deprecated
188188
--errors unknown-directive,unknown-macro,unresolved-doc-reference
189189
- name: dartdoc sass_api
190-
run: cd pkg/sass_api && dartdoc --quiet --no-generate-docs
190+
run: cd pkg/sass_api && dart run dartdoc --quiet --no-generate-docs
191191
--errors ambiguous-doc-reference,broken-link,deprecated
192192
--errors unknown-directive,unknown-macro,unresolved-doc-reference
193193

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
## 1.51.1
1+
## 1.52.0
2+
3+
* Add support for arbitrary modifiers at the end of plain CSS imports, in
4+
addition to the existing `supports()` and media queries. Sass now allows any
5+
sequence of identifiers of functions after the URL of an import for forwards
6+
compatibility with future additions to the CSS spec.
27

38
* Fix an issue where source locations tracked through variable references could
49
potentially become incorrect.
10+
511
* Fix a bug where a loud comment in the source can break the source map when
612
embedding the sources, when using the command-line interface or the legacy JS
713
API.

lib/src/ast/css/import.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// https://opensource.org/licenses/MIT.
44

55
import '../../visitor/interface/css.dart';
6-
import 'media_query.dart';
76
import 'node.dart';
87
import 'value.dart';
98

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

17-
/// The supports condition attached to this import.
18-
CssValue<String>? get supports;
19-
20-
/// The media query attached to this import.
21-
List<CssMediaQuery>? get media;
16+
/// The modifiers (such as media or supports queries) attached to this import.
17+
CssValue<String>? get modifiers;
2218

2319
T accept<T>(CssVisitor<T> visitor) => visitor.visitCssImport(this);
2420
}

lib/src/ast/css/modifiable/import.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import 'package:source_span/source_span.dart';
66

77
import '../../../visitor/interface/modifiable_css.dart';
88
import '../import.dart';
9-
import '../media_query.dart';
109
import '../value.dart';
1110
import 'node.dart';
1211

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

20-
/// The supports condition attached to this import.
21-
final CssValue<String>? supports;
22-
23-
/// The media query attached to this import.
24-
final List<CssMediaQuery>? media;
19+
final CssValue<String>? modifiers;
2520

2621
final FileSpan span;
2722

28-
ModifiableCssImport(this.url, this.span,
29-
{this.supports, Iterable<CssMediaQuery>? media})
30-
: media = media == null ? null : List.unmodifiable(media);
23+
ModifiableCssImport(this.url, this.span, {this.modifiers});
3124

3225
T accept<T>(ModifiableCssVisitor<T> visitor) => visitor.visitCssImport(this);
3326
}

lib/src/ast/sass.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export 'sass/expression/number.dart';
2525
export 'sass/expression/parenthesized.dart';
2626
export 'sass/expression/selector.dart';
2727
export 'sass/expression/string.dart';
28+
export 'sass/expression/supports.dart';
2829
export 'sass/expression/unary_operation.dart';
2930
export 'sass/expression/value.dart';
3031
export 'sass/expression/variable.dart';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2022 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'package:meta/meta.dart';
6+
import 'package:source_span/source_span.dart';
7+
8+
import '../../../visitor/interface/expression.dart';
9+
import '../expression.dart';
10+
import '../supports_condition.dart';
11+
12+
/// An expression-level `@supports` condition.
13+
///
14+
/// This appears only in the modifiers that come after a plain-CSS `@import`. It
15+
/// doesn't include the function name wrapping the condition.
16+
///
17+
/// {@category AST}
18+
@sealed
19+
class SupportsExpression implements Expression {
20+
/// The condition itself.
21+
final SupportsCondition condition;
22+
23+
FileSpan get span => condition.span;
24+
25+
SupportsExpression(this.condition);
26+
27+
T accept<T>(ExpressionVisitor<T> visitor) =>
28+
visitor.visitSupportsExpression(this);
29+
30+
String toString() => condition.toString();
31+
}

lib/src/ast/sass/import/static.dart

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:source_span/source_span.dart';
77

88
import '../import.dart';
99
import '../interpolation.dart';
10-
import '../supports_condition.dart';
1110

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

22-
/// The supports condition attached to this import, or `null` if no condition
23-
/// is attached.
24-
final SupportsCondition? supports;
25-
26-
/// The media query attached to this import, or `null` if no condition is
27-
/// attached.
28-
final Interpolation? media;
21+
/// The modifiers (such as media or supports queries) attached to this import,
22+
/// or `null` if none are attached.
23+
final Interpolation? modifiers;
2924

3025
final FileSpan span;
3126

32-
StaticImport(this.url, this.span, {this.supports, this.media});
27+
StaticImport(this.url, this.span, {this.modifiers});
3328

34-
String toString() {
35-
var buffer = StringBuffer(url);
36-
if (supports != null) buffer.write(" supports($supports)");
37-
if (media != null) buffer.write(" $media");
38-
return buffer.toString();
39-
}
29+
String toString() => "$url${modifiers == null ? '' : ' $modifiers'}";
4030
}

lib/src/ast/sass/interpolation.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:meta/meta.dart';
66
import 'package:source_span/source_span.dart';
77

8+
import '../../interpolation_buffer.dart';
89
import 'expression.dart';
910
import 'node.dart';
1011

@@ -40,6 +41,28 @@ class Interpolation implements SassNode {
4041
return first is String ? first : '';
4142
}
4243

44+
/// Creates a new [Interpolation] by concatenating a sequence of [String]s,
45+
/// [Expression]s, or nested [Interpolation]s.
46+
static Interpolation concat(
47+
Iterable<Object /* String | Expression | Interpolation */ > contents,
48+
FileSpan span) {
49+
var buffer = InterpolationBuffer();
50+
for (var element in contents) {
51+
if (element is String) {
52+
buffer.write(element);
53+
} else if (element is Expression) {
54+
buffer.add(element);
55+
} else if (element is Interpolation) {
56+
buffer.addInterpolation(element);
57+
} else {
58+
throw ArgumentError.value(contents, "contents",
59+
"May only contains Strings, Expressions, or Interpolations.");
60+
}
61+
}
62+
63+
return buffer.interpolation(span);
64+
}
65+
4366
Interpolation(Iterable<Object /* String | Expression */ > contents, this.span)
4467
: contents = List.unmodifiable(contents) {
4568
for (var i = 0; i < this.contents.length; i++) {

lib/src/parse/css.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ class CssParser extends ScssParser {
9494
var urlSpan = scanner.spanFrom(urlStart);
9595

9696
whitespace();
97-
var queries = tryImportQueries();
97+
var modifiers = tryImportModifiers();
9898
expectStatementSeparator("@import rule");
9999
return ImportRule([
100100
StaticImport(Interpolation([url], urlSpan), scanner.spanFrom(urlStart),
101-
supports: queries?.item1, media: queries?.item2)
101+
modifiers: modifiers)
102102
], scanner.spanFrom(start));
103103
}
104104

lib/src/parse/stylesheet.dart

Lines changed: 90 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,20 +1086,20 @@ abstract class StylesheetParser extends Parser {
10861086
if (next == $u || next == $U) {
10871087
var url = dynamicUrl();
10881088
whitespace();
1089-
var queries = tryImportQueries();
1089+
var modifiers = tryImportModifiers();
10901090
return StaticImport(Interpolation([url], scanner.spanFrom(start)),
10911091
scanner.spanFrom(start),
1092-
supports: queries?.item1, media: queries?.item2);
1092+
modifiers: modifiers);
10931093
}
10941094

10951095
var url = string();
10961096
var urlSpan = scanner.spanFrom(start);
10971097
whitespace();
1098-
var queries = tryImportQueries();
1099-
if (isPlainImportUrl(url) || queries != null) {
1098+
var modifiers = tryImportModifiers();
1099+
if (isPlainImportUrl(url) || modifiers != null) {
11001100
return StaticImport(
11011101
Interpolation([urlSpan.text], urlSpan), scanner.spanFrom(start),
1102-
supports: queries?.item1, media: queries?.item2);
1102+
modifiers: modifiers);
11031103
} else {
11041104
try {
11051105
return DynamicImport(parseImportUrl(url), urlSpan);
@@ -1135,54 +1135,100 @@ abstract class StylesheetParser extends Parser {
11351135
return url.startsWith("http://") || url.startsWith("https://");
11361136
}
11371137

1138-
/// Consumes a supports condition and/or a media query after an `@import`.
1138+
/// Consumes a sequence of modifiers (such as media or supports queries)
1139+
/// after an import argument.
11391140
///
1140-
/// Returns `null` if neither type of query can be found.
1141-
Tuple2<SupportsCondition?, Interpolation?>? tryImportQueries() {
1142-
SupportsCondition? supports;
1143-
if (scanIdentifier("supports")) {
1144-
scanner.expectChar($lparen);
1145-
var start = scanner.state;
1146-
if (scanIdentifier("not")) {
1147-
whitespace();
1148-
supports = SupportsNegation(
1149-
_supportsConditionInParens(), scanner.spanFrom(start));
1150-
} else if (scanner.peekChar() == $lparen) {
1151-
supports = _supportsCondition();
1152-
} else {
1153-
if (_lookingAtInterpolatedIdentifier()) {
1154-
var identifier = interpolatedIdentifier();
1155-
if (identifier.asPlain?.toLowerCase() == "not") {
1156-
error('"not" is not a valid identifier here.', identifier.span);
1157-
}
1141+
/// Returns `null` if there are no modifiers.
1142+
Interpolation? tryImportModifiers() {
1143+
// Exit before allocating anything if we're not looking at any modifiers, as
1144+
// is the most common case.
1145+
if (!_lookingAtInterpolatedIdentifier() && scanner.peekChar() != $lparen) {
1146+
return null;
1147+
}
11581148

1159-
if (scanner.scanChar($lparen)) {
1160-
var arguments = _interpolatedDeclarationValue(
1161-
allowEmpty: true, allowSemicolon: true);
1162-
scanner.expectChar($rparen);
1163-
supports = SupportsFunction(
1164-
identifier, arguments, scanner.spanFrom(start));
1149+
var start = scanner.state;
1150+
var buffer = InterpolationBuffer();
1151+
while (true) {
1152+
if (_lookingAtInterpolatedIdentifier()) {
1153+
if (!buffer.isEmpty) buffer.writeCharCode($space);
1154+
1155+
var identifier = interpolatedIdentifier();
1156+
buffer.addInterpolation(identifier);
1157+
1158+
var name = identifier.asPlain?.toLowerCase();
1159+
if (name != "and" && scanner.scanChar($lparen)) {
1160+
if (name == "supports") {
1161+
var query = _importSupportsQuery();
1162+
if (query is! SupportsDeclaration) buffer.writeCharCode($lparen);
1163+
buffer.add(SupportsExpression(query));
1164+
if (query is! SupportsDeclaration) buffer.writeCharCode($rparen);
11651165
} else {
1166-
// Backtrack to parse a variable declaration
1167-
scanner.state = start;
1166+
buffer.writeCharCode($lparen);
1167+
buffer.addInterpolation(_interpolatedDeclarationValue(
1168+
allowEmpty: true, allowSemicolon: true));
1169+
buffer.writeCharCode($rparen);
1170+
}
1171+
1172+
scanner.expectChar($rparen);
1173+
whitespace();
1174+
} else {
1175+
whitespace();
1176+
if (scanner.scanChar($comma)) {
1177+
buffer.write(", ");
1178+
buffer.addInterpolation(_mediaQueryList());
1179+
return buffer.interpolation(scanner.spanFrom(start));
11681180
}
11691181
}
1170-
if (supports == null) {
1171-
var name = expression();
1172-
scanner.expectChar($colon);
1173-
supports = _supportsDeclarationValue(name, start);
1174-
}
1182+
} else if (scanner.peekChar() == $lparen) {
1183+
if (!buffer.isEmpty) buffer.writeCharCode($space);
1184+
buffer.addInterpolation(_mediaQueryList());
1185+
return buffer.interpolation(scanner.spanFrom(start));
1186+
} else {
1187+
return buffer.interpolation(scanner.spanFrom(start));
11751188
}
1176-
scanner.expectChar($rparen);
1189+
}
1190+
}
1191+
1192+
/// Consumes the contents of a `supports()` function after an `@import` rule
1193+
/// (but not the function name or parentheses).
1194+
SupportsCondition _importSupportsQuery() {
1195+
if (scanIdentifier("not")) {
11771196
whitespace();
1197+
var start = scanner.state;
1198+
return SupportsNegation(
1199+
_supportsConditionInParens(), scanner.spanFrom(start));
1200+
} else if (scanner.peekChar() == $lparen) {
1201+
return _supportsCondition();
1202+
} else {
1203+
var function = _tryImportSupportsFunction();
1204+
if (function != null) return function;
1205+
1206+
var start = scanner.state;
1207+
var name = expression();
1208+
scanner.expectChar($colon);
1209+
return _supportsDeclarationValue(name, start);
1210+
}
1211+
}
1212+
1213+
/// Consumes a function call within a `supports()` function after an
1214+
/// `@import` if available.
1215+
SupportsFunction? _tryImportSupportsFunction() {
1216+
if (!_lookingAtInterpolatedIdentifier()) return null;
1217+
1218+
var start = scanner.state;
1219+
var name = interpolatedIdentifier();
1220+
assert(name.asPlain != "not");
1221+
1222+
if (!scanner.scanChar($lparen)) {
1223+
scanner.state = start;
1224+
return null;
11781225
}
11791226

1180-
var media =
1181-
_lookingAtInterpolatedIdentifier() || scanner.peekChar() == $lparen
1182-
? _mediaQueryList()
1183-
: null;
1184-
if (supports == null && media == null) return null;
1185-
return Tuple2(supports, media);
1227+
var value =
1228+
_interpolatedDeclarationValue(allowEmpty: true, allowSemicolon: true);
1229+
scanner.expectChar($rparen);
1230+
1231+
return SupportsFunction(name, value, scanner.spanFrom(start));
11861232
}
11871233

11881234
/// Consumes an `@include` rule.

0 commit comments

Comments
 (0)