2aab4f393e8576ba07e229e5661ca748b85eb10f
[WebKit-https.git] / WebCore / page / inspector / utilities.js
1 /*
2  * Copyright (C) 2007 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 Object.describe = function(obj, abbreviated)
30 {
31     if (obj === undefined)
32         return "undefined";
33     if (obj === null)
34         return "null";
35
36     var type1 = typeof obj;
37     var type2 = "";
38     if (type1 == "object" || type1 == "function") {
39         if (obj instanceof String)
40             type1 = "string";
41         else if (obj instanceof Array)
42             type1 = "array";
43         else if (obj instanceof Boolean)
44             type1 = "boolean";
45         else if (obj instanceof Number)
46             type1 = "number";
47         else if (obj instanceof Date)
48             type1 = "date";
49         else if (obj instanceof RegExp)
50             type1 = "regexp";
51         else if (obj instanceof Error)
52             type1 = "error";
53         type2 = Object.prototype.toString.call(obj).replace(/^\[object (.*)\]$/i, "$1");
54     }
55
56     switch (type1) {
57     case "object":
58         return type2;
59     case "array":
60         return "[" + obj.toString() + "]";
61     case "string":
62         if (obj.length > 100)
63             return "\"" + obj.substring(0, 100) + "\u2026\"";
64         return "\"" + obj + "\"";
65     case "function":
66         var objectText = String(obj);
67         if (!/^function /.test(objectText))
68             objectText = (type2 == "object") ? type1 : type2;
69         else if (abbreviated)
70             objectText = /.*/.exec(obj)[0].replace(/ +$/g, "");
71         return objectText;
72     case "regexp":
73         return String(obj).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1);
74     default:
75         return String(obj);
76     }
77 }
78
79 Object.sortedProperties = function(obj)
80 {
81     var properties = [];
82     for (var prop in obj) {
83         properties.push(prop);
84     }
85
86     properties.sort();
87     return properties;
88 }
89
90 Element.prototype.removeStyleClass = function(className) 
91 {
92     if (this.hasStyleClass(className))
93         this.className = this.className.replace(className, "");
94 }
95
96 Element.prototype.addStyleClass = function(className) 
97 {
98     if (!this.hasStyleClass(className))
99         this.className += (this.className.length ? " " + className : className);
100 }
101
102 Element.prototype.hasStyleClass = function(className) 
103 {
104     return this.className.indexOf(className) !== -1;
105 }
106
107 Element.prototype.scrollToElement = function(element)
108 {
109     if (!element || !this.isAncestor(element))
110         return;
111
112     var offsetTop = 0;
113     var current = element
114     while (current && current !== this) {
115         offsetTop += current.offsetTop;
116         current = current.offsetParent;
117     }
118
119     if (this.scrollTop > offsetTop)
120         this.scrollTop = offsetTop;
121     else if ((this.scrollTop + this.offsetHeight) < (offsetTop + element.offsetHeight))
122         this.scrollTop = offsetTop - this.offsetHeight + element.offsetHeight;
123 }
124
125 Node.prototype.firstParentOrSelfWithNodeName = function(nodeName)
126 {
127     for (var node = this; node && (node !== document); node = node.parentNode)
128         if (node.nodeName.toLowerCase() === nodeName.toLowerCase())
129             return node;
130
131     return null;
132 }
133
134 Node.prototype.firstParentOrSelfWithClass = function(className) 
135 {
136     for (var node = this; node && (node !== document); node = node.parentNode)
137         if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
138             return node;
139
140     return null;
141 }
142
143 Node.prototype.firstParentWithClass = function(className)
144 {
145     if (!this.parentNode)
146         return null;
147
148     return this.parentNode.firstParentOrSelfWithClass(className);
149 }
150
151 Element.prototype.query = function(query) 
152 {
153     return document.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
154 }
155
156 Element.prototype.removeChildren = function()
157 {
158     while (this.firstChild) 
159         this.removeChild(this.firstChild);        
160 }
161
162 Element.prototype.firstChildSkippingWhitespace = firstChildSkippingWhitespace;
163 Element.prototype.lastChildSkippingWhitespace = lastChildSkippingWhitespace;
164
165 Node.prototype.isWhitespace = isNodeWhitespace;
166 Node.prototype.nodeTypeName = nodeTypeName;
167 Node.prototype.displayName = nodeDisplayName;
168 Node.prototype.contentPreview = nodeContentPreview;
169 Node.prototype.isAncestor = isAncestorNode;
170 Node.prototype.isDescendant = isDescendantNode;
171 Node.prototype.firstCommonAncestor = firstCommonNodeAncestor;
172 Node.prototype.nextSiblingSkippingWhitespace = nextSiblingSkippingWhitespace;
173 Node.prototype.previousSiblingSkippingWhitespace = previousSiblingSkippingWhitespace;
174 Node.prototype.traverseNextNode = traverseNextNode;
175 Node.prototype.traversePreviousNode = traversePreviousNode;
176 Node.prototype.onlyTextChild = onlyTextChild;
177 Node.prototype.titleInfo = nodeTitleInfo;
178
179 String.prototype.hasSubstring = function(string, caseInsensitive)
180 {
181     if (!caseInsensitive)
182         return this.indexOf(string) !== -1;
183     return this.match(new RegExp(string.escapeForRegExp(), "i"));
184 }
185
186 String.prototype.escapeCharacters = function(chars)
187 {
188     var foundChar = false;
189     for (var i = 0; i < chars.length; ++i) {
190         if (this.indexOf(chars.charAt(i)) !== -1) {
191             foundChar = true;
192             break;
193         }
194     }
195
196     if (!foundChar)
197         return this;
198
199     var result = "";
200     for (var i = 0; i < this.length; ++i) {
201         if (chars.indexOf(this.charAt(i)) !== -1)
202             result += "\\";
203         result += this.charAt(i);
204     }
205
206     return result;
207 }
208
209 String.prototype.escapeForRegExp = function()
210 {
211     return this.escapeCharacters("^[]{}()\\.$*+?|");
212 }
213
214 String.prototype.escapeHTML = function()
215 {
216     return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
217 }
218
219 String.prototype.collapseWhitespace = function()
220 {
221     return this.replace(/[\s\xA0]+/g, " ");
222 }
223
224 String.prototype.trimLeadingWhitespace = function()
225 {
226     return this.replace(/^[\s\xA0]+/g, "");
227 }
228
229 String.prototype.trimTrailingWhitespace = function()
230 {
231     return this.replace(/[\s\xA0]+$/g, "");
232 }
233
234 String.prototype.trimWhitespace = function()
235 {
236     return this.replace(/^[\s\xA0]+|[\s\xA0]+$/g, "");
237 }
238
239 String.prototype.trimURL = function(baseURLDomain)
240 {
241     var result = this.replace(new RegExp("^http[s]?:\/\/", "i"), "");
242     if (baseURLDomain)
243         result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
244     return result;
245 }
246
247 function isNodeWhitespace()
248 {
249     if (!this || this.nodeType !== Node.TEXT_NODE)
250         return false;
251     if (!this.nodeValue.length)
252         return true;
253     return this.nodeValue.match(/^[\s\xA0]+$/);
254 }
255
256 function nodeTypeName()
257 {
258     if (!this)
259         return "(unknown)";
260
261     switch (this.nodeType) {
262         case Node.ELEMENT_NODE: return "Element";
263         case Node.ATTRIBUTE_NODE: return "Attribute";
264         case Node.TEXT_NODE: return "Text";
265         case Node.CDATA_SECTION_NODE: return "Character Data";
266         case Node.ENTITY_REFERENCE_NODE: return "Entity Reference";
267         case Node.ENTITY_NODE: return "Entity";
268         case Node.PROCESSING_INSTRUCTION_NODE: return "Processing Instruction";
269         case Node.COMMENT_NODE: return "Comment";
270         case Node.DOCUMENT_NODE: return "Document";
271         case Node.DOCUMENT_TYPE_NODE: return "Document Type";
272         case Node.DOCUMENT_FRAGMENT_NODE: return "Document Fragment";
273         case Node.NOTATION_NODE: return "Notation";
274     }
275
276     return "(unknown)";
277 }
278
279 function nodeDisplayName()
280 {
281     if (!this)
282         return "";
283
284     switch (this.nodeType) {
285         case Node.DOCUMENT_NODE:
286             return "Document";
287
288         case Node.ELEMENT_NODE:
289             var name = "<" + this.nodeName.toLowerCase();
290
291             if (this.hasAttributes()) {
292                 var value = this.getAttribute("id");
293                 if (value)
294                     name += " id=\"" + value + "\"";
295                 value = this.getAttribute("class");
296                 if (value)
297                     name += " class=\"" + value + "\"";
298                 if (this.nodeName.toLowerCase() === "a") {
299                     value = this.getAttribute("name");
300                     if (value)
301                         name += " name=\"" + value + "\"";
302                     value = this.getAttribute("href");
303                     if (value)
304                         name += " href=\"" + value + "\"";
305                 } else if (this.nodeName.toLowerCase() === "img") {
306                     value = this.getAttribute("src");
307                     if (value)
308                         name += " src=\"" + value + "\"";
309                 } else if (this.nodeName.toLowerCase() === "iframe") {
310                     value = this.getAttribute("src");
311                     if (value)
312                         name += " src=\"" + value + "\"";
313                 } else if (this.nodeName.toLowerCase() === "input") {
314                     value = this.getAttribute("name");
315                     if (value)
316                         name += " name=\"" + value + "\"";
317                     value = this.getAttribute("type");
318                     if (value)
319                         name += " type=\"" + value + "\"";
320                 } else if (this.nodeName.toLowerCase() === "form") {
321                     value = this.getAttribute("action");
322                     if (value)
323                         name += " action=\"" + value + "\"";
324                 }
325             }
326
327             return name + ">";
328
329         case Node.TEXT_NODE:
330             if (isNodeWhitespace.call(this))
331                 return "(whitespace)";
332             return "\"" + this.nodeValue + "\"";
333
334         case Node.COMMENT_NODE:
335             return "<!--" + this.nodeValue + "-->";
336     }
337
338     return this.nodeName.toLowerCase().collapseWhitespace();
339 }
340
341 function nodeContentPreview()
342 {
343     if (!this || !this.hasChildNodes || !this.hasChildNodes())
344         return "";
345
346     var limit = 0;
347     var preview = "";
348
349     // always skip whitespace here
350     var currentNode = traverseNextNode.call(this, true, this);
351     while (currentNode) {
352         if (currentNode.nodeType === Node.TEXT_NODE)
353             preview += currentNode.nodeValue.escapeHTML();
354         else
355             preview += nodeDisplayName.call(currentNode).escapeHTML();
356
357         currentNode = traverseNextNode.call(currentNode, true, this);
358
359         if (++limit > 4) {
360             preview += "&#x2026;"; // ellipsis
361             break;
362         }
363     }
364
365     return preview.collapseWhitespace();
366 }
367
368 function isAncestorNode(ancestor)
369 {
370     if (!this || !ancestor)
371         return false;
372
373     var currentNode = ancestor.parentNode;
374     while (currentNode) {
375         if (this === currentNode)
376             return true;
377         currentNode = currentNode.parentNode;
378     }
379
380     return false;
381 }
382
383 function isDescendantNode(descendant)
384 {
385     return isAncestorNode.call(descendant, this);
386 }
387
388 function firstCommonNodeAncestor(node)
389 {
390     if (!this || !node)
391         return;
392
393     var node1 = this.parentNode;
394     var node2 = node.parentNode;
395
396     if ((!node1 || !node2) || node1 !== node2)
397         return null;
398
399     while (node1 && node2) {
400         if (!node1.parentNode || !node2.parentNode)
401             break;
402         if (node1 !== node2)
403             break;
404
405         node1 = node1.parentNode;
406         node2 = node2.parentNode;
407     }
408
409     return node1;
410 }
411
412 function nextSiblingSkippingWhitespace()
413 {
414     if (!this)
415         return;
416     var node = this.nextSibling;
417     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
418         node = node.nextSibling;
419     return node;
420 }
421
422 function previousSiblingSkippingWhitespace()
423 {
424     if (!this)
425         return;
426     var node = this.previousSibling;
427     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
428         node = node.previousSibling;
429     return node;
430 }
431
432 function firstChildSkippingWhitespace()
433 {
434     if (!this)
435         return;
436     var node = this.firstChild;
437     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
438         node = nextSiblingSkippingWhitespace.call(node);
439     return node;
440 }
441
442 function lastChildSkippingWhitespace()
443 {
444     if (!this)
445         return;
446     var node = this.lastChild;
447     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
448         node = previousSiblingSkippingWhitespace.call(node);
449     return node;
450 }
451
452 function traverseNextNode(skipWhitespace, stayWithin)
453 {
454     if (!this)
455         return;
456
457     var node = skipWhitespace ? firstChildSkippingWhitespace.call(this) : this.firstChild;
458     if (node)
459         return node;
460
461     if (stayWithin && this === stayWithin)
462         return null;
463
464     node = skipWhitespace ? nextSiblingSkippingWhitespace.call(this) : this.nextSibling;
465     if (node)
466         return node;
467
468     node = this;
469     while (node && !(skipWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling) && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
470         node = node.parentNode;
471     if (!node)
472         return null;
473
474     return skipWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
475 }
476
477 function traversePreviousNode(skipWhitespace)
478 {
479     if (!this)
480         return;
481     var node = skipWhitespace ? previousSiblingSkippingWhitespace.call(this) : this.previousSibling;
482     while (node && (skipWhitespace ? lastChildSkippingWhitespace.call(node) : node.lastChild) )
483         node = skipWhitespace ? lastChildSkippingWhitespace.call(node) : node.lastChild;
484     if (node)
485         return node;
486     return this.parentNode;
487 }
488
489 function onlyTextChild(ignoreWhitespace)
490 {
491     if (!this)
492         return null;
493
494     var firstChild = ignoreWhitespace ? firstChildSkippingWhitespace.call(this) : this.firstChild;
495     if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE)
496         return null;
497
498     var sibling = ignoreWhitespace ? nextSiblingSkippingWhitespace.call(firstChild) : firstChild.nextSibling;
499     return sibling ? null : firstChild;
500 }
501
502 function nodeTitleInfo(hasChildren, linkify)
503 {
504     var info = {title: '', hasChildren: hasChildren};
505
506     switch (this.nodeType) {
507         case Node.DOCUMENT_NODE:
508             info.title = "Document";
509             break;
510
511         case Node.ELEMENT_NODE:
512             info.title = "<span class=\"webkit-html-tag\">&lt;" + this.nodeName.toLowerCase().escapeHTML();
513
514             if (this.hasAttributes()) {
515                 for (var i = 0; i < this.attributes.length; ++i) {
516                     var attr = this.attributes[i];
517                     var value = attr.value.escapeHTML();
518                     value = value.replace(/([\/;:\)\]\}])/g, "$1&#8203;");
519
520                     info.title += " <span class=\"webkit-html-attribute-name\">" + attr.name.escapeHTML() + "</span>=&#8203;";
521
522                     if (linkify && (attr.name === "src" || attr.name === "href"))
523                         info.title += linkify(attr.value, value, "webkit-html-attribute-value", this.nodeName.toLowerCase() == "a");
524                     else
525                         info.title += "<span class=\"webkit-html-attribute-value\">\"" + value + "\"</span>";
526                 }
527             }
528             info.title += "&gt;</span>&#8203;";
529
530             // If this element only has a single child that is a text node,
531             // just show that text and the closing tag inline rather than
532             // create a subtree for them
533
534             var textChild = onlyTextChild.call(this, Preferences.ignoreWhitespace);
535             var showInlineText = textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength;
536
537             if (showInlineText) {
538                 info.title += textChild.nodeValue.escapeHTML() + "&#8203;<span class=\"webkit-html-tag\">&lt;/" + this.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
539                 info.hasChildren = false;
540             }
541             break;
542
543         case Node.TEXT_NODE:
544             if (isNodeWhitespace.call(this))
545                 info.title = "(whitespace)";
546             else
547                 info.title = "\"" + this.nodeValue.escapeHTML() + "\"";
548             break
549
550         case Node.COMMENT_NODE:
551             info.title = "<span class=\"webkit-html-comment\">&lt;!--" + this.nodeValue.escapeHTML() + "--&gt;</span>";
552             break;
553
554         default:
555             info.title = this.nodeName.toLowerCase().collapseWhitespace().escapeHTML();
556     }
557
558     return info;
559 }
560
561 Number.secondsToString = function(seconds)
562 {
563     var ms = seconds * 1000;
564     if (ms < 1000)
565         return Math.round(ms) + "ms";
566
567     if (seconds < 60)
568         return (Math.round(seconds * 100) / 100) + "s";
569
570     var minutes = seconds / 60;
571     if (minutes < 60)
572         return (Math.round(minutes * 10) / 10) + "min";
573
574     var hours = minutes / 60;
575     if (hours < 24)
576         return (Math.round(hours * 10) / 10) + "hrs";
577
578     var days = hours / 24;
579     return (Math.round(days * 10) / 10) + " days";
580 }
581
582 Number.bytesToString = function(bytes)
583 {
584     if (bytes < 1024)
585         return bytes + "B";
586
587     var kilobytes = bytes / 1024;
588     if (kilobytes < 1024)
589         return (Math.round(kilobytes * 100) / 100) + "KB";
590
591     var megabytes = kilobytes / 1024;
592     return (Math.round(megabytes * 1000) / 1000) + "MB";
593 }
594
595 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
596 {
597     var length = this.value.length;
598     this.setSelectionRange(length, length);
599 }
600
601 // This code is in the public domain. Feel free to link back to http://jan.moesen.nu/
602 String.sprintf = function()
603 {
604         if (!arguments || arguments.length < 1 || !RegExp)
605         {
606                 return;
607         }
608         var str = arguments[0];
609         var re = /([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X)(.*)/;
610         var a = b = [], numSubstitutions = 0, numMatches = 0;
611         while (a = re.exec(str))
612         {
613                 var leftpart = a[1], pPad = a[2], pJustify = a[3], pMinLength = a[4];
614                 var pPrecision = a[5], pType = a[6], rightPart = a[7];
615
616                 //alert(a + '\n' + [a[0], leftpart, pPad, pJustify, pMinLength, pPrecision);
617
618                 numMatches++;
619                 if (pType == '%')
620                 {
621                         subst = '%';
622                 }
623                 else
624                 {
625                         numSubstitutions++;
626                         if (numSubstitutions >= arguments.length)
627                         {
628                                 alert('Error! Not enough function arguments (' + (arguments.length - 1) + ', excluding the string)\nfor the number of substitution parameters in string (' + numSubstitutions + ' so far).');
629                         }
630                         var param = arguments[numSubstitutions];
631                         var pad = '';
632                                if (pPad && pPad.substr(0,1) == "'") pad = leftpart.substr(1,1);
633                           else if (pPad) pad = pPad;
634                         var justifyRight = true;
635                                if (pJustify && pJustify === "-") justifyRight = false;
636                         var minLength = -1;
637                                if (pMinLength) minLength = parseInt(pMinLength);
638                         var precision = -1;
639                                if (pPrecision && pType == 'f') precision = parseInt(pPrecision.substring(1));
640                         var subst = param;
641                                if (pType == 'b') subst = parseInt(param).toString(2);
642                           else if (pType == 'c') subst = String.fromCharCode(parseInt(param));
643                           else if (pType == 'd') subst = parseInt(param) ? parseInt(param) : 0;
644                           else if (pType == 'u') subst = Math.abs(param);
645                           else if (pType == 'f') subst = (precision > -1) ? Math.round(parseFloat(param) * Math.pow(10, precision)) / Math.pow(10, precision): parseFloat(param);
646                           else if (pType == 'o') subst = parseInt(param).toString(8);
647                           else if (pType == 's') subst = param;
648                           else if (pType == 'x') subst = ('' + parseInt(param).toString(16)).toLowerCase();
649                           else if (pType == 'X') subst = ('' + parseInt(param).toString(16)).toUpperCase();
650                 }
651                 str = leftpart + subst + rightPart;
652         }
653         return str;
654 }