Reviewed by Adam.
[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 CSSStyleDeclaration.prototype.getShorthandValue = function(shorthandProperty)
248 {
249     var value = this.getPropertyValue(shorthandProperty);
250     if (!value) {
251         // Some shorthands (like border) return a null value, so compute a shorthand value.
252         // FIXME: remove this when http://bugs.webkit.org/show_bug.cgi?id=15823 is fixed.
253
254         var foundProperties = {};
255         for (var i = 0; i < this.length; ++i) {
256             var individualProperty = this[i];
257             if (individualProperty in foundProperties || this.getPropertyShorthand(individualProperty) !== shorthandProperty)
258                 continue;
259
260             var individualValue = this.getPropertyValue(individualProperty);
261             if (this.isPropertyImplicit(individualProperty) || individualValue === "initial")
262                 continue;
263
264             foundProperties[individualProperty] = true;
265
266             if (!value)
267                 value = "";
268             else if (value.length)
269                 value += " ";
270             value += individualValue;
271         }
272     }
273     return value;
274 }
275
276 CSSStyleDeclaration.prototype.getShorthandPriority = function(shorthandProperty)
277 {
278     var priority = this.getPropertyPriority(shorthandProperty);
279     if (!priority) {
280         for (var i = 0; i < this.length; ++i) {
281             var individualProperty = this[i];
282             if (this.getPropertyShorthand(individualProperty) !== shorthandProperty)
283                 continue;
284             priority = this.getPropertyPriority(individualProperty);
285             break;
286         }
287     }
288     return priority;
289 }
290
291 CSSStyleDeclaration.prototype.getLonghandProperties = function(shorthandProperty)
292 {
293     var properties = [];
294     var foundProperties = {};
295
296     for (var i = 0; i < this.length; ++i) {
297         var individualProperty = this[i];
298         if (individualProperty in foundProperties || this.getPropertyShorthand(individualProperty) !== shorthandProperty)
299             continue;
300         foundProperties[individualProperty] = true;
301         properties.push(individualProperty);
302     }
303
304     return properties;
305 }
306
307 CSSStyleDeclaration.prototype.getUniqueProperties = function()
308 {
309     var properties = [];
310     var foundProperties = {};
311
312     for (var i = 0; i < this.length; ++i) {
313         var property = this[i];
314         if (property in foundProperties)
315             continue;
316         foundProperties[property] = true;
317         properties.push(property);
318     }
319
320     return properties;
321 }
322
323 function isNodeWhitespace()
324 {
325     if (!this || this.nodeType !== Node.TEXT_NODE)
326         return false;
327     if (!this.nodeValue.length)
328         return true;
329     return this.nodeValue.match(/^[\s\xA0]+$/);
330 }
331
332 function nodeTypeName()
333 {
334     if (!this)
335         return "(unknown)";
336
337     switch (this.nodeType) {
338         case Node.ELEMENT_NODE: return "Element";
339         case Node.ATTRIBUTE_NODE: return "Attribute";
340         case Node.TEXT_NODE: return "Text";
341         case Node.CDATA_SECTION_NODE: return "Character Data";
342         case Node.ENTITY_REFERENCE_NODE: return "Entity Reference";
343         case Node.ENTITY_NODE: return "Entity";
344         case Node.PROCESSING_INSTRUCTION_NODE: return "Processing Instruction";
345         case Node.COMMENT_NODE: return "Comment";
346         case Node.DOCUMENT_NODE: return "Document";
347         case Node.DOCUMENT_TYPE_NODE: return "Document Type";
348         case Node.DOCUMENT_FRAGMENT_NODE: return "Document Fragment";
349         case Node.NOTATION_NODE: return "Notation";
350     }
351
352     return "(unknown)";
353 }
354
355 function nodeDisplayName()
356 {
357     if (!this)
358         return "";
359
360     switch (this.nodeType) {
361         case Node.DOCUMENT_NODE:
362             return "Document";
363
364         case Node.ELEMENT_NODE:
365             var name = "<" + this.nodeName.toLowerCase();
366
367             if (this.hasAttributes()) {
368                 var value = this.getAttribute("id");
369                 if (value)
370                     name += " id=\"" + value + "\"";
371                 value = this.getAttribute("class");
372                 if (value)
373                     name += " class=\"" + value + "\"";
374                 if (this.nodeName.toLowerCase() === "a") {
375                     value = this.getAttribute("name");
376                     if (value)
377                         name += " name=\"" + value + "\"";
378                     value = this.getAttribute("href");
379                     if (value)
380                         name += " href=\"" + value + "\"";
381                 } else if (this.nodeName.toLowerCase() === "img") {
382                     value = this.getAttribute("src");
383                     if (value)
384                         name += " src=\"" + value + "\"";
385                 } else if (this.nodeName.toLowerCase() === "iframe") {
386                     value = this.getAttribute("src");
387                     if (value)
388                         name += " src=\"" + value + "\"";
389                 } else if (this.nodeName.toLowerCase() === "input") {
390                     value = this.getAttribute("name");
391                     if (value)
392                         name += " name=\"" + value + "\"";
393                     value = this.getAttribute("type");
394                     if (value)
395                         name += " type=\"" + value + "\"";
396                 } else if (this.nodeName.toLowerCase() === "form") {
397                     value = this.getAttribute("action");
398                     if (value)
399                         name += " action=\"" + value + "\"";
400                 }
401             }
402
403             return name + ">";
404
405         case Node.TEXT_NODE:
406             if (isNodeWhitespace.call(this))
407                 return "(whitespace)";
408             return "\"" + this.nodeValue + "\"";
409
410         case Node.COMMENT_NODE:
411             return "<!--" + this.nodeValue + "-->";
412     }
413
414     return this.nodeName.toLowerCase().collapseWhitespace();
415 }
416
417 function nodeContentPreview()
418 {
419     if (!this || !this.hasChildNodes || !this.hasChildNodes())
420         return "";
421
422     var limit = 0;
423     var preview = "";
424
425     // always skip whitespace here
426     var currentNode = traverseNextNode.call(this, true, this);
427     while (currentNode) {
428         if (currentNode.nodeType === Node.TEXT_NODE)
429             preview += currentNode.nodeValue.escapeHTML();
430         else
431             preview += nodeDisplayName.call(currentNode).escapeHTML();
432
433         currentNode = traverseNextNode.call(currentNode, true, this);
434
435         if (++limit > 4) {
436             preview += "&#x2026;"; // ellipsis
437             break;
438         }
439     }
440
441     return preview.collapseWhitespace();
442 }
443
444 function isAncestorNode(ancestor)
445 {
446     if (!this || !ancestor)
447         return false;
448
449     var currentNode = ancestor.parentNode;
450     while (currentNode) {
451         if (this === currentNode)
452             return true;
453         currentNode = currentNode.parentNode;
454     }
455
456     return false;
457 }
458
459 function isDescendantNode(descendant)
460 {
461     return isAncestorNode.call(descendant, this);
462 }
463
464 function firstCommonNodeAncestor(node)
465 {
466     if (!this || !node)
467         return;
468
469     var node1 = this.parentNode;
470     var node2 = node.parentNode;
471
472     if ((!node1 || !node2) || node1 !== node2)
473         return null;
474
475     while (node1 && node2) {
476         if (!node1.parentNode || !node2.parentNode)
477             break;
478         if (node1 !== node2)
479             break;
480
481         node1 = node1.parentNode;
482         node2 = node2.parentNode;
483     }
484
485     return node1;
486 }
487
488 function nextSiblingSkippingWhitespace()
489 {
490     if (!this)
491         return;
492     var node = this.nextSibling;
493     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
494         node = node.nextSibling;
495     return node;
496 }
497
498 function previousSiblingSkippingWhitespace()
499 {
500     if (!this)
501         return;
502     var node = this.previousSibling;
503     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
504         node = node.previousSibling;
505     return node;
506 }
507
508 function firstChildSkippingWhitespace()
509 {
510     if (!this)
511         return;
512     var node = this.firstChild;
513     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
514         node = nextSiblingSkippingWhitespace.call(node);
515     return node;
516 }
517
518 function lastChildSkippingWhitespace()
519 {
520     if (!this)
521         return;
522     var node = this.lastChild;
523     while (node && node.nodeType === Node.TEXT_NODE && isNodeWhitespace.call(node))
524         node = previousSiblingSkippingWhitespace.call(node);
525     return node;
526 }
527
528 function traverseNextNode(skipWhitespace, stayWithin)
529 {
530     if (!this)
531         return;
532
533     var node = skipWhitespace ? firstChildSkippingWhitespace.call(this) : this.firstChild;
534     if (node)
535         return node;
536
537     if (stayWithin && this === stayWithin)
538         return null;
539
540     node = skipWhitespace ? nextSiblingSkippingWhitespace.call(this) : this.nextSibling;
541     if (node)
542         return node;
543
544     node = this;
545     while (node && !(skipWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling) && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
546         node = node.parentNode;
547     if (!node)
548         return null;
549
550     return skipWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
551 }
552
553 function traversePreviousNode(skipWhitespace)
554 {
555     if (!this)
556         return;
557     var node = skipWhitespace ? previousSiblingSkippingWhitespace.call(this) : this.previousSibling;
558     while (node && (skipWhitespace ? lastChildSkippingWhitespace.call(node) : node.lastChild) )
559         node = skipWhitespace ? lastChildSkippingWhitespace.call(node) : node.lastChild;
560     if (node)
561         return node;
562     return this.parentNode;
563 }
564
565 function onlyTextChild(ignoreWhitespace)
566 {
567     if (!this)
568         return null;
569
570     var firstChild = ignoreWhitespace ? firstChildSkippingWhitespace.call(this) : this.firstChild;
571     if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE)
572         return null;
573
574     var sibling = ignoreWhitespace ? nextSiblingSkippingWhitespace.call(firstChild) : firstChild.nextSibling;
575     return sibling ? null : firstChild;
576 }
577
578 function nodeTitleInfo(hasChildren, linkify)
579 {
580     var info = {title: "", hasChildren: hasChildren};
581
582     switch (this.nodeType) {
583         case Node.DOCUMENT_NODE:
584             info.title = "Document";
585             break;
586
587         case Node.ELEMENT_NODE:
588             info.title = "<span class=\"webkit-html-tag\">&lt;" + this.nodeName.toLowerCase().escapeHTML();
589
590             if (this.hasAttributes()) {
591                 for (var i = 0; i < this.attributes.length; ++i) {
592                     var attr = this.attributes[i];
593                     var value = attr.value.escapeHTML();
594                     value = value.replace(/([\/;:\)\]\}])/g, "$1&#8203;");
595
596                     info.title += " <span class=\"webkit-html-attribute-name\">" + attr.name.escapeHTML() + "</span>=&#8203;";
597
598                     if (linkify && (attr.name === "src" || attr.name === "href"))
599                         info.title += linkify(attr.value, value, "webkit-html-attribute-value", this.nodeName.toLowerCase() == "a");
600                     else
601                         info.title += "<span class=\"webkit-html-attribute-value\">\"" + value + "\"</span>";
602                 }
603             }
604             info.title += "&gt;</span>&#8203;";
605
606             // If this element only has a single child that is a text node,
607             // just show that text and the closing tag inline rather than
608             // create a subtree for them
609
610             var textChild = onlyTextChild.call(this, Preferences.ignoreWhitespace);
611             var showInlineText = textChild && textChild.textContent.length < Preferences.maxInlineTextChildLength;
612
613             if (showInlineText) {
614                 info.title += textChild.nodeValue.escapeHTML() + "&#8203;<span class=\"webkit-html-tag\">&lt;/" + this.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
615                 info.hasChildren = false;
616             }
617             break;
618
619         case Node.TEXT_NODE:
620             if (isNodeWhitespace.call(this))
621                 info.title = "(whitespace)";
622             else
623                 info.title = "\"" + this.nodeValue.escapeHTML() + "\"";
624             break
625
626         case Node.COMMENT_NODE:
627             info.title = "<span class=\"webkit-html-comment\">&lt;!--" + this.nodeValue.escapeHTML() + "--&gt;</span>";
628             break;
629
630         default:
631             info.title = this.nodeName.toLowerCase().collapseWhitespace().escapeHTML();
632     }
633
634     return info;
635 }
636
637 Number.secondsToString = function(seconds)
638 {
639     var ms = seconds * 1000;
640     if (ms < 1000)
641         return Math.round(ms) + "ms";
642
643     if (seconds < 60)
644         return (Math.round(seconds * 100) / 100) + "s";
645
646     var minutes = seconds / 60;
647     if (minutes < 60)
648         return (Math.round(minutes * 10) / 10) + "min";
649
650     var hours = minutes / 60;
651     if (hours < 24)
652         return (Math.round(hours * 10) / 10) + "hrs";
653
654     var days = hours / 24;
655     return (Math.round(days * 10) / 10) + " days";
656 }
657
658 Number.bytesToString = function(bytes)
659 {
660     if (bytes < 1024)
661         return bytes + "B";
662
663     var kilobytes = bytes / 1024;
664     if (kilobytes < 1024)
665         return (Math.round(kilobytes * 100) / 100) + "KB";
666
667     var megabytes = kilobytes / 1024;
668     return (Math.round(megabytes * 1000) / 1000) + "MB";
669 }
670
671 Number.constrain = function(num, min, max)
672 {
673     if (num < min)
674         num = min;
675     else if (num > max)
676         num = max;
677     return num;
678 }
679
680 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
681 {
682     var length = this.value.length;
683     this.setSelectionRange(length, length);
684 }
685
686 String.sprintf = function(format)
687 {
688     return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
689 }
690
691 String.vsprintf = function(format, substitutions)
692 {
693     if (!format || !substitutions || !substitutions.length)
694         return format;
695
696     var result = "";
697     var substitutionIndex = 0;
698
699     var index = 0;
700     for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
701         result += format.substring(index, precentIndex);
702         index = precentIndex + 1;
703
704         if (format[index] === "%") {
705             result += "%";
706             ++index;
707             continue;
708         }
709
710         if (!isNaN(format[index])) {
711             // The first character is a number, it might be a substitution index.
712             var number = parseInt(format.substring(index));
713             while (!isNaN(format[index]))
714                 ++index;
715             // If the number is greater than zero and ends with a "$",
716             // then this is a substitution index.
717             if (number > 0 && format[index] === "$") {
718                 substitutionIndex = (number - 1);
719                 ++index;
720             }
721         }
722
723         var precision = -1;
724         if (format[index] === ".") {
725             // This is a precision specifier. If no digit follows the ".",
726             // then the precision should be zero.
727             ++index;
728             precision = parseInt(format.substring(index));
729             if (isNaN(precision))
730                 precision = 0;
731             while (!isNaN(format[index]))
732                 ++index;
733         }
734
735         if (substitutionIndex >= substitutions.length) {
736             // If there are not enough substitutions for the current substitutionIndex
737             // just output the format specifier literally and move on.
738             console.error("String.vsprintf(\"" + format + "\", \"" + substitutions.join("\", \"") + "\"): not enough substitution arguments. Had " + substitutions.length + " but needed " + (substitutionIndex + 1) + ", so substitution was skipped.");
739             index = precentIndex + 1;
740             result += "%";
741             continue;
742         }
743
744         switch (format[index]) {
745         case "d":
746             var substitution = parseInt(substitutions[substitutionIndex]);
747             result += (!isNaN(substitution) ? substitution : 0);
748             break;
749         case "f":
750             var substitution = parseFloat(substitutions[substitutionIndex]);
751             if (substitution && precision > -1)
752                 substitution = substitution.toFixed(precision);
753             result += (!isNaN(substitution) ? substitution : (precision > -1 ? Number(0).toFixed(precision) : 0));
754             break;
755         default:
756             // Encountered an unsupported format character, treat as a string.
757             console.warn("String.vsprintf(\"" + format + "\", \"" + substitutions.join("\", \"") + "\"): unsupported format character \u201C" + format[index] + "\u201D. Treating as a string.");
758             // Fall through to treat this like a string.
759         case "@":
760         case "s":
761             result += substitutions[substitutionIndex];
762             break;
763         }
764
765         ++substitutionIndex;
766         ++index;
767     }
768
769     result += format.substring(index);
770
771     return result;
772 }