Web Inspector: DOM.performSearch should accept a list of context nodes
authorachicu@adobe.com <achicu@adobe.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 15 Nov 2013 22:08:13 +0000 (22:08 +0000)
committerachicu@adobe.com <achicu@adobe.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 15 Nov 2013 22:08:13 +0000 (22:08 +0000)
https://bugs.webkit.org/show_bug.cgi?id=124390

Reviewed by Timothy Hatcher.

Source/WebCore:

Extracted the code in InspectorDOMAgent::performSearch into its own helper class
called InspectorNodeFinder. Also added a new array parameter called "nodeIds"
that can be used to limit the search results to just partial subtrees.

Tests: inspector-protocol/dom/dom-search-crash.html
       inspector-protocol/dom/dom-search-with-context.html
       inspector-protocol/dom/dom-search.html

* CMakeLists.txt:
* GNUmakefile.list.am:
* WebCore.vcxproj/WebCore.vcxproj:
* WebCore.vcxproj/WebCore.vcxproj.filters:
* WebCore.xcodeproj/project.pbxproj:
* inspector/protocol/DOM.json:
* inspector/InspectorAllInOne.cpp:
* inspector/InspectorDOMAgent.cpp:
(WebCore::InspectorDOMAgent::performSearch):
* inspector/InspectorDOMAgent.h:
* inspector/InspectorNodeFinder.cpp: Added.
(WebCore::stripCharacters):
(WebCore::InspectorNodeFinder::InspectorNodeFinder):
(WebCore::InspectorNodeFinder::performSearch):
(WebCore::InspectorNodeFinder::searchUsingDOMTreeTraversal):
(WebCore::InspectorNodeFinder::matchesAttribute):
(WebCore::InspectorNodeFinder::matchesElement):
(WebCore::InspectorNodeFinder::searchUsingXPath):
(WebCore::InspectorNodeFinder::searchUsingCSSSelectors):
* inspector/InspectorNodeFinder.h: Added.
(WebCore::InspectorNodeFinder::results):

LayoutTests:

Added new inspector-protocol tests to check for the DOM.performSearch implementation.
Also, ported an existing test from the old "inspector" format.

* http/tests/inspector-protocol/resources/InspectorDOMListener.js: Added boilerplate code that
can be used to track node ids and class names.
(createDOMListener.createNodeAttributesMap):
(createDOMListener.collectNode):
(createDOMListener.onSetChildNodes):
(createDOMListener.onChildNodeRemoved):
(createDOMListener.onChildNodeInserted):
(createDOMListener.return.getNodeById):
(createDOMListener.return.getNodeIdentifier):
* http/tests/inspector-protocol/resources/InspectorTest.js:
(InspectorFrontendAPI.dispatchMessageAsync): Added a way to catch all the messages received in the inspector.
It is useful for debugging the test file.
(InspectorTest.addEventListener): Helper method to register multiple handlers for the same event.
* inspector-protocol/dom/dom-search-crash-expected.txt: Added.
* inspector-protocol/dom/dom-search-crash.html: Added.
* inspector-protocol/dom/dom-search-expected.txt: Added.
* inspector-protocol/dom/dom-search-with-context-expected.txt: Added.
* inspector-protocol/dom/dom-search-with-context.html: Added.
* inspector-protocol/dom/dom-search.html: Added.
* inspector-protocol/dom/resources/dom-search-crash-iframe.html: Cloned from inspector/dom/resources/dom-search-crash-iframe.html
* inspector-protocol/dom/resources/dom-search-iframe.html: Added.
* inspector-protocol/dom/resources/dom-search-queries.js: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@159357 268f45cc-cd09-0410-ab3c-d52691b4dbfc

24 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/inspector-protocol/resources/InspectorDOMListener.js [new file with mode: 0644]
LayoutTests/http/tests/inspector-protocol/resources/InspectorTest.js
LayoutTests/inspector-protocol/dom/dom-search-crash-expected.txt [new file with mode: 0644]
LayoutTests/inspector-protocol/dom/dom-search-crash.html [new file with mode: 0644]
LayoutTests/inspector-protocol/dom/dom-search-expected.txt [new file with mode: 0644]
LayoutTests/inspector-protocol/dom/dom-search-with-context-expected.txt [new file with mode: 0644]
LayoutTests/inspector-protocol/dom/dom-search-with-context.html [new file with mode: 0644]
LayoutTests/inspector-protocol/dom/dom-search.html [new file with mode: 0644]
LayoutTests/inspector-protocol/dom/resources/dom-search-crash-iframe.html [new file with mode: 0644]
LayoutTests/inspector-protocol/dom/resources/dom-search-iframe.html [new file with mode: 0644]
LayoutTests/inspector-protocol/dom/resources/dom-search-queries.js [new file with mode: 0644]
Source/WebCore/CMakeLists.txt
Source/WebCore/ChangeLog
Source/WebCore/GNUmakefile.list.am
Source/WebCore/WebCore.vcxproj/WebCore.vcxproj
Source/WebCore/WebCore.vcxproj/WebCore.vcxproj.filters
Source/WebCore/WebCore.xcodeproj/project.pbxproj
Source/WebCore/inspector/InspectorAllInOne.cpp
Source/WebCore/inspector/InspectorDOMAgent.cpp
Source/WebCore/inspector/InspectorDOMAgent.h
Source/WebCore/inspector/InspectorNodeFinder.cpp [new file with mode: 0644]
Source/WebCore/inspector/InspectorNodeFinder.h [new file with mode: 0644]
Source/WebCore/inspector/protocol/DOM.json

