,
- this.document);
+ var output = DOM(
,
+ this.document);
data.document = this.document;
try {
- output.appendChild(data.message);
+ output.append(data.message);
}
catch (e) {
util.reportError(e);
@@ -138,17 +138,17 @@ var MOW = Module("mow", {
let style = isString(data) ? "pre-wrap" : "nowrap";
this.lastOutput =
{data}
;
- var output = util.xmlToDom(this.lastOutput, this.document);
+ var output = DOM(this.lastOutput, this.document);
}
// FIXME: need to make sure an open MOW is closed when commands
// that don't generate output are executed
if (!this.visible) {
this.body.scrollTop = 0;
- body.textContent = "";
+ body.empty();
}
- body.appendChild(output);
+ body.append(output);
let str = typeof data !== "xml" && data.message || data;
if (!silent)
diff --git a/common/content/statusline.js b/common/content/statusline.js
index b5dbee70..2d93e512 100644
--- a/common/content/statusline.js
+++ b/common/content/statusline.js
@@ -42,7 +42,7 @@ var StatusLine = Module("statusline", {
color: inherit !important;
}
AddonButton:not(:hover) background: transparent;
- ]]>)({ padding: util.OS.isMacOSX ? "padding-right: 10px !important;" : "" }));
+ ]]>)({ padding: config.OS.isMacOSX ? "padding-right: 10px !important;" : "" }));
if (document.getElementById("appmenu-button"))
highlight.loadCSS(>, /tab-./g, function (m) util.OS.isMacOSX ? "tab-mac" : m),
+ ]]>>, /tab-./g, function (m) config.OS.isMacOSX ? "tab-mac" : m),
false, true);
this.timeout(function () {
@@ -75,14 +75,12 @@ var Tabs = Module("tabs", {
if (!node("dactyl-tab-number")) {
let img = node("tab-icon-image");
if (img) {
- let nodes = {};
- let dom = util.xmlToDom(
.*, document, nodes);
- img.parentNode.appendChild(dom);
- tab.__defineGetter__("dactylOrdinal", function () Number(nodes.icon.value));
- tab.__defineSetter__("dactylOrdinal", function (i) nodes.icon.value = nodes.label.textContent = i);
+ >.*, document).appendTo(img.parentNode);
+ tab.__defineGetter__("dactylOrdinal", function () Number(dom.nodes.icon.value));
+ tab.__defineSetter__("dactylOrdinal", function (i) dom.nodes.icon.value = dom.nodes.label.textContent = i);
}
}
}
diff --git a/common/modules/addons.jsm b/common/modules/addons.jsm
index 6f43950c..160b3178 100644
--- a/common/modules/addons.jsm
+++ b/common/modules/addons.jsm
@@ -111,7 +111,7 @@ var actions = {
name: "extr[ehash]",
description: "Reload an extension",
action: function (addon) {
- util.assert(util.haveGecko("2b"), _("command.notUseful", config.host));
+ util.assert(config.haveGecko("2b"), _("command.notUseful", config.host));
util.timeout(function () {
addon.userDisabled = true;
addon.userDisabled = false;
diff --git a/common/modules/base.jsm b/common/modules/base.jsm
index 0c8c82f4..8cde5ce2 100644
--- a/common/modules/base.jsm
+++ b/common/modules/base.jsm
@@ -144,6 +144,7 @@ function defineModule(name, params, module) {
use[mod] = use[mod] || [];
use[mod].push(module);
}
+ module._lastModule = currentModule;
currentModule = module;
module.startTime = Date.now();
}
@@ -190,6 +191,7 @@ function endModule() {
require(mod, currentModule.NAME, "use");
loaded[currentModule.NAME] = 1;
+ currentModule = currentModule._lastModule;
}
function require(obj, name, from) {
@@ -732,7 +734,7 @@ function Class() {
if (callable(args[0]))
superclass = args.shift();
- if (loaded.util && util.haveGecko("6.0a1")) // Bug 657418.
+ if (loaded.config && config.haveGecko("6.0a1")) // Bug 657418.
var Constructor = function Constructor() {
var self = Object.create(Constructor.prototype, {
constructor: { value: Constructor },
diff --git a/common/modules/config.jsm b/common/modules/config.jsm
index 4da5a88d..2623b84b 100644
--- a/common/modules/config.jsm
+++ b/common/modules/config.jsm
@@ -23,7 +23,7 @@ var ConfigBase = Class("ConfigBase", {
*/
init: function init() {
this.features.push = deprecated("Set.add", function push(feature) Set.add(this, feature));
- if (util.haveGecko("2b"))
+ if (this.haveGecko("2b"))
Set.add(this.features, "Gecko2");
this.timeout(function () {
@@ -40,7 +40,7 @@ var ConfigBase = Class("ConfigBase", {
highlight.loadCSS(this.CSS.replace(/__MSG_(.*?)__/g, function (m0, m1) _(m1)));
highlight.loadCSS(this.helpCSS.replace(/__MSG_(.*?)__/g, function (m0, m1) _(m1)));
- if (!util.haveGecko("2b"))
+ if (!this.haveGecko("2b"))
highlight.loadCSS(= 0;
+ let style = DOM(elem).style;
prefs[bg ? "safeSet" : "safeReset"]("ui.textHighlightBackground", hex(style.backgroundColor));
prefs[fg ? "safeSet" : "safeReset"]("ui.textHighlightForeground", hex(style.color));
};
@@ -148,6 +148,87 @@ var ConfigBase = Class("ConfigBase", {
.nth(function (l) Set.has(langs, l), 0);
},
+ /**
+ * A list of all known registered chrome and resource packages.
+ */
+ get chromePackages() {
+ // Horrible hack.
+ let res = {};
+ function process(manifest) {
+ for each (let line in manifest.split(/\n+/)) {
+ let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
+ if (match)
+ res[match[2]] = true;
+ }
+ }
+ function processJar(file) {
+ let jar = services.ZipReader(file);
+ if (jar) {
+ if (jar.hasEntry("chrome.manifest"))
+ process(File.readStream(jar.getInputStream("chrome.manifest")));
+ jar.close();
+ }
+ }
+
+ for each (let dir in ["UChrm", "AChrom"]) {
+ dir = File(services.directory.get(dir, Ci.nsIFile));
+ if (dir.exists() && dir.isDirectory())
+ for (let file in dir.iterDirectory())
+ if (/\.manifest$/.test(file.leafName))
+ process(file.read());
+
+ dir = File(dir.parent);
+ if (dir.exists() && dir.isDirectory())
+ for (let file in dir.iterDirectory())
+ if (/\.jar$/.test(file.leafName))
+ processJar(file);
+
+ dir = dir.child("extensions");
+ if (dir.exists() && dir.isDirectory())
+ for (let ext in dir.iterDirectory()) {
+ if (/\.xpi$/.test(ext.leafName))
+ processJar(ext);
+ else {
+ if (ext.isFile())
+ ext = File(ext.read().replace(/\n*$/, ""));
+ let mf = ext.child("chrome.manifest");
+ if (mf.exists())
+ process(mf.read());
+ }
+ }
+ }
+ return Object.keys(res).sort();
+ },
+
+ /**
+ * Returns true if the current Gecko runtime is of the given version
+ * or greater.
+ *
+ * @param {string} ver The required version.
+ * @returns {boolean}
+ */
+ haveGecko: function (ver) services.versionCompare.compare(services.runtime.platformVersion, ver) >= 0,
+
+ /** Dactyl's notion of the current operating system platform. */
+ OS: memoize({
+ _arch: services.runtime.OS,
+ /**
+ * @property {string} The normalised name of the OS. This is one of
+ * "Windows", "Mac OS X" or "Unix".
+ */
+ get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
+ /** @property {boolean} True if the OS is Windows. */
+ get isWindows() this._arch == "WINNT",
+ /** @property {boolean} True if the OS is Mac OS X. */
+ get isMacOSX() this._arch == "Darwin",
+ /** @property {boolean} True if the OS is some other *nix variant. */
+ get isUnix() !this.isWindows && !this.isMacOSX,
+ /** @property {RegExp} A RegExp which matches illegal characters in path components. */
+ get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g,
+
+ get pathListSep() this.isWindows ? ";" : ":"
+ }),
+
/**
* @property {string} The pathname of the VCS repository clone's root
* directory if the application is running from one via an extension
diff --git a/common/modules/finder.jsm b/common/modules/finder.jsm
index 6ff2caee..94f9e48a 100644
--- a/common/modules/finder.jsm
+++ b/common/modules/finder.jsm
@@ -403,8 +403,8 @@ var RangeFind = Class("RangeFind", {
focus: function focus() {
if (this.lastRange)
- var node = util.evaluateXPath(RangeFind.selectNodePath,
- this.lastRange.commonAncestorContainer).snapshotItem(0);
+ var node = DOM.XPath(RangeFind.selectNodePath,
+ this.lastRange.commonAncestorContainer).snapshotItem(0);
if (node) {
node.focus();
// Re-highlight collapsed selection
@@ -517,7 +517,7 @@ var RangeFind = Class("RangeFind", {
for (let frame in array.iterValues(win.frames)) {
let range = doc.createRange();
- if (util.computedStyle(frame.frameElement).visibility == "visible") {
+ if (DOM(frame.frameElement).style.visibility == "visible") {
range.selectNode(frame.frameElement);
pushRange(pageStart, RangeFind.endpoint(range, true));
pageStart = RangeFind.endpoint(range, false);
diff --git a/common/modules/io.jsm b/common/modules/io.jsm
index 59eb8d98..aeead9b3 100644
--- a/common/modules/io.jsm
+++ b/common/modules/io.jsm
@@ -299,7 +299,7 @@ var IO = Module("io", {
let rcFile1 = dir.child("." + config.name + "rc");
let rcFile2 = dir.child("_" + config.name + "rc");
- if (util.OS.isWindows)
+ if (config.OS.isWindows)
[rcFile1, rcFile2] = [rcFile2, rcFile1];
if (rcFile1.exists() && rcFile1.isFile())
@@ -393,9 +393,9 @@ var IO = Module("io", {
if (bin instanceof File || File.isAbsolutePath(bin))
return this.File(bin);
- let dirs = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
+ let dirs = services.environment.get("PATH").split(config.OS.isWindows ? ";" : ":");
// Windows tries the CWD first TODO: desirable?
- if (util.OS.isWindows)
+ if (config.OS.isWindows)
dirs = [io.cwd].concat(dirs);
for (let [, dir] in Iterator(dirs))
@@ -408,7 +408,7 @@ var IO = Module("io", {
// TODO: couldn't we just palm this off to the start command?
// automatically try to add the executable path extensions on windows
- if (util.OS.isWindows) {
+ if (config.OS.isWindows) {
let extensions = services.environment.get("PATHEXT").split(";");
for (let [, extension] in Iterator(extensions)) {
file = dir.child(bin + extension);
@@ -478,7 +478,7 @@ var IO = Module("io", {
system: function (command, input, callback) {
util.dactyl.echomsg(_("io.callingShell", command), 4);
- function escape(str) '"' + String.replace(str, /[\\"$]/g, "\\$&") + '"';
+ let { shellEscape } = util.closure;
return this.withTempFiles(function (stdin, stdout, cmd) {
if (input instanceof File)
@@ -505,17 +505,17 @@ var IO = Module("io", {
util.assert(shell, _("error.invalid", "'shell'"));
if (isArray(command))
- command = command.map(escape).join(" ");
+ command = command.map(shellEscape).join(" ");
// TODO: implement 'shellredir'
- if (util.OS.isWindows && !/sh/.test(shell.leafName)) {
+ if (config.OS.isWindows && !/sh/.test(shell.leafName)) {
command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
var res = this.run(shell, shcf.split(/\s+/).concat(command), callback ? async : true);
}
else {
- cmd.write("cd " + escape(this.cwd.path) + "\n" +
- ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
- escape(shell.path), shcf, escape(command)].join(" "));
+ cmd.write("cd " + shellEscape(this.cwd.path) + "\n" +
+ ["exec", ">" + shellEscape(stdout.path), "2>&1", "<" + shellEscape(stdin.path),
+ shellEscape(shell.path), shcf, shellEscape(command)].join(" "));
res = this.run("/bin/sh", ["-e", cmd.path], callback ? async : true);
}
@@ -556,7 +556,7 @@ var IO = Module("io", {
const rtpvar = config.idName + "_RUNTIME";
let rtp = services.environment.get(rtpvar);
if (!rtp) {
- rtp = "~/" + (util.OS.isWindows ? "" : ".") + config.name;
+ rtp = "~/" + (config.OS.isWindows ? "" : ".") + config.name;
services.environment.set(rtpvar, rtp);
}
return rtp;
@@ -644,7 +644,7 @@ var IO = Module("io", {
commands.add(["mks[yntax]"],
"Generate a Vim syntax file",
function (args) {
- let runtime = util.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
+ let runtime = config.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
let file = io.File(runtime + "syntax/" + config.name + ".vim");
if (args.length)
file = io.File(args[0]);
@@ -886,7 +886,7 @@ unlet s:cpo_save
completion.environment = function environment(context) {
context.title = ["Environment Variable", "Value"];
context.generate = function ()
- io.system(util.OS.isWindows ? "set" : "env")
+ io.system(config.OS.isWindows ? "set" : "env")
.output.split("\n")
.filter(function (line) line.indexOf("=") > 0)
.map(function (line) line.match(/([^=]+)=(.*)/).slice(1));
@@ -956,7 +956,7 @@ unlet s:cpo_save
completion.shellCommand = function shellCommand(context) {
context.title = ["Shell Command", "Path"];
context.generate = function () {
- let dirNames = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
+ let dirNames = services.environment.get("PATH").split(config.OS.pathListSep);
let commands = [];
for (let [, dirName] in Iterator(dirNames)) {
@@ -987,7 +987,7 @@ unlet s:cpo_save
if (!match.path) {
context.key = match.proto;
context.advance(match.proto.length);
- context.generate = function () util.chromePackages.map(function (p) [p, match.proto + p + "/"]);
+ context.generate = function () config.chromePackages.map(function (p) [p, match.proto + p + "/"]);
}
else if (match.scheme === "chrome") {
context.key = match.prefix;
@@ -1001,7 +1001,7 @@ unlet s:cpo_save
}
if (!match || match.scheme === "resource" && match.path)
if (/^(\.{0,2}|~)\/|^file:/.test(context.filter)
- || util.OS.isWindows && /^[a-z]:/i.test(context.filter)
+ || config.OS.isWindows && /^[a-z]:/i.test(context.filter)
|| util.getFile(context.filter)
|| io.isJarURL(context.filter))
completion.file(context, full);
@@ -1030,7 +1030,7 @@ unlet s:cpo_save
const { completion, options } = modules;
var shell, shellcmdflag;
- if (util.OS.isWindows) {
+ if (config.OS.isWindows) {
shell = "cmd.exe";
shellcmdflag = "/c";
}
@@ -1077,7 +1077,7 @@ unlet s:cpo_save
"string", shellcmdflag,
{
getter: function (value) {
- if (this.hasChanged || !util.OS.isWindows)
+ if (this.hasChanged || !config.OS.isWindows)
return value;
return /sh/.test(options["shell"]) ? "-c" : "/c";
}
diff --git a/common/modules/overlay.jsm b/common/modules/overlay.jsm
index 08c2829d..ec329f05 100644
--- a/common/modules/overlay.jsm
+++ b/common/modules/overlay.jsm
@@ -8,7 +8,7 @@ try {
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("overlay", {
- exports: ["ModuleBase"],
+ exports: ["ModuleBase", "overlay"],
require: ["config", "io", "services", "util"]
}, this);
@@ -28,11 +28,13 @@ var ModuleBase = Class("ModuleBase", {
var Overlay = Module("Overlay", {
init: function init() {
+ let overlay = this;
+
services["dactyl:"]; // Hack. Force module initialization.
config.loadStyles();
- util.overlayWindow(config.overlayChrome, function overlay(window) ({
+ util.overlayWindow(config.overlayChrome, function _overlay(window) ({
init: function onInit(document) {
/**
* @constructor Module
@@ -308,9 +310,15 @@ var Overlay = Module("Overlay", {
defineModule.loadLog.push("Loaded in " + (Date.now() - start) + "ms");
+ util.dump(overlay);
+
+ overlay.windows = array.uniq(overlay.windows.concat(window), true);
+
modules.events.listen(window, "unload", function onUnload() {
window.removeEventListener("unload", onUnload.wrapped, false);
+ overlay.windows = overlay.windows.filter(function (w) w != window);
+
for each (let mod in modules.moduleList.reverse()) {
mod.stale = true;
@@ -320,7 +328,19 @@ var Overlay = Module("Overlay", {
}, false);
}
}));
- }
+ },
+
+ /**
+ * The most recently active dactyl window.
+ */
+ get activeWindow() this.windows[0],
+
+ set activeWindow(win) this.windows = [win].concat(this.windows.filter(function (w) w != win)),
+
+ /**
+ * A list of extant dactyl windows.
+ */
+ windows: Class.memoize(function () [])
});
endModule();
diff --git a/common/modules/styles.jsm b/common/modules/styles.jsm
index 65c32ec2..d164c92e 100644
--- a/common/modules/styles.jsm
+++ b/common/modules/styles.jsm
@@ -702,7 +702,7 @@ var Styles = Module("Styles", {
}));
},
completion: function (dactyl, modules, window) {
- const names = Array.slice(util.computedStyle(window.document.createElement("div")));
+ const names = Array.slice(DOM(
, window.document).style);
modules.completion.css = function (context) {
context.title = ["CSS Property"];
context.keys = { text: function (p) p + ":", description: function () "" };
diff --git a/common/modules/util.jsm b/common/modules/util.jsm
index 59ba36bd..eeae6adf 100644
--- a/common/modules/util.jsm
+++ b/common/modules/util.jsm
@@ -13,7 +13,7 @@ Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("util", {
exports: ["$", "DOM", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
require: ["services"],
- use: ["commands", "config", "highlight", "messages", "storage", "template"]
+ use: ["commands", "config", "highlight", "messages", "overlay", "storage", "template"]
}, this);
var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
@@ -85,7 +85,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
}
},
- get activeWindow() this.windows[0],
+ activeWindow: deprecated("overlay.activeWindow", { get: function activeWindow() overlay.activeWindow }),
dactyl: update(function dactyl(obj) {
if (obj)
@@ -93,7 +93,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
return {
__noSuchMethod__: function (meth, args) {
- let win = util.activeWindow;
+ let win = overlay.activeWindow;
var dactyl = global && global.dactyl || win && win.dactyl;
if (!dactyl)
@@ -204,55 +204,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
return RegExp("[" + util.regexp.escape(list) + "]");
},
- get chromePackages() {
- // Horrible hack.
- let res = {};
- function process(manifest) {
- for each (let line in manifest.split(/\n+/)) {
- let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
- if (match)
- res[match[2]] = true;
- }
- }
- function processJar(file) {
- let jar = services.ZipReader(file);
- if (jar) {
- if (jar.hasEntry("chrome.manifest"))
- process(File.readStream(jar.getInputStream("chrome.manifest")));
- jar.close();
- }
- }
-
- for each (let dir in ["UChrm", "AChrom"]) {
- dir = File(services.directory.get(dir, Ci.nsIFile));
- if (dir.exists() && dir.isDirectory())
- for (let file in dir.iterDirectory())
- if (/\.manifest$/.test(file.leafName))
- process(file.read());
-
- dir = File(dir.parent);
- if (dir.exists() && dir.isDirectory())
- for (let file in dir.iterDirectory())
- if (/\.jar$/.test(file.leafName))
- processJar(file);
-
- dir = dir.child("extensions");
- if (dir.exists() && dir.isDirectory())
- for (let ext in dir.iterDirectory()) {
- if (/\.xpi$/.test(ext.leafName))
- processJar(ext);
- else {
- if (ext.isFile())
- ext = File(ext.read().replace(/\n*$/, ""));
- let mf = ext.child("chrome.manifest");
- if (mf.exists())
- process(mf.read());
- }
- }
- }
- return Object.keys(res).sort();
- },
-
/**
* Returns a shallow copy of *obj*.
*
@@ -451,79 +402,24 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
return stack.top;
},
- /**
- * Compiles a CSS spec and XPath pattern matcher based on the given
- * list. List elements prefixed with "xpath:" are parsed as XPath
- * patterns, while other elements are parsed as CSS specs. The
- * returned function will, given a node, return an iterator of all
- * descendants of that node which match the given specs.
- *
- * @param {[string]} list The list of patterns to match.
- * @returns {function(Node)}
- */
- compileMatcher: function compileMatcher(list) {
- let xpath = [], css = [];
- for (let elem in values(list))
- if (/^xpath:/.test(elem))
- xpath.push(elem.substr(6));
- else
- css.push(elem);
+ compileMatcher: deprecated("DOM.compileMatcher", { get: function compileMatcher() DOM.compileMatcher }),
+ computedStyle: deprecated("DOM#style", function computedStyle(elem) DOM(elem).style),
+ domToString: deprecated("DOM.stringify", { get: function domToString() DOM.stringify }),
+ editableInputs: deprecated("DOM.editableInputs", { get: function editableInputs(elem) DOM.editableInputs }),
+ escapeHTML: deprecated("DOM.escapeHTML", { get: function escapeHTML(elem) DOM.escapeHTML }),
+ evaluateXPath: deprecated("DOM.XPath",
+ function evaluateXPath(path, elem, asIterator) DOM.XPath(path, elem || util.activeWindow.content.document, asIterator)),
+ isVisible: deprecated("DOM#isVisible", function isVisible(elem) DOM(elem).isVisible),
+ makeXPath: deprecated("DOM.makeXPath", { get: function makeXPath(elem) DOM.makeXPath }),
+ namespaces: deprecated("DOM.namespaces", { get: function namespaces(elem) DOM.namespaces }),
+ namespaceNames: deprecated("DOM.namespaceNames", { get: function namespaceNames(elem) DOM.namespaceNames }),
+ parseForm: deprecated("DOM#formData", function parseForm(elem) values(DOM(elem).formData).toArray()),
+ scrollIntoView: deprecated("DOM#scrollIntoView", function scrollIntoView(elem, alignWithTop) DOM(elem).scrollIntoView(alignWithTop)),
+ validateMatcher: deprecated("DOM.validateMatcher", { get: function validateMatcher() DOM.validateMatcher }),
- return update(
- function matcher(node) {
- if (matcher.xpath)
- for (let elem in util.evaluateXPath(matcher.xpath, node))
- yield elem;
-
- if (matcher.css)
- for (let [, elem] in iter(node.querySelectorAll(matcher.css)))
- yield elem;
- }, {
- css: css.join(", "),
- xpath: xpath.join(" | ")
- });
- },
-
- /**
- * Validates a list as input for {@link #compileMatcher}. Returns
- * true if and only if every element of the list is a valid XPath or
- * CSS selector.
- *
- * @param {[string]} list The list of patterns to test
- * @returns {boolean} True when the patterns are all valid.
- */
- validateMatcher: function validateMatcher(list) {
- let evaluator = services.XPathEvaluator();
- let node = services.XMLDocument();
- return this.testValues(list, function (value) {
- if (/^xpath:/.test(value))
- evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver);
- else
- node.querySelector(value);
- return true;
- });
- },
-
- /**
- * Returns an object representing a Node's computed CSS style.
- *
- * @param {Node} node
- * @returns {Object}
- */
- computedStyle: function computedStyle(node) {
- while (!(node instanceof Ci.nsIDOMElement) && node.parentNode)
- node = node.parentNode;
- try {
- var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
- }
- catch (e) {}
- if (res == null) {
- util.dumpStack(_("error.nullComputedStyle", node));
- Cu.reportError(Error(_("error.nullComputedStyle", node)));
- return {};
- }
- return res;
- },
+ chromePackages: deprecated("config.chromePackages", { get: function chromePackages() config.chromePackages }),
+ haveGecko: deprecated("config.haveGecko", { get: function haveGecko() config.closure.haveGecko }),
+ OS: deprecated("config.OS", { get: function OS() config.OS }),
/**
* Converts any arbitrary string into an URI object. Returns null on
@@ -613,44 +509,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
dequote: function dequote(pattern, chars)
pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0),
- /**
- * Converts a given DOM Node, Range, or Selection to a string. If
- * *html* is true, the output is HTML, otherwise it is presentation
- * text.
- *
- * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
- * stringify.
- * @param {boolean} html Whether the output should be HTML rather
- * than presentation text.
- */
- domToString: function (node, html) {
- if (node instanceof Ci.nsISelection && node.isCollapsed)
- return "";
-
- if (node instanceof Ci.nsIDOMNode) {
- let range = node.ownerDocument.createRange();
- range.selectNode(node);
- node = range;
- }
- let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
- doc = doc.ownerDocument || doc;
-
- let encoder = services.HtmlEncoder();
- encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
- if (node instanceof Ci.nsISelection)
- encoder.setSelection(node);
- else if (node instanceof Ci.nsIDOMRange)
- encoder.setRange(node);
-
- let str = services.String(encoder.encodeToString());
- if (html)
- return str.data;
-
- let [result, length] = [{}, {}];
- services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
- return result.value.QueryInterface(Ci.nsISupportsString).data;
- },
-
/**
* Prints a message to the console. If *msg* is an object it is pretty
* printed.
@@ -687,25 +545,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
util.dump((arguments.length == 0 ? "Stack" : msg) + "\n" + stack + "\n");
},
- /**
- * The set of input element type attribute values that mark the element as
- * an editable field.
- */
- editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
- "month", "number", "password", "range", "search",
- "tel", "text", "time", "url", "week"]),
-
- /**
- * 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(/= 0,
-
/**
* Sends a synchronous or asynchronous HTTP request to *url* and returns
* the XMLHttpRequest object. If *callback* is specified the request is
@@ -1017,24 +761,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
*/
isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
- /** Dactyl's notion of the current operating system platform. */
- OS: memoize({
- _arch: services.runtime.OS,
- /**
- * @property {string} The normalised name of the OS. This is one of
- * "Windows", "Mac OS X" or "Unix".
- */
- get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
- /** @property {boolean} True if the OS is Windows. */
- get isWindows() this._arch == "WINNT",
- /** @property {boolean} True if the OS is Mac OS X. */
- get isMacOSX() this._arch == "Darwin",
- /** @property {boolean} True if the OS is some other *nix variant. */
- get isUnix() !this.isWindows && !this.isMacOSX,
- /** @property {RegExp} A RegExp which matches illegal characters in path components. */
- get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g
- }),
-
/**
* Returns true if *host* is a subdomain of *domain*.
*
@@ -1049,17 +775,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
},
- /**
- * Returns true if the given DOM node is currently visible.
- *
- * @param {Node} node
- * @returns {boolean}
- */
- isVisible: function (node) {
- let style = util.computedStyle(node);
- return style.visibility == "visible" && style.display != "none";
- },
-
/**
* Iterates over all currently open documents, including all
* top-level window and sub-frames thereof.
@@ -1109,20 +824,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
}, this).join("%25").replace(/[\s.,>)]$/, encodeURIComponent);
},
- /**
- * 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 array(nodes).map(util.debrace).flatten()
- .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
- .map(function (node) "//" + node).join(" | ");
- },
-
/**
* Creates a DTD fragment from the given object. Each property of
* the object is converted to an ENTITY declaration. SGML special
@@ -1150,7 +851,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
* @param {string} uri
* @returns {nsIURI}
*/
- // FIXME: createURI needed too?
newURI: function newURI(uri, charset, base) this.withProperErrors("newURI", services.io, uri, charset, base),
/**
@@ -1181,43 +881,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
if (!isObject(object))
return String(object);
- function namespaced(node) {
- var ns = util.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
- if (!ns)
- return node.localName;
- if (color)
- return <>
{ns}{node.localName}>
- return ns + ":" + node.localName;
- }
-
if (object instanceof Ci.nsIDOMElement) {
let elem = object;
if (elem.nodeType == elem.TEXT_NODE)
return elem.data;
- try {
- let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
- if (color)
- return
<{
- namespaced(elem)} {
- template.map(array.iterValues(elem.attributes),
- function (attr)
- {namespaced(attr)} +
- {attr.value},
- <> >)
- }{ !hasChildren ? "/>" : ">"
- }{ !hasChildren ? "" : <>...> +
- <{namespaced(elem)}>
- };
-
- let tag = "<" + [namespaced(elem)].concat(
- [namespaced(a) + "=" + template.highlight(a.value, true)
- for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
- return tag + (!hasChildren ? "/>" : ">..." + namespaced(elem) + ">");
- }
- catch (e) {
- return {}.toString.call(elem);
- }
+ return DOM(elem).repr(color);
}
try { // for window.JSON
@@ -1486,68 +1155,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
}
},
- /**
- * Parses the fields of a form and returns a URL/POST-data pair
- * that is the equivalent of submitting the form.
- *
- * @param {nsINode} field One of the fields of the given form.
- * @returns {array}
- */
- // Nuances gleaned from browser.jar/content/browser/browser.js
- parseForm: function parseForm(field) {
- function encode(name, value, param) {
- param = param ? "%s" : "";
- if (post)
- return name + "=" + encodeComponent(value + param);
- return encodeComponent(name) + "=" + encodeComponent(value) + param;
- }
-
- let form = field.form;
- let doc = form.ownerDocument;
-
- let charset = doc.characterSet;
- let converter = services.CharsetConv(charset);
- for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
- let c = services.CharsetConv(cs);
- if (c) {
- converter = services.CharsetConv(cs);
- charset = cs;
- }
- }
-
- let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
- let url = util.newURI(form.action, charset, uri).spec;
-
- let post = form.method.toUpperCase() == "POST";
-
- let encodeComponent = encodeURIComponent;
- if (charset !== "UTF-8")
- encodeComponent = function encodeComponent(str)
- escape(converter.ConvertFromUnicode(str) + converter.Finish());
-
- let elems = [];
- if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
- elems.push(encode(field.name, field.value));
-
- for (let [, elem] in iter(form.elements))
- if (elem.name && !elem.disabled) {
- if (Set.has(util.editableInputs, elem.type)
- || /^(?:hidden|textarea)$/.test(elem.type)
- || elem.type == "submit" && elem == field
- || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
- elems.push(encode(elem.name, elem.value, elem === field));
- else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
- for (let [, opt] in Iterator(elem.options))
- if (opt.selected)
- elems.push(encode(elem.name, opt.value));
- }
- }
-
- if (post)
- return [url, elems.join('&'), charset, elems];
- return [url + "?" + elems.join('&'), null, charset, elems];
- },
-
/**
* A generator that returns the values between *start* and *end*, in *step*
* increments.
@@ -1789,19 +1396,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
return ary.filter(function (h) h.length >= base.length);
},
- /**
- * 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, alignWithTop) {
- let win = elem.ownerDocument.defaultView;
- let rect = elem.getBoundingClientRect();
- if (!(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0))
- elem.scrollIntoView(arguments.length > 1 ? alignWithTop : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom));
- },
-
/**
* Returns the selection controller for the given window.
*
@@ -1813,6 +1407,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
.QueryInterface(Ci.nsISelectionController),
+ /**
+ * Escapes a string against shell meta-characters and argument
+ * separators.
+ */
+ shellEscape: function shellEscape(str) '"' + String.replace(str, /[\\"$]/g, "\\$&") + '"',
+
/**
* Suspend execution for at least *delay* milliseconds. Functions by
* yielding execution to the next item in the main event queue, and
@@ -2139,6 +1739,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
Array: array
});
+
+/**
+ * @class
+ *
+ * A jQuery-inspired DOM utility framework.
+ *
+ * Please note that while this currently implements an Array-like
+ * interface, this is *not a defined interface* and is very likely to
+ * change in the near future.
+ */
var DOM = Class("DOM", {
init: function init(val, context) {
let self;
@@ -2153,12 +1763,15 @@ var DOM = Class("DOM", {
if (val == null)
;
else if (typeof val == "xml")
- this[length++] = util.xmlToDom(val, context);
- else if (val instanceof Ci.nsIDOMNode)
+ this[length++] = util.xmlToDom(val, context, this.nodes);
+ else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
this[length++] = val;
else if ("length" in val)
for (let i = 0; i < val.length; i++)
this[length++] = val[i];
+ else if ("__iterator__" in val)
+ for (let elem in val)
+ this[length++] = elem;
this.length = length;
return self || this;
@@ -2171,6 +1784,8 @@ var DOM = Class("DOM", {
Empty: function Empty() this.constructor(null, this.document),
+ nodes: Class.memoize(function () ({})),
+
get items() {
for (let i = 0; i < this.length; i++)
yield this.eq(i);
@@ -2234,10 +1849,10 @@ var DOM = Class("DOM", {
if (callable(val))
return this.each(function (elem, i) {
- fn.call(this, munge(val.call(this, elem, i)), elem, i);
+ util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
}, self || this);
- fn.call(self || this, munge(val), this[0], 0);
+ util.withProperErrors(fn, self || this, munge(val), this[0], 0);
return this;
},
@@ -2276,8 +1891,16 @@ var DOM = Class("DOM", {
let res = this.Empty();
this.each(function (elem) {
- while((elem = fn.call(this, elem)) instanceof Ci.nsIDOMElement)
- res[res.length++] = elem;
+ while(true) {
+ elem = fn.call(this, elem)
+ if (elem instanceof Ci.nsIDOMElement)
+ res[res.length++] = elem;
+ else if (elem && "length" in elem)
+ for (let i = 0; i < tmp.length; i++)
+ res[res.length++] = tmp[j];
+ else
+ break;
+ }
}, self || this);
return res;
},
@@ -2288,10 +1911,10 @@ var DOM = Class("DOM", {
for (let i = 0; i < this.length; i++) {
let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
- if ("length" in tmp)
+ if (tmp && "length" in tmp)
for (let j = 0; j < tmp.length; j++)
res[res.length++] = tmp[j];
- else
+ else if (tmp !== undefined)
res[res.length++] = tmp;
}
@@ -2311,6 +1934,15 @@ var DOM = Class("DOM", {
get parent() this.map(function (elem) elem.parentNode, this),
+ get offsetParent() this.map(function (elem) {
+ do {
+ var parent = elem.offsetParent;
+ if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
+ return parent;
+ }
+ while (parent);
+ }, this),
+
get ancestors() this.all(function (elem) elem.parentNode),
get children() this.map(function (elem) Array.filter(elem.childNodes,
@@ -2354,6 +1986,7 @@ var DOM = Class("DOM", {
has: function has(hl) ~this.list.indexOf(hl),
add: function add(hl) self.each(function () {
+ highlight.loaded[hl] = true;
this.attrNS(NS, "highlight",
array.uniq(this.highlight.list.concat(hl)).join(" "));
}),
@@ -2371,7 +2004,213 @@ var DOM = Class("DOM", {
get rect() this[0].getBoundingClientRect(),
- get style() util.computedStyle(this[0]),
+ get viewport() {
+ let r = this.rect;
+ return {
+ width: this[0].clientWidth,
+ height: this[0].clientHeight,
+ top: r.top + this[0].clientTop,
+ get bottom() this.top + this.height,
+ left: r.left + this[0].clientLeft,
+ get right() this.left + this.width
+ }
+ },
+
+ /**
+ * Returns true if the given DOM node is currently visible.
+ * @returns {boolean}
+ */
+ get isVisible() {
+ let style = this.style;
+ return style.visibility == "visible" && style.display != "none";
+ },
+
+ get editor() {
+ this[0] instanceof Ci.nsIDOMNSEditableElement;
+ if (this[0].editor instanceof Ci.nsIEditor)
+ return this[0].editor;
+
+ try {
+ return this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
+ .getEditorForWindow(this[0]);
+ }
+ catch (e) {}
+
+ return null;
+ },
+
+ get isEditable() !!this.editor,
+
+ get isInput() this[0] instanceof Ci.nsIDOMHTMLInputElement && this.isEditable,
+
+ /**
+ * Returns an object representing a Node's computed CSS style.
+ * @returns {Object}
+ */
+ get style() {
+ let node = this[0];
+ while (!(node instanceof Ci.nsIDOMElement) && node.parentNode)
+ node = node.parentNode;
+
+ try {
+ var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
+ }
+ catch (e) {}
+
+ if (res == null) {
+ util.dumpStack(_("error.nullComputedStyle", node));
+ Cu.reportError(Error(_("error.nullComputedStyle", node)));
+ return {};
+ }
+ return res;
+ },
+
+ /**
+ * Parses the fields of a form and returns a URL/POST-data pair
+ * that is the equivalent of submitting the form.
+ *
+ * @returns {object} An object with the following elements:
+ * url: The URL the form points to.
+ * postData: A string containing URL-encoded post data, if this
+ * form is to be POSTed
+ * charset: The character set of the GET or POST data.
+ * elements: The key=value pairs used to generate query information.
+ */
+ // Nuances gleaned from browser.jar/content/browser/browser.js
+ get formData() {
+ function encode(name, value, param) {
+ param = param ? "%s" : "";
+ if (post)
+ return name + "=" + encodeComponent(value + param);
+ return encodeComponent(name) + "=" + encodeComponent(value) + param;
+ }
+
+ let field = this[0];
+ let form = field.form;
+ let doc = form.ownerDocument;
+
+ let charset = doc.characterSet;
+ let converter = services.CharsetConv(charset);
+ for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
+ let c = services.CharsetConv(cs);
+ if (c) {
+ converter = services.CharsetConv(cs);
+ charset = cs;
+ }
+ }
+
+ let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
+ let url = util.newURI(form.action, charset, uri).spec;
+
+ let post = form.method.toUpperCase() == "POST";
+
+ let encodeComponent = encodeURIComponent;
+ if (charset !== "UTF-8")
+ encodeComponent = function encodeComponent(str)
+ escape(converter.ConvertFromUnicode(str) + converter.Finish());
+
+ let elems = [];
+ if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
+ elems.push(encode(field.name, field.value));
+
+ for (let [, elem] in iter(form.elements))
+ if (elem.name && !elem.disabled) {
+ if (DOM(elem).isInput
+ || /^(?:hidden|textarea)$/.test(elem.type)
+ || elem.type == "submit" && elem == field
+ || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
+ elems.push(encode(elem.name, elem.value, elem === field));
+ else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
+ for (let [, opt] in Iterator(elem.options))
+ if (opt.selected)
+ elems.push(encode(elem.name, opt.value));
+ }
+ }
+
+ if (post)
+ return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
+ return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
+ },
+
+ /**
+ * Generates an XPath expression for the given element.
+ *
+ * @returns {string}
+ */
+ get xpath() {
+ function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
+
+ let res = [];
+ let doc = this.document;
+ for (let elem = this[0];; elem = elem.parentNode) {
+ if (!(elem instanceof Ci.nsIDOMElement))
+ res.push("");
+ else if (elem.id)
+ res.push("id(" + quote(elem.id) + ")");
+ else {
+ let name = elem.localName;
+ if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
+ if (elem.namespaceURI in DOM.namespaceNames)
+ name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
+ else
+ name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
+
+ res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
+ continue;
+ }
+ break;
+ }
+
+ return res.reverse().join("/");
+ },
+
+ /**
+ * Returns a string or XML representation of this node.
+ *
+ * @param {boolean} color If true, return a colored, XML
+ * representation of this node.
+ */
+ repr: function repr(color) {
+ function namespaced(node) {
+ var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
+ if (!ns)
+ return node.localName;
+ if (color)
+ return <>
{ns}{node.localName}>
+ return ns + ":" + node.localName;
+ }
+
+ let res = [];
+ this.each(function (elem) {
+ try {
+ let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
+ if (color)
+ res.push(
<{
+ namespaced(elem)} {
+ template.map(array.iterValues(elem.attributes),
+ function (attr)
+ {namespaced(attr)} +
+ {attr.value},
+ <> >)
+ }{ !hasChildren ? "/>" : ">"
+ }{ !hasChildren ? "" : <>...> +
+ <{namespaced(elem)}>
+ });
+ else {
+ let tag = "<" + [namespaced(elem)].concat(
+ [namespaced(a) + "=" + template.highlight(a.value, true)
+ for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
+
+ res.push(tag + (!hasChildren ? "/>" : ">..." + namespaced(elem) + ">"));
+ }
+ }
+ catch (e) {
+ res.push({}.toString.call(elem));
+ }
+ }, this);
+ return template.map(res, util.identity, <>,>);
+ },
attr: function attr(key, val) {
return this.attrNS("", key, val);
@@ -2574,7 +2413,32 @@ var DOM = Class("DOM", {
if (callable(arg))
return this.listen("blur", arg, extra);
return this.each(function (elem) { elem.blur(); }, this);
- }
+ },
+
+ /**
+ * Scrolls an element into view if and only if it's not already
+ * fully visible.
+ */
+ scrollIntoView: function scrollIntoView(alignWithTop) {
+ return this.each(function (elem) {
+ let rect = this.rect;
+
+ let force = false;
+ if (rect)
+ for (let parent in this.ancestors.items) {
+ let isect = util.intersection(rect, parent.viewport);
+ force = isect.width != rect.width || isect.height != rect.height;
+ if (force)
+ break;
+ }
+
+ let win = this.document.defaultView;
+
+ if (force || !(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0))
+ elem.scrollIntoView(arguments.length ? alignWithTop
+ : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom));
+ });
+ },
}, {
/**
* Creates an actual event from a pseudo-event object.
@@ -2643,7 +2507,7 @@ var DOM = Class("DOM", {
* @param {Event} event The event to dispatch.
*/
dispatch: Class.memoize(function ()
- util.haveGecko("2b")
+ config.haveGecko("2b")
? function dispatch(target, event, extra) {
try {
this.feedingEvent = extra;
@@ -2674,7 +2538,184 @@ var DOM = Class("DOM", {
this.feedingEvent = null;
}
})
- })
+ }),
+
+ /**
+ * The set of input element type attribute values that mark the element as
+ * an editable field.
+ */
+ editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
+ "month", "number", "password", "range", "search",
+ "tel", "text", "time", "url", "week"]),
+
+ /**
+ * Converts a given DOM Node, Range, or Selection to a string. If
+ * *html* is true, the output is HTML, otherwise it is presentation
+ * text.
+ *
+ * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
+ * stringify.
+ * @param {boolean} html Whether the output should be HTML rather
+ * than presentation text.
+ */
+ stringify: function stringify(node, html) {
+ if (node instanceof Ci.nsISelection && node.isCollapsed)
+ return "";
+
+ if (node instanceof Ci.nsIDOMNode) {
+ let range = node.ownerDocument.createRange();
+ range.selectNode(node);
+ node = range;
+ }
+ let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
+ doc = doc.ownerDocument || doc;
+
+ let encoder = services.HtmlEncoder();
+ encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
+ if (node instanceof Ci.nsISelection)
+ encoder.setSelection(node);
+ else if (node instanceof Ci.nsIDOMRange)
+ encoder.setRange(node);
+
+ let str = services.String(encoder.encodeToString());
+ if (html)
+ return str.data;
+
+ let [result, length] = [{}, {}];
+ services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
+ return result.value.QueryInterface(Ci.nsISupportsString).data;
+ },
+
+ /**
+ * Compiles a CSS spec and XPath pattern matcher based on the given
+ * list. List elements prefixed with "xpath:" are parsed as XPath
+ * patterns, while other elements are parsed as CSS specs. The
+ * returned function will, given a node, return an iterator of all
+ * descendants of that node which match the given specs.
+ *
+ * @param {[string]} list The list of patterns to match.
+ * @returns {function(Node)}
+ */
+ compileMatcher: function compileMatcher(list) {
+ let xpath = [], css = [];
+ for (let elem in values(list))
+ if (/^xpath:/.test(elem))
+ xpath.push(elem.substr(6));
+ else
+ css.push(elem);
+
+ return update(
+ function matcher(node) {
+ if (matcher.xpath)
+ for (let elem in DOM.XPath(matcher.xpath, node))
+ yield elem;
+
+ if (matcher.css)
+ for (let [, elem] in iter(node.querySelectorAll(matcher.css)))
+ yield elem;
+ }, {
+ css: css.join(", "),
+ xpath: xpath.join(" | ")
+ });
+ },
+
+ /**
+ * Validates a list as input for {@link #compileMatcher}. Returns
+ * true if and only if every element of the list is a valid XPath or
+ * CSS selector.
+ *
+ * @param {[string]} list The list of patterns to test
+ * @returns {boolean} True when the patterns are all valid.
+ */
+ validateMatcher: function validateMatcher(list) {
+ let evaluator = services.XPathEvaluator();
+ let node = services.XMLDocument();
+ return this.testValues(list, function (value) {
+ if (/^xpath:/.test(value))
+ evaluator.createExpression(value.substr(6), DOM.XPath.resolver);
+ else
+ node.querySelector(value);
+ return true;
+ });
+ },
+
+ /**
+ * Converts HTML special characters in *str* to the equivalent HTML
+ * entities.
+ *
+ * @param {string} str
+ * @returns {string}
+ */
+ escapeHTML: function escapeHTML(str) {
+ let map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" };
+ return str.replace(/['"&<>]/g, function (m) map[m]);
+ },
+
+
+ /**
+ * 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 {Node} elem The context element.
+ * @param {boolean} asIterator Whether to return the results as an
+ * XPath iterator.
+ * @returns {Object} Iterable result of the evaluation.
+ */
+ XPath: update(
+ function XPath(expression, elem, asIterator) {
+ try {
+ let doc = elem.ownerDocument || elem;
+
+ if (isArray(expression))
+ expression = DOM.makeXPath(expression);
+
+ let result = doc.evaluate(expression, elem,
+ XPath.resolver,
+ asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
+ null
+ );
+
+ return Object.create(result, {
+ __iterator__: {
+ value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
+ : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
+ }
+ });
+ }
+ catch (e) {
+ throw e.stack ? e : Error(e);
+ }
+ },
+ {
+ resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
+ }),
+
+ /**
+ * 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 DOM.XPath}.
+ *
+ * @param nodes {Array(string)}
+ * @returns {string}
+ */
+ makeXPath: function makeXPath(nodes) {
+ return array(nodes).map(util.debrace).flatten()
+ .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
+ .map(function (node) "//" + node).join(" | ");
+ },
+
+ namespaces: {
+ xul: XUL.uri,
+ xhtml: XHTML.uri,
+ html: XHTML.uri,
+ xhtml2: "http://www.w3.org/2002/06/xhtml2",
+ dactyl: NS.uri
+ },
+
+ namespaceNames: Class.memoize(function ()
+ iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
});
Object.keys(DOM.Event.types).forEach(function (event) {