Skip to content
This repository was archived by the owner on Nov 1, 2024. It is now read-only.

support more selector #101

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
build/
packages
.packages
.iml

# Or the files created by dart2js.
*.dart.js
Expand Down
207 changes: 196 additions & 11 deletions lib/src/query_selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class SelectorEvaluator extends Visitor {
combinator = s.combinator;
break;
case TokenKind.COMBINATOR_NONE:
combinator = null;
break;
default:
throw _unsupported(selector);
Expand Down Expand Up @@ -139,12 +140,13 @@ class SelectorEvaluator extends Visitor {
case 'root':
// TODO(jmesserly): fix when we have a .ownerDocument pointer
// return _element == _element.ownerDocument.rootElement;
return _element.localName == 'html' && _element.parentNode == null;
return _element.localName == 'html' &&
(_element.parentNode == null || _element.parentNode is Document);

// http://dev.w3.org/csswg/selectors-4/#the-empty-pseudo
case 'empty':
return _element.nodes
.any((n) => !(n is Element || n is Text && n.text.isNotEmpty));
.every((n) => !(n is Element || n is Text && n.text.isNotEmpty));

// http://dev.w3.org/csswg/selectors-4/#the-blank-pseudo
case 'blank':
Expand All @@ -159,6 +161,39 @@ class SelectorEvaluator extends Visitor {
case 'last-child':
return _element.nextElementSibling == null;

//http://drafts.csswg.org/selectors-4/#first-of-type-pseudo
//http://drafts.csswg.org/selectors-4/#last-of-type-pseudo
//http://drafts.csswg.org/selectors-4/#only-of-type-pseudo
case 'first-of-type':
case 'last-of-type':
case 'only-of-type':
var parent = _element.parentNode;
if (parent != null) {
var children = parent.children.where((Element el) {
return el.localName == _element.localName;
}).toList();

var index = children.indexOf(_element);
var isFirst = index == 0;
var isLast = index == children.length - 1;

if (isFirst && selector.name == 'first-of-type') {
return true;
}

if (isLast && selector.name == 'last-of-type') {
return true;
}

if (isFirst && isLast && selector.name == 'only-of-type') {
return true;
}

return false;
}

break;

// http://dev.w3.org/csswg/selectors-4/#the-only-child-pseudo
case 'only-child':
return _element.previousElementSibling == null &&
Expand All @@ -168,9 +203,48 @@ class SelectorEvaluator extends Visitor {
case 'link':
return _element.attributes['href'] != null;

//https://drafts.csswg.org/selectors-4/#checked-pseudo
//https://drafts.csswg.org/selectors-4/#enabled-pseudo
//https://drafts.csswg.org/selectors-4/#disabled-pseudo
case 'enabled':
case 'disabled':
var isDisabled = selector.name == 'disabled';
var interactableTypes = [
'button',
'input',
'select',
'textarea',
'optgroup',
'option',
'fieldset'
];
if (interactableTypes.contains(_element.localName)) {
var disabled = _element.attributes['disabled'];

if (disabled != null) {
return isDisabled;
}
}

return !isDisabled;

//https://drafts.csswg.org/selectors-4/#checked-pseudo
case 'checked':
var isCheckable = _element.localName == 'option' ||
(_element.localName == 'input' &&
(_element.attributes['type'] == 'checkbox' ||
_element.attributes['type'] == 'radio'));

if (isCheckable) {
return _element.attributes['checked'] != null;
}
return false;

case 'visited':
case 'target':
// Always return false since we aren't a browser. This is allowed per:
// http://dev.w3.org/csswg/selectors-4/#visited-pseudo
// http://drafts.csswg.org/selectors-4/#target-pseudo
return false;
}

Expand Down Expand Up @@ -204,22 +278,133 @@ class SelectorEvaluator extends Visitor {
bool visitPseudoElementFunctionSelector(PseudoElementFunctionSelector s) =>
throw _unimplemented(s);

num _countExpressionList(List<Expression> list) {
Expression first = list[0];
num sum = 0;
num modulus = 1;
if (first is OperatorMinus) {
modulus = -1;
list = list.sublist(1);
}
list.forEach((Expression item) {
sum += (item as NumberTerm).value;
});
return sum * modulus;
}

Map<String, num> _parseNthExpressions(List<Expression> exprs) {
num A;
num B = 0;

if (exprs.isNotEmpty) {
if (exprs.length == 1 && (exprs[0] is LiteralTerm)) {
LiteralTerm literal = exprs[0];
if (literal is NumberTerm) {
B = literal.value;
} else {
String value = literal.value.toString();
if (value == 'even') {
A = 2;
B = 1;
} else if (value == 'odd') {
A = 2;
B = 0;
} else if (value == 'n') {
A = 1;
B = 0;
} else {
return null;
}
}
}

List<Expression> bTerms = [];
List<Expression> aTerms = [];
var nIndex = exprs.indexWhere((expr) {
return (expr is LiteralTerm) && expr.value.toString() == 'n';
});

if (nIndex > -1) {
bTerms.addAll(exprs.sublist(nIndex + 1));
aTerms.addAll(exprs.sublist(0, nIndex));
} else {
bTerms.addAll(exprs);
}

if (bTerms.isNotEmpty) {
B = _countExpressionList(bTerms);
}

if (aTerms.isNotEmpty) {
if (aTerms.length == 1 && aTerms[0] is OperatorMinus) {
A = -1;
} else {
A = _countExpressionList(aTerms);
}
} else {
if (nIndex == 0) {
A = 1;
}
}
}

return {'A': A, 'B': B};
}

@override
bool visitPseudoClassFunctionSelector(PseudoClassFunctionSelector selector) {
switch (selector.name) {
// http://dev.w3.org/csswg/selectors-4/#child-index

// http://dev.w3.org/csswg/selectors-4/#the-nth-child-pseudo

case 'nth-child':
// TODO(jmesserly): support An+B syntax too.
final exprs = selector.expression.expressions;
if (exprs.length == 1 && exprs[0] is LiteralTerm) {
final literal = exprs[0] as LiteralTerm;
final parent = _element.parentNode;
return parent != null &&
(literal.value as num) > 0 &&
parent.nodes.indexOf(_element) == literal.value;
case 'nth-last-child':
case 'nth-of-type':
case 'nth-last-of-type':
// i = An + B
var nthData = _parseNthExpressions(selector.expression.expressions);
if (nthData == null) {
break;
}

var A = nthData['A'];
var B = nthData['B'];

var parent = _element.parentNode;

if (parent != null) {
var elIndex;
var children = parent.children;

if (selector.name == 'nth-of-type' ||
selector.name == 'nth-last-of-type') {
children = children.where((Element el) {
return el.localName == _element.localName;
}).toList();
}

if (selector.name == 'nth-last-child' ||
selector.name == 'nth-last-of-type') {
elIndex = children.length - children.indexOf(_element);
} else {
elIndex = children.indexOf(_element) + 1;
}

if (A == null) {
return B > 0 && elIndex == B;
} else {
var divideResult = (elIndex - B) / A;

if (divideResult >= 1) {
return divideResult % divideResult.ceil() == 0;
} else {
return divideResult == 0;
}
}
} else {
return false;
}

break;

// http://dev.w3.org/csswg/selectors-4/#the-lang-pseudo
Expand Down
2 changes: 1 addition & 1 deletion test/selectors/level1-content.html
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@

<div id="pseudo-nth">
<table id="pseudo-nth-table1">
<tr id="pseudo-nth-tr1"><td id="pseudo-nth-td1"></td><td id="pseudo-nth-td2"></td><td id="pseudo-nth-td3"></td><td id="pseudo-nth-td4"></td><td id="pseudo-nth--td5"></td><td id="pseudo-nth-td6"></td></tr>
<tr id="pseudo-nth-tr1"><td id="pseudo-nth-td1"></td><td id="pseudo-nth-td2"></td><td id="pseudo-nth-td3"></td><td id="pseudo-nth-td4"></td><td id="pseudo-nth-td5"></td><td id="pseudo-nth-td6"></td></tr>
<tr id="pseudo-nth-tr2"><td id="pseudo-nth-td7"></td><td id="pseudo-nth-td8"></td><td id="pseudo-nth-td9"></td><td id="pseudo-nth-td10"></td><td id="pseudo-nth-td11"></td><td id="pseudo-nth-td12"></td></tr>
<tr id="pseudo-nth-tr3"><td id="pseudo-nth-td13"></td><td id="pseudo-nth-td14"></td><td id="pseudo-nth-td15"></td><td id="pseudo-nth-td16"></td><td id="pseudo-nth-td17"></td><td id="pseudo-nth-td18"></td></tr>
</table>
Expand Down
5 changes: 3 additions & 2 deletions test/selectors/level1_baseline_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ Document getTestContentDocument() {
return parse(File(testPath).readAsStringSync());
}

var testType = testQsaBaseline; // Only run baseline tests.
var docType = 'html'; // Only run tests suitable for HTML

var testType = testQsaBaseline | testQsaAdditional; // Only run baseline tests.
var docType = "html"; // Only run tests suitable for HTML

void main() {
/*
Expand Down
49 changes: 36 additions & 13 deletions test/selectors/selectors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -762,11 +762,35 @@ var validSelectors = [
'level': 3,
'testType': testQsaAdditional | testMatchBaseline
},
{
'name':":nth-child selector, matching every child element, starting from the seven",
'selector': "#pseudo-nth li:nth-child(n+7)",
'expect': [
"pseudo-nth-li7",
"pseudo-nth-li8",
"pseudo-nth-li9",
"pseudo-nth-li10",
"pseudo-nth-li11",
"pseudo-nth-li12"
],
'level': 3,
'testType': testQsaAdditional | testMatchBaseline
},
{
'name': ":nth-child selector, matching every third em element from the end",
'selector': "#pseudo-nth-p1 em:nth-child(3n)",
'expect': [
"pseudo-nth-em2",
"pseudo-nth-em3",
],
'level': 3,
'testType': testQsaAdditional | testMatchBaseline
},
{
'name':
':nth-child selector, matching every fourth child element, starting from the third',
'selector': '#pseudo-nth-p1 :nth-child(4n-1)',
'expect': ['pseudo-nth-em2', 'pseudo-nth-span3'],
":nth-child selector, matching every fourth child element, starting from the third",
'selector': "#pseudo-nth-p1 :nth-child(4n-1)",
'expect': ["pseudo-nth-em2", "pseudo-nth-span3"],
'level': 3,
'testType': testQsaAdditional | testMatchBaseline
},
Expand Down Expand Up @@ -1095,16 +1119,15 @@ var validSelectors = [
'level': 3,
'testType': testQsaAdditional
},
{
'name':
':target pseudo-class selector, matching the element referenced by the URL fragment identifier',
'selector': ':target',
'expect': ['target'],
'exclude': ['fragment', 'detached'],
'level': 3,
'testType': testQsaAdditional | testMatchBaseline
},

// {
// 'name':
// ":target pseudo-class selector, matching the element referenced by the URL fragment identifier",
// 'selector': ":target",
// 'expect': ["target"],
// 'exclude': ["fragment", "detached"],
// 'level': 3,
// 'testType': testQsaAdditional | testMatchBaseline
// },
// - :lang()
{
'name': ':lang pseudo-class selector, matching inherited language',
Expand Down