1 /* 2 * Copyright (C) 2011 Google Inc. All rights reserved. 3 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 4 * Copyright (C) 2008 Matt Lilek <webkit (at) mattlilek.com> 5 * Copyright (C) 2009 Joseph Pecoraro 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 17 * its contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 21 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 24 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 29 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 WebInspector.DOMPresentationUtils = {} 33 34 WebInspector.DOMPresentationUtils.decorateNodeLabel = function(node, parentElement) 35 { 36 var title = node.nodeNameInCorrectCase(); 37 38 var nameElement = document.createElement("span"); 39 nameElement.textContent = title; 40 parentElement.appendChild(nameElement); 41 42 var idAttribute = node.getAttribute("id"); 43 if (idAttribute) { 44 var idElement = document.createElement("span"); 45 parentElement.appendChild(idElement); 46 47 var part = "#" + idAttribute; 48 title += part; 49 idElement.createTextChild(part); 50 51 // Mark the name as extra, since the ID is more important. 52 nameElement.className = "extra"; 53 } 54 55 var classAttribute = node.getAttribute("class"); 56 if (classAttribute) { 57 var classes = classAttribute.split(/\s+/); 58 var foundClasses = {}; 59 60 if (classes.length) { 61 var classesElement = document.createElement("span"); 62 classesElement.className = "extra"; 63 parentElement.appendChild(classesElement); 64 65 for (var i = 0; i < classes.length; ++i) { 66 var className = classes[i]; 67 if (className && !(className in foundClasses)) { 68 var part = "." + className; 69 title += part; 70 classesElement.createTextChild(part); 71 foundClasses[className] = true; 72 } 73 } 74 } 75 } 76 parentElement.title = title; 77 } 78 79 /** 80 * @param {!Element} container 81 * @param {string} nodeTitle 82 */ 83 WebInspector.DOMPresentationUtils.createSpansForNodeTitle = function(container, nodeTitle) 84 { 85 var match = nodeTitle.match(/([^#.]+)(#[^.]+)?(\..*)?/); 86 container.createChild("span", "webkit-html-tag-name").textContent = match[1]; 87 if (match[2]) 88 container.createChild("span", "webkit-html-attribute-value").textContent = match[2]; 89 if (match[3]) 90 container.createChild("span", "webkit-html-attribute-name").textContent = match[3]; 91 } 92 93 /** 94 * @param {?WebInspector.DOMNode} node 95 * @return {!Node} 96 */ 97 WebInspector.DOMPresentationUtils.linkifyNodeReference = function(node) 98 { 99 if (!node) 100 return document.createTextNode(WebInspector.UIString("<node>")); 101 102 var link = document.createElement("span"); 103 link.className = "node-link"; 104 WebInspector.DOMPresentationUtils.decorateNodeLabel(node, link); 105 106 link.addEventListener("click", WebInspector.Revealer.reveal.bind(WebInspector.Revealer, node, undefined), false); 107 link.addEventListener("mouseover", node.highlight.bind(node, undefined, undefined), false); 108 link.addEventListener("mouseout", node.domModel().hideDOMNodeHighlight.bind(node.domModel()), false); 109 110 return link; 111 } 112 113 /** 114 * @param {string} imageURL 115 * @param {!WebInspector.Target} target 116 * @param {boolean} showDimensions 117 * @param {function(!Element=)} userCallback 118 * @param {!Object=} precomputedDimensions 119 */ 120 WebInspector.DOMPresentationUtils.buildImagePreviewContents = function(target, imageURL, showDimensions, userCallback, precomputedDimensions) 121 { 122 var resource = target.resourceTreeModel.resourceForURL(imageURL); 123 if (!resource) { 124 userCallback(); 125 return; 126 } 127 128 var imageElement = document.createElement("img"); 129 imageElement.addEventListener("load", buildContent, false); 130 imageElement.addEventListener("error", errorCallback, false); 131 resource.populateImageSource(imageElement); 132 133 function errorCallback() 134 { 135 // Drop the event parameter when invoking userCallback. 136 userCallback(); 137 } 138 139 function buildContent() 140 { 141 var container = document.createElement("table"); 142 container.className = "image-preview-container"; 143 var naturalWidth = precomputedDimensions ? precomputedDimensions.naturalWidth : imageElement.naturalWidth; 144 var naturalHeight = precomputedDimensions ? precomputedDimensions.naturalHeight : imageElement.naturalHeight; 145 var offsetWidth = precomputedDimensions ? precomputedDimensions.offsetWidth : naturalWidth; 146 var offsetHeight = precomputedDimensions ? precomputedDimensions.offsetHeight : naturalHeight; 147 var description; 148 if (showDimensions) { 149 if (offsetHeight === naturalHeight && offsetWidth === naturalWidth) 150 description = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight); 151 else 152 description = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight); 153 } 154 155 container.createChild("tr").createChild("td", "image-container").appendChild(imageElement); 156 if (description) 157 container.createChild("tr").createChild("td").createChild("span", "description").textContent = description; 158 userCallback(container); 159 } 160 } 161 162 /** 163 * @param {!WebInspector.DOMNode} node 164 * @param {boolean=} justSelector 165 * @return {string} 166 */ 167 WebInspector.DOMPresentationUtils.fullQualifiedSelector = function(node, justSelector) 168 { 169 if (node.nodeType() !== Node.ELEMENT_NODE) 170 return node.localName() || node.nodeName().toLowerCase(); 171 return WebInspector.DOMPresentationUtils.cssPath(node, justSelector); 172 } 173 174 /** 175 * @param {!WebInspector.DOMNode} node 176 * @return {string} 177 */ 178 WebInspector.DOMPresentationUtils.simpleSelector = function(node) 179 { 180 var lowerCaseName = node.localName() || node.nodeName().toLowerCase(); 181 if (node.nodeType() !== Node.ELEMENT_NODE) 182 return lowerCaseName; 183 if (lowerCaseName === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class")) 184 return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]"; 185 if (node.getAttribute("id")) 186 return lowerCaseName + "#" + node.getAttribute("id"); 187 if (node.getAttribute("class")) 188 return (lowerCaseName === "div" ? "" : lowerCaseName) + "." + node.getAttribute("class").trim().replace(/\s+/g, "."); 189 return lowerCaseName; 190 } 191 192 /** 193 * @param {!WebInspector.DOMNode} node 194 * @param {boolean=} optimized 195 * @return {string} 196 */ 197 WebInspector.DOMPresentationUtils.cssPath = function(node, optimized) 198 { 199 if (node.nodeType() !== Node.ELEMENT_NODE) 200 return ""; 201 202 var steps = []; 203 var contextNode = node; 204 while (contextNode) { 205 var step = WebInspector.DOMPresentationUtils._cssPathStep(contextNode, !!optimized, contextNode === node); 206 if (!step) 207 break; // Error - bail out early. 208 steps.push(step); 209 if (step.optimized) 210 break; 211 contextNode = contextNode.parentNode; 212 } 213 214 steps.reverse(); 215 return steps.join(" > "); 216 } 217 218 /** 219 * @param {!WebInspector.DOMNode} node 220 * @param {boolean} optimized 221 * @param {boolean} isTargetNode 222 * @return {?WebInspector.DOMNodePathStep} 223 */ 224 WebInspector.DOMPresentationUtils._cssPathStep = function(node, optimized, isTargetNode) 225 { 226 if (node.nodeType() !== Node.ELEMENT_NODE) 227 return null; 228 229 var id = node.getAttribute("id"); 230 if (optimized) { 231 if (id) 232 return new WebInspector.DOMNodePathStep(idSelector(id), true); 233 var nodeNameLower = node.nodeName().toLowerCase(); 234 if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html") 235 return new WebInspector.DOMNodePathStep(node.nodeNameInCorrectCase(), true); 236 } 237 var nodeName = node.nodeNameInCorrectCase(); 238 239 if (id) 240 return new WebInspector.DOMNodePathStep(nodeName + idSelector(id), true); 241 var parent = node.parentNode; 242 if (!parent || parent.nodeType() === Node.DOCUMENT_NODE) 243 return new WebInspector.DOMNodePathStep(nodeName, true); 244 245 /** 246 * @param {!WebInspector.DOMNode} node 247 * @return {!Array.<string>} 248 */ 249 function prefixedElementClassNames(node) 250 { 251 var classAttribute = node.getAttribute("class"); 252 if (!classAttribute) 253 return []; 254 255 return classAttribute.split(/\s+/g).filter(Boolean).map(function(name) { 256 // The prefix is required to store "__proto__" in a object-based map. 257 return "$" + name; 258 }); 259 } 260 261 /** 262 * @param {string} id 263 * @return {string} 264 */ 265 function idSelector(id) 266 { 267 return "#" + escapeIdentifierIfNeeded(id); 268 } 269 270 /** 271 * @param {string} ident 272 * @return {string} 273 */ 274 function escapeIdentifierIfNeeded(ident) 275 { 276 if (isCSSIdentifier(ident)) 277 return ident; 278 var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident); 279 var lastIndex = ident.length - 1; 280 return ident.replace(/./g, function(c, i) { 281 return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c; 282 }); 283 } 284 285 /** 286 * @param {string} c 287 * @param {boolean} isLast 288 * @return {string} 289 */ 290 function escapeAsciiChar(c, isLast) 291 { 292 return "\\" + toHexByte(c) + (isLast ? "" : " "); 293 } 294 295 /** 296 * @param {string} c 297 */ 298 function toHexByte(c) 299 { 300 var hexByte = c.charCodeAt(0).toString(16); 301 if (hexByte.length === 1) 302 hexByte = "0" + hexByte; 303 return hexByte; 304 } 305 306 /** 307 * @param {string} c 308 * @return {boolean} 309 */ 310 function isCSSIdentChar(c) 311 { 312 if (/[a-zA-Z0-9_-]/.test(c)) 313 return true; 314 return c.charCodeAt(0) >= 0xA0; 315 } 316 317 /** 318 * @param {string} value 319 * @return {boolean} 320 */ 321 function isCSSIdentifier(value) 322 { 323 return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value); 324 } 325 326 var prefixedOwnClassNamesArray = prefixedElementClassNames(node); 327 var needsClassNames = false; 328 var needsNthChild = false; 329 var ownIndex = -1; 330 var elementIndex = -1; 331 var siblings = parent.children(); 332 for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) { 333 var sibling = siblings[i]; 334 if (sibling.nodeType() !== Node.ELEMENT_NODE) 335 continue; 336 elementIndex += 1; 337 if (sibling === node) { 338 ownIndex = elementIndex; 339 continue; 340 } 341 if (needsNthChild) 342 continue; 343 if (sibling.nodeNameInCorrectCase() !== nodeName) 344 continue; 345 346 needsClassNames = true; 347 var ownClassNames = prefixedOwnClassNamesArray.keySet(); 348 var ownClassNameCount = 0; 349 for (var name in ownClassNames) 350 ++ownClassNameCount; 351 if (ownClassNameCount === 0) { 352 needsNthChild = true; 353 continue; 354 } 355 var siblingClassNamesArray = prefixedElementClassNames(sibling); 356 for (var j = 0; j < siblingClassNamesArray.length; ++j) { 357 var siblingClass = siblingClassNamesArray[j]; 358 if (!ownClassNames.hasOwnProperty(siblingClass)) 359 continue; 360 delete ownClassNames[siblingClass]; 361 if (!--ownClassNameCount) { 362 needsNthChild = true; 363 break; 364 } 365 } 366 } 367 368 var result = nodeName; 369 if (isTargetNode && nodeName.toLowerCase() === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class")) 370 result += "[type=\"" + node.getAttribute("type") + "\"]"; 371 if (needsNthChild) { 372 result += ":nth-child(" + (ownIndex + 1) + ")"; 373 } else if (needsClassNames) { 374 for (var prefixedName in prefixedOwnClassNamesArray.keySet()) 375 result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1)); 376 } 377 378 return new WebInspector.DOMNodePathStep(result, false); 379 } 380 381 /** 382 * @param {!WebInspector.DOMNode} node 383 * @param {boolean=} optimized 384 * @return {string} 385 */ 386 WebInspector.DOMPresentationUtils.xPath = function(node, optimized) 387 { 388 if (node.nodeType() === Node.DOCUMENT_NODE) 389 return "/"; 390 391 var steps = []; 392 var contextNode = node; 393 while (contextNode) { 394 var step = WebInspector.DOMPresentationUtils._xPathValue(contextNode, optimized); 395 if (!step) 396 break; // Error - bail out early. 397 steps.push(step); 398 if (step.optimized) 399 break; 400 contextNode = contextNode.parentNode; 401 } 402 403 steps.reverse(); 404 return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/"); 405 } 406 407 /** 408 * @param {!WebInspector.DOMNode} node 409 * @param {boolean=} optimized 410 * @return {?WebInspector.DOMNodePathStep} 411 */ 412 WebInspector.DOMPresentationUtils._xPathValue = function(node, optimized) 413 { 414 var ownValue; 415 var ownIndex = WebInspector.DOMPresentationUtils._xPathIndex(node); 416 if (ownIndex === -1) 417 return null; // Error. 418 419 switch (node.nodeType()) { 420 case Node.ELEMENT_NODE: 421 if (optimized && node.getAttribute("id")) 422 return new WebInspector.DOMNodePathStep("//*[@id=\"" + node.getAttribute("id") + "\"]", true); 423 ownValue = node.localName(); 424 break; 425 case Node.ATTRIBUTE_NODE: 426 ownValue = "@" + node.nodeName(); 427 break; 428 case Node.TEXT_NODE: 429 case Node.CDATA_SECTION_NODE: 430 ownValue = "text()"; 431 break; 432 case Node.PROCESSING_INSTRUCTION_NODE: 433 ownValue = "processing-instruction()"; 434 break; 435 case Node.COMMENT_NODE: 436 ownValue = "comment()"; 437 break; 438 case Node.DOCUMENT_NODE: 439 ownValue = ""; 440 break; 441 default: 442 ownValue = ""; 443 break; 444 } 445 446 if (ownIndex > 0) 447 ownValue += "[" + ownIndex + "]"; 448 449 return new WebInspector.DOMNodePathStep(ownValue, node.nodeType() === Node.DOCUMENT_NODE); 450 }, 451 452 /** 453 * @param {!WebInspector.DOMNode} node 454 * @return {number} 455 */ 456 WebInspector.DOMPresentationUtils._xPathIndex = function(node) 457 { 458 // Returns -1 in case of error, 0 if no siblings matching the same expression, <XPath index among the same expression-matching sibling nodes> otherwise. 459 function areNodesSimilar(left, right) 460 { 461 if (left === right) 462 return true; 463 464 if (left.nodeType() === Node.ELEMENT_NODE && right.nodeType() === Node.ELEMENT_NODE) 465 return left.localName() === right.localName(); 466 467 if (left.nodeType() === right.nodeType()) 468 return true; 469 470 // XPath treats CDATA as text nodes. 471 var leftType = left.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType(); 472 var rightType = right.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType(); 473 return leftType === rightType; 474 } 475 476 var siblings = node.parentNode ? node.parentNode.children() : null; 477 if (!siblings) 478 return 0; // Root node - no siblings. 479 var hasSameNamedElements; 480 for (var i = 0; i < siblings.length; ++i) { 481 if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) { 482 hasSameNamedElements = true; 483 break; 484 } 485 } 486 if (!hasSameNamedElements) 487 return 0; 488 var ownIndex = 1; // XPath indices start with 1. 489 for (var i = 0; i < siblings.length; ++i) { 490 if (areNodesSimilar(node, siblings[i])) { 491 if (siblings[i] === node) 492 return ownIndex; 493 ++ownIndex; 494 } 495 } 496 return -1; // An error occurred: |node| not found in parent's children. 497 } 498 499 /** 500 * @constructor 501 * @param {string} value 502 * @param {boolean} optimized 503 */ 504 WebInspector.DOMNodePathStep = function(value, optimized) 505 { 506 this.value = value; 507 this.optimized = optimized || false; 508 } 509 510 WebInspector.DOMNodePathStep.prototype = { 511 /** 512 * @return {string} 513 */ 514 toString: function() 515 { 516 return this.value; 517 } 518 } 519