-
Notifications
You must be signed in to change notification settings - Fork 347
/
media_query.dart
194 lines (167 loc) · 7.04 KB
/
media_query.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// Copyright 2016 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 '../../logger.dart';
import '../../parse/media_query.dart';
import '../../utils.dart';
/// A plain CSS media query, as used in `@media` and `@import`.
class CssMediaQuery {
/// The modifier, probably either "not" or "only".
///
/// This may be `null` if no modifier is in use.
final String? modifier;
/// The media type, for example "screen" or "print".
///
/// This may be `null`. If so, [features] will not be empty.
final String? type;
/// Feature queries, including parentheses.
final List<String> features;
/// Whether this media query only specifies features.
bool get isCondition => modifier == null && type == null;
/// Whether this media query matches all media types.
bool get matchesAllTypes => type == null || equalsIgnoreCase(type, 'all');
/// Parses a media query from [contents].
///
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
static List<CssMediaQuery> parseList(String contents,
{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 that only specifies features.
CssMediaQuery.condition(Iterable<String> features)
: modifier = null,
type = null,
features = List.unmodifiable(features);
/// Merges this with [other] to return a query that matches the intersection
/// of both inputs.
MediaQueryMergeResult merge(CssMediaQuery other) {
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]));
}
String? modifier;
String? type;
List<String> features;
if ((ourModifier == 'not') != (theirModifier == 'not')) {
if (ourType == theirType) {
var negativeFeatures =
ourModifier == 'not' ? this.features : other.features;
var positiveFeatures =
ourModifier == 'not' ? other.features : this.features;
// If the negative features are a subset of the positive features, 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)) {
return MediaQueryMergeResult.empty;
} else {
return MediaQueryMergeResult.unrepresentable;
}
} else if (matchesAllTypes || other.matchesAllTypes) {
return MediaQueryMergeResult.unrepresentable;
}
if (ourModifier == 'not') {
modifier = theirModifier;
type = theirType;
features = other.features;
} else {
modifier = ourModifier;
type = ourType;
features = this.features;
}
} 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;
// If one set of features is a superset of the other, use those features
// because they're strictly narrower.
if (fewerFeatures.every(moreFeatures.contains)) {
modifier = ourModifier; // "not"
type = ourType;
features = moreFeatures;
} else {
// Otherwise, there's no way to represent the intersection.
return MediaQueryMergeResult.unrepresentable;
}
} else if (matchesAllTypes) {
modifier = theirModifier;
// 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];
} else if (other.matchesAllTypes) {
modifier = ourModifier;
type = ourType;
features = [...this.features, ...other.features];
} else if (ourType != theirType) {
return MediaQueryMergeResult.empty;
} else {
modifier = ourModifier ?? theirModifier;
type = ourType;
features = [...this.features, ...other.features];
}
return MediaQuerySuccessfulMergeResult._(CssMediaQuery(
type == ourType ? this.type : other.type,
modifier: modifier == ourModifier ? this.modifier : other.modifier,
features: features));
}
bool operator ==(Object other) =>
other is CssMediaQuery &&
other.modifier == modifier &&
other.type == type &&
listEquals(other.features, features);
int get hashCode => modifier.hashCode ^ type.hashCode ^ listHash(features);
String toString() {
var buffer = StringBuffer();
if (modifier != null) buffer.write("$modifier ");
if (type != null) {
buffer.write(type);
if (features.isNotEmpty) buffer.write(" and ");
}
buffer.write(features.join(" and "));
return buffer.toString();
}
}
/// The interface of possible return values of [CssMediaQuery.merge].
///
/// This is either the singleton values [empty] or [unrepresentable], or an
/// instance of [MediaQuerySuccessfulMergeResult].
abstract class MediaQueryMergeResult {
/// A singleton value indicating that there are no contexts that match both
/// input queries.
static const empty = _SingletonCssMediaQueryMergeResult("empty");
/// A singleton value indicating that the contexts that match both input
/// queries can't be represented by a Level 3 media query.
static const unrepresentable =
_SingletonCssMediaQueryMergeResult("unrepresentable");
}
/// The subclass [MediaQueryMergeResult] that represents singleton enum values.
class _SingletonCssMediaQueryMergeResult implements MediaQueryMergeResult {
/// The name of the result type.
final String _name;
const _SingletonCssMediaQueryMergeResult(this._name);
String toString() => _name;
}
/// A successful result of [CssMediaQuery.merge].
class MediaQuerySuccessfulMergeResult implements MediaQueryMergeResult {
/// The merged media query.
final CssMediaQuery query;
MediaQuerySuccessfulMergeResult._(this.query);
}