// Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2009 by Doug Kearns // Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. "use strict"; /** @scope modules */ const XHTML = Namespace("html", "http://www.w3.org/1999/xhtml"); const XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); const NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator"); default xml namespace = XHTML; const Util = Module("util", { init: function () { this.Array = array; }, get activeWindow() services.get("windowWatcher").activeWindow, callInMainThread: function (callback, self) { let mainThread = services.get("threadManager").mainThread; if (!services.get("threadManager").isMainThread) mainThread.dispatch({ run: callback.call(self) }, mainThread.DISPATCH_NORMAL); else callback.call(self); }, /** * Returns a shallow copy of obj. * * @param {Object} obj * @returns {Object} */ cloneObject: function cloneObject(obj) { if (isarray(obj)) return obj.slice(); let newObj = {}; for (let [k, v] in Iterator(obj)) newObj[k] = v; return newObj; }, /** * Clips a string to a given length. If the input string is longer * than length, an ellipsis is appended. * * @param {string} str The string to truncate. * @param {number} length The length of the returned string. * @returns {string} */ clip: function clip(str, length) { return str.length <= length ? str : str.substr(0, length - 3) + "..."; }, /** * Compares two strings, case insensitively. Return values are as * in String#localeCompare. * * @param {string} a * @param {string} b * @returns {number} */ compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()), /** * Returns an object representing a Node's computed CSS style. * * @param {Node} node * @returns {Object} */ computedStyle: function computedStyle(node) { while (node instanceof Ci.nsIDOMText && node.parentNode) node = node.parentNode; return node.ownerDocument.defaultView.getComputedStyle(node, null); }, /** * Copies a string to the system clipboard. If verbose is specified * the copied string is also echoed to the command line. * * @param {string} str * @param {boolean} verbose */ copyToClipboard: function copyToClipboard(str, verbose) { const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); clipboardHelper.copyString(str); if (verbose) dactyl.echo("Yanked " + str, commandline.FORCE_SINGLELINE); }, /** * Converts any arbitrary string into an URI object. * * @param {string} str * @returns {Object} */ // FIXME: newURI needed too? createURI: function createURI(str) { const fixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); return fixup.createFixupURI(str, fixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP); }, /** * Expands brace globbing patterns in a string. * * Example: * "a{b,c}d" => ["abd", "acd"] * * @param {string} pattern The pattern to deglob. * @returns [string] The resulting strings. */ debrace: function deglobBrace(pattern) { function split(pattern, re, fn, dequote) { let end = 0, match, res = []; while (match = re.exec(pattern)) { end = match.index + match[0].length; res.push(match[1]); if (fn) fn(match); } res.push(pattern.substr(end)); return res.map(function (s) util.dequote(s, dequote)); } let patterns = [], res = []; let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy, function (match) { patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy, null, ",{}")); }, "{}"); function rec(acc) { if (acc.length == patterns.length) res.push(util.Array.zip(substrings, acc).join("")); else for (let [, pattern] in Iterator(patterns[acc.length])) rec(acc.concat(pattern)); } rec([]); return res; }, /** * Removes certain backslash-quoted characters while leaving other * backslash-quoting sequences untouched. * * @param {string} pattern The string to unquote. * @param {string} chars The characters to unquote. * @returns {string} */ dequote: function dequote(pattern, chars) pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0), /** * Converts HTML special characters in str to the equivalent HTML * entities. * * @param {string} str * @returns {string} */ escapeHTML: function escapeHTML(str) { return str.replace(/&/g, "&").replace(/str. * * @param {string} str * @returns {string} */ escapeRegex: function escapeRegex(str) { return str.replace(/([\\{}()[\].?*+])/g, "\\$1"); }, /** * Escapes quotes, newline and tab characters in str. The returned * string is delimited by delimiter or " if delimiter is not * specified. {@see String#quote}. * * @param {string} str * @param {string} delimiter * @returns {string} */ escapeString: function escapeString(str, delimiter) { if (delimiter == undefined) delimiter = '"'; return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter; }, /** * Evaluates an XPath expression in the current or provided * document. It provides the xhtml, xhtml2 and dactyl XML * namespaces. The result may be used as an iterator. * * @param {string} expression The XPath expression to evaluate. * @param {Document} doc The document to evaluate the expression in. * @default The current document. * @param {Node} elem The context element. * @default doc * @param {boolean} asIterator Whether to return the results as an * XPath iterator. */ evaluateXPath: function (expression, doc, elem, asIterator) { if (!doc) doc = content.document; if (!elem) elem = doc; if (isarray(expression)) expression = util.makeXPath(expression); let result = doc.evaluate(expression, elem, function lookupNamespaceURI(prefix) { return { xul: XUL.uri, xhtml: XHTML.uri, xhtml2: "http://www.w3.org/2002/06/xhtml2", dactyl: NS.uri }[prefix] || null; }, asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); return { __proto__: result, __iterator__: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; } : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); } } }, extend: function extend(dest) { Array.slice(arguments, 1).filter(util.identity).forEach(function (src) { for (let [k, v] in Iterator(src)) { let get = src.__lookupGetter__(k), set = src.__lookupSetter__(k); if (!get && !set) dest[k] = v; if (get) dest.__defineGetter__(k, get); if (set) dest.__defineSetter__(k, set); } }); return dest; }, /** * Returns the selection controller for the given window. * * @param {Window} window * @returns {nsISelectionController} */ selectionController: function (win) win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell) .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsISelectionDisplay) .QueryInterface(Ci.nsISelectionController), /** * Converts bytes to a pretty printed data size string. * * @param {number} bytes The number of bytes. * @param {string} decimalPlaces The number of decimal places to use if * humanReadable is true. * @param {boolean} humanReadable Use byte multiples. * @returns {string} */ formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) { const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; let unitIndex = 0; let tmpNum = parseInt(bytes, 10) || 0; let strNum = [tmpNum + ""]; if (humanReadable) { while (tmpNum >= 1024) { tmpNum /= 1024; if (++unitIndex > (unitVal.length - 1)) break; } let decPower = Math.pow(10, decimalPlaces); strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2); if (!strNum[1]) strNum[1] = ""; while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces) strNum[1] += "0"; } for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000 strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u); if (unitIndex) // decimalPlaces only when > Bytes strNum[0] += "." + strNum[1]; return strNum[0] + " " + unitVal[unitIndex]; }, /** * Sends a synchronous or asynchronous HTTP request to url and * returns the XMLHttpRequest object. If callback is specified the * request is asynchronous and the callback is invoked with the * object as its argument. * * @param {string} url * @param {function(XMLHttpRequest)} callback * @returns {XMLHttpRequest} */ httpGet: function httpGet(url, callback) { try { let xmlhttp = services.create("xmlhttp"); xmlhttp.mozBackgroundRequest = true; if (callback) { xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4) callback(xmlhttp); }; } xmlhttp.open("GET", url, !!callback); xmlhttp.send(null); return xmlhttp; } catch (e) { dactyl.log("Error opening " + String.quote(url) + ": " + e, 1); return null; } }, /** * The identity function. * * @param {Object} k * @returns {Object} */ identity: function identity(k) k, /** * Returns the intersection of two rectangles. * * @param {Object} r1 * @param {Object} r2 * @returns {Object} */ intersection: function (r1, r2) ({ get width() this.right - this.left, get height() this.bottom - this.top, left: Math.max(r1.left, r2.left), right: Math.min(r1.right, r2.right), top: Math.max(r1.top, r2.top), bottom: Math.min(r1.bottom, r2.bottom) }), /** * Returns an XPath union expression constructed from the specified node * tests. An expression is built with node tests for both the null and * XHTML namespaces. See {@link Buffer#evaluateXPath}. * * @param nodes {Array(string)} * @returns {string} */ makeXPath: function makeXPath(nodes) { return util.Array(nodes).map(util.debrace).flatten() .map(function (node) [node, "xhtml:" + node]).flatten() .map(function (node) "//" + node).join(" | "); }, /** * Returns the array that results from applying func to each * property of obj. * * @param {Object} obj * @param {function} func * @returns {Array} */ map: function map(obj, func) { let ary = []; for (let i in Iterator(obj)) ary.push(func(i)); return ary; }, /** * Memoize the lookup of a property in an object. * * @param {object} obj The object to alter. * @param {string} key The name of the property to memoize. * @param {function} getter A function of zero to two arguments which * will return the property's value. obj is * passed as the first argument, key as the * second. */ memoize: memoize, /** * Converts a URI string into a URI object. * * @param {string} uri * @returns {nsIURI} */ // FIXME: createURI needed too? newURI: function (uri) { return services.get("io").newURI(uri, null, null); }, /** * Pretty print a JavaScript object. Use HTML markup to color certain items * if color is true. * * @param {Object} object The object to pretty print. * @param {boolean} color Whether the output should be colored. * @returns {string} */ objectToString: function objectToString(object, color) { // Use E4X literals so html is automatically quoted // only when it's asked for. No one wants to see < // on their console or :map :foo in their buffer // when they expect :map :foo. XML.prettyPrinting = false; XML.ignoreWhitespace = false; if (object === null) return "null\n"; if (typeof object != "object") return false; const NAMESPACES = util.Array.toObject([ [NS, 'dactyl'], [XHTML, 'html'], [XUL, 'xul'] ]); if (object instanceof Ci.nsIDOMElement) { let elem = object; if (elem.nodeType == elem.TEXT_NODE) return elem.data; function namespaced(node) { var ns = NAMESPACES[node.namespaceURI]; if (ns) return ns + ":" + node.localName; return node.localName.toLowerCase(); } try { let tag = "<" + [namespaced(elem)].concat( [namespaced(a) + "=" + template.highlight(a.value, true) for ([i, a] in util.Array.iteritems(elem.attributes))]).join(" "); if (!elem.firstChild || /^\s*$/.test(elem.firstChild) && !elem.firstChild.nextSibling) tag += '/>'; else tag += '>...'; return tag; } catch (e) { return {}.toString.call(elem); } } try { // for window.JSON var obj = String(object); } catch (e) { obj = "[Object]"; } obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () ^J); let string = <>{obj}::
; let keys = []; try { // window.content often does not want to be queried with "var i in object" let hasValue = !("__iterator__" in object); /* if (modules.isPrototypeOf(object)) { object = Iterator(object); hasValue = false; } */ for (let i in object) { let value = ]]>; try { value = object[i]; } catch (e) {} if (!hasValue) { if (isarray(i) && i.length == 2) [i, value] = i; else var noVal = true; } value = template.highlight(value, true, 150); let key = {i}; if (!isNaN(i)) i = parseInt(i); else if (/^[A-Z_]+$/.test(i)) i = ""; keys.push([i, <>{key}{noVal ? "" : <>: {value}}
]); } } catch (e) {} function compare(a, b) { if (!isNaN(a[0]) && !isNaN(b[0])) return a[0] - b[0]; return String.localeCompare(a[0], b[0]); } string += template.map(keys.sort(compare), function (f) f[1]); return color ? string : [s for each (s in string)].join(""); }, /** * A generator that returns the values between start and end, * in step increments. * * @param {number} start The interval's start value. * @param {number} end The interval's end value. * @param {boolean} step The value to step the range by. May be * negative. @default 1 * @returns {Iterator(Object)} */ range: function range(start, end, step) { if (!step) step = 1; if (step > 0) { for (; start < end; start += step) yield start; } else { while (start > end) yield start += step; } }, /** * An interruptible generator that returns all values between start * and end. The thread yields every time milliseconds. * * @param {number} start The interval's start value. * @param {number} end The interval's end value. * @param {number} time The time in milliseconds between thread yields. * @returns {Iterator(Object)} */ interruptibleRange: function interruptibleRange(start, end, time) { let endTime = Date.now() + time; while (start < end) { if (Date.now() > endTime) { util.threadYield(true, true); endTime = Date.now() + time; } yield start++; } }, /** * Reads a string from the system clipboard. * * This is same as Firefox's readFromClipboard function, but is needed for * apps like Thunderbird which do not provide it. * * @returns {string} */ readFromClipboard: function readFromClipboard() { let str; try { const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); transferable.addDataFlavor("text/unicode"); if (clipboard.supportsSelectionClipboard()) clipboard.getData(transferable, clipboard.kSelectionClipboard); else clipboard.getData(transferable, clipboard.kGlobalClipboard); let data = {}; let dataLen = {}; transferable.getTransferData("text/unicode", data, dataLen); if (data) { data = data.value.QueryInterface(Ci.nsISupportsString); str = data.data.substring(0, dataLen.value / 2); } } catch (e) {} return str; }, /** * Scrolls an element into view if and only if it's not already * fully visible. * * @param {Node} elem The element to make visible. */ scrollIntoView: function scrollIntoView(elem) { let win = elem.ownerDocument.defaultView; let rect = elem.getBoundingClientRect(); if (!(rect && rect.top < win.innerHeight && rect.bottom >= 0 && rect.left < win.innerWidth && rect.right >= 0)) elem.scrollIntoView(); }, sleep: function (delay) { let mainThread = services.get("threadManager").mainThread; let end = Date.now() + delay; while (Date.now() < end) mainThread.processNextEvent(true); return true; }, /** * Split a string on literal occurrences of a marker. * * Specifically this ignores occurrences preceded by a backslash, or * contained within 'single' or "double" quotes. * * It assumes backslash escaping on strings, and will thus not count quotes * that are preceded by a backslash or within other quotes as starting or * ending quoted sections of the string. * * @param {string} str * @param {RegExp} marker */ splitLiteral: function splitLiteral(str, marker) { let results = []; let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source); let cont = true; while (cont) { cont = false; str = str.replace(resep, function (match, before) { results.push(before); cont = true; return ""; }); } results.push(str); return results; }, threadYield: function (flush, interruptable) { let mainThread = services.get("threadManager").mainThread; /* FIXME */ util.interrupted = false; do { mainThread.processNextEvent(!flush); if (util.interrupted) throw new Error("Interrupted"); } while (flush === true && mainThread.hasPendingEvents()); }, /** * Converts an E4X XML literal to a DOM node. * * @param {Node} node * @param {Document} doc * @param {Object} nodes If present, nodes with the "key" attribute are * stored here, keyed to the value thereof. * @returns {Node} */ xmlToDom: function xmlToDom(node, doc, nodes) { XML.prettyPrinting = false; if (node.length() != 1) { let domnode = doc.createDocumentFragment(); for each (let child in node) domnode.appendChild(arguments.callee(child, doc, nodes)); return domnode; } switch (node.nodeKind()) { case "text": return doc.createTextNode(String(node)); case "element": let domnode = doc.createElementNS(node.namespace(), node.localName()); for each (let attr in node.@*) domnode.setAttributeNS(attr.name() == "highlight" ? NS.uri : attr.namespace(), attr.name(), String(attr)); for each (let child in node.*) domnode.appendChild(arguments.callee(child, doc, nodes)); if (nodes && node.@key) nodes[node.@key] = domnode; return domnode; default: return null; } } }, { Array: array }); /** * Math utility methods. * @singleton */ var Math = { __proto__: window.Math, /** * Returns the specified value constrained to the range min - * max. * * @param {number} value The value to constrain. * @param {number} min The minimum constraint. * @param {number} max The maximum constraint. * @returns {number} */ constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max) }; // vim: set fdm=marker sw=4 ts=4 et: