#include "DOMNodeHighlighter.h"
#include "DOMWindow.h"
#include "Document.h"
+#include "DocumentFragment.h"
#include "DocumentType.h"
#include "Event.h"
#include "EventContext.h"
#include "HitTestResult.h"
#include "HTMLElement.h"
#include "HTMLFrameOwnerElement.h"
+#include "IdentifiersFactory.h"
#include "InjectedScriptManager.h"
#include "InspectorClient.h"
#include "InspectorFrontend.h"
#include "ScriptEventListener.h"
#include "StyleSheetList.h"
#include "Text.h"
-
-#if ENABLE(XPATH)
#include "XPathResult.h"
-#endif
#include "markup.h"
static const char documentRequested[] = "documentRequested";
};
+static const size_t maxTextSize = 10000;
+static const UChar ellipsisUChar[] = { 0x2026, 0 };
+
static Color parseColor(const RefPtr<InspectorObject>* colorObject)
{
if (!colorObject || !(*colorObject))
return parseColor(&colorObject);
}
-class MatchJob {
-public:
- virtual void match(ListHashSet<Node*>& resultCollector) = 0;
- virtual ~MatchJob() { }
-
-protected:
- MatchJob(Document* document, const String& query)
- : m_document(document)
- , m_query(query) { }
-
- void addNodesToResults(PassRefPtr<NodeList> nodes, ListHashSet<Node*>& resultCollector)
- {
- for (unsigned i = 0; nodes && i < nodes->length(); ++i)
- resultCollector.add(nodes->item(i));
- }
-
- RefPtr<Document> m_document;
- String m_query;
-};
-
class RevalidateStyleAttributeTask {
public:
RevalidateStyleAttributeTask(InspectorDOMAgent*);
HashSet<RefPtr<Element> > m_elements;
};
-namespace {
-
-class MatchExactIdJob : public WebCore::MatchJob {
-public:
- MatchExactIdJob(Document* document, const String& query) : WebCore::MatchJob(document, query) { }
- virtual ~MatchExactIdJob() { }
-
-protected:
- virtual void match(ListHashSet<Node*>& resultCollector)
- {
- if (m_query.isEmpty())
- return;
-
- Element* element = m_document->getElementById(m_query);
- if (element)
- resultCollector.add(element);
- }
-};
-
-class MatchExactClassNamesJob : public WebCore::MatchJob {
-public:
- MatchExactClassNamesJob(Document* document, const String& query) : WebCore::MatchJob(document, query) { }
- virtual ~MatchExactClassNamesJob() { }
-
- virtual void match(ListHashSet<Node*>& resultCollector)
- {
- if (!m_query.isEmpty())
- addNodesToResults(m_document->getElementsByClassName(m_query), resultCollector);
- }
-};
-
-class MatchExactTagNamesJob : public WebCore::MatchJob {
-public:
- MatchExactTagNamesJob(Document* document, const String& query) : WebCore::MatchJob(document, query) { }
- virtual ~MatchExactTagNamesJob() { }
-
- virtual void match(ListHashSet<Node*>& resultCollector)
- {
- if (!m_query.isEmpty())
- addNodesToResults(m_document->getElementsByName(m_query), resultCollector);
- }
-};
-
-class MatchQuerySelectorAllJob : public WebCore::MatchJob {
-public:
- MatchQuerySelectorAllJob(Document* document, const String& query) : WebCore::MatchJob(document, query) { }
- virtual ~MatchQuerySelectorAllJob() { }
-
- virtual void match(ListHashSet<Node*>& resultCollector)
- {
- if (m_query.isEmpty())
- return;
-
- ExceptionCode ec = 0;
- RefPtr<NodeList> list = m_document->querySelectorAll(m_query, ec);
- if (!ec)
- addNodesToResults(list, resultCollector);
- }
-};
-
-class MatchXPathJob : public WebCore::MatchJob {
-public:
- MatchXPathJob(Document* document, const String& query) : WebCore::MatchJob(document, query) { }
- virtual ~MatchXPathJob() { }
-
- virtual void match(ListHashSet<Node*>& resultCollector)
- {
-#if ENABLE(XPATH)
- if (m_query.isEmpty())
- return;
-
- ExceptionCode ec = 0;
- RefPtr<XPathResult> result = m_document->evaluate(m_query, m_document.get(), 0, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE, 0, ec);
- if (ec || !result)
- return;
-
- 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->nodeType() == Node::ATTRIBUTE_NODE)
- node = static_cast<Attr*>(node)->ownerElement();
- resultCollector.add(node);
- }
-#else
- UNUSED_PARAM(resultCollector);
-#endif
- }
-};
-
-class MatchPlainTextJob : public MatchXPathJob {
-public:
- MatchPlainTextJob(Document* document, const String& query) : MatchXPathJob(document, query)
- {
- m_query = "//text()[contains(., '" + m_query + "')] | //comment()[contains(., '" + m_query + "')]";
- }
- virtual ~MatchPlainTextJob() { }
-};
-
-}
-
RevalidateStyleAttributeTask::RevalidateStyleAttributeTask(InspectorDOMAgent* domAgent)
: m_domAgent(domAgent)
, m_timer(this, &RevalidateStyleAttributeTask::onTimer)
}
InspectorDOMAgent::InspectorDOMAgent(InstrumentingAgents* instrumentingAgents, InspectorPageAgent* pageAgent, InspectorClient* client, InspectorState* inspectorState, InjectedScriptManager* injectedScriptManager)
- : m_instrumentingAgents(instrumentingAgents)
+ : InspectorBaseAgent<InspectorDOMAgent>("DOM", instrumentingAgents, inspectorState)
, m_pageAgent(pageAgent)
, m_client(client)
- , m_inspectorState(inspectorState)
, m_injectedScriptManager(injectedScriptManager)
, m_frontend(0)
, m_domListener(0)
, m_lastNodeId(1)
- , m_matchJobsTimer(this, &InspectorDOMAgent::onMatchJobsTimer)
, m_searchingForNode(false)
{
}
m_frontend = 0;
m_instrumentingAgents->setInspectorDOMAgent(0);
- m_inspectorState->setBoolean(DOMAgentState::documentRequested, false);
+ m_state->setBoolean(DOMAgentState::documentRequested, false);
reset();
}
void InspectorDOMAgent::reset()
{
ErrorString error;
- cancelSearch(&error);
+ m_searchResults.clear();
discardBindings();
if (m_revalidateStyleAttrTask)
m_revalidateStyleAttrTask->reset();
m_document = doc;
- if (!m_inspectorState->getBoolean(DOMAgentState::documentRequested))
+ if (!m_state->getBoolean(DOMAgentState::documentRequested))
return;
// Immediately communicate 0 document or document that has finished loading.
void InspectorDOMAgent::getDocument(ErrorString*, RefPtr<InspectorObject>* root)
{
- m_inspectorState->setBoolean(DOMAgentState::documentRequested, true);
+ m_state->setBoolean(DOMAgentState::documentRequested, true);
if (!m_document)
return;
ExceptionCode ec = 0;
element->setAttribute(name, value, ec);
if (ec)
- *errorString = "Internal error: could not set attribute value.";
+ *errorString = "Internal error: could not set attribute value";
}
void InspectorDOMAgent::setAttributesAsText(ErrorString* errorString, int elementId, const String& text, const String* const name)
ExceptionCode ec = 0;
RefPtr<Element> parsedElement = element->document()->createElement("span", ec);
if (ec) {
- *errorString = "Internal error: could not set attribute value.";
+ *errorString = "Internal error: could not set attribute value";
return;
}
toHTMLElement(parsedElement.get())->setInnerHTML("<span " + text + "></span>", ec);
if (ec) {
- *errorString = "Could not parse value as attributes.";
+ *errorString = "Could not parse value as attributes";
return;
}
Node* child = parsedElement->firstChild();
if (!child) {
- *errorString = "Could not parse value as attributes.";
+ *errorString = "Could not parse value as attributes";
return;
}
if (!attrMap && name) {
element->removeAttribute(*name, ec);
if (ec)
- *errorString = "Could not remove attribute.";
+ *errorString = "Could not remove attribute";
return;
}
unsigned numAttrs = attrMap->length();
for (unsigned i = 0; i < numAttrs; ++i) {
// Add attribute pair
- const Attribute *attribute = attrMap->attributeItem(i);
+ const Attribute* attribute = attrMap->attributeItem(i);
foundOriginalAttribute = foundOriginalAttribute || (name && attribute->name().toString() == *name);
element->setAttribute(attribute->name(), attribute->value(), ec);
if (ec) {
- *errorString = "Internal error: could not set attribute value.";
+ *errorString = "Internal error: could not set attribute value";
return;
}
}
if (!foundOriginalAttribute && name) {
element->removeAttribute(*name, ec);
if (ec)
- *errorString = "Could not remove attribute.";
+ *errorString = "Could not remove attribute";
return;
}
}
void InspectorDOMAgent::getOuterHTML(ErrorString* errorString, int nodeId, WTF::String* outerHTML)
{
- HTMLElement* element = assertHTMLElement(errorString, nodeId);
- if (element)
- *outerHTML = element->outerHTML();
+ Node* node = assertNode(errorString, nodeId);
+ if (!node)
+ return;
+
+ if (node->isHTMLElement()) {
+ *outerHTML = static_cast<HTMLElement*>(node)->outerHTML();
+ return;
+ }
+
+ if (node->isCommentNode()) {
+ *outerHTML = "<!--" + node->nodeValue() + "-->";
+ return;
+ }
+
+ if (node->isTextNode()) {
+ *outerHTML = node->nodeValue();
+ return;
+ }
+
+ *errorString = "Only HTMLElements, Comments, and Text nodes are supported";
}
void InspectorDOMAgent::setOuterHTML(ErrorString* errorString, int nodeId, const String& outerHTML, int* newId)
{
- HTMLElement* htmlElement = assertHTMLElement(errorString, nodeId);
- if (!htmlElement)
+ Node* node = assertNode(errorString, nodeId);
+ if (!node)
return;
- bool requiresTotalUpdate = htmlElement->tagName() == "HTML" || htmlElement->tagName() == "BODY" || htmlElement->tagName() == "HEAD";
+ Element* parentElement = node->parentElement();
+ if (!parentElement)
+ return;
- bool childrenRequested = m_childrenRequested.contains(nodeId);
- Node* previousSibling = htmlElement->previousSibling();
- ContainerNode* parentNode = htmlElement->parentNode();
+ Document* document = node->ownerDocument();
+ if (!document->isHTMLDocument()) {
+ *errorString = "Not an HTML document";
+ return;
+ }
+
+ Node* previousSibling = node->previousSibling(); // Remember previous sibling before replacing node.
+
+ RefPtr<DocumentFragment> fragment = DocumentFragment::create(document);
+ fragment->parseHTML(outerHTML, parentElement);
ExceptionCode ec = 0;
- htmlElement->setOuterHTML(outerHTML, ec);
- if (ec)
+ parentElement->replaceChild(fragment.release(), node, ec);
+ if (ec) {
+ *errorString = "Failed to replace Node with new contents";
return;
+ }
+
+ bool requiresTotalUpdate = false;
+ if (node->isHTMLElement())
+ requiresTotalUpdate = node->nodeName() == "HTML" || node->nodeName() == "BODY" || node->nodeName() == "HEAD";
if (requiresTotalUpdate) {
RefPtr<Document> document = m_document;
return;
}
- Node* newNode = previousSibling ? previousSibling->nextSibling() : parentNode->firstChild();
+ Node* newNode = previousSibling ? previousSibling->nextSibling() : parentElement->firstChild();
if (!newNode) {
// The only child node has been deleted.
*newId = 0;
}
*newId = pushNodePathToFrontend(newNode);
+
+ bool childrenRequested = m_childrenRequested.contains(nodeId);
if (childrenRequested)
pushChildNodesToFrontend(*newId);
}
}
}
-void InspectorDOMAgent::performSearch(ErrorString* error, const String& whitespaceTrimmedQuery, const bool* const runSynchronously)
+void InspectorDOMAgent::performSearch(ErrorString*, const String& whitespaceTrimmedQuery, String* searchId, int* resultCount)
{
// FIXME: Few things are missing here:
// 1) Search works with node granularity - number of matches within node is not calculated.
bool endTagFound = whitespaceTrimmedQuery.reverseFind('>') + 1 == queryLength;
String tagNameQuery = whitespaceTrimmedQuery;
- if (startTagFound || endTagFound)
- tagNameQuery = tagNameQuery.substring(startTagFound ? 1 : 0, endTagFound ? queryLength - 1 : queryLength);
- if (!Document::isValidName(tagNameQuery))
- tagNameQuery = "";
-
- String attributeNameQuery = whitespaceTrimmedQuery;
- if (!Document::isValidName(attributeNameQuery))
- attributeNameQuery = "";
+ if (startTagFound)
+ tagNameQuery = tagNameQuery.right(tagNameQuery.length() - 1);
+ if (endTagFound)
+ tagNameQuery = tagNameQuery.left(tagNameQuery.length() - 1);
- String escapedQuery = whitespaceTrimmedQuery;
- escapedQuery.replace("'", "\\'");
- String escapedTagNameQuery = tagNameQuery;
- escapedTagNameQuery.replace("'", "\\'");
-
- // Clear pending jobs.
- cancelSearch(error);
-
- // Find all frames, iframes and object elements to search their documents.
Vector<Document*> docs = documents();
+ ListHashSet<Node*> resultCollector;
+
for (Vector<Document*>::iterator it = docs.begin(); it != docs.end(); ++it) {
Document* document = *it;
-
- if (!tagNameQuery.isEmpty() && startTagFound && endTagFound) {
- m_pendingMatchJobs.append(new MatchExactTagNamesJob(document, tagNameQuery));
- m_pendingMatchJobs.append(new MatchPlainTextJob(document, escapedQuery));
- continue;
+ Node* node = document->documentElement();
+
+ // Manual plain text search.
+ while ((node = node->traverseNextNode(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 (node->nodeName().findIgnoringCase(tagNameQuery) != notFound) {
+ resultCollector.add(node);
+ break;
+ }
+ // Go through all attributes and serialize them.
+ const NamedNodeMap* attrMap = static_cast<Element*>(node)->attributes(true);
+ if (!attrMap)
+ break;
+
+ unsigned numAttrs = attrMap->length();
+ for (unsigned i = 0; i < numAttrs; ++i) {
+ // Add attribute pair
+ const Attribute* attribute = attrMap->attributeItem(i);
+ if (attribute->localName().find(whitespaceTrimmedQuery) != notFound) {
+ resultCollector.add(node);
+ break;
+ }
+ if (attribute->value().find(whitespaceTrimmedQuery) != notFound) {
+ resultCollector.add(node);
+ break;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
}
- if (!tagNameQuery.isEmpty() && startTagFound) {
- m_pendingMatchJobs.append(new MatchXPathJob(document, "//*[starts-with(name(), '" + escapedTagNameQuery + "')]"));
- m_pendingMatchJobs.append(new MatchPlainTextJob(document, escapedQuery));
- continue;
+ // 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->nodeType() == Node::ATTRIBUTE_NODE)
+ node = static_cast<Attr*>(node)->ownerElement();
+ resultCollector.add(node);
+ }
}
+ }
- if (!tagNameQuery.isEmpty() && endTagFound) {
- // FIXME: we should have a matchEndOfTagNames search function if endTagFound is true but not startTagFound.
- // This requires ends-with() support in XPath, WebKit only supports starts-with() and contains().
- m_pendingMatchJobs.append(new MatchXPathJob(document, "//*[contains(name(), '" + escapedTagNameQuery + "')]"));
- m_pendingMatchJobs.append(new MatchPlainTextJob(document, escapedQuery));
- continue;
- }
+ *searchId = IdentifiersFactory::createIdentifier();
+ SearchResults::iterator resultsIt = m_searchResults.add(*searchId, Vector<RefPtr<Node> >()).first;
- bool matchesEveryNode = whitespaceTrimmedQuery == "//*" || whitespaceTrimmedQuery == "*";
- if (matchesEveryNode) {
- // These queries will match every node. Matching everything isn't useful and can be slow for large pages,
- // so limit the search functions list to plain text and attribute matching for these.
- m_pendingMatchJobs.append(new MatchXPathJob(document, "//*[contains(@*, '" + escapedQuery + "')]"));
- m_pendingMatchJobs.append(new MatchPlainTextJob(document, escapedQuery));
- continue;
- }
+ for (ListHashSet<Node*>::iterator it = resultCollector.begin(); it != resultCollector.end(); ++it)
+ resultsIt->second.append(*it);
+
+ *resultCount = resultsIt->second.size();
+}
- m_pendingMatchJobs.append(new MatchExactIdJob(document, whitespaceTrimmedQuery));
- m_pendingMatchJobs.append(new MatchExactClassNamesJob(document, whitespaceTrimmedQuery));
- m_pendingMatchJobs.append(new MatchExactTagNamesJob(document, tagNameQuery));
- m_pendingMatchJobs.append(new MatchQuerySelectorAllJob(document, "[" + attributeNameQuery + "]"));
- m_pendingMatchJobs.append(new MatchQuerySelectorAllJob(document, whitespaceTrimmedQuery));
- m_pendingMatchJobs.append(new MatchXPathJob(document, "//*[contains(@*, '" + escapedQuery + "')]"));
- if (!tagNameQuery.isEmpty())
- m_pendingMatchJobs.append(new MatchXPathJob(document, "//*[contains(name(), '" + escapedTagNameQuery + "')]"));
- m_pendingMatchJobs.append(new MatchPlainTextJob(document, escapedQuery));
- m_pendingMatchJobs.append(new MatchXPathJob(document, whitespaceTrimmedQuery));
+void InspectorDOMAgent::getSearchResults(ErrorString* errorString, const String& searchId, int fromIndex, int toIndex, RefPtr<InspectorArray>* nodeIds)
+{
+ SearchResults::iterator it = m_searchResults.find(searchId);
+ if (it == m_searchResults.end()) {
+ *errorString = "No search session with given id found";
+ return;
}
- if (runSynchronously && *runSynchronously) {
- // For tests.
- ListHashSet<Node*> resultCollector;
- for (Deque<MatchJob*>::iterator it = m_pendingMatchJobs.begin(); it != m_pendingMatchJobs.end(); ++it)
- (*it)->match(resultCollector);
- reportNodesAsSearchResults(resultCollector);
- cancelSearch(error);
+ int size = it->second.size();
+ if (fromIndex < 0 || toIndex > size || fromIndex >= toIndex) {
+ *errorString = "Invalid search result range";
return;
}
- m_matchJobsTimer.startOneShot(0);
+
+ for (int i = fromIndex; i < toIndex; ++i)
+ (*nodeIds)->pushNumber(pushNodePathToFrontend((it->second)[i].get()));
}
-void InspectorDOMAgent::cancelSearch(ErrorString*)
+void InspectorDOMAgent::discardSearchResults(ErrorString*, const String& searchId)
{
- if (m_matchJobsTimer.isActive())
- m_matchJobsTimer.stop();
- deleteAllValues(m_pendingMatchJobs);
- m_pendingMatchJobs.clear();
- m_searchResults.clear();
+ m_searchResults.remove(searchId);
}
bool InspectorDOMAgent::handleMousePress()
if (!anchorNode)
return;
if (anchorNode->parentNode() != targetElement) {
- *error = "Anchor node must be child of the target element.";
+ *error = "Anchor node must be child of the target element";
return;
}
}
ExceptionCode ec = 0;
bool success = targetElement->insertBefore(node, anchorNode, ec);
if (ec || !success) {
- *error = "Could not drop node.";
+ *error = "Could not drop node";
return;
}
*newNodeId = pushNodePathToFrontend(node);
String objectGroupName = objectGroup ? *objectGroup : "";
Node* node = nodeForId(nodeId);
if (!node) {
- *error = "No node with given id found.";
+ *error = "No node with given id found";
+ return;
+ }
+ RefPtr<InspectorObject> object = resolveNode(node, objectGroupName);
+ if (!object) {
+ *error = "Node with given id does not belong to the document";
return;
}
- *result = resolveNode(node, objectGroupName);
+ *result = object;
}
void InspectorDOMAgent::getAttributes(ErrorString* errorString, int nodeId, RefPtr<InspectorArray>* result)
*nodeId = 0;
}
-String InspectorDOMAgent::documentURLString(Document* document) const
+// static
+String InspectorDOMAgent::documentURLString(Document* document)
{
if (!document || document->url().isNull())
return "";
String nodeValue;
switch (node->nodeType()) {
- case Node::TEXT_NODE:
- case Node::COMMENT_NODE:
- case Node::CDATA_SECTION_NODE:
- nodeValue = node->nodeValue();
- break;
- case Node::ATTRIBUTE_NODE:
- localName = node->localName();
- break;
- case Node::DOCUMENT_FRAGMENT_NODE:
- break;
- case Node::DOCUMENT_NODE:
- case Node::ELEMENT_NODE:
- default:
- nodeName = node->nodeName();
- localName = node->localName();
- break;
+ case Node::TEXT_NODE:
+ case Node::COMMENT_NODE:
+ case Node::CDATA_SECTION_NODE:
+ nodeValue = node->nodeValue();
+ if (nodeValue.length() > maxTextSize) {
+ nodeValue = nodeValue.left(maxTextSize);
+ nodeValue.append(ellipsisUChar);
+ }
+ break;
+ case Node::ATTRIBUTE_NODE:
+ localName = node->localName();
+ break;
+ case Node::DOCUMENT_FRAGMENT_NODE:
+ break;
+ case Node::DOCUMENT_NODE:
+ case Node::ELEMENT_NODE:
+ default:
+ nodeName = node->nodeName();
+ localName = node->localName();
+ break;
}
value->setNumber("nodeId", id);
unsigned numAttrs = attrMap->length();
for (unsigned i = 0; i < numAttrs; ++i) {
// Add attribute pair
- const Attribute *attribute = attrMap->attributeItem(i);
+ const Attribute* attribute = attrMap->attributeItem(i);
attributesValue->pushString(attribute->name().toString());
attributesValue->pushString(attribute->value());
}
{
// Re-push document once it is loaded.
discardBindings();
- if (m_inspectorState->getBoolean(DOMAgentState::documentRequested))
+ if (m_state->getBoolean(DOMAgentState::documentRequested))
m_frontend->documentUpdated();
}
return node;
}
-void InspectorDOMAgent::onMatchJobsTimer(Timer<InspectorDOMAgent>*)
-{
- if (!m_pendingMatchJobs.size()) {
- ErrorString error;
- cancelSearch(&error);
- return;
- }
-
- ListHashSet<Node*> resultCollector;
- MatchJob* job = m_pendingMatchJobs.takeFirst();
- job->match(resultCollector);
- delete job;
-
- reportNodesAsSearchResults(resultCollector);
-
- m_matchJobsTimer.startOneShot(0.025);
-}
-
-void InspectorDOMAgent::reportNodesAsSearchResults(ListHashSet<Node*>& resultCollector)
-{
- RefPtr<InspectorArray> nodeIds = InspectorArray::create();
- for (ListHashSet<Node*>::iterator it = resultCollector.begin(); it != resultCollector.end(); ++it) {
- if (m_searchResults.contains(*it))
- continue;
- m_searchResults.add(*it);
- nodeIds->pushNumber(pushNodePathToFrontend(*it));
- }
- m_frontend->searchResults(nodeIds.release());
-}
-
void InspectorDOMAgent::copyNode(ErrorString*, int nodeId)
{
Node* node = nodeForId(nodeId);