blob: a42d73b4e7fd103e693993c138d08a0ff690f78e [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 = new Logger('WipDomModel');
final WipDom _dom;
Map<int, _Node> _nodeCache = {};
Future<_Node> _root;
Stream<AttributeModifiedEvent> onAttributeModified;
Stream<AttributeRemovedEvent> onAttributeRemoved;
Stream<CharacterDataModifiedEvent> onCharacterDataModified;
Stream<ChildNodeCountUpdatedEvent> onChildNodeCountUpdated;
Stream<ChildNodeInsertedEvent> onChildNodeInserted;
Stream<ChildNodeRemovedEvent> onChildNodeRemoved;
Stream<DocumentUpdatedEvent> onDocumentUpdated;
Stream<SetChildNodesEvent> onSetChildNodes;
WipDomModel(this._dom) {
onAttributeModified =
new StreamTransformer.fromHandlers(handleData: _onAttributeModified)
.bind(_dom.onAttributeModified)
..listen(_logEvent);
onAttributeRemoved =
new StreamTransformer.fromHandlers(handleData: _onAttributeRemoved)
.bind(_dom.onAttributeRemoved)
..listen(_logEvent);
onCharacterDataModified =
new StreamTransformer.fromHandlers(handleData: _onCharacterDataModified)
.bind(_dom.onCharacterDataModified)
..listen(_logEvent);
onChildNodeCountUpdated =
new StreamTransformer.fromHandlers(handleData: _onChildNodeCountUpdated)
.bind(_dom.onChildNodeCountUpdated)
..listen(_logEvent);
onChildNodeInserted =
new StreamTransformer.fromHandlers(handleData: _onChildNodeInserted)
.bind(_dom.onChildNodeInserted)
..listen(_logEvent);
onChildNodeRemoved =
new StreamTransformer.fromHandlers(handleData: _onChildNodeRemoved)
.bind(_dom.onChildNodeRemoved)
..listen(_logEvent);
onDocumentUpdated =
new StreamTransformer.fromHandlers(handleData: _onDocumentUpdated)
.bind(_dom.onDocumentUpdated)
..listen(_logEvent);
onSetChildNodes =
new StreamTransformer.fromHandlers(handleData: _onSetChildNodes)
.bind(_dom.onSetChildNodes)
..listen(_logEvent);
}
_logEvent(WipEvent event) {
_log.finest('Event $event');
}
_onAttributeModified(
AttributeModifiedEvent event, EventSink<AttributeModifiedEvent> sink) {
var node = _getOrCreateNode(event.nodeId);
node._attributes[event.name] = event.value;
sink.add(event);
}
_onAttributeRemoved(
AttributeRemovedEvent event, EventSink<AttributeRemovedEvent> sink) {
var node = _getOrCreateNode(event.nodeId);
node._attributes.remove(event.name);
sink.add(event);
}
_onCharacterDataModified(CharacterDataModifiedEvent event,
EventSink<CharacterDataModifiedEvent> sink) {
var node = _getOrCreateNode(event.nodeId);
node._nodeValue = event.characterData;
sink.add(event);
}
_onChildNodeCountUpdated(ChildNodeCountUpdatedEvent event,
EventSink<ChildNodeCountUpdatedEvent> sink) {
var node = _getOrCreateNode(event.nodeId);
node._childNodeCount = event.childNodeCount;
sink.add(event);
}
_onChildNodeInserted(
ChildNodeInsertedEvent event, EventSink<ChildNodeInsertedEvent> sink) {
var parent = _getOrCreateNode(event.parentNodeId);
int index = 0;
if (event.previousNodeId != null) {
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);
}
_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);
}
_onDocumentUpdated(
DocumentUpdatedEvent event, EventSink<DocumentUpdatedEvent> sink) {
_nodeCache.clear();
_root = null;
sink.add(event);
}
_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 = new 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() {
if (_root == null) {
_root = _dom.getDocument().then((n) => _getOrCreateNodeFromNode(n));
}
return _root;
}
_Node _getOrCreateNode(int nodeId) =>
_nodeCache.putIfAbsent(nodeId, () => new _Node(nodeId));
_Node _getOrCreateNodeFromNode(Node src) {
try {
var node = _getOrCreateNode(src.nodeId);
if (src.attributes != null) {
node._attributes = new Map.from(src.attributes);
}
if (src.children != null) {
node._children =
src.children.map(_getOrCreateNodeFromNode).toList(growable: true);
}
node._childNodeCount = src.childNodeCount;
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;
}
}
noSuchMethod(Invocation invocation) => reflect(_dom).delegate(invocation);
}
class _Node implements Node {
Map<String, String> _attributes;
@override
Map<String, String> get attributes =>
_attributes != null ? new UnmodifiableMapView(_attributes) : null;
int _childNodeCount;
@override
int get childNodeCount => _childNodeCount;
List<_Node> _children;
@override
List<Node> get children =>
_children != null ? new 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(new Set());
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 = [];
children.forEach((child) {
if (child != null) {
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) {
if (k != null) {
result..add(k)..add(v);
}
});
return result;
}