@@ -138,12 +138,13 @@ class SelectorEvaluator extends Visitor {
138
138
case 'root' :
139
139
// TODO(jmesserly): fix when we have a .ownerDocument pointer
140
140
// return _element == _element.ownerDocument.rootElement;
141
- return _element.localName == 'html' && _element.parentNode == null ;
141
+ return _element.localName == 'html' &&
142
+ (_element.parentNode == null || _element.parentNode is Document );
142
143
143
144
// http://dev.w3.org/csswg/selectors-4/#the-empty-pseudo
144
145
case 'empty' :
145
146
return _element.nodes
146
- .any ((n) => ! (n is Element || n is Text && n.text.isNotEmpty));
147
+ .every ((n) => ! (n is Element || n is Text && n.text.isNotEmpty));
147
148
148
149
// http://dev.w3.org/csswg/selectors-4/#the-blank-pseudo
149
150
case 'blank' :
@@ -158,6 +159,39 @@ class SelectorEvaluator extends Visitor {
158
159
case 'last-child' :
159
160
return _element.nextElementSibling == null ;
160
161
162
+ //http://drafts.csswg.org/selectors-4/#first-of-type-pseudo
163
+ //http://drafts.csswg.org/selectors-4/#last-of-type-pseudo
164
+ //http://drafts.csswg.org/selectors-4/#only-of-type-pseudo
165
+ case 'first-of-type' :
166
+ case 'last-of-type' :
167
+ case 'only-of-type' :
168
+ var parent = _element.parentNode;
169
+ if (parent != null ) {
170
+ var children = parent.children.where ((Element el) {
171
+ return el.localName == _element.localName;
172
+ }).toList ();
173
+
174
+ var index = children.indexOf (_element);
175
+ var isFirst = index == 0 ;
176
+ var isLast = index == children.length - 1 ;
177
+
178
+ if (isFirst && selector.name == 'first-of-type' ) {
179
+ return true ;
180
+ }
181
+
182
+ if (isLast && selector.name == 'last-of-type' ) {
183
+ return true ;
184
+ }
185
+
186
+ if (isFirst && isLast && selector.name == 'only-of-type' ) {
187
+ return true ;
188
+ }
189
+
190
+ return false ;
191
+ }
192
+
193
+ break ;
194
+
161
195
// http://dev.w3.org/csswg/selectors-4/#the-only-child-pseudo
162
196
case 'only-child' :
163
197
return _element.previousElementSibling == null &&
@@ -167,9 +201,48 @@ class SelectorEvaluator extends Visitor {
167
201
case 'link' :
168
202
return _element.attributes['href' ] != null ;
169
203
204
+ //https://drafts.csswg.org/selectors-4/#checked-pseudo
205
+ //https://drafts.csswg.org/selectors-4/#enabled-pseudo
206
+ //https://drafts.csswg.org/selectors-4/#disabled-pseudo
207
+ case 'enabled' :
208
+ case 'disabled' :
209
+ var isDisabled = selector.name == 'disabled' ;
210
+ var interactableTypes = [
211
+ 'button' ,
212
+ 'input' ,
213
+ 'select' ,
214
+ 'textarea' ,
215
+ 'optgroup' ,
216
+ 'option' ,
217
+ 'fieldset'
218
+ ];
219
+ if (interactableTypes.contains (_element.localName)) {
220
+ var disabled = _element.attributes['disabled' ];
221
+
222
+ if (disabled != null ) {
223
+ return isDisabled;
224
+ }
225
+ }
226
+
227
+ return ! isDisabled;
228
+
229
+ //https://drafts.csswg.org/selectors-4/#checked-pseudo
230
+ case 'checked' :
231
+ var isCheckable = _element.localName == 'option' ||
232
+ (_element.localName == 'input' &&
233
+ (_element.attributes['type' ] == 'checkbox' ||
234
+ _element.attributes['type' ] == 'radio' ));
235
+
236
+ if (isCheckable) {
237
+ return _element.attributes['checked' ] != null ;
238
+ }
239
+ return false ;
240
+
170
241
case 'visited' :
242
+ case 'target' :
171
243
// Always return false since we aren't a browser. This is allowed per:
172
244
// http://dev.w3.org/csswg/selectors-4/#visited-pseudo
245
+ // http://drafts.csswg.org/selectors-4/#target-pseudo
173
246
return false ;
174
247
}
175
248
@@ -201,21 +274,125 @@ class SelectorEvaluator extends Visitor {
201
274
bool visitPseudoElementFunctionSelector (PseudoElementFunctionSelector s) =>
202
275
throw _unimplemented (s);
203
276
277
+ num _countExpressionList (List <Expression > list) {
278
+ Expression first = list[0 ];
279
+ num sum = 0 ;
280
+ num modulus = 1 ;
281
+ if (first is OperatorMinus ) {
282
+ modulus = - 1 ;
283
+ list = list.sublist (1 );
284
+ }
285
+ list.forEach ((Expression item) {
286
+ sum += (item as NumberTerm ).value;
287
+ });
288
+ return sum * modulus;
289
+ }
290
+
291
+ Map <String , num > _parseNthExpressions (List <Expression > exprs) {
292
+ num A ;
293
+ num B = 0 ;
294
+
295
+ if (exprs.isNotEmpty) {
296
+ if (exprs.length == 1 && (exprs[0 ] is LiteralTerm )) {
297
+ LiteralTerm literal = exprs[0 ];
298
+ if (literal is NumberTerm ) {
299
+ B = literal.value;
300
+ } else {
301
+ String value = literal.value.toString ();
302
+ if (value == 'even' ) {
303
+ A = 2 ;
304
+ B = 1 ;
305
+ } else if (value == 'odd' ) {
306
+ A = 2 ;
307
+ B = 0 ;
308
+ } else if (value == 'n' ) {
309
+ A = 1 ;
310
+ B = 0 ;
311
+ } else {
312
+ return null ;
313
+ }
314
+ }
315
+ }
316
+
317
+ List <Expression > bTerms = [];
318
+ List <Expression > aTerms = [];
319
+ var nIndex = exprs.indexWhere ((expr) {
320
+ return (expr is LiteralTerm ) && expr.value.toString () == 'n' ;
321
+ });
322
+
323
+ if (nIndex > - 1 ) {
324
+ bTerms.addAll (exprs.sublist (nIndex + 1 ));
325
+ aTerms.addAll (exprs.sublist (0 , nIndex));
326
+ } else {
327
+ bTerms.addAll (exprs);
328
+ }
329
+
330
+ if (bTerms.isNotEmpty) {
331
+ B = _countExpressionList (bTerms);
332
+ }
333
+
334
+ if (aTerms.isNotEmpty) {
335
+ if (aTerms.length == 1 && aTerms[0 ] is OperatorMinus ) {
336
+ A = - 1 ;
337
+ } else {
338
+ A = _countExpressionList (aTerms);
339
+ }
340
+ }
341
+ }
342
+
343
+ return {'A' : A , 'B' : B };
344
+ }
345
+
204
346
bool visitPseudoClassFunctionSelector (PseudoClassFunctionSelector selector) {
205
347
switch (selector.name) {
206
348
// http://dev.w3.org/csswg/selectors-4/#child-index
207
-
208
349
// http://dev.w3.org/csswg/selectors-4/#the-nth-child-pseudo
350
+
209
351
case 'nth-child' :
210
- // TODO(jmesserly): support An+B syntax too.
211
- var exprs = selector.expression.expressions;
212
- if (exprs.length == 1 && exprs[0 ] is LiteralTerm ) {
213
- LiteralTerm literal = exprs[0 ];
214
- var parent = _element.parentNode;
215
- return parent != null &&
216
- literal.value > 0 &&
217
- parent.nodes.indexOf (_element) == literal.value;
352
+ case 'nth-last-child' :
353
+ case 'nth-of-type' :
354
+ case 'nth-last-of-type' :
355
+ // i = An + B
356
+ var nthData = _parseNthExpressions (selector.expression.expressions);
357
+ if (nthData == null ) {
358
+ break ;
359
+ }
360
+
361
+ var A = nthData['A' ];
362
+ var B = nthData['B' ];
363
+
364
+ var parent = _element.parentNode;
365
+ if (parent != null ) {
366
+ var elIndex;
367
+ var children = parent.children;
368
+
369
+ if (selector.name == 'nth-of-type' ||
370
+ selector.name == 'nth-last-of-type' ) {
371
+ children = children.where ((Element el) {
372
+ return el.localName == _element.localName;
373
+ }).toList ();
374
+ }
375
+
376
+ if (selector.name == 'nth-last-child' ||
377
+ selector.name == 'nth-last-of-type' ) {
378
+ elIndex = children.length - children.indexOf (_element);
379
+ } else {
380
+ elIndex = children.indexOf (_element) + 1 ;
381
+ }
382
+
383
+ if (A == null ) {
384
+ return B > 0 && elIndex == B ;
385
+ } else {
386
+ var divideResult = (elIndex - B ) / A ;
387
+
388
+ if (divideResult >= 1 ) {
389
+ return divideResult % divideResult.ceil () == 0 ;
390
+ } else {
391
+ return divideResult == 0 ;
392
+ }
393
+ }
218
394
}
395
+
219
396
break ;
220
397
221
398
// http://dev.w3.org/csswg/selectors-4/#the-lang-pseudo
0 commit comments