• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@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
32WebInspector.DOMPresentationUtils = {}
33
34WebInspector.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.appendChild(document.createTextNode(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.appendChild(document.createTextNode(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 */
83WebInspector.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 */
97WebInspector.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", node.reveal.bind(node), 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 */
120WebInspector.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 */
167WebInspector.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 */
178WebInspector.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 */
197WebInspector.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 */
224WebInspector.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 */
386WebInspector.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 */
412WebInspector.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 */
456WebInspector.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 */
504WebInspector.DOMNodePathStep = function(value, optimized)
505{
506    this.value = value;
507    this.optimized = optimized || false;
508}
509
510WebInspector.DOMNodePathStep.prototype = {
511    /**
512     * @return {string}
513     */
514    toString: function()
515    {
516        return this.value;
517    }
518}
519