/// A simple tree API that results from parsing html. Intended to be compatible /// with dart:html, but it is missing many types and APIs. library; // ignore_for_file: constant_identifier_names // TODO(jmesserly): lots to do here. Originally I wanted to generate this using // our Blink IDL generator, but another idea is to directly use the excellent // http://dom.spec.whatwg.org/ and http://html.spec.whatwg.org/ and just // implement that. import 'dart:collection'; import 'package:source_span/source_span.dart'; import 'dom_parsing.dart'; import 'parser.dart'; import 'src/constants.dart'; import 'src/css_class_set.dart'; import 'src/list_proxy.dart'; import 'src/query_selector.dart' as query; import 'src/token.dart'; import 'src/tokenizer.dart'; export 'src/css_class_set.dart' show CssClassSet; // TODO(jmesserly): this needs to be replaced by an AttributeMap for attributes // that exposes namespace info. class AttributeName implements Comparable { /// The namespace prefix, e.g. `xlink`. final String? prefix; /// The attribute name, e.g. `title`. final String name; /// The namespace url, e.g. `http://www.w3.org/1999/xlink` final String namespace; const AttributeName(this.prefix, this.name, this.namespace); @override String toString() { // Implement: // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#serializing-html-fragments // If we get here we know we are xml, xmlns, or xlink, because of // [HtmlParser.adjustForeignAttriubtes] is the only place we create // an AttributeName. return prefix != null ? '$prefix:$name' : name; } @override int get hashCode { var h = prefix.hashCode; h = 37 * (h & 0x1FFFFF) + name.hashCode; h = 37 * (h & 0x1FFFFF) + namespace.hashCode; return h & 0x3FFFFFFF; } @override int compareTo(Object other) { // Not sure about this sort order if (other is! AttributeName) return 1; var cmp = (prefix ?? '').compareTo(other.prefix ?? ''); if (cmp != 0) return cmp; cmp = name.compareTo(other.name); if (cmp != 0) return cmp; return namespace.compareTo(other.namespace); } @override bool operator ==(Object other) => other is AttributeName && prefix == other.prefix && name == other.name && namespace == other.namespace; } // http://dom.spec.whatwg.org/#parentnode mixin _ParentNode implements Node { // TODO(jmesserly): this is only a partial implementation /// Seaches for the first descendant node matching the given selectors, using /// a preorder traversal. /// /// NOTE: Not all selectors from /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/) /// are implemented. For example, nth-child does not implement An+B syntax /// and *-of-type is not implemented. If a selector is not implemented this /// method will throw [UnimplementedError]. Element? querySelector(String selector) => query.querySelector(this, selector); /// Returns all descendant nodes matching the given selectors, using a /// preorder traversal. /// /// NOTE: Not all selectors from /// [selectors level 4](http://dev.w3.org/csswg/selectors-4/) /// are implemented. For example, nth-child does not implement An+B syntax /// and *-of-type is not implemented. If a selector is not implemented this /// method will throw [UnimplementedError]. List querySelectorAll(String selector) => query.querySelectorAll(this, selector); } // http://dom.spec.whatwg.org/#interface-nonelementparentnode mixin _NonElementParentNode implements _ParentNode { // TODO(jmesserly): could be faster, should throw on invalid id. Element? getElementById(String id) => querySelector('#$id'); } // This doesn't exist as an interface in the spec, but it's useful to merge // common methods from these: // http://dom.spec.whatwg.org/#interface-document // http://dom.spec.whatwg.org/#element abstract mixin class _ElementAndDocument implements _ParentNode { // TODO(jmesserly): could be faster, should throw on invalid tag/class names. List getElementsByTagName(String localName) => querySelectorAll(localName); List getElementsByClassName(String classNames) => querySelectorAll(classNames.splitMapJoin(' ', onNonMatch: (m) => m.isNotEmpty ? '.$m' : m, onMatch: (m) => '')); } /// Really basic implementation of a DOM-core like Node. abstract class Node { static const int ATTRIBUTE_NODE = 2; static const int CDATA_SECTION_NODE = 4; static const int COMMENT_NODE = 8; static const int DOCUMENT_FRAGMENT_NODE = 11; static const int DOCUMENT_NODE = 9; static const int DOCUMENT_TYPE_NODE = 10; static const int ELEMENT_NODE = 1; static const int ENTITY_NODE = 6; static const int ENTITY_REFERENCE_NODE = 5; static const int NOTATION_NODE = 12; static const int PROCESSING_INSTRUCTION_NODE = 7; static const int TEXT_NODE = 3; /// The parent of the current node (or null for the document node). Node? parentNode; /// The parent element of this node. /// /// Returns null if this node either does not have a parent or its parent is /// not an element. Element? get parent { final parentNode = this.parentNode; return parentNode is Element ? parentNode : null; } // TODO(jmesserly): should move to Element. /// A map holding name, value pairs for attributes of the node. /// /// Note that attribute order needs to be stable for serialization, so we use /// a LinkedHashMap. Each key is a [String] or [AttributeName]. LinkedHashMap attributes = LinkedHashMap(); /// A list of child nodes of the current node. This must /// include all elements but not necessarily other node types. late final nodes = NodeList._(this); late final List children = FilteredElementList(this); // TODO(jmesserly): consider using an Expando for this, and put it in // dom_parsing. Need to check the performance affect. /// The source span of this node, if it was created by the [HtmlParser]. FileSpan? sourceSpan; /// The attribute spans if requested. Otherwise null. LinkedHashMap? _attributeSpans; LinkedHashMap? _attributeValueSpans; Node._(); /// If [sourceSpan] is available, this contains the spans of each attribute. /// The span of an attribute is the entire attribute, including the name and /// quotes (if any). For example, the span of "attr" in `` /// would be the text `attr="value"`. LinkedHashMap? get attributeSpans { _ensureAttributeSpans(); return _attributeSpans; } /// If [sourceSpan] is available, this contains the spans of each attribute's /// value. Unlike [attributeSpans], this span will include only the value. /// For example, the value span of "attr" in `` would be the /// text `value`. LinkedHashMap? get attributeValueSpans { _ensureAttributeSpans(); return _attributeValueSpans; } /// Returns a copy of this node. /// /// If [deep] is `true`, then all of this node's children and decendents are /// copied as well. If [deep] is `false`, then only this node is copied. Node clone(bool deep); int get nodeType; // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface String get _outerHtml { final str = StringBuffer(); _addOuterHtml(str); return str.toString(); } String get _innerHtml { final str = StringBuffer(); _addInnerHtml(str); return str.toString(); } // Implemented per: http://dom.spec.whatwg.org/#dom-node-textcontent String? get text => null; set text(String? value) {} void append(Node node) => nodes.add(node); Node? get firstChild => nodes.isNotEmpty ? nodes[0] : null; void _addOuterHtml(StringBuffer str); void _addInnerHtml(StringBuffer str) { for (var child in nodes) { child._addOuterHtml(str); } } Node remove() { // TODO(jmesserly): is parent == null an error? parentNode?.nodes.remove(this); return this; } /// Insert [node] as a child of the current node, before [refNode] in the void insertBefore(Node node, Node? refNode) { if (refNode == null) { nodes.add(node); } else { nodes.insert(nodes.indexOf(refNode), node); } } /// Replaces this node with another node. Node replaceWith(Node otherNode) { if (parentNode == null) { throw UnsupportedError('Node must have a parent to replace it.'); } parentNode!.nodes[parentNode!.nodes.indexOf(this)] = otherNode; return this; } // TODO(jmesserly): should this be a property or remove? /// Return true if the node has children or text. bool hasContent() => nodes.isNotEmpty; /// Move all the children of the current node to [newParent]. /// This is needed so that trees that don't store text as nodes move the /// text in the correct way. void reparentChildren(Node newParent) { newParent.nodes.addAll(nodes); nodes.clear(); } bool hasChildNodes() => nodes.isNotEmpty; bool contains(Node node) => nodes.contains(node); /// Initialize [attributeSpans] using [sourceSpan]. void _ensureAttributeSpans() { if (_attributeSpans != null) return; final attributeSpans = _attributeSpans = LinkedHashMap(); final attributeValueSpans = _attributeValueSpans = LinkedHashMap(); if (sourceSpan == null) return; final tokenizer = HtmlTokenizer(sourceSpan!.text, generateSpans: true, attributeSpans: true); tokenizer.moveNext(); final token = tokenizer.current as StartTagToken; if (token.attributeSpans == null) return; // no attributes for (var attr in token.attributeSpans!) { final offset = sourceSpan!.start.offset; final name = attr.name!; attributeSpans[name] = sourceSpan!.file.span(offset + attr.start, offset + attr.end); if (attr.startValue != null) { attributeValueSpans[name] = sourceSpan!.file .span(offset + attr.startValue!, offset + attr.endValue); } } } T _clone(T shallowClone, bool deep) { if (deep) { for (var child in nodes) { shallowClone.append(child.clone(true)); } } return shallowClone; } } class Document extends Node with _ParentNode, _NonElementParentNode, _ElementAndDocument { Document() : super._(); factory Document.html(String html) => parse(html); @override int get nodeType => Node.DOCUMENT_NODE; // TODO(jmesserly): optmize this if needed Element? get documentElement => querySelector('html'); Element? get head => documentElement?.querySelector('head'); Element? get body => documentElement?.querySelector('body'); /// Returns a fragment of HTML or XML that represents the element and its /// contents. // TODO(jmesserly): this API is not specified in: // nor is it in dart:html, instead // only Element has outerHtml. However it is quite useful. Should we move it // to dom_parsing, where we keep other custom APIs? String get outerHtml => _outerHtml; @override String toString() => '#document'; @override void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); @override Document clone(bool deep) => _clone(Document(), deep); Element createElement(String tag) => Element.tag(tag); // TODO(jmesserly): this is only a partial implementation of: // http://dom.spec.whatwg.org/#dom-document-createelementns Element createElementNS(String? namespaceUri, String? tag) { if (namespaceUri == '') namespaceUri = null; return Element._(tag, namespaceUri); } DocumentFragment createDocumentFragment() => DocumentFragment(); } class DocumentFragment extends Node with _ParentNode, _NonElementParentNode { DocumentFragment() : super._(); factory DocumentFragment.html(String html) => parseFragment(html); @override int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; /// Returns a fragment of HTML or XML that represents the element and its /// contents. // TODO(jmesserly): this API is not specified in: // nor is it in dart:html, instead // only Element has outerHtml. However it is quite useful. Should we move it // to dom_parsing, where we keep other custom APIs? String get outerHtml => _outerHtml; @override String toString() => '#document-fragment'; @override DocumentFragment clone(bool deep) => _clone(DocumentFragment(), deep); @override void _addOuterHtml(StringBuffer str) => _addInnerHtml(str); @override String? get text => _getText(this); @override set text(String? value) => _setText(this, value); } class DocumentType extends Node { final String? name; final String? publicId; final String? systemId; DocumentType(this.name, this.publicId, this.systemId) : super._(); @override int get nodeType => Node.DOCUMENT_TYPE_NODE; @override String toString() { if (publicId != null || systemId != null) { // TODO(jmesserly): the html5 serialization spec does not add these. But // it seems useful, and the parser can handle it, so for now keeping it. final pid = publicId ?? ''; final sid = systemId ?? ''; return ''; } else { return ''; } } @override void _addOuterHtml(StringBuffer str) { str.write(toString()); } @override DocumentType clone(bool deep) => DocumentType(name, publicId, systemId); } class Text extends Node { /// The text node's data, stored as either a String or StringBuffer. /// We support storing a StringBuffer here to support fast [appendData]. /// It will flatten back to a String on read. Object _data; Text(String? data) : _data = data ?? '', super._(); @override int get nodeType => Node.TEXT_NODE; String get data => _data = _data.toString(); set data(String value) { // Handle unsound null values. _data = identical(value, null) ? '' : value; } @override String toString() => '"$data"'; @override void _addOuterHtml(StringBuffer str) => writeTextNodeAsHtml(str, this); @override Text clone(bool deep) => Text(data); void appendData(String data) { if (_data is! StringBuffer) _data = StringBuffer(_data); final sb = _data as StringBuffer; sb.write(data); } @override String get text => data; @override // Text has a non-nullable `text` field, while Node has a nullable field. set text(covariant String value) { data = value; } } // TODO(jmesserly): Elements should have a pointer back to their document class Element extends Node with _ParentNode, _ElementAndDocument { final String? namespaceUri; /// The [local name](http://dom.spec.whatwg.org/#concept-element-local-name) /// of this element. final String? localName; // TODO(jmesserly): consider using an Expando for this, and put it in // dom_parsing. Need to check the performance affect. /// The source span of the end tag this element, if it was created by the /// [HtmlParser]. May be `null` if does not have an implicit end tag. FileSpan? endSourceSpan; Element._(this.localName, [this.namespaceUri]) : super._(); Element.tag(this.localName) : namespaceUri = Namespaces.html, super._(); static final _startTagRegexp = RegExp('<(\\w+)'); static final _customParentTagMap = const { 'body': 'html', 'head': 'html', 'caption': 'table', 'td': 'tr', 'colgroup': 'table', 'col': 'colgroup', 'tr': 'tbody', 'tbody': 'table', 'tfoot': 'table', 'thead': 'table', 'track': 'audio', }; // TODO(jmesserly): this is from dart:html _ElementFactoryProvider... // TODO(jmesserly): have a look at fixing some things in dart:html, in // particular: is the parent tag map complete? Is it faster without regexp? // TODO(jmesserly): for our version we can do something smarter in the parser. // All we really need is to set the correct parse state. factory Element.html(String html) { // TODO(jacobr): this method can be made more robust and performant. // 1) Cache the dummy parent elements required to use innerHTML rather than // creating them every call. // 2) Verify that the html does not contain leading or trailing text nodes. // 3) Verify that the html does not contain both and tags. // 4) Detach the created element from its dummy parent. var parentTag = 'div'; String? tag; final match = _startTagRegexp.firstMatch(html); if (match != null) { tag = match.group(1)!.toLowerCase(); if (_customParentTagMap.containsKey(tag)) { parentTag = _customParentTagMap[tag]!; } } final fragment = parseFragment(html, container: parentTag); Element element; if (fragment.children.length == 1) { element = fragment.children[0]; } else if (parentTag == 'html' && fragment.children.length == 2) { // You'll always get a head and a body when starting from html. element = fragment.children[tag == 'head' ? 0 : 1]; } else { throw ArgumentError('HTML had ${fragment.children.length} ' 'top level elements but 1 expected'); } element.remove(); return element; } @override int get nodeType => Node.ELEMENT_NODE; // TODO(jmesserly): we can make this faster Element? get previousElementSibling { if (parentNode == null) return null; final siblings = parentNode!.nodes; for (var i = siblings.indexOf(this) - 1; i >= 0; i--) { final s = siblings[i]; if (s is Element) return s; } return null; } Element? get nextElementSibling { final parentNode = this.parentNode; if (parentNode == null) return null; final siblings = parentNode.nodes; for (var i = siblings.indexOf(this) + 1; i < siblings.length; i++) { final s = siblings[i]; if (s is Element) return s; } return null; } @override String toString() { final prefix = Namespaces.getPrefix(namespaceUri); return "<${prefix == null ? '' : '$prefix '}$localName>"; } @override String get text => _getText(this); @override set text(String? value) => _setText(this, value); /// Returns a fragment of HTML or XML that represents the element and its /// contents. String get outerHtml => _outerHtml; /// Returns a fragment of HTML or XML that represents the element's contents. /// Can be set, to replace the contents of the element with nodes parsed from /// the given string. String get innerHtml => _innerHtml; // TODO(jmesserly): deprecate in favor of: // set innerHtml(String value) { nodes.clear(); // TODO(jmesserly): should be able to get the same effect by adding the // fragment directly. nodes.addAll(parseFragment(value, container: localName!).nodes); } @override void _addOuterHtml(StringBuffer str) { // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#serializing-html-fragments // Element is the most complicated one. str.write('<'); str.write(_getSerializationPrefix(namespaceUri)); str.write(localName); if (attributes.isNotEmpty) { attributes.forEach((key, v) { // Note: AttributeName.toString handles serialization of attribute // namespace, if needed. str.write(' '); str.write(key); str.write('="'); str.write(htmlSerializeEscape(v, attributeMode: true)); str.write('"'); }); } str.write('>'); if (nodes.isNotEmpty) { if (localName == 'pre' || localName == 'textarea' || localName == 'listing') { final first = nodes[0]; if (first is Text && first.data.startsWith('\n')) { // These nodes will remove a leading \n at parse time, so if we still // have one, it means we started with two. Add it back. str.write('\n'); } } _addInnerHtml(str); } // void elements must not have an end tag // http://dev.w3.org/html5/markup/syntax.html#void-elements if (!isVoidElement(localName)) str.write(''); } static String _getSerializationPrefix(String? uri) { if (uri == null || uri == Namespaces.html || uri == Namespaces.mathml || uri == Namespaces.svg) { return ''; } final prefix = Namespaces.getPrefix(uri); // TODO(jmesserly): the spec doesn't define "qualified name". // I'm not sure if this is correct, but it should parse reasonably. return prefix == null ? '' : '$prefix:'; } @override Element clone(bool deep) { final result = Element._(localName, namespaceUri) ..attributes = LinkedHashMap.from(attributes); return _clone(result, deep); } // http://dom.spec.whatwg.org/#dom-element-id String get id { final result = attributes['id']; return result ?? ''; } set id(String value) { attributes['id'] = value; } // http://dom.spec.whatwg.org/#dom-element-classname String get className { final result = attributes['class']; return result ?? ''; } set className(String value) { attributes['class'] = value; } /// The set of CSS classes applied to this element. /// /// This set makes it easy to add, remove or toggle the classes applied to /// this element. /// /// element.classes.add('selected'); /// element.classes.toggle('isOnline'); /// element.classes.remove('selected'); CssClassSet get classes => ElementCssClassSet(this); } class Comment extends Node { String? data; Comment(this.data) : super._(); @override int get nodeType => Node.COMMENT_NODE; @override String toString() => ''; @override void _addOuterHtml(StringBuffer str) { str.write(''); } @override Comment clone(bool deep) => Comment(data); @override String? get text => data; @override set text(String? value) { data = value; } } // TODO(jmesserly): fix this to extend one of the corelib classes if possible. // (The requirement to remove the node from the old node list makes it tricky.) // TODO(jmesserly): is there any way to share code with the _NodeListImpl? class NodeList extends ListProxy { final Node _parent; NodeList._(this._parent); Node _setParent(Node node) { // Note: we need to remove the node from its previous parent node, if any, // before updating its parent pointer to point at our parent. node.remove(); node.parentNode = _parent; return node; } @override void add(Node element) { if (element is DocumentFragment) { addAll(element.nodes); } else { super.add(_setParent(element)); } } void addLast(Node value) => add(value); @override void addAll(Iterable iterable) { // Note: we need to be careful if collection is another NodeList. // In particular: // 1. we need to copy the items before updating their parent pointers, // _flattenDocFragments does a copy internally. // 2. we should update parent pointers in reverse order. That way they // are removed from the original NodeList (if any) from the end, which // is faster. final list = _flattenDocFragments(iterable); for (var node in list.reversed) { _setParent(node); } super.addAll(list); } @override void insert(int index, Node element) { if (element is DocumentFragment) { insertAll(index, element.nodes); } else { super.insert(index, _setParent(element)); } } @override Node removeLast() => super.removeLast()..parentNode = null; @override Node removeAt(int index) => super.removeAt(index)..parentNode = null; @override void clear() { for (var node in this) { node.parentNode = null; } super.clear(); } @override void operator []=(int index, Node value) { if (value is DocumentFragment) { removeAt(index); insertAll(index, value.nodes); } else { this[index].parentNode = null; super[index] = _setParent(value); } } // TODO(jmesserly): These aren't implemented in DOM _NodeListImpl, see // http://code.google.com/p/dart/issues/detail?id=5371 @override void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { var fromVar = iterable as List; if (fromVar is NodeList) { // Note: this is presumed to make a copy fromVar = fromVar.sublist(skipCount, skipCount + end); } // Note: see comment in [addAll]. We need to be careful about the order of // operations if [from] is also a NodeList. for (var i = end - 1; i >= 0; i--) { this[start + i] = fromVar[skipCount + i]; } } @override void replaceRange(int start, int end, Iterable newContents) { removeRange(start, end); insertAll(start, newContents); } @override void removeRange(int start, int end) { for (var i = start; i < end; i++) { this[i].parentNode = null; } super.removeRange(start, end); } @override void removeWhere(bool Function(Node) test) { for (var node in where(test)) { node.parentNode = null; } super.removeWhere(test); } @override void retainWhere(bool Function(Node) test) { for (var node in where((n) => !test(n))) { node.parentNode = null; } super.retainWhere(test); } @override void insertAll(int index, Iterable iterable) { // Note: we need to be careful how we copy nodes. See note in addAll. final list = _flattenDocFragments(iterable); for (var node in list.reversed) { _setParent(node); } super.insertAll(index, list); } List _flattenDocFragments(Iterable collection) { // Note: this function serves two purposes: // * it flattens document fragments // * it creates a copy of [collections] when `collection is NodeList`. final result = []; for (var node in collection) { if (node is DocumentFragment) { result.addAll(node.nodes); } else { result.add(node); } } return result; } } /// An indexable collection of a node's descendants in the document tree, /// filtered so that only elements are in the collection. // TODO(jmesserly): this was copied from dart:html // TODO(jmesserly): "implements List" is a workaround for analyzer bug. class FilteredElementList extends IterableBase with ListMixin implements List { final List _childNodes; /// Creates a collection of the elements that descend from a node. /// /// Example usage: /// /// var filteredElements = new FilteredElementList(query("#container")); /// // filteredElements is [a, b, c]. FilteredElementList(Node node) : _childNodes = node.nodes; // We can't memoize this, since it's possible that children will be messed // with externally to this class. // // TODO(nweiz): we don't always need to create a new list. For example // forEach, every, any, ... could directly work on the _childNodes. List get _filtered => _childNodes.whereType().toList(growable: false); @override void forEach(void Function(Element) action) { _filtered.forEach(action); } @override void operator []=(int index, Element value) { this[index].replaceWith(value); } @override set length(int newLength) { final len = length; if (newLength >= len) { return; } else if (newLength < 0) { throw ArgumentError('Invalid list length'); } removeRange(newLength, len); } @override String join([String separator = '']) => _filtered.join(separator); @override void add(Element element) { _childNodes.add(element); } @override void addAll(Iterable iterable) { for (var element in iterable) { add(element); } } @override bool contains(Object? element) { return element is Element && _childNodes.contains(element); } @override Iterable get reversed => _filtered.reversed; @override void sort([int Function(Element, Element)? compare]) { throw UnsupportedError('TODO(jacobr): should we impl?'); } @override void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { throw UnimplementedError(); } @override void fillRange(int start, int end, [Element? fill]) { throw UnimplementedError(); } @override void replaceRange(int start, int end, Iterable newContents) { throw UnimplementedError(); } @override void removeRange(int start, int end) { _filtered.sublist(start, end).forEach((el) => el.remove()); } @override void clear() { // Currently, ElementList#clear clears even non-element nodes, so we follow // that behavior. _childNodes.clear(); } @override Element removeLast() { return last..remove(); } @override Iterable map(T Function(Element) f) => _filtered.map(f); @override Iterable where(bool Function(Element) test) => _filtered.where(test); @override Iterable expand(Iterable Function(Element) f) => _filtered.expand(f); @override void insert(int index, Element element) { _childNodes.insert(index, element); } @override void insertAll(int index, Iterable iterable) { _childNodes.insertAll(index, iterable); } @override Element removeAt(int index) { final result = this[index]; result.remove(); return result; } @override bool remove(Object? element) { if (element is! Element) return false; for (var i = 0; i < length; i++) { final indexElement = this[i]; if (identical(indexElement, element)) { indexElement.remove(); return true; } } return false; } @override Element reduce(Element Function(Element, Element) combine) { return _filtered.reduce(combine); } @override T fold( T initialValue, T Function(T previousValue, Element element) combine) { return _filtered.fold(initialValue, combine); } @override bool every(bool Function(Element) test) => _filtered.every(test); @override bool any(bool Function(Element) test) => _filtered.any(test); @override List toList({bool growable = true}) => List.of(this, growable: growable); @override Set toSet() => Set.from(this); @override Element firstWhere(bool Function(Element) test, {Element Function()? orElse}) { return _filtered.firstWhere(test, orElse: orElse); } @override Element lastWhere(bool Function(Element) test, {Element Function()? orElse}) { return _filtered.lastWhere(test, orElse: orElse); } @override Element singleWhere(bool Function(Element) test, {Element Function()? orElse}) { if (orElse != null) throw UnimplementedError('orElse'); return _filtered.singleWhere(test); } @override Element elementAt(int index) { return this[index]; } @override bool get isEmpty => _filtered.isEmpty; @override int get length => _filtered.length; @override Element operator [](int index) => _filtered[index]; @override Iterator get iterator => _filtered.iterator; @override List sublist(int start, [int? end]) => _filtered.sublist(start, end); @override Iterable getRange(int start, int end) => _filtered.getRange(start, end); @override int indexOf(Object? element, [int start = 0]) => // Cast forced by ListMixin https://github.com/dart-lang/sdk/issues/31311 _filtered.indexOf(element as Element, start); @override int lastIndexOf(Object? element, [int? start]) { start ??= length - 1; // Cast forced by ListMixin https://github.com/dart-lang/sdk/issues/31311 return _filtered.lastIndexOf(element as Element, start); } @override Element get first => _filtered.first; @override Element get last => _filtered.last; @override Element get single => _filtered.single; } // http://dom.spec.whatwg.org/#dom-node-textcontent // For Element and DocumentFragment String _getText(Node node) => (_ConcatTextVisitor()..visit(node)).toString(); void _setText(Node node, String? value) { node.nodes.clear(); node.append(Text(value)); } class _ConcatTextVisitor extends TreeVisitor { final _str = StringBuffer(); @override String toString() => _str.toString(); @override void visitText(Text node) { _str.write(node.data); } }