// Copyright 2015 Google. All rights reserved. Use of this source code is // governed by a BSD-style license that can be found in the LICENSE file. library wip.dom_model; import 'dart:async' show EventSink, Future, Stream, StreamTransformer; import 'dart:collection' show UnmodifiableListView, UnmodifiableMapView; import 'dart:mirrors' show reflect; import 'package:logging/logging.dart' show Logger; import 'webkit_inspection_protocol.dart' show AttributeModifiedEvent, AttributeRemovedEvent, CharacterDataModifiedEvent, ChildNodeCountUpdatedEvent, ChildNodeInsertedEvent, ChildNodeRemovedEvent, DocumentUpdatedEvent, Node, SetChildNodesEvent, WipDom, WipEvent; /// Implementation of WipDom that maintains and updates a model of the DOM /// based on incoming events. class WipDomModel implements WipDom { static final _log = Logger('WipDomModel'); final WipDom _dom; final Map _nodeCache = {}; Future<_Node>? _root; @override late final Stream onAttributeModified = StreamTransformer.fromHandlers(handleData: _onAttributeModified) .bind(_dom.onAttributeModified); @override late final Stream onAttributeRemoved = StreamTransformer.fromHandlers(handleData: _onAttributeRemoved) .bind(_dom.onAttributeRemoved); @override late final Stream onCharacterDataModified = StreamTransformer.fromHandlers(handleData: _onCharacterDataModified) .bind(_dom.onCharacterDataModified); @override late final Stream onChildNodeCountUpdated = StreamTransformer.fromHandlers(handleData: _onChildNodeCountUpdated) .bind(_dom.onChildNodeCountUpdated); @override late final Stream onChildNodeInserted = StreamTransformer.fromHandlers(handleData: _onChildNodeInserted) .bind(_dom.onChildNodeInserted); @override late final Stream onChildNodeRemoved = StreamTransformer.fromHandlers(handleData: _onChildNodeRemoved) .bind(_dom.onChildNodeRemoved); @override late final Stream onDocumentUpdated = StreamTransformer.fromHandlers(handleData: _onDocumentUpdated) .bind(_dom.onDocumentUpdated); @override late final Stream onSetChildNodes = StreamTransformer.fromHandlers(handleData: _onSetChildNodes) .bind(_dom.onSetChildNodes); WipDomModel(this._dom) { onAttributeModified.listen(_logEvent); onAttributeRemoved.listen(_logEvent); onCharacterDataModified.listen(_logEvent); onChildNodeCountUpdated.listen(_logEvent); onChildNodeInserted.listen(_logEvent); onChildNodeRemoved.listen(_logEvent); onDocumentUpdated.listen(_logEvent); onSetChildNodes.listen(_logEvent); } void _logEvent(WipEvent event) { _log.finest('Event $event'); } void _onAttributeModified( AttributeModifiedEvent event, EventSink sink) { var node = _getOrCreateNode(event.nodeId); node._attributes![event.name] = event.value; sink.add(event); } void _onAttributeRemoved( AttributeRemovedEvent event, EventSink sink) { var node = _getOrCreateNode(event.nodeId); node._attributes!.remove(event.name); sink.add(event); } void _onCharacterDataModified(CharacterDataModifiedEvent event, EventSink sink) { var node = _getOrCreateNode(event.nodeId); node._nodeValue = event.characterData; sink.add(event); } void _onChildNodeCountUpdated(ChildNodeCountUpdatedEvent event, EventSink sink) { var node = _getOrCreateNode(event.nodeId); node._childNodeCount = event.childNodeCount; sink.add(event); } void _onChildNodeInserted( ChildNodeInsertedEvent event, EventSink sink) { var parent = _getOrCreateNode(event.parentNodeId); int index = parent._children!.indexOf(_getOrCreateNode(event.previousNodeId)) + 1; var node = _getOrCreateNodeFromNode(event.node); parent._children!.insert(index, node); parent._childNodeCount = parent._children!.length; sink.add(event); } void _onChildNodeRemoved( ChildNodeRemovedEvent event, EventSink sink) { var parent = _getOrCreateNode(event.parentNodeId); var node = _nodeCache.remove(event.nodeId); parent._children!.remove(node); parent._childNodeCount = parent._children!.length; sink.add(event); } void _onDocumentUpdated( DocumentUpdatedEvent event, EventSink sink) { _nodeCache.clear(); _root = null; sink.add(event); } void _onSetChildNodes( SetChildNodesEvent event, EventSink sink) { var parent = _getOrCreateNode(event.nodeId); parent._children = event.nodes.map(_getOrCreateNodeFromNode).toList(growable: true); parent._childNodeCount = parent._children!.length; sink.add(event); } @override Future> getAttributes(int nodeId) async { Map attributes = await _dom.getAttributes(nodeId); var node = _getOrCreateNode(nodeId); node._attributes = Map.from(attributes); return attributes; } /// Unlike the standard [WipDom.getDocument] call, this will not /// reset the internal state of the debugger remote end when called /// multiple times on the same page. @override Future getDocument() { _root ??= _dom.getDocument().then((n) => _getOrCreateNodeFromNode(n)); return _root!; } _Node _getOrCreateNode(int nodeId) => _nodeCache.putIfAbsent(nodeId, () => _Node(nodeId)); _Node _getOrCreateNodeFromNode(Node src) { try { var node = _getOrCreateNode(src.nodeId); if (src.attributes != null) { node._attributes = Map.of(src.attributes!); } if (src.children != null) { node._children = src.children!.map(_getOrCreateNodeFromNode).toList(growable: true); } node._childNodeCount = src.childNodeCount ?? 0; if (src.contentDocument != null) { node._contentDocument = _getOrCreateNodeFromNode(src.contentDocument!); } node._documentUrl = src.documentUrl; node._internalSubset = src.internalSubset; node._localName = src.localName; node._name = src.name; node._nodeName = src.nodeName; node._nodeType = src.nodeType; node._nodeValue = src.nodeValue; node._publicId = src.publicId; node._systemId = src.systemId; node._value = src.value; node._xmlVersion = src.xmlVersion; return node; } catch (e, s) { _log.severe('Error parsing: $src.nodeId', e, s); rethrow; } } @override dynamic noSuchMethod(Invocation invocation) => reflect(_dom).delegate(invocation); } class _Node implements Node { Map? _attributes; @override Map? get attributes => _attributes != null ? UnmodifiableMapView(_attributes!) : null; int? _childNodeCount; @override int? get childNodeCount => _childNodeCount; List<_Node>? _children; @override List? get children => _children != null ? UnmodifiableListView(_children!) : null; _Node? _contentDocument; @override Node? get contentDocument => _contentDocument; String? _documentUrl; @override String? get documentUrl => _documentUrl; String? _internalSubset; @override String? get internalSubset => _internalSubset; String? _localName; @override String get localName => _localName!; String? _name; @override String? get name => _name; @override final int nodeId; String? _nodeName; @override String get nodeName => _nodeName!; int? _nodeType; @override int get nodeType => _nodeType!; String? _nodeValue; @override String get nodeValue => _nodeValue!; String? _publicId; @override String? get publicId => _publicId; String? _systemId; @override String? get systemId => _systemId; String? _value; @override String? get value => _value; String? _xmlVersion; @override String? get xmlVersion => _xmlVersion; _Node(this.nodeId); Map toJson() => _toJsonInternal({}); Map _toJsonInternal(Set visited) { var map = { 'localName': localName, 'nodeId': nodeId, 'nodeName': nodeName, 'nodeType': nodeType, 'nodeValue': nodeValue }; if (visited.add(nodeId)) { if (attributes != null && attributes!.isNotEmpty) { map['attributes'] = flattenAttributesMap(attributes!); } if (childNodeCount != null) { map['childNodeCount'] = childNodeCount!; } if (_children != null && _children!.isNotEmpty) { var newChildren = []; for (var child in _children!) { newChildren.add(child._toJsonInternal(visited)); } map['children'] = newChildren; } if (_contentDocument != null) { map['contentDocument'] = _contentDocument!._toJsonInternal(visited); } if (documentUrl != null) { map['documentUrl'] = documentUrl!; } if (internalSubset != null) { map['internalSubset'] = internalSubset!; } if (name != null) { map['name'] = name!; } if (publicId != null) { map['publicId'] = publicId!; } if (systemId != null) { map['systemId'] = systemId!; } if (value != null) { map['value'] = value!; } if (xmlVersion != null) { map['xmlVersion'] = xmlVersion!; } } return map; } } List flattenAttributesMap(Map attributes) { var result = []; attributes.forEach((k, v) { result ..add(k) ..add(v); }); return result; }