2 * Copyright (C) 2007 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
28 * Contains diff method based on Javascript Diff Algorithm By John Resig
29 * http://ejohn.org/files/jsdiff.js (released under the MIT license).
32 function setupPrototypeUtilities() {
34 Function.prototype.bind = function(thisObject)
37 var args = Array.prototype.slice.call(arguments, 1);
40 return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0)));
42 bound.toString = function() {
43 return "bound: " + func;
48 Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction)
56 stayWithinNode = this;
58 if (!direction || direction === "backward" || direction === "both") {
61 if (node === stayWithinNode) {
63 startNode = stayWithinNode;
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) {
81 node = node.traversePreviousNode(stayWithinNode);
85 startNode = stayWithinNode;
93 if (!direction || direction === "forward" || direction === "both") {
96 if (node === stayWithinNode) {
98 endNode = stayWithinNode;
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) {
116 node = node.traverseNextNode(stayWithinNode);
120 endNode = stayWithinNode;
121 endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
128 var result = this.ownerDocument.createRange();
129 result.setStart(startNode, startOffset);
130 result.setEnd(endNode, endOffset);
135 Node.prototype.traverseNextTextNode = function(stayWithin)
137 var node = this.traverseNextNode(stayWithin);
141 while (node && node.nodeType !== Node.TEXT_NODE)
142 node = node.traverseNextNode(stayWithin);
147 Node.prototype.rangeBoundaryForOffset = function(offset)
149 var node = this.traverseNextTextNode(this);
150 while (node && offset > node.nodeValue.length) {
151 offset -= node.nodeValue.length;
152 node = node.traverseNextTextNode(this);
155 return { container: this, offset: 0 };
156 return { container: node, offset: offset };
159 Element.prototype.removeStyleClass = function(className)
161 this.classList.remove(className);
164 Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
166 var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
167 if (regex.test(this.className))
168 this.className = this.className.replace(regex, " ");
171 Element.prototype.addStyleClass = function(className)
173 this.classList.add(className);
176 Element.prototype.hasStyleClass = function(className)
178 return this.classList.contains(className);
181 Element.prototype.positionAt = function(x, y)
183 this.style.left = x + "px";
184 this.style.top = y + "px";
187 Element.prototype.pruneEmptyTextNodes = function()
189 var sibling = this.firstChild;
191 var nextSibling = sibling.nextSibling;
192 if (sibling.nodeType === this.TEXT_NODE && sibling.nodeValue === "")
193 this.removeChild(sibling);
194 sibling = nextSibling;
198 Element.prototype.isScrolledToBottom = function()
200 // This code works only for 0-width border
201 return this.scrollTop + this.clientHeight === this.scrollHeight;
204 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
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())
213 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
215 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
218 Node.prototype.enclosingNodeOrSelfWithClass = function(className)
220 for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
221 if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
226 Node.prototype.enclosingNodeWithClass = function(className)
228 if (!this.parentNode)
230 return this.parentNode.enclosingNodeOrSelfWithClass(className);
233 Element.prototype.query = function(query)
235 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
238 Element.prototype.removeChildren = function()
241 this.textContent = "";
244 Element.prototype.isInsertionCaretInside = function()
246 var selection = window.getSelection();
247 if (!selection.rangeCount || !selection.isCollapsed)
249 var selectionRange = selection.getRangeAt(0);
250 return selectionRange.startContainer === this || selectionRange.startContainer.isDescendant(this);
253 Element.prototype.createChild = function(elementName, className)
255 var element = this.ownerDocument.createElement(elementName);
257 element.className = className;
258 this.appendChild(element);
262 DocumentFragment.prototype.createChild = Element.prototype.createChild;
264 Element.prototype.totalOffsetLeft = function()
267 for (var element = this; element; element = element.offsetParent)
268 total += element.offsetLeft + (this !== element ? element.clientLeft : 0);
272 Element.prototype.totalOffsetTop = function()
275 for (var element = this; element; element = element.offsetParent)
276 total += element.offsetTop + (this !== element ? element.clientTop : 0);
280 Element.prototype.offsetRelativeToWindow = function(targetWindow)
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)
291 curElement = curWindow.frameElement;
292 curWindow = curWindow.parent;
295 return elementOffset;
298 Element.prototype.setTextAndTitle = function(text)
300 this.textContent = text;
304 KeyboardEvent.prototype.__defineGetter__("data", function()
306 // Emulate "data" attribute from DOM 3 TextInput event.
307 // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data
310 if (!this.ctrlKey && !this.metaKey)
311 return String.fromCharCode(this.charCode);
316 if (!this.ctrlKey && !this.metaKey && !this.altKey)
317 return String.fromCharCode(this.which);
323 Text.prototype.select = function(start, end)
326 end = end || this.textContent.length;
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);
340 Element.prototype.__defineGetter__("selectionLeftOffset", function() {
341 // Calculate selection offset relative to the current element.
343 var selection = window.getSelection();
344 if (!selection.containsNode(this, true))
347 var leftOffset = selection.anchorOffset;
348 var node = selection.anchorNode;
350 while (node !== this) {
351 while (node.previousSibling) {
352 node = node.previousSibling;
353 leftOffset += node.textContent.length;
355 node = node.parentNode;
361 String.prototype.hasSubstring = function(string, caseInsensitive)
363 if (!caseInsensitive)
364 return this.indexOf(string) !== -1;
365 return this.match(new RegExp(string.escapeForRegExp(), "i"));
368 String.prototype.findAll = function(string)
371 var i = this.indexOf(string);
374 i = this.indexOf(string, i + string.length);
379 String.prototype.lineEndings = function()
381 if (!this._lineEndings) {
382 this._lineEndings = this.findAll("\n");
383 this._lineEndings.push(this.length);
385 return this._lineEndings;
388 String.prototype.asParsedURL = function()
396 var match = this.match(/^([^:]+):\/\/([^\/:]*)(?::([\d]+))?(?:(\/[^#]*)(?:#(.*))?)?$/i);
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];
408 String.prototype.escapeCharacters = function(chars)
410 var foundChar = false;
411 for (var i = 0; i < chars.length; ++i) {
412 if (this.indexOf(chars.charAt(i)) !== -1) {
422 for (var i = 0; i < this.length; ++i) {
423 if (chars.indexOf(this.charAt(i)) !== -1)
425 result += this.charAt(i);
431 String.prototype.escapeForRegExp = function()
433 return this.escapeCharacters("^[]{}()\\.$*+?|");
436 String.prototype.escapeHTML = function()
438 return this.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); //" doublequotes just for editor
441 String.prototype.collapseWhitespace = function()
443 return this.replace(/[\s\xA0]+/g, " ");
446 String.prototype.trimMiddle = function(maxLength)
448 if (this.length <= maxLength)
450 var leftHalf = maxLength >> 1;
451 var rightHalf = maxLength - leftHalf - 1;
452 return this.substr(0, leftHalf) + "\u2026" + this.substr(this.length - rightHalf, rightHalf);
455 String.prototype.trimURL = function(baseURLDomain)
457 var result = this.replace(/^(https|http|file):\/\//i, "");
459 result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp(), "i"), "");
463 String.prototype.removeURLFragment = function()
465 var fragmentIndex = this.indexOf("#");
466 if (fragmentIndex == -1)
467 fragmentIndex = this.length;
468 return this.substring(0, fragmentIndex);
471 Node.prototype.isAncestor = function(node)
476 var currentNode = node.parentNode;
477 while (currentNode) {
478 if (this === currentNode)
480 currentNode = currentNode.parentNode;
485 Node.prototype.isDescendant = function(descendant)
487 return !!descendant && descendant.isAncestor(this);
490 Node.prototype.traverseNextNode = function(stayWithin)
492 var node = this.firstChild;
496 if (stayWithin && this === stayWithin)
499 node = this.nextSibling;
504 while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
505 node = node.parentNode;
509 return node.nextSibling;
512 Node.prototype.traversePreviousNode = function(stayWithin)
514 if (stayWithin && this === stayWithin)
516 var node = this.previousSibling;
517 while (node && node.lastChild)
518 node = node.lastChild;
521 return this.parentNode;
524 window.parentNode = function(node)
526 return node.parentNode;
529 Number.constrain = function(num, min, max)
538 Date.prototype.toRFC3339 = function()
542 return x > 9 ? x : '0' + x
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);
555 HTMLTextAreaElement.prototype.moveCursorToEnd = function()
557 var length = this.value.length;
558 this.setSelectionRange(length, length);
561 Object.defineProperty(Array.prototype, "remove",
566 value: function(value, onlyFirst)
569 var index = this.indexOf(value);
571 this.splice(index, 1);
575 var length = this.length;
576 for (var i = 0; i < length; ++i) {
577 if (this[i] === value)
583 Object.defineProperty(Array.prototype, "keySet",
591 for (var i = 0; i < this.length; ++i)
592 keys[this[i]] = true;
597 Object.defineProperty(Array.prototype, "upperBound",
600 * @this {Array.<number>}
602 value: function(value)
605 var count = this.length;
607 var step = count >> 1;
608 var middle = first + step;
609 if (value >= this[middle]) {
619 Array.diff = function(left, right)
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);
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);
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] };
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 };
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 };
661 return { left: o, right: n };
664 Array.convert = function(list)
666 // Cast array-like object to an array.
667 return Array.prototype.slice.call(list);
670 String.sprintf = function(format)
672 return String.vsprintf(format, Array.prototype.slice.call(arguments, 1));
675 String.tokenizeFormatString = function(format)
678 var substitutionIndex = 0;
680 function addStringToken(str)
682 tokens.push({ type: "string", value: str });
685 function addSpecifierToken(specifier, precision, substitutionIndex)
687 tokens.push({ type: "specifier", specifier: specifier, precision: precision, substitutionIndex: substitutionIndex });
691 for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
692 addStringToken(format.substring(index, precentIndex));
693 index = precentIndex + 1;
695 if (format[index] === "%") {
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]))
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);
715 if (format[index] === ".") {
716 // This is a precision specifier. If no digit follows the ".",
717 // then the precision should be zero.
719 precision = parseInt(format.substring(index), 10);
720 if (isNaN(precision))
722 while (!isNaN(format[index]))
726 addSpecifierToken(format[index], precision, substitutionIndex);
732 addStringToken(format.substring(index));
737 String.standardFormatters = {
738 d: function(substitution)
740 return !isNaN(substitution) ? substitution : 0;
743 f: function(substitution, token)
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);
750 s: function(substitution)
756 String.vsprintf = function(format, substitutions)
758 return String.format(format, substitutions, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
761 String.format = function(format, substitutions, formatters, initialValue, append)
763 if (!format || !substitutions || !substitutions.length)
764 return { formattedResult: append(initialValue, format), unusedSubstitutions: substitutions };
766 function prettyFunctionName()
768 return "String.format(\"" + format + "\", \"" + substitutions.join("\", \"") + "\")";
773 console.warn(prettyFunctionName() + ": " + msg);
778 console.error(prettyFunctionName() + ": " + msg);
781 var result = initialValue;
782 var tokens = String.tokenizeFormatString(format);
783 var usedSubstitutionIndexes = {};
785 for (var i = 0; i < tokens.length; ++i) {
786 var token = tokens[i];
788 if (token.type === "string") {
789 result = append(result, token.value);
793 if (token.type !== "specifier") {
794 error("Unknown token type \"" + token.type + "\" found.");
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);
806 usedSubstitutionIndexes[token.substitutionIndex] = true;
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]);
815 result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token));
818 var unusedSubstitutions = [];
819 for (var i = 0; i < substitutions.length; ++i) {
820 if (i in usedSubstitutionIndexes)
822 unusedSubstitutions.push(substitutions[i]);
825 return { formattedResult: result, unusedSubstitutions: unusedSubstitutions };
828 } // setupPrototypeUtilities()
830 setupPrototypeUtilities();
832 function isEnterKey(event) {
834 return event.keyCode !== 229 && event.keyIdentifier === "Enter";
837 function highlightSearchResult(element, offset, length, domChanges)
839 var result = highlightSearchResults(element, [{offset: offset, length: length }], domChanges);
840 return result.length ? result[0] : null;
844 * @param {Array.<Object>=} changes
846 function highlightSearchResults(element, resultRanges, changes)
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);
854 var snapshotLength = textNodeSnapshot.snapshotLength;
855 var snapshotNodeOffset = 0;
856 var currentSnapshotItem = 0;
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;
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;
876 snapshotNodeOffset += textNodeLength;
884 var highlightNode = ownerDocument.createElement("span");
885 highlightNode.className = "webkit-search-result";
886 highlightNode.textContent = lineText.substring(startOffset, endOffset);
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 });
894 textNode.parentElement.insertBefore(highlightNode, textNode);
895 changes.push({ node: highlightNode, type: "added", nextSibling: textNode, parent: textNode.parentElement });
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);
904 var parentElement = textNode.parentElement;
905 var anchorElement = textNode.nextSibling;
907 length -= text.length - textNodeOffset;
908 textNode.textContent = text.substring(0, textNodeOffset);
909 changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
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 });
921 length -= text.length;
922 textNode.textContent = "";
923 changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
926 parentElement.insertBefore(highlightNode, anchorElement);
927 changes.push({ node: highlightNode, type: "added", nextSibling: anchorElement, parent: parentElement });
928 highlightNodes.push(highlightNode);
931 return highlightNodes;
934 function applyDomChanges(domChanges)
936 for (var i = 0, size = domChanges.length; i < size; ++i) {
937 var entry = domChanges[i];
938 switch (entry.type) {
940 entry.parent.insertBefore(entry.node, entry.nextSibling);
943 entry.node.textContent = entry.newText;
949 function revertDomChanges(domChanges)
951 for (var i = 0, size = domChanges.length; i < size; ++i) {
952 var entry = domChanges[i];
953 switch (entry.type) {
955 if (entry.node.parentElement)
956 entry.node.parentElement.removeChild(entry.node);
959 entry.node.textContent = entry.oldText;
965 function createSearchRegex(query, extraFlags)
967 // This should be kept the same as the one in InspectorPageAgent.cpp.
968 var regexSpecialCharacters = "[](){}+-*.,?\\^$|";
970 for (var i = 0; i < query.length; ++i) {
971 var c = query.charAt(i);
972 if (regexSpecialCharacters.indexOf(c) != -1)
976 return new RegExp(regex, "i" + (extraFlags || ""));
979 function countRegexMatches(regex, content)
984 while (text && (match = regex.exec(text))) {
985 if (match[0].length > 0)
987 text = text.substring(match.index + 1);