mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-26 20:24:42 +01:00
Add partial support for Media Queries Level 4 (#1749)
See sass/sass#2538 See #1728
This commit is contained in:
parent
0d4c0d0365
commit
eeedebcee5
@ -8,6 +8,14 @@
|
||||
|
||||
See https://sass-lang.com/d/bogus-combinators for more details.
|
||||
|
||||
* Add partial support for new media query syntax from Media Queries Level 4. The
|
||||
only exception are logical operations nested within parentheses, as these were
|
||||
previously interpreted differently as SassScript expressions.
|
||||
|
||||
A parenthesized media condition that begins with `not` or an opening
|
||||
parenthesis now produces a deprecation warning. In a future release, these
|
||||
will be interpreted as plain CSS instead.
|
||||
|
||||
* Deprecate passing non-`deg` units to `color.hwb()`'s `$hue` argument.
|
||||
|
||||
### Dart API
|
||||
|
@ -28,7 +28,8 @@ export 'src/exception.dart' show SassException;
|
||||
export 'src/importer.dart';
|
||||
export 'src/logger.dart';
|
||||
export 'src/syntax.dart';
|
||||
export 'src/value.dart' hide ColorFormat, SassApiColor, SassApiValue, SpanColorFormat;
|
||||
export 'src/value.dart'
|
||||
hide ColorFormat, SassApiColor, SassApiValue, SpanColorFormat;
|
||||
export 'src/visitor/serialize.dart' show OutputStyle;
|
||||
export 'src/evaluation_context.dart' show warn;
|
||||
|
||||
|
@ -15,14 +15,24 @@ class CssMediaQuery {
|
||||
|
||||
/// The media type, for example "screen" or "print".
|
||||
///
|
||||
/// This may be `null`. If so, [features] will not be empty.
|
||||
/// This may be `null`. If so, [conditions] will not be empty.
|
||||
final String? type;
|
||||
|
||||
/// Feature queries, including parentheses.
|
||||
final List<String> features;
|
||||
/// Whether [conditions] is a conjunction or a disjunction.
|
||||
///
|
||||
/// In other words, if this is `true this query matches when _all_
|
||||
/// [conditions] are met, and if it's `false` this query matches when _any_
|
||||
/// condition in [conditions] is met.
|
||||
///
|
||||
/// If this is [false], [modifier] and [type] will both be `null`.
|
||||
final bool conjunction;
|
||||
|
||||
/// Whether this media query only specifies features.
|
||||
bool get isCondition => modifier == null && type == null;
|
||||
/// Media conditions, including parentheses.
|
||||
///
|
||||
/// This is anything that can appear in the [`<media-in-parens>`] production.
|
||||
///
|
||||
/// [`<media-in-parens>`]: https://drafts.csswg.org/mediaqueries-4/#typedef-media-in-parens
|
||||
final List<String> conditions;
|
||||
|
||||
/// Whether this media query matches all media types.
|
||||
bool get matchesAllTypes => type == null || equalsIgnoreCase(type, 'all');
|
||||
@ -36,47 +46,67 @@ class CssMediaQuery {
|
||||
{Object? url, Logger? logger}) =>
|
||||
MediaQueryParser(contents, url: url, logger: logger).parse();
|
||||
|
||||
/// Creates a media query specifies a type and, optionally, features.
|
||||
CssMediaQuery(this.type, {this.modifier, Iterable<String>? features})
|
||||
: features = features == null ? const [] : List.unmodifiable(features);
|
||||
/// Creates a media query specifies a type and, optionally, conditions.
|
||||
///
|
||||
/// This always sets [conjunction] to `true`.
|
||||
CssMediaQuery.type(this.type, {this.modifier, Iterable<String>? conditions})
|
||||
: conjunction = true,
|
||||
conditions =
|
||||
conditions == null ? const [] : List.unmodifiable(conditions);
|
||||
|
||||
/// Creates a media query that only specifies features.
|
||||
CssMediaQuery.condition(Iterable<String> features)
|
||||
/// Creates a media query that matches [conditions] according to
|
||||
/// [conjunction].
|
||||
///
|
||||
/// The [conjunction] argument may not be null if [conditions] is longer than
|
||||
/// a single element.
|
||||
CssMediaQuery.condition(Iterable<String> conditions, {bool? conjunction})
|
||||
: modifier = null,
|
||||
type = null,
|
||||
features = List.unmodifiable(features);
|
||||
conjunction = conjunction ?? true,
|
||||
conditions = List.unmodifiable(conditions) {
|
||||
if (this.conditions.length > 1 && conjunction == null) {
|
||||
throw ArgumentError(
|
||||
"If conditions is longer than one element, conjunction may not be "
|
||||
"null.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges this with [other] to return a query that matches the intersection
|
||||
/// of both inputs.
|
||||
MediaQueryMergeResult merge(CssMediaQuery other) {
|
||||
if (!conjunction || !other.conjunction) {
|
||||
return MediaQueryMergeResult.unrepresentable;
|
||||
}
|
||||
|
||||
var ourModifier = this.modifier?.toLowerCase();
|
||||
var ourType = this.type?.toLowerCase();
|
||||
var theirModifier = other.modifier?.toLowerCase();
|
||||
var theirType = other.type?.toLowerCase();
|
||||
|
||||
if (ourType == null && theirType == null) {
|
||||
return MediaQuerySuccessfulMergeResult._(
|
||||
CssMediaQuery.condition([...this.features, ...other.features]));
|
||||
return MediaQuerySuccessfulMergeResult._(CssMediaQuery.condition(
|
||||
[...this.conditions, ...other.conditions],
|
||||
conjunction: true));
|
||||
}
|
||||
|
||||
String? modifier;
|
||||
String? type;
|
||||
List<String> features;
|
||||
List<String> conditions;
|
||||
if ((ourModifier == 'not') != (theirModifier == 'not')) {
|
||||
if (ourType == theirType) {
|
||||
var negativeFeatures =
|
||||
ourModifier == 'not' ? this.features : other.features;
|
||||
var positiveFeatures =
|
||||
ourModifier == 'not' ? other.features : this.features;
|
||||
var negativeConditions =
|
||||
ourModifier == 'not' ? this.conditions : other.conditions;
|
||||
var positiveConditions =
|
||||
ourModifier == 'not' ? other.conditions : this.conditions;
|
||||
|
||||
// If the negative features are a subset of the positive features, the
|
||||
// If the negative conditions are a subset of the positive conditions, the
|
||||
// query is empty. For example, `not screen and (color)` has no
|
||||
// intersection with `screen and (color) and (grid)`.
|
||||
//
|
||||
// However, `not screen and (color)` *does* intersect with `screen and
|
||||
// (grid)`, because it means `not (screen and (color))` and so it allows
|
||||
// a screen with no color but with a grid.
|
||||
if (negativeFeatures.every(positiveFeatures.contains)) {
|
||||
if (negativeConditions.every(positiveConditions.contains)) {
|
||||
return MediaQueryMergeResult.empty;
|
||||
} else {
|
||||
return MediaQueryMergeResult.unrepresentable;
|
||||
@ -88,30 +118,30 @@ class CssMediaQuery {
|
||||
if (ourModifier == 'not') {
|
||||
modifier = theirModifier;
|
||||
type = theirType;
|
||||
features = other.features;
|
||||
conditions = other.conditions;
|
||||
} else {
|
||||
modifier = ourModifier;
|
||||
type = ourType;
|
||||
features = this.features;
|
||||
conditions = this.conditions;
|
||||
}
|
||||
} else if (ourModifier == 'not') {
|
||||
assert(theirModifier == 'not');
|
||||
// CSS has no way of representing "neither screen nor print".
|
||||
if (ourType != theirType) return MediaQueryMergeResult.unrepresentable;
|
||||
|
||||
var moreFeatures = this.features.length > other.features.length
|
||||
? this.features
|
||||
: other.features;
|
||||
var fewerFeatures = this.features.length > other.features.length
|
||||
? other.features
|
||||
: this.features;
|
||||
var moreConditions = this.conditions.length > other.conditions.length
|
||||
? this.conditions
|
||||
: other.conditions;
|
||||
var fewerConditions = this.conditions.length > other.conditions.length
|
||||
? other.conditions
|
||||
: this.conditions;
|
||||
|
||||
// If one set of features is a superset of the other, use those features
|
||||
// If one set of conditions is a superset of the other, use those conditions
|
||||
// because they're strictly narrower.
|
||||
if (fewerFeatures.every(moreFeatures.contains)) {
|
||||
if (fewerConditions.every(moreConditions.contains)) {
|
||||
modifier = ourModifier; // "not"
|
||||
type = ourType;
|
||||
features = moreFeatures;
|
||||
conditions = moreConditions;
|
||||
} else {
|
||||
// Otherwise, there's no way to represent the intersection.
|
||||
return MediaQueryMergeResult.unrepresentable;
|
||||
@ -121,41 +151,41 @@ class CssMediaQuery {
|
||||
// Omit the type if either input query did, since that indicates that they
|
||||
// aren't targeting a browser that requires "all and".
|
||||
type = (other.matchesAllTypes && ourType == null) ? null : theirType;
|
||||
features = [...this.features, ...other.features];
|
||||
conditions = [...this.conditions, ...other.conditions];
|
||||
} else if (other.matchesAllTypes) {
|
||||
modifier = ourModifier;
|
||||
type = ourType;
|
||||
features = [...this.features, ...other.features];
|
||||
conditions = [...this.conditions, ...other.conditions];
|
||||
} else if (ourType != theirType) {
|
||||
return MediaQueryMergeResult.empty;
|
||||
} else {
|
||||
modifier = ourModifier ?? theirModifier;
|
||||
type = ourType;
|
||||
features = [...this.features, ...other.features];
|
||||
conditions = [...this.conditions, ...other.conditions];
|
||||
}
|
||||
|
||||
return MediaQuerySuccessfulMergeResult._(CssMediaQuery(
|
||||
return MediaQuerySuccessfulMergeResult._(CssMediaQuery.type(
|
||||
type == ourType ? this.type : other.type,
|
||||
modifier: modifier == ourModifier ? this.modifier : other.modifier,
|
||||
features: features));
|
||||
conditions: conditions));
|
||||
}
|
||||
|
||||
bool operator ==(Object other) =>
|
||||
other is CssMediaQuery &&
|
||||
other.modifier == modifier &&
|
||||
other.type == type &&
|
||||
listEquals(other.features, features);
|
||||
listEquals(other.conditions, conditions);
|
||||
|
||||
int get hashCode => modifier.hashCode ^ type.hashCode ^ listHash(features);
|
||||
int get hashCode => modifier.hashCode ^ type.hashCode ^ listHash(conditions);
|
||||
|
||||
String toString() {
|
||||
var buffer = StringBuffer();
|
||||
if (modifier != null) buffer.write("$modifier ");
|
||||
if (type != null) {
|
||||
buffer.write(type);
|
||||
if (features.isNotEmpty) buffer.write(" and ");
|
||||
if (conditions.isNotEmpty) buffer.write(" and ");
|
||||
}
|
||||
buffer.write(features.join(" and "));
|
||||
buffer.write(conditions.join(conjunction ? " and " : " or "));
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ class MediaQueryParser extends Parser {
|
||||
do {
|
||||
whitespace();
|
||||
queries.add(_mediaQuery());
|
||||
whitespace();
|
||||
} while (scanner.scanChar($comma));
|
||||
scanner.expectDone();
|
||||
return queries;
|
||||
@ -29,52 +30,93 @@ class MediaQueryParser extends Parser {
|
||||
/// Consumes a single media query.
|
||||
CssMediaQuery _mediaQuery() {
|
||||
// This is somewhat duplicated in StylesheetParser._mediaQuery.
|
||||
if (scanner.peekChar() == $lparen) {
|
||||
var conditions = [_mediaInParens()];
|
||||
whitespace();
|
||||
|
||||
var conjunction = true;
|
||||
if (scanIdentifier("and")) {
|
||||
expectWhitespace();
|
||||
conditions.addAll(_mediaLogicSequence("and"));
|
||||
} else if (scanIdentifier("or")) {
|
||||
expectWhitespace();
|
||||
conjunction = false;
|
||||
conditions.addAll(_mediaLogicSequence("or"));
|
||||
}
|
||||
|
||||
return CssMediaQuery.condition(conditions, conjunction: conjunction);
|
||||
}
|
||||
|
||||
String? modifier;
|
||||
String? type;
|
||||
if (scanner.peekChar() != $lparen) {
|
||||
var identifier1 = identifier();
|
||||
whitespace();
|
||||
var identifier1 = identifier();
|
||||
|
||||
if (equalsIgnoreCase(identifier1, "not")) {
|
||||
expectWhitespace();
|
||||
if (!lookingAtIdentifier()) {
|
||||
// For example, "@media screen {"
|
||||
return CssMediaQuery(identifier1);
|
||||
}
|
||||
|
||||
var identifier2 = identifier();
|
||||
whitespace();
|
||||
|
||||
if (equalsIgnoreCase(identifier2, "and")) {
|
||||
// For example, "@media screen and ..."
|
||||
type = identifier1;
|
||||
} else {
|
||||
modifier = identifier1;
|
||||
type = identifier2;
|
||||
if (scanIdentifier("and")) {
|
||||
// For example, "@media only screen and ..."
|
||||
whitespace();
|
||||
} else {
|
||||
// For example, "@media only screen {"
|
||||
return CssMediaQuery(type, modifier: modifier);
|
||||
}
|
||||
// For example, "@media not (...) {"
|
||||
return CssMediaQuery.condition(["(not ${_mediaInParens()})"]);
|
||||
}
|
||||
}
|
||||
|
||||
// We've consumed either `IDENTIFIER "and"`, `IDENTIFIER IDENTIFIER "and"`,
|
||||
// or no text.
|
||||
whitespace();
|
||||
if (!lookingAtIdentifier()) {
|
||||
// For example, "@media screen {"
|
||||
return CssMediaQuery.type(identifier1);
|
||||
}
|
||||
|
||||
var features = <String>[];
|
||||
do {
|
||||
whitespace();
|
||||
scanner.expectChar($lparen);
|
||||
features.add("(${declarationValue()})");
|
||||
scanner.expectChar($rparen);
|
||||
whitespace();
|
||||
} while (scanIdentifier("and"));
|
||||
var identifier2 = identifier();
|
||||
|
||||
if (type == null) {
|
||||
return CssMediaQuery.condition(features);
|
||||
if (equalsIgnoreCase(identifier2, "and")) {
|
||||
expectWhitespace();
|
||||
// For example, "@media screen and ..."
|
||||
type = identifier1;
|
||||
} else {
|
||||
return CssMediaQuery(type, modifier: modifier, features: features);
|
||||
whitespace();
|
||||
modifier = identifier1;
|
||||
type = identifier2;
|
||||
if (scanIdentifier("and")) {
|
||||
// For example, "@media only screen and ..."
|
||||
expectWhitespace();
|
||||
} else {
|
||||
// For example, "@media only screen {"
|
||||
return CssMediaQuery.type(type, modifier: modifier);
|
||||
}
|
||||
}
|
||||
|
||||
// We've consumed either `IDENTIFIER "and"` or
|
||||
// `IDENTIFIER IDENTIFIER "and"`.
|
||||
|
||||
if (scanIdentifier("not")) {
|
||||
// For example, "@media screen and not (...) {"
|
||||
expectWhitespace();
|
||||
return CssMediaQuery.type(type,
|
||||
modifier: modifier, conditions: ["(not ${_mediaInParens()})"]);
|
||||
}
|
||||
|
||||
return CssMediaQuery.type(type,
|
||||
modifier: modifier, conditions: _mediaLogicSequence("and"));
|
||||
}
|
||||
|
||||
/// Consumes one or more `<media-in-parens>` expressions separated by
|
||||
/// [operator] and returns them.
|
||||
List<String> _mediaLogicSequence(String operator) {
|
||||
var result = <String>[];
|
||||
while (true) {
|
||||
result.add(_mediaInParens());
|
||||
whitespace();
|
||||
|
||||
if (!scanIdentifier(operator)) return result;
|
||||
expectWhitespace();
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes a `<media-in-parens>` expression and returns it, parentheses
|
||||
/// included.
|
||||
String _mediaInParens() {
|
||||
scanner.expectChar($lparen, name: "media condition in parentheses");
|
||||
var result = "(${declarationValue()})";
|
||||
scanner.expectChar($rparen);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,17 @@ class Parser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [whitespace], but throws an error if no whitespace is consumed.
|
||||
@protected
|
||||
void expectWhitespace() {
|
||||
if (scanner.isDone ||
|
||||
!(isWhitespace(scanner.peekChar()) || scanComment())) {
|
||||
scanner.error("Expected whitespace.");
|
||||
}
|
||||
|
||||
whitespace();
|
||||
}
|
||||
|
||||
/// Consumes and ignores a silent (Sass-style) comment.
|
||||
@protected
|
||||
void silentComment() {
|
||||
@ -591,15 +602,39 @@ class Parser {
|
||||
if (!lookingAtIdentifier()) return false;
|
||||
|
||||
var start = scanner.state;
|
||||
for (var letter in text.codeUnits) {
|
||||
if (scanIdentChar(letter, caseSensitive: caseSensitive)) continue;
|
||||
if (_consumeIdentifier(text, caseSensitive) && !lookingAtIdentifierBody()) {
|
||||
return true;
|
||||
} else {
|
||||
scanner.state = start;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lookingAtIdentifierBody()) return true;
|
||||
/// Returns whether an identifier whose name exactly matches [text] is at the
|
||||
/// current scanner position.
|
||||
///
|
||||
/// This doesn't move the scan pointer forward
|
||||
@protected
|
||||
bool matchesIdentifier(String text, {bool caseSensitive = false}) {
|
||||
if (!lookingAtIdentifier()) return false;
|
||||
|
||||
var start = scanner.state;
|
||||
var result =
|
||||
_consumeIdentifier(text, caseSensitive) && !lookingAtIdentifierBody();
|
||||
scanner.state = start;
|
||||
return false;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Consumes [text] as an identifer, but doesn't verify whether there's
|
||||
/// additional identifier text afterwards.
|
||||
///
|
||||
/// Returns `true` if the full [text] is consumed and `false` otherwise, but
|
||||
/// doesn't reset the scan pointer.
|
||||
bool _consumeIdentifier(String text, bool caseSensitive) {
|
||||
for (var letter in text.codeUnits) {
|
||||
if (!scanIdentChar(letter, caseSensitive: caseSensitive)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Consumes an identifier and asserts that its name exactly matches [text].
|
||||
|
@ -3449,6 +3449,7 @@ abstract class StylesheetParser extends Parser {
|
||||
while (true) {
|
||||
whitespace();
|
||||
_mediaQuery(buffer);
|
||||
whitespace();
|
||||
if (!scanner.scanChar($comma)) break;
|
||||
buffer.writeCharCode($comma);
|
||||
buffer.writeCharCode($space);
|
||||
@ -3459,61 +3460,127 @@ abstract class StylesheetParser extends Parser {
|
||||
/// Consumes a single media query.
|
||||
void _mediaQuery(InterpolationBuffer buffer) {
|
||||
// This is somewhat duplicated in MediaQueryParser._mediaQuery.
|
||||
if (scanner.peekChar() != $lparen) {
|
||||
buffer.addInterpolation(interpolatedIdentifier());
|
||||
if (scanner.peekChar() == $lparen) {
|
||||
_mediaInParens(buffer);
|
||||
whitespace();
|
||||
|
||||
if (!_lookingAtInterpolatedIdentifier()) {
|
||||
// For example, "@media screen {".
|
||||
return;
|
||||
if (scanIdentifier("and")) {
|
||||
buffer.write(" and ");
|
||||
expectWhitespace();
|
||||
_mediaLogicSequence(buffer, "and");
|
||||
} else if (scanIdentifier("or")) {
|
||||
buffer.write(" or ");
|
||||
expectWhitespace();
|
||||
_mediaLogicSequence(buffer, "or");
|
||||
}
|
||||
|
||||
buffer.writeCharCode($space);
|
||||
var identifier = interpolatedIdentifier();
|
||||
whitespace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (equalsIgnoreCase(identifier.asPlain, "and")) {
|
||||
// For example, "@media screen and ..."
|
||||
var identifier1 = interpolatedIdentifier();
|
||||
if (equalsIgnoreCase(identifier1.asPlain, "not")) {
|
||||
// For example, "@media not (...) {"
|
||||
expectWhitespace();
|
||||
|
||||
if (!_lookingAtInterpolatedIdentifier()) {
|
||||
buffer.write("not ");
|
||||
_mediaOrInterp(buffer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
whitespace();
|
||||
buffer.addInterpolation(identifier1);
|
||||
if (!_lookingAtInterpolatedIdentifier()) {
|
||||
// For example, "@media screen {".
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.writeCharCode($space);
|
||||
var identifier2 = interpolatedIdentifier();
|
||||
|
||||
if (equalsIgnoreCase(identifier2.asPlain, "and")) {
|
||||
expectWhitespace();
|
||||
// For example, "@media screen and ..."
|
||||
buffer.write(" and ");
|
||||
} else {
|
||||
whitespace();
|
||||
buffer.addInterpolation(identifier2);
|
||||
if (scanIdentifier("and")) {
|
||||
// For example, "@media only screen and ..."
|
||||
expectWhitespace();
|
||||
buffer.write(" and ");
|
||||
} else {
|
||||
buffer.addInterpolation(identifier);
|
||||
if (scanIdentifier("and")) {
|
||||
// For example, "@media only screen and ..."
|
||||
whitespace();
|
||||
buffer.write(" and ");
|
||||
} else {
|
||||
// For example, "@media only screen {"
|
||||
return;
|
||||
}
|
||||
// For example, "@media only screen {"
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We've consumed either `IDENTIFIER "and"` or
|
||||
// `IDENTIFIER IDENTIFIER "and"`.
|
||||
|
||||
if (scanIdentifier("not")) {
|
||||
// For example, "@media screen and not (...) {"
|
||||
expectWhitespace();
|
||||
buffer.write("not ");
|
||||
_mediaOrInterp(buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
_mediaLogicSequence(buffer, "and");
|
||||
return;
|
||||
}
|
||||
|
||||
/// Consumes one or more `MediaOrInterp` expressions separated by [operator]
|
||||
/// and writes them to [buffer].
|
||||
void _mediaLogicSequence(InterpolationBuffer buffer, String operator) {
|
||||
while (true) {
|
||||
_mediaOrInterp(buffer);
|
||||
whitespace();
|
||||
buffer.addInterpolation(_mediaFeature());
|
||||
whitespace();
|
||||
if (!scanIdentifier("and")) break;
|
||||
buffer.write(" and ");
|
||||
|
||||
if (!scanIdentifier(operator)) return;
|
||||
expectWhitespace();
|
||||
|
||||
buffer.writeCharCode($space);
|
||||
buffer.write(operator);
|
||||
buffer.writeCharCode($space);
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes a media query feature.
|
||||
Interpolation _mediaFeature() {
|
||||
/// Consumes a `MediaOrInterp` expression and writes it to [buffer].
|
||||
void _mediaOrInterp(InterpolationBuffer buffer) {
|
||||
if (scanner.peekChar() == $hash) {
|
||||
var interpolation = singleInterpolation();
|
||||
return Interpolation([interpolation], interpolation.span);
|
||||
buffer
|
||||
.addInterpolation(Interpolation([interpolation], interpolation.span));
|
||||
} else {
|
||||
_mediaInParens(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
var start = scanner.state;
|
||||
var buffer = InterpolationBuffer();
|
||||
scanner.expectChar($lparen);
|
||||
/// Consumes a `MediaInParens` expression and writes it to [buffer].
|
||||
void _mediaInParens(InterpolationBuffer buffer) {
|
||||
scanner.expectChar($lparen, name: "media condition in parentheses");
|
||||
buffer.writeCharCode($lparen);
|
||||
whitespace();
|
||||
|
||||
buffer.add(_expressionUntilComparison());
|
||||
var needsParenDeprecation = scanner.peekChar() == $lparen;
|
||||
var needsNotDeprecation = matchesIdentifier("not");
|
||||
|
||||
var expression = _expressionUntilComparison();
|
||||
if (needsParenDeprecation || needsNotDeprecation) {
|
||||
logger.warn(
|
||||
'Starting a @media query with "${needsParenDeprecation ? '(' : 'not'}" '
|
||||
"is deprecated because it conflicts with official CSS syntax.\n"
|
||||
"\n"
|
||||
"To preserve existing behavior: #{$expression}\n"
|
||||
'To migrate to new behavior: #{"$expression"}\n'
|
||||
"\n"
|
||||
"For details, see https://sass-lang.com/d/media-logic",
|
||||
span: expression.span,
|
||||
deprecation: true);
|
||||
}
|
||||
|
||||
buffer.add(expression);
|
||||
if (scanner.scanChar($colon)) {
|
||||
whitespace();
|
||||
buffer.writeCharCode($colon);
|
||||
@ -3549,8 +3616,6 @@ abstract class StylesheetParser extends Parser {
|
||||
scanner.expectChar($rparen);
|
||||
whitespace();
|
||||
buffer.writeCharCode($rparen);
|
||||
|
||||
return buffer.interpolation(scanner.spanFrom(start));
|
||||
}
|
||||
|
||||
/// Consumes an expression until it reaches a top-level `<`, `>`, or a `=`
|
||||
|
@ -221,7 +221,12 @@ class _SerializeVisitor
|
||||
_for(node, () {
|
||||
_buffer.write("@media");
|
||||
|
||||
if (!_isCompressed || !node.queries.first.isCondition) {
|
||||
var firstQuery = node.queries.first;
|
||||
if (!_isCompressed ||
|
||||
firstQuery.modifier != null ||
|
||||
firstQuery.type != null ||
|
||||
(firstQuery.conditions.length == 1 &&
|
||||
firstQuery.conditions.first.startsWith("(not "))) {
|
||||
_buffer.writeCharCode($space);
|
||||
}
|
||||
|
||||
@ -287,13 +292,21 @@ class _SerializeVisitor
|
||||
|
||||
if (query.type != null) {
|
||||
_buffer.write(query.type);
|
||||
if (query.features.isNotEmpty) {
|
||||
if (query.conditions.isNotEmpty) {
|
||||
_buffer.write(" and ");
|
||||
}
|
||||
}
|
||||
|
||||
_writeBetween(
|
||||
query.features, _isCompressed ? "and " : " and ", _buffer.write);
|
||||
if (query.conditions.length == 1 &&
|
||||
query.conditions.first.startsWith("(not ")) {
|
||||
_buffer.write("not ");
|
||||
var condition = query.conditions.first;
|
||||
_buffer.write(condition.substring("(not ".length, condition.length - 1));
|
||||
} else {
|
||||
var operator = query.conjunction ? "and" : "or";
|
||||
_writeBetween(query.conditions,
|
||||
_isCompressed ? "$operator " : " $operator ", _buffer.write);
|
||||
}
|
||||
}
|
||||
|
||||
void visitCssStyleRule(CssStyleRule node) {
|
||||
|
@ -1,4 +1,19 @@
|
||||
## 1.1.0
|
||||
## 2.0.0
|
||||
|
||||
* Refactor the `CssMediaQuery` API to support new logical operators:
|
||||
|
||||
* Rename the `features` field to `conditions`, to reflect the fact that it can
|
||||
contain more than just the `<media-feature>` production.
|
||||
|
||||
* Add a `conjunction` field to track whether `conditions` are matched
|
||||
conjunctively or disjunctively.
|
||||
|
||||
* Rename the default constructor to `CssMediaQuery.type()` to reflect the fact
|
||||
that it's no longer by far the most commonly used form of media query.
|
||||
|
||||
* Add a required `conjunction` argument to `CssMediaQuery.condition()`.
|
||||
|
||||
* Delete the `isCondition` getter.
|
||||
|
||||
* Provide access to Sass's selector AST, including the following classes:
|
||||
`Selector`, `ListSelector`, `ComplexSelector`, `ComplexSelectorComponent`,
|
||||
|
@ -2,7 +2,7 @@ name: sass_api
|
||||
# Note: Every time we add a new Sass AST node, we need to bump the *major*
|
||||
# version because it's a breaking change for anyone who's implementing the
|
||||
# visitor interface(s).
|
||||
version: 1.1.0-dev
|
||||
version: 2.0.0-dev
|
||||
description: Additional APIs for Dart Sass.
|
||||
homepage: https://github.com/sass/dart-sass
|
||||
|
||||
|
@ -209,15 +209,36 @@ void main() {
|
||||
|
||||
// Removing whitespace after "and", "or", or "not" is forbidden because it
|
||||
// would cause it to parse as a function token.
|
||||
test('removes whitespace before "and" when possible', () {
|
||||
expect(
|
||||
_compile("""
|
||||
@media screen and (min-width: 900px) and (max-width: 100px) {
|
||||
a {b: c}
|
||||
}
|
||||
"""),
|
||||
equals("@media screen and (min-width: 900px)and (max-width: 100px)"
|
||||
"{a{b:c}}"));
|
||||
group('preserves whitespace when necessary', () {
|
||||
test('around "and"', () {
|
||||
expect(
|
||||
_compile("""
|
||||
@media screen and (min-width: 900px) and (max-width: 100px) {
|
||||
a {b: c}
|
||||
}
|
||||
"""),
|
||||
equals("@media screen and (min-width: 900px)and (max-width: 100px)"
|
||||
"{a{b:c}}"));
|
||||
});
|
||||
|
||||
test('around "or"', () {
|
||||
expect(
|
||||
_compile("""
|
||||
@media (min-width: 900px) or (max-width: 100px) or (print) {
|
||||
a {b: c}
|
||||
}
|
||||
"""),
|
||||
equals("@media(min-width: 900px)or (max-width: 100px)or (print)"
|
||||
"{a{b:c}}"));
|
||||
});
|
||||
|
||||
test('after "not"', () {
|
||||
expect(_compile("""
|
||||
@media not (min-width: 900px) {
|
||||
a {b: c}
|
||||
}
|
||||
"""), equals("@media not (min-width: 900px){a{b:c}}"));
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves whitespace around the modifier", () {
|
||||
|
Loading…
Reference in New Issue
Block a user