index 03b8738..6178955 100644 (file)
@@ -1,3 +1,36 @@
+2013-11-15  Alexandru Chiculita  <achicu@adobe.com>
+
+        Web Inspector: DOM.performSearch should accept a list of context nodes
+        https://bugs.webkit.org/show_bug.cgi?id=124390
+
+        Reviewed by Timothy Hatcher.
+
+        Added new inspector-protocol tests to check for the DOM.performSearch implementation.
+        Also, ported an existing test from the old "inspector" format.
+
+        * http/tests/inspector-protocol/resources/InspectorDOMListener.js: Added boilerplate code that
+        can be used to track node ids and class names.
+        (createDOMListener.createNodeAttributesMap):
+        (createDOMListener.collectNode):
+        (createDOMListener.onSetChildNodes):
+        (createDOMListener.onChildNodeRemoved):
+        (createDOMListener.onChildNodeInserted):
+        (createDOMListener.return.getNodeById):
+        (createDOMListener.return.getNodeIdentifier):
+        * http/tests/inspector-protocol/resources/InspectorTest.js:
+        (InspectorFrontendAPI.dispatchMessageAsync): Added a way to catch all the messages received in the inspector.
+        It is useful for debugging the test file.
+        (InspectorTest.addEventListener): Helper method to register multiple handlers for the same event.
+        * inspector-protocol/dom/dom-search-crash-expected.txt: Added.
+        * inspector-protocol/dom/dom-search-crash.html: Added.
+        * inspector-protocol/dom/dom-search-expected.txt: Added.
+        * inspector-protocol/dom/dom-search-with-context-expected.txt: Added.
+        * inspector-protocol/dom/dom-search-with-context.html: Added.
+        * inspector-protocol/dom/dom-search.html: Added.
+        * inspector-protocol/dom/resources/dom-search-crash-iframe.html: Cloned from inspector/dom/resources/dom-search-crash-iframe.html
+        * inspector-protocol/dom/resources/dom-search-iframe.html: Added.
+        * inspector-protocol/dom/resources/dom-search-queries.js: Added.
+
 2013-11-15  Tim Horton  <timothy_horton@apple.com>
 
         Make lint-test-expectations pass for platform/win
diff --git a/LayoutTests/http/tests/inspector-protocol/resources/InspectorDOMListener.js b/LayoutTests/http/tests/inspector-protocol/resources/InspectorDOMListener.js
new file mode 100644 (file)
index 0000000..44eb219
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2013 Adobe Systems Incorporated. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer.
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+ * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+function createDOMListener()
+{
+    var nodesById = {};
+
+    InspectorTest.addEventListener("DOM.setChildNodes", onSetChildNodes);
+    InspectorTest.addEventListener("DOM.childNodeRemoved", onChildNodeRemoved);
+    InspectorTest.addEventListener("DOM.childNodeInserted", onChildNodeInserted);
+
+    function createNodeAttributesMap(attributes)
+    {
+        var attributesMap = {};
+        for (var i = 0; i < attributes.length; i += 2)
+            attributesMap[attributes[i]] = attributes[i + 1];
+        return attributesMap;
+    }
+
+    function collectNode(node)
+    {
+        if (node.nodeType === 1)
+            node.attributes = createNodeAttributesMap(node.attributes);
+        if (node.children)
+            node.children.forEach(collectNode);
+        nodesById[node.nodeId] = node;
+    }
+
+    function nodeToString(node)
+    {
+        switch (node.nodeType) {
+        case 1:
+            var name = node.localName;
+            if (node.attributes.id)
+                name += "#" + node.attributes.id;
+            if (node.attributes["class"])
+                name += node.attributes["class"].split(" ").map(function(className) { return "." + className; }).join("");
+            return name;
+        case 3:
+            return "<text node " + JSON.stringify(node.nodeValue) + ">";
+        default:
+            return "<nodeType " + node.nodeType + ">";
+        }
+    }
+
+    function onSetChildNodes(response)
+    {
+        response.params.nodes.forEach(collectNode);
+    }
+
+    function onChildNodeRemoved(response)
+    {
+        delete nodesById[response.params.nodeId];
+    }
+
+    function onChildNodeInserted(response)
+    {
+        collectNode(response.params.node);
+    }
+
+    return {
+        getNodeById: function(id)
+        {
+            return nodesById[id];
+        },
+
+        getNodeIdentifier: function(nodeId)
+        {
+            if (!nodeId)
+                return "<invalid node id>";
+            var node = nodesById[nodeId];
+            return node ? nodeToString(node) : "<unknown node>";
+        },
+
+        collectNode: collectNode
+    };
+}
index 8e0fb6e..3beed64 100644 (file)
@@ -62,10 +62,38 @@ InspectorFrontendAPI.dispatchMessageAsync = function(messageObject)
         var eventHandler = InspectorTest.eventHandler[eventName];
         if (eventHandler)
             eventHandler(messageObject);
+        else if (InspectorTest.defaultEventHandler)
+            InspectorTest.defaultEventHandler(messageObject);
     }
 }
 
 /**
+* Registers an event handler for messages coming from the InspectorBackend.
+* If multiple callbacks are registered for the same event, it will chain the execution.
+* @param {string} event name
+* @param {function} handler to be executed
+* @param {boolean} execute the handler before all other handlers
+*/
+InspectorTest.addEventListener = function(eventName, callback, capture)
+{
+    if (!InspectorTest.eventHandler[eventName]) {
+        InspectorTest.eventHandler[eventName] = callback;
+        return;
+    }
+    var firstHandler = InspectorTest.eventHandler[eventName];
+    var secondHandler = callback;
+    if (capture) {
+        // Swap firstHandler with the new callback, so that we execute the callback first.
+        [firstHandler, secondHandler] = [secondHandler, firstHandler];
+    }
+    InspectorTest.eventHandler[eventName] = function(messageObject)
+    {
+        firstHandler(messageObject);
+        secondHandler(messageObject);
+    };
+}
+
+/**
 * Logs message to document.
 * @param {string} message
 */
@@ -163,7 +191,7 @@ InspectorTest.importInspectorScripts = function()
         InspectorTest.importScript("../../../../../Source/WebInspectorUI/UserInterface/" + inspectorScripts[i] + ".js");
 
     // The initialization should be in sync with WebInspector.loaded in Main.js.
-    // FIXME: As soon as we can support all the observers and managers we should remove UI related tasks 
+    // FIXME: As soon as we can support all the observers and managers we should remove UI related tasks
     // from WebInspector.loaded, so that it can be used from the LayoutTests.
 
     InspectorBackend.registerPageDispatcher(new WebInspector.PageObserver);
