blob: 9bb5f052d67f10d195e76622009db647c9dc70c5 [file] [log] [blame]
// 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<int, _Node> _nodeCache = {};
Future<_Node>? _root;
@override
late final Stream<AttributeModifiedEvent> onAttributeModified =
StreamTransformer.fromHandlers(handleData: _onAttributeModified)
.bind(_dom.onAttributeModified);
@override
late final Stream<AttributeRemovedEvent> onAttributeRemoved =
StreamTransformer.fromHandlers(handleData: _onAttributeRemoved)
.bind(_dom.onAttributeRemoved);
@override
late final Stream<CharacterDataModifiedEvent> onCharacterDataModified =
StreamTransformer.fromHandlers(handleData: _onCharacterDataModified)
.bind(_dom.onCharacterDataModified);
@override
late final Stream<ChildNodeCountUpdatedEvent> onChildNodeCountUpdated =
StreamTransformer.fromHandlers(handleData: _onChildNodeCountUpdated)
.bind(_dom.onChildNodeCountUpdated);
@override
late final Stream<ChildNodeInsertedEvent> onChildNodeInserted =
StreamTransformer.fromHandlers(handleData: _onChildNodeInserted)
.bind(_dom.onChildNodeInserted);
@override
late final Stream<ChildNodeRemovedEvent> onChildNodeRemoved =
StreamTransformer.fromHandlers(handleData: _onChildNodeRemoved)
.bind(_dom.onChildNodeRemoved);
@override
late final Stream<DocumentUpdatedEvent> onDocumentUpdated =
StreamTransformer.fromHandlers(handleData: _onDocumentUpdated)
.bind(_dom.onDocumentUpdated);
@override
late final Stream<SetChildNodesEvent> 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<AttributeModifiedEvent> sink) {
var node = _getOrCreateNode(event.nodeId);
node._attributes![event.name] = event.value;
sink.add(event);
}
void _onAttributeRemoved(
AttributeRemovedEvent event, EventSink<AttributeRemovedEvent> sink) {
var node = _getOrCreateNode(event.nodeId);
node._attributes!.remove(event.name);
sink.add(event);
}
void _onCharacterDataModified(CharacterDataModifiedEvent event,
EventSink<CharacterDataModifiedEvent> sink) {
var node = _getOrCreateNode(event.nodeId);
node._nodeValue = event.characterData;
sink.add(event);
}
void _onChildNodeCountUpdated(ChildNodeCountUpdatedEvent event,
EventSink<ChildNodeCountUpdatedEvent> sink) {
var node = _getOrCreateNode(event.nodeId);
node._childNodeCount = event.childNodeCount;
sink.add(event);
}
void _onChildNodeInserted(
ChildNodeInsertedEvent event, EventSink<ChildNodeInsertedEvent> 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<ChildNodeRemovedEvent> 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<DocumentUpdatedEvent> sink) {
_nodeCache.clear();
_root = null;
sink.add(event);
}
void _onSetChildNodes(
SetChildNodesEvent event, EventSink<SetChildNodesEvent> 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<Map<String, String>> getAttributes(int nodeId) async {
Map<String, String> 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<Node> 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<String, String>? _attributes;
@override
Map<String, String>? get attributes =>
_attributes != null ? UnmodifiableMapView(_attributes!) : null;
int? _childNodeCount;
@override
int? get childNodeCount => _childNodeCount;
List<_Node>? _children;
@override
List<Node>? 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<String> flattenAttributesMap(Map<String, String> attributes) {
var result = <String>[];
attributes.forEach((k, v) {
result
..add(k)
..add(v);
});
return result;
}