Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

Use DOM tree to debug browser against SimpleDOMBuilder #4658

Merged
merged 5 commits into from
Aug 6, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions src/LiveDevelopment/Agents/RemoteFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@


/*jslint vars: true, plusplus: true, browser: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */
/*global define, $, window, document, navigator */
/*global define, $, window, document, navigator, Node*/

/**
* RemoteFunctions define the functions to be executed in the browser. This
Expand Down Expand Up @@ -583,14 +583,50 @@ function RemoteFunctions(experimental) {
}
});
}

/**
*
* @param {Element} elem
*/
function _domElementToJSON(elem) {
var json = { tag: elem.tagName.toLowerCase(), attributes: {}, children: [] },
i,
len,
node;

len = elem.attributes.length;
for (i = 0; i < len; i++) {
node = elem.attributes.item(i);
json.attributes[node.name] = node.nodeValue;
}

len = elem.childNodes.length;
for (i = 0; i < len; i++) {
node = elem.childNodes.item(i);

// ignores comment nodes and visuals generated by live preview
if (node.nodeType === Node.ELEMENT_NODE && node.className !== HIGHLIGHT_CLASSNAME) {
json.children.push(_domElementToJSON(node));
} else if (node.nodeType === Node.TEXT_NODE) {
json.children.push({ content: node.nodeValue });
}
}

return json;
}

function getSimpleDOM() {
return JSON.stringify(_domElementToJSON(document.documentElement));
}