diff --git a/LayoutTests/inspector-protocol/dom/dom-search-crash-expected.txt b/LayoutTests/inspector-protocol/dom/dom-search-crash-expected.txt
new file mode 100644 (file)
index 0000000..2631e1c
--- /dev/null
@@ -0,0 +1,5 @@
+Tests that elements panel search is not crashing on documentElement-less cases.
+
+
+PASS: Test passes if it doesn't crash.
+
diff --git a/LayoutTests/inspector-protocol/dom/dom-search-crash.html b/LayoutTests/inspector-protocol/dom/dom-search-crash.html
new file mode 100644 (file)
index 0000000..09a925d
--- /dev/null
@@ -0,0 +1,27 @@
+<html>
+<head>
+<script type="text/javascript" src="../../http/tests/inspector-protocol/resources/protocol-test.js"></script>
+<script>
+function test()
+{
+    InspectorTest.sendCommand("DOM.getDocument", {}, onGotDocument);
+
+    function onGotDocument(message) {
+        InspectorTest.checkForError(message);
+        InspectorTest.sendCommand("DOM.performSearch", {query: "FooBar"}, onSearchCompleted);
+    }
+
+    function onSearchCompleted(message)
+    {
+        InspectorTest.checkForError(message);
+        InspectorTest.log("PASS: Test passes if it doesn't crash.");
+        InspectorTest.completeTest();
+    }
+}
+</script>
+</head>
+<body>
+<p>Tests that elements panel search is not crashing on documentElement-less cases.</p>
+<iframe src="resources/dom-search-crash-iframe.html" onload="runTest()"></iframe>
+</body>
+</html>
diff --git a/LayoutTests/inspector-protocol/dom/dom-search-expected.txt b/LayoutTests/inspector-protocol/dom/dom-search-expected.txt
new file mode 100644 (file)
index 0000000..401ebd7
--- /dev/null
@@ -0,0 +1,58 @@
+Testing DOM.performSearch with no parent node ids.
+
+
+=== Query: "body" ===
+Count: 2
+body.main-frame
+body.inside-iframe
+=== Query: "<body" ===
+Count: 2
+body.main-frame
+body.inside-iframe
+=== Query: "body>" ===
+Count: 2
+body.main-frame
+body.inside-iframe
+=== Query: "<body>" ===
+Count: 2
+body.main-frame
+body.inside-iframe
+=== Query: "onload" ===
+Count: 1
+body.main-frame
+=== Query: "runTest()" ===
+Count: 1
+body.main-frame
+=== Query: "\"runTest()" ===
+Count: 1
+body.main-frame
+=== Query: "\"runTest()\"" ===
+Count: 1
+body.main-frame
+=== Query: "runTest()\"" ===
+Count: 1
+body.main-frame
+=== Query: ".body-inside-iframe" ===
+Count: 0
+=== Query: "*" ===
+Count: 12
+html
+head
+script
+script
+body.main-frame
+p
+iframe
+html.inside-iframe
+head.inside-iframe
+body.inside-iframe
+div.base1.inside-iframe
+p.inside-iframe
+=== Query: "/html/body" ===
+Count: 2
+body.main-frame
+body.inside-iframe
+=== Query: "/html/body/@onload" ===
+Count: 1
+body.main-frame
+
diff --git a/LayoutTests/inspector-protocol/dom/dom-search-with-context-expected.txt b/LayoutTests/inspector-protocol/dom/dom-search-with-context-expected.txt
new file mode 100644 (file)
index 0000000..99e4431
--- /dev/null
@@ -0,0 +1,25 @@
+Testing DOM.performSearch with parent node ids.
+
+
+=== Query: "p" in [] ===
+Count: 0
+=== Query: "p" in [.base1] ===
+Count: 1
+p.base1.main-frame
+=== Query: "p" in [.base2] ===
+Count: 1
+p.base2.main-frame
+=== Query: "p" in [.base1, .base2] ===
+Count: 2
+p.base1.main-frame
+p.base2.main-frame
+=== Query: "p" in [iframe] ===
+Count: 1
+p.inside-iframe
+=== Query: "//p" in [.base1] ===
+Count: 1
+p.base1.main-frame
+=== Query: "//div" in [.base1] ===
+Count: 1
+div.base1
+
diff --git a/LayoutTests/inspector-protocol/dom/dom-search-with-context.html b/LayoutTests/inspector-protocol/dom/dom-search-with-context.html
new file mode 100644 (file)
index 0000000..7ed8329
--- /dev/null
@@ -0,0 +1,129 @@
+<html>
+<head>
+<script type="text/javascript" src="../../http/tests/inspector-protocol/resources/protocol-test.js"></script>
+<script>
+function test()
+{
+    // Create a DOM listener to convert nodeIds to tag names.
+    InspectorTest.importScript("InspectorDOMListener.js");
+    var dom = createDOMListener();
+
+    // Caching the output to avoid searching through the log.
+    var output = [];
+    var documentId;
+    var domSearchQueries = [
+        {
+            query: "p",
+            nodes: []
+        },
+        {
+            query: "p",
+            nodes: [".base1"]
+        },
+        {
+            query: "p",
+            nodes: [".base2"]
+        },
+        {
+            query: "p",
+            nodes: [".base1", ".base2"]
+        },
+        {
+            query: "p",
+            nodes: ["iframe"]
+        },
+
+        // XPath should return just the children of the selected nodes.
+        {
+            query: "//p",
+            nodes: [".base1"]
+        },
+        {
+            query: "//div",
+            nodes: [".base1"]
+        }
+    ];
+
+    InspectorTest.sendCommand("DOM.getDocument", {}, onGotDocument);
+
+    function onGotDocument(message) {
+        InspectorTest.checkForError(message);
+        dom.collectNode(message.result.root);
+        documentId = message.result.root.nodeId;
+        performSearches(domSearchQueries, testFinished);
+    }
+
+    function performSearches(list, callback)
+    {
+        function next() {
+            if (list.length)
+                search(list.shift(), next);
+            else
+                callback();
+        }
+        next();
+    }
+
+    function search(queryData, callback)
+    {
+        resolveSelectors(queryData.nodes, function(nodeIds) {
+            output.push("=== Query: " + JSON.stringify(queryData.query) + " in [" + queryData.nodes.join(", ") + "] ===");
+            InspectorTest.sendCommand("DOM.performSearch", {query: queryData.query, nodeIds: nodeIds}, function(message) {
+                InspectorTest.checkForError(message);
+                printSearchResults(message.result, callback);
+            });
+        });
+    }
+
+    function resolveSelectors(nodes, callback)
+    {
+        var results = new Array(nodes.length);
+        var remaining = nodes.length;
+        if (!remaining)
+            return callback(results);
+        nodes.forEach(function(selector, index) {
+            InspectorTest.sendCommand("DOM.querySelector", {nodeId: documentId, selector: selector}, function(message) {
+                InspectorTest.checkForError(message);
+                results[index] = message.result.nodeId;
+                if (--remaining <= 0)
+                    callback(results);
+            });
+        });
+    }
+
+    function printSearchResults(results, callback)
+    {
+        output.push("Count: " + results.resultCount);
+        if (!results.resultCount)
+            return callback();
+
+        var options = {"searchId": results.searchId, "fromIndex": 0, "toIndex": results.resultCount};
+        InspectorTest.sendCommand("DOM.getSearchResults", options, function(message) {
+            for (var nodeId of message.result.nodeIds)
+                output.push(dom.getNodeIdentifier(nodeId));
+            callback();
+        });
+    }
+
+    function testFinished()
+    {
+        InspectorTest.log(output.join("\n"));
+        InspectorTest.completeTest();
+    }
+}
+</script>
+</head>
+<body onload="runTest()" class="main-frame">
+    <p>Testing DOM.performSearch with parent node ids.</p>
+
+    <div class="base1">
+        <p class="base1 main-frame"></p>
+    </div>
+
+    <div class="base2">
+        <p class="base2 main-frame"></p>
+    </div>
+
+    <iframe src="resources/dom-search-iframe.html"></iframe>
+</body>
+</html>
diff --git a/LayoutTests/inspector-protocol/dom/dom-search.html b/LayoutTests/inspector-protocol/dom/dom-search.html
new file mode 100644 (file)
index 0000000..5b0cfdc
--- /dev/null
@@ -0,0 +1,71 @@
+<html>
+<head>
+<script type="text/javascript" src="../../http/tests/inspector-protocol/resources/protocol-test.js"></script>
+<script>
+function test()
+{
+    // Loading the queries from external file to avoid having them show up in the results.
+    InspectorTest.importScript("../../../../inspector-protocol/dom/resources/dom-search-queries.js");
+
+    // Create a DOM listener to convert nodeIds to tag names.
+    InspectorTest.importScript("InspectorDOMListener.js");
+    var dom = createDOMListener();
+
+    // Caching the output to avoid searching through the log.
+    var output = [];
+
+    InspectorTest.sendCommand("DOM.getDocument", {}, onGotDocument);
+
+    function onGotDocument(message) {
+        InspectorTest.checkForError(message);
+        dom.collectNode(message.result.root);
+        performSearches(domSearchQueries, testFinished);
+    }
+
+    function performSearches(list, callback)
+    {
+        function next() {
+            if (list.length)
+                search(list.shift(), next);
+            else
+                callback();
+        }
+        next();
+    }
+
+    function search(query, callback)
+    {
+        output.push("=== Query: " + JSON.stringify(query) + " ===");
+        InspectorTest.sendCommand("DOM.performSearch", {query: query}, function(message) {
+            InspectorTest.checkForError(message);
+            printSearchResults(message.result, callback);
+        });
+    }
+
+    function printSearchResults(results, callback)
+    {
+        output.push("Count: " + results.resultCount);
+        if (!results.resultCount)
+            return callback();
+
+        var options = {"searchId": results.searchId, "fromIndex": 0, "toIndex": results.resultCount};
+        InspectorTest.sendCommand("DOM.getSearchResults", options, function onResultsReceived(message) {
+            for (var nodeId of message.result.nodeIds)
+                output.push(dom.getNodeIdentifier(nodeId));
+            callback();
+        });
+    }
+
+    function testFinished()
+    {
+        InspectorTest.log(output.join("\n"));
+        InspectorTest.completeTest();
+    }
+}
+</script>
+</head>
+<body onload="runTest()" class="main-frame">
+    <p>Testing DOM.performSearch with no parent node ids.</p>
+    <iframe src="resources/dom-search-iframe.html"></iframe>
+</body>
+</html>
diff --git a/LayoutTests/inspector-protocol/dom/resources/dom-search-crash-iframe.html b/LayoutTests/inspector-protocol/dom/resources/dom-search-crash-iframe.html
new file mode 100644 (file)
index 0000000..33f71de
--- /dev/null
@@ -0,0 +1,3 @@
+<script>
+document.documentElement.parentNode.removeChild(document.documentElement);
+</script>
diff --git a/LayoutTests/inspector-protocol/dom/resources/dom-search-iframe.html b/LayoutTests/inspector-protocol/dom/resources/dom-search-iframe.html
new file mode 100644 (file)
index 0000000..bb03c76
--- /dev/null
@@ -0,0 +1,9 @@
+<html class="inside-iframe">
+    <head class="inside-iframe">
+    </head>
+    <body class="inside-iframe">
+        <div class="base1 inside-iframe">
+            <p class="inside-iframe"></p>
+        </div>
+    </body>
+</html>
\ No newline at end of file
diff --git a/LayoutTests/inspector-protocol/dom/resources/dom-search-queries.js b/LayoutTests/inspector-protocol/dom/resources/dom-search-queries.js
new file mode 100644 (file)
index 0000000..81cf35e
--- /dev/null
@@ -0,0 +1,25 @@
+// Having the queries in an external file, so that DOM search will not find the script when searching for values.
+
+var domSearchQueries = [
+    "body",
+    "<body",
+    "body>",
+    "<body>",
+
+    // Attribute names
+    "onload",
+
+    // Attribute values
+    "runTest()",
+    "\"runTest()",
+    "\"runTest()\"",
+    "runTest()\"",
+
+    // CSS selectors
+    ".body-inside-iframe",
+    "*",
+
+    // XPath query
+    "/html/body",
+    "/html/body/@onload",
+];
\ No newline at end of file
index e15d8ef..db3127d 100644 (file)
@@ -1594,6 +1594,7 @@ set(WebCore_SOURCES
     inspector/InspectorInstrumentation.cpp
     inspector/InspectorLayerTreeAgent.cpp
     inspector/InspectorMemoryAgent.cpp
+    inspector/InspectorNodeFinder.cpp
     inspector/InspectorOverlay.cpp
     inspector/InspectorPageAgent.cpp
     inspector/InspectorProfilerAgent.cpp
index d27247b..0249d8f 100644 (file)
@@ -1,3 +1,40 @@
+2013-11-15  Alexandru Chiculita  <achicu@adobe.com>
+
+        Web Inspector: DOM.performSearch should accept a list of context nodes
+        https://bugs.webkit.org/show_bug.cgi?id=124390
+
+        Reviewed by Timothy Hatcher.
+
+        Extracted the code in InspectorDOMAgent::performSearch into its own helper class
+        called InspectorNodeFinder. Also added a new array parameter called "nodeIds"
+        that can be used to limit the search results to just partial subtrees.
+
+        Tests: inspector-protocol/dom/dom-search-crash.html
+               inspector-protocol/dom/dom-search-with-context.html
+               inspector-protocol/dom/dom-search.html
+
+        * CMakeLists.txt:
+        * GNUmakefile.list.am:
+        * WebCore.vcxproj/WebCore.vcxproj:
+        * WebCore.vcxproj/WebCore.vcxproj.filters:
+        * WebCore.xcodeproj/project.pbxproj:
+        * inspector/protocol/DOM.json:
+        * inspector/InspectorAllInOne.cpp:
+        * inspector/InspectorDOMAgent.cpp:
+        (WebCore::InspectorDOMAgent::performSearch):
+        * inspector/InspectorDOMAgent.h:
+        * inspector/InspectorNodeFinder.cpp: Added.
+        (WebCore::stripCharacters):
+        (WebCore::InspectorNodeFinder::InspectorNodeFinder):
+        (WebCore::InspectorNodeFinder::performSearch):
+        (WebCore::InspectorNodeFinder::searchUsingDOMTreeTraversal):
+        (WebCore::InspectorNodeFinder::matchesAttribute):
+        (WebCore::InspectorNodeFinder::matchesElement):
+        (WebCore::InspectorNodeFinder::searchUsingXPath):
+        (WebCore::InspectorNodeFinder::searchUsingCSSSelectors):
+        * inspector/InspectorNodeFinder.h: Added.
+        (WebCore::InspectorNodeFinder::results):
+
 2013-11-15  Brady Eidson  <beidson@apple.com>
 
         Remove IDBBackingStoreInterface.h includes that are no longer needed
index 562b182..28a15d6 100644 (file)
@@ -3822,6 +3822,8 @@ webcore_sources += \
        Source/WebCore/inspector/InspectorLayerTreeAgent.h \
        Source/WebCore/inspector/InspectorMemoryAgent.cpp \
        Source/WebCore/inspector/InspectorMemoryAgent.h \
+       Source/WebCore/inspector/InspectorNodeFinder.cpp \
+       Source/WebCore/inspector/InspectorNodeFinder.h \
        Source/WebCore/inspector/InspectorOverlay.cpp \
        Source/WebCore/inspector/InspectorOverlay.h \
        Source/WebCore/inspector/InspectorPageAgent.cpp \
index c26b9f7..51e918e 100644 (file)
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Production|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Production|x64'">true</ExcludedFromBuild>
     </ClCompile>
+    <ClCompile Include="..\inspector\InspectorNodeFinder.cpp">
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release_WinCairo|Win32'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release_WinCairo|x64'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Production|Win32'">true</ExcludedFromBuild>
+      <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Production|x64'">true</ExcludedFromBuild>
+    </ClCompile>
     <ClCompile Include="..\inspector\InspectorOverlay.cpp">
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
       <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
     <ClInclude Include="..\inspector\InspectorInstrumentation.h" />
     <ClInclude Include="..\inspector\InspectorLayerTreeAgent.h" />
     <ClInclude Include="..\inspector\InspectorMemoryAgent.h" />
+    <ClInclude Include="..\inspector\InspectorNodeFinder.h" />
     <ClInclude Include="..\inspector\InspectorOverlay.h" />
     <ClInclude Include="..\inspector\InspectorPageAgent.h" />
     <ClInclude Include="..\inspector\InspectorProfilerAgent.h" />
index 55a993f..a395dab 100644 (file)
     <ClCompile Include="..\inspector\InspectorDOMDebuggerAgent.cpp">
       <Filter>inspector</Filter>
     </ClCompile>
+    <ClCompile Include="..\inspector\InspectorDOMNodeAgent.cpp">
+      <Filter>inspector</Filter>
+    </ClCompile>
     <ClCompile Include="..\inspector\InspectorDOMStorageAgent.cpp">
       <Filter>inspector</Filter>
     </ClCompile>
     <ClInclude Include="..\inspector\InspectorMemoryAgent.h">
       <Filter>inspector</Filter>
     </ClInclude>
+    <ClInclude Include="..\inspector\InspectorNodeFinder.h">
+      <Filter>inspector</Filter>
+    </ClInclude>
     <ClInclude Include="..\inspector\InspectorOverlay.h">
       <Filter>inspector</Filter>
     </ClInclude>
index 6a65fc0..1fdc00f 100644 (file)
                503D0CAB14B5B08700F32F58 /* CustomFilterRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 503D0CA814B5B08700F32F58 /* CustomFilterRenderer.h */; settings = {ATTRIBUTES = (); }; };
                503D0CAC14B5B08700F32F57 /* CustomFilterProgramClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 503D0CA914B5B08700F32F57 /* CustomFilterProgramClient.h */; settings = {ATTRIBUTES = (Private, ); }; };
                503D0CAE14B5B0BA00F32F57 /* StyleCustomFilterProgram.h in Headers */ = {isa = PBXBuildFile; fileRef = 503D0CAD14B5B0BA00F32F57 /* StyleCustomFilterProgram.h */; settings = {ATTRIBUTES = (); }; };
