• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 * Contains diff method based on Javascript Diff Algorithm By John Resig
29 * http://ejohn.org/files/jsdiff.js (released under the MIT license).
30 */
31
32Function.prototype.bind = function(thisObject)
33{
34    var func = this;
35    var args = Array.prototype.slice.call(arguments, 1);
36    function bound()
37    {
38        return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0)));
39    }
40    bound.toString = function() {
41        return "bound: " + func;
42    };
43    return bound;
44}
45
46Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction)
47{
48    var startNode;
49    var startOffset = 0;
50    var endNode;
51    var endOffset = 0;
52
53    if (!stayWithinNode)
54        stayWithinNode = this;
55
56    if (!direction || direction === "backward" || direction === "both") {
57        var node = this;
58        while (node) {
59            if (node === stayWithinNode) {
60                if (!startNode)
61                    startNode = stayWithinNode;
62                break;
63            }
64
65            if (node.nodeType === Node.TEXT_NODE) {
66                var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1));
67                for (var i = start; i >= 0; --i) {
68                    if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
69                        startNode = node;
70                        startOffset = i + 1;
71                        break;
72                    }
73                }
74            }
75
76            if (startNode)
77                break;
78
79            node = node.traversePreviousNode(stayWithinNode);
80        }
81
82        if (!startNode) {
83            startNode = stayWithinNode;
84            startOffset = 0;
85        }
86    } else {
87        startNode = this;
88        startOffset = offset;
89    }
90
91    if (!direction || direction === "forward" || direction === "both") {
92        node = this;
93        while (node) {
94            if (node === stayWithinNode) {
95                if (!endNode)
96                    endNode = stayWithinNode;
97                break;
98            }
99
100            if (node.nodeType === Node.TEXT_NODE) {
101                var start = (node === this ? offset : 0);
102                for (var i = start; i < node.nodeValue.length; ++i) {
103                    if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
104                        endNode = node;
105                        endOffset = i;
106                        break;
107                    }
108                }
109            }
110
111            if (endNode)
112                break;
113
114            node = node.traverseNextNode(stayWithinNode);
115        }
116
117        if (!endNode) {
118            endNode = stayWithinNode;
119            endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
120        }
121    } else {
122        endNode = this;
123        endOffset = offset;
124    }
125
126    var result = this.ownerDocument.createRange();
127    result.setStart(startNode, startOffset);
128    result.setEnd(endNode, endOffset);
129
130    return result;
131}
132
133Node.prototype.traverseNextTextNode = function(stayWithin)
134{
135    var node = this.traverseNextNode(stayWithin);
136    if (!node)
137        return;
138
139    while (node && node.nodeType !== Node.TEXT_NODE)
140        node = node.traverseNextNode(stayWithin);
141
142    return node;
143}
144
145Node.prototype.rangeBoundaryForOffset = function(offset)
146{
147    var node = this.traverseNextTextNode(this);
148    while (node && offset > node.nodeValue.length) {
149        offset -= node.nodeValue.length;
150        node = node.traverseNextTextNode(this);
151    }
152    if (!node)
153        return { container: this, offset: 0 };
154    return { container: node, offset: offset };
155}
156
157Element.prototype.removeStyleClass = function(className)
158{
159    this.classList.remove(className);
160}
161
162Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
163{
164    var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
165    if (regex.test(this.className))
166        this.className = this.className.replace(regex, " ");
167}
168
169Element.prototype.addStyleClass = function(className)
170{
171    this.classList.add(className);
172}
173
174Element.prototype.hasStyleClass = function(className)
175{
176    return this.classList.contains(className);
177}
178
179Element.prototype.positionAt = function(x, y)
180{
181    this.style.left = x + "px";
182    this.style.top = y + "px";
183}
184
185Element.prototype.pruneEmptyTextNodes = function()
186{
187    var sibling = this.firstChild;
188    while (sibling) {
189        var nextSibling = sibling.nextSibling;
190        if (sibling.nodeType === this.TEXT_NODE && sibling.nodeValue === "")
191            this.removeChild(sibling);
192        sibling = nextSibling;
193    }
194}
195
196Element.prototype.isScrolledToBottom = function()
197{
198    // This code works only for 0-width border
199    return this.scrollTop + this.clientHeight === this.scrollHeight;
200}
201
202Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
203{
204    for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
205        for (var i = 0; i < nameArray.length; ++i)
206            if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
207                return node;
208    return null;
209}
210
211Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
212{
213    return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
214}
215
216Node.prototype.enclosingNodeOrSelfWithClass = function(className)
217{
218    for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
219        if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
220            return node;
221    return null;
222}
223
224Node.prototype.enclosingNodeWithClass = function(className)
225{
226    if (!this.parentNode)
227        return null;
228    return this.parentNode.enclosingNodeOrSelfWithClass(className);
229}
230
231Element.prototype.query = function(query)
232{
233    return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
234}
235
236Element.prototype.removeChildren = function()
237{
238    if (this.firstChild)
239        this.textContent = "";
240}
241
242Element.prototype.isInsertionCaretInside = function()
243{
244    var selection = window.getSelection();
245    if (!selection.rangeCount || !selection.isCollapsed)
246        return false;
247    var selectionRange = selection.getRangeAt(0);
248    return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
249}
250
251Element.prototype.createChild = function(elementName, className)
252{
253    var element = document.createElement(elementName);
254    if (className)
255        element.className = className;
256    this.appendChild(element);
257    return element;
258}
259
260Element.prototype.__defineGetter__("totalOffsetLeft", function()
261{
262    var total = 0;
263    for (var element = this; element; element = element.offsetParent)
264        total += element.offsetLeft + (this !== element ? element.clientLeft : 0);
265    return total;
266});
267
268Element.prototype.__defineGetter__("totalOffsetTop", function()
269{
270    var total = 0;
271    for (var element = this; element; element = element.offsetParent)
272        total += element.offsetTop + (this !== element ? element.clientTop : 0);
273    return total;
274});
275
276Element.prototype.offsetRelativeToWindow = function(targetWindow)
277{
278    var elementOffset = {x: 0, y: 0};
279    var curElement = this;
280    var curWindow = this.ownerDocument.defaultView;
281    while (curWindow && curElement) {
282        elementOffset.x += curElement.totalOffsetLeft;
283        elementOffset.y += curElement.totalOffsetTop;
284        if (curWindow === targetWindow)
285            break;
286
287        curElement = curWindow.frameElement;
288        curWindow = curWindow.parent;
289    }
290
291    return elementOffset;
292}
293
294KeyboardEvent.prototype.__defineGetter__("data", function()
295{
296    // Emulate "data" attribute from DOM 3 TextInput event.
297    // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data
298    switch (this.type) {
299        case "keypress":
300            if (!this.ctrlKey && !this.metaKey)
301                return String.fromCharCode(this.charCode);
302            else
303                return "";
304        case "keydown":
305        case "keyup":
306            if (!this.ctrlKey && !this.metaKey && !this.altKey)
307                return String.fromCharCode(this.which);
308            else
309                return "";
310    }
311});
312
313Text.prototype.select = function(start, end)
314{
315    start = start || 0;
316    end = end || this.textContent.length;
317
318    if (start < 0)
319        start = end + start;
320
321    var selection = window.getSelection();
322    selection.removeAllRanges();
323    var range = document.createRange();
324    range.setStart(this, start);
325    range.setEnd(this, end);
326    selection.addRange(range);
327    return this;
328}
329
330Element.prototype.__defineGetter__("selectionLeftOffset", function() {
331    // Calculate selection offset relative to the current element.
332
333    var selection = window.getSelection();
334    if (!selection.containsNode(this, true))
335        return null;
336
337    var leftOffset = selection.anchorOffset;
338    var node = selection.anchorNode;
339
340    while (node !== this) {
341        while (node.previousSibling) {
342            node = node.previousSibling;
343            leftOffset += node.textContent.length;
344        }
345        node = node.parentNode;
346    }
347
348    return leftOffset;
349});
350
351Node.prototype.isWhitespace = isNodeWhitespace;
352Node.prototype.displayName = nodeDisplayName;
353Node.prototype.isAncestor = function(node)
354{
355    return isAncestorNode(this, node);
356};
357Node.prototype.isDescendant = isDescendantNode;
358Node.prototype.traverseNextNode = traverseNextNode;
359Node.prototype.traversePreviousNode = traversePreviousNode;
360
361String.prototype.hasSubstring = function(string, caseInsensitive)
362{
363    if (!caseInsensitive)
364        return this.indexOf(string) !== -1;
365    return this.match(new RegExp(string.escapeForRegExp(), "i"));
366}
367
368String.prototype.findAll = function(string)
369{
370    var matches = [];
371    var i = this.indexOf(string);
372    while (i !== -1) {
373        matches.push(i);
374        i = this.indexOf(string, i + string.length);
375    }
376    return matches;
377}
378
379String.prototype.lineEndings = function()
380{
381    if (!this._lineEndings) {
382        this._lineEndings = this.findAll("\n");
383        this._lineEndings.push(this.length);
384    }
385    return this._lineEndings;
386}
387
388String.prototype.asParsedURL = function()
389{
390    // RegExp groups:
391    // 1 - scheme
392    // 2 - hostname
393    // 3 - ?port
394    // 4 - ?path
395    // 5 - ?fragment
396    var match = this.match(/^([^:]+):\/\/([^\/:]*)(?::([\d]+))?(?:(\/[^#]*)(?:#(.*))?)?$/i);
397    if (!match)
398        return null;
399    var result = {};
400    result.scheme = match[1].toLowerCase();
401    result.host = match[2];
402    result.port = match[3];
403    result.path = match[4] || "/";
404    result.fragment = match[5];
405    return result;
406}
407
408String.prototype.escapeCharacters = function(chars)
409{
410    var foundChar = false;
411    for (var i = 0; i < chars.length; ++i) {
412        if (this.indexOf(chars.charAt(i)) !== -1) {
413            foundChar = true;
414            break;
415        }
416    }
417
418    if (!foundChar)
419        return this;
420
421    var result = "";
422    for (var i = 0; i < this.length; ++i) {
423        if (chars.indexOf(this.charAt(i)) !== -1)
424            result += "\\";
425        result += this.charAt(i);
426    }
427
428    return result;
429}
430
431String.prototype.escapeForRegExp = function()
432{
433    return this.escapeCharacters("^[]{}()\\.$*+?|");
434}
435
436String.prototype.escapeHTML = function()
437{
438    return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
439}
440
441String.prototype.collapseWhitespace = function()
442{
443    return this.replace(/[\s\xA0]+/g, " ");
444}
445
446String.prototype.trimURL = function(baseURLDomain)
447{
448    var result = this.replace(/^(https|http|file):\/\//i, "");
449    if (baseURLDomain)
450        result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
451    return result;
452}
453
454String.prototype.removeURLFragment = function()
455{
456    var fragmentIndex = this.indexOf("#");
457    if (fragmentIndex == -1)
458        fragmentIndex = this.length;
459    return this.substring(0, fragmentIndex);
460}
461
462function isNodeWhitespace()
463{
464    if (!this || this.nodeType !== Node.TEXT_NODE)
465        return false;
466    if (!this.nodeValue.length)
467        return true;
468    return this.nodeValue.match(/^[\s\xA0]+$/);
469}
470
471function nodeDisplayName()
472{
473    if (!this)
474        return "";
475
476    switch (this.nodeType) {
477        case Node.DOCUMENT_NODE:
478            return "Document";
479
480        case Node.ELEMENT_NODE:
481            var name = "<" + this.nodeName.toLowerCase();
482
483            if (this.hasAttributes()) {
484                var value = this.getAttribute("id");
485                if (value)
486                    name += " id=\"" + value + "\"";
487                value = this.getAttribute("class");
488                if (value)
489                    name += " class=\"" + value + "\"";
490                if (this.nodeName.toLowerCase() === "a") {
491                    value = this.getAttribute("name");
492                    if (value)
493                        name += " name=\"" + value + "\"";
494                    value = this.getAttribute("href");
495                    if (value)
496                        name += " href=\"" + value + "\"";
497                } else if (this.nodeName.toLowerCase() === "img") {
498                    value = this.getAttribute("src");
499                    if (value)
500                        name += " src=\"" + value + "\"";
501                } else if (this.nodeName.toLowerCase() === "iframe") {
502                    value = this.getAttribute("src");
503                    if (value)
504                        name += " src=\"" + value + "\"";
505                } else if (this.nodeName.toLowerCase() === "input") {
506                    value = this.getAttribute("name");
507                    if (value)
508                        name += " name=\"" + value + "\"";
509                    value = this.getAttribute("type");
510                    if (value)
511                        name += " type=\"" + value + "\"";
512                } else if (this.nodeName.toLowerCase() === "form") {
513                    value = this.getAttribute("action");
514                    if (value)
515                        name += " action=\"" + value + "\"";
516                }
517            }
518
519            return name + ">";
520
521        case Node.TEXT_NODE:
522            if (isNodeWhitespace.call(this))
523                return "(whitespace)";
524            return "\"" + this.nodeValue + "\"";
525
526        case Node.COMMENT_NODE:
527            return "<!--" + this.nodeValue + "-->";
528
529        case Node.DOCUMENT_TYPE_NODE:
530            var docType = "<!DOCTYPE " + this.nodeName;
531            if (this.publicId) {
532                docType += " PUBLIC \"" + this.publicId + "\"";
533                if (this.systemId)
534                    docType += " \"" + this.systemId + "\"";
535            } else if (this.systemId)
536                docType += " SYSTEM \"" + this.systemId + "\"";
537            if (this.internalSubset)
538                docType += " [" + this.internalSubset + "]";
539            return docType + ">";
540    }
541
542    return this.nodeName.toLowerCase().collapseWhitespace();
543}
544
545function isAncestorNode(ancestor, node)
546{
547    if (!node || !ancestor)
548        return false;
549
550    var currentNode = node.parentNode;
551    while (currentNode) {
552        if (ancestor === currentNode)
553            return true;
554        currentNode = currentNode.parentNode;
555    }
556    return false;
557}
558
559function isDescendantNode(descendant)
560{
561    return isAncestorNode(descendant, this);
562}
563
564function traverseNextNode(stayWithin)
565{
566    if (!this)
567        return;
568
569    var node = this.firstChild;
570    if (node)
571        return node;
572
573    if (stayWithin && this === stayWithin)
574        return null;
575
576    node = this.nextSibling;
577    if (node)
578        return node;
579
580    node = this;
581    while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
582        node = node.parentNode;
583    if (!node)
584        return null;
585
586    return node.nextSibling;
587}
588
589function traversePreviousNode(stayWithin)
590{
591    if (!this)
592        return;
593    if (stayWithin && this === stayWithin)
594        return null;
595    var node = this.previousSibling;
596    while (node && node.lastChild)
597        node = node.lastChild;
598    if (node)
599        return node;
600    return this.parentNode;
601}
602
603function getDocumentForNode(node)
604{
605    return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
606}
607
608function parentNode(node)
609{
610    return node.parentNode;
611}
612
613Number.millisToString = function(ms, higherResolution)
614{
615    return Number.secondsToString(ms / 1000, higherResolution);
616}
617
618Number.secondsToString = function(seconds, higherResolution)
619{
620    if (seconds === 0)
621        return "0";
622
623    var ms = seconds * 1000;
624    if (higherResolution && ms < 1000)
625        return WebInspector.UIString("%.3fms", ms);
626    else if (ms < 1000)
627        return WebInspector.UIString("%.0fms", ms);
628
629    if (seconds < 60)
630        return WebInspector.UIString("%.2fs", seconds);
631
632    var minutes = seconds / 60;
633    if (minutes < 60)
634        return WebInspector.UIString("%.1fmin", minutes);
635
636    var hours = minutes / 60;
637    if (hours < 24)
638        return WebInspector.UIString("%.1fhrs", hours);
639
640    var days = hours / 24;
641    return WebInspector.UIString("%.1f days", days);
642}
643
644Number.bytesToString = function(bytes, higherResolution)
645{
646    if (typeof higherResolution === "undefined")
647        higherResolution = true;
648
649    if (bytes < 1024)
650        return WebInspector.UIString("%.0fB", bytes);
651
652    var kilobytes = bytes / 1024;
653    if (higherResolution && kilobytes < 1024)
654        return WebInspector.UIString("%.2fKB", kilobytes);
655    else if (kilobytes < 1024)
656        return WebInspector.UIString("%.0fKB", kilobytes);
657
658    var megabytes = kilobytes / 1024;
659    if (higherResolution)
660        return WebInspector.UIString("%.2fMB", megabytes);
661    else
662        return WebInspector.UIString("%.0fMB", megabytes);
663}
664
665Number.constrain = function(num, min, max)
666{
667    if (num < min)
668        num = min;
669    else if (num > max)
670        num = max;
671    return num;
672}
673
674HTMLTextAreaElement.prototype.moveCursorToEnd = function()
675{
676    var length = this.value.length;
677    this.setSelectionRange(length, length);
678}
679
680Object.defineProperty(Array.prototype, "remove", { value: function(value, onlyFirst)
681{
682    if (onlyFirst) {
683        var index = this.indexOf(value);
684        if (index !== -1)
685            this.splice(index, 1);
686        return;
687    }
688
689    var length = this.length;
690    for (var i = 0; i < length; ++i) {
691        if (this[i] === value)
692            this.splice(i, 1);
693    }
694}});
695
696Object.defineProperty(Array.prototype, "keySet", { value: function()
697{
698    var keys = {};
699    for (var i = 0; i < this.length; ++i)
700        keys[this[i]] = true;
701    return keys;
702}});
703
704Object.defineProperty(Array.prototype, "upperBound", { value: function(value)
705{
706    var first = 0;
707    var count = this.length;
708    while (count > 0) {
709      var step = count >> 1;
710      var middle = first + step;
711      if (value >= this[middle]) {
712          first = middle + 1;
713          count -= step + 1;
714      } else
715          count = step;
716    }
717    return first;
718}});
719
720Array.diff = function(left, right)
721{
722    var o = left;
723    var n = right;
724
725    var ns = {};
726    var os = {};
727
728    for (var i = 0; i < n.length; i++) {
729        if (ns[n[i]] == null)
730            ns[n[i]] = { rows: [], o: null };
731        ns[n[i]].rows.push(i);
732    }
733
734    for (var i = 0; i < o.length; i++) {
735        if (os[o[i]] == null)
736            os[o[i]] = { rows: [], n: null };
737        os[o[i]].rows.push(i);
738    }
739
740    for (var i in ns) {
741        if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
742            n[ns[i].rows[0]] = { text: n[ns[i].rows[0]], row: os[i].rows[0] };
743            o[os[i].rows[0]] = { text: o[os[i].rows[0]], row: ns[i].rows[0] };
744        }
745    }
746
747    for (var i = 0; i < n.length - 1; i++) {
748        if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && n[i + 1] == o[n[i].row + 1]) {
749            n[i + 1] = { text: n[i + 1], row: n[i].row + 1 };
750            o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1 };
751        }
752    }
753
754    for (var i = n.length - 1; i > 0; i--) {
755        if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
756            n[i - 1] == o[n[i].row - 1]) {
757            n[i - 1] = { text: n[i - 1], row: n[i].row - 1 };
758            o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1 };
759        }
760    }
761
762    return { left: o, right: n };
763}
764
765Array.convert = function(list)
766{
767    // Cast array-like object to an array.
768    return Array.prototype.slice.call(list);
769}
770
771function binarySearch(object, array, comparator)
772{
773    var first = 0;
774    var last = array.length - 1;
775
776    while (first <= last) {
777        var mid = (first + last) >> 1;
778        var c = comparator(object, array[mid]);
779        if (c > 0)
780            first = mid + 1;
781        else if (c < 0)
782            last = mid - 1;
783        else
784            return mid;
785    }
786
787    // Return the nearest lesser index, "-1" means "0, "-2" means "1", etc.
788    return -(first + 1);
789}
790
791Object.defineProperty(Array.prototype, "binaryIndexOf", { value: function(value, comparator)
792{
793    var result = binarySearch(value, this, comparator);
794    return result >= 0 ? result : -1;
795}});
796
797function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunction)
798{
799    var index = binarySearch(anObject, aList, aFunction);
800    if (index < 0)
801        // See binarySearch implementation.
802        return -index - 1;
803    else {
804        // Return the first occurance of an item in the list.
805        while (index > 0 && aFunction(anObject, aList[index - 1]) === 0)
806            index--;
807        return index;
808    }
809}
810
811String.sprintf = function(format)
812{
813    return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
814}
815
816String.tokenizeFormatString = function(format)
817{
818    var tokens = [];
819    var substitutionIndex = 0;
820
821    function addStringToken(str)
822    {
823        tokens.push({ type: "string", value: str });
824    }
825
826    function addSpecifierToken(specifier, precision, substitutionIndex)
827    {
828        tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
829    }
830
831    var index = 0;
832    for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
833        addStringToken(format.substring(index, precentIndex));
834        index = precentIndex + 1;
835
836        if (format[index] === "%") {
837            addStringToken("%");
838            ++index;
839            continue;
840        }
841
842        if (!isNaN(format[index])) {
843            // The first character is a number, it might be a substitution index.
844            var number = parseInt(format.substring(index));
845            while (!isNaN(format[index]))
846                ++index;
847            // If the number is greater than zero and ends with a "$",
848            // then this is a substitution index.
849            if (number > 0 && format[index] === "$") {
850                substitutionIndex = (number - 1);
851                ++index;
852            }
853        }
854
855        var precision = -1;
856        if (format[index] === ".") {
857            // This is a precision specifier. If no digit follows the ".",
858            // then the precision should be zero.
859            ++index;
860            precision = parseInt(format.substring(index));
861            if (isNaN(precision))
862                precision = 0;
863            while (!isNaN(format[index]))
864                ++index;
865        }
866
867        addSpecifierToken(format[index], precision, substitutionIndex);
868
869        ++substitutionIndex;
870        ++index;
871    }
872
873    addStringToken(format.substring(index));
874
875    return tokens;
876}
877
878String.standardFormatters = {
879    d: function(substitution)
880    {
881        if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) === "number")
882            substitution = substitution.description;
883        substitution = parseInt(substitution);
884        return !isNaN(substitution) ? substitution : 0;
885    },
886
887    f: function(substitution, token)
888    {
889        if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) === "number")
890            substitution = substitution.description;
891        substitution = parseFloat(substitution);
892        if (substitution && token.precision > -1)
893            substitution = substitution.toFixed(token.precision);
894        return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
895    },
896
897    s: function(substitution)
898    {
899        if (typeof substitution == "object" && WebInspector.RemoteObject.type(substitution) !== "null")
900            substitution = substitution.description;
901        return substitution;
902    },
903};
904
905String.vsprintf = function(format, substitutions)
906{
907    return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
908}
909
910String.format = function(format, substitutions, formatters, initialValue, append)
911{
912    if (!format || !substitutions || !substitutions.length)
913        return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
914
915    function prettyFunctionName()
916    {
917        return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
918    }
919
920    function warn(msg)
921    {
922        console.warn(prettyFunctionName() + ": " + msg);
923    }
924
925    function error(msg)
926    {
927        console.error(prettyFunctionName() + ": " + msg);
928    }
929
930    var result = initialValue;
931    var tokens = String.tokenizeFormatString(format);
932    var usedSubstitutionIndexes = {};
933
934    for (var i = 0; i < tokens.length; ++i) {
935        var token = tokens[i];
936
937        if (token.type === "string") {
938            result = append(result, token.value);
939            continue;
940        }
941
942        if (token.type !== "specifier") {
943            error("Unknown token type \"" + token.type + "\" found.");
944            continue;
945        }
946
947        if (token.substitutionIndex >= substitutions.length) {
948            // If there are not enough substitutions for the current substitutionIndex
949            // just output the format specifier literally and move on.
950            error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
951            result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
952            continue;
953        }
954
955        usedSubstitutionIndexes[token.substitutionIndex] = true;
956
957        if (!(token.specifier in formatters)) {
958            // Encountered an unsupported format character, treat as a string.
959            warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
960            result = append(result, substitutions[token.substitutionIndex]);
961            continue;
962        }
963
964        result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
965    }
966
967    var unusedSubstitutions = [];
968    for (var i = 0; i < substitutions.length; ++i) {
969        if (i in usedSubstitutionIndexes)
970            continue;
971        unusedSubstitutions.push(substitutions[i]);
972    }
973
974    return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
975}
976
977function isEnterKey(event) {
978    // Check if in IME.
979    return event.keyCode !== 229 && event.keyIdentifier === "Enter";
980}
981
982function highlightSearchResult(element, offset, length)
983{
984    var result = highlightSearchResults(element, [{offset: offset, length: length }]);
985    return result.length ? result[0] : null;
986}
987
988function highlightSearchResults(element, resultRanges)
989{
990    var highlightNodes = [];
991    var lineText = element.textContent;
992    var textNodeSnapshot = document.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
993
994    var snapshotLength = textNodeSnapshot.snapshotLength;
995    var snapshotNodeOffset = 0;
996    var currentSnapshotItem = 0;
997
998    for (var i = 0; i < resultRanges.length; ++i) {
999        var resultLength = resultRanges[i].length;
1000        var startOffset = resultRanges[i].offset;
1001        var endOffset = startOffset + resultLength;
1002        var length = resultLength;
1003        var textNode;
1004        var textNodeOffset;
1005        var found;
1006
1007        while (currentSnapshotItem < snapshotLength) {
1008            textNode = textNodeSnapshot.snapshotItem(currentSnapshotItem++);
1009            var textNodeLength = textNode.nodeValue.length;
1010            if (snapshotNodeOffset + textNodeLength >= startOffset) {
1011                textNodeOffset = startOffset - snapshotNodeOffset;
1012                snapshotNodeOffset += textNodeLength;
1013                found = true;
1014                break;
1015            }
1016            snapshotNodeOffset += textNodeLength;
1017        }
1018
1019        if (!found) {
1020            textNode = element;
1021            textNodeOffset = 0;
1022        }
1023
1024        var highlightNode = document.createElement("span");
1025        highlightNode.className = "webkit-search-result";
1026        highlightNode.textContent = lineText.substring(startOffset, endOffset);
1027
1028        var text = textNode.textContent;
1029        if (textNodeOffset + resultLength < text.length) {
1030            // Selection belongs to a single split mode.
1031            textNode.textContent = text.substring(textNodeOffset + resultLength);
1032            textNode.parentElement.insertBefore(highlightNode, textNode);
1033            var prefixNode = document.createTextNode(text.substring(0, textNodeOffset));
1034            textNode.parentElement.insertBefore(prefixNode, highlightNode);
1035
1036            highlightNodes.push(highlightNode);
1037            continue;
1038        }
1039
1040        var parentElement = textNode.parentElement;
1041        var anchorElement = textNode.nextSibling;
1042
1043        length -= text.length - textNodeOffset;
1044        textNode.textContent = text.substring(0, textNodeOffset);
1045
1046        while (currentSnapshotItem < snapshotLength) {
1047            textNode = textNodeSnapshot.snapshotItem(currentSnapshotItem++);
1048            snapshotNodeOffset += textNode.nodeValue.length;
1049            var text = textNode.textContent;
1050            if (length < text.length) {
1051                textNode.textContent = text.substring(length);
1052                break;
1053            }
1054
1055            length -= text.length;
1056            textNode.textContent = "";
1057        }
1058
1059        parentElement.insertBefore(highlightNode, anchorElement);
1060        highlightNodes.push(highlightNode);
1061    }
1062
1063    return highlightNodes;
1064}
1065
1066function createSearchRegex(query, extraFlags)
1067{
1068    var regex = "";
1069    for (var i = 0; i < query.length; ++i) {
1070        var char = query.charAt(i);
1071        if (char === "]")
1072            char = "\\]";
1073        regex += "[" + char + "]";
1074    }
1075    return new RegExp(regex, "i" + (extraFlags || ""));
1076}
1077
1078function offerFileForDownload(contents)
1079{
1080    var builder = new BlobBuilder();
1081    builder.append(contents);
1082    var blob = builder.getBlob("application/octet-stream");
1083    var url = window.webkitURL.createObjectURL(blob);
1084    window.open(url);
1085}
1086