-
Notifications
You must be signed in to change notification settings - Fork 347
/
pseudo.dart
221 lines (188 loc) · 6.78 KB
/
pseudo.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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// 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 'package:charcode/charcode.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import '../../utils.dart';
import '../../util/nullable.dart';
import '../../visitor/interface/selector.dart';
import '../selector.dart';
/// A pseudo-class or pseudo-element selector.
///
/// The semantics of a specific pseudo selector depends on its name. Some
/// selectors take arguments, including other selectors. Sass manually encodes
/// logic for each pseudo selector that takes a selector as an argument, to
/// ensure that extension and other selector operations work properly.
///
/// {@category AST}
@sealed
class PseudoSelector extends SimpleSelector {
/// The name of this selector.
final String name;
/// Like [name], but without any vendor prefixes.
///
/// @nodoc
@internal
final String normalizedName;
/// Whether this is a pseudo-class selector.
///
/// This is `true` if and only if [isElement] is `false`.
final bool isClass;
/// Whether this is a pseudo-element selector.
///
/// This is `true` if and only if [isClass] is `false`.
bool get isElement => !isClass;
/// Whether this is syntactically a pseudo-class selector.
///
/// This is the same as [isClass] unless this selector is a pseudo-element
/// that was written syntactically as a pseudo-class (`:before`, `:after`,
/// `:first-line`, or `:first-letter`).
///
/// This is `true` if and only if [isSyntacticElement] is `false`.
final bool isSyntacticClass;
/// Whether this is syntactically a pseudo-element selector.
///
/// This is `true` if and only if [isSyntacticClass] is `false`.
bool get isSyntacticElement => !isSyntacticClass;
/// Whether this is a valid `:host` selector.
///
/// @nodoc
@internal
bool get isHost => isClass && name == 'host';
/// Whether this is a valid `:host-context` selector.
///
/// @nodoc
@internal
bool get isHostContext =>
isClass && name == 'host-context' && selector != null;
/// The non-selector argument passed to this selector.
///
/// This is `null` if there's no argument. If [argument] and [selector] are
/// both non-`null`, the selector follows the argument.
final String? argument;
/// The selector argument passed to this selector.
///
/// This is `null` if there's no selector. If [argument] and [selector] are
/// both non-`null`, the selector follows the argument.
final SelectorList? selector;
late final int specificity = () {
if (isElement) return 1;
var selector = this.selector;
if (selector == null) return super.specificity;
// https://drafts.csswg.org/selectors/#specificity-rules
switch (normalizedName) {
case 'where':
return 0;
case 'is':
case 'not':
case 'has':
case 'matches':
return selector.components
.map((component) => component.specificity)
.max;
case 'nth-child':
case 'nth-last-child':
return super.specificity +
selector.components.map((component) => component.specificity).max;
default:
return super.specificity;
}
}();
PseudoSelector(this.name,
{bool element = false, this.argument, this.selector})
: isClass = !element && !_isFakePseudoElement(name),
isSyntacticClass = !element,
normalizedName = unvendor(name);
/// Returns whether [name] is the name of a pseudo-element that can be written
/// with pseudo-class syntax (`:before`, `:after`, `:first-line`, or
/// `:first-letter`)
static bool _isFakePseudoElement(String name) {
switch (name.codeUnitAt(0)) {
case $a:
case $A:
return equalsIgnoreCase(name, "after");
case $b:
case $B:
return equalsIgnoreCase(name, "before");
case $f:
case $F:
return equalsIgnoreCase(name, "first-line") ||
equalsIgnoreCase(name, "first-letter");
default:
return false;
}
}
/// Returns a new [PseudoSelector] based on this, but with the selector
/// replaced with [selector].
PseudoSelector withSelector(SelectorList selector) => PseudoSelector(name,
element: isElement, argument: argument, selector: selector);
/// @nodoc
@internal
PseudoSelector addSuffix(String suffix) {
if (argument != null || selector != null) super.addSuffix(suffix);
return PseudoSelector(name + suffix, element: isElement);
}
/// @nodoc
@internal
List<SimpleSelector>? unify(List<SimpleSelector> compound) {
if (name == 'host' || name == 'host-context') {
if (!compound.every((simple) =>
simple is PseudoSelector &&
(simple.isHost || simple.selector != null))) {
return null;
}
} else if (compound.length == 1) {
var other = compound.first;
if (other is UniversalSelector ||
(other is PseudoSelector && (other.isHost || other.isHostContext))) {
return other.unify([this]);
}
}
if (compound.contains(this)) return compound;
var result = <SimpleSelector>[];
var addedThis = false;
for (var simple in compound) {
if (simple is PseudoSelector && simple.isElement) {
// A given compound selector may only contain one pseudo element. If
// [compound] has a different one than [this], unification fails.
if (isElement) return null;
// Otherwise, this is a pseudo selector and should come before pseudo
// elements.
result.add(this);
addedThis = true;
}
result.add(simple);
}
if (!addedThis) result.add(this);
return result;
}
bool isSuperselector(SimpleSelector other) {
if (super.isSuperselector(other)) return true;
var selector = this.selector;
if (selector == null) return this == other;
if (other is PseudoSelector &&
isElement &&
other.isElement &&
normalizedName == 'slotted' &&
other.name == name) {
return other.selector.andThen(selector.isSuperselector) ?? false;
}
// Fall back to the logic defined in functions.dart, which knows how to
// compare selector pseudoclasses against raw selectors.
return CompoundSelector([this]).isSuperselector(CompoundSelector([other]));
}
T accept<T>(SelectorVisitor<T> visitor) => visitor.visitPseudoSelector(this);
// This intentionally uses identity for the selector list, if one is available.
bool operator ==(Object other) =>
other is PseudoSelector &&
other.name == name &&
other.isClass == isClass &&
other.argument == argument &&
other.selector == selector;
int get hashCode =>
name.hashCode ^
isElement.hashCode ^
argument.hashCode ^
selector.hashCode;
}