+               504AACCD1834455900E3D9BC /* InspectorNodeFinder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 504AACCB1834455900E3D9BC /* InspectorNodeFinder.cpp */; };
+               504AACCE1834455900E3D9BC /* InspectorNodeFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = 504AACCC1834455900E3D9BC /* InspectorNodeFinder.h */; };
                5081E3C33CE580C16EF8B48B /* CachedResourceRequest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5081E3DF3CFC80C16EF8B48B /* CachedResourceRequest.cpp */; };
                5081E3E03CFF80C16EF8B48B /* CachedResourceRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 5081E3E13D0280C16EF8B48B /* CachedResourceRequest.h */; settings = {ATTRIBUTES = (Private, ); }; };
                508CCA4F13CF106B003151F3 /* RenderFlowThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 508CCA4D13CF106B003151F3 /* RenderFlowThread.h */; };
                503D0CA814B5B08700F32F58 /* CustomFilterRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CustomFilterRenderer.h; path = filters/CustomFilterRenderer.h; sourceTree = "<group>"; };
                503D0CA914B5B08700F32F57 /* CustomFilterProgramClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CustomFilterProgramClient.h; path = filters/CustomFilterProgramClient.h; sourceTree = "<group>"; };
                503D0CAD14B5B0BA00F32F57 /* StyleCustomFilterProgram.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StyleCustomFilterProgram.h; path = style/StyleCustomFilterProgram.h; sourceTree = "<group>"; };
+               504AACCB1834455900E3D9BC /* InspectorNodeFinder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = InspectorNodeFinder.cpp; sourceTree = "<group>"; };
+               504AACCC1834455900E3D9BC /* InspectorNodeFinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorNodeFinder.h; sourceTree = "<group>"; };
                5081E3DF3CFC80C16EF8B48B /* CachedResourceRequest.cpp */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; path = CachedResourceRequest.cpp; sourceTree = "<group>"; };
                5081E3E13D0280C16EF8B48B /* CachedResourceRequest.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = CachedResourceRequest.h; sourceTree = "<group>"; };
                508CCA4D13CF106B003151F3 /* RenderFlowThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RenderFlowThread.h; sourceTree = "<group>"; };
                                7A24587A1021EAF4000A00AA /* InspectorDOMAgent.h */,
                                F3D4C47612E07663003DA150 /* InspectorDOMDebuggerAgent.cpp */,
                                F3D4C47712E07663003DA150 /* InspectorDOMDebuggerAgent.h */,
