blob: 1d2d126f210c54e76cf29fbf8bad995c92f0fb5e [file] [log] [blame] [edit]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
* 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.
*/
WebInspector.DOMPresentationUtils = {}
WebInspector.DOMPresentationUtils.decorateNodeLabel = function(node, parentElement)
{
var title = node.nodeNameInCorrectCase();
var nameElement = createElement("span");
nameElement.textContent = title;
parentElement.appendChild(nameElement);
var idAttribute = node.getAttribute("id");
if (idAttribute) {
var idElement = createElement("span");
parentElement.appendChild(idElement);
var part = "#" + idAttribute;
title += part;
idElement.createTextChild(part);
// Mark the name as extra, since the ID is more important.
nameElement.className = "extra";
}
var classAttribute = node.getAttribute("class");
if (classAttribute) {
var classes = classAttribute.split(/\s+/);
var foundClasses = {};
if (classes.length) {
var classesElement = createElement("span");
classesElement.className = "extra";
parentElement.appendChild(classesElement);
for (var i = 0; i < classes.length; ++i) {
var className = classes[i];
if (className && !(className in foundClasses)) {
var part = "." + className;
title += part;
classesElement.createTextChild(part);
foundClasses[className] = true;
}
}
}
}
parentElement.title = title;
}
/**
* @param {!Element} container
* @param {string} nodeTitle
*/
WebInspector.DOMPresentationUtils.createSpansForNodeTitle = function(container, nodeTitle)
{
var match = nodeTitle.match(/([^#.]+)(#[^.]+)?(\..*)?/);
container.createChild("span", "webkit-html-tag-name").textContent = match[1];
if (match[2])
container.createChild("span", "webkit-html-attribute-value").textContent = match[2];
if (match[3])
container.createChild("span", "webkit-html-attribute-name").textContent = match[3];
}
/**
* @param {?WebInspector.DOMNode} node
* @return {!Node}
*/
WebInspector.DOMPresentationUtils.linkifyNodeReference = function(node)
{
if (!node)
return createTextNode(WebInspector.UIString("<node>"));
var root = createElement("span");
var shadowRoot = WebInspector.createShadowRootWithCoreStyles(root);
shadowRoot.appendChild(WebInspector.Widget.createStyleElement("components/domUtils.css"));
var link = shadowRoot.createChild("div", "node-link");
WebInspector.DOMPresentationUtils.decorateNodeLabel(node, link);
link.addEventListener("click", WebInspector.Revealer.reveal.bind(WebInspector.Revealer, node, undefined), false);
link.addEventListener("mouseover", node.highlight.bind(node, undefined, undefined), false);
link.addEventListener("mouseleave", WebInspector.DOMModel.hideDOMNodeHighlight.bind(WebInspector.DOMModel), false);
return root;
}
/**
* @param {!WebInspector.DeferredDOMNode} deferredNode
* @return {!Node}
*/
WebInspector.DOMPresentationUtils.linkifyDeferredNodeReference = function(deferredNode)
{
var root = createElement("div");
var shadowRoot = WebInspector.createShadowRootWithCoreStyles(root);
shadowRoot.appendChild(WebInspector.Widget.createStyleElement("components/domUtils.css"));
var link = shadowRoot.createChild("div", "node-link");
link.createChild("content");
link.addEventListener("click", deferredNode.resolve.bind(deferredNode, onDeferredNodeResolved), false);
link.addEventListener("mousedown", consumeEvent, false);
/**
* @param {?WebInspector.DOMNode} node
*/
function onDeferredNodeResolved(node)
{
WebInspector.Revealer.reveal(node);
}
return root;
}
/**
* @param {!WebInspector.Target} target
* @param {string} originalImageURL
* @param {boolean} showDimensions
* @param {function(!Element=)} userCallback
* @param {!Object=} precomputedFeatures
*/
WebInspector.DOMPresentationUtils.buildImagePreviewContents = function(target, originalImageURL, showDimensions, userCallback, precomputedFeatures)
{
var resource = target.resourceTreeModel.resourceForURL(originalImageURL);
var imageURL = originalImageURL;
if (!isImageResource(resource) && precomputedFeatures && precomputedFeatures.currentSrc) {
imageURL = precomputedFeatures.currentSrc;
resource = target.resourceTreeModel.resourceForURL(imageURL);
}
if (!isImageResource(resource)) {
userCallback();
return;
}
var imageElement = createElement("img");
imageElement.addEventListener("load", buildContent, false);
imageElement.addEventListener("error", errorCallback, false);
resource.populateImageSource(imageElement);
function errorCallback()
{
// Drop the event parameter when invoking userCallback.
userCallback();
}
/**
* @param {?WebInspector.Resource} resource
* @return {boolean}
*/
function isImageResource(resource)
{
return !!resource && resource.resourceType() === WebInspector.resourceTypes.Image;
}
function buildContent()
{
var container = createElement("table");
container.className = "image-preview-container";
var naturalWidth = precomputedFeatures ? precomputedFeatures.naturalWidth : imageElement.naturalWidth;
var naturalHeight = precomputedFeatures ? precomputedFeatures.naturalHeight : imageElement.naturalHeight;
var offsetWidth = precomputedFeatures ? precomputedFeatures.offsetWidth : naturalWidth;
var offsetHeight = precomputedFeatures ? precomputedFeatures.offsetHeight : naturalHeight;
var description;
if (showDimensions) {
if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
description = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight);
else
description = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight);
}
container.createChild("tr").createChild("td", "image-container").appendChild(imageElement);
if (description)
container.createChild("tr").createChild("td").createChild("span", "description").textContent = description;
if (imageURL !== originalImageURL)
container.createChild("tr").createChild("td").createChild("span", "description").textContent = String.sprintf("currentSrc: %s", imageURL.trimMiddle(100));
userCallback(container);
}
}
/**
* @param {!WebInspector.Target} target
* @param {!WebInspector.Linkifier} linkifier
* @param {!Array.<!ConsoleAgent.CallFrame>=} stackTrace
* @param {!ConsoleAgent.AsyncStackTrace=} asyncStackTrace
* @return {!Element}
*/
WebInspector.DOMPresentationUtils.buildStackTracePreviewContents = function(target, linkifier, stackTrace, asyncStackTrace)
{
var element = createElement("span");
element.style.display = "inline-block";
var shadowRoot = WebInspector.createShadowRootWithCoreStyles(element);
shadowRoot.appendChild(WebInspector.Widget.createStyleElement("components/domUtils.css"));
var contentElement = shadowRoot.createChild("table", "stack-preview-container");
/**
* @param {!Array.<!ConsoleAgent.CallFrame>} stackTrace
*/
function appendStackTrace(stackTrace)
{
for (var stackFrame of stackTrace) {
var row = createElement("tr");
row.createChild("td").textContent = WebInspector.beautifyFunctionName(stackFrame.functionName);
row.createChild("td").textContent = " @ ";
row.createChild("td").appendChild(linkifier.linkifyConsoleCallFrame(target, stackFrame));
contentElement.appendChild(row);
}
}
if (stackTrace)
appendStackTrace(stackTrace);
while (asyncStackTrace) {
var callFrames = asyncStackTrace.callFrames;
if (!callFrames || !callFrames.length)
break;
var row = contentElement.createChild("tr");
row.createChild("td", "stack-preview-async-description").textContent = WebInspector.asyncStackTraceLabel(asyncStackTrace.description);
row.createChild("td");
row.createChild("td");
appendStackTrace(callFrames);
asyncStackTrace = asyncStackTrace.asyncStackTrace;
}
return element;
}
/**
* @param {!WebInspector.DOMNode} node
* @param {boolean=} justSelector
* @return {string}
*/
WebInspector.DOMPresentationUtils.fullQualifiedSelector = function(node, justSelector)
{
if (node.nodeType() !== Node.ELEMENT_NODE)
return node.localName() || node.nodeName().toLowerCase();
return WebInspector.DOMPresentationUtils.cssPath(node, justSelector);
}
/**
* @param {!WebInspector.DOMNode} node
* @return {string}
*/
WebInspector.DOMPresentationUtils.simpleSelector = function(node)
{
var lowerCaseName = node.localName() || node.nodeName().toLowerCase();
if (node.nodeType() !== Node.ELEMENT_NODE)
return lowerCaseName;
if (lowerCaseName === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class"))
return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]";
if (node.getAttribute("id"))
return lowerCaseName + "#" + node.getAttribute("id");
if (node.getAttribute("class"))
return (lowerCaseName === "div" ? "" : lowerCaseName) + "." + node.getAttribute("class").trim().replace(/\s+/g, ".");
return lowerCaseName;
}
/**
* @param {!WebInspector.DOMNode} node
* @param {boolean=} optimized
* @return {string}
*/
WebInspector.DOMPresentationUtils.cssPath = function(node, optimized)
{
if (node.nodeType() !== Node.ELEMENT_NODE)
return "";
var steps = [];
var contextNode = node;
while (contextNode) {
var step = WebInspector.DOMPresentationUtils._cssPathStep(contextNode, !!optimized, contextNode === node);
if (!step)
break; // Error - bail out early.
steps.push(step);
if (step.optimized)
break;
contextNode = contextNode.parentNode;
}
steps.reverse();
return steps.join(" > ");
}
/**
* @param {!WebInspector.DOMNode} node
* @param {boolean} optimized
* @param {boolean} isTargetNode
* @return {?WebInspector.DOMNodePathStep}
*/
WebInspector.DOMPresentationUtils._cssPathStep = function(node, optimized, isTargetNode)
{
if (node.nodeType() !== Node.ELEMENT_NODE)
return null;
var id = node.getAttribute("id");
if (optimized) {
if (id)
return new WebInspector.DOMNodePathStep(idSelector(id), true);
var nodeNameLower = node.nodeName().toLowerCase();
if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html")
return new WebInspector.DOMNodePathStep(node.nodeNameInCorrectCase(), true);
}
var nodeName = node.nodeNameInCorrectCase();
if (id)
return new WebInspector.DOMNodePathStep(nodeName + idSelector(id), true);
var parent = node.parentNode;
if (!parent || parent.nodeType() === Node.DOCUMENT_NODE)
return new WebInspector.DOMNodePathStep(nodeName, true);
/**
* @param {!WebInspector.DOMNode} node
* @return {!Array.<string>}
*/
function prefixedElementClassNames(node)
{
var classAttribute = node.getAttribute("class");
if (!classAttribute)
return [];
return classAttribute.split(/\s+/g).filter(Boolean).map(function(name) {
// The prefix is required to store "__proto__" in a object-based map.
return "$" + name;
});
}
/**
* @param {string} id
* @return {string}
*/
function idSelector(id)
{
return "#" + escapeIdentifierIfNeeded(id);
}
/**
* @param {string} ident
* @return {string}
*/
function escapeIdentifierIfNeeded(ident)
{
if (isCSSIdentifier(ident))
return ident;
var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident);
var lastIndex = ident.length - 1;
return ident.replace(/./g, function(c, i) {
return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c;
});
}
/**
* @param {string} c
* @param {boolean} isLast
* @return {string}
*/
function escapeAsciiChar(c, isLast)
{
return "\\" + toHexByte(c) + (isLast ? "" : " ");
}
/**
* @param {string} c
*/
function toHexByte(c)
{
var hexByte = c.charCodeAt(0).toString(16);
if (hexByte.length === 1)
hexByte = "0" + hexByte;
return hexByte;
}
/**
* @param {string} c
* @return {boolean}
*/
function isCSSIdentChar(c)
{
if (/[a-zA-Z0-9_-]/.test(c))
return true;
return c.charCodeAt(0) >= 0xA0;
}
/**
* @param {string} value
* @return {boolean}
*/
function isCSSIdentifier(value)
{
return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value);
}
var prefixedOwnClassNamesArray = prefixedElementClassNames(node);
var needsClassNames = false;
var needsNthChild = false;
var ownIndex = -1;
var elementIndex = -1;
var siblings = parent.children();
for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {
var sibling = siblings[i];
if (sibling.nodeType() !== Node.ELEMENT_NODE)
continue;
elementIndex += 1;
if (sibling === node) {
ownIndex = elementIndex;
continue;
}
if (needsNthChild)
continue;
if (sibling.nodeNameInCorrectCase() !== nodeName)
continue;
needsClassNames = true;
var ownClassNames = prefixedOwnClassNamesArray.keySet();
var ownClassNameCount = 0;
for (var name in ownClassNames)
++ownClassNameCount;
if (ownClassNameCount === 0) {
needsNthChild = true;
continue;
}
var siblingClassNamesArray = prefixedElementClassNames(sibling);
for (var j = 0; j < siblingClassNamesArray.length; ++j) {
var siblingClass = siblingClassNamesArray[j];
if (!ownClassNames.hasOwnProperty(siblingClass))
continue;
delete ownClassNames[siblingClass];
if (!--ownClassNameCount) {
needsNthChild = true;
break;
}
}
}
var result = nodeName;
if (isTargetNode && nodeName.toLowerCase() === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class"))
result += "[type=\"" + node.getAttribute("type") + "\"]";
if (needsNthChild) {
result += ":nth-child(" + (ownIndex + 1) + ")";
} else if (needsClassNames) {
for (var prefixedName in prefixedOwnClassNamesArray.keySet())
result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1));
}
return new WebInspector.DOMNodePathStep(result, false);
}
/**
* @param {!WebInspector.DOMNode} node
* @param {boolean=} optimized
* @return {string}
*/
WebInspector.DOMPresentationUtils.xPath = function(node, optimized)
{
if (node.nodeType() === Node.DOCUMENT_NODE)
return "/";
var steps = [];
var contextNode = node;
while (contextNode) {
var step = WebInspector.DOMPresentationUtils._xPathValue(contextNode, optimized);
if (!step)
break; // Error - bail out early.
steps.push(step);
if (step.optimized)
break;
contextNode = contextNode.parentNode;
}
steps.reverse();
return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/");
}
/**
* @param {!WebInspector.DOMNode} node
* @param {boolean=} optimized
* @return {?WebInspector.DOMNodePathStep}
*/
WebInspector.DOMPresentationUtils._xPathValue = function(node, optimized)
{
var ownValue;
var ownIndex = WebInspector.DOMPresentationUtils._xPathIndex(node);
if (ownIndex === -1)
return null; // Error.
switch (node.nodeType()) {
case Node.ELEMENT_NODE:
if (optimized && node.getAttribute("id"))
return new WebInspector.DOMNodePathStep("//*[@id=\"" + node.getAttribute("id") + "\"]", true);
ownValue = node.localName();
break;
case Node.ATTRIBUTE_NODE:
ownValue = "@" + node.nodeName();
break;
case Node.TEXT_NODE:
case Node.CDATA_SECTION_NODE:
ownValue = "text()";
break;
case Node.PROCESSING_INSTRUCTION_NODE:
ownValue = "processing-instruction()";
break;
case Node.COMMENT_NODE:
ownValue = "comment()";
break;
case Node.DOCUMENT_NODE:
ownValue = "";
break;
default:
ownValue = "";
break;
}
if (ownIndex > 0)
ownValue += "[" + ownIndex + "]";
return new WebInspector.DOMNodePathStep(ownValue, node.nodeType() === Node.DOCUMENT_NODE);
}
/**
* @param {!WebInspector.DOMNode} node
* @return {number}
*/
WebInspector.DOMPresentationUtils._xPathIndex = function(node)
{
// Returns -1 in case of error, 0 if no siblings matching the same expression, <XPath index among the same expression-matching sibling nodes> otherwise.
function areNodesSimilar(left, right)
{
if (left === right)
return true;
if (left.nodeType() === Node.ELEMENT_NODE && right.nodeType() === Node.ELEMENT_NODE)
return left.localName() === right.localName();
if (left.nodeType() === right.nodeType())
return true;
// XPath treats CDATA as text nodes.
var leftType = left.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType();
var rightType = right.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType();
return leftType === rightType;
}
var siblings = node.parentNode ? node.parentNode.children() : null;
if (!siblings)
return 0; // Root node - no siblings.
var hasSameNamedElements;
for (var i = 0; i < siblings.length; ++i) {
if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
hasSameNamedElements = true;
break;
}
}
if (!hasSameNamedElements)
return 0;
var ownIndex = 1; // XPath indices start with 1.
for (var i = 0; i < siblings.length; ++i) {
if (areNodesSimilar(node, siblings[i])) {
if (siblings[i] === node)
return ownIndex;
++ownIndex;
}
}
return -1; // An error occurred: |node| not found in parent's children.
}
/**
* @constructor
* @param {string} value
* @param {boolean} optimized
*/
WebInspector.DOMNodePathStep = function(value, optimized)
{
this.value = value;
this.optimized = optimized || false;
}
WebInspector.DOMNodePathStep.prototype = {
/**
* @override
* @return {string}
*/
toString: function()
{
return this.value;
}
}
/**
* @interface
*/
WebInspector.DOMPresentationUtils.MarkerDecorator = function()
{
}
WebInspector.DOMPresentationUtils.MarkerDecorator.prototype = {
/**
* @param {!WebInspector.DOMNode} node
* @return {?{title: string, color: string}}
*/
decorate: function(node) { }
}