| /* |
| * Copyright (C) 2013 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| var emDash = "\u2014"; |
| var enDash = "\u2013"; |
| var figureDash = "\u2012"; |
| var ellipsis = "\u2026"; |
| var zeroWidthSpace = "\u200b"; |
| var multiplicationSign = "\u00d7"; |
| |
| function xor(a, b) |
| { |
| if (a) |
| return b ? false : a; |
| return b || false; |
| } |
| |
| Object.defineProperty(Object, "shallowCopy", |
| { |
| value(object) |
| { |
| // Make a new object and copy all the key/values. The values are not copied. |
| var copy = {}; |
| var keys = Object.keys(object); |
| for (var i = 0; i < keys.length; ++i) |
| copy[keys[i]] = object[keys[i]]; |
| return copy; |
| } |
| }); |
| |
| Object.defineProperty(Object, "shallowEqual", |
| { |
| value(a, b) |
| { |
| // Checks if two objects have the same top-level properties. |
| |
| if (!(a instanceof Object) || !(b instanceof Object)) |
| return false; |
| |
| if (a === b) |
| return true; |
| |
| if (Array.shallowEqual(a, b)) |
| return true; |
| |
| if (a.constructor !== b.constructor) |
| return false; |
| |
| let aKeys = Object.keys(a); |
| let bKeys = Object.keys(b); |
| if (aKeys.length !== bKeys.length) |
| return false; |
| |
| for (let aKey of aKeys) { |
| if (!(aKey in b)) |
| return false; |
| |
| let aValue = a[aKey]; |
| let bValue = b[aKey]; |
| if (aValue !== bValue && !Array.shallowEqual(aValue, bValue)) |
| return false; |
| } |
| |
| return true; |
| } |
| }); |
| |
| Object.defineProperty(Object, "filter", |
| { |
| value(object, callback) |
| { |
| let filtered = {}; |
| for (let key in object) { |
| if (callback(key, object[key])) |
| filtered[key] = object[key]; |
| } |
| return filtered; |
| } |
| }); |
| |
| Object.defineProperty(Object.prototype, "valueForCaseInsensitiveKey", |
| { |
| value(key) |
| { |
| if (this.hasOwnProperty(key)) |
| return this[key]; |
| |
| var lowerCaseKey = key.toLowerCase(); |
| for (var currentKey in this) { |
| if (currentKey.toLowerCase() === lowerCaseKey) |
| return this[currentKey]; |
| } |
| |
| return undefined; |
| } |
| }); |
| |
| Object.defineProperty(Map, "fromObject", |
| { |
| value(object) |
| { |
| let map = new Map; |
| for (let key in object) |
| map.set(key, object[key]); |
| return map; |
| } |
| }); |
| |
| Object.defineProperty(Map.prototype, "take", |
| { |
| value(key) |
| { |
| let deletedValue = this.get(key); |
| this.delete(key); |
| return deletedValue; |
| } |
| }); |
| |
| Object.defineProperty(Map.prototype, "getOrInitialize", |
| { |
| value(key, initialValue) |
| { |
| console.assert(initialValue !== undefined, "getOrInitialize should not be used with undefined."); |
| |
| let value = this.get(key); |
| if (value) |
| return value; |
| |
| this.set(key, initialValue); |
| return initialValue; |
| } |
| }); |
| |
| Object.defineProperty(Set.prototype, "find", |
| { |
| value(predicate) |
| { |
| for (let item of this) { |
| if (predicate(item, this)) |
| return item; |
| } |
| return undefined; |
| }, |
| }); |
| |
| Object.defineProperty(Set.prototype, "addAll", |
| { |
| value(iterable) |
| { |
| for (let item of iterable) |
| this.add(item); |
| }, |
| }); |
| |
| Object.defineProperty(Set.prototype, "take", |
| { |
| value(key) |
| { |
| if (this.has(key)) { |
| this.delete(key); |
| return key; |
| } |
| |
| return undefined; |
| } |
| }); |
| |
| Object.defineProperty(Set.prototype, "equals", |
| { |
| value(other) |
| { |
| return this.size === other.size && this.isSubsetOf(other); |
| } |
| }); |
| |
| Object.defineProperty(Set.prototype, "difference", |
| { |
| value(other) |
| { |
| if (other === this) |
| return new Set; |
| |
| let result = new Set; |
| for (let item of this) { |
| if (!other.has(item)) |
| result.add(item); |
| } |
| |
| return result; |
| } |
| }); |
| |
| Object.defineProperty(Set.prototype, "firstValue", |
| { |
| get() |
| { |
| return this.values().next().value; |
| } |
| }); |
| |
| Object.defineProperty(Set.prototype, "lastValue", |
| { |
| get() |
| { |
| return Array.from(this.values()).lastValue; |
| } |
| }); |
| |
| Object.defineProperty(Set.prototype, "intersects", |
| { |
| value(other) |
| { |
| if (!this.size || !other.size) |
| return false; |
| |
| for (let item of this) { |
| if (other.has(item)) |
| return true; |
| } |
| |
| return false; |
| } |
| }); |
| |
| Object.defineProperty(Set.prototype, "isSubsetOf", |
| { |
| value(other) |
| { |
| for (let item of this) { |
| if (!other.has(item)) |
| return false; |
| } |
| |
| return true; |
| } |
| }); |
| |
| Object.defineProperty(Node.prototype, "traverseNextNode", |
| { |
| value(stayWithin) |
| { |
| var node = this.firstChild; |
| if (node) |
| return node; |
| |
| if (stayWithin && this === stayWithin) |
| return null; |
| |
| node = this.nextSibling; |
| if (node) |
| return node; |
| |
| node = this; |
| while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin)) |
| node = node.parentNode; |
| if (!node) |
| return null; |
| |
| return node.nextSibling; |
| } |
| }); |
| |
| Object.defineProperty(Node.prototype, "traversePreviousNode", |
| { |
| value(stayWithin) |
| { |
| if (stayWithin && this === stayWithin) |
| return null; |
| var node = this.previousSibling; |
| while (node && node.lastChild) |
| node = node.lastChild; |
| if (node) |
| return node; |
| return this.parentNode; |
| } |
| }); |
| |
| |
| Object.defineProperty(Node.prototype, "rangeOfWord", |
| { |
| value(offset, stopCharacters, stayWithinNode, direction) |
| { |
| var startNode; |
| var startOffset = 0; |
| var endNode; |
| var endOffset = 0; |
| |
| if (!stayWithinNode) |
| stayWithinNode = this; |
| |
| if (!direction || direction === "backward" || direction === "both") { |
| var node = this; |
| while (node) { |
| if (node === stayWithinNode) { |
| if (!startNode) |
| startNode = stayWithinNode; |
| break; |
| } |
| |
| if (node.nodeType === Node.TEXT_NODE) { |
| let start = node === this ? (offset - 1) : (node.nodeValue.length - 1); |
| for (var i = start; i >= 0; --i) { |
| if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { |
| startNode = node; |
| startOffset = i + 1; |
| break; |
| } |
| } |
| } |
| |
| if (startNode) |
| break; |
| |
| node = node.traversePreviousNode(stayWithinNode); |
| } |
| |
| if (!startNode) { |
| startNode = stayWithinNode; |
| startOffset = 0; |
| } |
| } else { |
| startNode = this; |
| startOffset = offset; |
| } |
| |
| if (!direction || direction === "forward" || direction === "both") { |
| node = this; |
| while (node) { |
| if (node === stayWithinNode) { |
| if (!endNode) |
| endNode = stayWithinNode; |
| break; |
| } |
| |
| if (node.nodeType === Node.TEXT_NODE) { |
| let start = node === this ? offset : 0; |
| for (var i = start; i < node.nodeValue.length; ++i) { |
| if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { |
| endNode = node; |
| endOffset = i; |
| break; |
| } |
| } |
| } |
| |
| if (endNode) |
| break; |
| |
| node = node.traverseNextNode(stayWithinNode); |
| } |
| |
| if (!endNode) { |
| endNode = stayWithinNode; |
| endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length; |
| } |
| } else { |
| endNode = this; |
| endOffset = offset; |
| } |
| |
| var result = this.ownerDocument.createRange(); |
| result.setStart(startNode, startOffset); |
| result.setEnd(endNode, endOffset); |
| |
| return result; |
| |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "realOffsetWidth", |
| { |
| get() |
| { |
| return this.getBoundingClientRect().width; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "realOffsetHeight", |
| { |
| get() |
| { |
| return this.getBoundingClientRect().height; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "totalOffsetLeft", |
| { |
| get() |
| { |
| return this.getBoundingClientRect().left; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "totalOffsetRight", |
| { |
| get() |
| { |
| return this.getBoundingClientRect().right; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "totalOffsetTop", |
| { |
| get() |
| { |
| return this.getBoundingClientRect().top; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "totalOffsetBottom", |
| { |
| get() |
| { |
| return this.getBoundingClientRect().bottom; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "removeChildren", |
| { |
| value() |
| { |
| // This has been tested to be the fastest removal method. |
| if (this.firstChild) |
| this.textContent = ""; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "isInsertionCaretInside", |
| { |
| value() |
| { |
| var selection = window.getSelection(); |
| if (!selection.rangeCount || !selection.isCollapsed) |
| return false; |
| var selectionRange = selection.getRangeAt(0); |
| return selectionRange.startContainer === this || this.contains(selectionRange.startContainer); |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "createChild", |
| { |
| value(elementName, className) |
| { |
| var element = this.ownerDocument.createElement(elementName); |
| if (className) |
| element.className = className; |
| this.appendChild(element); |
| return element; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "isScrolledToBottom", |
| { |
| value() |
| { |
| // This code works only for 0-width border |
| return this.scrollTop + this.clientHeight === this.scrollHeight; |
| } |
| }); |
| |
| Object.defineProperty(Element.prototype, "recalculateStyles", |
| { |
| value() |
| { |
| this.ownerDocument.defaultView.getComputedStyle(this); |
| } |
| }); |
| |
| Object.defineProperty(DocumentFragment.prototype, "createChild", |
| { |
| value: Element.prototype.createChild |
| }); |
| |
| (function() { |
| const fontSymbol = Symbol("font"); |
| |
| Object.defineProperty(HTMLInputElement.prototype, "autosize", |
| { |
| value(extra = 0) |
| { |
| extra += 6; // UserAgent styles add 1px padding and 2px border. |
| if (this.type === "number") |
| extra += 13; // Number input inner spin button width. |
| extra += 2; // Add extra pixels for the cursor. |
| |
| WI.ImageUtilities.scratchCanvasContext2D((context) => { |
| this[fontSymbol] ||= window.getComputedStyle(this).font; |
| |
| context.font = this[fontSymbol]; |
| let textMetrics = context.measureText(this.value || this.placeholder); |
| this.style.setProperty("width", (textMetrics.width + extra) + "px"); |
| }); |
| }, |
| }); |
| })(); |
| |
| Object.defineProperty(Event.prototype, "stop", |
| { |
| value() |
| { |
| this.stopImmediatePropagation(); |
| this.preventDefault(); |
| } |
| }); |
| |
| Object.defineProperty(KeyboardEvent.prototype, "commandOrControlKey", |
| { |
| get() |
| { |
| return WI.Platform.name === "mac" ? this.metaKey : this.ctrlKey; |
| } |
| }); |
| |
| Object.defineProperty(MouseEvent.prototype, "commandOrControlKey", |
| { |
| get() |
| { |
| return WI.Platform.name === "mac" ? this.metaKey : this.ctrlKey; |
| } |
| }); |
| |
| Object.defineProperty(Array, "isTypedArray", |
| { |
| value(array) |
| { |
| if (!array) |
| return false; |
| |
| let constructor = array.constructor; |
| return constructor === Int8Array |
| || constructor === Int16Array |
| || constructor === Int32Array |
| || constructor === Uint8Array |
| || constructor === Uint8ClampedArray |
| || constructor === Uint16Array |
| || constructor === Uint32Array |
| || constructor === Float32Array |
| || constructor === Float64Array; |
| } |
| }); |
| |
| Object.defineProperty(Array, "shallowEqual", |
| { |
| value(a, b) |
| { |
| function isArrayLike(x) { |
| return Array.isArray(x) || Array.isTypedArray(x); |
| } |
| |
| if (!isArrayLike(a) || !isArrayLike(b)) |
| return false; |
| |
| if (a === b) |
| return true; |
| |
| let length = a.length; |
| |
| if (length !== b.length) |
| return false; |
| |
| for (let i = 0; i < length; ++i) { |
| if (a[i] === b[i]) |
| continue; |
| |
| if (!Object.shallowEqual(a[i], b[i])) |
| return false; |
| } |
| |
| return true; |
| } |
| }); |
| |
| Object.defineProperty(Array, "diffArrays", |
| { |
| value(initialArray, currentArray, onEach, comparator) |
| { |
| "use strict"; |
| |
| function defaultComparator(initial, current) { |
| return initial === current; |
| } |
| comparator = comparator || defaultComparator; |
| |
| // Find the shortest prefix of matching items in both arrays. |
| // |
| // initialArray = ["a", "b", "b", "c"] |
| // currentArray = ["c", "b", "b", "a"] |
| // findShortestEdit() // [1, 1] |
| // |
| function findShortestEdit() { |
| let deletionCount = initialArray.length; |
| let additionCount = currentArray.length; |
| let editCount = deletionCount + additionCount; |
| for (let i = 0; i < initialArray.length; ++i) { |
| if (i > editCount) { |
| // Break since any possible edits at this point are going to be longer than the one already found. |
| break; |
| } |
| |
| for (let j = 0; j < currentArray.length; ++j) { |
| let newEditCount = i + j; |
| if (newEditCount > editCount) { |
| // Break since any possible edits at this point are going to be longer than the one already found. |
| break; |
| } |
| |
| if (comparator(initialArray[i], currentArray[j])) { |
| // A candidate for the shortest edit found. |
| if (newEditCount < editCount) { |
| editCount = newEditCount; |
| deletionCount = i; |
| additionCount = j; |
| } |
| break; |
| } |
| } |
| } |
| return [deletionCount, additionCount]; |
| } |
| |
| function commonPrefixLength(listA, listB) { |
| let shorterListLength = Math.min(listA.length, listB.length); |
| let i = 0; |
| while (i < shorterListLength) { |
| if (!comparator(listA[i], listB[i])) |
| break; |
| ++i; |
| } |
| return i; |
| } |
| |
| function fireOnEach(count, diffAction, array) { |
| for (let i = 0; i < count; ++i) |
| onEach(array[i], diffAction); |
| } |
| |
| while (initialArray.length || currentArray.length) { |
| // Remove common prefix. |
| let prefixLength = commonPrefixLength(initialArray, currentArray); |
| if (prefixLength) { |
| fireOnEach(prefixLength, 0, currentArray); |
| initialArray = initialArray.slice(prefixLength); |
| currentArray = currentArray.slice(prefixLength); |
| } |
| |
| if (!initialArray.length && !currentArray.length) |
| break; |
| |
| let [deletionCount, additionCount] = findShortestEdit(); |
| fireOnEach(deletionCount, -1, initialArray); |
| fireOnEach(additionCount, 1, currentArray); |
| initialArray = initialArray.slice(deletionCount); |
| currentArray = currentArray.slice(additionCount); |
| } |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "lastValue", |
| { |
| get() |
| { |
| if (!this.length) |
| return undefined; |
| return this[this.length - 1]; |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "adjacencies", |
| { |
| value: function*() { |
| for (let i = 1; i < this.length; ++i) |
| yield [this[i - 1], this[i]]; |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "remove", |
| { |
| value(value) |
| { |
| for (let i = 0; i < this.length; ++i) { |
| if (this[i] === value) { |
| this.splice(i, 1); |
| return true; |
| } |
| } |
| return false; |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "removeAll", |
| { |
| value(value) |
| { |
| for (let i = this.length - 1; i >= 0; --i) { |
| if (this[i] === value) |
| this.splice(i, 1); |
| } |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "toggleIncludes", |
| { |
| value(value, force) |
| { |
| let exists = this.includes(value); |
| if (exists === !!force) |
| return; |
| |
| if (exists) |
| this.remove(value); |
| else |
| this.push(value); |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "insertAtIndex", |
| { |
| value(value, index) |
| { |
| this.splice(index, 0, value); |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "pushAll", |
| { |
| value(iterable) |
| { |
| for (let item of iterable) |
| this.push(item); |
| }, |
| }); |
| |
| Object.defineProperty(Array.prototype, "partition", |
| { |
| value(callback) |
| { |
| let positive = []; |
| let negative = []; |
| for (let i = 0; i < this.length; ++i) { |
| let value = this[i]; |
| if (callback(value)) |
| positive.push(value); |
| else |
| negative.push(value); |
| } |
| return [positive, negative]; |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "isLowerCase", |
| { |
| value() |
| { |
| return String(this) === this.toLowerCase(); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "isUpperCase", |
| { |
| value() |
| { |
| return String(this) === this.toUpperCase(); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "isJSON", |
| { |
| value(predicate) |
| { |
| try { |
| let json = JSON.parse(this); |
| return !predicate || predicate(json); |
| } catch { } |
| return false; |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "truncateStart", |
| { |
| value(maxLength) |
| { |
| "use strict"; |
| |
| if (this.length <= maxLength) |
| return this; |
| return ellipsis + this.substr(this.length - maxLength + 1); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "truncateMiddle", |
| { |
| value(maxLength) |
| { |
| "use strict"; |
| |
| if (this.length <= maxLength) |
| return this; |
| var leftHalf = maxLength >> 1; |
| var rightHalf = maxLength - leftHalf - 1; |
| return this.substr(0, leftHalf) + ellipsis + this.substr(this.length - rightHalf, rightHalf); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "truncateEnd", |
| { |
| value(maxLength) |
| { |
| "use strict"; |
| |
| if (this.length <= maxLength) |
| return this; |
| return this.substr(0, maxLength - 1) + ellipsis; |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "truncate", |
| { |
| value(maxLength) |
| { |
| "use strict"; |
| |
| if (this.length <= maxLength) |
| return this; |
| |
| let clipped = this.slice(0, maxLength); |
| let indexOfLastWhitespace = clipped.search(/\s\S*$/); |
| if (indexOfLastWhitespace > Math.floor(maxLength / 2)) |
| clipped = clipped.slice(0, indexOfLastWhitespace - 1); |
| |
| return clipped + ellipsis; |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "collapseWhitespace", |
| { |
| value() |
| { |
| return this.replace(/[\s\xA0]+/g, " "); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "removeWhitespace", |
| { |
| value() |
| { |
| return this.replace(/[\s\xA0]+/g, ""); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "escapeCharacters", |
| { |
| value(charactersToEscape) |
| { |
| if (!charactersToEscape) |
| return this.valueOf(); |
| |
| let charactersToEscapeSet = new Set(charactersToEscape); |
| |
| let foundCharacter = false; |
| for (let c of this) { |
| if (!charactersToEscapeSet.has(c)) |
| continue; |
| foundCharacter = true; |
| break; |
| } |
| |
| if (!foundCharacter) |
| return this.valueOf(); |
| |
| let result = ""; |
| for (let c of this) { |
| if (charactersToEscapeSet.has(c)) |
| result += "\\"; |
| result += c; |
| } |
| |
| return result.valueOf(); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "escapeForRegExp", |
| { |
| value() |
| { |
| return this.escapeCharacters("^[]{}()\\.$*+?|"); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "capitalize", |
| { |
| value() |
| { |
| return this.charAt(0).toUpperCase() + this.slice(1); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "extendedLocaleCompare", |
| { |
| value(other) |
| { |
| return this.localeCompare(other, undefined, {numeric: true}); |
| } |
| }); |
| |
| Object.defineProperty(String, "tokenizeFormatString", |
| { |
| value(format) |
| { |
| var tokens = []; |
| var substitutionIndex = 0; |
| |
| function addStringToken(str) |
| { |
| tokens.push({type: "string", value: str}); |
| } |
| |
| function addSpecifierToken(specifier, precision, substitutionIndex) |
| { |
| tokens.push({type: "specifier", specifier, precision, substitutionIndex}); |
| } |
| |
| var index = 0; |
| for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) { |
| addStringToken(format.substring(index, precentIndex)); |
| index = precentIndex + 1; |
| |
| if (format[index] === "%") { |
| addStringToken("%"); |
| ++index; |
| continue; |
| } |
| |
| if (!isNaN(format[index])) { |
| // The first character is a number, it might be a substitution index. |
| var number = parseInt(format.substring(index), 10); |
| while (!isNaN(format[index])) |
| ++index; |
| |
| // If the number is greater than zero and ends with a "$", |
| // then this is a substitution index. |
| if (number > 0 && format[index] === "$") { |
| substitutionIndex = (number - 1); |
| ++index; |
| } |
| } |
| |
| const defaultPrecision = 6; |
| |
| let precision = defaultPrecision; |
| if (format[index] === ".") { |
| // This is a precision specifier. If no digit follows the ".", |
| // then use the default precision of six digits (ISO C99 specification). |
| ++index; |
| |
| precision = parseInt(format.substring(index), 10); |
| if (isNaN(precision)) |
| precision = defaultPrecision; |
| |
| while (!isNaN(format[index])) |
| ++index; |
| } |
| |
| addSpecifierToken(format[index], precision, substitutionIndex); |
| |
| ++substitutionIndex; |
| ++index; |
| } |
| |
| addStringToken(format.substring(index)); |
| |
| return tokens; |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "lineCount", |
| { |
| get() |
| { |
| "use strict"; |
| |
| let lineCount = 1; |
| let index = 0; |
| while (true) { |
| index = this.indexOf("\n", index); |
| if (index === -1) |
| return lineCount; |
| |
| index += "\n".length; |
| lineCount++; |
| } |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "lastLine", |
| { |
| get() |
| { |
| "use strict"; |
| |
| let index = this.lastIndexOf("\n"); |
| if (index === -1) |
| return this; |
| |
| return this.slice(index + "\n".length); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "hash", |
| { |
| get() |
| { |
| // Matches the wtf/Hasher.h (SuperFastHash) algorithm. |
| |
| // Arbitrary start value to avoid mapping all 0's to all 0's. |
| const stringHashingStartValue = 0x9e3779b9; |
| |
| var result = stringHashingStartValue; |
| var pendingCharacter = null; |
| for (var i = 0; i < this.length; ++i) { |
| var currentCharacter = this[i].charCodeAt(0); |
| if (pendingCharacter === null) { |
| pendingCharacter = currentCharacter; |
| continue; |
| } |
| |
| result += pendingCharacter; |
| result = (result << 16) ^ ((currentCharacter << 11) ^ result); |
| result += result >> 11; |
| |
| pendingCharacter = null; |
| } |
| |
| // Handle the last character in odd length strings. |
| if (pendingCharacter !== null) { |
| result += pendingCharacter; |
| result ^= result << 11; |
| result += result >> 17; |
| } |
| |
| // Force "avalanching" of final 31 bits. |
| result ^= result << 3; |
| result += result >> 5; |
| result ^= result << 2; |
| result += result >> 15; |
| result ^= result << 10; |
| |
| // Prevent 0 and negative results. |
| return (0xffffffff + result + 1).toString(36); |
| } |
| }); |
| |
| Object.defineProperty(String, "standardFormatters", |
| { |
| value: { |
| d: function(substitution) |
| { |
| return parseInt(substitution).toLocaleString(); |
| }, |
| |
| f: function(substitution, token) |
| { |
| let value = parseFloat(substitution); |
| if (isNaN(value)) |
| return NaN; |
| |
| let options = { |
| minimumFractionDigits: token.precision, |
| maximumFractionDigits: token.precision, |
| useGrouping: false |
| }; |
| return value.toLocaleString(undefined, options); |
| }, |
| |
| s: function(substitution) |
| { |
| return substitution; |
| } |
| } |
| }); |
| |
| Object.defineProperty(String, "format", |
| { |
| value(format, substitutions, formatters, initialValue, append) |
| { |
| if (!format || !substitutions || !substitutions.length) |
| return {formattedResult: append(initialValue, format), unusedSubstitutions: substitutions}; |
| |
| function prettyFunctionName() |
| { |
| return "String.format(\"" + format + "\", \"" + Array.from(substitutions).join("\", \"") + "\")"; |
| } |
| |
| function warn(msg) |
| { |
| console.warn(prettyFunctionName() + ": " + msg); |
| } |
| |
| function error(msg) |
| { |
| console.error(prettyFunctionName() + ": " + msg); |
| } |
| |
| var result = initialValue; |
| var tokens = String.tokenizeFormatString(format); |
| var usedSubstitutionIndexes = {}; |
| |
| for (var i = 0; i < tokens.length; ++i) { |
| var token = tokens[i]; |
| |
| if (token.type === "string") { |
| result = append(result, token.value); |
| continue; |
| } |
| |
| if (token.type !== "specifier") { |
| error("Unknown token type \"" + token.type + "\" found."); |
| continue; |
| } |
| |
| if (token.substitutionIndex >= substitutions.length) { |
| // If there are not enough substitutions for the current substitutionIndex |
| // just output the format specifier literally and move on. |
| error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (token.substitutionIndex + 1) + ", so substitution was skipped."); |
| result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier); |
| continue; |
| } |
| |
| usedSubstitutionIndexes[token.substitutionIndex] = true; |
| |
| if (!(token.specifier in formatters)) { |
| // Encountered an unsupported format character, treat as a string. |
| warn("unsupported format character \u201C" + token.specifier + "\u201D. Treating as a string."); |
| result = append(result, substitutions[token.substitutionIndex]); |
| continue; |
| } |
| |
| result = append(result, formatters[token.specifier](substitutions[token.substitutionIndex], token)); |
| } |
| |
| var unusedSubstitutions = []; |
| for (var i = 0; i < substitutions.length; ++i) { |
| if (i in usedSubstitutionIndexes) |
| continue; |
| unusedSubstitutions.push(substitutions[i]); |
| } |
| |
| return {formattedResult: result, unusedSubstitutions}; |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "format", |
| { |
| value() |
| { |
| return String.format(this, arguments, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult; |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "insertWordBreakCharacters", |
| { |
| value() |
| { |
| // Add zero width spaces after characters that are good to break after. |
| // Otherwise a string with no spaces will not break and overflow its container. |
| // This is mainly used on URL strings, so the characters are tailored for URLs. |
| return this.replace(/([\/;:\)\]\}&?])/g, "$1\u200b"); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "removeWordBreakCharacters", |
| { |
| value() |
| { |
| // Undoes what insertWordBreakCharacters did. |
| return this.replace(/\u200b/g, ""); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "levenshteinDistance", |
| { |
| value(s) |
| { |
| var m = this.length; |
| var n = s.length; |
| var d = new Array(m + 1); |
| |
| for (var i = 0; i <= m; ++i) { |
| d[i] = new Array(n + 1); |
| d[i][0] = i; |
| } |
| |
| for (var j = 0; j <= n; ++j) |
| d[0][j] = j; |
| |
| for (var j = 1; j <= n; ++j) { |
| for (var i = 1; i <= m; ++i) { |
| if (this[i - 1] === s[j - 1]) |
| d[i][j] = d[i - 1][j - 1]; |
| else { |
| var deletion = d[i - 1][j] + 1; |
| var insertion = d[i][j - 1] + 1; |
| var substitution = d[i - 1][j - 1] + 1; |
| d[i][j] = Math.min(deletion, insertion, substitution); |
| } |
| } |
| } |
| |
| return d[m][n]; |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "toCamelCase", |
| { |
| value() |
| { |
| return this.toLowerCase().replace(/[^\w]+(\w)/g, (match, group) => group.toUpperCase()); |
| } |
| }); |
| |
| Object.defineProperty(String.prototype, "hasMatchingEscapedQuotes", |
| { |
| value() |
| { |
| return /^\"(?:[^\"\\]|\\.)*\"$/.test(this) || /^\'(?:[^\'\\]|\\.)*\'$/.test(this); |
| } |
| }); |
| |
| Object.defineProperty(Math, "roundTo", |
| { |
| value(num, step) |
| { |
| return Math.round(num / step) * step; |
| } |
| }); |
| |
| // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web#Multiplying_a_matrix_and_a_point |
| Object.defineProperty(Math, "multiplyMatrixByVector", |
| { |
| value(matrix, vector) |
| { |
| let height = matrix.length; |
| let width = matrix[0].length; |
| console.assert(width === vector.length); |
| |
| let result = Array(width).fill(0); |
| for (let i = 0; i < width; ++i) { |
| for (let rowIndex = 0; rowIndex < height; ++rowIndex) |
| result[i] += vector[rowIndex] * matrix[i][rowIndex]; |
| } |
| |
| return result; |
| } |
| }); |
| |
| Object.defineProperty(Number, "constrain", |
| { |
| value(num, min, max) |
| { |
| if (isNaN(num) || max < min) |
| return min; |
| |
| if (num < min) |
| num = min; |
| else if (num > max) |
| num = max; |
| return num; |
| } |
| }); |
| |
| Object.defineProperty(Number, "percentageString", |
| { |
| value(fraction, precision = 1) |
| { |
| return fraction.toLocaleString(undefined, {minimumFractionDigits: precision, style: "percent"}); |
| } |
| }); |
| |
| Object.defineProperty(Number, "secondsToMillisecondsString", |
| { |
| value(seconds, higherResolution) |
| { |
| let ms = seconds * 1000; |
| |
| if (higherResolution) |
| return WI.UIString("%.2fms").format(ms); |
| return WI.UIString("%.1fms").format(ms); |
| } |
| }); |
| |
| Object.defineProperty(Number, "secondsToString", |
| { |
| value(seconds, higherResolution) |
| { |
| const epsilon = 0.0001; |
| |
| let ms = seconds * 1000; |
| if (ms < epsilon) |
| return WI.UIString("%.0fms").format(0); |
| |
| if (Math.abs(ms) < (10 + epsilon)) { |
| if (higherResolution) |
| return WI.UIString("%.3fms").format(ms); |
| return WI.UIString("%.2fms").format(ms); |
| } |
| |
| if (Math.abs(ms) < (100 + epsilon)) { |
| if (higherResolution) |
| return WI.UIString("%.2fms").format(ms); |
| return WI.UIString("%.1fms").format(ms); |
| } |
| |
| if (Math.abs(ms) < (1000 + epsilon)) { |
| if (higherResolution) |
| return WI.UIString("%.1fms").format(ms); |
| return WI.UIString("%.0fms").format(ms); |
| } |
| |
| // Do not go over seconds when in high resolution mode. |
| if (higherResolution || Math.abs(seconds) < 60) |
| return WI.UIString("%.2fs").format(seconds); |
| |
| let minutes = seconds / 60; |
| if (Math.abs(minutes) < 60) |
| return WI.UIString("%.1fmin").format(minutes); |
| |
| let hours = minutes / 60; |
| if (Math.abs(hours) < 24) |
| return WI.UIString("%.1fhrs").format(hours); |
| |
| let days = hours / 24; |
| return WI.UIString("%.1f days").format(days); |
| } |
| }); |
| |
| Object.defineProperty(Number, "bytesToString", |
| { |
| value(bytes, higherResolution, bytesThreshold) |
| { |
| higherResolution ??= true; |
| bytesThreshold ??= 1000; |
| |
| if (Math.abs(bytes) < bytesThreshold) |
| return WI.UIString("%.0f B").format(bytes); |
| |
| let kilobytes = bytes / 1000; |
| if (Math.abs(kilobytes) < 1000) { |
| if (higherResolution || Math.abs(kilobytes) < 10) |
| return WI.UIString("%.2f KB").format(kilobytes); |
| return WI.UIString("%.1f KB").format(kilobytes); |
| } |
| |
| let megabytes = kilobytes / 1000; |
| if (Math.abs(megabytes) < 1000) { |
| if (higherResolution || Math.abs(megabytes) < 10) |
| return WI.UIString("%.2f MB").format(megabytes); |
| return WI.UIString("%.1f MB").format(megabytes); |
| } |
| |
| let gigabytes = megabytes / 1000; |
| if (higherResolution || Math.abs(gigabytes) < 10) |
| return WI.UIString("%.2f GB").format(gigabytes); |
| return WI.UIString("%.1f GB").format(gigabytes); |
| } |
| }); |
| |
| Object.defineProperty(Number, "abbreviate", |
| { |
| value(num) |
| { |
| if (num < 1000) |
| return num.toLocaleString(); |
| |
| if (num < 1_000_000) |
| return WI.UIString("%.1fK").format(Math.round(num / 100) / 10); |
| |
| if (num < 1_000_000_000) |
| return WI.UIString("%.1fM").format(Math.round(num / 100_000) / 10); |
| |
| return WI.UIString("%.1fB").format(Math.round(num / 100_000_000) / 10); |
| } |
| }); |
| |
| Object.defineProperty(Number, "zeroPad", |
| { |
| value(num, length) |
| { |
| let string = num.toLocaleString(); |
| return string.padStart(length, "0"); |
| }, |
| }); |
| |
| Object.defineProperty(Number, "countDigits", |
| { |
| value(num) |
| { |
| if (num === 0) |
| return 1; |
| |
| num = Math.abs(num); |
| return Math.floor(Math.log(num) * Math.LOG10E) + 1; |
| } |
| }); |
| |
| Object.defineProperty(Number.prototype, "maxDecimals", |
| { |
| value(decimals) |
| { |
| let power = 10 ** decimals; |
| return Math.round(this * power) / power; |
| } |
| }); |
| |
| Object.defineProperty(Uint32Array, "isLittleEndian", |
| { |
| value() |
| { |
| if ("_isLittleEndian" in this) |
| return this._isLittleEndian; |
| |
| var buffer = new ArrayBuffer(4); |
| var longData = new Uint32Array(buffer); |
| var data = new Uint8Array(buffer); |
| |
| longData[0] = 0x0a0b0c0d; |
| |
| this._isLittleEndian = data[0] === 0x0d && data[1] === 0x0c && data[2] === 0x0b && data[3] === 0x0a; |
| |
| return this._isLittleEndian; |
| } |
| }); |
| |
| function isEmptyObject(object) |
| { |
| for (var property in object) |
| return false; |
| return true; |
| } |
| |
| function isEnterKey(event) |
| { |
| // Check if this is an IME event. |
| return event.keyCode !== 229 && event.keyIdentifier === "Enter"; |
| } |
| |
| function resolveDotsInPath(path) |
| { |
| if (!path) |
| return path; |
| |
| if (path.indexOf("./") === -1) |
| return path; |
| |
| console.assert(path.charAt(0) === "/"); |
| |
| var result = []; |
| |
| var components = path.split("/"); |
| for (var i = 0; i < components.length; ++i) { |
| var component = components[i]; |
| |
| // Skip over "./". |
| if (component === ".") |
| continue; |
| |
| // Rewind one component for "../". |
| if (component === "..") { |
| if (result.length === 1) |
| continue; |
| result.pop(); |
| continue; |
| } |
| |
| result.push(component); |
| } |
| |
| return result.join("/"); |
| } |
| |
| function parseMIMEType(fullMimeType) |
| { |
| if (!fullMimeType) |
| return {type: fullMimeType, boundary: null, encoding: null}; |
| |
| var typeParts = fullMimeType.split(/\s*;\s*/); |
| console.assert(typeParts.length >= 1); |
| |
| var type = typeParts[0]; |
| var boundary = null; |
| var encoding = null; |
| |
| for (var i = 1; i < typeParts.length; ++i) { |
| var subparts = typeParts[i].split(/\s*=\s*/); |
| if (subparts.length !== 2) |
| continue; |
| |
| if (subparts[0].toLowerCase() === "boundary") |
| boundary = subparts[1]; |
| else if (subparts[0].toLowerCase() === "charset") |
| encoding = subparts[1].replace("^\"|\"$", ""); // Trim quotes. |
| } |
| |
| return {type, boundary: boundary || null, encoding: encoding || null}; |
| } |
| |
| function simpleGlobStringToRegExp(globString, regExpFlags) |
| { |
| // Only supports "*" globs. |
| |
| if (!globString) |
| return null; |
| |
| // Escape everything from String.prototype.escapeForRegExp except "*". |
| var regexString = globString.escapeCharacters("^[]{}()\\.$+?|"); |
| |
| // Unescape all doubly escaped backslashes in front of escaped asterisks. |
| // So "\\*" will become "\*" again, undoing escapeCharacters escaping of "\". |
| // This makes "\*" match a literal "*" instead of using the "*" for globbing. |
| regexString = regexString.replace(/\\\\\*/g, "\\*"); |
| |
| // The following regex doesn't match an asterisk that has a backslash in front. |
| // It also catches consecutive asterisks so they collapse down when replaced. |
| var unescapedAsteriskRegex = /(^|[^\\])\*+/g; |
| if (unescapedAsteriskRegex.test(globString)) { |
| // Replace all unescaped asterisks with ".*". |
| regexString = regexString.replace(unescapedAsteriskRegex, "$1.*"); |
| |
| // Match edge boundaries when there is an asterisk to better meet the expectations |
| // of the user. When someone types "*.js" they don't expect "foo.json" to match. They |
| // would only expect that if they type "*.js*". We use \b (instead of ^ and $) to allow |
| // matches inside paths or URLs, so "ba*.js" will match "foo/bar.js" but not "boo/bbar.js". |
| // When there isn't an asterisk the regexString is just a substring search. |
| regexString = "\\b" + regexString + "\\b"; |
| } |
| |
| return new RegExp(regexString, regExpFlags); |
| } |
| |
| Object.defineProperty(Array.prototype, "lowerBound", |
| { |
| // Return index of the leftmost element that is equal or greater |
| // than the specimen object. If there's no such element (i.e. all |
| // elements are smaller than the specimen) returns array.length. |
| // The function works for sorted array. |
| value(object, comparator) |
| { |
| function defaultComparator(a, b) |
| { |
| return a - b; |
| } |
| comparator = comparator || defaultComparator; |
| var l = 0; |
| var r = this.length; |
| while (l < r) { |
| var m = (l + r) >> 1; |
| if (comparator(object, this[m]) > 0) |
| l = m + 1; |
| else |
| r = m; |
| } |
| return r; |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "upperBound", |
| { |
| // Return index of the leftmost element that is greater |
| // than the specimen object. If there's no such element (i.e. all |
| // elements are smaller than the specimen) returns array.length. |
| // The function works for sorted array. |
| value(object, comparator) |
| { |
| function defaultComparator(a, b) |
| { |
| return a - b; |
| } |
| comparator = comparator || defaultComparator; |
| var l = 0; |
| var r = this.length; |
| while (l < r) { |
| var m = (l + r) >> 1; |
| if (comparator(object, this[m]) >= 0) |
| l = m + 1; |
| else |
| r = m; |
| } |
| return r; |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "binaryIndexOf", |
| { |
| value(value, comparator) |
| { |
| function defaultComparator(a, b) |
| { |
| return a - b; |
| } |
| comparator = comparator || defaultComparator; |
| |
| var index = this.lowerBound(value, comparator); |
| return index < this.length && comparator(value, this[index]) === 0 ? index : -1; |
| } |
| }); |
| |
| Object.defineProperty(Array.prototype, "groupBy", |
| { |
| value(groupFunction, minGroupSize = 1) |
| { |
| let result = []; |
| let startIndex = null; |
| |
| let flush = (endIndex) => { |
| if (startIndex === null) |
| return; |
| let group = this.slice(startIndex, endIndex + 1); |
| let adjacentCount = (endIndex + 1) - startIndex; |
| if (adjacentCount >= minGroupSize) |
| result.push(group); |
| else |
| result.pushAll(group); |
| } |
| |
| this.forEach((item, i) => { |
| if (groupFunction(item)) { |
| startIndex ??= i; |
| if (i === this.length - 1) |
| flush(this.length - 1); |
| } else { |
| flush(i - 1); |
| result.push(item); |
| startIndex = null; |
| } |
| }); |
| |
| return result; |
| } |
| }); |
| |
| Object.defineProperty(Promise, "chain", |
| { |
| async value(callbacks, initialValue) |
| { |
| let results = []; |
| for (let i = 0; i < callbacks.length; ++i) |
| results.push(await callbacks[i](results.lastValue || initialValue || null, i)); |
| return results; |
| } |
| }); |
| |
| Object.defineProperty(Promise, "delay", |
| { |
| value(delay) |
| { |
| return new Promise((resolve) => setTimeout(resolve, delay || 0)); |
| } |
| }); |
| |
| function appendWebInspectorSourceURL(string) |
| { |
| if (string.includes("//# sourceURL")) |
| return string; |
| return "\n//# sourceURL=__WebInspectorInternal__\n" + string; |
| } |
| |
| function appendWebInspectorConsoleEvaluationSourceURL(string) |
| { |
| if (string.includes("//# sourceURL")) |
| return string; |
| return "\n//# sourceURL=__WebInspectorConsoleEvaluation__\n" + string; |
| } |
| |
| function isWebInspectorBootstrapScript(url) |
| { |
| return url === WI.NetworkManager.bootstrapScriptURL; |
| } |
| |
| function isWebInspectorInternalScript(url) |
| { |
| return url === "__WebInspectorInternal__"; |
| } |
| |
| function isWebInspectorConsoleEvaluationScript(url) |
| { |
| return url === "__WebInspectorConsoleEvaluation__"; |
| } |
| |
| function isWebKitInjectedScript(url) |
| { |
| return url && url.startsWith("__InjectedScript_") && url.endsWith(".js"); |
| } |
| |
| function isWebKitInternalScript(url) |
| { |
| if (isWebInspectorConsoleEvaluationScript(url)) |
| return false; |
| |
| if (isWebKitInjectedScript(url)) |
| return true; |
| |
| return url && url.startsWith("__Web") && url.endsWith("__"); |
| } |
| |
| function isFunctionStringNativeCode(str) |
| { |
| return str.endsWith("{\n [native code]\n}"); |
| } |
| |
| function whitespaceRatio(content, start, end) |
| { |
| let whitespaceScore = 0; |
| let size = end - start; |
| |
| for (let i = start; i < end; i++) { |
| let char = content[i]; |
| if (char === " ") |
| whitespaceScore++; |
| else if (char === "\t") |
| whitespaceScore += 4; |
| else if (char === "\n") |
| whitespaceScore += 8; |
| } |
| |
| let ratio = whitespaceScore / size; |
| return ratio; |
| } |
| |
| function isTextLikelyMinified(content) |
| { |
| const autoFormatMaxCharactersToCheck = 2500; |
| const autoFormatWhitespaceRatio = 0.2; |
| |
| if (content.length <= autoFormatMaxCharactersToCheck) { |
| let ratio = whitespaceRatio(content, 0, content.length); |
| return ratio < autoFormatWhitespaceRatio; |
| } |
| |
| let startRatio = whitespaceRatio(content, 0, autoFormatMaxCharactersToCheck); |
| if (startRatio < autoFormatWhitespaceRatio) |
| return true; |
| |
| let endRatio = whitespaceRatio(content, content.length - autoFormatMaxCharactersToCheck, content.length); |
| if (endRatio < autoFormatWhitespaceRatio) |
| return true; |
| |
| return false; |
| } |
| |
| function doubleQuotedString(str) |
| { |
| return JSON.stringify(str); |
| } |
| |
| function insertionIndexForObjectInListSortedByFunction(object, list, comparator, insertionIndexAfter) |
| { |
| if (insertionIndexAfter) { |
| return list.upperBound(object, comparator); |
| } else { |
| return list.lowerBound(object, comparator); |
| } |
| } |
| |
| function insertObjectIntoSortedArray(object, array, comparator) |
| { |
| array.splice(insertionIndexForObjectInListSortedByFunction(object, array, comparator), 0, object); |
| } |
| |
| WI.setReentrantCheck = function(object, key) |
| { |
| key = "__checkReentrant_" + key; |
| object[key] = (object[key] || 0) + 1; |
| return object[key] === 1; |
| }; |
| |
| WI.clearReentrantCheck = function(object, key) |
| { |
| key = "__checkReentrant_" + key; |
| object[key] = (object[key] || 0) - 1; |
| return object[key] === 0; |
| }; |