+                               504AACCB1834455900E3D9BC /* InspectorNodeFinder.cpp */,
+                               504AACCC1834455900E3D9BC /* InspectorNodeFinder.h */,
                                7A74ECB8101839A500BF939E /* InspectorDOMStorageAgent.cpp */,
                                7A74ECB9101839A600BF939E /* InspectorDOMStorageAgent.h */,
                                2277775F1345DEA9008EA455 /* InspectorFrontendChannel.h */,
                                A9D248070D757E7D00FDF959 /* JSDOMMimeType.h in Headers */,
                                A9D248090D757E7D00FDF959 /* JSDOMMimeTypeArray.h in Headers */,
                                52CCA9E315E3F62C0053C77F /* JSDOMNamedFlowCollection.h in Headers */,
+                               504AACCE1834455900E3D9BC /* InspectorNodeFinder.h in Headers */,
                                1ACE53E00A8D18810022947D /* JSDOMParser.h in Headers */,
                                E19AC3F31824DC7900349426 /* CryptoAlgorithmSHA384.h in Headers */,
                                FB91392A16AE4FC0001FE682 /* JSDOMPath.h in Headers */,
                                511EF2CA17F0FD3500E4FA16 /* JSIDBTransaction.cpp in Sources */,
                                41D168E710226E89009BC827 /* SharedWorkerGlobalScope.cpp in Sources */,
                                E1B784201639CBBE0007B692 /* SharedWorkerRepository.cpp in Sources */,
