OLD | NEW |
1 /* | 1 /* |
2 * Copyright (C) 2011 Google Inc. All rights reserved. | 2 * Copyright (C) 2011 Google Inc. All rights reserved. |
3 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. | 3 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. |
4 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> | 4 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> |
5 * Copyright (C) 2009 Joseph Pecoraro | 5 * Copyright (C) 2009 Joseph Pecoraro |
6 * | 6 * |
7 * Redistribution and use in source and binary forms, with or without | 7 * Redistribution and use in source and binary forms, with or without |
8 * modification, are permitted provided that the following conditions | 8 * modification, are permitted provided that the following conditions |
9 * are met: | 9 * are met: |
10 * | 10 * |
(...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
160 } | 160 } |
161 | 161 |
162 /** | 162 /** |
163 * @param {!WebInspector.DOMNode} node | 163 * @param {!WebInspector.DOMNode} node |
164 * @param {boolean=} justSelector | 164 * @param {boolean=} justSelector |
165 * @return {string} | 165 * @return {string} |
166 */ | 166 */ |
167 WebInspector.DOMPresentationUtils.appropriateSelectorFor = function(node, justSe
lector) | 167 WebInspector.DOMPresentationUtils.appropriateSelectorFor = function(node, justSe
lector) |
168 { | 168 { |
169 var lowerCaseName = node.localName() || node.nodeName().toLowerCase(); | 169 var lowerCaseName = node.localName() || node.nodeName().toLowerCase(); |
| 170 if (node.nodeType() !== Node.ELEMENT_NODE) |
| 171 return lowerCaseName; |
| 172 if (lowerCaseName === "input" && node.getAttribute("type") && !node.getAttri
bute("id") && !node.getAttribute("class")) |
| 173 return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]"; |
| 174 |
| 175 return WebInspector.DOMPresentationUtils.cssPath(node, justSelector); |
| 176 } |
| 177 |
| 178 /** |
| 179 * @param {!WebInspector.DOMNode} node |
| 180 * @param {boolean=} optimized |
| 181 * @return {string} |
| 182 */ |
| 183 WebInspector.DOMPresentationUtils.cssPath = function(node, optimized) |
| 184 { |
| 185 if (node.nodeType() !== Node.ELEMENT_NODE) |
| 186 return ""; |
| 187 |
| 188 var steps = []; |
| 189 var contextNode = node; |
| 190 while (contextNode) { |
| 191 var step = WebInspector.DOMPresentationUtils._cssPathValue(contextNode,
optimized); |
| 192 if (!step) |
| 193 break; // Error - bail out early. |
| 194 steps.push(step); |
| 195 if (step.optimized) |
| 196 break; |
| 197 contextNode = contextNode.parentNode; |
| 198 } |
| 199 |
| 200 steps.reverse(); |
| 201 return steps.join(" > "); |
| 202 } |
| 203 |
| 204 /** |
| 205 * @param {!WebInspector.DOMNode} node |
| 206 * @param {boolean=} optimized |
| 207 * @return {WebInspector.DOMNodePathStep} |
| 208 */ |
| 209 WebInspector.DOMPresentationUtils._cssPathValue = function(node, optimized) |
| 210 { |
| 211 if (node.nodeType() !== Node.ELEMENT_NODE) |
| 212 return null; |
170 | 213 |
171 var id = node.getAttribute("id"); | 214 var id = node.getAttribute("id"); |
172 if (id) { | 215 if (optimized) { |
173 var selector = "#" + id; | 216 if (id) |
174 return (justSelector ? selector : lowerCaseName + selector); | 217 return new WebInspector.DOMNodePathStep(idSelector(id), true); |
175 } | 218 var nodeNameLower = node.nodeName().toLowerCase(); |
176 | 219 if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLowe
r === "html") |
177 var className = node.getAttribute("class"); | 220 return new WebInspector.DOMNodePathStep(node.nodeNameInCorrectCase()
, true); |
178 if (className) { | 221 } |
179 var selector = "." + className.trim().replace(/\s+/g, "."); | 222 var nodeName = node.nodeNameInCorrectCase(); |
180 return (justSelector ? selector : lowerCaseName + selector); | 223 |
181 } | 224 if (id) |
182 | 225 return new WebInspector.DOMNodePathStep(nodeName + idSelector(id), true)
; |
183 if (lowerCaseName === "input" && node.getAttribute("type")) | 226 var parent = node.parentNode; |
184 return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]"; | 227 if (!parent || parent.nodeType() === Node.DOCUMENT_NODE) |
185 | 228 return new WebInspector.DOMNodePathStep(nodeName, true); |
186 return lowerCaseName; | 229 |
187 } | 230 /** |
| 231 * @param {WebInspector.DOMNode} node |
| 232 * @return {Array.<string>} |
| 233 */ |
| 234 function prefixedElementClassNames(node) |
| 235 { |
| 236 var classAttribute = node.getAttribute("class"); |
| 237 if (!classAttribute) |
| 238 return []; |
| 239 |
| 240 return classAttribute.split(/\s+/g).filter(Boolean).map(function(name) { |
| 241 // The prefix is required to store "__proto__" in a object-based map
. |
| 242 return "$" + name; |
| 243 }); |
| 244 } |
| 245 |
| 246 /** |
| 247 * @param {string} id |
| 248 * @return {string} |
| 249 */ |
| 250 function idSelector(id) |
| 251 { |
| 252 return "#" + escapeIdentifierIfNeeded(id); |
| 253 } |
| 254 |
| 255 /** |
| 256 * @param {string} ident |
| 257 * @return {string} |
| 258 */ |
| 259 function escapeIdentifierIfNeeded(ident) |
| 260 { |
| 261 if (isCSSIdentifier(ident)) |
| 262 return ident; |
| 263 var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident); |
| 264 var lastIndex = ident.length - 1; |
| 265 return ident.replace(/./g, function(c, i) { |
| 266 return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? esca
peAsciiChar(c, i === lastIndex) : c; |
| 267 }); |
| 268 } |
| 269 |
| 270 /** |
| 271 * @param {string} c |
| 272 * @param {boolean} isLast |
| 273 * @return {string} |
| 274 */ |
| 275 function escapeAsciiChar(c, isLast) |
| 276 { |
| 277 return "\\" + toHexByte(c) + (isLast ? "" : " "); |
| 278 } |
| 279 |
| 280 /** |
| 281 * @param {string} c |
| 282 */ |
| 283 function toHexByte(c) |
| 284 { |
| 285 var hexByte = c.charCodeAt(0).toString(16); |
| 286 if (hexByte.length === 1) |
| 287 hexByte = "0" + hexByte; |
| 288 return hexByte; |
| 289 } |
| 290 |
| 291 /** |
| 292 * @param {string} c |
| 293 * @return {boolean} |
| 294 */ |
| 295 function isCSSIdentChar(c) |
| 296 { |
| 297 if (/[a-zA-Z0-9_-]/.test(c)) |
| 298 return true; |
| 299 return c.charCodeAt(0) >= 0xA0; |
| 300 } |
| 301 |
| 302 /** |
| 303 * @param {string} value |
| 304 * @return {boolean} |
| 305 */ |
| 306 function isCSSIdentifier(value) |
| 307 { |
| 308 return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value); |
| 309 } |
| 310 |
| 311 var prefixedOwnClassNamesArray = prefixedElementClassNames(node); |
| 312 var needsClassNames = false; |
| 313 var needsNthChild = false; |
| 314 var ownIndex = -1; |
| 315 var siblings = parent.children(); |
| 316 for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length;
++i) { |
| 317 var sibling = siblings[i]; |
| 318 if (sibling === node) { |
| 319 ownIndex = i; |
| 320 continue; |
| 321 } |
| 322 if (needsNthChild) |
| 323 continue; |
| 324 if (sibling.nodeNameInCorrectCase() !== nodeName) |
| 325 continue; |
| 326 |
| 327 needsClassNames = true; |
| 328 var ownClassNames = prefixedOwnClassNamesArray.keySet(); |
| 329 var ownClassNameCount = 0; |
| 330 for (var name in ownClassNames) |
| 331 ++ownClassNameCount; |
| 332 if (ownClassNameCount === 0) { |
| 333 needsNthChild = true; |
| 334 continue; |
| 335 } |
| 336 var siblingClassNamesArray = prefixedElementClassNames(sibling); |
| 337 for (var j = 0; j < siblingClassNamesArray.length; ++j) { |
| 338 var siblingClass = siblingClassNamesArray[j]; |
| 339 if (!ownClassNames.hasOwnProperty(siblingClass)) |
| 340 continue; |
| 341 delete ownClassNames[siblingClass]; |
| 342 if (!--ownClassNameCount) { |
| 343 needsNthChild = true; |
| 344 break; |
| 345 } |
| 346 } |
| 347 } |
| 348 |
| 349 var result = nodeName; |
| 350 if (needsNthChild) { |
| 351 result += ":nth-child(" + (ownIndex + 1) + ")"; |
| 352 } else if (needsClassNames) { |
| 353 for (var prefixedName in prefixedOwnClassNamesArray.keySet()) |
| 354 result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1)); |
| 355 } |
| 356 |
| 357 return new WebInspector.DOMNodePathStep(result, false); |
| 358 } |
| 359 |
| 360 /** |
| 361 * @param {!WebInspector.DOMNode} node |
| 362 * @param {boolean=} optimized |
| 363 * @return {string} |
| 364 */ |
| 365 WebInspector.DOMPresentationUtils.xPath = function(node, optimized) |
| 366 { |
| 367 if (node.nodeType() === Node.DOCUMENT_NODE) |
| 368 return "/"; |
| 369 |
| 370 var steps = []; |
| 371 var contextNode = node; |
| 372 while (contextNode) { |
| 373 var step = WebInspector.DOMPresentationUtils._xPathValue(contextNode, op
timized); |
| 374 if (!step) |
| 375 break; // Error - bail out early. |
| 376 steps.push(step); |
| 377 if (step.optimized) |
| 378 break; |
| 379 contextNode = contextNode.parentNode; |
| 380 } |
| 381 |
| 382 steps.reverse(); |
| 383 return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/"); |
| 384 } |
| 385 |
| 386 /** |
| 387 * @param {!WebInspector.DOMNode} node |
| 388 * @param {boolean=} optimized |
| 389 * @return {WebInspector.DOMNodePathStep} |
| 390 */ |
| 391 WebInspector.DOMPresentationUtils._xPathValue = function(node, optimized) |
| 392 { |
| 393 var ownValue; |
| 394 var ownIndex = WebInspector.DOMPresentationUtils._xPathIndex(node); |
| 395 if (ownIndex === -1) |
| 396 return null; // Error. |
| 397 |
| 398 switch (node.nodeType()) { |
| 399 case Node.ELEMENT_NODE: |
| 400 if (optimized && node.getAttribute("id")) |
| 401 return new WebInspector.DOMNodePathStep("//*[@id=\"" + node.getAttri
bute("id") + "\"]", true); |
| 402 ownValue = node.localName(); |
| 403 break; |
| 404 case Node.ATTRIBUTE_NODE: |
| 405 ownValue = "@" + node.nodeName(); |
| 406 break; |
| 407 case Node.TEXT_NODE: |
| 408 case Node.CDATA_SECTION_NODE: |
| 409 ownValue = "text()"; |
| 410 break; |
| 411 case Node.PROCESSING_INSTRUCTION_NODE: |
| 412 ownValue = "processing-instruction()"; |
| 413 break; |
| 414 case Node.COMMENT_NODE: |
| 415 ownValue = "comment()"; |
| 416 break; |
| 417 case Node.DOCUMENT_NODE: |
| 418 ownValue = ""; |
| 419 break; |
| 420 default: |
| 421 ownValue = ""; |
| 422 break; |
| 423 } |
| 424 |
| 425 if (ownIndex > 0) |
| 426 ownValue += "[" + ownIndex + "]"; |
| 427 |
| 428 return new WebInspector.DOMNodePathStep(ownValue, node.nodeType() === Node.D
OCUMENT_NODE); |
| 429 }, |
| 430 |
| 431 /** |
| 432 * @param {!WebInspector.DOMNode} node |
| 433 * @return {number} |
| 434 */ |
| 435 WebInspector.DOMPresentationUtils._xPathIndex = function(node) |
| 436 { |
| 437 // Returns -1 in case of error, 0 if no siblings matching the same expressio
n, <XPath index among the same expression-matching sibling nodes> otherwise. |
| 438 function areNodesSimilar(left, right) |
| 439 { |
| 440 if (left === right) |
| 441 return true; |
| 442 |
| 443 if (left.nodeType() === Node.ELEMENT_NODE && right.nodeType() === Node.E
LEMENT_NODE) |
| 444 return left.localName() === right.localName(); |
| 445 |
| 446 if (left.nodeType() === right.nodeType()) |
| 447 return true; |
| 448 |
| 449 // XPath treats CDATA as text nodes. |
| 450 var leftType = left.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_N
ODE : left.nodeType(); |
| 451 var rightType = right.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT
_NODE : right.nodeType(); |
| 452 return leftType === rightType; |
| 453 } |
| 454 |
| 455 var siblings = node.parentNode ? node.parentNode.children() : null; |
| 456 if (!siblings) |
| 457 return 0; // Root node - no siblings. |
| 458 var hasSameNamedElements; |
| 459 for (var i = 0; i < siblings.length; ++i) { |
| 460 if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) { |
| 461 hasSameNamedElements = true; |
| 462 break; |
| 463 } |
| 464 } |
| 465 if (!hasSameNamedElements) |
| 466 return 0; |
| 467 var ownIndex = 1; // XPath indices start with 1. |
| 468 for (var i = 0; i < siblings.length; ++i) { |
| 469 if (areNodesSimilar(node, siblings[i])) { |
| 470 if (siblings[i] === node) |
| 471 return ownIndex; |
| 472 ++ownIndex; |
| 473 } |
| 474 } |
| 475 return -1; // An error occurred: |node| not found in parent's children. |
| 476 } |
| 477 |
| 478 /** |
| 479 * @constructor |
| 480 * @param {string} value |
| 481 * @param {boolean} optimized |
| 482 */ |
| 483 WebInspector.DOMNodePathStep = function(value, optimized) |
| 484 { |
| 485 this.value = value; |
| 486 this.optimized = optimized || false; |
| 487 } |
| 488 |
| 489 WebInspector.DOMNodePathStep.prototype = { |
| 490 /** |
| 491 * @return {string} |
| 492 */ |
| 493 toString: function() |
| 494 { |
| 495 return this.value; |
| 496 } |
| 497 } |
OLD | NEW |