initial import
[vuplus_webkit] / Source / WebCore / inspector / front-end / 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  * 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
32 function setupPrototypeUtilities() {
33
34 Function.prototype.bind = function(thisObject)
35 {
36     var func = this;
37     var args = Array.prototype.slice.call(arguments, 1);
38     function bound()
39     {
40         return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0)));
41     }
42     bound.toString = function() {
43         return "bound: " + func;
44     };
45     return bound;
46 }
47
48 Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction)
49 {
50     var startNode;
51     var startOffset = 0;
52     var endNode;
53     var endOffset = 0;
54
55     if (!stayWithinNode)
56         stayWithinNode = this;
57
58     if (!direction || direction === "backward" || direction === "both") {
59         var node = this;
60         while (node) {
61             if (node === stayWithinNode) {
62                 if (!startNode)
63                     startNode = stayWithinNode;
64                 break;
65             }
66
67             if (node.nodeType === Node.TEXT_NODE) {
68                 var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1));
69                 for (var i = start; i >= 0; --i) {
70                     if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
71                         startNode = node;
72                         startOffset = i + 1;
73                         break;
74                     }
75                 }
76             }
77
78             if (startNode)
79                 break;
80
81             node = node.traversePreviousNode(stayWithinNode);
82         }
83
84         if (!startNode) {
85             startNode = stayWithinNode;
86             startOffset = 0;
87         }
88     } else {
89         startNode = this;
90         startOffset = offset;
91     }
92
93     if (!direction || direction === "forward" || direction === "both") {
94         node = this;
95         while (node) {
96             if (node === stayWithinNode) {
97                 if (!endNode)
98                     endNode = stayWithinNode;
99                 break;
100             }
101
102             if (node.nodeType === Node.TEXT_NODE) {
103                 var start = (node === this ? offset : 0);
104                 for (var i = start; i < node.nodeValue.length; ++i) {
105                     if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
106                         endNode = node;
107                         endOffset = i;
108                         break;
109                     }
110                 }
111             }
112
113             if (endNode)
114                 break;
115
116             node = node.traverseNextNode(stayWithinNode);
117         }
118
119         if (!endNode) {
120             endNode = stayWithinNode;
121             endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
122         }
123     } else {
124         endNode = this;
125         endOffset = offset;
126     }
127
128     var result = this.ownerDocument.createRange();
129     result.setStart(startNode, startOffset);
130     result.setEnd(endNode, endOffset);
131
132     return result;
133 }
134
135 Node.prototype.traverseNextTextNode = function(stayWithin)
136 {
137     var node = this.traverseNextNode(stayWithin);
138     if (!node)
139         return;
140
141     while (node && node.nodeType !== Node.TEXT_NODE)
142         node = node.traverseNextNode(stayWithin);
143
144     return node;
145 }
146
147 Node.prototype.rangeBoundaryForOffset = function(offset)
148 {
149     var node = this.traverseNextTextNode(this);
150     while (node && offset > node.nodeValue.length) {
151         offset -= node.nodeValue.length;
152         node = node.traverseNextTextNode(this);
153     }
154     if (!node)
155         return { container: this, offset: 0 };
156     return { container: node, offset: offset };
157 }
158
159 Element.prototype.removeStyleClass = function(className)
160 {
161     this.classList.remove(className);
162 }
163
164 Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
165 {
166     var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
167     if (regex.test(this.className))
168         this.className = this.className.replace(regex, " ");
169 }
170
171 Element.prototype.addStyleClass = function(className)
172 {
173     this.classList.add(className);
174 }
175
176 Element.prototype.hasStyleClass = function(className)
177 {
178     return this.classList.contains(className);
179 }
180
181 Element.prototype.positionAt = function(x, y)
182 {
183     this.style.left = x + "px";
184     this.style.top = y + "px";
185 }
186
187 Element.prototype.pruneEmptyTextNodes = function()
188 {
189     var sibling = this.firstChild;
190     while (sibling) {
191         var nextSibling = sibling.nextSibling;
192         if (sibling.nodeType === this.TEXT_NODE && sibling.nodeValue === "")
193             this.removeChild(sibling);
194         sibling = nextSibling;
195     }
196 }
197
198 Element.prototype.isScrolledToBottom = function()
199 {
200     // This code works only for 0-width border
201     return this.scrollTop + this.clientHeight === this.scrollHeight;
202 }
203
204 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
205 {
206     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
207         for (var i = 0; i < nameArray.length; ++i)
208             if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
209                 return node;
210     return null;
211 }
212
213 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
214 {
215     return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
216 }
217
218 Node.prototype.enclosingNodeOrSelfWithClass = function(className)
219 {
220     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
221         if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
222             return node;
223     return null;
224 }
225
226 Node.prototype.enclosingNodeWithClass = function(className)
227 {
228     if (!this.parentNode)
229         return null;
230     return this.parentNode.enclosingNodeOrSelfWithClass(className);
231 }
232
233 Element.prototype.query = function(query)
234 {
235     return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
236 }
237
238 Element.prototype.removeChildren = function()
239 {
240     if (this.firstChild)
241         this.textContent = "";
242 }
243
244 Element.prototype.isInsertionCaretInside = function()
245 {
246     var selection = window.getSelection();
247     if (!selection.rangeCount || !selection.isCollapsed)
248         return false;
249     var selectionRange = selection.getRangeAt(0);
250     return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
251 }
252
253 Element.prototype.createChild = function(elementName, className)
254 {
255     var element = this.ownerDocument.createElement(elementName);
256     if (className)
257         element.className = className;
258     this.appendChild(element);
259     return element;
260 }
261
262 DocumentFragment.prototype.createChild = Element.prototype.createChild;
263
264 Element.prototype.totalOffsetLeft = function()
265 {
266     var total = 0;
267     for (var element = this; element; element = element.offsetParent)
268         total += element.offsetLeft + (this !== element ? element.clientLeft : 0);
269     return total;
270 }
271
272 Element.prototype.totalOffsetTop = function()
273 {
274     var total = 0;
275     for (var element = this; element; element = element.offsetParent)
276         total += element.offsetTop + (this !== element ? element.clientTop : 0);
277     return total;
278 }
279
280 Element.prototype.offsetRelativeToWindow = function(targetWindow)
281 {
282     var elementOffset = {x: 0, y: 0};
283     var curElement = this;
284     var curWindow = this.ownerDocument.defaultView;
285     while (curWindow && curElement) {
286         elementOffset.x += curElement.totalOffsetLeft();
287         elementOffset.y += curElement.totalOffsetTop();
288         if (curWindow === targetWindow)
289             break;
290
291         curElement = curWindow.frameElement;
292         curWindow = curWindow.parent;
293     }
294
295     return elementOffset;
296 }
297
298 Element.prototype.setTextAndTitle = function(text)
299 {
300     this.textContent = text;
301     this.title = text;
302 }
303
304 KeyboardEvent.prototype.__defineGetter__("data", function()
305 {
306     // Emulate "data" attribute from DOM 3 TextInput event.
307     // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data
308     switch (this.type) {
309         case "keypress":
310             if (!this.ctrlKey && !this.metaKey)
311                 return String.fromCharCode(this.charCode);
312             else
313                 return "";
314         case "keydown":
315         case "keyup":
316             if (!this.ctrlKey && !this.metaKey && !this.altKey)
317                 return String.fromCharCode(this.which);
318             else
319                 return "";
320     }
321 });
322
323 Text.prototype.select = function(start, end)
324 {
325     start = start || 0;
326     end = end || this.textContent.length;
327
328     if (start < 0)
329         start = end + start;
330
331     var selection = this.ownerDocument.defaultView.getSelection();
332     selection.removeAllRanges();
333     var range = this.ownerDocument.createRange();
334     range.setStart(this, start);
335     range.setEnd(this, end);
336     selection.addRange(range);
337     return this;
338 }
339
340 Element.prototype.__defineGetter__("selectionLeftOffset", function() {
341     // Calculate selection offset relative to the current element.
342
343     var selection = window.getSelection();
344     if (!selection.containsNode(this, true))
345         return null;
346
347     var leftOffset = selection.anchorOffset;
348     var node = selection.anchorNode;
349
350     while (node !== this) {
351         while (node.previousSibling) {
352             node = node.previousSibling;
353             leftOffset += node.textContent.length;
354         }
355         node = node.parentNode;
356     }
357
358     return leftOffset;
359 });
360
361 String.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
368 String.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
379 String.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
388 String.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
408 String.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
431 String.prototype.escapeForRegExp = function()
432 {
433     return this.escapeCharacters("^[]{}()\\.$*+?|");
434 }
435
436 String.prototype.escapeHTML = function()
437 {
438     return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); //" doublequotes just for editor
439 }
440
441 String.prototype.collapseWhitespace = function()
442 {
443     return this.replace(/[\s\xA0]+/g, " ");
444 }
445
446 String.prototype.trimMiddle = function(maxLength)
447 {
448     if (this.length <= maxLength)
449         return this;
450     var leftHalf = maxLength >> 1;
451     var rightHalf = maxLength - leftHalf - 1;
452     return this.substr(0, leftHalf) + "\u2026" + this.substr(this.length - rightHalf, rightHalf);
453 }
454
455 String.prototype.trimURL = function(baseURLDomain)
456 {
457     var result = this.replace(/^(https|http|file):\/\//i, "");
458     if (baseURLDomain)
459         result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
460     return result;
461 }
462
463 String.prototype.removeURLFragment = function()
464 {
465     var fragmentIndex = this.indexOf("#");
466     if (fragmentIndex == -1)
467         fragmentIndex = this.length;
468     return this.substring(0, fragmentIndex);
469 }
470
471 Node.prototype.isAncestor = function(node)
472 {
473     if (!node)
474         return false;
475
476     var currentNode = node.parentNode;
477     while (currentNode) {
478         if (this === currentNode)
479             return true;
480         currentNode = currentNode.parentNode;
481     }
482     return false;
483 }
484
485 Node.prototype.isDescendant = function(descendant)
486 {
487     return !!descendant && descendant.isAncestor(this);
488 }
489
490 Node.prototype.traverseNextNode = function(stayWithin)
491 {
492     var node = this.firstChild;
493     if (node)
494         return node;
495
496     if (stayWithin && this === stayWithin)
497         return null;
498
499     node = this.nextSibling;
500     if (node)
501         return node;
502
503     node = this;
504     while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
505         node = node.parentNode;
506     if (!node)
507         return null;
508
509     return node.nextSibling;
510 }
511
512 Node.prototype.traversePreviousNode = function(stayWithin)
513 {
514     if (stayWithin && this === stayWithin)
515         return null;
516     var node = this.previousSibling;
517     while (node && node.lastChild)
518         node = node.lastChild;
519     if (node)
520         return node;
521     return this.parentNode;
522 }
523
524 window.parentNode = function(node)
525 {
526     return node.parentNode;
527 }
528
529 Number.constrain = function(num, min, max)
530 {
531     if (num < min)
532         num = min;
533     else if (num > max)
534         num = max;
535     return num;
536 }
537
538 Date.prototype.toRFC3339 = function()
539 {
540     function leadZero(x)
541     {
542         return x > 9 ? x : '0' + x
543     }
544     var offset = Math.abs(this.getTimezoneOffset());
545     var offsetString = Math.floor(offset / 60) + ':' + leadZero(offset % 60);
546     return this.getFullYear() + '-' +
547            leadZero(this.getMonth() + 1) + '-' +
548            leadZero(this.getDate()) + 'T' +
549            leadZero(this.getHours()) + ':' +
550            leadZero(this.getMinutes()) + ':' +
551            leadZero(this.getSeconds()) +
552            (!offset ? "Z" : (this.getTimezoneOffset() > 0 ? '-' : '+') + offsetString);
553 }
554
555 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
556 {
557     var length = this.value.length;
558     this.setSelectionRange(length, length);
559 }
560
561 Object.defineProperty(Array.prototype, "remove",
562 {
563     /**
564      * @this {Array.<*>}
565      */
566     value: function(value, onlyFirst)
567     {
568         if (onlyFirst) {
569             var index = this.indexOf(value);
570             if (index !== -1)
571                 this.splice(index, 1);
572             return;
573         }
574
575         var length = this.length;
576         for (var i = 0; i < length; ++i) {
577             if (this[i] === value)
578                 this.splice(i, 1);
579         }
580     }
581 });
582
583 Object.defineProperty(Array.prototype, "keySet",
584 {
585     /**
586      * @this {Array.<*>}
587      */
588     value: function()
589     {
590         var keys = {};
591         for (var i = 0; i < this.length; ++i)
592             keys[this[i]] = true;
593         return keys;
594     }
595 });
596
597 Object.defineProperty(Array.prototype, "upperBound",
598 {
599     /**
600      * @this {Array.<number>}
601      */
602     value: function(value)
603     {
604         var first = 0;
605         var count = this.length;
606         while (count > 0) {
607           var step = count >> 1;
608           var middle = first + step;
609           if (value >= this[middle]) {
610               first = middle + 1;
611               count -= step + 1;
612           } else
613               count = step;
614         }
615         return first;
616     }
617 });
618
619 Array.diff = function(left, right)
620 {
621     var o = left;
622     var n = right;
623
624     var ns = {};
625     var os = {};
626
627     for (var i = 0; i < n.length; i++) {
628         if (ns[n[i]] == null)
629             ns[n[i]] = { rows: [], o: null };
630         ns[n[i]].rows.push(i);
631     }
632
633     for (var i = 0; i < o.length; i++) {
634         if (os[o[i]] == null)
635             os[o[i]] = { rows: [], n: null };
636         os[o[i]].rows.push(i);
637     }
638
639     for (var i in ns) {
640         if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
641             n[ns[i].rows[0]] = { text: n[ns[i].rows[0]], row: os[i].rows[0] };
642             o[os[i].rows[0]] = { text: o[os[i].rows[0]], row: ns[i].rows[0] };
643         }
644     }
645
646     for (var i = 0; i < n.length - 1; i++) {
647         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]) {
648             n[i + 1] = { text: n[i + 1], row: n[i].row + 1 };
649             o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1 };
650         }
651     }
652
653     for (var i = n.length - 1; i > 0; i--) {
654         if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
655             n[i - 1] == o[n[i].row - 1]) {
656             n[i - 1] = { text: n[i - 1], row: n[i].row - 1 };
657             o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1 };
658         }
659     }
660
661     return { left: o, right: n };
662 }
663
664 Array.convert = function(list)
665 {
666     // Cast array-like object to an array.
667     return Array.prototype.slice.call(list);
668 }
669
670 String.sprintf = function(format)
671 {
672     return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
673 }
674
675 String.tokenizeFormatString = function(format)
676 {
677     var tokens = [];
678     var substitutionIndex = 0;
679
680     function addStringToken(str)
681     {
682         tokens.push({ type: "string", value: str });
683     }
684
685     function addSpecifierToken(specifier, precision, substitutionIndex)
686     {
687         tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
688     }
689
690     var index = 0;
691     for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
692         addStringToken(format.substring(index, precentIndex));
693         index = precentIndex + 1;
694
695         if (format[index] === "%") {
696             addStringToken("%");
697             ++index;
698             continue;
699         }
700
701         if (!isNaN(format[index])) {
702             // The first character is a number, it might be a substitution index.
703             var number = parseInt(format.substring(index), 10);
704             while (!isNaN(format[index]))
705                 ++index;
706             // If the number is greater than zero and ends with a "$",
707             // then this is a substitution index.
708             if (number > 0 && format[index] === "$") {
709                 substitutionIndex = (number - 1);
710                 ++index;
711             }
712         }
713
714         var precision = -1;
715         if (format[index] === ".") {
716             // This is a precision specifier. If no digit follows the ".",
717             // then the precision should be zero.
718             ++index;
719             precision = parseInt(format.substring(index), 10);
720             if (isNaN(precision))
721                 precision = 0;
722             while (!isNaN(format[index]))
723                 ++index;
724         }
725
726         addSpecifierToken(format[index], precision, substitutionIndex);
727
728         ++substitutionIndex;
729         ++index;
730     }
731
732     addStringToken(format.substring(index));
733
734     return tokens;
735 }
736
737 String.standardFormatters = {
738     d: function(substitution)
739     {
740         return !isNaN(substitution) ? substitution : 0;
741     },
742
743     f: function(substitution, token)
744     {
745         if (substitution && token.precision > -1)
746             substitution = substitution.toFixed(token.precision);
747         return !isNaN(substitution) ? substitution : (token.precision > -1 ? Number(0).toFixed(token.precision) : 0);
748     },
749
750     s: function(substitution)
751     {
752         return substitution;
753     }
754 }
755
756 String.vsprintf = function(format, substitutions)
757 {
758     return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
759 }
760
761 String.format = function(format, substitutions, formatters, initialValue, append)
762 {
763     if (!format || !substitutions || !substitutions.length)
764         return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
765
766     function prettyFunctionName()
767     {
768         return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
769     }
770
771     function warn(msg)
772     {
773         console.warn(prettyFunctionName() + ": " + msg);
774     }
775
776     function error(msg)
777     {
778         console.error(prettyFunctionName() + ": " + msg);
779     }
780
781     var result = initialValue;
782     var tokens = String.tokenizeFormatString(format);
783     var usedSubstitutionIndexes = {};
784
785     for (var i = 0; i < tokens.length; ++i) {
786         var token = tokens[i];
787
788         if (token.type === "string") {
789             result = append(result, token.value);
790             continue;
791         }
792
793         if (token.type !== "specifier") {
794             error("Unknown token type \"" + token.type + "\" found.");
795             continue;
796         }
797
798         if (token.substitutionIndex >= substitutions.length) {
799             // If there are not enough substitutions for the current substitutionIndex
800             // just output the format specifier literally and move on.
801             error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped.");
802             result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
803             continue;
804         }
805
806         usedSubstitutionIndexes[token.substitutionIndex] = true;
807
808         if (!(token.specifier in formatters)) {
809             // Encountered an unsupported format character, treat as a string.
810             warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string.");
811             result = append(result, substitutions[token.substitutionIndex]);
812             continue;
813         }
814
815         result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
816     }
817
818     var unusedSubstitutions = [];
819     for (var i = 0; i < substitutions.length; ++i) {
820         if (i in usedSubstitutionIndexes)
821             continue;
822         unusedSubstitutions.push(substitutions[i]);
823     }
824
825     return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
826 }
827
828 } // setupPrototypeUtilities()
829
830 setupPrototypeUtilities();
831
832 function isEnterKey(event) {
833     // Check if in IME.
834     return event.keyCode !== 229 && event.keyIdentifier === "Enter";
835 }
836
837 function highlightSearchResult(element, offset, length, domChanges)
838 {
839     var result = highlightSearchResults(element, [{offset: offset, length: length }], domChanges);
840     return result.length ? result[0] : null;
841 }
842
843 /**
844  * @param {Array.<Object>=} changes
845  */
846 function highlightSearchResults(element, resultRanges, changes)
847 {
848     changes = changes || [];
849     var highlightNodes = [];
850     var lineText = element.textContent;
851     var ownerDocument = element.ownerDocument;
852     var textNodeSnapshot = ownerDocument.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
853
854     var snapshotLength = textNodeSnapshot.snapshotLength;
855     var snapshotNodeOffset = 0;
856     var currentSnapshotItem = 0;
857
858     for (var i = 0; i < resultRanges.length; ++i) {
859         var resultLength = resultRanges[i].length;
860         var startOffset = resultRanges[i].offset;
861         var endOffset = startOffset + resultLength;
862         var length = resultLength;
863         var textNode;
864         var textNodeOffset;
865         var found;
866
867         while (currentSnapshotItem < snapshotLength) {
868             textNode = textNodeSnapshot.snapshotItem(currentSnapshotItem++);
869             var textNodeLength = textNode.nodeValue.length;
870             if (snapshotNodeOffset + textNodeLength > startOffset) {
871                 textNodeOffset = startOffset - snapshotNodeOffset;
872                 snapshotNodeOffset += textNodeLength;
873                 found = true;
874                 break;
875             }
876             snapshotNodeOffset += textNodeLength;
877         }
878
879         if (!found) {
880             textNode = element;
881             textNodeOffset = 0;
882         }
883
884         var highlightNode = ownerDocument.createElement("span");
885         highlightNode.className = "webkit-search-result";
886         highlightNode.textContent = lineText.substring(startOffset, endOffset);
887
888         var text = textNode.textContent;
889         if (textNodeOffset + resultLength < text.length) {
890             // Selection belongs to a single split mode.
891             textNode.textContent = text.substring(textNodeOffset + resultLength);
892             changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
893
894             textNode.parentElement.insertBefore(highlightNode, textNode);
895             changes.push({ node: highlightNode, type: "added", nextSibling: textNode, parent: textNode.parentElement });
896
897             var prefixNode = ownerDocument.createTextNode(text.substring(0, textNodeOffset));
898             textNode.parentElement.insertBefore(prefixNode, highlightNode);
899             changes.push({ node: prefixNode, type: "added", nextSibling: highlightNode, parent: textNode.parentElement });
900             highlightNodes.push(highlightNode);
901             continue;
902         }
903
904         var parentElement = textNode.parentElement;
905         var anchorElement = textNode.nextSibling;
906
907         length -= text.length - textNodeOffset;
908         textNode.textContent = text.substring(0, textNodeOffset);
909         changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
910
911         while (currentSnapshotItem < snapshotLength) {
912             textNode = textNodeSnapshot.snapshotItem(currentSnapshotItem++);
913             snapshotNodeOffset += textNode.nodeValue.length;
914             text = textNode.textContent;
915             if (length < text.length) {
916                 textNode.textContent = text.substring(length);
917                 changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
918                 break;
919             }
920
921             length -= text.length;
922             textNode.textContent = "";
923             changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
924         }
925
926         parentElement.insertBefore(highlightNode, anchorElement);
927         changes.push({ node: highlightNode, type: "added", nextSibling: anchorElement, parent: parentElement });
928         highlightNodes.push(highlightNode);
929     }
930
931     return highlightNodes;
932 }
933
934 function applyDomChanges(domChanges)
935 {
936     for (var i = 0, size = domChanges.length; i < size; ++i) {
937         var entry = domChanges[i];
938         switch (entry.type) {
939         case "added":
940             entry.parent.insertBefore(entry.node, entry.nextSibling);
941             break;
942         case "changed":
943             entry.node.textContent = entry.newText;
944             break;
945         }
946     }
947 }
948
949 function revertDomChanges(domChanges)
950 {
951     for (var i = 0, size = domChanges.length; i < size; ++i) {
952         var entry = domChanges[i];
953         switch (entry.type) {
954         case "added":
955             if (entry.node.parentElement)
956                 entry.node.parentElement.removeChild(entry.node);
957             break;
958         case "changed":
959             entry.node.textContent = entry.oldText;
960             break;
961         }
962     }
963 }
964
965 function createSearchRegex(query, extraFlags)
966 {
967     // This should be kept the same as the one in InspectorPageAgent.cpp.
968     var regexSpecialCharacters = "[](){}+-*.,?\\^$|";
969     var regex = "";
970     for (var i = 0; i < query.length; ++i) {
971         var c = query.charAt(i);
972         if (regexSpecialCharacters.indexOf(c) != -1)
973             regex += "\\";
974         regex += c;
975     }
976     return new RegExp(regex, "i" + (extraFlags || ""));
977 }
978
979 function countRegexMatches(regex, content)
980 {
981     var text = content;
982     var result = 0;
983     var match;
984     while (text && (match = regex.exec(text))) {
985         if (match[0].length > 0)
986             ++result;
987         text = text.substring(match.index + 1);
988     }
989     return result;
990 }