+                               504AACCD1834455900E3D9BC /* InspectorNodeFinder.cpp in Sources */,
                                41D168ED10226E89009BC827 /* SharedWorkerThread.cpp in Sources */,
                                B2C3DA640D006CD600EF6F26 /* SimpleFontData.cpp in Sources */,
                                163E88F7118A39D200ED9231 /* SimpleFontDataCoreText.cpp in Sources */,
index 3be023e..263bdd9 100644 (file)
@@ -61,6 +61,7 @@
 #include "InspectorInstrumentation.cpp"
 #include "InspectorLayerTreeAgent.cpp"
 #include "InspectorMemoryAgent.cpp"
+#include "InspectorNodeFinder.cpp"
 #include "InspectorOverlay.cpp"
 #include "InspectorPageAgent.cpp"
 #include "InspectorProfilerAgent.cpp"
index 25eed86..d3c2a03 100644 (file)
@@ -71,6 +71,7 @@
 #include "InspectorClient.h"
 #include "InspectorFrontend.h"
 #include "InspectorHistory.h"
+#include "InspectorNodeFinder.h"
 #include "InspectorOverlay.h"
 #include "InspectorPageAgent.h"
 #include "InstrumentingAgents.h"
@@ -80,7 +81,6 @@
 #include "MutationEvent.h"
 #include "Node.h"
 #include "NodeList.h"
-#include "NodeTraversal.h"
 #include "Page.h"
 #include "Pasteboard.h"
 #include "RenderStyle.h"
@@ -96,7 +96,6 @@
 #include "htmlediting.h"
 #include "markup.h"
 #include <wtf/HashSet.h>
-#include <wtf/ListHashSet.h>
 #include <wtf/OwnPtr.h>
 #include <wtf/Vector.h>
 #include <wtf/text/CString.h>
@@ -881,128 +880,45 @@ void InspectorDOMAgent::getEventListeners(Node* node, Vector<EventListenerInfo>&
     }
 }
 