return {
"keepAlive": keepAlive,
"showGoto": showGoto,
"hideHighlight": hideHighlight,
"highlight": highlight,
"highlightRule": highlightRule,
"redrawHighlights": redrawHighlights,
"applyDOMEdits": applyDOMEdits
"keepAlive" : keepAlive,
"showGoto" : showGoto,
"hideHighlight" : hideHighlight,
"highlight" : highlight,
"highlightRule" : highlightRule,
"redrawHighlights" : redrawHighlights,
"applyDOMEdits" : applyDOMEdits,
"getSimpleDOM" : getSimpleDOM
};
}
43 changes: 38 additions & 5 deletions src/LiveDevelopment/Documents/HTMLDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ define(function HTMLDocumentModule(require, exports, module) {
RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"),
StringUtils = require("utils/StringUtils");

/** Constructor
*
* @param Document the source document from Brackets
/**
* Constructor
* @param {!DocumentManager.Document} doc the source document from Brackets
* @param {editor=} editor
*/
var HTMLDocument = function HTMLDocument(doc, editor) {
this.doc = doc;
Expand Down Expand Up @@ -150,14 +151,46 @@ define(function HTMLDocumentModule(require, exports, module) {
}
}
};

HTMLDocument.prototype._compareWithBrowser = function (change) {
var self = this;

RemoteAgent.call("getSimpleDOM").done(function (res) {
var browserSimpleDOM = JSON.parse(res.result.value),
edits = HTMLInstrumentation._getBrowserDiff(self.editor, browserSimpleDOM),
skipDelta,
node;

if (edits.length > 0) {
console.warn("Browser DOM does not match after change: " + JSON.stringify(change));

edits.forEach(function (delta) {
// ignore textDelete in html root element
node = browserSimpleDOM.nodeMap[delta.parentID];
skipDelta = node && node.tag === "html" && delta.type === "textDelete";

if (!skipDelta) {
console.log(delta);
}
});
}
});
};

/** Triggered on change by the editor */
HTMLDocument.prototype.onChange = function onChange(event, editor, change) {
// Only handles attribute changes currently.
// TODO: text changes should be easy to add
// TODO: if new tags are added, need to instrument them
var edits = HTMLInstrumentation.getUnappliedEditList(editor, change);
RemoteAgent.call("applyDOMEdits", edits);
var self = this,
edits = HTMLInstrumentation.getUnappliedEditList(editor, change),
applyEditsPromise = RemoteAgent.call("applyDOMEdits", edits);

// compare in-memory vs. in-browser DOM
// set a conditional breakpoint at the top of this function: "this._debug = true, false"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the comment, it seems like this is supposed to be conditional on a debug flag?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the comment.

applyEditsPromise.done(function () {
self._compareWithBrowser(change);
});

// var marker = HTMLInstrumentation._getMarkerAtDocumentPos(
// this.editor,
Expand Down
4 changes: 2 additions & 2 deletions src/LiveDevelopment/Inspector/inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<title>Inspector 1.0 Documentation</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/jquery-ui.min.js"></script>
<script src="http://twitter.github.com/bootstrap/assets/js/bootstrap-dropdown.js"></script>
<script src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>
<script>
$(function () {
var _domain = $(window.location.hash);
Expand Down Expand Up @@ -33,7 +33,7 @@
});
});
</script>
<link href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css" rel="stylesheet">
<link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.no-icons.min.css" rel="stylesheet">
<style>
body {padding: 70px 0 20px 0;}
h3, dl {font-family: Menlo, Monaco, "Courier New", monospace;}
Expand Down
134 changes: 105 additions & 29 deletions src/language/HTMLInstrumentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@ define(function (require, exports, module) {
return textNode.parent.children[childIndex - 1].tagID + "t";
}


function _getTextNodeHash(text) {
return MurmurHash3.hashString(text, text.length, seed);
}

SimpleDOMBuilder.prototype.build = function (strict) {
var self = this;
var token, tagLabel, lastClosedTag, lastIndex = 0;
Expand Down Expand Up @@ -450,9 +455,9 @@ define(function (require, exports, module) {
var newNode = {
parent: stack[stack.length - 1],
content: token.contents,
weight: token.contents.length,
signature: MurmurHash3.hashString(token.contents, token.contents.length, seed)
weight: token.contents.length
};
newNode.signature = _getTextNodeHash(newNode.content);
parent.children.push(newNode);
newNode.tagID = getTextNodeID(newNode);
nodeMap[newNode.tagID] = newNode;
Expand Down Expand Up @@ -844,7 +849,10 @@ define(function (require, exports, module) {
// extract forEach iterator callback
var queuePush = function (child) { queue.push(child); };

while (!!(currentElement = queue.shift())) {
do {
// we can assume queue is non-empty for the first loop iteration
currentElement = queue.shift();

oldElement = oldNode.nodeMap[currentElement.tagID];
if (oldElement) {
matches[currentElement.tagID] = true;
Expand All @@ -869,23 +877,30 @@ define(function (require, exports, module) {
currentElement.children.forEach(queuePush);
}
}
}
} while (queue.length);

Object.keys(matches).forEach(function (tagID) {
var currentElement;
var subtreeRoot = newNode.nodeMap[tagID];
if (subtreeRoot.children) {
attributeCompare(edits, oldNode.nodeMap[tagID], subtreeRoot);
var nav = new DOMNavigator(subtreeRoot);
while (!!(currentElement = nav.next())) {
var currentTagID = currentElement.tagID;
if (!oldNode.nodeMap[currentTagID]) {
// this condition can happen for new elements
continue;
var nav = new DOMNavigator(subtreeRoot),
currentElement;

// breadth-first traversal of DOM tree to diff attributes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually the main traversal that looks for diffs, not just diffing the attributes (though that's kind of subtle... the addition toe matches for currentTagID below is what the generated diff is based on). (In other words, the comment above should be something more like "breadth-first traversal of DOM tree to diff attributes and match up the old and new nodes").

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

while (true) {
currentElement = nav.next();

if (!currentElement) {
break;
}
matches[currentTagID] = true;
if (currentElement.children) {
attributeCompare(edits, oldNode.nodeMap[currentTagID], currentElement);

var currentTagID = currentElement.tagID;

if (oldNode.nodeMap[currentTagID]) {
matches[currentTagID] = true;
if (currentElement.children) {
attributeCompare(edits, oldNode.nodeMap[currentTagID], currentElement);
}
}
}
}
Expand Down Expand Up @@ -1001,21 +1016,82 @@ define(function (require, exports, module) {
}
}

/**
* @private
* Add SimpleDOMBuilder metadata to browser DOM tree JSON representation
* @param {Object} root
*/
function _processBrowserSimpleDOM(root) {
var nodeMap = {},
signatureMap = {};

function _processElement(elem) {
elem.tagID = elem.attributes["data-brackets-id"];
elem.weight = 0;

// remove data-brackets-id attribute for diff
delete elem.attributes["data-brackets-id"];

elem.children.forEach(function (child) {
// set parent
child.parent = elem;

if (child.children) {
_processElement(child);
} else if (child.content) {
child.weight = child.content.length;
child.signature = _getTextNodeHash(child.content);
child.tagID = getTextNodeID(child);

nodeMap[child.tagID] = child;
signatureMap[child.signature] = child;
}
});

elem.signature = _updateHash(elem);

nodeMap[elem.tagID] = elem;
signatureMap[elem.signature] = elem;
}

_processElement(root);

root.nodeMap = nodeMap;
root.signatureMap = signatureMap;
}

/**
* @private
* Diff the browser DOM with the in-editor DOM
* @param {Editor} editor
* @param {Object} browserSimpleDOM
*/
function _getBrowserDiff(editor, browserSimpleDOM) {
var cachedValue = _cachedValues[editor.document.file.fullPath],
editorDOM = cachedValue.dom;

_processBrowserSimpleDOM(browserSimpleDOM);

return domdiff(editorDOM, browserSimpleDOM);
}

$(DocumentManager).on("beforeDocumentDelete", _removeDocFromCache);

exports.scanDocument = scanDocument;
exports.generateInstrumentedHTML = generateInstrumentedHTML;
exports.getUnappliedEditList = getUnappliedEditList;

// For unit testing
exports._markText = _markText;
exports._getMarkerAtDocumentPos = _getMarkerAtDocumentPos;
exports._getTagIDAtDocumentPos = _getTagIDAtDocumentPos;
exports._buildSimpleDOM = _buildSimpleDOM;
exports._markTextFromDOM = _markTextFromDOM;
exports._updateDOM = _updateDOM;
exports._DOMNavigator = DOMNavigator;
exports._seed = seed;
exports._allowIncremental = allowIncremental;
exports._dumpDOM = _dumpDOM;
// private methods
exports._markText = _markText;
exports._getMarkerAtDocumentPos = _getMarkerAtDocumentPos;
exports._getTagIDAtDocumentPos = _getTagIDAtDocumentPos;
exports._buildSimpleDOM = _buildSimpleDOM;
exports._markTextFromDOM = _markTextFromDOM;
exports._updateDOM = _updateDOM;
exports._DOMNavigator = DOMNavigator;
exports._seed = seed;
exports._allowIncremental = allowIncremental;
exports._dumpDOM = _dumpDOM;
exports._getBrowserDiff = _getBrowserDiff;

// public API
exports.scanDocument = scanDocument;
exports.generateInstrumentedHTML = generateInstrumentedHTML;
exports.getUnappliedEditList = getUnappliedEditList;
});