| Index: Source/devtools/front_end/DOMPresentationUtils.js
|
| diff --git a/Source/devtools/front_end/DOMPresentationUtils.js b/Source/devtools/front_end/DOMPresentationUtils.js
|
| index 67f1392eb06d7ce309983879ec2b22ab16c2ab5f..7d25da830dd5accbce663387fea87d68a11f8620 100644
|
| --- a/Source/devtools/front_end/DOMPresentationUtils.js
|
| +++ b/Source/devtools/front_end/DOMPresentationUtils.js
|
| @@ -167,21 +167,331 @@ WebInspector.DOMPresentationUtils.buildImagePreviewContents = function(imageURL,
|
| WebInspector.DOMPresentationUtils.appropriateSelectorFor = function(node, justSelector)
|
| {
|
| 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") + "\"]";
|
| +
|
| + return WebInspector.DOMPresentationUtils.cssPath(node, justSelector);
|
| +}
|
| +
|
| +/**
|
| + * @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._cssPathValue(contextNode, optimized);
|
| + 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
|
| + * @return {WebInspector.DOMNodePathStep}
|
| + */
|
| +WebInspector.DOMPresentationUtils._cssPathValue = function(node, optimized)
|
| +{
|
| + if (node.nodeType() !== Node.ELEMENT_NODE)
|
| + return null;
|
|
|
| var id = node.getAttribute("id");
|
| - if (id) {
|
| - var selector = "#" + id;
|
| - return (justSelector ? selector : lowerCaseName + selector);
|
| + 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();
|
|
|
| - var className = node.getAttribute("class");
|
| - if (className) {
|
| - var selector = "." + className.trim().replace(/\s+/g, ".");
|
| - return (justSelector ? selector : lowerCaseName + selector);
|
| + 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;
|
| + });
|
| }
|
|
|
| - if (lowerCaseName === "input" && node.getAttribute("type"))
|
| - return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]";
|
| + /**
|
| + * @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 siblings = parent.children();
|
| + for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {
|
| + var sibling = siblings[i];
|
| + if (sibling === node) {
|
| + ownIndex = i;
|
| + 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;
|
| + }
|
| + }
|
| + }
|
|
|
| - return lowerCaseName;
|
| + var result = nodeName;
|
| + 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 = {
|
| + /**
|
| + * @return {string}
|
| + */
|
| + toString: function()
|
| + {
|
| + return this.value;
|
| + }
|
| }
|
|
|