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
29Object.proxyType = function(objectProxy)
30{
31    if (objectProxy === null)
32        return "null";
33
34    var type = typeof objectProxy;
35    if (type !== "object" && type !== "function")
36        return type;
37
38    return objectProxy.type;
39}
40
41Object.properties = function(obj)
42{
43    var properties = [];
44    for (var prop in obj)
45        properties.push(prop);
46    return properties;
47}
48
49Object.sortedProperties = function(obj, sortFunc)
50{
51    return Object.properties(obj).sort(sortFunc);
52}
53
54Function.prototype.bind = function(thisObject)
55{
56    var func = this;
57    var args = Array.prototype.slice.call(arguments, 1);
58    return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))) };
59}
60
61Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction)
62{
63    var startNode;
64    var startOffset = 0;
65    var endNode;
66    var endOffset = 0;
67
68    if (!stayWithinNode)
69        stayWithinNode = this;
70
71    if (!direction || direction === "backward" || direction === "both") {
72        var node = this;
73        while (node) {
74            if (node === stayWithinNode) {
75                if (!startNode)
76                    startNode = stayWithinNode;
77                break;
78            }
79
80            if (node.nodeType === Node.TEXT_NODE) {
81                var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1));
82                for (var i = start; i >= 0; --i) {
83                    if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
84                        startNode = node;
85                        startOffset = i + 1;
86                        break;
87                    }
88                }
89            }
90
91            if (startNode)
92                break;
93
94            node = node.traversePreviousNode(stayWithinNode);
95        }
96
97        if (!startNode) {
98            startNode = stayWithinNode;
99            startOffset = 0;
100        }
101    } else {
102        startNode = this;
103        startOffset = offset;
104    }
105
106    if (!direction || direction === "forward" || direction === "both") {
107        node = this;
108        while (node) {
109            if (node === stayWithinNode) {
110                if (!endNode)
111                    endNode = stayWithinNode;
112                break;
113            }
114
115            if (node.nodeType === Node.TEXT_NODE) {
116                var start = (node === this ? offset : 0);
117                for (var i = start; i < node.nodeValue.length; ++i) {
118                    if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
119                        endNode = node;
120                        endOffset = i;
121                        break;
122                    }
123                }
124            }
125
126            if (endNode)
127                break;
128
129            node = node.traverseNextNode(stayWithinNode);
130        }
131
132        if (!endNode) {
133            endNode = stayWithinNode;
134            endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
135        }
136    } else {
137        endNode = this;
138        endOffset = offset;
139    }
140
141    var result = this.ownerDocument.createRange();
142    result.setStart(startNode, startOffset);
143    result.setEnd(endNode, endOffset);
144
145    return result;
146}
147
148Node.prototype.traverseNextTextNode = function(stayWithin)
149{
150    var node = this.traverseNextNode(stayWithin);
151    if (!node)
152        return;
153
154    while (node && node.nodeType !== Node.TEXT_NODE)
155        node = node.traverseNextNode(stayWithin);
156
157    return node;
158}
159
160Node.prototype.rangeBoundaryForOffset = function(offset)
161{
162    var node = this.traverseNextTextNode(this);
163    while (node && offset > node.nodeValue.length) {
164        offset -= node.nodeValue.length;
165        node = node.traverseNextTextNode(this);
166    }
167    if (!node)
168        return { container: this, offset: 0 };
169    return { container: node, offset: offset };
170}
171
172Element.prototype.removeStyleClass = function(className)
173{
174    // Test for the simple case first.
175    if (this.className === className) {
176        this.className = "";
177        return;
178    }
179
180    var index = this.className.indexOf(className);
181    if (index === -1)
182        return;
183
184    var newClassName = " " + this.className + " ";
185    this.className = newClassName.replace(" " + className + " ", " ");
186}
187
188Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
189{
190    var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
191    if (regex.test(this.className))
192        this.className = this.className.replace(regex, " ");
193}
194
195Element.prototype.addStyleClass = function(className)
196{
197    if (className && !this.hasStyleClass(className))
198        this.className += (this.className.length ? " " + className : className);
199}
200
201Element.prototype.hasStyleClass = function(className)
202{
203    if (!className)
204        return false;
205    // Test for the simple case
206    if (this.className === className)
207        return true;
208
209    var index = this.className.indexOf(className);
210    if (index === -1)
211        return false;
212    var toTest = " " + this.className + " ";
213    return toTest.indexOf(" " + className + " ", index) !== -1;
214}
215
216Element.prototype.positionAt = function(x, y)
217{
218    this.style.left = x + "px";
219    this.style.top = y + "px";
220}
221
222Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
223{
224    for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
225        for (var i = 0; i < nameArray.length; ++i)
226            if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
227                return node;
228    return null;
229}
230
231Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
232{
233    return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
234}
235
236Node.prototype.enclosingNodeOrSelfWithClass = function(className)
237{
238    for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
239        if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
240            return node;
241    return null;
242}
243
244Node.prototype.enclosingNodeWithClass = function(className)
245{
246    if (!this.parentNode)
247        return null;
248    return this.parentNode.enclosingNodeOrSelfWithClass(className);
249}
250
251Element.prototype.query = function(query)
252{
253    return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
254}
255
256Element.prototype.removeChildren = function()
257{
258    this.innerHTML = "";
259}
260
261Element.prototype.isInsertionCaretInside = function()
262{
263    var selection = window.getSelection();
264    if (!selection.rangeCount || !selection.isCollapsed)
265        return false;
266    var selectionRange = selection.getRangeAt(0);
267    return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
268}
269
270Element.prototype.__defineGetter__("totalOffsetLeft", function()
271{
272    var total = 0;
273    for (var element = this; element; element = element.offsetParent)
274        total += element.offsetLeft;
275    return total;
276});
277
278Element.prototype.__defineGetter__("totalOffsetTop", function()
279{
280    var total = 0;
281    for (var element = this; element; element = element.offsetParent)
282        total += element.offsetTop;
283    return total;
284});
285
286Element.prototype.offsetRelativeToWindow = function(targetWindow)
287{
288    var elementOffset = {x: 0, y: 0};
289    var curElement = this;
290    var curWindow = this.ownerDocument.defaultView;
291    while (curWindow && curElement) {
292        elementOffset.x += curElement.totalOffsetLeft;
293        elementOffset.y += curElement.totalOffsetTop;
294        if (curWindow === targetWindow)
295            break;
296
297        curElement = curWindow.frameElement;
298        curWindow = curWindow.parent;
299    }
300
301    return elementOffset;
302}
303
304Node.prototype.isWhitespace = isNodeWhitespace;
305Node.prototype.displayName = nodeDisplayName;
306Node.prototype.isAncestor = function(node)
307{
308    return isAncestorNode(this, node);
309};
310Node.prototype.isDescendant = isDescendantNode;
311Node.prototype.traverseNextNode = traverseNextNode;
312Node.prototype.traversePreviousNode = traversePreviousNode;
313Node.prototype.onlyTextChild = onlyTextChild;
314
315String.prototype.hasSubstring = function(string, caseInsensitive)
316{
317    if (!caseInsensitive)
318        return this.indexOf(string) !== -1;
319    return this.match(new RegExp(string.escapeForRegExp(), "i"));
320}
321
322String.prototype.escapeCharacters = function(chars)
323{
324    var foundChar = false;
325    for (var i = 0; i < chars.length; ++i) {
326        if (this.indexOf(chars.charAt(i)) !== -1) {
327            foundChar = true;
328            break;
329        }
330    }
331
332    if (!foundChar)
333        return this;
334
335    var result = "";
336    for (var i = 0; i < this.length; ++i) {
337        if (chars.indexOf(this.charAt(i)) !== -1)
338            result += "\\";
339        result += this.charAt(i);
340    }
341
342    return result;
343}
344
345String.prototype.escapeForRegExp = function()
346{
347    return this.escapeCharacters("^[]{}()\\.$*+?|");
348}
349
350String.prototype.escapeHTML = function()
351{
352    return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
353}
354
355String.prototype.collapseWhitespace = function()
356{
357    return this.replace(/[\s\xA0]+/g, " ");
358}
359
360String.prototype.trimURL = function(baseURLDomain)
361{
362    var result = this.replace(/^https?:\/\//i, "");
363    if (baseURLDomain)
364        result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
365    return result;
366}
367
368function isNodeWhitespace()
369{
370    if (!this || this.nodeType !== Node.TEXT_NODE)
371        return false;
372    if (!this.nodeValue.length)
373        return true;
374    return this.nodeValue.match(/^[\s\xA0]+$/);
375}
376
377function nodeDisplayName()
378{
379    if (!this)
380        return "";
381
382    switch (this.nodeType) {
383        case Node.DOCUMENT_NODE:
384            return "Document";
385
386        case Node.ELEMENT_NODE:
387            var name = "<" + this.nodeName.toLowerCase();
388
389            if (this.hasAttributes()) {
390                var value = this.getAttribute("id");
391                if (value)
392                    name += " id=\"" + value + "\"";
393                value = this.getAttribute("class");
394                if (value)
395                    name += " class=\"" + value + "\"";
396                if (this.nodeName.toLowerCase() === "a") {
397                    value = this.getAttribute("name");
398                    if (value)
399                        name += " name=\"" + value + "\"";
400                    value = this.getAttribute("href");
401                    if (value)
402                        name += " href=\"" + value + "\"";
403                } else if (this.nodeName.toLowerCase() === "img") {
404                    value = this.getAttribute("src");
405                    if (value)
406                        name += " src=\"" + value + "\"";
407                } else if (this.nodeName.toLowerCase() === "iframe") {
408                    value = this.getAttribute("src");
409                    if (value)
410                        name += " src=\"" + value + "\"";
411                } else if (this.nodeName.toLowerCase() === "input") {
412                    value = this.getAttribute("name");
413                    if (value)
414                        name += " name=\"" + value + "\"";
415                    value = this.getAttribute("type");
416                    if (value)
417                        name += " type=\"" + value + "\"";
418                } else if (this.nodeName.toLowerCase() === "form") {
419                    value = this.getAttribute("action");
420                    if (value)
421                        name += " action=\"" + value + "\"";
422                }
423            }
424
425            return name + ">";
426
427        case Node.TEXT_NODE:
428            if (isNodeWhitespace.call(this))
429                return "(whitespace)";
430            return "\"" + this.nodeValue + "\"";
431
432        case Node.COMMENT_NODE:
433            return "<!--" + this.nodeValue + "-->";
434
435        case Node.DOCUMENT_TYPE_NODE:
436            var docType = "<!DOCTYPE " + this.nodeName;
437            if (this.publicId) {
438                docType += " PUBLIC \"" + this.publicId + "\"";
439                if (this.systemId)
440                    docType += " \"" + this.systemId + "\"";
441            } else if (this.systemId)
442                docType += " SYSTEM \"" + this.systemId + "\"";
443            if (this.internalSubset)
444                docType += " [" + this.internalSubset + "]";
445            return docType + ">";
446    }
447
448    return this.nodeName.toLowerCase().collapseWhitespace();
449}
450
451function isAncestorNode(ancestor, node)
452{
453    if (!node || !ancestor)
454        return false;
455
456    var currentNode = node.parentNode;
457    while (currentNode) {
458        if (ancestor === currentNode)
459            return true;
460        currentNode = currentNode.parentNode;
461    }
462    return false;
463}
464
465function isDescendantNode(descendant)
466{
467    return isAncestorNode(descendant, this);
468}
469
470function traverseNextNode(stayWithin)
471{
472    if (!this)
473        return;
474
475    var node = this.firstChild;
476    if (node)
477        return node;
478
479    if (stayWithin && this === stayWithin)
480        return null;
481
482    node = this.nextSibling;
483    if (node)
484        return node;
485
486    node = this;
487    while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
488        node = node.parentNode;
489    if (!node)
490        return null;
491
492    return node.nextSibling;
493}
494
495function traversePreviousNode(stayWithin)
496{
497    if (!this)
498        return;
499    if (stayWithin && this === stayWithin)
500        return null;
501    var node = this.previousSibling;
502    while (node && node.lastChild)
503        node = node.lastChild;
504    if (node)
505        return node;
506    return this.parentNode;
507}
508
509function onlyTextChild()
510{
511    if (!this)
512        return null;
513
514    var firstChild = this.firstChild;
515    if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE)
516        return null;
517
518    var sibling = firstChild.nextSibling;
519    return sibling ? null : firstChild;
520}
521
522function appropriateSelectorForNode(node, justSelector)
523{
524    if (!node)
525        return "";
526
527    var lowerCaseName = node.localName || node.nodeName.toLowerCase();
528
529    var id = node.getAttribute("id");
530    if (id) {
531        var selector = "#" + id;
532        return (justSelector ? selector : lowerCaseName + selector);
533    }
534
535    var className = node.getAttribute("class");
536    if (className) {
537        var selector = "." + className.replace(/\s+/, ".");
538        return (justSelector ? selector : lowerCaseName + selector);
539    }
540
541    if (lowerCaseName === "input" && node.getAttribute("type"))
542        return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]";
543
544    return lowerCaseName;
545}
546
547function getDocumentForNode(node)
548{
549    return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
550}
551
552function parentNode(node)
553{
554    return node.parentNode;
555}
556
557Number.secondsToString = function(seconds, formatterFunction, higherResolution)
558{
559    if (!formatterFunction)
560        formatterFunction = String.sprintf;
561
562    if (seconds === 0)
563        return "0";
564
565    var ms = seconds * 1000;
566    if (higherResolution && ms < 1000)
567        return formatterFunction("%.3fms", ms);
568    else if (ms < 1000)
569        return formatterFunction("%.0fms", ms);
570
571    if (seconds < 60)
572        return formatterFunction("%.2fs", seconds);
573
574    var minutes = seconds / 60;
575    if (minutes < 60)
576        return formatterFunction("%.1fmin", minutes);
577
578    var hours = minutes / 60;
579    if (hours < 24)
580        return formatterFunction("%.1fhrs", hours);
581
582    var days = hours / 24;
583    return formatterFunction("%.1f days", days);
584}
585
586Number.bytesToString = function(bytes, formatterFunction, higherResolution)
587{
588    if (!formatterFunction)
589        formatterFunction = String.sprintf;
590    if (typeof higherResolution === "undefined")
591        higherResolution = true;
592
593    if (bytes < 1024)
594        return formatterFunction("%.0fB", bytes);
595
596    var kilobytes = bytes / 1024;
597    if (higherResolution && kilobytes < 1024)
598        return formatterFunction("%.2fKB", kilobytes);
599    else if (kilobytes < 1024)
600        return formatterFunction("%.0fKB", kilobytes);
601
602    var megabytes = kilobytes / 1024;
603    if (higherResolution)
604        return formatterFunction("%.3fMB", megabytes);
605    else
606        return formatterFunction("%.0fMB", megabytes);
607}
608
609Number.constrain = function(num, min, max)
610{
611    if (num < min)
612        num = min;
613    else if (num > max)
614        num = max;
615    return num;
616}
617
618HTMLTextAreaElement.prototype.moveCursorToEnd = function()
619{
620    var length = this.value.length;
621    this.setSelectionRange(length, length);
622}
623
624Array.prototype.remove = function(value, onlyFirst)
625{
626    if (onlyFirst) {
627        var index = this.indexOf(value);
628        if (index !== -1)
629            this.splice(index, 1);
630        return;
631    }
632
633    var length = this.length;
634    for (var i = 0; i < length; ++i) {
635        if (this[i] === value)
636            this.splice(i, 1);
637    }
638}
639
640Array.prototype.keySet = function()
641{
642    var keys = {};
643    for (var i = 0; i < this.length; ++i)
644        keys[this[i]] = true;
645    return keys;
646}
647
648function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunction)
649{
650    // indexOf returns (-lowerBound - 1). Taking (-result - 1) works out to lowerBound.
651    return (-indexOfObjectInListSortedByFunction(anObject, aList, aFunction) - 1);
652}
653
654function indexOfObjectInListSortedByFunction(anObject, aList, aFunction)
655{
656    var first = 0;
657    var last = aList.length - 1;
658    var floor = Math.floor;
659    var mid, c;
660
661    while (first <= last) {
662        mid = floor((first + last) / 2);
663        c = aFunction(anObject, aList[mid]);
664
665        if (c > 0)
666            first = mid + 1;
667        else if (c < 0)
668            last = mid - 1;
669        else {
670            // Return the first occurance of an item in the list.
671            while (mid > 0 && aFunction(anObject, aList[mid - 1]) === 0)
672                mid--;
673            first = mid;
674            break;
675        }
676    }
677
678    // By returning 1 less than the negative lower search bound, we can reuse this function
679    // for both indexOf and insertionIndexFor, with some simple arithmetic.
680    return (-first - 1);
681}
682
683String.sprintf = function(format)
684{
685    return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
686}
687
688String.tokenizeFormatString = function(format)
689{
690    var tokens = [];
691    var substitutionIndex = 0;
692
693    function addStringToken(str)
694    {
695        tokens.push({ type: "string", value: str });
696    }
697
698    function addSpecifierToken(specifier, precision, substitutionIndex)
699    {
700        tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
701    }
702
703    var index = 0;
704    for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
705        addStringToken(format.substring(index, precentIndex));
706        index = precentIndex + 1;
707
708        if (format[index] === "%") {
709            addStringToken("%");
710            ++index;
711            continue;
712        }
713
714        if (!isNaN(format[index])) {
715            // The first character is a number, it might be a substitution index.
716            var number = parseInt(format.substring(index));
717            while (!isNaN(format[index]))
718                ++index;
719            // If the number is greater than zero and ends with a "$",
720            // then this is a substitution index.
721            if (number > 0 && format[index] === "$") {
722                substitutionIndex = (number - 1);
723                ++index;
724            }
725        }
726
727        var precision = -1;
728        if (format[index] === ".") {
729            // This is a precision specifier. If no digit follows the ".",
730            // then the precision should be zero.
731            ++index;
732            precision = parseInt(format.substring(index));
733            if (isNaN(precision))
734                precision = 0;
735            while (!isNaN(format[index]))
736                ++index;
737        }
738
739        addSpecifierToken(format[index], precision, substitutionIndex);
740
741        ++substitutionIndex;
742        ++index;
743    }
744
745    addStringToken(format.substring(index));
746
747    return tokens;
748}
749
750String.standardFormatters = {
751    d: function(substitution)
752    {
753        if (typeof substitution == "object" && Object.proxyType(substitution) === "number")
754            substitution = substitution.description;
755        substitution = parseInt(substitution);
756        return !isNaN(substitution) ? substitution : 0;
757    },
758
759    f: function(substitution, token)
760    {
761        if (typeof substitution == "object" && Object.proxyType(substitution) === "number")
762            substitution = substitution.description;
763        substitution = parseFloat(substitution);
764        if (substitution && token.precision > -1)
765            substitution = substitution.toFixed(token.precision);
766        return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
767    },
768
769    s: function(substitution)
770    {
771        if (typeof substitution == "object" && Object.proxyType(substitution) !== "null")
772            substitution = substitution.description;
773        return substitution;
774    },
775};
776
777String.vsprintf = function(format, substitutions)
778{
779    return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
780}
781
782String.format = function(format, substitutions, formatters, initialValue, append)
783{
784    if (!format || !substitutions || !substitutions.length)
785        return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
786
787    function prettyFunctionName()
788    {
789        return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
790    }
791
792    function warn(msg)
793    {
794        console.warn(prettyFunctionName() + ": " + msg);
795    }
796
797    function error(msg)
798    {
799        console.error(prettyFunctionName() + ": " + msg);
800    }
801
802    var result = initialValue;
803    var tokens = String.tokenizeFormatString(format);
804    var usedSubstitutionIndexes = {};
805
806    for (var i = 0; i < tokens.length; ++i) {
807        var token = tokens[i];
808
809        if (token.type === "string") {
810            result = append(result, token.value);
811            continue;
812        }
813
814        if (token.type !== "specifier") {
815            error("Unknown token type \"" + token.type + "\" found.");
816            continue;
817        }
818
819        if (token.substitutionIndex >= substitutions.length) {
820            // If there are not enough substitutions for the current substitutionIndex
821            // just output the format specifier literally and move on.
822            error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
823            result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
824            continue;
825        }
826
827        usedSubstitutionIndexes[token.substitutionIndex] = true;
828
829        if (!(token.specifier in formatters)) {
830            // Encountered an unsupported format character, treat as a string.
831            warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
832            result = append(result, substitutions[token.substitutionIndex]);
833            continue;
834        }
835
836        result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
837    }
838
839    var unusedSubstitutions = [];
840    for (var i = 0; i < substitutions.length; ++i) {
841        if (i in usedSubstitutionIndexes)
842            continue;
843        unusedSubstitutions.push(substitutions[i]);
844    }
845
846    return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
847}
848
849function isEnterKey(event) {
850    // Check if in IME.
851    return event.keyCode !== 229 && event.keyIdentifier === "Enter";
852}
853