• 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
93WebInspector.DOMPresentationUtils.linkifyNodeReference = function(node)
94{
95    var link = document.createElement("span");
96    link.className = "node-link";
97    WebInspector.DOMPresentationUtils.decorateNodeLabel(node, link);
98
99    link.addEventListener("click", WebInspector.domAgent.inspectElement.bind(WebInspector.domAgent, node.id), false);
100    link.addEventListener("mouseover", WebInspector.domAgent.highlightDOMNode.bind(WebInspector.domAgent, node.id, "", undefined), false);
101    link.addEventListener("mouseout", WebInspector.domAgent.hideDOMNodeHighlight.bind(WebInspector.domAgent), false);
102
103    return link;
104}
105
106WebInspector.DOMPresentationUtils.linkifyNodeById = function(nodeId)
107{
108    var node = WebInspector.domAgent.nodeForId(nodeId);
109    if (!node)
110        return document.createTextNode(WebInspector.UIString("<node>"));
111    return WebInspector.DOMPresentationUtils.linkifyNodeReference(node);
112}
113
114/**
115 * @param {string} imageURL
116 * @param {boolean} showDimensions
117 * @param {function(!Element=)} userCallback
118 * @param {!Object=} precomputedDimensions
119 */
120WebInspector.DOMPresentationUtils.buildImagePreviewContents = function(imageURL, showDimensions, userCallback, precomputedDimensions)
121{
122    var resource = WebInspector.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.appropriateSelectorFor = function(node, justSelector)
168{
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.getAttribute("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 */
183WebInspector.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 */
209WebInspector.DOMPresentationUtils._cssPathValue = function(node, optimized)
210{
211    if (node.nodeType() !== Node.ELEMENT_NODE)
212        return null;
213
214    var id = node.getAttribute("id");
215    if (optimized) {
216        if (id)
217            return new WebInspector.DOMNodePathStep(idSelector(id), true);
218        var nodeNameLower = node.nodeName().toLowerCase();
219        if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html")
220            return new WebInspector.DOMNodePathStep(node.nodeNameInCorrectCase(), true);
221    }
222    var nodeName = node.nodeNameInCorrectCase();
223
224    if (id)
225        return new WebInspector.DOMNodePathStep(nodeName + idSelector(id), true);
226    var parent = node.parentNode;
227    if (!parent || parent.nodeType() === Node.DOCUMENT_NODE)
228        return new WebInspector.DOMNodePathStep(nodeName, true);
229
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)) ? escapeAsciiChar(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 */
365WebInspector.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, optimized);
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 */
391WebInspector.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.getAttribute("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.DOCUMENT_NODE);
429},
430
431/**
432 * @param {!WebInspector.DOMNode} node
433 * @return {number}
434 */
435WebInspector.DOMPresentationUtils._xPathIndex = function(node)
436{
437    // Returns -1 in case of error, 0 if no siblings matching the same expression, <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.ELEMENT_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_NODE : 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 */
483WebInspector.DOMNodePathStep = function(value, optimized)
484{
485    this.value = value;
486    this.optimized = optimized || false;
487}
488
489WebInspector.DOMNodePathStep.prototype = {
490    /**
491     * @return {string}
492     */
493    toString: function()
494    {
495        return this.value;
496    }
497}
498