Index: mojo/public/dart/third_party/html/lib/src/query_selector.dart |
diff --git a/mojo/public/dart/third_party/html/lib/src/query_selector.dart b/mojo/public/dart/third_party/html/lib/src/query_selector.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e586d1aa593b1d259c698760996c570793b55f9c |
--- /dev/null |
+++ b/mojo/public/dart/third_party/html/lib/src/query_selector.dart |
@@ -0,0 +1,293 @@ |
+/// Query selector implementation for our DOM. |
+library html.src.query; |
+ |
+import 'package:csslib/parser.dart' as css; |
+import 'package:csslib/parser.dart' show TokenKind; |
+import 'package:csslib/visitor.dart'; // the CSSOM |
+import 'package:html/dom.dart'; |
+import 'package:html/src/constants.dart' show isWhitespaceCC; |
+ |
+bool matches(Node node, String selector) => |
+ new SelectorEvaluator().matches(node, _parseSelectorList(selector)); |
+ |
+Element querySelector(Node node, String selector) => |
+ new SelectorEvaluator().querySelector(node, _parseSelectorList(selector)); |
+ |
+List<Element> querySelectorAll(Node node, String selector) { |
+ var results = []; |
+ new SelectorEvaluator().querySelectorAll( |
+ node, _parseSelectorList(selector), results); |
+ return results; |
+} |
+ |
+// http://dev.w3.org/csswg/selectors-4/#grouping |
+SelectorGroup _parseSelectorList(String selector) { |
+ var errors = []; |
+ var group = css.parseSelectorGroup(selector, errors: errors); |
+ if (group == null || errors.isNotEmpty) { |
+ throw new FormatException("'$selector' is not a valid selector: $errors"); |
+ } |
+ return group; |
+} |
+ |
+class SelectorEvaluator extends Visitor { |
+ /// The current HTML element to match against. |
+ Element _element; |
+ |
+ bool matches(Element element, SelectorGroup selector) { |
+ _element = element; |
+ return visitSelectorGroup(selector); |
+ } |
+ |
+ Element querySelector(Node root, SelectorGroup selector) { |
+ for (var node in root.nodes) { |
+ if (node is! Element) continue; |
+ if (matches(node, selector)) return node; |
+ var result = querySelector(node, selector); |
+ if (result != null) return result; |
+ } |
+ return null; |
+ } |
+ |
+ void querySelectorAll( |
+ Node root, SelectorGroup selector, List<Element> results) { |
+ for (var node in root.nodes) { |
+ if (node is! Element) continue; |
+ if (matches(node, selector)) results.add(node); |
+ querySelectorAll(node, selector, results); |
+ } |
+ } |
+ |
+ bool visitSelectorGroup(SelectorGroup group) => |
+ group.selectors.any(visitSelector); |
+ |
+ bool visitSelector(Selector selector) { |
+ var old = _element; |
+ var result = true; |
+ |
+ // Note: evaluate selectors right-to-left as it's more efficient. |
+ int combinator = null; |
+ for (var s in selector.simpleSelectorSequences.reversed) { |
+ if (combinator == null) { |
+ result = s.simpleSelector.visit(this); |
+ } else if (combinator == TokenKind.COMBINATOR_DESCENDANT) { |
+ // descendant combinator |
+ // http://dev.w3.org/csswg/selectors-4/#descendant-combinators |
+ do { |
+ _element = _element.parent; |
+ } while (_element != null && !s.simpleSelector.visit(this)); |
+ |
+ if (_element == null) result = false; |
+ } else if (combinator == TokenKind.COMBINATOR_TILDE) { |
+ // Following-sibling combinator |
+ // http://dev.w3.org/csswg/selectors-4/#general-sibling-combinators |
+ do { |
+ _element = _element.previousElementSibling; |
+ } while (_element != null && !s.simpleSelector.visit(this)); |
+ |
+ if (_element == null) result = false; |
+ } |
+ |
+ if (!result) break; |
+ |
+ switch (s.combinator) { |
+ case TokenKind.COMBINATOR_PLUS: |
+ // Next-sibling combinator |
+ // http://dev.w3.org/csswg/selectors-4/#adjacent-sibling-combinators |
+ _element = _element.previousElementSibling; |
+ break; |
+ case TokenKind.COMBINATOR_GREATER: |
+ // Child combinator |
+ // http://dev.w3.org/csswg/selectors-4/#child-combinators |
+ _element = _element.parent; |
+ break; |
+ case TokenKind.COMBINATOR_DESCENDANT: |
+ case TokenKind.COMBINATOR_TILDE: |
+ // We need to iterate through all siblings or parents. |
+ // For now, just remember what the combinator was. |
+ combinator = s.combinator; |
+ break; |
+ case TokenKind.COMBINATOR_NONE: |
+ break; |
+ default: |
+ throw _unsupported(selector); |
+ } |
+ |
+ if (_element == null) { |
+ result = false; |
+ break; |
+ } |
+ } |
+ |
+ _element = old; |
+ return result; |
+ } |
+ |
+ _unimplemented(SimpleSelector selector) => new UnimplementedError( |
+ "'$selector' selector of type " |
+ "${selector.runtimeType} is not implemented"); |
+ |
+ _unsupported(selector) => |
+ new FormatException("'$selector' is not a valid selector"); |
+ |
+ bool visitPseudoClassSelector(PseudoClassSelector selector) { |
+ switch (selector.name) { |
+ // http://dev.w3.org/csswg/selectors-4/#structural-pseudos |
+ |
+ // http://dev.w3.org/csswg/selectors-4/#the-root-pseudo |
+ case 'root': |
+ // TODO(jmesserly): fix when we have a .ownerDocument pointer |
+ // return _element == _element.ownerDocument.rootElement; |
+ return _element.localName == 'html' && _element.parentNode == null; |
+ |
+ // 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)); |
+ |
+ // http://dev.w3.org/csswg/selectors-4/#the-blank-pseudo |
+ case 'blank': |
+ return _element.nodes.any((n) => !(n is Element || |
+ n is Text && n.text.runes.any((r) => !isWhitespaceCC(r)))); |
+ |
+ // http://dev.w3.org/csswg/selectors-4/#the-first-child-pseudo |
+ case 'first-child': |
+ return _element.previousElementSibling == null; |
+ |
+ // http://dev.w3.org/csswg/selectors-4/#the-last-child-pseudo |
+ case 'last-child': |
+ return _element.nextElementSibling == null; |
+ |
+ // http://dev.w3.org/csswg/selectors-4/#the-only-child-pseudo |
+ case 'only-child': |
+ return _element.previousElementSibling == null && |
+ _element.nextElementSibling == null; |
+ |
+ // http://dev.w3.org/csswg/selectors-4/#link |
+ case 'link': |
+ return _element.attributes['href'] != null; |
+ |
+ case 'visited': |
+ // Always return false since we aren't a browser. This is allowed per: |
+ // http://dev.w3.org/csswg/selectors-4/#visited-pseudo |
+ return false; |
+ } |
+ |
+ // :before, :after, :first-letter/line can't match DOM elements. |
+ if (_isLegacyPsuedoClass(selector.name)) return false; |
+ |
+ throw _unimplemented(selector); |
+ } |
+ |
+ bool visitPseudoElementSelector(PseudoElementSelector selector) { |
+ // :before, :after, :first-letter/line can't match DOM elements. |
+ if (_isLegacyPsuedoClass(selector.name)) return false; |
+ |
+ throw _unimplemented(selector); |
+ } |
+ |
+ static bool _isLegacyPsuedoClass(String name) { |
+ switch (name) { |
+ case 'before': |
+ case 'after': |
+ case 'first-line': |
+ case 'first-letter': |
+ return true; |
+ default: |
+ return false; |
+ } |
+ } |
+ |
+ bool visitPseudoElementFunctionSelector(PseudoElementFunctionSelector s) => |
+ throw _unimplemented(s); |
+ |
+ 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. |
+ var exprs = selector.expression.expressions; |
+ if (exprs.length == 1 && exprs[0] is LiteralTerm) { |
+ LiteralTerm literal = exprs[0]; |
+ var parent = _element.parentNode; |
+ return parent != null && |
+ literal.value > 0 && |
+ parent.nodes.indexOf(_element) == literal.value; |
+ } |
+ break; |
+ |
+ // http://dev.w3.org/csswg/selectors-4/#the-lang-pseudo |
+ case 'lang': |
+ // TODO(jmesserly): shouldn't need to get the raw text here, but csslib |
+ // gets confused by the "-" in the expression, such as in "es-AR". |
+ var toMatch = selector.expression.span.text; |
+ var lang = _getInheritedLanguage(_element); |
+ // TODO(jmesserly): implement wildcards in level 4 |
+ return lang != null && lang.startsWith(toMatch); |
+ } |
+ throw _unimplemented(selector); |
+ } |
+ |
+ static String _getInheritedLanguage(Node node) { |
+ while (node != null) { |
+ var lang = node.attributes['lang']; |
+ if (lang != null) return lang; |
+ node = node.parent; |
+ } |
+ return null; |
+ } |
+ |
+ bool visitNamespaceSelector(NamespaceSelector selector) { |
+ // Match element tag name |
+ if (!selector.nameAsSimpleSelector.visit(this)) return false; |
+ |
+ if (selector.isNamespaceWildcard) return true; |
+ |
+ if (selector.namespace == '') return _element.namespaceUri == null; |
+ |
+ throw _unimplemented(selector); |
+ } |
+ |
+ bool visitElementSelector(ElementSelector selector) => |
+ selector.isWildcard || _element.localName == selector.name.toLowerCase(); |
+ |
+ bool visitIdSelector(IdSelector selector) => _element.id == selector.name; |
+ |
+ bool visitClassSelector(ClassSelector selector) => |
+ _element.classes.contains(selector.name); |
+ |
+ // TODO(jmesserly): negation should support any selectors in level 4, |
+ // not just simple selectors. |
+ // http://dev.w3.org/csswg/selectors-4/#negation |
+ bool visitNegationSelector(NegationSelector selector) => |
+ !selector.negationArg.visit(this); |
+ |
+ bool visitAttributeSelector(AttributeSelector selector) { |
+ // Match name first |
+ var value = _element.attributes[selector.name.toLowerCase()]; |
+ if (value == null) return false; |
+ |
+ if (selector.operatorKind == TokenKind.NO_MATCH) return true; |
+ |
+ var select = '${selector.value}'; |
+ switch (selector.operatorKind) { |
+ case TokenKind.EQUALS: |
+ return value == select; |
+ case TokenKind.INCLUDES: |
+ return value.split(' ').any((v) => v.isNotEmpty && v == select); |
+ case TokenKind.DASH_MATCH: |
+ return value.startsWith(select) && |
+ (value.length == select.length || value[select.length] == '-'); |
+ case TokenKind.PREFIX_MATCH: |
+ return value.startsWith(select); |
+ case TokenKind.SUFFIX_MATCH: |
+ return value.endsWith(select); |
+ case TokenKind.SUBSTRING_MATCH: |
+ return value.contains(select); |
+ default: |
+ throw _unsupported(selector); |
+ } |
+ } |
+} |