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