5d295d4f66b6919e48d8e9ea32de07742ceac9eb
[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.type = function(obj, win)
30 {
31     if (obj === null)
32         return "null";
33
34     var type = typeof obj;
35     if (type !== "object" && type !== "function")
36         return type;
37
38     win = win || window;
39
40     if (obj instanceof win.String)
41         return "string";
42     if (obj instanceof win.Array)
43         return "array";
44     if (obj instanceof win.Boolean)
45         return "boolean";
46     if (obj instanceof win.Number)
47         return "number";
48     if (obj instanceof win.Date)
49         return "date";
50     if (obj instanceof win.RegExp)
51         return "regexp";
52     if (obj instanceof win.Error)
53         return "error";
54     return type;
55 }
56
57 Object.describe = function(obj, abbreviated)
58 {
59     var type1 = Object.type(obj);
60     var type2 = Object.prototype.toString.call(obj).replace(/^\[object (.*)\]$/i, "$1");
61
62     switch (type1) {
63     case "object":
64         return type2;
65     case "array":
66         return "[" + obj.toString() + "]";
67     case "string":
68         if (obj.length > 100)
69             return "\"" + obj.substring(0, 100) + "\u2026\"";
70         return "\"" + obj + "\"";
71     case "function":
72         var objectText = String(obj);
73         if (!/^function /.test(objectText))
74             objectText = (type2 == "object") ? type1 : type2;
75         else if (abbreviated)
76             objectText = /.*/.exec(obj)[0].replace(/ +$/g, "");
77         return objectText;
78     case "regexp":
79         return String(obj).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1);
80     default:
81         return String(obj);
82     }
83 }
84
85 Object.sortedProperties = function(obj)
86 {
87     var properties = [];
88     for (var prop in obj)
89         properties.push(prop);
90     properties.sort();
91     return properties;
92 }
93
94 Function.prototype.bind = function(thisObject)
95 {
96     var func = this;
97     var args = Array.prototype.slice.call(arguments, 1);
98     return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))) };
99 }
100
101 Element.prototype.removeStyleClass = function(className) 
102 {
103     // Test for the simple case before using a RegExp.
104     if (this.className === className) {
105         this.className = "";
106         return;
107     }
108
109     var regex = new RegExp("(^|\\s+)" + className.escapeForRegExp() + "($|\\s+)");
110     if (regex.test(this.className))
111         this.className = this.className.replace(regex, " ");
112 }
113
114 Element.prototype.addStyleClass = function(className) 
115 {
116     if (className && !this.hasStyleClass(className))
117         this.className += (this.className.length ? " " + className : className);
118 }
119
120 Element.prototype.hasStyleClass = function(className) 
121 {
122     if (!className)
123         return false;
124     // Test for the simple case before using a RegExp.
125     if (this.className === className)
126         return true;
127     var regex = new RegExp("(^|\\s)" + className.escapeForRegExp() + "($|\\s)");
128     return regex.test(this.className);
129 }
130
131 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
132 {
133     for (var node = this; node && (node !== document); node = node.parentNode)
134         for (var i = 0; i < nameArray.length; ++i)
135             if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
136                 return node;
137     return null;
138 }
139
140 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
141 {
142     return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
143 }
144
145 Node.prototype.enclosingNodeOrSelfWithClass = function(className)
146 {
147     for (var node = this; node && (node !== document); node = node.parentNode)
148         if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
149             return node;
150     return null;
151 }
152
153 Node.prototype.enclosingNodeWithClass = function(className)
154 {
155     if (!this.parentNode)
156         return null;
157     return this.parentNode.enclosingNodeOrSelfWithClass(className);
158 }
159
160 Element.prototype.query = function(query) 
161 {
162     return document.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
163 }
164
165 Element.prototype.removeChildren = function()
166 {
167     while (this.firstChild) 
168         this.removeChild(this.firstChild);        
169 }
170
171 Element.prototype.isInsertionCaretInside = function()
172 {
173     var selection = window.getSelection();
174     if (!selection.rangeCount || !selection.isCollapsed)
175         return false;
176     var selectionRange = selection.getRangeAt(0);
177     return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
178 }
179
180 Element.prototype.__defineGetter__("totalOffsetLeft", function()
181 {
182     var total = 0;
183     for (var element = this; element; element = element.offsetParent)
184         total += element.offsetLeft;
185     return total;
186 });
187
188 Element.prototype.__defineGetter__("totalOffsetTop", function()
189 {
190     var total = 0;
191     for (var element = this; element; element = element.offsetParent)
192         total += element.offsetTop;
193     return total;
194 });
195
196 Element.prototype.firstChildSkippingWhitespace = firstChildSkippingWhitespace;
197 Element.prototype.lastChildSkippingWhitespace = lastChildSkippingWhitespace;
198
199 Node.prototype.isWhitespace = isNodeWhitespace;
200 Node.prototype.nodeTypeName = nodeTypeName;
201 Node.prototype.displayName = nodeDisplayName;
202 Node.prototype.contentPreview = nodeContentPreview;
203 Node.prototype.isAncestor = isAncestorNode;
204 Node.prototype.isDescendant = isDescendantNode;
205 Node.prototype.firstCommonAncestor = firstCommonNodeAncestor;
206 Node.prototype.nextSiblingSkippingWhitespace = nextSiblingSkippingWhitespace;
207 Node.prototype.previousSiblingSkippingWhitespace = previousSiblingSkippingWhitespace;
208 Node.prototype.traverseNextNode = traverseNextNode;
209 Node.prototype.traversePreviousNode = traversePreviousNode;
210 Node.prototype.onlyTextChild = onlyTextChild;
211
212 String.prototype.hasSubstring = function(string, caseInsensitive)
213 {
214     if (!caseInsensitive)
215         return this.indexOf(string) !== -1;
216     return this.match(new RegExp(string.escapeForRegExp(), "i"));
217 }
218
219 String.prototype.escapeCharacters = function(chars)
220 {
221     var foundChar = false;
222     for (var i = 0; i < chars.length; ++i) {
223         if (this.indexOf(chars.charAt(i)) !== -1) {
224             foundChar = true;
225             break;
226         }
227     }
228
229     if (!foundChar)
230         return this;
231
232     var result = "";
233     for (var i = 0; i < this.length; ++i) {
234         if (chars.indexOf(this.charAt(i)) !== -1)
235             result += "\\";
236         result += this.charAt(i);
237     }
238
239     return result;
240 }
241
242 String.prototype.escapeForRegExp = function()
243 {
244     return this.escapeCharacters("^[]{}()\\.$*+?|");
245 }
246
247 String.prototype.escapeHTML = function()
248 {
249     return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
250 }
251
252 String.prototype.collapseWhitespace = function()
253 {
254     return this.replace(/[\s\xA0]+/g, " ");
255 }
256
257 String.prototype.trimLeadingWhitespace = function()
258 {
259     return this.replace(/^[\s\xA0]+/g, "");
260 }
261
262 String.prototype.trimTrailingWhitespace = function()
263 {
264     return this.replace(/[\s\xA0]+$/g, "");
265 }
266
267 String.prototype.trimWhitespace = function()
268 {
269     return this.replace(/^[\s\xA0]+|[\s\xA0]+$/g, "");
270 }
271
272 String.prototype.trimURL = function(baseURLDomain)
273 {
274     var result = this.replace(new RegExp("^http[s]?:\/\/", "i"), "");
275     if (baseURLDomain)
276         result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
277     return result;
278 }
279
280 function getShorthandValue(style, shorthandProperty)
281 {
282     var value = style.getPropertyValue(shorthandProperty);
283     if (!value) {
284         // Some shorthands (like border) return a null value, so compute a shorthand value.
285         // FIXME: remove this when http://bugs.webkit.org/show_bug.cgi?id=15823 is fixed.
286
287         var foundProperties = {};
288         for (var i = 0; i < style.length; ++i) {
289             var individualProperty = style[i];
290             if (individualProperty in foundProperties || style.getPropertyShorthand(individualProperty) !== shorthandProperty)
291                 continue;
292
293             var individualValue = style.getPropertyValue(individualProperty);
294             if (style.isPropertyImplicit(individualProperty) || individualValue === "initial")
295                 continue;
296
297             foundProperties[individualProperty] = true;
298
299             if (!value)
300                 value = "";
301             else if (value.length)
302                 value += " ";
303             value += individualValue;
304         }
305     }
306     return value;
307 }
308
309 function getShorthandPriority(style, shorthandProperty)
310 {
311     var priority = style.getPropertyPriority(shorthandProperty);
312     if (!priority) {
313         for (var i = 0; i < style.length; ++i) {
314             var individualProperty = style[i];
315             if (style.getPropertyShorthand(individualProperty) !== shorthandProperty)
316                 continue;
317             priority = style.getPropertyPriority(individualProperty);
318             break;
319         }
320     }
321     return priority;
322 }
323
324 function getLonghandProperties(style, shorthandProperty)
325 {
326     var properties = [];
327     var foundProperties = {};
328
329     for (var i = 0; i < style.length; ++i) {
330         var individualProperty = style[i];
331         if (individualProperty in foundProperties || style.getPropertyShorthand(individualProperty) !== shorthandProperty)
332             continue;
333         foundProperties[individualProperty] = true;
334         properties.push(individualProperty);
335     }
336
337     return properties;
338 }
339
340 function getUniqueStyleProperties(style)
341 {
342     var properties = [];
343     var foundProperties = {};
344
345     for (var i = 0; i < style.length; ++i) {
346         var property = style[i];
347         if (property in foundProperties)
348             continue;
349         foundProperties[property] = true;
350         properties.push(property);
351     }
352
353     return properties;
354 }
355
356 function isNodeWhitespace()
357 {
358     if (!this || this.nodeType !== Node.TEXT_NODE)
359         return false;
360     if (!this.nodeValue.length)
361         return true;
362     return this.nodeValue.match(/^[\s\xA0]+$/);
363 }
364
365 function nodeTypeName()
366 {
367     if (!this)
368         return "(unknown)";
369
370     switch (this.nodeType) {
371         case Node.ELEMENT_NODE: return "Element";
372         case Node.ATTRIBUTE_NODE: return "Attribute";
373         case Node.TEXT_NODE: return "Text";
374         case Node.CDATA_SECTION_NODE: return "Character Data";
375         case Node.ENTITY_REFERENCE_NODE: return "Entity Reference";
376         case Node.ENTITY_NODE: return "Entity";
377         case Node.PROCESSING_INSTRUCTION_NODE: return "Processing Instruction";
378         case Node.COMMENT_NODE: return "Comment";
379         case Node.DOCUMENT_NODE: return "Document";
380         case Node.DOCUMENT_TYPE_NODE: return "Document Type";
381         case Node.DOCUMENT_FRAGMENT_NODE: return "Document Fragment";
382         case Node.NOTATION_NODE: return "Notation";
383     }
384
385     return "(unknown)";
386 }
387
388 function nodeDisplayName()
389 {
390     if (!this)
391         return "";
392
393     switch (this.nodeType) {
394         case Node.DOCUMENT_NODE:
395             return "Document";
396
397         case Node.ELEMENT_NODE:
398             var name = "<" + this.nodeName.toLowerCase();
399
400             if (this.hasAttributes()) {
401                 var value = this.getAttribute("id");
402                 if (value)
403                     name += " id=\"" + value + "\"";
404                 value = this.getAttribute("class");
405                 if (value)
406                     name += " class=\"" + value + "\"";
407                 if (this.nodeName.toLowerCase() === "a") {
408                     value = this.getAttribute("name");
409                     if (value)
410                         name += " name=\"" + value + "\"";
411                     value = this.getAttribute("href");
412                     if (value)
413                         name += " href=\"" + value + "\"";
414                 } else if (this.nodeName.toLowerCase() === "img") {
415                     value = this.getAttribute("src");
416                     if (value)
417                         name += " src=\"" + value + "\"";
418                 } else if (this.nodeName.toLowerCase() === "iframe") {
419                     value = this.getAttribute("src");
420                     if (value)
421                         name += " src=\"" + value + "\"";
422                 } else if (this.nodeName.toLowerCase() === "input") {
423                     value = this.getAttribute("name");
424                     if (value)
425                         name += " name=\"" + value + "\"";
426                     value = this.getAttribute("type");
427                     if (value)
428                         name += " type=\"" + value + "\"";
429                 } else if (this.nodeName.toLowerCase() === "form") {
430                     value = this.getAttribute("action");
431                     if (value)
432                         name += " action=\"" + value + "\"";
433                 }
434             }
435
436             return name + ">";
437
438         case Node.TEXT_NODE:
439             if (isNodeWhitespace.call(this))
440                 return "(whitespace)";
441             return "\"" + this.nodeValue + "\"";
442
443         case Node.COMMENT_NODE:
444             return "<!--" + this.nodeValue + "-->";
445             
446         case Node.DOCUMENT_TYPE_NODE:
447             var docType = "<!DOCTYPE " + this.nodeName;
448             if (this.publicId) {
449                 docType += " PUBLIC \"" + this.publicId + "\"";
450                 if (this.systemId)
451                     docType += " \"" + this.systemId + "\"";
452             } else if (this.systemId)
453                 docType += " SYSTEM \"" + this.systemId + "\"";
454             if (this.internalSubset)
455                 docType += " [" + this.internalSubset + "]";
456             return docType + ">";
457     }
458
459     return this.nodeName.toLowerCase().collapseWhitespace();
460 }
461
462 function nodeContentPreview()
463 {
464     if (!this || !this.hasChildNodes || !this.hasChildNodes())
465         return "";
466
467     var limit = 0;
468     var preview = "";
469
470     // always skip whitespace here
471     var currentNode = traverseNextNode.call(this, true, this);
472     while (currentNode) {
473         if (currentNode.nodeType === Node.TEXT_NODE)
474             preview += currentNode.nodeValue.escapeHTML();
475         else
476             preview += nodeDisplayName.call(currentNode).escapeHTML();
477
478         currentNode = traverseNextNode.call(currentNode, true, this);
479
480         if (++limit > 4) {
481             preview += "&#x2026;"; // ellipsis
482             break;
483         }
484     }
485
486     return preview.collapseWhitespace();
487 }
488
489 function isAncestorNode(ancestor)
490 {
491     if (!this || !ancestor)
492         return false;
493
494     var currentNode = ancestor.parentNode;
495     while (currentNode) {
496         if (this === currentNode)
497             return true;
498         currentNode = currentNode.parentNode;
499     }
500
501     return false;
502 }
503
504 function isDescendantNode(descendant)
505 {
506     return isAncestorNode.call(descendant, this);
507 }
508
509 function firstCommonNodeAncestor(node)
510 {
511     if (!this || !node)
512         return;
513
514     var node1 = this.parentNode;
515     var node2 = node.parentNode;
516
517     if ((!node1 || !node2) || node1 !== node2)
518         return null;
519
520     while (node1 && node2) {
521         if (!node1.parentNode || !node2.parentNode)
522             break;
523         if (node1 !== node2)
524             break;
525
526         node1 = node1.parentNode;
527         node2 = node2.parentNode;
528     }
529
530     return node1;
531 }
532
533 function nextSiblingSkippingWhitespace()
534 {
535     if (!this)
536         return;
537     var node = this.nextSibling;
538     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
539         node = node.nextSibling;
540     return node;
541 }
542
543 function previousSiblingSkippingWhitespace()
544 {
545     if (!this)
546         return;
547     var node = this.previousSibling;
548     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
549         node = node.previousSibling;
550     return node;
551 }
552
553 function firstChildSkippingWhitespace()
554 {
555     if (!this)
556         return;
557     var node = this.firstChild;
558     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
559         node = nextSiblingSkippingWhitespace.call(node);
560     return node;
561 }
562
563 function lastChildSkippingWhitespace()
564 {
565     if (!this)
566         return;
567     var node = this.lastChild;
568     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
569         node = previousSiblingSkippingWhitespace.call(node);
570     return node;
571 }
572
573 function traverseNextNode(skipWhitespace, stayWithin)
574 {
575     if (!this)
576         return;
577
578     var node = skipWhitespace ? firstChildSkippingWhitespace.call(this) : this.firstChild;
579     if (node)
580         return node;
581
582     if (stayWithin && this === stayWithin)
583         return null;
584
585     node = skipWhitespace ? nextSiblingSkippingWhitespace.call(this) : this.nextSibling;
586     if (node)
587         return node;
588
589     node = this;
590     while (node && !(skipWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling) && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
591         node = node.parentNode;
592     if (!node)
593         return null;
594
595     return skipWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
596 }
597
598 function traversePreviousNode(skipWhitespace)
599 {
600     if (!this)
601         return;
602     var node = skipWhitespace ? previousSiblingSkippingWhitespace.call(this) : this.previousSibling;
603     while (node && (skipWhitespace ? lastChildSkippingWhitespace.call(node) : node.lastChild) )
604         node = skipWhitespace ? lastChildSkippingWhitespace.call(node) : node.lastChild;
605     if (node)
606         return node;
607     return this.parentNode;
608 }
609
610 function onlyTextChild(ignoreWhitespace)
611 {
612     if (!this)
613         return null;
614
615     var firstChild = ignoreWhitespace ? firstChildSkippingWhitespace.call(this) : this.firstChild;
616     if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE)
617         return null;
618
619     var sibling = ignoreWhitespace ? nextSiblingSkippingWhitespace.call(firstChild) : firstChild.nextSibling;
620     return sibling ? null : firstChild;
621 }
622
623 function nodeTitleInfo(hasChildren, linkify)
624 {
625     var info = {title: "", hasChildren: hasChildren};
626
627     switch (this.nodeType) {
628         case Node.DOCUMENT_NODE:
629             info.title = "Document";
630             break;
631
632         case Node.ELEMENT_NODE:
633             info.title = "<span class=\"webkit-html-tag\">&lt;" + this.nodeName.toLowerCase().escapeHTML();
634
635             if (this.hasAttributes()) {
636                 for (var i = 0; i < this.attributes.length; ++i) {
637                     var attr = this.attributes[i];
638                     var value = attr.value.escapeHTML();
639                     value = value.replace(/([\/;:\)\]\}])/g, "$1&#8203;");
640
641                     info.title += " <span class=\"webkit-html-attribute\"><span class=\"webkit-html-attribute-name\">" + attr.name.escapeHTML() + "</span>=&#8203;\"";
642
643                     if (linkify && (attr.name === "src" || attr.name === "href"))
644                         info.title += linkify(attr.value, value, "webkit-html-attribute-value", this.nodeName.toLowerCase() == "a");
645                     else
646                         info.title += "<span class=\"webkit-html-attribute-value\">" + value + "</span>";
647                     info.title += "\"</span>";
648                 }
649             }
650             info.title += "&gt;</span>&#8203;";
651
652             // If this element only has a single child that is a text node,
653             // just show that text and the closing tag inline rather than
654             // create a subtree for them
655
656             var textChild = onlyTextChild.call(this, Preferences.ignoreWhitespace);
657             var showInlineText = textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength;
658
659             if (showInlineText) {
660                 info.title += "<span class=\"webkit-html-text-node\">" + textChild.nodeValue.escapeHTML() + "</span>&#8203;<span class=\"webkit-html-tag\">&lt;/" + this.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
661                 info.hasChildren = false;
662             }
663             break;
664
665         case Node.TEXT_NODE:
666             if (isNodeWhitespace.call(this))
667                 info.title = "(whitespace)";
668             else
669                 info.title = "\"<span class=\"webkit-html-text-node\">" + this.nodeValue.escapeHTML() + "</span>\"";
670             break
671
672         case Node.COMMENT_NODE:
673             info.title = "<span class=\"webkit-html-comment\">&lt;!--" + this.nodeValue.escapeHTML() + "--&gt;</span>";
674             break;
675
676         case Node.DOCUMENT_TYPE_NODE:
677             info.title = "<span class=\"webkit-html-doctype\">&lt;!DOCTYPE " + this.nodeName;
678             if (this.publicId) {
679                 info.title += " PUBLIC \"" + this.publicId + "\"";
680                 if (this.systemId)
681                     info.title += " \"" + this.systemId + "\"";
682             } else if (this.systemId)
683                 info.title += " SYSTEM \"" + this.systemId + "\"";
684             if (this.internalSubset)
685                 info.title += " [" + this.internalSubset + "]";
686             info.title += "&gt;</span>";
687             break;
688         default:
689             info.title = this.nodeName.toLowerCase().collapseWhitespace().escapeHTML();
690     }
691
692     return info;
693 }
694
695 Number.secondsToString = function(seconds, formatterFunction)
696 {
697     if (!formatterFunction)
698         formatterFunction = String.sprintf;
699
700     var ms = seconds * 1000;
701     if (ms < 1000)
702         return formatterFunction("%.0fms", ms);
703
704     if (seconds < 60)
705         return formatterFunction("%.2fs", seconds);
706
707     var minutes = seconds / 60;
708     if (minutes < 60)
709         return formatterFunction("%.1fmin", minutes);
710
711     var hours = minutes / 60;
712     if (hours < 24)
713         return formatterFunction("%.1fhrs", hours);
714
715     var days = hours / 24;
716     return formatterFunction("%.1f days", days);
717 }
718
719 Number.bytesToString = function(bytes, formatterFunction)
720 {
721     if (!formatterFunction)
722         formatterFunction = String.sprintf;
723
724     if (bytes < 1024)
725         return formatterFunction("%.0fB", bytes);
726
727     var kilobytes = bytes / 1024;
728     if (kilobytes < 1024)
729         return formatterFunction("%.2fKB", kilobytes);
730
731     var megabytes = kilobytes / 1024;
732     return formatterFunction("%.3fMB", megabytes);
733 }
734
735 Number.constrain = function(num, min, max)
736 {
737     if (num < min)
738         num = min;
739     else if (num > max)
740         num = max;
741     return num;
742 }
743
744 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
745 {
746     var length = this.value.length;
747     this.setSelectionRange(length, length);
748 }
749
750 String.sprintf = function(format)
751 {
752     return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
753 }
754
755 String.tokenizeFormatString = function(format)
756 {
757     var tokens = [];
758     var substitutionIndex = 0;
759
760     function addStringToken(str)
761     {
762         tokens.push({ type: "string", value: str });
763     }
764
765     function addSpecifierToken(specifier, precision, substitutionIndex)
766     {
767         tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
768     }
769
770     var index = 0;
771     for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
772         addStringToken(format.substring(index, precentIndex));
773         index = precentIndex + 1;
774
775         if (format[index] === "%") {
776             addStringToken("%");
777             ++index;
778             continue;
779         }
780
781         if (!isNaN(format[index])) {
782             // The first character is a number, it might be a substitution index.
783             var number = parseInt(format.substring(index));
784             while (!isNaN(format[index]))
785                 ++index;
786             // If the number is greater than zero and ends with a "$",
787             // then this is a substitution index.
788             if (number > 0 && format[index] === "$") {
789                 substitutionIndex = (number - 1);
790                 ++index;
791             }
792         }
793
794         var precision = -1;
795         if (format[index] === ".") {
796             // This is a precision specifier. If no digit follows the ".",
797             // then the precision should be zero.
798             ++index;
799             precision = parseInt(format.substring(index));
800             if (isNaN(precision))
801                 precision = 0;
802             while (!isNaN(format[index]))
803                 ++index;
804         }
805
806         addSpecifierToken(format[index], precision, substitutionIndex);
807
808         ++substitutionIndex;
809         ++index;
810     }
811
812     addStringToken(format.substring(index));
813
814     return tokens;
815 }
816
817 String.standardFormatters = {
818     d: function(substitution)
819     {
820         substitution = parseInt(substitution);
821         return !isNaN(substitution) ? substitution : 0;
822     },
823
824     f: function(substitution, token)
825     {
826         substitution = parseFloat(substitution);
827         if (substitution && token.precision > -1)
828             substitution = substitution.toFixed(token.precision);
829         return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
830     },
831
832     s: function(substitution)
833     {
834         return substitution;
835     },
836 };
837
838 String.vsprintf = function(format, substitutions)
839 {
840     return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
841 }
842
843 String.format = function(format, substitutions, formatters, initialValue, append)
844 {
845     if (!format || !substitutions || !substitutions.length)
846         return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
847
848     function prettyFunctionName()
849     {
850         return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
851     }
852
853     function warn(msg)
854     {
855         console.warn(prettyFunctionName() + ": " + msg);
856     }
857
858     function error(msg)
859     {
860         console.error(prettyFunctionName() + ": " + msg);
861     }
862
863     var result = initialValue;
864     var tokens = String.tokenizeFormatString(format);
865     var usedSubstitutionIndexes = {};
866
867     for (var i = 0; i < tokens.length; ++i) {
868         var token = tokens[i];
869
870         if (token.type === "string") {
871             result = append(result, token.value);
872             continue;
873         }
874
875         if (token.type !== "specifier") {
876             error("Unknown token type \"" + token.type + "\" found.");
877             continue;
878         }
879
880         if (token.substitutionIndex >= substitutions.length) {
881             // If there are not enough substitutions for the current substitutionIndex
882             // just output the format specifier literally and move on.
883             error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
884             result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
885             continue;
886         }
887
888         usedSubstitutionIndexes[token.substitutionIndex] = true;
889
890         if (!(token.specifier in formatters)) {
891             // Encountered an unsupported format character, treat as a string.
892             warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
893             result = append(result, substitutions[token.substitutionIndex]);
894             continue;
895         }
896
897         result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
898     }
899
900     var unusedSubstitutions = [];
901     for (var i = 0; i < substitutions.length; ++i) {
902         if (i in usedSubstitutionIndexes)
903             continue;
904         unusedSubstitutions.push(substitutions[i]);
905     }
906
907     return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
908 }