OLD | NEW |
| (Empty) |
1 /// Query selector implementation for our DOM. | |
2 library html.src.query; | |
3 | |
4 import 'package:csslib/parser.dart' as css; | |
5 import 'package:csslib/parser.dart' show TokenKind; | |
6 import 'package:csslib/visitor.dart'; // the CSSOM | |
7 import 'package:html/dom.dart'; | |
8 import 'package:html/src/constants.dart' show isWhitespaceCC; | |
9 | |
10 bool matches(Node node, String selector) => | |
11 new SelectorEvaluator().matches(node, _parseSelectorList(selector)); | |
12 | |
13 Element querySelector(Node node, String selector) => | |
14 new SelectorEvaluator().querySelector(node, _parseSelectorList(selector)); | |
15 | |
16 List<Element> querySelectorAll(Node node, String selector) { | |
17 var results = []; | |
18 new SelectorEvaluator().querySelectorAll( | |
19 node, _parseSelectorList(selector), results); | |
20 return results; | |
21 } | |
22 | |
23 // http://dev.w3.org/csswg/selectors-4/#grouping | |
24 SelectorGroup _parseSelectorList(String selector) { | |
25 var errors = []; | |
26 var group = css.parseSelectorGroup(selector, errors: errors); | |
27 if (group == null || errors.isNotEmpty) { | |
28 throw new FormatException("'$selector' is not a valid selector: $errors"); | |
29 } | |
30 return group; | |
31 } | |
32 | |
33 class SelectorEvaluator extends Visitor { | |
34 /// The current HTML element to match against. | |
35 Element _element; | |
36 | |
37 bool matches(Element element, SelectorGroup selector) { | |
38 _element = element; | |
39 return visitSelectorGroup(selector); | |
40 } | |
41 | |
42 Element querySelector(Node root, SelectorGroup selector) { | |
43 for (var node in root.nodes) { | |
44 if (node is! Element) continue; | |
45 if (matches(node, selector)) return node; | |
46 var result = querySelector(node, selector); | |
47 if (result != null) return result; | |
48 } | |
49 return null; | |
50 } | |
51 | |
52 void querySelectorAll( | |
53 Node root, SelectorGroup selector, List<Element> results) { | |
54 for (var node in root.nodes) { | |
55 if (node is! Element) continue; | |
56 if (matches(node, selector)) results.add(node); | |
57 querySelectorAll(node, selector, results); | |
58 } | |
59 } | |
60 | |
61 bool visitSelectorGroup(SelectorGroup group) => | |
62 group.selectors.any(visitSelector); | |
63 | |
64 bool visitSelector(Selector selector) { | |
65 var old = _element; | |
66 var result = true; | |
67 | |
68 // Note: evaluate selectors right-to-left as it's more efficient. | |
69 int combinator = null; | |
70 for (var s in selector.simpleSelectorSequences.reversed) { | |
71 if (combinator == null) { | |
72 result = s.simpleSelector.visit(this); | |
73 } else if (combinator == TokenKind.COMBINATOR_DESCENDANT) { | |
74 // descendant combinator | |
75 // http://dev.w3.org/csswg/selectors-4/#descendant-combinators | |
76 do { | |
77 _element = _element.parent; | |
78 } while (_element != null && !s.simpleSelector.visit(this)); | |
79 | |
80 if (_element == null) result = false; | |
81 } else if (combinator == TokenKind.COMBINATOR_TILDE) { | |
82 // Following-sibling combinator | |
83 // http://dev.w3.org/csswg/selectors-4/#general-sibling-combinators | |
84 do { | |
85 _element = _element.previousElementSibling; | |
86 } while (_element != null && !s.simpleSelector.visit(this)); | |
87 | |
88 if (_element == null) result = false; | |
89 } | |
90 | |
91 if (!result) break; | |
92 | |
93 switch (s.combinator) { | |
94 case TokenKind.COMBINATOR_PLUS: | |
95 // Next-sibling combinator | |
96 // http://dev.w3.org/csswg/selectors-4/#adjacent-sibling-combinators | |
97 _element = _element.previousElementSibling; | |
98 break; | |
99 case TokenKind.COMBINATOR_GREATER: | |
100 // Child combinator | |
101 // http://dev.w3.org/csswg/selectors-4/#child-combinators | |
102 _element = _element.parent; | |
103 break; | |
104 case TokenKind.COMBINATOR_DESCENDANT: | |
105 case TokenKind.COMBINATOR_TILDE: | |
106 // We need to iterate through all siblings or parents. | |
107 // For now, just remember what the combinator was. | |
108 combinator = s.combinator; | |
109 break; | |
110 case TokenKind.COMBINATOR_NONE: | |
111 break; | |
112 default: | |
113 throw _unsupported(selector); | |
114 } | |
115 | |
116 if (_element == null) { | |
117 result = false; | |
118 break; | |
119 } | |
120 } | |
121 | |
122 _element = old; | |
123 return result; | |
124 } | |
125 | |
126 _unimplemented(SimpleSelector selector) => new UnimplementedError( | |
127 "'$selector' selector of type " | |
128 "${selector.runtimeType} is not implemented"); | |
129 | |
130 _unsupported(selector) => | |
131 new FormatException("'$selector' is not a valid selector"); | |
132 | |
133 bool visitPseudoClassSelector(PseudoClassSelector selector) { | |
134 switch (selector.name) { | |
135 // http://dev.w3.org/csswg/selectors-4/#structural-pseudos | |
136 | |
137 // http://dev.w3.org/csswg/selectors-4/#the-root-pseudo | |
138 case 'root': | |
139 // TODO(jmesserly): fix when we have a .ownerDocument pointer | |
140 // return _element == _element.ownerDocument.rootElement; | |
141 return _element.localName == 'html' && _element.parentNode == null; | |
142 | |
143 // http://dev.w3.org/csswg/selectors-4/#the-empty-pseudo | |
144 case 'empty': | |
145 return _element.nodes | |
146 .any((n) => !(n is Element || n is Text && n.text.isNotEmpty)); | |
147 | |
148 // http://dev.w3.org/csswg/selectors-4/#the-blank-pseudo | |
149 case 'blank': | |
150 return _element.nodes.any((n) => !(n is Element || | |
151 n is Text && n.text.runes.any((r) => !isWhitespaceCC(r)))); | |
152 | |
153 // http://dev.w3.org/csswg/selectors-4/#the-first-child-pseudo | |
154 case 'first-child': | |
155 return _element.previousElementSibling == null; | |
156 | |
157 // http://dev.w3.org/csswg/selectors-4/#the-last-child-pseudo | |
158 case 'last-child': | |
159 return _element.nextElementSibling == null; | |
160 | |
161 // http://dev.w3.org/csswg/selectors-4/#the-only-child-pseudo | |
162 case 'only-child': | |
163 return _element.previousElementSibling == null && | |
164 _element.nextElementSibling == null; | |
165 | |
166 // http://dev.w3.org/csswg/selectors-4/#link | |
167 case 'link': | |
168 return _element.attributes['href'] != null; | |
169 | |
170 case 'visited': | |
171 // Always return false since we aren't a browser. This is allowed per: | |
172 // http://dev.w3.org/csswg/selectors-4/#visited-pseudo | |
173 return false; | |
174 } | |
175 | |
176 // :before, :after, :first-letter/line can't match DOM elements. | |
177 if (_isLegacyPsuedoClass(selector.name)) return false; | |
178 | |
179 throw _unimplemented(selector); | |
180 } | |
181 | |
182 bool visitPseudoElementSelector(PseudoElementSelector selector) { | |
183 // :before, :after, :first-letter/line can't match DOM elements. | |
184 if (_isLegacyPsuedoClass(selector.name)) return false; | |
185 | |
186 throw _unimplemented(selector); | |
187 } | |
188 | |
189 static bool _isLegacyPsuedoClass(String name) { | |
190 switch (name) { | |
191 case 'before': | |
192 case 'after': | |
193 case 'first-line': | |
194 case 'first-letter': | |
195 return true; | |
196 default: | |
197 return false; | |
198 } | |
199 } | |
200 | |
201 bool visitPseudoElementFunctionSelector(PseudoElementFunctionSelector s) => | |
202 throw _unimplemented(s); | |
203 | |
204 bool visitPseudoClassFunctionSelector(PseudoClassFunctionSelector selector) { | |
205 switch (selector.name) { | |
206 // http://dev.w3.org/csswg/selectors-4/#child-index | |
207 | |
208 // http://dev.w3.org/csswg/selectors-4/#the-nth-child-pseudo | |
209 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; | |
218 } | |
219 break; | |
220 | |
221 // http://dev.w3.org/csswg/selectors-4/#the-lang-pseudo | |
222 case 'lang': | |
223 // TODO(jmesserly): shouldn't need to get the raw text here, but csslib | |
224 // gets confused by the "-" in the expression, such as in "es-AR". | |
225 var toMatch = selector.expression.span.text; | |
226 var lang = _getInheritedLanguage(_element); | |
227 // TODO(jmesserly): implement wildcards in level 4 | |
228 return lang != null && lang.startsWith(toMatch); | |
229 } | |
230 throw _unimplemented(selector); | |
231 } | |
232 | |
233 static String _getInheritedLanguage(Node node) { | |
234 while (node != null) { | |
235 var lang = node.attributes['lang']; | |
236 if (lang != null) return lang; | |
237 node = node.parent; | |
238 } | |
239 return null; | |
240 } | |
241 | |
242 bool visitNamespaceSelector(NamespaceSelector selector) { | |
243 // Match element tag name | |
244 if (!selector.nameAsSimpleSelector.visit(this)) return false; | |
245 | |
246 if (selector.isNamespaceWildcard) return true; | |
247 | |
248 if (selector.namespace == '') return _element.namespaceUri == null; | |
249 | |
250 throw _unimplemented(selector); | |
251 } | |
252 | |
253 bool visitElementSelector(ElementSelector selector) => | |
254 selector.isWildcard || _element.localName == selector.name.toLowerCase(); | |
255 | |
256 bool visitIdSelector(IdSelector selector) => _element.id == selector.name; | |
257 | |
258 bool visitClassSelector(ClassSelector selector) => | |
259 _element.classes.contains(selector.name); | |
260 | |
261 // TODO(jmesserly): negation should support any selectors in level 4, | |
262 // not just simple selectors. | |
263 // http://dev.w3.org/csswg/selectors-4/#negation | |
264 bool visitNegationSelector(NegationSelector selector) => | |
265 !selector.negationArg.visit(this); | |
266 | |
267 bool visitAttributeSelector(AttributeSelector selector) { | |
268 // Match name first | |
269 var value = _element.attributes[selector.name.toLowerCase()]; | |
270 if (value == null) return false; | |
271 | |
272 if (selector.operatorKind == TokenKind.NO_MATCH) return true; | |
273 | |
274 var select = '${selector.value}'; | |
275 switch (selector.operatorKind) { | |
276 case TokenKind.EQUALS: | |
277 return value == select; | |
278 case TokenKind.INCLUDES: | |
279 return value.split(' ').any((v) => v.isNotEmpty && v == select); | |
280 case TokenKind.DASH_MATCH: | |
281 return value.startsWith(select) && | |
282 (value.length == select.length || value[select.length] == '-'); | |
283 case TokenKind.PREFIX_MATCH: | |
284 return value.startsWith(select); | |
285 case TokenKind.SUFFIX_MATCH: | |
286 return value.endsWith(select); | |
287 case TokenKind.SUBSTRING_MATCH: | |
288 return value.contains(select); | |
289 default: | |
290 throw _unsupported(selector); | |
291 } | |
292 } | |
293 } | |
OLD | NEW |