| OLD | NEW |
| (Empty) |
| 1 /// Query selector implementation for html5lib's DOM. | |
| 2 library html5lib.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:html5lib/dom.dart'; | |
| 8 import 'package:html5lib/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() | |
| 19 .querySelectorAll(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(Node root, SelectorGroup selector, | |
| 53 List<Element> results) { | |
| 54 | |
| 55 for (var node in root.nodes) { | |
| 56 if (node is! Element) continue; | |
| 57 if (matches(node, selector)) results.add(node); | |
| 58 querySelectorAll(node, selector, results); | |
| 59 } | |
| 60 } | |
| 61 | |
| 62 | |
| 63 bool visitSelectorGroup(SelectorGroup group) => | |
| 64 group.selectors.any(visitSelector); | |
| 65 | |
| 66 bool visitSelector(Selector selector) { | |
| 67 var old = _element; | |
| 68 var result = true; | |
| 69 | |
| 70 // Note: evaluate selectors right-to-left as it's more efficient. | |
| 71 int combinator = null; | |
| 72 for (var s in selector.simpleSelectorSequences.reversed) { | |
| 73 if (combinator == null) { | |
| 74 result = s.simpleSelector.visit(this); | |
| 75 } else if (combinator == TokenKind.COMBINATOR_DESCENDANT) { | |
| 76 // descendant combinator | |
| 77 // http://dev.w3.org/csswg/selectors-4/#descendant-combinators | |
| 78 do { | |
| 79 _element = _element.parent; | |
| 80 } while (_element != null && !s.simpleSelector.visit(this)); | |
| 81 | |
| 82 if (_element == null) result = false; | |
| 83 } else if (combinator == TokenKind.COMBINATOR_TILDE) { | |
| 84 // Following-sibling combinator | |
| 85 // http://dev.w3.org/csswg/selectors-4/#general-sibling-combinators | |
| 86 do { | |
| 87 _element = _element.previousElementSibling; | |
| 88 } while (_element != null && !s.simpleSelector.visit(this)); | |
| 89 | |
| 90 if (_element == null) result = false; | |
| 91 } | |
| 92 | |
| 93 if (!result) break; | |
| 94 | |
| 95 switch (s.combinator) { | |
| 96 case TokenKind.COMBINATOR_PLUS: | |
| 97 // Next-sibling combinator | |
| 98 // http://dev.w3.org/csswg/selectors-4/#adjacent-sibling-combinators | |
| 99 _element = _element.previousElementSibling; | |
| 100 break; | |
| 101 case TokenKind.COMBINATOR_GREATER: | |
| 102 // Child combinator | |
| 103 // http://dev.w3.org/csswg/selectors-4/#child-combinators | |
| 104 _element = _element.parent; | |
| 105 break; | |
| 106 case TokenKind.COMBINATOR_DESCENDANT: | |
| 107 case TokenKind.COMBINATOR_TILDE: | |
| 108 // We need to iterate through all siblings or parents. | |
| 109 // For now, just remember what the combinator was. | |
| 110 combinator = s.combinator; | |
| 111 break; | |
| 112 case TokenKind.COMBINATOR_NONE: break; | |
| 113 default: 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) => | |
| 127 new UnimplementedError("'$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.any((n) => !(n is Element || | |
| 146 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 | |
| 183 bool visitPseudoElementSelector(PseudoElementSelector selector) { | |
| 184 // :before, :after, :first-letter/line can't match DOM elements. | |
| 185 if (_isLegacyPsuedoClass(selector.name)) return false; | |
| 186 | |
| 187 throw _unimplemented(selector); | |
| 188 } | |
| 189 | |
| 190 static bool _isLegacyPsuedoClass(String name) { | |
| 191 switch (name) { | |
| 192 case 'before': case 'after': case 'first-line': case 'first-letter': | |
| 193 return true; | |
| 194 default: return false; | |
| 195 } | |
| 196 } | |
| 197 | |
| 198 bool visitPseudoElementFunctionSelector(PseudoElementFunctionSelector s) => | |
| 199 throw _unimplemented(s); | |
| 200 | |
| 201 bool visitPseudoClassFunctionSelector(PseudoClassFunctionSelector selector) { | |
| 202 switch (selector.name) { | |
| 203 // http://dev.w3.org/csswg/selectors-4/#child-index | |
| 204 | |
| 205 // http://dev.w3.org/csswg/selectors-4/#the-nth-child-pseudo | |
| 206 case 'nth-child': | |
| 207 // TODO(jmesserly): support An+B syntax too. | |
| 208 var exprs = selector.expression.expressions; | |
| 209 if (exprs.length == 1 && exprs[0] is LiteralTerm) { | |
| 210 LiteralTerm literal = exprs[0]; | |
| 211 var parent = _element.parentNode; | |
| 212 return parent != null && literal.value > 0 && | |
| 213 parent.nodes.indexOf(_element) == literal.value; | |
| 214 } | |
| 215 break; | |
| 216 | |
| 217 // http://dev.w3.org/csswg/selectors-4/#the-lang-pseudo | |
| 218 case 'lang': | |
| 219 // TODO(jmesserly): shouldn't need to get the raw text here, but csslib | |
| 220 // gets confused by the "-" in the expression, such as in "es-AR". | |
| 221 var toMatch = selector.expression.span.text; | |
| 222 var lang = _getInheritedLanguage(_element); | |
| 223 // TODO(jmesserly): implement wildcards in level 4 | |
| 224 return lang != null && lang.startsWith(toMatch); | |
| 225 } | |
| 226 throw _unimplemented(selector); | |
| 227 } | |
| 228 | |
| 229 static String _getInheritedLanguage(Node node) { | |
| 230 while (node != null) { | |
| 231 var lang = node.attributes['lang']; | |
| 232 if (lang != null) return lang; | |
| 233 node = node.parent; | |
| 234 } | |
| 235 return null; | |
| 236 } | |
| 237 | |
| 238 bool visitNamespaceSelector(NamespaceSelector selector) { | |
| 239 // Match element tag name | |
| 240 if (!selector.nameAsSimpleSelector.visit(this)) return false; | |
| 241 | |
| 242 if (selector.isNamespaceWildcard) return true; | |
| 243 | |
| 244 if (selector.namespace == '') return _element.namespaceUri == null; | |
| 245 | |
| 246 throw _unimplemented(selector); | |
| 247 } | |
| 248 | |
| 249 bool visitElementSelector(ElementSelector selector) => | |
| 250 selector.isWildcard || _element.localName == selector.name.toLowerCase(); | |
| 251 | |
| 252 bool visitIdSelector(IdSelector selector) => _element.id == selector.name; | |
| 253 | |
| 254 bool visitClassSelector(ClassSelector selector) => | |
| 255 _element.classes.contains(selector.name); | |
| 256 | |
| 257 // TODO(jmesserly): negation should support any selectors in level 4, | |
| 258 // not just simple selectors. | |
| 259 // http://dev.w3.org/csswg/selectors-4/#negation | |
| 260 bool visitNegationSelector(NegationSelector selector) => | |
| 261 !selector.negationArg.visit(this); | |
| 262 | |
| 263 bool visitAttributeSelector(AttributeSelector selector) { | |
| 264 // Match name first | |
| 265 var value = _element.attributes[selector.name.toLowerCase()]; | |
| 266 if (value == null) return false; | |
| 267 | |
| 268 if (selector.operatorKind == TokenKind.NO_MATCH) return true; | |
| 269 | |
| 270 var select = '${selector.value}'; | |
| 271 switch (selector.operatorKind) { | |
| 272 case TokenKind.EQUALS: | |
| 273 return value == select; | |
| 274 case TokenKind.INCLUDES: | |
| 275 return value.split(' ').any((v) => v.isNotEmpty && v == select); | |
| 276 case TokenKind.DASH_MATCH: | |
| 277 return value.startsWith(select) && | |
| 278 (value.length == select.length || value[select.length] == '-'); | |
| 279 case TokenKind.PREFIX_MATCH: | |
| 280 return value.startsWith(select); | |
| 281 case TokenKind.SUFFIX_MATCH: | |
| 282 return value.endsWith(select); | |
| 283 case TokenKind.SUBSTRING_MATCH: | |
| 284 return value.contains(select); | |
| 285 default: throw _unsupported(selector); | |
| 286 } | |
| 287 } | |
| 288 } | |
| OLD | NEW |