-void InspectorDOMAgent::performSearch(ErrorString*, const String& whitespaceTrimmedQuery, String* searchId, int* resultCount)
+void InspectorDOMAgent::performSearch(ErrorString* errorString, const String& whitespaceTrimmedQuery, const RefPtr<InspectorArray>* nodeIds, String* searchId, int* resultCount)
 {
-    // FIXME: Few things are missing here:
-    // 1) Search works with node granularity - number of matches within node is not calculated.
-    // 2) There is no need to push all search results to the front-end at a time, pushing next / previous result
-    //    is sufficient.
+    // FIXME: Search works with node granularity - number of matches within node is not calculated.
+    InspectorNodeFinder finder(whitespaceTrimmedQuery);
 
-    unsigned queryLength = whitespaceTrimmedQuery.length();
-    bool startTagFound = !whitespaceTrimmedQuery.find('<');
-    bool endTagFound = whitespaceTrimmedQuery.reverseFind('>') + 1 == queryLength;
-    bool startQuoteFound = !whitespaceTrimmedQuery.find('"');
-    bool endQuoteFound = whitespaceTrimmedQuery.reverseFind('"') + 1 == queryLength;
-    bool exactAttributeMatch = startQuoteFound && endQuoteFound;
-
-    String tagNameQuery = whitespaceTrimmedQuery;
-    String attributeQuery = whitespaceTrimmedQuery;
-    if (startTagFound)
-        tagNameQuery = tagNameQuery.right(tagNameQuery.length() - 1);
-    if (endTagFound)
-        tagNameQuery = tagNameQuery.left(tagNameQuery.length() - 1);
-    if (startQuoteFound)
-        attributeQuery = attributeQuery.right(attributeQuery.length() - 1);
-    if (endQuoteFound)
-        attributeQuery = attributeQuery.left(attributeQuery.length() - 1);
-
-    Vector<Document*> docs = documents();
-    ListHashSet<Node*> resultCollector;
-
-    for (Vector<Document*>::iterator it = docs.begin(); it != docs.end(); ++it) {
-        Document* document = *it;
-        Node* node = document->documentElement();
-        if (!node)
-            continue;
-
-        // Manual plain text search.
-        while ((node = NodeTraversal::next(node, document->documentElement()))) {
-            switch (node->nodeType()) {
-            case Node::TEXT_NODE:
-            case Node::COMMENT_NODE:
-            case Node::CDATA_SECTION_NODE: {
-                String text = node->nodeValue();
-                if (text.findIgnoringCase(whitespaceTrimmedQuery) != notFound)
-                    resultCollector.add(node);
-                break;
-            }
-            case Node::ELEMENT_NODE: {
-                if ((!startTagFound && !endTagFound && (node->nodeName().findIgnoringCase(tagNameQuery) != notFound))
-                    || (startTagFound && endTagFound && equalIgnoringCase(node->nodeName(), tagNameQuery))
-                    || (startTagFound && !endTagFound && node->nodeName().startsWith(tagNameQuery, false))
-                    || (!startTagFound && endTagFound && node->nodeName().endsWith(tagNameQuery, false))) {
-                    resultCollector.add(node);
-                    break;
-                }
-                // Go through all attributes and serialize them.
-                const Element* element = toElement(node);
-                if (!element->hasAttributes())
-                    break;
-
-                unsigned numAttrs = element->attributeCount();
-                for (unsigned i = 0; i < numAttrs; ++i) {
-                    // Add attribute pair
-                    const Attribute& attribute = element->attributeAt(i);
-                    if (attribute.localName().find(whitespaceTrimmedQuery) != notFound) {
-                        resultCollector.add(node);
-                        break;
-                    }
-                    size_t foundPosition = attribute.value().find(attributeQuery);
-                    if (foundPosition != notFound) {
-                        if (!exactAttributeMatch || (!foundPosition && attribute.value().length() == attributeQuery.length())) {
-                            resultCollector.add(node);
-                            break;
-                        }
-                    }
-                }
-                break;
+    if (nodeIds) {
+        const RefPtr<InspectorArray>& nodeIdsRef = *nodeIds;
+        for (unsigned i = 0; i < nodeIdsRef->length(); ++i) {
+            RefPtr<InspectorValue> nodeValue = nodeIdsRef->get(i);
+            if (!nodeValue) {
+                *errorString = "Invalid nodeIds item.";
+                return;
             }
-            default:
-                break;
+            int nodeId = 0;
+            if (!nodeValue->asNumber(&nodeId)) {
+                *errorString = "Invalid nodeIds item type. Expecting integer types.";
+                return;
             }
-        }
-
-        // XPath evaluation
-        for (Vector<Document*>::iterator it = docs.begin(); it != docs.end(); ++it) {
-            Document* document = *it;
-            ExceptionCode ec = 0;
-            RefPtr<XPathResult> result = document->evaluate(whitespaceTrimmedQuery, document, 0, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE, 0, ec);
-            if (ec || !result)
-                continue;
-
-            unsigned long size = result->snapshotLength(ec);
-            for (unsigned long i = 0; !ec && i < size; ++i) {
-                Node* node = result->snapshotItem(i, ec);
-                if (ec)
-                    break;
-
-                if (node->isAttributeNode())
-                    node = toAttr(node)->ownerElement();
-                resultCollector.add(node);
+            Node* node = assertNode(errorString, nodeId);
+            if (!node) {
+                // assertNode should have filled the errorString for us.
+                ASSERT(errorString->length());
+                return;
             }
+            finder.performSearch(node);
         }
-
-        // Selector evaluation
-        for (Vector<Document*>::iterator it = docs.begin(); it != docs.end(); ++it) {
-            Document* document = *it;
-            ExceptionCode ec = 0;
-            RefPtr<NodeList> nodeList = document->querySelectorAll(whitespaceTrimmedQuery, ec);
-            if (ec || !nodeList)
-                continue;
-
-            unsigned size = nodeList->length();
-            for (unsigned i = 0; i < size; ++i)
-                resultCollector.add(nodeList->item(i));
-        }
+    } else if (m_document) {
+        // There's no need to iterate the frames tree because
+        // the search helper will go inside the frame owner elements.
+        finder.performSearch(m_document.get());
     }
 
     *searchId = IdentifiersFactory::createIdentifier();
-    SearchResults::iterator resultsIt = m_searchResults.add(*searchId, Vector<RefPtr<Node>>()).iterator;
 
-    for (ListHashSet<Node*>::iterator it = resultCollector.begin(); it != resultCollector.end(); ++it)
-        resultsIt->value.append(*it);
+    auto& resultsVector = m_searchResults.add(*searchId, Vector<RefPtr<Node>>()).iterator->value;
+    for (auto iterator = finder.results().begin(); iterator != finder.results().end(); ++iterator)
+        resultsVector.append(*iterator);
 
-    *resultCount = resultsIt->value.size();
+    *resultCount = resultsVector.size();
 }
 
 void InspectorDOMAgent::getSearchResults(ErrorString* errorString, const String& searchId, int fromIndex, int toIndex, RefPtr<TypeBuilder::Array<int>>& nodeIds)
index 1196c98..a5d60bc 100644 (file)
@@ -41,7 +41,6 @@
 #include "Timer.h"
 
 #include <wtf/Deque.h>
-#include <wtf/ListHashSet.h>
 #include <wtf/HashMap.h>
 #include <wtf/HashSet.h>
 #include <wtf/OwnPtr.h>
@@ -131,7 +130,7 @@ public:
     virtual void setOuterHTML(ErrorString*, int nodeId, const String& outerHTML);
     virtual void setNodeValue(ErrorString*, int nodeId, const String& value);
     virtual void getEventListenersForNode(ErrorString*, int nodeId, const WTF::String* objectGroup, RefPtr<TypeBuilder::Array<TypeBuilder::DOM::EventListener>>& listenersArray);
-    virtual void performSearch(ErrorString*, const String& whitespaceTrimmedQuery, String* searchId, int* resultCount);
+    virtual void performSearch(ErrorString*, const String& whitespaceTrimmedQuery, const RefPtr<InspectorArray>* nodeIds, String* searchId, int* resultCount);
     virtual void getSearchResults(ErrorString*, const String& searchId, int fromIndex, int toIndex, RefPtr<TypeBuilder::Array<int>>&);
     virtual void discardSearchResults(ErrorString*, const String& searchId);
     virtual void resolveNode(ErrorString*, int nodeId, const String* objectGroup, RefPtr<TypeBuilder::Runtime::RemoteObject>& result);
diff --git a/Source/WebCore/inspector/InspectorNodeFinder.cpp b/Source/WebCore/inspector/InspectorNodeFinder.cpp
new file mode 100644 (file)
index 0000000..8e10c27
--- /dev/null
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2013 Adobe Systems Inc. All rights reserved.
+ * Copyright (C) 2009 Apple Inc. All rights reserved.
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2009 Joseph Pecoraro
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1.  Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ * 2.  Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ *     its contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "InspectorNodeFinder.h"
+
+#include "Attr.h"
+#include "Attribute.h"
+#include "Document.h"
+#include "Element.h"
+#include "HTMLFrameOwnerElement.h"
+#include "NodeList.h"
+#include "NodeTraversal.h"
+#include "XPathResult.h"
+
+namespace WebCore {
+
+static String stripCharacters(String string, const char startCharacter, const char endCharacter, bool& startCharFound, bool& endCharFound)
+{
+    startCharFound = string.startsWith(startCharacter);
+    endCharFound = string.endsWith(endCharacter);
+
+    unsigned start = startCharFound ? 1 : 0;
+    unsigned end = string.length() - (endCharFound ? 1 : 0);
+    return string.substring(start, end - start);
+}
+
+InspectorNodeFinder::InspectorNodeFinder(String whitespaceTrimmedQuery)
+    : m_whitespaceTrimmedQuery(whitespaceTrimmedQuery)
+{
+    m_tagNameQuery = stripCharacters(whitespaceTrimmedQuery, '<', '>', m_startTagFound, m_endTagFound);
+
+    bool startQuoteFound, endQuoteFound;
+    m_attributeQuery = stripCharacters(whitespaceTrimmedQuery, '"', '"', startQuoteFound, endQuoteFound);
+    m_exactAttributeMatch = startQuoteFound && endQuoteFound;
+}
+
+void InspectorNodeFinder::performSearch(Node* parentNode)
+{
+    searchUsingXPath(parentNode);
+    searchUsingCSSSelectors(parentNode);
+
+    // Keep the DOM tree traversal last. This way iframe content will come after their parents.
+    searchUsingDOMTreeTraversal(parentNode);
+}
+
+void InspectorNodeFinder::searchUsingDOMTreeTraversal(Node* parentNode)
+{
+    // Manual plain text search.
+    for (auto node = parentNode; node; node = NodeTraversal::next(node, parentNode)) {
+        switch (node->nodeType()) {
+        case Node::TEXT_NODE:
+        case Node::COMMENT_NODE:
+        case Node::CDATA_SECTION_NODE: {
+            if (node->nodeValue().findIgnoringCase(m_whitespaceTrimmedQuery) != notFound)
+                m_results.add(node);
+            break;
+        }
+        case Node::ELEMENT_NODE: {
+            if (matchesElement(*toElement(node)))
+                m_results.add(node);
+
+            // Search inside frame elements.
+            if (node->isFrameOwnerElement()) {
+                HTMLFrameOwnerElement* frameOwner = toHTMLFrameOwnerElement(node);
+                if (Document* document = frameOwner->contentDocument())
+                    performSearch(document);
+            }
+
+            break;
+        }
+        default:
+            break;
+        }
+    }
+}
+
+bool InspectorNodeFinder::matchesAttribute(const Attribute& attribute)
+{
+    if (attribute.localName().find(m_whitespaceTrimmedQuery) != notFound)
+        return true;
+    return m_exactAttributeMatch ? attribute.value() == m_attributeQuery : attribute.value().find(m_attributeQuery) != notFound;
+}
+
+bool InspectorNodeFinder::matchesElement(const Element& element)
+{
+    String nodeName = element.nodeName();
+    if ((!m_startTagFound && !m_endTagFound && (nodeName.findIgnoringCase(m_tagNameQuery) != notFound))
+        || (m_startTagFound && m_endTagFound && equalIgnoringCase(nodeName, m_tagNameQuery))
+        || (m_startTagFound && !m_endTagFound && nodeName.startsWith(m_tagNameQuery, false))
+        || (!m_startTagFound && m_endTagFound && nodeName.endsWith(m_tagNameQuery, false)))
+        return true;
+
+    if (!element.hasAttributes())
+        return false;
+
+    unsigned numAttrs = element.attributeCount();
+    for (unsigned i = 0; i < numAttrs; ++i) {
+        if (matchesAttribute(element.attributeAt(i)))
+            return true;
+    }
+
+    return false;
+}
+
+void InspectorNodeFinder::searchUsingXPath(Node* parentNode)
+{
+    ExceptionCode ec = 0;
+    RefPtr<XPathResult> result = parentNode->document().evaluate(m_whitespaceTrimmedQuery, parentNode, 0, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE, 0, ec);
+    if (ec || !result)
+        return;
+
+    unsigned long size = result->snapshotLength(ec);
+    if (ec)
+        return;
+
+    for (unsigned long i = 0; i < size; ++i) {
+        Node* node = result->snapshotItem(i, ec);
+        if (ec)
+            return;
+
+        if (node->isAttributeNode())
+            node = toAttr(node)->ownerElement();
+
+        // XPath can get out of the context node that we pass as the starting point to evaluate, so we need to filter for just the nodes we care about.
+        if (node == parentNode || node->isDescendantOf(parentNode))
+            m_results.add(node);
+    }
+}
+
+void InspectorNodeFinder::searchUsingCSSSelectors(Node* parentNode)
+{
+    if (!parentNode->isContainerNode())
+        return;
+
+    ExceptionCode ec = 0;
+    RefPtr<NodeList> nodeList = toContainerNode(parentNode)->querySelectorAll(m_whitespaceTrimmedQuery, ec);
+    if (ec || !nodeList)
+        return;
+
+    unsigned size = nodeList->length();
+    for (unsigned i = 0; i < size; ++i)
+        m_results.add(nodeList->item(i));
+}
+
+} // namespace WebCore
diff --git a/Source/WebCore/inspector/InspectorNodeFinder.h b/Source/WebCore/inspector/InspectorNodeFinder.h
new file mode 100644 (file)
index 0000000..941253f
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 Adobe Systems Inc. All rights reserved.
+ * Copyright (C) 2009 Apple Inc. All rights reserved.
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1.  Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ * 2.  Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ *     its contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef InspectorNodeFinder_h
+#define InspectorNodeFinder_h
+
+#include <wtf/ListHashSet.h>
+#include <wtf/text/WTFString.h>
+
+namespace WebCore {
+
+class Attribute;
+class Element;
+class Node;
+
+class InspectorNodeFinder {
+public:
+    InspectorNodeFinder(String whitespaceTrimmedQuery);
+    void performSearch(Node*);
+    const ListHashSet<Node*>& results() const { return m_results; }
+
+private:
+    bool matchesAttribute(const Attribute&);
+    bool matchesElement(const Element&);
+
+    void searchUsingDOMTreeTraversal(Node*);
+    void searchUsingXPath(Node*);
+    void searchUsingCSSSelectors(Node*);
+
+    bool m_startTagFound;
+    bool m_endTagFound;
+    bool m_exactAttributeMatch;
+
+    String m_whitespaceTrimmedQuery;
+    String m_tagNameQuery;
+    String m_attributeQuery;
+
+    ListHashSet<Node*> m_results;
+};
+
+} // namespace WebCore
+
+#endif // InspectorNodeFinder_h
index f14c858..e83f470 100644 (file)
         {
             "name": "performSearch",
             "parameters": [
-                { "name": "query", "type": "string", "description": "Plain text or query selector or XPath search query." }
+                { "name": "query", "type": "string", "description": "Plain text or query selector or XPath search query." },
+                { "name": "nodeIds", "type": "array", "items": { "$ref": "NodeId" }, "optional": true, "description": "Ids of nodes to use as starting points for the search." }
             ],
             "returns": [
                 { "name": "searchId", "type": "string", "description": "Unique search session identifier." },