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