From 6a25312c7dbd913322490a6b14674772b3076e63 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sun, 8 Nov 2009 20:54:31 -0500 Subject: [PATCH] Recfactoring: * Standard module format. All modules are explicitly declared as modules, they're created via a constructor and instantiated automatically. They're dependency aware. They stringify properly. * Classes are declared the same way (rather like Structs already were). They also stringify properly. Plus, each instance has a rather nifty closure member that closes all of its methods around 'this', so you can pass them to map, forEach, setTimeout, etc. Modules are themselves classes, with a special metaclass, as it were. * Doug Crockford is dead, metaphorically speaking. Closure-based classes just don't fit into any of the common JavaScript frameworks, and they're inefficient and confusing. Now, all class and module members are accessed explicitly via 'this', which makes it very clear that they're class members and not (e.g.) local variables, without anything nasty like Hungarian notation. * Strictly one module per file. Classes that belong to a module live in the same file. * For the moment, there are quite a few utility functions sitting in base.c, because my class implementation used them, and I haven't had the time or inclination to sort them out. I plan to reconcile them with the current mess that is the util namespace. * Changed bracing style. --- HACKING | 76 +- common/content/autocommands.js | 275 ++ common/content/base.js | 293 ++ common/content/bookmarks.js | 1931 ++++------- common/content/browser.js | 445 ++- common/content/buffer.js | 2905 +++++++---------- common/content/commandline.js | 1867 +++++++++++ common/content/commands.js | 1983 ++++++------ common/content/completion.js | 1707 +++++----- common/content/editor.js | 2137 ++++++------- common/content/eval.js | 3 +- common/content/events.js | 2989 ++++++++---------- common/content/finder.js | 823 +++-- common/content/help.js | 3 +- common/content/hints.js | 1016 +++--- common/content/history.js | 233 ++ common/content/io.js | 1978 ++++++------ common/content/liberator-overlay.js | 38 +- common/content/liberator.js | 2689 +++++++--------- common/content/liberator.xul | 27 +- common/content/mappings.js | 766 +++-- common/content/marks.js | 343 ++ common/content/modes.js | 386 ++- common/content/modules.js | 73 + common/content/options.js | 2072 ++++++------ common/content/quickmarks.js | 173 + common/content/sanitizer.js | 455 ++- common/content/services.js | 172 +- common/content/statusline.js | 245 ++ common/content/style.js | 597 ++-- common/content/tabs.js | 2110 ++++++------- common/content/template.js | 141 +- common/content/ui.js | 2312 -------------- common/content/util.js | 518 ++- muttator/components/about-handler.js | 2 +- muttator/components/commandline-handler.js | 2 +- muttator/content/config.js | 12 - vimperator/Makefile | 2 +- vimperator/chrome.manifest | 2 +- vimperator/components/about-handler.js | 2 +- vimperator/components/commandline-handler.js | 2 +- vimperator/content/config.js | 34 +- vimperator/install.rdf | 2 +- xulmus/components/about-handler.js | 2 +- xulmus/components/commandline-handler.js | 2 +- xulmus/content/config.js | 14 + 46 files changed, 15950 insertions(+), 17909 deletions(-) create mode 100644 common/content/autocommands.js create mode 100644 common/content/base.js create mode 100644 common/content/commandline.js create mode 100644 common/content/history.js create mode 100644 common/content/marks.js create mode 100644 common/content/modules.js create mode 100644 common/content/quickmarks.js create mode 100644 common/content/statusline.js delete mode 100644 common/content/ui.js diff --git a/HACKING b/HACKING index b2ee6c37..f8e1db72 100644 --- a/HACKING +++ b/HACKING @@ -18,16 +18,15 @@ important, please ask. == Coding Style == In general: Just look at the existing source code! -We try to be quite consistent, but of course, that's not always possible. -Also we try to target experienced JavaScript developers which do not -necessarily need to have a good understanding of Vimperator's source code, nor -do they probably know in-depth concepts of other languages like Lisp or Python. -Therefore, the coding style should feel natural to any JavaScript developer -so it is easy to read and understand. Of course, this does not mean, you have -to avoid all new JavaScript features like list comprehension or generators. -Use them, when they make sense, but don't use them, when the resulting code -is hard to read. +We try to target experienced JavaScript developers who do not +necessarily need to have a good understanding of Vimperator's source +code, nor necessarily understand in-depth concepts of other +languages like Lisp or Python. Therefore, the coding style should +feel natural to any JavaScript developer. Of course, this does not +mean, you have to avoid all new JavaScript features like list +comprehension or generators. Use them, when they make sense, but +don't use them when the resulting code is hard to read. === The most important style issues are: === @@ -40,29 +39,41 @@ is hard to read. * Use " for enclosing strings instead of ', unless using ' avoids escaping of lots of " Example: alert("foo") instead of alert('foo'); +* Use // regexp literals rather than RegExp constructors, unless + you're constructing an expression on the fly, or RegExp + constructors allow you to escape less /s than the additional + escaping of special characters required by string quoting. + + Good: /application\/xhtml\+xml/ + Bad: RegExp("application/xhtml\\+xml") + Good: RegExp("http://(www\\.)vimperator.org/(.*)/(.*)") + Bad: /http:\/\/(www\.)vimperator.org\/(.*)\/(.*)/ + * Exactly one space after if/for/while/catch etc. and after a comma, but none after a parenthesis or after a function call: for (pre; condition; post) but: alert("foo"); -* Opening curly brackets { must be on a new line, unless it is used in a closure: - function myFunction () - { - if (foo) - { - baz = false; - return bar; - } - else - { - return baz; - } - } - but: - setTimeout(function () { - ... - }); +* Bracing is formatted as follows: + function myFunction () { + if (foo) + return bar; + else { + baz = false; + return baz; + } + } + var quux = frob("you", + { + a: 1, + b: 42, + c: { + hoopy: "frood" + } + }); + + When in doubt, look for similar code. * No braces for one-line conditional statements: Right: @@ -97,9 +108,9 @@ is hard to read. * Use UNIX new lines (\n), not windows (\r\n) or old Mac ones (\r) -* Use Iterators, Array#forEach, or for (let i = 0; i < ary.length; i++) - to iterate over arrays. for (let i in ary) and for each (let i in ary) - include members in an Array.prototype, which some extensions alter. +* Use Iterators or Array#forEach to iterate over arrays. for (let i + in ary) and for each (let i in ary) include members in an + Array.prototype, which some extensions alter. Right: for (let [,elem] in Iterator(ary)) for (let [k, v] in Iterator(obj)) @@ -137,11 +148,4 @@ Additionally, maybe there should be some benchmark information here -- something to let a developer know what's "too" slow...? Or general guidelines about optimization? -== Source Code Management == - -TODO: Document the existence of remote branches and discuss when and how - to push to them. At least provide an index so that devs know where - to look if an old branch needs to be maintained or a feature needs - to be added to a new branch. - // vim: set ft=asciidoc fdm=marker sw=4 ts=4 et ai: diff --git a/common/content/autocommands.js b/common/content/autocommands.js new file mode 100644 index 00000000..5ce1ab49 --- /dev/null +++ b/common/content/autocommands.js @@ -0,0 +1,275 @@ +// Copyright (c) 2006-2009 by Martin Stubenschrott +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. + + +/** @scope modules */ + +const AutoCommand = new Struct("event", "pattern", "command"); + +/** + * @instance autocommands + */ +const AutoCommands = Module("autocommands", { + init: function () { + this._store = []; + }, + + __iterator__: function () util.Array.itervalues(this._store), + + /** + * Adds a new autocommand. cmd will be executed when one of the + * specified events occurs and the URL of the applicable buffer + * matches regex. + * + * @param {Array} events The array of event names for which this + * autocommand should be executed. + * @param {string} regex The URL pattern to match against the buffer URL. + * @param {string} cmd The Ex command to run. + */ + add: function (events, regex, cmd) { + if (typeof events == "string") { + events = events.split(","); + liberator.log("DEPRECATED: the events list arg to autocommands.add() should be an array of event names"); + } + events.forEach(function (event) { + this._store.push(new AutoCommand(event, RegExp(regex), cmd)); + }); + }, + + /** + * Returns all autocommands with a matching event and + * regex. + * + * @param {string} event The event name filter. + * @param {string} regex The URL pattern filter. + * @returns {AutoCommand[]} + */ + get: function (event, regex) { + return this._store.filter(function (autoCmd) matchAutoCmd(autoCmd, event, regex)); + }, + + /** + * Deletes all autocommands with a matching event and + * regex. + * + * @param {string} event The event name filter. + * @param {string} regex The URL pattern filter. + */ + remove: function (event, regex) { + this._store = this._store.filter(function (autoCmd) !matchAutoCmd(autoCmd, event, regex)); + }, + + /** + * Lists all autocommands with a matching event and + * regex. + * + * @param {string} event The event name filter. + * @param {string} regex The URL pattern filter. + */ + list: function (event, regex) { + let cmds = {}; + + // XXX + this._store.forEach(function (autoCmd) { + if (matchAutoCmd(autoCmd, event, regex)) { + cmds[autoCmd.event] = cmds[autoCmd.event] || []; + cmds[autoCmd.event].push(autoCmd); + } + }); + + let list = template.commandOutput( + + + + + { + template.map(cmds, function ([event, items]) + + + + + + template.map(items, function (item) + + + + )) + } +
----- Auto Commands -----
{event}
 {item.pattern.source}{item.command}
); + + commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + }, + + /** + * Triggers the execution of all autocommands registered for + * event. A map of args is passed to each autocommand + * when it is being executed. + * + * @param {string} event The event to fire. + * @param {Object} args The args to pass to each autocommand. + */ + trigger: function (event, args) { + if (options.get("eventignore").has("all", event)) + return; + + let autoCmds = this._store.filter(function (autoCmd) autoCmd.event == event); + + liberator.echomsg("Executing " + event + " Auto commands for \"*\"", 8); + + let lastPattern = null; + let url = args.url || ""; + + for (let [, autoCmd] in Iterator(autoCmds)) { + if (autoCmd.pattern.test(url)) { + if (!lastPattern || lastPattern.source != autoCmd.pattern.source) + liberator.echomsg("Executing " + event + " Auto commands for \"" + autoCmd.pattern.source + "\"", 8); + + lastPattern = autoCmd.pattern; + liberator.echomsg("autocommand " + autoCmd.command, 9); + + if (typeof autoCmd.command == "function") { + try { + autoCmd.command.call(autoCmd, args); + } + catch (e) { + liberator.reportError(e); + liberator.echoerr(e); + } + } + else + liberator.execute(commands.replaceTokens(autoCmd.command, args), null, true); + } + } + } +}, { + matchAutoCmd: function (autoCmd, event, regex) { + return (!event || autoCmd.event == event) && (!regex || autoCmd.pattern.source == regex); + }, +}, { + commands: function () { + commands.add(["au[tocmd]"], + "Execute commands automatically on events", + function (args) { + let [event, regex, cmd] = args; + let events = []; + + try { + RegExp(regex); + } + catch (e) { + liberator.assert(false, "E475: Invalid argument: " + regex); + } + + if (event) { + // NOTE: event can only be a comma separated list for |:au {event} {pat} {cmd}| + let validEvents = config.autocommands.map(function (event) event[0]); + validEvents.push("*"); + + events = event.split(","); + liberator.assert(events.every(function (event) validEvents.indexOf(event) >= 0), + "E216: No such group or event: " + event); + } + + if (cmd) { // add new command, possibly removing all others with the same event/pattern + if (args.bang) + autocommands.remove(event, regex); + if (args["-javascript"]) + cmd = eval("(function (args) { with(args) {" + cmd + "} })"); + autocommands.add(events, regex, cmd); + } + else { + if (event == "*") + event = null; + + if (args.bang) { + // TODO: "*" only appears to work in Vim when there is a {group} specified + if (args[0] != "*" || regex) + autocommands.remove(event, regex); // remove all + } + else + autocommands.list(event, regex); // list all + } + }, { + bang: true, + completer: function (context) completion.autocmdEvent(context), + literal: 2, + options: [[["-javascript", "-js"], commands.OPTION_NOARG]] + }); + + [ + { + name: "do[autocmd]", + description: "Apply the autocommands matching the specified URL pattern to the current buffer" + }, { + name: "doautoa[ll]", + description: "Apply the autocommands matching the specified URL pattern to all buffers" + } + ].forEach(function (command) { + commands.add([command.name], + command.description, + // TODO: Perhaps this should take -args to pass to the command? + function (args) { + // Vim compatible + if (args.length == 0) + return void liberator.echomsg("No matching autocommands"); + + let [event, url] = args; + let defaultURL = url || buffer.URL; + let validEvents = config.autocommands.map(function (e) e[0]); + + // TODO: add command validators + liberator.assert(event != "*", + "E217: Can't execute autocommands for ALL events"); + liberator.assert(validEvents.indexOf(event) >= 0, + "E216: No such group or event: " + args); + liberator.assert(autocommands.get(event).some(function (c) c.pattern.test(defaultURL)), + "No matching autocommands"); + + if (this.name == "doautoall" && liberator.has("tabs")) { + let current = tabs.index(); + + for (let i = 0; i < tabs.count; i++) { + tabs.select(i); + // if no url arg is specified use the current buffer's URL + autocommands.trigger(event, { url: url || buffer.URL }); + } + + tabs.select(current); + } + else + autocommands.trigger(event, { url: defaultURL }); + }, { + argCount: "*", // FIXME: kludged for proper error message should be "1". + completer: function (context) completion.autocmdEvent(context) + }); + }); + }, + completion: function () { + completion.setFunctionCompleter(autocommands.get, [function () config.autocommands]); + + completion.autocmdEvent = function autocmdEvent(context) { + context.completions = config.autocommands; + }; + + completion.macro = function macro(context) { + context.title = ["Macro", "Keys"]; + context.completions = [item for (item in events.getMacros())]; + }; + }, + options: function () { + options.add(["eventignore", "ei"], + "List of autocommand event names which should be ignored", + "stringlist", "", + { + completer: function () config.autocommands.concat([["all", "All events"]]), + validator: Option.validateCompleter + }); + + options.add(["focuscontent", "fc"], + "Try to stay in normal mode after loading a web page", + "boolean", false); + }, +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/base.js b/common/content/base.js new file mode 100644 index 00000000..119810c6 --- /dev/null +++ b/common/content/base.js @@ -0,0 +1,293 @@ +function array(obj) { + if (isgenerator(obj)) + obj = [k for (k in obj)]; + else if (obj.length) + obj = Array.slice(obj); + return util.Array(obj); +} + +function keys(obj) { + if ('__iterator__' in obj) { + var iter = obj.__iterator__; + yield '__iterator__'; + // This is dangerous, but necessary. + delete obj.__iterator__; + } + for (var k in obj) + if (obj.hasOwnProperty(k)) + yield k; + if (iter !== undefined) + obj.__iterator__ = iter; +} +function values(obj) { + for (var k in obj) + if (obj.hasOwnProperty(k)) + yield obj[k]; +} +function foreach(iter, fn, self) { + for (let val in iter) + fn.call(self, val); +} + +function dict(ary) { + var obj = {}; + for (var i=0; i < ary.length; i++) { + var val = ary[i]; + obj[val[0]] = val[1]; + } + return obj; +} + +function set(ary) { + var obj = {} + if (ary) + for (var i=0; i < ary.length; i++) + obj[ary[i]] = true; + return obj; +} +set.add = function(set, key) { set[key] = true } +set.remove = function(set, key) { delete set[key] } + +function iter(obj) { + if (obj instanceof Ci.nsISimpleEnumerator) + return (function () { + while (obj.hasMoreElements()) + yield obj.getNext(); + })() + if (isinstance(obj, [Ci.nsIStringEnumerator, Ci.nsIUTF8StringEnumerator])) + return (function () { + while (obj.hasMore()) + yield obj.getNext(); + })(); + if (isinstance(obj, Ci.nsIDOMNodeIterator)) + return (function () { + try { + while (true) + yield obj.nextNode() + } + catch (e) {} + })(); + if (isinstance(obj, [HTMLCollection, NodeList])) + return util.Array.iteritems(obj); + if (obj instanceof NamedNodeMap) + return (function () { + for (let i=0; i < obj.length; i++) + yield [obj.name, obj] + })(); + return Iterator(obj); +} + +function issubclass(targ, src) { + return src === targ || + targ && typeof targ === "function" && targ.prototype instanceof src; +} + +function isinstance(targ, src) { + const types = { + boolean: Boolean, + string: String, + function: Function, + number: Number, + } + src = Array.concat(src); + for (var i=0; i < src.length; i++) { + if (targ instanceof src[i]) + return true; + var type = types[typeof targ]; + if (type && issubclass(src[i], type)) + return true; + } + return false; +} + +function isobject(obj) { + return typeof obj === "object" && obj != null; +} + +function isarray(obj) { + return Object.prototype.toString(obj) == "[object Array]"; +} + +function isgenerator(val) { + return Object.prototype.toString(obj) == "[object Generator]"; +} + +function isstring(val) { + return typeof val === "string" || val instanceof String; +} + +function callable(val) { + return typeof val === "function"; +} + +function call(fn) { + fn.apply(arguments[1], Array.slice(arguments, 2)); + return fn; +} + +function curry(fn, length, acc) { + if (length == null) + length = fn.length; + if (length == 0) + return fn; + + /* Close over function with 'this' */ + function close(self, fn) function () fn.apply(self, Array.slice(arguments)); + + let first = (arguments.length < 3); + if (acc == null) + acc = []; + + return function() { + let args = acc.concat(Array.slice(arguments)); + + /* The curried result should preserve 'this' */ + if (arguments.length == 0) + return close(this, arguments.callee); + + if (args.length >= length) + return fn.apply(this, args); + + if (first) + fn = close(this, fn); + return curry(fn, length, args); + } +} + +function update(targ) { + for (let i=1; i < arguments.length; i++) { + let src = arguments[i]; + foreach(keys(src || {}), function(k) { + var get = src.__lookupGetter__(k), + set = src.__lookupSetter__(k); + if (!get && !set) { + var v = src[k]; + targ[k] = v; + if (targ.__proto__ && callable(v)) { + v.superapply = function(self, args) { + return targ.__proto__[k].apply(self, args); + } + v.supercall = function(self) { + return v.superapply(self, Array.slice(arguments, 1)); + } + } + } + if (get) + targ.__defineGetter__(k, get); + if (set) + targ.__defineSetter__(k, set); + }); + } + return targ; +} + +function extend(subc, superc, overrides) { + subc.prototype = { __proto__: superc.prototype }; + update(subc.prototype, overrides); + + subc.superclass = superc.prototype; + subc.prototype.constructor = subc; + subc.prototype.__class__ = subc; + + if (superc.prototype.constructor === Object.prototype.constructor) + superc.prototype.constructor = superc; +} + +function Class() { + function constructor() { + let self = { + __proto__: Constructor.prototype, + constructor: Constructor, + get closure() { + delete this.closure; + const self = this; + return this.closure = dict([k for (k in this) if (!self.__lookupGetter__(k) && callable(self[k]))].map( + function (k) [k, function () self[k].apply(self, arguments)])); + } + }; + var res = self.init.apply(self, arguments); + return res !== undefined ? res : self + } + + var args = Array.slice(arguments); + if (isstring(args[0])) + var name = args.shift(); + var superclass = Class; + if (callable(args[0])) + superclass = args.shift(); + + var Constructor = eval("(function " + (name || superclass.name) + + String.substr(constructor, 20) + ")"); + + if (!('init' in superclass.prototype)) { + var superc = superclass; + superclass = function Shim() {} + extend(superclass, superc, { + init: superc, + }); + } + + extend(Constructor, superclass, args[0]); + update(Constructor, args[1]); + args = args.slice(2); + Array.forEach(args, function(obj) { + if (callable(obj)) + obj = obj.prototype; + update(Constructor.prototype, obj); + }); + return Constructor; +} +Class.toString = function () "[class " + this.constructor.name + "]", +Class.prototype = { + init: function() {}, + toString: function () "[instance " + this.constructor.name + "]", +}; + +const Struct = Class("Struct", { + init: function () { + let args = Array.slice(arguments); + this.__defineGetter__("length", function () args.length); + this.__defineGetter__("members", function () args.slice()); + for (let arg in Iterator(args)) { + let [i, name] = arg; + this.__defineGetter__(name, function () this[i]); + this.__defineSetter__(name, function (val) { this[i] = val; }); + } + function Struct() { + let self = this instanceof arguments.callee ? this : new arguments.callee(); + //for (let [k, v] in Iterator(Array.slice(arguments))) // That is makes using struct twice as slow as the following code: + for (let i = 0; i < arguments.length; i++) { + if (arguments[i] != undefined) + self[i] = arguments[i]; + } + return self; + } + Struct.prototype = this; + Struct.defaultValue = function (key, val) { + let i = args.indexOf(key); + Struct.prototype.__defineGetter__(i, function () (this[i] = val.call(this), this[i])); // Kludge for FF 3.0 + Struct.prototype.__defineSetter__(i, function (val) { + let value = val; + this.__defineGetter__(i, function () value); + this.__defineSetter__(i, function (val) { value = val }); + }); + }; + return this.constructor = Struct; + }, + + clone: function clone() { + return this.constructor.apply(null, this.slice()); + }, + // Iterator over our named members + __iterator__: function () { + let self = this; + return ([v, self[v]] for ([k, v] in Iterator(self.members))) + } +}); +// Add no-sideeffect array methods. Can't set new Array() as the prototype or +// get length() won't work. +for (let [, k] in Iterator(["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf", + "map", "reduce", "reduceRight", "reverse", "slice", "some", "sort"])) + Struct.prototype[k] = Array.prototype[k]; + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js index d2ccda20..14e6bc61 100644 --- a/common/content/bookmarks.js +++ b/common/content/bookmarks.js @@ -6,1263 +6,686 @@ const DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png"; -// Try to import older command line history, quick marks, etc. -liberator.registerObserver("load", function () { - let branch = "extensions." + config.name.toLowerCase(); - if (!options.getPref(branch + ".commandline_cmd_history")) - return; +// also includes methods for dealing with keywords and search engines +const Bookmarks = Module("bookmarks", { + requires: ["autocommands", "liberator", "storage", "services"], - let store = storage["history-command"]; - let pref = options.getPref(branch + ".commandline_cmd_history"); - for (let [k, v] in Iterator(pref.split("\n"))) - store.push(v); + init: function () { + const faviconService = services.get("favicon"); + const bookmarksService = services.get("bookmarks"); + const historyService = services.get("history"); + const tagging = PlacesUtils.tagging; - store = storage["quickmarks"]; - pref = options.getPref(branch + ".quickmarks") - .split("\n"); - while (pref.length > 0) - store.set(pref.shift(), pref.shift()); + this.getFavicon = getFavicon; + function getFavicon(uri) { + try { + return faviconService.getFaviconImageForPage(util.newURI(uri)).spec; + } + catch (e) { + return ""; + } + } - options.resetPref(branch + ".commandline_cmd_history"); - options.resetPref(branch + ".commandline_search_history"); - options.resetPref(branch + ".quickmarks"); + // Fix for strange Firefox bug: + // Error: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIObserverService.addObserver]" + // nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)" + // location: "JS frame :: file://~firefox/components/nsTaggingService.js :: anonymous :: line 89" + // data: no] + // Source file: file://~firefox/components/nsTaggingService.js + tagging.getTagsForURI(window.makeURI("http://mysterious.bug"), {}); + + const Bookmark = new Struct("url", "title", "icon", "keyword", "tags", "id"); + const Keyword = new Struct("keyword", "title", "icon", "url"); + Bookmark.defaultValue("icon", function () getFavicon(this.url)); + Bookmark.prototype.__defineGetter__("extra", function () [ + ["keyword", this.keyword, "Keyword"], + ["tags", this.tags.join(", "), "Tag"] + ].filter(function (item) item[1])); + + const storage = modules.storage; + function Cache(name, store) { + const rootFolders = [bookmarksService.toolbarFolder, bookmarksService.bookmarksMenuFolder, bookmarksService.unfiledBookmarksFolder]; + const sleep = liberator.sleep; // Storage objects are global to all windows, 'liberator' isn't. + + let bookmarks = []; + let self = this; + + this.__defineGetter__("name", function () name); + this.__defineGetter__("store", function () store); + this.__defineGetter__("bookmarks", function () this.load()); + + this.__defineGetter__("keywords", + function () [new Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(self.bookmarks)) if (k.keyword)]); + + this.__iterator__ = function () (val for ([, val] in Iterator(self.bookmarks))); + + function loadBookmark(node) { + try { + let uri = util.newURI(node.uri); + let keyword = bookmarksService.getKeywordForBookmark(node.itemId); + let tags = tagging.getTagsForURI(uri, {}) || []; + let bmark = new Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId); + + bookmarks.push(bmark); + return bmark; + } + catch (e) { + liberator.dump("Failed to create bookmark for URI: " + node.uri); + liberator.reportError(e); + return null; + } + } + + function readBookmark(id) { + return { + itemId: id, + uri: bookmarksService.getBookmarkURI(id).spec, + title: bookmarksService.getItemTitle(id) + }; + } + + function deleteBookmark(id) { + let length = bookmarks.length; + bookmarks = bookmarks.filter(function (item) item.id != id); + return bookmarks.length < length; + } + + this.findRoot = function findRoot(id) { + do { + var root = id; + id = bookmarksService.getFolderIdForItem(id); + } while (id != bookmarksService.placesRoot && id != root); + return root; + } + + this.isBookmark = function (id) rootFolders.indexOf(self.findRoot(id)) >= 0; + + this.isRegularBookmark = function findRoot(id) { + do { + var root = id; + if (services.get("livemark") && services.get("livemark").isLivemark(id)) + return false; + id = bookmarksService.getFolderIdForItem(id); + } while (id != bookmarksService.placesRoot && id != root); + return rootFolders.indexOf(root) >= 0; + } + + // since we don't use a threaded bookmark loading (by set preload) + // anymore, is this loading synchronization still needed? --mst + let loading = false; + this.load = function load() { + if (loading) { + while (loading) + sleep(10); + return bookmarks; + } + + // update our bookmark cache + bookmarks = []; + loading = true; + + let folders = rootFolders.slice(); + let query = historyService.getNewQuery(); + let options = historyService.getNewQueryOptions(); + while (folders.length > 0) { + query.setFolders(folders, 1); + folders.shift(); + let result = historyService.executeQuery(query, options); + let folder = result.root; + folder.containerOpen = true; + + // iterate over the immediate children of this folder + for (let i = 0; i < folder.childCount; i++) { + let node = folder.getChild(i); + if (node.type == node.RESULT_TYPE_FOLDER) // folder + folders.push(node.itemId); + else if (node.type == node.RESULT_TYPE_URI) // bookmark + loadBookmark(node); + } + + // close a container after using it! + folder.containerOpen = false; + } + this.__defineGetter__("bookmarks", function () bookmarks); + loading = false; + return bookmarks; + }; + + var observer = { + onBeginUpdateBatch: function onBeginUpdateBatch() {}, + onEndUpdateBatch: function onEndUpdateBatch() {}, + onItemVisited: function onItemVisited() {}, + onItemMoved: function onItemMoved() {}, + onItemAdded: function onItemAdded(itemId, folder, index) { + // liberator.dump("onItemAdded(" + itemId + ", " + folder + ", " + index + ")\n"); + if (bookmarksService.getItemType(itemId) == bookmarksService.TYPE_BOOKMARK) { + if (self.isBookmark(itemId)) { + let bmark = loadBookmark(readBookmark(itemId)); + storage.fireEvent(name, "add", bmark); + } + } + }, + onItemRemoved: function onItemRemoved(itemId, folder, index) { + // liberator.dump("onItemRemoved(" + itemId + ", " + folder + ", " + index + ")\n"); + if (deleteBookmark(itemId)) + storage.fireEvent(name, "remove", itemId); + }, + onItemChanged: function onItemChanged(itemId, property, isAnnotation, value) { + if (isAnnotation) + return; + // liberator.dump("onItemChanged(" + itemId + ", " + property + ", " + value + ")\n"); + let bookmark = bookmarks.filter(function (item) item.id == itemId)[0]; + if (bookmark) { + if (property == "tags") + value = tagging.getTagsForURI(util.newURI(bookmark.url), {}); + if (property in bookmark) + bookmark[property] = value; + storage.fireEvent(name, "change", itemId); + } + }, + QueryInterface: function QueryInterface(iid) { + if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + bookmarksService.addObserver(observer, false); + } + + let bookmarkObserver = function (key, event, arg) { + if (event == "add") + autocommands.trigger("BookmarkAdd", arg); + statusline.updateUrl(); + }; + + this._cache = storage.newObject("bookmark-cache", Cache, { store: false }); + storage.addObserver("bookmark-cache", bookmarkObserver, window); + }, + + + get format() ({ + anchored: false, + title: ["URL", "Info"], + keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" }, + process: [template.icon, template.bookmarkDescription] + }), + + // TODO: why is this a filter? --djk + get: function get(filter, tags, maxItems, extra) { + return completion.runCompleter("bookmark", filter, maxItems, tags, extra); + }, + + // if starOnly = true it is saved in the unfiledBookmarksFolder, otherwise in the bookmarksMenuFolder + add: function add(starOnly, title, url, keyword, tags, force) { + try { + let uri = util.createURI(url); + if (!force) { + for (let bmark in this._cache) { + if (bmark[0] == uri.spec) { + var id = bmark[5]; + if (title) + services.get("bookmarks").setItemTitle(id, title); + break; + } + } + } + if (id == undefined) + id = services.get("bookmarks").insertBookmark( + services.get("bookmarks")[starOnly ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"], + uri, -1, title || url); + if (!id) + return false; + + if (keyword) + services.get("bookmarks").setKeywordForBookmark(id, keyword); + if (tags) { + PlacesUtils.tagging.untagURI(uri, null); + PlacesUtils.tagging.tagURI(uri, tags); + } + } + catch (e) { + liberator.log(e, 0); + return false; + } + + return true; + }, + + toggle: function toggle(url) { + if (!url) + return; + + let count = this.remove(url); + if (count > 0) + commandline.echo("Removed bookmark: " + url, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE); + else { + let title = buffer.title || url; + let extra = ""; + if (title != url) + extra = " (" + title + ")"; + this.add(true, title, url); + commandline.echo("Added bookmark: " + url + extra, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE); + } + }, + + isBookmarked: function isBookmarked(url) { + try { + return services.get("bookmarks").getBookmarkIdsForURI(makeURI(url), {}) + .some(this._cache.isRegularBookmark); + } + catch (e) { + return false; + } + }, + + // returns number of deleted bookmarks + remove: function remove(url) { + try { + let uri = util.newURI(url); + let bmarks = services.get("bookmarks").getBookmarkIdsForURI(uri, {}) + .filter(this._cache.isRegularBookmark); + bmarks.forEach(services.get("bookmarks").removeItem); + return bmarks.length; + } + catch (e) { + liberator.log(e, 0); + return 0; + } + }, + + // TODO: add filtering + // also ensures that each search engine has a Liberator-friendly alias + getSearchEngines: function getSearchEngines() { + let searchEngines = []; + for (let [, engine] in Iterator(services.get("browserSearch").getVisibleEngines({}))) { + let alias = engine.alias; + if (!alias || !/^[a-z0-9_-]+$/.test(alias)) + alias = engine.name.replace(/^\W*([a-zA-Z_-]+).*/, "$1").toLowerCase(); + if (!alias) + alias = "search"; // for search engines which we can't find a suitable alias + + // make sure we can use search engines which would have the same alias (add numbers at the end) + let newAlias = alias; + for (let j = 1; j <= 10; j++) { // <=10 is intentional + if (!searchEngines.some(function (item) item[0] == newAlias)) + break; + + newAlias = alias + j; + } + // only write when it changed, writes are really slow + if (engine.alias != newAlias) + engine.alias = newAlias; + + searchEngines.push([engine.alias, engine.description, engine.iconURI && engine.iconURI.spec]); + } + + return searchEngines; + }, + + getSuggestions: function getSuggestions(engineName, query, callback) { + const responseType = "application/x-suggestions+json"; + + let engine = services.get("browserSearch").getEngineByAlias(engineName); + if (engine && engine.supportsResponseType(responseType)) + var queryURI = engine.getSubmission(query, responseType).uri.spec; + if (!queryURI) + return []; + + function process(resp) { + let results = []; + try { + results = services.get("json").decode(resp.responseText)[1]; + results = [[item, ""] for ([k, item] in Iterator(results)) if (typeof item == "string")]; + } + catch (e) {} + if (!callback) + return results; + callback(results); + } + + let resp = util.httpGet(queryURI, callback && process); + if (!callback) + return process(resp); + }, + + // TODO: add filtering + // format of returned array: + // [keyword, helptext, url] + getKeywords: function getKeywords() { + return this._cache.keywords; + }, + + // full search string including engine name as first word in @param text + // if @param useDefSearch is true, it uses the default search engine + // @returns the url for the search string + // if the search also requires a postData, [url, postData] is returned + getSearchURL: function getSearchURL(text, useDefsearch) { + let searchString = (useDefsearch ? options["defsearch"] + " " : "") + text; + + // we need to make sure our custom alias have been set, even if the user + // did not :open once before + this.getSearchEngines(); + + // ripped from Firefox + function getShortcutOrURI(url) { + var shortcutURL = null; + var keyword = url; + var param = ""; + var offset = url.indexOf(" "); + if (offset > 0) { + keyword = url.substr(0, offset); + param = url.substr(offset + 1); + } + + var engine = services.get("browserSearch").getEngineByAlias(keyword); + if (engine) { + var submission = engine.getSubmission(param, null); + return [submission.uri.spec, submission.postData]; + } + + [shortcutURL, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword); + if (!shortcutURL) + return [url, null]; + + let data = window.unescape(postData || ""); + if (/%s/i.test(shortcutURL) || /%s/i.test(data)) { + var charset = ""; + var matches = shortcutURL.match(/^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/); + if (matches) + [, shortcutURL, charset] = matches; + else { + try { + charset = services.get("history").getCharsetForURI(window.makeURI(shortcutURL)); + } + catch (e) {} + } + var encodedParam; + if (charset) + encodedParam = escape(window.convertFromUnicode(charset, param)); + else + encodedParam = encodeURIComponent(param); + shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param); + if (/%s/i.test(data)) + postData = window.getPostDataStream(data, param, encodedParam, "application/x-www-form-urlencoded"); + } + else if (param) + return [shortcutURL, null]; + return [shortcutURL, postData]; + } + + let [url, postData] = getShortcutOrURI(searchString); + + if (url == searchString) + return null; + if (postData) + return [url, postData]; + return url; // can be null + }, + + // if openItems is true, open the matching bookmarks items in tabs rather than display + list: function list(filter, tags, openItems, maxItems) { + // FIXME: returning here doesn't make sense + // Why the hell doesn't it make sense? --Kris + // Because it unconditionally bypasses the final error message + // block and does so only when listing items, not opening them. In + // short it breaks the :bmarks command which doesn't make much + // sense to me but I'm old-fashioned. --djk + if (!openItems) + return completion.listCompleter("bookmark", filter, maxItems, tags); + let items = completion.runCompleter("bookmark", filter, maxItems, tags); + + if (items.length) + return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB); + + if (filter.length > 0 && tags.length > 0) + liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\" and string: \"" + filter + "\""); + else if (filter.length > 0) + liberator.echoerr("E283: No bookmarks matching string: \"" + filter + "\""); + else if (tags.length > 0) + liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\""); + else + liberator.echoerr("No bookmarks set"); + } +}, { +}, { + commands: function () { + commands.add(["ju[mps]"], + "Show jumplist", + function () { + let sh = history.session; + let list = template.jumps(sh.index, sh); + commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + }, + { argCount: "0" }); + + // TODO: Clean this up. + function tags(context, args) { + let filter = context.filter; + let have = filter.split(","); + + args.completeFilter = have.pop(); + + let prefix = filter.substr(0, filter.length - args.completeFilter.length); + let tags = util.Array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(this._cache.bookmarks))])); + + return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)]; + } + + function title(context, args) { + if (!args.bang) + return [[content.document.title, "Current Page Title"]]; + context.keys.text = "title"; + context.keys.description = "url"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter }); + } + + function keyword(context, args) { + if (!args.bang) + return []; + context.keys.text = "keyword"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] }); + } + + commands.add(["bma[rk]"], + "Add a bookmark", + function (args) { + let url = args.length == 0 ? buffer.URL : args[0]; + let title = args["-title"] || (args.length == 0 ? buffer.title : null); + let keyword = args["-keyword"] || null; + let tags = args["-tags"] || []; + + if (bookmarks.add(false, title, url, keyword, tags, args.bang)) { + let extra = (title == url) ? "" : " (" + title + ")"; + liberator.echomsg("Added bookmark: " + url + extra, 1, commandline.FORCE_SINGLELINE); + } + else + liberator.echoerr("Exxx: Could not add bookmark `" + title + "'", commandline.FORCE_SINGLELINE); + }, { + argCount: "?", + bang: true, + completer: function (context, args) { + if (!args.bang) { + context.completions = [[content.document.documentURI, "Current Location"]]; + return; + } + completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }); + }, + options: [[["-title", "-t"], commands.OPTION_STRING, null, title], + [["-tags", "-T"], commands.OPTION_LIST, null, tags], + [["-keyword", "-k"], commands.OPTION_STRING, function (arg) /\w/.test(arg)]] + }); + + commands.add(["bmarks"], + "List or open multiple bookmarks", + function (args) { + bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"]); + }, + { + bang: true, + completer: function completer(context, args) { + context.quote = null; + context.filter = args.join(" "); + completion.bookmark(context, args["-tags"]); + }, + options: [[["-tags", "-T"], commands.OPTION_LIST, null, tags], + [["-max", "-m"], commands.OPTION_INT]] + }); + + commands.add(["delbm[arks]"], + "Delete a bookmark", + function (args) { + if (args.bang) { + commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ", + function (resp) { + if (resp && resp.match(/^y(es)?$/i)) { + bookmarks._cache.bookmarks.forEach(function (bmark) { services.get("bookmarks").removeItem(bmark.id); }); + liberator.echomsg("All bookmarks deleted", 1, commandline.FORCE_SINGLELINE); + } + }); + } + else { + let url = args.string || buffer.URL; + let deletedCount = bookmarks.remove(url); + + liberator.echomsg(deletedCount + " bookmark(s) with url " + url.quote() + " deleted", 1, commandline.FORCE_SINGLELINE); + } + + }, + { + argCount: "?", + bang: true, + completer: function completer(context) completion.bookmark(context), + literal: 0 + }); + }, + mappings: function () { + var myModes = config.browserModes; + + mappings.add(myModes, ["a"], + "Open a prompt to bookmark the current URL", + function () { + let options = {}; + + let bmarks = bookmarks.get(buffer.URL).filter(function (bmark) bmark.url == buffer.URL); + + if (bmarks.length == 1) { + let bmark = bmarks[0]; + + options["-title"] = bmark.title; + if (bmark.keyword) + options["-keyword"] = bmark.keyword; + if (bmark.tags.length > 0) + options["-tags"] = bmark.tags.join(", "); + } + else { + if (buffer.title != buffer.URL) + options["-title"] = buffer.title; + } + + commandline.open(":", + commands.commandToString({ command: "bmark", options: options, arguments: [buffer.URL], bang: bmarks.length == 1 }), + modes.EX); + }); + + mappings.add(myModes, ["A"], + "Toggle bookmarked state of current URL", + function () { bookmarks.toggle(buffer.URL); }); + }, + options: function () { + options.add(["defsearch", "ds"], + "Set the default search engine", + "string", "google", + { + completer: function completer(context) { + completion.search(context, true); + context.completions = [["", "Don't perform searches by default"]].concat(context.completions); + }, + validator: Option.validateCompleter + }); + }, + completion: function () { + completion.bookmark = function bookmark(context, tags, extra) { + context.title = ["Bookmark", "Title"]; + context.format = bookmarks.format; + for (let val in Iterator(extra || [])) { + let [k, v] = val; // Need block scope here for the closure + if (v) + context.filters.push(function (item) this._match(v, item[k])); + } + // Need to make a copy because set completions() checks instanceof Array, + // and this may be an Array from another window. + context.completions = Array.slice(storage["bookmark-cache"].bookmarks); + completion.urls(context, tags); + }; + + completion.search = function search(context, noSuggest) { + let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/); + let keywords = bookmarks.getKeywords(); + let engines = bookmarks.getSearchEngines(); + + context.title = ["Search Keywords"]; + context.completions = keywords.concat(engines); + context.keys = { text: 0, description: 1, icon: 2 }; + + if (!space || noSuggest) + return; + + context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest", + keyword, true); + + let item = keywords.filter(function (k) k.keyword == keyword)[0]; + if (item && item.url.indexOf("%s") > -1) + context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) { + context.format = history.format; + context.title = [keyword + " Quick Search"]; + // context.background = true; + context.compare = CompletionContext.Sort.unsorted; + context.generate = function () { + let [begin, end] = item.url.split("%s"); + + return history.get({ uri: window.makeURI(begin), uriIsPrefix: true }).map(function (item) { + let rest = item.url.length - end.length; + let query = item.url.substring(begin.length, rest); + if (item.url.substr(rest) == end && query.indexOf("&") == -1) { + item.url = decodeURIComponent(query.replace(/#.*/, "")); + return item; + } + }).filter(util.identity); + }; + }); + }; + + completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) { + if (!context.filter) + return; + + let engineList = (engineAliases || options["suggestengines"] || "google").split(","); + + let completions = []; + engineList.forEach(function (name) { + let engine = services.get("browserSearch").getEngineByAlias(name); + if (!engine) + return; + let [, word] = /^\s*(\S+)/.exec(context.filter) || []; + if (!kludge && word == name) // FIXME: Check for matching keywords + return; + let ctxt = context.fork(name, 0); + + ctxt.title = [engine.description + " Suggestions"]; + ctxt.compare = CompletionContext.Sort.unsorted; + ctxt.incomplete = true; + bookmarks.getSuggestions(name, ctxt.filter, function (compl) { + ctxt.incomplete = false; + ctxt.completions = compl; + }); + }); + }; + + completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest); + completion.addUrlCompleter("b", "Bookmarks", completion.bookmark); + completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search); + }, }); -// also includes methods for dealing with keywords and search engines -function Bookmarks() //{{{ -{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - const historyService = PlacesUtils.history; - const bookmarksService = PlacesUtils.bookmarks; - const taggingService = PlacesUtils.tagging; - const faviconService = services.get("favicon"); - const livemarkService = services.get("livemark"); - - // XXX for strange Firefox bug :( - // Error: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIObserverService.addObserver]" - // nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)" - // location: "JS frame :: file://~firefox/components/nsTaggingService.js :: anonymous :: line 89" - // data: no] - // Source file: file://~firefox/components/nsTaggingService.js - taggingService.getTagsForURI(window.makeURI("http://mysterious.bug"), {}); - - const Bookmark = new Struct("url", "title", "icon", "keyword", "tags", "id"); - const Keyword = new Struct("keyword", "title", "icon", "url"); - Bookmark.defaultValue("icon", function () getFavicon(this.url)); - Bookmark.prototype.__defineGetter__("extra", function () [ - ["keyword", this.keyword, "Keyword"], - ["tags", this.tags.join(", "), "Tag"] - ].filter(function (item) item[1])); - - const storage = modules.storage; - function Cache(name, store) - { - const rootFolders = [bookmarksService.toolbarFolder, bookmarksService.bookmarksMenuFolder, bookmarksService.unfiledBookmarksFolder]; - const sleep = liberator.sleep; // Storage objects are global to all windows, 'liberator' isn't. - - let bookmarks = []; - let self = this; - - this.__defineGetter__("name", function () name); - this.__defineGetter__("store", function () store); - this.__defineGetter__("bookmarks", function () this.load()); - - this.__defineGetter__("keywords", - function () [new Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(self.bookmarks)) if (k.keyword)]); - - this.__iterator__ = function () (val for ([, val] in Iterator(self.bookmarks))); - - function loadBookmark(node) - { - try - { - let uri = util.newURI(node.uri); - let keyword = bookmarksService.getKeywordForBookmark(node.itemId); - let tags = taggingService.getTagsForURI(uri, {}) || []; - let bmark = new Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId); - - bookmarks.push(bmark); - return bmark; - } - catch (e) - { - liberator.dump("Failed to create bookmark for URI: " + node.uri); - liberator.reportError(e); - return null; - } - } - - function readBookmark(id) - { - return { - itemId: id, - uri: bookmarksService.getBookmarkURI(id).spec, - title: bookmarksService.getItemTitle(id) - }; - } - - function deleteBookmark(id) - { - let length = bookmarks.length; - bookmarks = bookmarks.filter(function (item) item.id != id); - return bookmarks.length < length; - } - - this.findRoot = function findRoot(id) - { - do - { - var root = id; - id = bookmarksService.getFolderIdForItem(id); - } while (id != bookmarksService.placesRoot && id != root); - return root; - } - - this.isBookmark = function (id) rootFolders.indexOf(self.findRoot(id)) >= 0; - - this.isRegularBookmark = function findRoot(id) - { - do - { - var root = id; - if (livemarkService && livemarkService.isLivemark(id)) - return false; - id = bookmarksService.getFolderIdForItem(id); - } while (id != bookmarksService.placesRoot && id != root); - return rootFolders.indexOf(root) >= 0; - } - - // since we don't use a threaded bookmark loading (by set preload) - // anymore, is this loading synchronization still needed? --mst - let loading = false; - this.load = function load() - { - if (loading) - { - while (loading) - sleep(10); - return bookmarks; - } - - // update our bookmark cache - bookmarks = []; - loading = true; - - let folders = rootFolders.slice(); - let query = historyService.getNewQuery(); - let options = historyService.getNewQueryOptions(); - while (folders.length > 0) - { - query.setFolders(folders, 1); - folders.shift(); - let result = historyService.executeQuery(query, options); - let folder = result.root; - folder.containerOpen = true; - - // iterate over the immediate children of this folder - for (let i = 0; i < folder.childCount; i++) - { - let node = folder.getChild(i); - if (node.type == node.RESULT_TYPE_FOLDER) // folder - folders.push(node.itemId); - else if (node.type == node.RESULT_TYPE_URI) // bookmark - loadBookmark(node); - } - - // close a container after using it! - folder.containerOpen = false; - } - this.__defineGetter__("bookmarks", function () bookmarks); - loading = false; - return bookmarks; - }; - - var observer = { - onBeginUpdateBatch: function onBeginUpdateBatch() {}, - onEndUpdateBatch: function onEndUpdateBatch() {}, - onItemVisited: function onItemVisited() {}, - onItemMoved: function onItemMoved() {}, - onItemAdded: function onItemAdded(itemId, folder, index) - { - // liberator.dump("onItemAdded(" + itemId + ", " + folder + ", " + index + ")\n"); - if (bookmarksService.getItemType(itemId) == bookmarksService.TYPE_BOOKMARK) - { - if (self.isBookmark(itemId)) - { - let bmark = loadBookmark(readBookmark(itemId)); - storage.fireEvent(name, "add", bmark); - } - } - }, - onItemRemoved: function onItemRemoved(itemId, folder, index) - { - // liberator.dump("onItemRemoved(" + itemId + ", " + folder + ", " + index + ")\n"); - if (deleteBookmark(itemId)) - storage.fireEvent(name, "remove", itemId); - }, - onItemChanged: function onItemChanged(itemId, property, isAnnotation, value) - { - if (isAnnotation) - return; - // liberator.dump("onItemChanged(" + itemId + ", " + property + ", " + value + ")\n"); - let bookmark = bookmarks.filter(function (item) item.id == itemId)[0]; - if (bookmark) - { - if (property == "tags") - value = taggingService.getTagsForURI(util.newURI(bookmark.url), {}); - if (property in bookmark) - bookmark[property] = value; - storage.fireEvent(name, "change", itemId); - } - }, - QueryInterface: function QueryInterface(iid) - { - if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports)) - return this; - throw Cr.NS_ERROR_NO_INTERFACE; - } - }; - - bookmarksService.addObserver(observer, false); - } - - function getFavicon(uri) - { - try - { - return faviconService.getFaviconImageForPage(util.newURI(uri)).spec; - } - catch (e) - { - return ""; - } - } - - let bookmarkObserver = function (key, event, arg) - { - if (event == "add") - autocommands.trigger("BookmarkAdd", arg); - statusline.updateUrl(); - }; - - var cache = storage.newObject("bookmark-cache", Cache, { store: false }); - storage.addObserver("bookmark-cache", bookmarkObserver, window); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// OPTIONS ///////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - options.add(["defsearch", "ds"], - "Set the default search engine", - "string", "google", - { - completer: function completer(context) - { - completion.search(context, true); - context.completions = [["", "Don't perform searches by default"]].concat(context.completions); - }, - validator: Option.validateCompleter - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - var myModes = config.browserModes; - - mappings.add(myModes, ["a"], - "Open a prompt to bookmark the current URL", - function () - { - let options = {}; - - let bmarks = bookmarks.get(buffer.URL).filter(function (bmark) bmark.url == buffer.URL); - - if (bmarks.length == 1) - { - let bmark = bmarks[0]; - - options["-title"] = bmark.title; - if (bmark.keyword) - options["-keyword"] = bmark.keyword; - if (bmark.tags.length > 0) - options["-tags"] = bmark.tags.join(", "); - } - else - { - if (buffer.title != buffer.URL) - options["-title"] = buffer.title; - } - - commandline.open(":", - commands.commandToString({ command: "bmark", options: options, arguments: [buffer.URL], bang: bmarks.length == 1 }), - modes.EX); - }); - - mappings.add(myModes, ["A"], - "Toggle bookmarked state of current URL", - function () { bookmarks.toggle(buffer.URL); }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - commands.add(["ju[mps]"], - "Show jumplist", - function () - { - let sh = history.session; - let list = template.jumps(sh.index, sh); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - }, - { argCount: "0" }); - - // TODO: Clean this up. - function tags(context, args) - { - let filter = context.filter; - let have = filter.split(","); - - args.completeFilter = have.pop(); - - let prefix = filter.substr(0, filter.length - args.completeFilter.length); - let tags = util.Array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(cache.bookmarks))])); - - return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)]; - } - - function title(context, args) - { - if (!args.bang) - return [[content.document.title, "Current Page Title"]]; - context.keys.text = "title"; - context.keys.description = "url"; - return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter }); - } - - function keyword(context, args) - { - if (!args.bang) - return []; - context.keys.text = "keyword"; - return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] }); - } - - commands.add(["bma[rk]"], - "Add a bookmark", - function (args) - { - let url = args.length == 0 ? buffer.URL : args[0]; - let title = args["-title"] || (args.length == 0 ? buffer.title : null); - let keyword = args["-keyword"] || null; - let tags = args["-tags"] || []; - - if (bookmarks.add(false, title, url, keyword, tags, args.bang)) - { - let extra = (title == url) ? "" : " (" + title + ")"; - liberator.echomsg("Added bookmark: " + url + extra, 1, commandline.FORCE_SINGLELINE); - } - else - liberator.echoerr("Exxx: Could not add bookmark `" + title + "'", commandline.FORCE_SINGLELINE); - }, - { - argCount: "?", - bang: true, - completer: function (context, args) - { - if (!args.bang) - { - context.completions = [[content.document.documentURI, "Current Location"]]; - return; - } - completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }); - }, - options: [[["-title", "-t"], commands.OPTION_STRING, null, title], - [["-tags", "-T"], commands.OPTION_LIST, null, tags], - [["-keyword", "-k"], commands.OPTION_STRING, function (arg) /\w/.test(arg)]] - }); - - commands.add(["bmarks"], - "List or open multiple bookmarks", - function (args) - { - bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"]); - }, - { - bang: true, - completer: function completer(context, args) - { - context.quote = null; - context.filter = args.join(" "); - completion.bookmark(context, args["-tags"]); - }, - options: [[["-tags", "-T"], commands.OPTION_LIST, null, tags], - [["-max", "-m"], commands.OPTION_INT]] - }); - - commands.add(["delbm[arks]"], - "Delete a bookmark", - function (args) - { - if (args.bang) - { - commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ", - function (resp) { - if (resp && resp.match(/^y(es)?$/i)) - { - cache.bookmarks.forEach(function (bmark) { bookmarksService.removeItem(bmark.id); }); - liberator.echomsg("All bookmarks deleted", 1, commandline.FORCE_SINGLELINE); - } - }); - } - else - { - let url = args.string || buffer.URL; - let deletedCount = bookmarks.remove(url); - - liberator.echomsg(deletedCount + " bookmark(s) with url " + url.quote() + " deleted", 1, commandline.FORCE_SINGLELINE); - } - - }, - { - argCount: "?", - bang: true, - completer: function completer(context) completion.bookmark(context), - literal: 0 - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMPLETIONS ///////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - completion.bookmark = function bookmark(context, tags, extra) { - context.title = ["Bookmark", "Title"]; - context.format = bookmarks.format; - for (let val in Iterator(extra || [])) - { - let [k, v] = val; // Need block scope here for the closure - if (v) - context.filters.push(function (item) this._match(v, item[k])); - } - // Need to make a copy because set completions() checks instanceof Array, - // and this may be an Array from another window. - context.completions = Array.slice(storage["bookmark-cache"].bookmarks); - completion.urls(context, tags); - }; - - completion.search = function search(context, noSuggest) { - let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/); - let keywords = bookmarks.getKeywords(); - let engines = bookmarks.getSearchEngines(); - - context.title = ["Search Keywords"]; - context.completions = keywords.concat(engines); - context.keys = { text: 0, description: 1, icon: 2 }; - - if (!space || noSuggest) - return; - - context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest", - keyword, true); - - let item = keywords.filter(function (k) k.keyword == keyword)[0]; - if (item && item.url.indexOf("%s") > -1) - context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) { - context.format = history.format; - context.title = [keyword + " Quick Search"]; - // context.background = true; - context.compare = CompletionContext.Sort.unsorted; - context.generate = function () { - let [begin, end] = item.url.split("%s"); - - return history.get({ uri: window.makeURI(begin), uriIsPrefix: true }).map(function (item) { - let rest = item.url.length - end.length; - let query = item.url.substring(begin.length, rest); - if (item.url.substr(rest) == end && query.indexOf("&") == -1) - { - item.url = decodeURIComponent(query.replace(/#.*/, "")); - return item; - } - }).filter(util.identity); - }; - }); - }; - - completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) { - if (!context.filter) - return; - - let engineList = (engineAliases || options["suggestengines"] || "google").split(","); - - let completions = []; - engineList.forEach(function (name) { - let engine = services.get("browserSearch").getEngineByAlias(name); - if (!engine) - return; - let [, word] = /^\s*(\S+)/.exec(context.filter) || []; - if (!kludge && word == name) // FIXME: Check for matching keywords - return; - let ctxt = context.fork(name, 0); - - ctxt.title = [engine.description + " Suggestions"]; - ctxt.compare = CompletionContext.Sort.unsorted; - ctxt.incomplete = true; - bookmarks.getSuggestions(name, ctxt.filter, function (compl) { - ctxt.incomplete = false; - ctxt.completions = compl; - }); - }); - }; - - completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest); - completion.addUrlCompleter("b", "Bookmarks", completion.bookmark); - completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - - get format() ({ - anchored: false, - title: ["URL", "Info"], - keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" }, - process: [template.icon, template.bookmarkDescription] - }), - - // TODO: why is this a filter? --djk - get: function get(filter, tags, maxItems, extra) - { - return completion.runCompleter("bookmark", filter, maxItems, tags, extra); - }, - - // if starOnly = true it is saved in the unfiledBookmarksFolder, otherwise in the bookmarksMenuFolder - add: function add(starOnly, title, url, keyword, tags, force) - { - try - { - let uri = util.createURI(url); - if (!force) - { - for (let bmark in cache) - { - if (bmark[0] == uri.spec) - { - var id = bmark[5]; - if (title) - bookmarksService.setItemTitle(id, title); - break; - } - } - } - if (id == undefined) - id = bookmarksService.insertBookmark( - bookmarksService[starOnly ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"], - uri, -1, title || url); - if (!id) - return false; - - if (keyword) - bookmarksService.setKeywordForBookmark(id, keyword); - if (tags) - { - taggingService.untagURI(uri, null); - taggingService.tagURI(uri, tags); - } - } - catch (e) - { - liberator.log(e, 0); - return false; - } - - return true; - }, - - toggle: function toggle(url) - { - if (!url) - return; - - let count = this.remove(url); - if (count > 0) - commandline.echo("Removed bookmark: " + url, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE); - else - { - let title = buffer.title || url; - let extra = ""; - if (title != url) - extra = " (" + title + ")"; - this.add(true, title, url); - commandline.echo("Added bookmark: " + url + extra, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE); - } - }, - - isBookmarked: function isBookmarked(url) - { - try - { - return bookmarksService.getBookmarkIdsForURI(makeURI(url), {}) - .some(cache.isRegularBookmark); - } - catch (e) - { - return false; - } - }, - - // returns number of deleted bookmarks - remove: function remove(url) - { - try - { - let uri = util.newURI(url); - let bmarks = bookmarksService.getBookmarkIdsForURI(uri, {}) - .filter(cache.isRegularBookmark); - bmarks.forEach(bookmarksService.removeItem); - return bmarks.length; - } - catch (e) - { - liberator.log(e, 0); - return 0; - } - }, - - getFavicon: function (url) getFavicon(url), - - // TODO: add filtering - // also ensures that each search engine has a Liberator-friendly alias - getSearchEngines: function getSearchEngines() - { - let searchEngines = []; - for (let [, engine] in Iterator(services.get("browserSearch").getVisibleEngines({}))) - { - let alias = engine.alias; - if (!alias || !/^[a-z0-9_-]+$/.test(alias)) - alias = engine.name.replace(/^\W*([a-zA-Z_-]+).*/, "$1").toLowerCase(); - if (!alias) - alias = "search"; // for search engines which we can't find a suitable alias - - // make sure we can use search engines which would have the same alias (add numbers at the end) - let newAlias = alias; - for (let j = 1; j <= 10; j++) // <=10 is intentional - { - if (!searchEngines.some(function (item) item[0] == newAlias)) - break; - - newAlias = alias + j; - } - // only write when it changed, writes are really slow - if (engine.alias != newAlias) - engine.alias = newAlias; - - searchEngines.push([engine.alias, engine.description, engine.iconURI && engine.iconURI.spec]); - } - - return searchEngines; - }, - - getSuggestions: function getSuggestions(engineName, query, callback) - { - const responseType = "application/x-suggestions+json"; - - let engine = services.get("browserSearch").getEngineByAlias(engineName); - if (engine && engine.supportsResponseType(responseType)) - var queryURI = engine.getSubmission(query, responseType).uri.spec; - if (!queryURI) - return []; - - function process(resp) - { - let results = []; - try - { - results = services.get("json").decode(resp.responseText)[1]; - results = [[item, ""] for ([k, item] in Iterator(results)) if (typeof item == "string")]; - } - catch (e) {} - if (!callback) - return results; - callback(results); - } - - let resp = util.httpGet(queryURI, callback && process); - if (!callback) - return process(resp); - }, - - // TODO: add filtering - // format of returned array: - // [keyword, helptext, url] - getKeywords: function getKeywords() - { - return cache.keywords; - }, - - // full search string including engine name as first word in @param text - // if @param useDefSearch is true, it uses the default search engine - // @returns the url for the search string - // if the search also requires a postData, [url, postData] is returned - getSearchURL: function getSearchURL(text, useDefsearch) - { - let searchString = (useDefsearch ? options["defsearch"] + " " : "") + text; - - // we need to make sure our custom alias have been set, even if the user - // did not :open once before - this.getSearchEngines(); - - // ripped from Firefox - function getShortcutOrURI(url) { - var shortcutURL = null; - var keyword = url; - var param = ""; - var offset = url.indexOf(" "); - if (offset > 0) - { - keyword = url.substr(0, offset); - param = url.substr(offset + 1); - } - - var engine = services.get("browserSearch").getEngineByAlias(keyword); - if (engine) - { - var submission = engine.getSubmission(param, null); - return [submission.uri.spec, submission.postData]; - } - - [shortcutURL, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword); - if (!shortcutURL) - return [url, null]; - - let data = window.unescape(postData || ""); - if (/%s/i.test(shortcutURL) || /%s/i.test(data)) - { - var charset = ""; - var matches = shortcutURL.match(/^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/); - if (matches) - [, shortcutURL, charset] = matches; - else - { - try - { - charset = PlacesUtils.history.getCharsetForURI(window.makeURI(shortcutURL)); - } - catch (e) {} - } - var encodedParam; - if (charset) - encodedParam = escape(window.convertFromUnicode(charset, param)); - else - encodedParam = encodeURIComponent(param); - shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param); - if (/%s/i.test(data)) - postData = window.getPostDataStream(data, param, encodedParam, "application/x-www-form-urlencoded"); - } - else if (param) - return [shortcutURL, null]; - return [shortcutURL, postData]; - } - - let [url, postData] = getShortcutOrURI(searchString); - - if (url == searchString) - return null; - if (postData) - return [url, postData]; - return url; // can be null - }, - - // if openItems is true, open the matching bookmarks items in tabs rather than display - list: function list(filter, tags, openItems, maxItems) - { - // FIXME: returning here doesn't make sense - // Why the hell doesn't it make sense? --Kris - // Because it unconditionally bypasses the final error message - // block and does so only when listing items, not opening them. In - // short it breaks the :bmarks command which doesn't make much - // sense to me but I'm old-fashioned. --djk - if (!openItems) - return completion.listCompleter("bookmark", filter, maxItems, tags); - let items = completion.runCompleter("bookmark", filter, maxItems, tags); - - if (items.length) - return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB); - - if (filter.length > 0 && tags.length > 0) - liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\" and string: \"" + filter + "\""); - else if (filter.length > 0) - liberator.echoerr("E283: No bookmarks matching string: \"" + filter + "\""); - else if (tags.length > 0) - liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\""); - else - liberator.echoerr("No bookmarks set"); - } - }; - //}}} -} //}}} - -function History() //{{{ -{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - const historyService = PlacesUtils.history; - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - var myModes = config.browserModes; - - mappings.add(myModes, - [""], "Go to an older position in the jump list", - function (count) { history.stepTo(-Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, - [""], "Go to a newer position in the jump list", - function (count) { history.stepTo(Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, - ["H", "", ""], "Go back in the browser history", - function (count) { history.stepTo(-Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, - ["L", "", ""], "Go forward in the browser history", - function (count) { history.stepTo(Math.max(count, 1)); }, - { count: true }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - commands.add(["ba[ck]"], - "Go back in the browser history", - function (args) - { - let url = args.literalArg; - - if (args.bang) - history.goToStart(); - else - { - if (url) - { - let sh = history.session; - if (/^\d+(:|$)/.test(url) && sh.index - parseInt(url) in sh) - return void window.getWebNavigation().gotoIndex(sh.index - parseInt(url)); - - for (let [i, ent] in Iterator(sh.slice(0, sh.index).reverse())) - if (ent.URI.spec == url) - return void window.getWebNavigation().gotoIndex(i); - liberator.echoerr("Exxx: URL not found in history"); - } - else - history.stepTo(-Math.max(args.count, 1)); - } - }, - { - argCount: "?", - bang: true, - completer: function completer(context) - { - let sh = history.session; - - context.anchored = false; - context.compare = CompletionContext.Sort.unsorted; - context.filters = [CompletionContext.Filter.textDescription]; - context.completions = sh.slice(0, sh.index).reverse(); - context.keys = { text: function (item) (sh.index - item.index) + ": " + item.URI.spec, description: "title", icon: "icon" }; - }, - count: true, - literal: 0 - }); - - commands.add(["fo[rward]", "fw"], - "Go forward in the browser history", - function (args) - { - let url = args.literalArg; - - if (args.bang) - history.goToEnd(); - else - { - if (url) - { - let sh = history.session; - if (/^\d+(:|$)/.test(url) && sh.index + parseInt(url) in sh) - return void window.getWebNavigation().gotoIndex(sh.index + parseInt(url)); - - for (let [i, ent] in Iterator(sh.slice(sh.index + 1))) - if (ent.URI.spec == url) - return void window.getWebNavigation().gotoIndex(i); - liberator.echoerr("Exxx: URL not found in history"); - } - else - history.stepTo(Math.max(args.count, 1)); - } - }, - { - argCount: "?", - bang: true, - completer: function completer(context) - { - let sh = history.session; - - context.anchored = false; - context.compare = CompletionContext.Sort.unsorted; - context.filters = [CompletionContext.Filter.textDescription]; - context.completions = sh.slice(sh.index + 1); - context.keys = { text: function (item) (item.index - sh.index) + ": " + item.URI.spec, description: "title", icon: "icon" }; - }, - count: true, - literal: 0 - }); - - commands.add(["hist[ory]", "hs"], - "Show recently visited URLs", - function (args) { history.list(args.join(" "), args.bang, args["-max"] || 1000); }, - { - bang: true, - completer: function (context) { context.quote = null; completion.history(context); }, - // completer: function (filter) completion.history(filter) - options: [[["-max", "-m"], commands.OPTION_INT]] - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMPLETIONS ///////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - completion.history = function _history(context, maxItems) { - context.format = history.format; - context.title = ["History"]; - context.compare = CompletionContext.Sort.unsorted; - //context.background = true; - if (context.maxItems == null) - context.maxItems = 100; - context.regenerate = true; - context.generate = function () history.get(context.filter, this.maxItems); - }; - - completion.addUrlCompleter("h", "History", completion.history); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - - get format() bookmarks.format, - - get service() historyService, - - get: function get(filter, maxItems) - { - // no query parameters will get all history - let query = historyService.getNewQuery(); - let options = historyService.getNewQueryOptions(); - - if (typeof filter == "string") - filter = { searchTerms: filter }; - for (let [k, v] in Iterator(filter)) - query[k] = v; - options.sortingMode = options.SORT_BY_DATE_DESCENDING; - options.resultType = options.RESULTS_AS_URI; - if (maxItems > 0) - options.maxResults = maxItems; - - // execute the query - let root = historyService.executeQuery(query, options).root; - root.containerOpen = true; - let items = util.map(util.range(0, root.childCount), function (i) { - let node = root.getChild(i); - return { - url: node.uri, - title: node.title, - icon: node.icon ? node.icon.spec : DEFAULT_FAVICON - }; - }); - root.containerOpen = false; // close a container after using it! - - return items; - }, - - get session() - { - let sh = window.getWebNavigation().sessionHistory; - let obj = []; - obj.index = sh.index; - obj.__iterator__ = function () util.Array.iteritems(this); - for (let i in util.range(0, sh.count)) - { - obj[i] = { index: i, __proto__: sh.getEntryAtIndex(i, false) }; - util.memoize(obj[i], "icon", - function (obj) services.get("favicon").getFaviconImageForPage(obj.URI).spec); - } - return obj; - }, - - // TODO: better names - stepTo: function stepTo(steps) - { - let start = 0; - let end = window.getWebNavigation().sessionHistory.count - 1; - let current = window.getWebNavigation().sessionHistory.index; - - if (current == start && steps < 0 || current == end && steps > 0) - liberator.beep(); - else - { - let index = util.Math.constrain(current + steps, start, end); - window.getWebNavigation().gotoIndex(index); - } - }, - - goToStart: function goToStart() - { - let index = window.getWebNavigation().sessionHistory.index; - - if (index > 0) - window.getWebNavigation().gotoIndex(0); - else - liberator.beep(); - - }, - - goToEnd: function goToEnd() - { - let sh = window.getWebNavigation().sessionHistory; - let max = sh.count - 1; - - if (sh.index < max) - window.getWebNavigation().gotoIndex(max); - else - liberator.beep(); - - }, - - // if openItems is true, open the matching history items in tabs rather than display - list: function list(filter, openItems, maxItems) - { - // FIXME: returning here doesn't make sense - // Why the hell doesn't it make sense? --Kris - // See comment at bookmarks.list --djk - if (!openItems) - return completion.listCompleter("history", filter, maxItems); - let items = completion.runCompleter("history", filter, maxItems); - - if (items.length) - return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB); - - if (filter.length > 0) - liberator.echoerr("E283: No history matching \"" + filter + "\""); - else - liberator.echoerr("No history set"); - } - }; - //}}} -} //}}} - -/** @scope modules */ - -/** - * @instance quickmarks - */ -function QuickMarks() //{{{ -{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - var qmarks = storage.newMap("quickmarks", { store: true }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - var myModes = config.browserModes; - - mappings.add(myModes, - ["go"], "Jump to a QuickMark", - function (arg) { quickmarks.jumpTo(arg, liberator.CURRENT_TAB); }, - { arg: true }); - - mappings.add(myModes, - ["gn"], "Jump to a QuickMark in a new tab", - function (arg) - { - quickmarks.jumpTo(arg, - /\bquickmark\b/.test(options["activate"]) ? - liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB); - }, - { arg: true }); - - mappings.add(myModes, - ["M"], "Add new QuickMark for current URL", - function (arg) - { - if (/[^a-zA-Z0-9]/.test(arg)) - return void liberator.beep(); - - quickmarks.add(arg, buffer.URL); - }, - { arg: true }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - commands.add(["delqm[arks]"], - "Delete the specified QuickMarks", - function (args) - { - // TODO: finish arg parsing - we really need a proper way to do this. :) - // assert(args.bang ^ args.string) - liberator.assert( args.bang || args.string, "E471: Argument required"); - liberator.assert(!args.bang || !args.string, "E474: Invalid argument"); - - if (args.bang) - quickmarks.removeAll(); - else - quickmarks.remove(args.string); - }, - { - bang: true, - completer: function (context) - { - context.title = ["QuickMark", "URL"]; - context.completions = qmarks; - } - }); - - commands.add(["qma[rk]"], - "Mark a URL with a letter for quick access", - function (args) - { - let matches = args.string.match(/^([a-zA-Z0-9])(?:\s+(.+))?$/); - if (!matches) - liberator.echoerr("E488: Trailing characters"); - else if (!matches[2]) - quickmarks.add(matches[1], buffer.URL); - else - quickmarks.add(matches[1], matches[2]); - }, - { argCount: "+" }); - - commands.add(["qmarks"], - "Show all QuickMarks", - function (args) - { - args = args.string; - - // ignore invalid qmark characters unless there are no valid qmark chars - liberator.assert(!args || /[a-zA-Z0-9]/.test(args), "E283: No QuickMarks matching \"" + args + "\""); - - let filter = args.replace(/[^a-zA-Z0-9]/g, ""); - quickmarks.list(filter); - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - - /** - * Adds a new quickmark with name qmark referencing - * the URL location. Any existing quickmark with the same name - * will be replaced. - * - * @param {string} qmark The name of the quickmark {A-Z}. - * @param {string} location The URL accessed by this quickmark. - */ - add: function add(qmark, location) - { - qmarks.set(qmark, location); - liberator.echomsg("Added Quick Mark '" + qmark + "': " + location, 1); - }, - - /** - * Deletes the specified quickmarks. The filter is a list of - * quickmarks and ranges are supported. Eg. "ab c d e-k". - * - * @param {string} filter The list of quickmarks to delete. - * - */ - remove: function remove(filter) - { - let pattern = RegExp("[" + filter.replace(/\s+/g, "") + "]"); - - for (let [qmark, ] in qmarks) - { - if (pattern.test(qmark)) - qmarks.remove(qmark); - } - }, - - /** - * Removes all quickmarks. - */ - removeAll: function removeAll() - { - qmarks.clear(); - }, - - /** - * Opens the URL referenced by the specified qmark. - * - * @param {string} qmark The quickmark to open. - * @param {number} where A constant describing where to open the page. - * See {@link Liberator#open}. - */ - jumpTo: function jumpTo(qmark, where) - { - let url = qmarks.get(qmark); - - if (url) - liberator.open(url, where); - else - liberator.echoerr("E20: QuickMark not set"); - }, - - /** - * Lists all quickmarks matching filter in the message window. - * - * @param {string} filter The list of quickmarks to display. Eg. "abc" - * Ranges are not supported. - */ - // FIXME: filter should match that of quickmarks.remove or vice versa - list: function list(filter) - { - let marks = [k for ([k, v] in qmarks)]; - let lowercaseMarks = marks.filter(function (x) /[a-z]/.test(x)).sort(); - let uppercaseMarks = marks.filter(function (x) /[A-Z]/.test(x)).sort(); - let numberMarks = marks.filter(function (x) /[0-9]/.test(x)).sort(); - - marks = Array.concat(lowercaseMarks, uppercaseMarks, numberMarks); - - liberator.assert(marks.length > 0, "No QuickMarks set"); - - if (filter.length > 0) - { - marks = marks.filter(function (qmark) filter.indexOf(qmark) >= 0); - liberator.assert(marks.length >= 0, "E283: No QuickMarks matching \"" + filter + "\""); - } - - let items = [[mark, qmarks.get(mark)] for ([k, mark] in Iterator(marks))]; - template.genericTable(items, { title: ["QuickMark", "URL"] }); - } - }; - //}}} -} //}}} - // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/browser.js b/common/content/browser.js index 842aa185..00d580db 100644 --- a/common/content/browser.js +++ b/common/content/browser.js @@ -9,15 +9,10 @@ /** * @instance browser */ -function Browser() //{{{ -{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - +const Browser = Module("browser", { +}, { // TODO: support 'nrformats'? -> probably not worth it --mst - function incrementURL(count) - { + incrementURL: function (count) { let matches = buffer.URL.match(/(.*?)(\d+)(\D*)$/); if (!matches) return void liberator.beep(); @@ -25,253 +20,219 @@ function Browser() //{{{ let [, pre, number, post] = matches; let newNumber = parseInt(number, 10) + count; let newNumberStr = String(newNumber > 0 ? newNumber : 0); - if (number.match(/^0/)) // add 0009 should become 0010 - { + if (number.match(/^0/)) { // add 0009 should become 0010 while (newNumberStr.length < number.length) newNumberStr = "0" + newNumberStr; } liberator.open(pre + newNumberStr + post); } - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// OPTIONS ///////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - options.add(["encoding", "enc"], - "Sets the current buffer's character encoding", - "string", "UTF-8", - { - scope: options.OPTION_SCOPE_LOCAL, - getter: function () getBrowser().docShell.QueryInterface(Ci.nsIDocCharset).charset, - setter: function (val) +}, { + options: function () { + options.add(["encoding", "enc"], + "Sets the current buffer's character encoding", + "string", "UTF-8", { - if (options["encoding"] == val) - return val; + scope: options.OPTION_SCOPE_LOCAL, + getter: function () getBrowser().docShell.QueryInterface(Ci.nsIDocCharset).charset, + setter: function (val) { + if (options["encoding"] == val) + return val; - // Stolen from browser.jar/content/browser/browser.js, more or less. - try - { - getBrowser().docShell.QueryInterface(Ci.nsIDocCharset).charset = val; - PlacesUtils.history.setCharsetForURI(getWebNavigation().currentURI, val); - getWebNavigation().reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); - } - catch (e) { liberator.reportError(e); } - }, - completer: function (context) completion.charset(context), - validator: Option.validateCompleter - }); - - // only available in FF 3.5 - services.add("privateBrowsing", "@mozilla.org/privatebrowsing;1", Ci.nsIPrivateBrowsingService); - if (services.get("privateBrowsing")) - { - options.add(["private", "pornmode"], - "Set the 'private browsing' option", - "boolean", false, - { - setter: function (value) services.get("privateBrowsing").privateBrowsingEnabled = value, - getter: function () services.get("privateBrowsing").privateBrowsingEnabled - }); - let services = modules.services; // Storage objects are global to all windows, 'modules' isn't. - storage.newObject("private-mode", function () { - ({ - init: function () - { - services.get("observer").addObserver(this, "private-browsing", false); - services.get("observer").addObserver(this, "quit-application", false); - this.private = services.get("privateBrowsing").privateBrowsingEnabled; + // Stolen from browser.jar/content/browser/browser.js, more or less. + try { + getBrowser().docShell.QueryInterface(Ci.nsIDocCharset).charset = val; + PlacesUtils.history.setCharsetForURI(getWebNavigation().currentURI, val); + getWebNavigation().reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); + } + catch (e) { liberator.reportError(e); } }, - observe: function (subject, topic, data) + completer: function (context) completion.charset(context), + validator: Option.validateCompleter + }); + + // only available in FF 3.5 + services.add("privateBrowsing", "@mozilla.org/privatebrowsing;1", Ci.nsIPrivateBrowsingService); + if (services.get("privateBrowsing")) { + options.add(["private", "pornmode"], + "Set the 'private browsing' option", + "boolean", false, { - if (topic == "private-browsing") - { - if (data == "enter") - storage.privateMode = true; - else if (data == "exit") - storage.privateMode = false; - storage.fireEvent("private-mode", "change", storage.privateMode); + setter: function (value) services.get("privateBrowsing").privateBrowsingEnabled = value, + getter: function () services.get("privateBrowsing").privateBrowsingEnabled + }); + let services = modules.services; // Storage objects are global to all windows, 'modules' isn't. + storage.newObject("private-mode", function () { + ({ + init: function () { + services.get("observer").addObserver(this, "private-browsing", false); + services.get("observer").addObserver(this, "quit-application", false); + this.private = services.get("privateBrowsing").privateBrowsingEnabled; + }, + observe: function (subject, topic, data) { + if (topic == "private-browsing") { + if (data == "enter") + storage.privateMode = true; + else if (data == "exit") + storage.privateMode = false; + storage.fireEvent("private-mode", "change", storage.privateMode); + } + else if (topic == "quit-application") { + services.get("observer").removeObserver(this, "quit-application"); + services.get("observer").removeObserver(this, "private-browsing"); + } } - else if (topic == "quit-application") - { - services.get("observer").removeObserver(this, "quit-application"); - services.get("observer").removeObserver(this, "private-browsing"); + }).init(); + }, { store: false }); + storage.addObserver("private-mode", + function (key, event, value) { + autocommands.trigger("PrivateMode", { state: value }); + }, window); + } + + options.add(["urlseparator"], + "Set the separator regex used to separate multiple URL args", + "string", ",\\s"); + }, + + mappings: function () { + mappings.add([modes.NORMAL], + ["y"], "Yank current location to the clipboard", + function () { util.copyToClipboard(buffer.URL, true); }); + + // opening websites + mappings.add([modes.NORMAL], + ["o"], "Open one or more URLs", + function () { commandline.open(":", "open ", modes.EX); }); + + mappings.add([modes.NORMAL], ["O"], + "Open one or more URLs, based on current location", + function () { commandline.open(":", "open " + buffer.URL, modes.EX); }); + + mappings.add([modes.NORMAL], ["t"], + "Open one or more URLs in a new tab", + function () { commandline.open(":", "tabopen ", modes.EX); }); + + mappings.add([modes.NORMAL], ["T"], + "Open one or more URLs in a new tab, based on current location", + function () { commandline.open(":", "tabopen " + buffer.URL, modes.EX); }); + + mappings.add([modes.NORMAL], ["w"], + "Open one or more URLs in a new window", + function () { commandline.open(":", "winopen ", modes.EX); }); + + mappings.add([modes.NORMAL], ["W"], + "Open one or more URLs in a new window, based on current location", + function () { commandline.open(":", "winopen " + buffer.URL, modes.EX); }); + + mappings.add([modes.NORMAL], + [""], "Increment last number in URL", + function (count) { Browser.incrementURL(Math.max(count, 1)); }, + { count: true }); + + mappings.add([modes.NORMAL], + [""], "Decrement last number in URL", + function (count) { Browser.incrementURL(-Math.max(count, 1)); }, + { count: true }); + + mappings.add([modes.NORMAL], ["~"], + "Open home directory", + function () { liberator.open("~"); }); + + mappings.add([modes.NORMAL], ["gh"], + "Open homepage", + function () { BrowserHome(); }); + + mappings.add([modes.NORMAL], ["gH"], + "Open homepage in a new tab", + function () { + let homepages = gHomeButton.getHomePage(); + liberator.open(homepages, { from: "homepage", where: liberator.NEW_TAB }); + }); + + mappings.add([modes.NORMAL], ["gu"], + "Go to parent directory", + function (count) { + function isDirectory(url) { + if (/^file:\/|^\//.test(url)) { + let file = io.File(url); + return file.exists() && file.isDirectory(); + } + else { + // for all other locations just check if the URL ends with / + return /\/$/.test(url); } } - }).init(); - }, { store: false }); - storage.addObserver("private-mode", - function (key, event, value) { - autocommands.trigger("PrivateMode", { state: value }); - }, window); + + if (count < 1) + count = 1; + + // XXX + let url = buffer.URL; + for (let i = 0; i < count; i++) { + if (isDirectory(url)) + url = url.replace(/^(.*?:)(.*?)([^\/]+\/*)$/, "$1$2/"); + else + url = url.replace(/^(.*?:)(.*?)(\/+[^\/]+)$/, "$1$2/"); + } + url = url.replace(/^(.*:\/+.*?)\/+$/, "$1/"); // get rid of more than 1 / at the end + + if (url == buffer.URL) + liberator.beep(); + else + liberator.open(url); + }, + { count: true }); + + mappings.add([modes.NORMAL], ["gU"], + "Go to the root of the website", + function () { + let uri = content.document.location; + if (/(about|mailto):/.test(uri.protocol)) // exclude these special protocols for now + return void liberator.beep(); + liberator.open(uri.protocol + "//" + (uri.host || "") + "/"); + }); + + mappings.add([modes.NORMAL], [""], + "Redraw the screen", + function () { commands.get("redraw").execute("", false); }); + }, + + commands: function () { + commands.add(["downl[oads]", "dl"], + "Show progress of current downloads", + function () { + liberator.open("chrome://mozapps/content/downloads/downloads.xul", + options.get("newtab").has("all", "downloads") + ? liberator.NEW_TAB : liberator.CURRENT_TAB); + }, + { argCount: "0" }); + + commands.add(["o[pen]", "e[dit]"], + "Open one or more URLs in the current tab", + function (args) { + args = args.string; + + if (args) + liberator.open(args); + else + liberator.open("about:blank"); + }, { + completer: function (context) completion.url(context), + literal: 0, + privateData: true, + }); + + commands.add(["redr[aw]"], + "Redraw the screen", + function () { + let wu = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + wu.redraw(); + modes.show(); + }, + { argCount: "0" }); } - - options.add(["urlseparator"], - "Set the separator regex used to separate multiple URL args", - "string", ",\\s"); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - mappings.add([modes.NORMAL], - ["y"], "Yank current location to the clipboard", - function () { util.copyToClipboard(buffer.URL, true); }); - - // opening websites - mappings.add([modes.NORMAL], - ["o"], "Open one or more URLs", - function () { commandline.open(":", "open ", modes.EX); }); - - mappings.add([modes.NORMAL], ["O"], - "Open one or more URLs, based on current location", - function () { commandline.open(":", "open " + buffer.URL, modes.EX); }); - - mappings.add([modes.NORMAL], ["t"], - "Open one or more URLs in a new tab", - function () { commandline.open(":", "tabopen ", modes.EX); }); - - mappings.add([modes.NORMAL], ["T"], - "Open one or more URLs in a new tab, based on current location", - function () { commandline.open(":", "tabopen " + buffer.URL, modes.EX); }); - - mappings.add([modes.NORMAL], ["w"], - "Open one or more URLs in a new window", - function () { commandline.open(":", "winopen ", modes.EX); }); - - mappings.add([modes.NORMAL], ["W"], - "Open one or more URLs in a new window, based on current location", - function () { commandline.open(":", "winopen " + buffer.URL, modes.EX); }); - - mappings.add([modes.NORMAL], - [""], "Increment last number in URL", - function (count) { incrementURL(Math.max(count, 1)); }, - { count: true }); - - mappings.add([modes.NORMAL], - [""], "Decrement last number in URL", - function (count) { incrementURL(-Math.max(count, 1)); }, - { count: true }); - - mappings.add([modes.NORMAL], ["~"], - "Open home directory", - function () { liberator.open("~"); }); - - mappings.add([modes.NORMAL], ["gh"], - "Open homepage", - function () { BrowserHome(); }); - - mappings.add([modes.NORMAL], ["gH"], - "Open homepage in a new tab", - function () - { - let homepages = gHomeButton.getHomePage(); - liberator.open(homepages, { from: "homepage", where: liberator.NEW_TAB }); - }); - - mappings.add([modes.NORMAL], ["gu"], - "Go to parent directory", - function (count) - { - function isDirectory(url) - { - if (/^file:\/|^\//.test(url)) - { - let file = io.File(url); - return file.exists() && file.isDirectory(); - } - else - { - // for all other locations just check if the URL ends with / - return /\/$/.test(url); - } - } - - if (count < 1) - count = 1; - - // XXX - let url = buffer.URL; - for (let i = 0; i < count; i++) - { - if (isDirectory(url)) - url = url.replace(/^(.*?:)(.*?)([^\/]+\/*)$/, "$1$2/"); - else - url = url.replace(/^(.*?:)(.*?)(\/+[^\/]+)$/, "$1$2/"); - } - url = url.replace(/^(.*:\/+.*?)\/+$/, "$1/"); // get rid of more than 1 / at the end - - if (url == buffer.URL) - liberator.beep(); - else - liberator.open(url); - }, - { count: true }); - - mappings.add([modes.NORMAL], ["gU"], - "Go to the root of the website", - function () - { - let uri = content.document.location; - if (/(about|mailto):/.test(uri.protocol)) // exclude these special protocols for now - return void liberator.beep(); - liberator.open(uri.protocol + "//" + (uri.host || "") + "/"); - }); - - mappings.add([modes.NORMAL], [""], - "Redraw the screen", - function () { commands.get("redraw").execute("", false); }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - commands.add(["downl[oads]", "dl"], - "Show progress of current downloads", - function () - { - liberator.open("chrome://mozapps/content/downloads/downloads.xul", - options.get("newtab").has("all", "downloads") - ? liberator.NEW_TAB : liberator.CURRENT_TAB); - }, - { argCount: "0" }); - - commands.add(["o[pen]", "e[dit]"], - "Open one or more URLs in the current tab", - function (args) - { - args = args.string; - - if (args) - liberator.open(args); - else - liberator.open("about:blank"); - }, - { - completer: function (context) completion.url(context), - literal: 0, - privateData: true, - }); - - commands.add(["redr[aw]"], - "Redraw the screen", - function () - { - let wu = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - wu.redraw(); - modes.show(); - }, - { argCount: "0" }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - // TODO: extract browser-specific functionality from liberator - }; - //}}} -} //}}} +}); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/buffer.js b/common/content/buffer.js index 51623b24..30dbdfcf 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -14,32 +14,779 @@ const Point = new Struct("x", "y"); * files. * @instance buffer */ -function Buffer() //{{{ -{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ +const Buffer = Module("buffer", { + init: function () { - if ("ZoomManager" in window) - { - const ZOOM_MIN = Math.round(ZoomManager.MIN * 100); - const ZOOM_MAX = Math.round(ZoomManager.MAX * 100); + this.pageInfo = {}; + + this.addPageInfoSection("f", "Feeds", function (verbose) { + let doc = window.content.document; + + const feedTypes = { + "application/rss+xml": "RSS", + "application/atom+xml": "Atom", + "text/xml": "XML", + "application/xml": "XML", + "application/rdf+xml": "XML" + }; + + function isValidFeed(data, principal, isFeed) { + if (!data || !principal) + return false; + + if (!isFeed) { + var type = data.type && data.type.toLowerCase(); + type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); + + isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 || + // really slimy: general XML types with magic letters in the title + type in feedTypes && /\brss\b/i.test(data.title); + } + + if (isFeed) { + try { + window.urlSecurityCheck(data.href, principal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + } + catch (e) { + isFeed = false; + } + } + + if (type) + data.type = type; + + return isFeed; + } + + let nFeed = 0; + for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) { + let rel = link.rel.toLowerCase(); + let feed = { title: link.title, href: link.href, type: link.type || "" }; + if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) { + nFeed++; + let type = feedTypes[feed.type] || "RSS"; + if (verbose) + yield [feed.title, template.highlightURL(feed.href, true) +  ({type})]; + } + } + + if (!verbose && nFeed) + yield nFeed + " feed" + (nFeed > 1 ? "s" : ""); + }); + + this.addPageInfoSection("g", "General Info", function (verbose) { + let doc = window.content.document; + + // get file size + const ACCESS_READ = Ci.nsICache.ACCESS_READ; + let cacheKey = doc.location.toString().replace(/#.*$/, ""); + + for (let proto in util.Array.itervalues(["HTTP", "FTP"])) { + try { + var cacheEntryDescriptor = services.get("cache").createSession(proto, 0, true) + .openCacheEntry(cacheKey, ACCESS_READ, false); + break; + } + catch (e) {} + } + + let pageSize = []; // [0] bytes; [1] kbytes + if (cacheEntryDescriptor) { + pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false); + pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true); + if (pageSize[1] == pageSize[0]) + pageSize.length = 1; // don't output "xx Bytes" twice + } + + let lastModVerbose = new Date(doc.lastModified).toLocaleString(); + let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X"); + + if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970) + lastModVerbose = lastMod = null; + + if (!verbose) { + if (pageSize[0]) + yield (pageSize[1] || pageSize[0]) + " bytes"; + yield lastMod; + return; + } + + yield ["Title", doc.title]; + yield ["URL", template.highlightURL(doc.location.toString(), true)]; + + let ref = "referrer" in doc && doc.referrer; + if (ref) + yield ["Referrer", template.highlightURL(ref, true)]; + + if (pageSize[0]) + yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")" + : pageSize[0]]; + + yield ["Mime-Type", doc.contentType]; + yield ["Encoding", doc.characterSet]; + yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"]; + if (lastModVerbose) + yield ["Last Modified", lastModVerbose]; + }); + + this.addPageInfoSection("m", "Meta Tags", function (verbose) { + // get meta tag data, sort and put into pageMeta[] + let metaNodes = window.content.document.getElementsByTagName("meta"); + + return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)]) + .sort(function (a, b) util.compareIgnoreCase(a[0], b[0])); + }); + }, + + /** + * @property {Array} The alternative style sheets for the current + * buffer. Only returns style sheets for the 'screen' media type. + */ + get alternateStyleSheets() { + let stylesheets = window.getAllStyleSheets(window.content); + + return stylesheets.filter( + function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title) + ); + }, + + /** + * @property {Object} A map of page info sections to their + * content generating functions. + */ + pageInfo: null, + + /** + * @property {number} A value indicating whether the buffer is loaded. + * Values may be: + * 0 - Loading. + * 1 - Fully loaded. + * 2 - Load failed. + */ + get loaded() { + if (window.content.document.pageIsFullyLoaded !== undefined) + return window.content.document.pageIsFullyLoaded; + return 0; // in doubt return "loading" + }, + set loaded(value) { + window.content.document.pageIsFullyLoaded = value; + }, + + /** + * @property {Node} The last focused input field in the buffer. Used + * by the "gi" key binding. + */ + get lastInputField() window.content.document.lastInputField || null, + set lastInputField(value) { window.content.document.lastInputField = value; }, + + /** + * @property {string} The current top-level document's URL. + */ + get URL() window.content.location.href, + + /** + * @property {string} The current top-level document's URL, sans any + * fragment identifier. + */ + get URI() { + let loc = window.content.location; + return loc.href.substr(0, loc.href.length - loc.hash.length); + }, + + /** + * @property {number} The buffer's height in pixels. + */ + get pageHeight() window.content.innerHeight, + + /** + * @property {number} The current browser's text zoom level, as a + * percentage with 100 as 'normal'. Only affects text size. + */ + get textZoom() getBrowser().markupDocumentViewer.textZoom * 100, + set textZoom(value) { Buffer.setZoom(value, false); }, + + /** + * @property {number} The current browser's text zoom level, as a + * percentage with 100 as 'normal'. Affects text size, as well as + * image size and block size. + */ + get fullZoom() getBrowser().markupDocumentViewer.fullZoom * 100, + set fullZoom(value) { Buffer.setZoom(value, true); }, + + /** + * @property {string} The current document's title. + */ + get title() window.content.document.title, + + /** + * @property {number} The buffer's horizontal scroll percentile. + */ + get scrollXPercent() { + let win = Buffer.findScrollableWindow(); + if (win.scrollMaxX > 0) + return Math.round(win.scrollX / win.scrollMaxX * 100); + else + return 0; + }, + + /** + * @property {number} The buffer's vertical scroll percentile. + */ + get scrollYPercent() { + let win = Buffer.findScrollableWindow(); + if (win.scrollMaxY > 0) + return Math.round(win.scrollY / win.scrollMaxY * 100); + else + return 0; + }, + + /** + * Adds a new section to the page information output. + * + * @param {string} option The section's value in 'pageinfo'. + * @param {string} title The heading for this section's + * output. + * @param {function} func The function to generate this + * section's output. + */ + addPageInfoSection: function addPageInfoSection(option, title, func) { + this.pageInfo[option] = [func, title]; + }, + + /** + * Returns the currently selected word. If the selection is + * null, it tries to guess the word that the caret is + * positioned in. + * + * NOTE: might change the selection + * + * @returns {string} + */ + // FIXME: getSelection() doesn't always preserve line endings, see: + // https://www.mozdev.org/bugs/show_bug.cgi?id=19303 + getCurrentWord: function () { + let selection = window.content.getSelection(); + let range = selection.getRangeAt(0); + if (selection.isCollapsed) { + let selController = this.selectionController; + let caretmode = selController.getCaretEnabled(); + selController.setCaretEnabled(true); + // Only move backwards if the previous character is not a space. + if (range.startOffset > 0 && !/\s/.test(range.startContainer.textContent[range.startOffset - 1])) + selController.wordMove(false, false); + + selController.wordMove(true, true); + selController.setCaretEnabled(caretmode); + return String.match(selection, /\w*/)[0]; + } + if (util.computedStyle(range.startContainer).whiteSpace == "pre" + && util.computedStyle(range.endContainer).whiteSpace == "pre") + return String(range); + return String(selection); + }, + + /** + * Focuses the given element. In contrast to a simple + * elem.focus() call, this function works for iframes and + * image maps. + * + * @param {Node} elem The element to focus. + */ + focusElement: function (elem) { + let doc = window.content.document; + if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement) + return void elem.contentWindow.focus(); + else if (elem instanceof HTMLInputElement && elem.type == "file") { + Buffer.openUploadPrompt(elem); + buffer.lastInputField = elem; + return; + } + + elem.focus(); + + // for imagemap + if (elem instanceof HTMLAreaElement) { + try { + let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat); + + elem.dispatchEvent(events.create(doc, "mouseover", { screenX: x, screenY: y })); + } + catch (e) {} + } + }, + + /** + * Tries to guess links the like of "next" and "prev". Though it has a + * singularly horrendous name, it turns out to be quite useful. + * + * @param {string} rel The relationship to look for. Looks for + * links with matching @rel or @rev attributes, and, + * failing that, looks for an option named rel + + * "pattern", and finds the last link matching that + * RegExp. + */ + followDocumentRelationship: function (rel) { + let regexes = options.get(rel + "pattern").values + .map(function (re) RegExp(re, "i")); + + function followFrame(frame) { + function iter(elems) { + for (let i = 0; i < elems.length; i++) + if (elems[i].rel.toLowerCase() == rel || elems[i].rev.toLowerCase() == rel) + yield elems[i]; + } + + // s have higher priority than normal hrefs + let elems = frame.document.getElementsByTagName("link"); + for (let elem in iter(elems)) { + liberator.open(elem.href); + return true; + } + + // no links? ok, look for hrefs + elems = frame.document.getElementsByTagName("a"); + for (let elem in iter(elems)) { + buffer.followLink(elem, liberator.CURRENT_TAB); + return true; + } + + let res = util.evaluateXPath(options.get("hinttags").defaultValue, frame.document); + for (let [, regex] in Iterator(regexes)) { + for (let i in util.range(res.snapshotLength, 0, -1)) { + let elem = res.snapshotItem(i); + if (regex.test(elem.textContent) || + regex.test(elem.title) || + Array.some(elem.childNodes, function (child) regex.test(child.alt))) + { + buffer.followLink(elem, liberator.CURRENT_TAB); + return true; + } + } + } + return false; + } + + let ret = followFrame(window.content); + if (!ret) + // only loop through frames if the main content didn't match + ret = Array.some(window.content.frames, followFrame); + + if (!ret) + liberator.beep(); + }, + + /** + * Fakes a click on a link. + * + * @param {Node} elem The element to click. + * @param {number} where Where to open the link. See + * {@link liberator.open}. + */ + followLink: function (elem, where) { + let doc = elem.ownerDocument; + let view = doc.defaultView; + let offsetX = 1; + let offsetY = 1; + + if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement) { + elem.contentWindow.focus(); + return; + } + else if (elem instanceof HTMLAreaElement) { // for imagemap + let coords = elem.getAttribute("coords").split(","); + offsetX = Number(coords[0]) + 1; + offsetY = Number(coords[1]) + 1; + } + else if (elem instanceof HTMLInputElement && elem.type == "file") { + Buffer.openUploadPrompt(elem); + return; + } + + let ctrlKey = false, shiftKey = false; + switch (where) { + case liberator.NEW_TAB: + case liberator.NEW_BACKGROUND_TAB: + ctrlKey = true; + shiftKey = (where != liberator.NEW_BACKGROUND_TAB); + break; + case liberator.NEW_WINDOW: + shiftKey = true; + break; + case liberator.CURRENT_TAB: + break; + default: + liberator.log("Invalid where argument for followLink()", 0); + } + + elem.focus(); + + options.withContext(function () { + options.setPref("browser.tabs.loadInBackground", true); + ["mousedown", "mouseup", "click"].forEach(function (event) { + elem.dispatchEvent(events.create(doc, event, { + screenX: offsetX, screenY: offsetY, + ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey + })); + }); + }); + }, + + /** + * @property {nsISelectionController} The current document's selection + * controller. + */ + get selectionController() getBrowser().docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController), + + /** + * Opens the appropriate context menu for elem. + * + * @param {Node} elem The context element. + */ + openContextMenu: function (elem) { + document.popupNode = elem; + let menu = document.getElementById("contentAreaContextMenu"); + menu.showPopup(elem, -1, -1, "context", "bottomleft", "topleft"); + }, + + /** + * Saves a page link to disk. + * + * @param {HTMLAnchorElement} elem The page link to save. + * @param {boolean} skipPrompt Whether to open the "Save Link As..." + * dialog. + */ + saveLink: function (elem, skipPrompt) { + let doc = elem.ownerDocument; + let url = window.makeURLAbsolute(elem.baseURI, elem.href); + let text = elem.textContent; + + try { + window.urlSecurityCheck(url, doc.nodePrincipal); + // we always want to save that link relative to the current working directory + options.setPref("browser.download.lastDir", io.getCurrentDirectory().path); + window.saveURL(url, text, null, true, skipPrompt, makeURI(url, doc.characterSet)); + } + catch (e) { + liberator.echoerr(e); + } + }, + + /** + * Scrolls to the bottom of the current buffer. + */ + scrollBottom: function () { + Buffer.scrollToPercent(null, 100); + }, + + /** + * Scrolls the buffer laterally cols columns. + * + * @param {number} cols The number of columns to scroll. A positive + * value scrolls right and a negative value left. + */ + scrollColumns: function (cols) { + Buffer.scrollHorizontal(null, "columns", cols); + }, + + /** + * Scrolls to the top of the current buffer. + */ + scrollEnd: function () { + Buffer.scrollToPercent(100, null); + }, + + /** + * Scrolls the buffer vertically lines rows. + * + * @param {number} lines The number of lines to scroll. A positive + * value scrolls down and a negative value up. + */ + scrollLines: function (lines) { + Buffer.scrollVertical(null, "lines", lines); + }, + + /** + * Scrolls the buffer vertically pages pages. + * + * @param {number} pages The number of pages to scroll. A positive + * value scrolls down and a negative value up. + */ + scrollPages: function (pages) { + Buffer.scrollVertical(null, "pages", pages); + }, + + /** + * Scrolls the buffer vertically 'scroll' lines. + * + * @param {boolean} direction The direction to scroll. If true then + * scroll up and if false scroll down. + * @param {number} count The multiple of 'scroll' lines to scroll. + * @optional + */ + scrollByScrollSize: function (direction, count) { + direction = direction ? 1 : -1; + count = count || 1; + let win = Buffer.findScrollableWindow(); + + Buffer.checkScrollYBounds(win, direction); + + if (options["scroll"] > 0) + this.scrollLines(options["scroll"] * direction); + else // scroll half a page down in pixels + win.scrollBy(0, win.innerHeight / 2 * direction); + }, + + _scrollByScrollSize: function _scrollByScrollSize(count, direction) { + if (count > 0) + options["scroll"] = count; + buffer.scrollByScrollSize(direction); + }, + + /** + * Scrolls the buffer to the specified screen percentiles. + * + * @param {number} x The horizontal page percentile. + * @param {number} y The vertical page percentile. + */ + scrollToPercent: function (x, y) { + Buffer.scrollToPercent(x, y); + }, + + /** + * Scrolls the buffer to the specified screen pixels. + * + * @param {number} x The horizontal pixel. + * @param {number} y The vertical pixel. + */ + scrollTo: function (x, y) { + marks.add("'", true); + content.scrollTo(x, y); + }, + + /** + * Scrolls the current buffer laterally to its leftmost. + */ + scrollStart: function () { + Buffer.scrollToPercent(0, null); + }, + + /** + * Scrolls the current buffer vertically to the top. + */ + scrollTop: function () { + Buffer.scrollToPercent(null, 0); + }, + + // TODO: allow callback for filtering out unwanted frames? User defined? + /** + * Shifts the focus to another frame within the buffer. Each buffer + * contains at least one frame. + * + * @param {number} count The number of frames to skip through. + * @param {boolean} forward The direction of motion. + */ + shiftFrameFocus: function (count, forward) { + if (!(window.content.document instanceof HTMLDocument)) + return; + + count = Math.max(count, 1); + let frames = []; + + // find all frames - depth-first search + (function (frame) { + if (frame.document.body instanceof HTMLBodyElement) + frames.push(frame); + Array.forEach(frame.frames, arguments.callee); + })(window.content); + + if (frames.length == 0) // currently top is always included + return; + + // remove all unfocusable frames + // TODO: find a better way to do this - walking the tree is too slow + let start = document.commandDispatcher.focusedWindow; + frames = frames.filter(function (frame) { + frame.focus(); + return document.commandDispatcher.focusedWindow == frame; + }); + start.focus(); + + // find the currently focused frame index + // TODO: If the window is a frameset then the first _frame_ should be + // focused. Since this is not the current FF behaviour, + // we initalize current to -1 so the first call takes us to the + // first frame. + let current = frames.indexOf(document.commandDispatcher.focusedWindow); + + // calculate the next frame to focus + let next = current; + if (forward) { + next = current + count; + + if (next > frames.length - 1) { + if (current == frames.length - 1) + liberator.beep(); + next = frames.length - 1; // still allow the frame indicator to be activated + } + } + else { + next = current - count; + + if (next < 0) { + if (current == 0) + liberator.beep(); + next = 0; // still allow the frame indicator to be activated + } + } + + // focus next frame and scroll into view + frames[next].focus(); + if (frames[next] != window.content) + frames[next].frameElement.scrollIntoView(false); + + // add the frame indicator + let doc = frames[next].document; + let indicator = util.xmlToDom(
, doc); + doc.body.appendChild(indicator); + + setTimeout(function () { doc.body.removeChild(indicator); }, 500); + + // Doesn't unattach + //doc.body.setAttributeNS(NS.uri, "activeframe", "true"); + //setTimeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500); + }, + + // similar to pageInfo + // TODO: print more useful information, just like the DOM inspector + /** + * Displays information about the specified element. + * + * @param {Node} elem The element to query. + */ + showElementInfo: function (elem) { + liberator.echo(<>Element:
{util.objectToString(elem, true)}, commandline.FORCE_MULTILINE); + }, + + /** + * Displays information about the current buffer. + * + * @param {boolean} verbose Display more verbose information. + * @param {string} sections A string limiting the displayed sections. + * @default The value of 'pageinfo'. + */ + showPageInfo: function (verbose, sections) { + // Ctrl-g single line output + if (!verbose) { + let file = content.document.location.pathname.split("/").pop() || "[No Name]"; + let title = content.document.title || "[No Title]"; + + let info = template.map("gf", function (opt) + template.map(this.pageInfo[opt][0](), util.identity, ", "), + ", "); + + if (bookmarks.isBookmarked(this.URL)) + info += ", bookmarked"; + + let pageInfoText = <>{file.quote()} [{info}] {title}; + liberator.echo(pageInfoText, commandline.FORCE_SINGLELINE); + return; + } + + let option = sections || options["pageinfo"]; + let list = template.map(option, function (option) { + let opt = this.pageInfo[option]; + if (opt) + return template.table(opt[1], opt[0](true)); + },
); + liberator.echo(list, commandline.FORCE_MULTILINE); + }, + + /** + * Opens a viewer to inspect the source of the currently selected + * range. + */ + viewSelectionSource: function () { + // copied (and tuned somebit) from browser.jar -> nsContextMenu.js + let focusedWindow = document.commandDispatcher.focusedWindow; + if (focusedWindow == window) + focusedWindow = content; + + let docCharset = null; + if (focusedWindow) + docCharset = "charset=" + focusedWindow.document.characterSet; + + let reference = null; + reference = focusedWindow.getSelection(); + + let docUrl = null; + window.openDialog("chrome://global/content/viewPartialSource.xul", + "_blank", "scrollbars,resizable,chrome,dialog=no", + docUrl, docCharset, reference, "selection"); + }, + + /** + * Opens a viewer to inspect the source of the current buffer or the + * specified url. Either the default viewer or the configured + * external editor is used. + * + * @param {string} url The URL of the source. + * @default The current buffer. + * @param {boolean} useExternalEditor View the source in the external editor. + */ + viewSource: function (url, useExternalEditor) { + url = url || buffer.URI; + + if (useExternalEditor) + editor.editFileExternally(url); + else { + const PREFIX = "view-source:"; + if (url.indexOf(PREFIX) == 0) + url = url.substr(PREFIX.length); + else + url = PREFIX + url; + liberator.open(url, { hide: true }); + } + }, + + /** + * Increases the zoom level of the current buffer. + * + * @param {number} steps The number of zoom levels to jump. + * @param {boolean} fullZoom Whether to use full zoom or text zoom. + */ + zoomIn: function (steps, fullZoom) { + Buffer.bumpZoomLevel(steps, fullZoom); + }, + + /** + * Decreases the zoom level of the current buffer. + * + * @param {number} steps The number of zoom levels to jump. + * @param {boolean} fullZoom Whether to use full zoom or text zoom. + */ + zoomOut: function (steps, fullZoom) { + Buffer.bumpZoomLevel(-steps, fullZoom); } +}, { + ZOOM_MIN: "ZoomManager" in window && Math.round(ZoomManager.MIN * 100), + ZOOM_MAX: "ZoomManager" in window && Math.round(ZoomManager.MAX * 100), - function setZoom(value, fullZoom) - { - liberator.assert(value >= ZOOM_MIN && value <= ZOOM_MAX, - "Zoom value out of range (" + ZOOM_MIN + " - " + ZOOM_MAX + "%)"); + setZoom: function setZoom(value, fullZoom) { + liberator.assert(value >= Buffer.ZOOM_MIN || value <= Buffer.ZOOM_MAX, + "Zoom value out of range (" + Buffer.ZOOM_MIN + " - " + Buffer.ZOOM_MAX + "%)"); ZoomManager.useFullZoom = fullZoom; ZoomManager.zoom = value / 100; if ("FullZoom" in window) FullZoom._applySettingToPref(); liberator.echomsg((fullZoom ? "Full" : "Text") + " zoom: " + value + "%"); - } + }, - function bumpZoomLevel(steps, fullZoom) - { + bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) { let values = ZoomManager.zoomValues; let cur = values.indexOf(ZoomManager.snap(ZoomManager.zoom)); let i = util.Math.constrain(cur + steps, 0, values.length - 1); @@ -47,18 +794,16 @@ function Buffer() //{{{ if (i == cur && fullZoom == ZoomManager.useFullZoom) liberator.beep(); - setZoom(Math.round(values[i] * 100), fullZoom); - } + Buffer.setZoom(Math.round(values[i] * 100), fullZoom); + }, - function checkScrollYBounds(win, direction) - { + checkScrollYBounds: function checkScrollYBounds(win, direction) { // NOTE: it's possible to have scrollY > scrollMaxY - FF bug? if (direction > 0 && win.scrollY >= win.scrollMaxY || direction < 0 && win.scrollY == 0) liberator.beep(); - } + }, - function findScrollableWindow() - { + findScrollableWindow: function findScrollableWindow() { let win = window.document.commandDispatcher.focusedWindow; if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0)) return win; @@ -72,20 +817,17 @@ function Buffer() //{{{ return frame; return win; - } + }, - function findScrollable(dir, horizontal) - { + findScrollable: function findScrollable(dir, horizontal) { let pos = "scrollTop", size = "clientHeight", max = "scrollHeight", layoutSize = "offsetHeight", overflow = "overflowX", border1 = "borderTopWidth", border2 = "borderBottomWidth"; if (horizontal) pos = "scrollLeft", size = "clientWidth", max = "scrollWidth", layoutSize = "offsetWidth", overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth"; - function find(elem) - { - for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode) - { + function find(elem) { + for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode) { let style = util.computedStyle(elem); let borderSize = parseInt(style[border1]) + parseInt(style[border2]); let realSize = elem[size]; @@ -102,18 +844,16 @@ function Buffer() //{{{ if (content.getSelection().rangeCount) var elem = find(content.getSelection().getRangeAt(0).startContainer); - if (!(elem instanceof Element)) - { - let doc = findScrollableWindow().document; + if (!(elem instanceof Element)) { + let doc = Buffer.findScrollableWindow().document; elem = find(doc.body || doc.getElementsByTagName("body")[0] || doc.documentElement); } return elem; - } + }, - function scrollVertical(elem, increment, number) - { - elem = elem || findScrollable(number, false); + scrollVertical: function scrollVertical(elem, increment, number) { + elem = elem || Buffer.findScrollable(number, false); let fontSize = parseInt(util.computedStyle(elem).fontSize); let increment; if (increment == "lines") @@ -124,10 +864,9 @@ function Buffer() //{{{ throw Error() elem.scrollTop += number * increment; - } - function scrollHorizontal(elem, increment, number) - { - elem = elem || findScrollable(number, true); + }, + scrollHorizontal: function scrollHorizontal(elem, increment, number) { + elem = elem || Buffer.findScrollable(number, true); let fontSize = parseInt(util.computedStyle(elem).fontSize); let increment; if (increment == "columns") @@ -138,11 +877,10 @@ function Buffer() //{{{ throw Error() elem.scrollLeft += number * increment; - } + }, - function scrollElemToPercent(elem, horizontal, vertical) - { - elem = elem || findScrollable(); + scrollElemToPercent: function scrollElemToPercent(elem, horizontal, vertical) { + elem = elem || Buffer.findScrollable(); marks.add("'", true); if (horizontal != null) @@ -150,11 +888,10 @@ function Buffer() //{{{ if (vertical != null) elem.scrollTop = (elem.scrollHeight - elem.clientHeight) * (vertical / 100); - } + }, - function scrollToPercent(horizontal, vertical) - { - let win = findScrollableWindow(); + scrollToPercent: function scrollToPercent(horizontal, vertical) { + let win = Buffer.findScrollableWindow(); let h, v; if (horizontal == null) @@ -169,539 +906,191 @@ function Buffer() //{{{ marks.add("'", true); win.scrollTo(h, v); - } + }, - // Holds option: [function, title] to generate :pageinfo sections - var pageInfo = {}; - function addPageInfoSection(option, title, func) - { - pageInfo[option] = [func, title]; - } - - function openUploadPrompt(elem) - { + openUploadPrompt: function openUploadPrompt(elem) { commandline.input("Upload file: ", function (path) { let file = io.File(path); if (!file.exists()) return void liberator.beep(); elem.value = file.path; - }, - { + }, { completer: completion.file, default: elem.value }); - } + }, +}, { + commands: function () { + commands.add(["frameo[nly]"], + "Show only the current frame's page", + function (args) { + liberator.open(tabs.localStore.focusedFrame.document.documentURI); + }, + { argCount: "0" }); - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// OPTIONS ///////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + commands.add(["ha[rdcopy]"], + "Print current document", + function (args) { + let arg = args[0]; - options.add(["nextpattern"], // \u00BB is » (>> in a single char) - "Patterns to use when guessing the 'next' page in a document sequence", - "stringlist", "\\bnext\\b,^>$,^(>>|\u00BB)$,^(>|\u00BB),(>|\u00BB)$,\\bmore\\b"); + // FIXME: arg handling is a bit of a mess, check for filename + liberator.assert(!arg || arg[0] == ">" && !liberator.has("Win32"), + "E488: Trailing characters"); - options.add(["previouspattern"], // \u00AB is « (<< in a single char) - "Patterns to use when guessing the 'previous' page in a document sequence", - "stringlist", "\\bprev|previous\\b,^<$,^(<<|\u00AB)$,^(<|\u00AB),(<|\u00AB)$"); + options.withContext(function () { + if (arg) { + options.setPref("print.print_to_file", "true"); + options.setPref("print.print_to_filename", io.File(arg.substr(1)).path); + liberator.echomsg("Printing to file: " + arg.substr(1)); + } + else + liberator.echomsg("Sending to printer..."); - options.add(["pageinfo", "pa"], - "Desired info in the :pageinfo output", - "charlist", "gfm", - { - completer: function (context) [[k, v[1]] for ([k, v] in Iterator(pageInfo))], - validator: Option.validateCompleter - }); + options.setPref("print.always_print_silent", args.bang); + options.setPref("print.show_print_progress", !args.bang); - options.add(["scroll", "scr"], - "Number of lines to scroll with and commands", - "number", 0, - { validator: function (value) value >= 0 }); - - options.add(["showstatuslinks", "ssli"], - "Show the destination of the link under the cursor in the status bar", - "number", 1, - { - completer: function (context) [ - ["0", "Don't show link destination"], - ["1", "Show the link in the status line"], - ["2", "Show the link in the command line"] - ], - validator: Option.validateCompleter - }); - - options.add(["usermode", "um"], - "Show current website with a minimal style sheet to make it easily accessible", - "boolean", false, - { - setter: function (value) getBrowser().markupDocumentViewer.authorStyleDisabled = value, - getter: function () getBrowser().markupDocumentViewer.authorStyleDisabled - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - var myModes = config.browserModes; - - mappings.add(myModes, ["."], - "Repeat the last key event", - function (count) - { - if (mappings.repeat) - { - for (let i in util.interruptibleRange(0, Math.max(count, 1), 100)) - mappings.repeat(); - } - }, - { count: true }); - - mappings.add(myModes, ["i", ""], - "Start caret mode", - function () - { - // setting this option notifies an observer which takes care of the - // mode setting - options.setPref("accessibility.browsewithcaret", true); - }); - - mappings.add(myModes, [""], - "Stop loading the current web page", - function () { tabs.stop(getBrowser().mCurrentTab); }); - - // scrolling - mappings.add(myModes, ["j", "", ""], - "Scroll document down", - function (count) { buffer.scrollLines(Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, ["k", "", ""], - "Scroll document up", - function (count) { buffer.scrollLines(-Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, liberator.has("mail") ? ["h"] : ["h", ""], - "Scroll document to the left", - function (count) { buffer.scrollColumns(-Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, liberator.has("mail") ? ["l"] : ["l", ""], - "Scroll document to the right", - function (count) { buffer.scrollColumns(Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, ["0", "^"], - "Scroll to the absolute left of the document", - function () { buffer.scrollStart(); }); - - mappings.add(myModes, ["$"], - "Scroll to the absolute right of the document", - function () { buffer.scrollEnd(); }); - - mappings.add(myModes, ["gg", ""], - "Go to the top of the document", - function (count) { buffer.scrollToPercent(buffer.scrollXPercent, Math.max(count, 0)); }, - { count: true }); - - mappings.add(myModes, ["G", ""], - "Go to the end of the document", - function (count) { buffer.scrollToPercent(buffer.scrollXPercent, count >= 0 ? count : 100); }, - { count: true }); - - mappings.add(myModes, ["%"], - "Scroll to {count} percent of the document", - function (count) - { - if (count > 0 && count <= 100) - buffer.scrollToPercent(buffer.scrollXPercent, count); - else - liberator.beep(); - }, - { count: true }); - - function scrollByScrollSize(count, direction) - { - if (count > 0) - options["scroll"] = count; - buffer.scrollByScrollSize(direction); - } - - mappings.add(myModes, [""], - "Scroll window downwards in the buffer", - function (count) { scrollByScrollSize(count, true); }, - { count: true }); - - mappings.add(myModes, [""], - "Scroll window upwards in the buffer", - function (count) { scrollByScrollSize(count, false); }, - { count: true }); - - mappings.add(myModes, ["", "", ""], - "Scroll up a full page", - function (count) { buffer.scrollPages(-Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, ["", "", ""], - "Scroll down a full page", - function (count) { buffer.scrollPages(Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, ["]f"], - "Focus next frame", - function (count) { buffer.shiftFrameFocus(Math.max(count, 1), true); }, - { count: true }); - - mappings.add(myModes, ["[f"], - "Focus previous frame", - function (count) { buffer.shiftFrameFocus(Math.max(count, 1), false); }, - { count: true }); - - mappings.add(myModes, ["]]"], - "Follow the link labeled 'next' or '>' if it exists", - function (count) { buffer.followDocumentRelationship("next"); }, - { count: true }); - - mappings.add(myModes, ["[["], - "Follow the link labeled 'prev', 'previous' or '<' if it exists", - function (count) { buffer.followDocumentRelationship("previous"); }, - { count: true }); - - mappings.add(myModes, ["gf"], - "View source", - function () { buffer.viewSource(null, false); }); - - mappings.add(myModes, ["gF"], - "View source with an external editor", - function () { buffer.viewSource(null, true); }); - - mappings.add(myModes, ["|"], - "Toggle between rendered and source view", - function () { buffer.viewSource(null, false); }); - - mappings.add(myModes, ["gi"], - "Focus last used input field", - function (count) - { - if (count < 1 && buffer.lastInputField) - buffer.focusElement(buffer.lastInputField); - else - { - let xpath = ["input[not(@type) or @type='text' or @type='password' or @type='file']", - "textarea[not(@disabled) and not(@readonly)]"]; - - let elements = [m for (m in util.evaluateXPath(xpath))].filter(function (match) { - let computedStyle = util.computedStyle(match); - return computedStyle.visibility != "hidden" && computedStyle.display != "none"; + getBrowser().contentWindow.print(); }); - if (elements.length > 0) - buffer.focusElement(elements[util.Math.constrain(count, 1, elements.length) - 1]); - else - liberator.beep(); - } - }, - { count: true }); - - mappings.add(myModes, ["gP"], - "Open (put) a URL based on the current clipboard contents in a new buffer", - function () - { - liberator.open(util.readFromClipboard(), - liberator[options.get("activate").has("paste") ? "NEW_BACKGROUND_TAB" : "NEW_TAB"]); - }); - - mappings.add(myModes, ["p", ""], - "Open (put) a URL based on the current clipboard contents in the current buffer", - function () - { - let url = util.readFromClipboard(); - if (url) - liberator.open(url); - else - liberator.beep(); - }); - - mappings.add(myModes, ["P"], - "Open (put) a URL based on the current clipboard contents in a new buffer", - function () - { - let url = util.readFromClipboard(); - if (url) - liberator.open(url, { from: "activate", where: liberator.NEW_TAB }); - else - liberator.beep(); - }); - - // reloading - mappings.add(myModes, ["r"], - "Reload the current web page", - function () { tabs.reload(getBrowser().mCurrentTab, false); }); - - mappings.add(myModes, ["R"], - "Reload while skipping the cache", - function () { tabs.reload(getBrowser().mCurrentTab, true); }); - - // yanking - mappings.add(myModes, ["Y"], - "Copy selected text or current word", - function () - { - let sel = buffer.getCurrentWord(); - - if (sel) - util.copyToClipboard(sel, true); - else - liberator.beep(); - }); - - // zooming - mappings.add(myModes, ["zi", "+"], - "Enlarge text zoom of current web page", - function (count) { buffer.zoomIn(Math.max(count, 1), false); }, - { count: true }); - - mappings.add(myModes, ["zm"], - "Enlarge text zoom of current web page by a larger amount", - function (count) { buffer.zoomIn(Math.max(count, 1) * 3, false); }, - { count: true }); - - mappings.add(myModes, ["zo", "-"], - "Reduce text zoom of current web page", - function (count) { buffer.zoomOut(Math.max(count, 1), false); }, - { count: true }); - - mappings.add(myModes, ["zr"], - "Reduce text zoom of current web page by a larger amount", - function (count) { buffer.zoomOut(Math.max(count, 1) * 3, false); }, - { count: true }); - - mappings.add(myModes, ["zz"], - "Set text zoom value of current web page", - function (count) { buffer.textZoom = count > 1 ? count : 100; }, - { count: true }); - - mappings.add(myModes, ["zI"], - "Enlarge full zoom of current web page", - function (count) { buffer.zoomIn(Math.max(count, 1), true); }, - { count: true }); - - mappings.add(myModes, ["zM"], - "Enlarge full zoom of current web page by a larger amount", - function (count) { buffer.zoomIn(Math.max(count, 1) * 3, true); }, - { count: true }); - - mappings.add(myModes, ["zO"], - "Reduce full zoom of current web page", - function (count) { buffer.zoomOut(Math.max(count, 1), true); }, - { count: true }); - - mappings.add(myModes, ["zR"], - "Reduce full zoom of current web page by a larger amount", - function (count) { buffer.zoomOut(Math.max(count, 1) * 3, true); }, - { count: true }); - - mappings.add(myModes, ["zZ"], - "Set full zoom value of current web page", - function (count) { buffer.fullZoom = count > 1 ? count : 100; }, - { count: true }); - - // page info - mappings.add(myModes, [""], - "Print the current file name", - function (count) { buffer.showPageInfo(false); }, - { count: true }); - - mappings.add(myModes, ["g"], - "Print file information", - function () { buffer.showPageInfo(true); }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - commands.add(["frameo[nly]"], - "Show only the current frame's page", - function (args) - { - liberator.open(tabs.localStore.focusedFrame.document.documentURI); - }, - { argCount: "0" }); - - commands.add(["ha[rdcopy]"], - "Print current document", - function (args) - { - let arg = args[0]; - - // FIXME: arg handling is a bit of a mess, check for filename - liberator.assert(!arg || arg[0] == ">" && !liberator.has("Win32"), - "E488: Trailing characters"); - - options.withContext(function () { if (arg) - { - options.setPref("print.print_to_file", "true"); - options.setPref("print.print_to_filename", io.File(arg.substr(1)).path); - liberator.echomsg("Printing to file: " + arg.substr(1)); - } + liberator.echomsg("Printed: " + arg.substr(1)); else - liberator.echomsg("Sending to printer..."); - - options.setPref("print.always_print_silent", args.bang); - options.setPref("print.show_print_progress", !args.bang); - - getBrowser().contentWindow.print(); + liberator.echomsg("Print job sent."); + }, + { + argCount: "?", + literal: 0, + bang: true }); - if (arg) - liberator.echomsg("Printed: " + arg.substr(1)); - else - liberator.echomsg("Print job sent."); - }, - { - argCount: "?", - literal: 0, - bang: true - }); - - commands.add(["pa[geinfo]"], - "Show various page information", - function (args) { buffer.showPageInfo(true, args[0]); }, - { - argCount: "?", - completer: function (context) + commands.add(["pa[geinfo]"], + "Show various page information", + function (args) { buffer.showPageInfo(true, args[0]); }, { - completion.optionValue(context, "pageinfo", "+", ""); - context.title = ["Page Info"]; - } - }); + argCount: "?", + completer: function (context) { + completion.optionValue(context, "pageinfo", "+", ""); + context.title = ["Page Info"]; + } + }); - commands.add(["pagest[yle]", "pas"], - "Select the author style sheet to apply", - function (args) - { - let arg = args.literalArg; + commands.add(["pagest[yle]", "pas"], + "Select the author style sheet to apply", + function (args) { + let arg = args.literalArg; - let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title); + let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title); - liberator.assert(!arg || titles.indexOf(arg) >= 0, - "E475: Invalid argument: " + arg); + liberator.assert(!arg || titles.indexOf(arg) >= 0, + "E475: Invalid argument: " + arg); - if (options["usermode"]) - options["usermode"] = false; + if (options["usermode"]) + options["usermode"] = false; - window.stylesheetSwitchAll(window.content, arg); - }, - { - argCount: "?", - completer: function (context) completion.alternateStyleSheet(context), - literal: 0 - }); - - commands.add(["re[load]"], - "Reload the current web page", - function (args) { tabs.reload(getBrowser().mCurrentTab, args.bang); }, - { - bang: true, - argCount: "0" - }); - - // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional? - commands.add(["sav[eas]", "w[rite]"], - "Save current document to disk", - function (args) - { - let doc = window.content.document; - let chosenData = null; - let filename = args[0]; - - if (filename) + window.stylesheetSwitchAll(window.content, arg); + }, { - let file = io.File(filename); + argCount: "?", + completer: function (context) completion.alternateStyleSheet(context), + literal: 0 + }); - liberator.assert(!file.exists() || args.bang, - "E13: File exists (add ! to override)"); - - chosenData = { file: file, uri: window.makeURI(doc.location.href, doc.characterSet) }; - } - - // if browser.download.useDownloadDir = false then the "Save As" - // dialog is used with this as the default directory - // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored? - options.setPref("browser.download.lastDir", io.getCurrentDirectory().path); - - try + commands.add(["re[load]"], + "Reload the current web page", + function (args) { tabs.reload(getBrowser().mCurrentTab, args.bang); }, { - var contentDisposition = window.content - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .getDocumentMetadata("content-disposition"); - } - catch (e) {} + bang: true, + argCount: "0" + }); - window.internalSave(doc.location.href, doc, null, contentDisposition, - doc.contentType, false, null, chosenData, doc.referrer ? - window.makeURI(doc.referrer) : null, true); - }, - { - argCount: "?", - bang: true, - completer: function (context) completion.file(context) - }); + // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional? + commands.add(["sav[eas]", "w[rite]"], + "Save current document to disk", + function (args) { + let doc = window.content.document; + let chosenData = null; + let filename = args[0]; - commands.add(["st[op]"], - "Stop loading the current web page", - function () { tabs.stop(getBrowser().mCurrentTab); }, - { argCount: "0" }); + if (filename) { + let file = io.File(filename); - commands.add(["vie[wsource]"], - "View source code of current document", - function (args) { buffer.viewSource(args[0], args.bang); }, - { - argCount: "?", - bang: true, - completer: function (context) completion.url(context, "bhf") - }); + liberator.assert(!file.exists() || args.bang, + "E13: File exists (add ! to override)"); - commands.add(["zo[om]"], - "Set zoom value of current web page", - function (args) - { - let arg = args[0]; - let level; + chosenData = { file: file, uri: window.makeURI(doc.location.href, doc.characterSet) }; + } - if (!arg) - level = 100; - else if (/^\d+$/.test(arg)) - level = parseInt(arg, 10); - else if (/^[+-]\d+$/.test(arg)) + // if browser.download.useDownloadDir = false then the "Save As" + // dialog is used with this as the default directory + // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored? + options.setPref("browser.download.lastDir", io.getCurrentDirectory().path); + + try { + var contentDisposition = window.content + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .getDocumentMetadata("content-disposition"); + } + catch (e) {} + + window.internalSave(doc.location.href, doc, null, contentDisposition, + doc.contentType, false, null, chosenData, doc.referrer ? + window.makeURI(doc.referrer) : null, true); + }, { - if (args.bang) - level = buffer.fullZoom + parseInt(arg, 10); + argCount: "?", + bang: true, + completer: function (context) completion.file(context) + }); + + commands.add(["st[op]"], + "Stop loading the current web page", + function () { tabs.stop(getBrowser().mCurrentTab); }, + { argCount: "0" }); + + commands.add(["vie[wsource]"], + "View source code of current document", + function (args) { buffer.viewSource(args[0], args.bang); }, + { + argCount: "?", + bang: true, + completer: function (context) completion.url(context, "bhf") + }); + + commands.add(["zo[om]"], + "Set zoom value of current web page", + function (args) { + let arg = args[0]; + let level; + + if (!arg) + level = 100; + else if (/^\d+$/.test(arg)) + level = parseInt(arg, 10); + else if (/^[+-]\d+$/.test(arg)) { + if (args.bang) + level = buffer.fullZoom + parseInt(arg, 10); + else + level = buffer.textZoom + parseInt(arg, 10); + + // relative args shouldn't take us out of range + level = util.Math.constrain(level, Buffer.ZOOM_MIN, Buffer.ZOOM_MAX); + } else - level = buffer.textZoom + parseInt(arg, 10); + liberator.assert(false, "E488: Trailing characters"); - // relative args shouldn't take us out of range - level = util.Math.constrain(level, ZOOM_MIN, ZOOM_MAX); - } - else - liberator.assert(false, "E488: Trailing characters"); - - if (args.bang) - buffer.fullZoom = level; - else - buffer.textZoom = level; - }, - { - argCount: "?", - bang: true - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMPLETIONS ///////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - liberator.registerObserver("load_completion", function () { + if (args.bang) + buffer.fullZoom = level; + else + buffer.textZoom = level; + }, + { + argCount: "?", + bang: true + }); + }, + completion: function () { completion.alternateStyleSheet = function alternateStylesheet(context) { context.title = ["Stylesheet", "Location"]; @@ -750,1220 +1139,306 @@ function Buffer() //{{{ }; }); }; - }); + }, + mappings: function () { + var myModes = config.browserModes; - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PAGE INFO /////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - addPageInfoSection("f", "Feeds", function (verbose) { - let doc = window.content.document; - - const feedTypes = { - "application/rss+xml": "RSS", - "application/atom+xml": "Atom", - "text/xml": "XML", - "application/xml": "XML", - "application/rdf+xml": "XML" - }; - - function isValidFeed(data, principal, isFeed) - { - if (!data || !principal) - return false; - - if (!isFeed) - { - var type = data.type && data.type.toLowerCase(); - type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); - - isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 || - // really slimy: general XML types with magic letters in the title - type in feedTypes && /\brss\b/i.test(data.title); - } - - if (isFeed) - { - try - { - window.urlSecurityCheck(data.href, principal, - Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + mappings.add(myModes, ["."], + "Repeat the last key event", + function (count) { + if (mappings.repeat) { + for (let i in util.interruptibleRange(0, Math.max(count, 1), 100)) + mappings.repeat(); } - catch (e) - { - isFeed = false; - } - } + }, + { count: true }); - if (type) - data.type = type; - - return isFeed; - } - - let nFeed = 0; - for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) - { - let rel = link.rel.toLowerCase(); - let feed = { title: link.title, href: link.href, type: link.type || "" }; - if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) - { - nFeed++; - let type = feedTypes[feed.type] || "RSS"; - if (verbose) - yield [feed.title, template.highlightURL(feed.href, true) +  ({type})]; - } - } - - if (!verbose && nFeed) - yield nFeed + " feed" + (nFeed > 1 ? "s" : ""); - }); - - addPageInfoSection("g", "General Info", function (verbose) { - let doc = window.content.document; - - // get file size - const ACCESS_READ = Ci.nsICache.ACCESS_READ; - let cacheKey = doc.location.toString().replace(/#.*$/, ""); - - for (let proto in util.Array.itervalues(["HTTP", "FTP"])) - { - try - { - var cacheEntryDescriptor = services.get("cache").createSession(proto, 0, true) - .openCacheEntry(cacheKey, ACCESS_READ, false); - break; - } - catch (e) {} - } - - let pageSize = []; // [0] bytes; [1] kbytes - if (cacheEntryDescriptor) - { - pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false); - pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true); - if (pageSize[1] == pageSize[0]) - pageSize.length = 1; // don't output "xx Bytes" twice - } - - let lastModVerbose = new Date(doc.lastModified).toLocaleString(); - let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X"); - - if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970) - lastModVerbose = lastMod = null; - - if (!verbose) - { - if (pageSize[0]) - yield (pageSize[1] || pageSize[0]) + " bytes"; - yield lastMod; - return; - } - - yield ["Title", doc.title]; - yield ["URL", template.highlightURL(doc.location.toString(), true)]; - - let ref = "referrer" in doc && doc.referrer; - if (ref) - yield ["Referrer", template.highlightURL(ref, true)]; - - if (pageSize[0]) - yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")" - : pageSize[0]]; - - yield ["Mime-Type", doc.contentType]; - yield ["Encoding", doc.characterSet]; - yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"]; - if (lastModVerbose) - yield ["Last Modified", lastModVerbose]; - }); - - addPageInfoSection("m", "Meta Tags", function (verbose) { - // get meta tag data, sort and put into pageMeta[] - let metaNodes = window.content.document.getElementsByTagName("meta"); - - return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)]) - .sort(function (a, b) util.compareIgnoreCase(a[0], b[0])); - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - - get evaluateXPath() util.evaluateXPath, - - /** - * @property {Array} The alternative style sheets for the current - * buffer. Only returns style sheets for the 'screen' media type. - */ - get alternateStyleSheets() - { - let stylesheets = window.getAllStyleSheets(window.content); - - return stylesheets.filter( - function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title) - ); - }, - - /** - * @property {Object} A map of page info sections to their - * content generating functions. - */ - get pageInfo() pageInfo, - - /** - * @property {number} A value indicating whether the buffer is loaded. - * Values may be: - * 0 - Loading. - * 1 - Fully loaded. - * 2 - Load failed. - */ - get loaded() - { - if (window.content.document.pageIsFullyLoaded !== undefined) - return window.content.document.pageIsFullyLoaded; - else - return 0; // in doubt return "loading" - }, - set loaded(value) - { - window.content.document.pageIsFullyLoaded = value; - }, - - /** - * @property {Node} The last focused input field in the buffer. Used - * by the "gi" key binding. - */ - get lastInputField() window.content.document.lastInputField || null, - set lastInputField(value) { window.content.document.lastInputField = value; }, - - /** - * @property {string} The current top-level document's URL. - */ - get URL() window.content.location.href, - - /** - * @property {string} The current top-level document's URL, sans any - * fragment identifier. - */ - get URI() - { - let loc = window.content.location; - return loc.href.substr(0, loc.href.length - loc.hash.length); - }, - - /** - * @property {number} The buffer's height in pixels. - */ - get pageHeight() window.content.innerHeight, - - /** - * @property {number} The current browser's text zoom level, as a - * percentage with 100 as 'normal'. Only affects text size. - */ - get textZoom() getBrowser().markupDocumentViewer.textZoom * 100, - set textZoom(value) { setZoom(value, false); }, - - /** - * @property {number} The current browser's text zoom level, as a - * percentage with 100 as 'normal'. Affects text size, as well as - * image size and block size. - */ - get fullZoom() getBrowser().markupDocumentViewer.fullZoom * 100, - set fullZoom(value) { setZoom(value, true); }, - - /** - * @property {string} The current document's title. - */ - get title() window.content.document.title, - - /** - * @property {number} The buffer's horizontal scroll percentile. - */ - get scrollXPercent() - { - let win = findScrollableWindow(); - if (win.scrollMaxX > 0) - return Math.round(win.scrollX / win.scrollMaxX * 100); - else - return 0; - }, - - /** - * @property {number} The buffer's vertical scroll percentile. - */ - get scrollYPercent() - { - let win = findScrollableWindow(); - if (win.scrollMaxY > 0) - return Math.round(win.scrollY / win.scrollMaxY * 100); - else - return 0; - }, - - /** - * Adds a new section to the page information output. - * - * @param {string} option The section's value in 'pageinfo'. - * @param {string} title The heading for this section's - * output. - * @param {function} func The function to generate this - * section's output. - */ - addPageInfoSection: addPageInfoSection, - - /** - * Returns the currently selected word. If the selection is - * null, it tries to guess the word that the caret is - * positioned in. - * - * NOTE: might change the selection - * - * @returns {string} - */ - // FIXME: getSelection() doesn't always preserve line endings, see: - // https://www.mozdev.org/bugs/show_bug.cgi?id=19303 - getCurrentWord: function () - { - let selection = window.content.getSelection(); - let range = selection.getRangeAt(0); - if (selection.isCollapsed) - { - let selController = this.selectionController; - let caretmode = selController.getCaretEnabled(); - selController.setCaretEnabled(true); - // Only move backwards if the previous character is not a space. - if (range.startOffset > 0 && !/\s/.test(range.startContainer.textContent[range.startOffset - 1])) - selController.wordMove(false, false); - - selController.wordMove(true, true); - selController.setCaretEnabled(caretmode); - return String.match(selection, /\w*/)[0]; - } - if (util.computedStyle(range.startContainer).whiteSpace == "pre" - && util.computedStyle(range.endContainer).whiteSpace == "pre") - return String(range); - return String(selection); - }, - - /** - * Focuses the given element. In contrast to a simple - * elem.focus() call, this function works for iframes and - * image maps. - * - * @param {Node} elem The element to focus. - */ - focusElement: function (elem) - { - let doc = window.content.document; - if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement) - return void elem.contentWindow.focus(); - else if (elem instanceof HTMLInputElement && elem.type == "file") - { - openUploadPrompt(elem); - buffer.lastInputField = elem; - return; - } - - elem.focus(); - - // for imagemap - if (elem instanceof HTMLAreaElement) - { - try - { - let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat); - - elem.dispatchEvent(events.create(doc, "mouseover", { screenX: x, screenY: y })); - } - catch (e) {} - } - }, - - /** - * Tries to guess links the like of "next" and "prev". Though it has a - * singularly horrendous name, it turns out to be quite useful. - * - * @param {string} rel The relationship to look for. Looks for - * links with matching @rel or @rev attributes, and, - * failing that, looks for an option named rel + - * "pattern", and finds the last link matching that - * RegExp. - */ - followDocumentRelationship: function (rel) - { - let regexes = options.get(rel + "pattern").values - .map(function (re) RegExp(re, "i")); - - function followFrame(frame) - { - function iter(elems) - { - for (let i = 0; i < elems.length; i++) - if (elems[i].rel.toLowerCase() == rel || elems[i].rev.toLowerCase() == rel) - yield elems[i]; - } - - // s have higher priority than normal
hrefs - let elems = frame.document.getElementsByTagName("link"); - for (let elem in iter(elems)) - { - liberator.open(elem.href); - return true; - } - - // no links? ok, look for hrefs - elems = frame.document.getElementsByTagName("a"); - for (let elem in iter(elems)) - { - buffer.followLink(elem, liberator.CURRENT_TAB); - return true; - } - - let res = util.evaluateXPath(options.get("hinttags").defaultValue, frame.document); - for (let [, regex] in Iterator(regexes)) - { - for (let i in util.range(res.snapshotLength, 0, -1)) - { - let elem = res.snapshotItem(i); - if (regex.test(elem.textContent) || - regex.test(elem.title) || - Array.some(elem.childNodes, function (child) regex.test(child.alt))) - { - buffer.followLink(elem, liberator.CURRENT_TAB); - return true; - } - } - } - return false; - } - - let ret = followFrame(window.content); - if (!ret) - // only loop through frames if the main content didn't match - ret = Array.some(window.content.frames, followFrame); - - if (!ret) - liberator.beep(); - }, - - /** - * Fakes a click on a link. - * - * @param {Node} elem The element to click. - * @param {number} where Where to open the link. See - * {@link liberator.open}. - */ - followLink: function (elem, where) - { - let doc = elem.ownerDocument; - let view = doc.defaultView; - let offsetX = 1; - let offsetY = 1; - - if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement) - { - elem.contentWindow.focus(); - return; - } - else if (elem instanceof HTMLAreaElement) // for imagemap - { - let coords = elem.getAttribute("coords").split(","); - offsetX = Number(coords[0]) + 1; - offsetY = Number(coords[1]) + 1; - } - else if (elem instanceof HTMLInputElement && elem.type == "file") - { - openUploadPrompt(elem); - return; - } - - let ctrlKey = false, shiftKey = false; - switch (where) - { - case liberator.NEW_TAB: - case liberator.NEW_BACKGROUND_TAB: - ctrlKey = true; - shiftKey = (where != liberator.NEW_BACKGROUND_TAB); - break; - case liberator.NEW_WINDOW: - shiftKey = true; - break; - case liberator.CURRENT_TAB: - break; - default: - liberator.log("Invalid where argument for followLink()", 0); - } - - elem.focus(); - - options.withContext(function () { - options.setPref("browser.tabs.loadInBackground", true); - ["mousedown", "mouseup", "click"].forEach(function (event) { - elem.dispatchEvent(events.create(doc, event, { - screenX: offsetX, screenY: offsetY, - ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey - })); - }); + mappings.add(myModes, ["i", ""], + "Start caret mode", + function () { + // setting this option notifies an observer which takes care of the + // mode setting + options.setPref("accessibility.browsewithcaret", true); }); - }, - /** - * @property {nsISelectionController} The current document's selection - * controller. - */ - get selectionController() getBrowser().docShell - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsISelectionDisplay) - .QueryInterface(Ci.nsISelectionController), + mappings.add(myModes, [""], + "Stop loading the current web page", + function () { tabs.stop(getBrowser().mCurrentTab); }); - /** - * Opens the appropriate context menu for elem. - * - * @param {Node} elem The context element. - */ - openContextMenu: function (elem) - { - document.popupNode = elem; - let menu = document.getElementById("contentAreaContextMenu"); - menu.showPopup(elem, -1, -1, "context", "bottomleft", "topleft"); - }, + // scrolling + mappings.add(myModes, ["j", "", ""], + "Scroll document down", + function (count) { buffer.scrollLines(Math.max(count, 1)); }, + { count: true }); - /** - * Saves a page link to disk. - * - * @param {HTMLAnchorElement} elem The page link to save. - * @param {boolean} skipPrompt Whether to open the "Save Link As..." - * dialog. - */ - saveLink: function (elem, skipPrompt) - { - let doc = elem.ownerDocument; - let url = window.makeURLAbsolute(elem.baseURI, elem.href); - let text = elem.textContent; + mappings.add(myModes, ["k", "", ""], + "Scroll document up", + function (count) { buffer.scrollLines(-Math.max(count, 1)); }, + { count: true }); - try - { - window.urlSecurityCheck(url, doc.nodePrincipal); - // we always want to save that link relative to the current working directory - options.setPref("browser.download.lastDir", io.getCurrentDirectory().path); - window.saveURL(url, text, null, true, skipPrompt, makeURI(url, doc.characterSet)); - } - catch (e) - { - liberator.echoerr(e); - } - }, + mappings.add(myModes, liberator.has("mail") ? ["h"] : ["h", ""], + "Scroll document to the left", + function (count) { buffer.scrollColumns(-Math.max(count, 1)); }, + { count: true }); - /** - * Scrolls to the bottom of the current buffer. - */ - scrollBottom: function () - { - scrollToPercent(null, 100); - }, + mappings.add(myModes, liberator.has("mail") ? ["l"] : ["l", ""], + "Scroll document to the right", + function (count) { buffer.scrollColumns(Math.max(count, 1)); }, + { count: true }); - /** - * Scrolls the buffer laterally cols columns. - * - * @param {number} cols The number of columns to scroll. A positive - * value scrolls right and a negative value left. - */ - scrollColumns: function (cols) - { - scrollHorizontal(null, "columns", cols); - }, + mappings.add(myModes, ["0", "^"], + "Scroll to the absolute left of the document", + function () { buffer.scrollStart(); }); - /** - * Scrolls to the top of the current buffer. - */ - scrollEnd: function () - { - scrollToPercent(100, null); - }, + mappings.add(myModes, ["$"], + "Scroll to the absolute right of the document", + function () { buffer.scrollEnd(); }); - /** - * Scrolls the buffer vertically lines rows. - * - * @param {number} lines The number of lines to scroll. A positive - * value scrolls down and a negative value up. - */ - scrollLines: function (lines) - { - scrollVertical(null, "lines", lines); - }, + mappings.add(myModes, ["gg", ""], + "Go to the top of the document", + function (count) { buffer.scrollToPercent(buffer.scrollXPercent, Math.max(count, 0)); }, + { count: true }); - /** - * Scrolls the buffer vertically pages pages. - * - * @param {number} pages The number of pages to scroll. A positive - * value scrolls down and a negative value up. - */ - scrollPages: function (pages) - { - scrollVertical(null, "pages", pages); - }, + mappings.add(myModes, ["G", ""], + "Go to the end of the document", + function (count) { buffer.scrollToPercent(buffer.scrollXPercent, count >= 0 ? count : 100); }, + { count: true }); - /** - * Scrolls the buffer vertically 'scroll' lines. - * - * @param {boolean} direction The direction to scroll. If true then - * scroll up and if false scroll down. - * @param {number} count The multiple of 'scroll' lines to scroll. - * @optional - */ - scrollByScrollSize: function (direction, count) - { - direction = direction ? 1 : -1; - count = count || 1; - let win = findScrollableWindow(); - - checkScrollYBounds(win, direction); - - if (options["scroll"] > 0) - this.scrollLines(options["scroll"] * direction); - else // scroll half a page down in pixels - win.scrollBy(0, win.innerHeight / 2 * direction); - }, - - /** - * Scrolls the buffer to the specified screen percentiles. - * - * @param {number} x The horizontal page percentile. - * @param {number} y The vertical page percentile. - */ - scrollToPercent: function (x, y) - { - scrollToPercent(x, y); - }, - - /** - * Scrolls the buffer to the specified screen pixels. - * - * @param {number} x The horizontal pixel. - * @param {number} y The vertical pixel. - */ - scrollTo: function (x, y) - { - marks.add("'", true); - content.scrollTo(x, y); - }, - - /** - * Scrolls the current buffer laterally to its leftmost. - */ - scrollStart: function () - { - scrollToPercent(0, null); - }, - - /** - * Scrolls the current buffer vertically to the top. - */ - scrollTop: function () - { - scrollToPercent(null, 0); - }, - - // TODO: allow callback for filtering out unwanted frames? User defined? - /** - * Shifts the focus to another frame within the buffer. Each buffer - * contains at least one frame. - * - * @param {number} count The number of frames to skip through. - * @param {boolean} forward The direction of motion. - */ - shiftFrameFocus: function (count, forward) - { - if (!(window.content.document instanceof HTMLDocument)) - return; - - count = Math.max(count, 1); - let frames = []; - - // find all frames - depth-first search - (function (frame) { - if (frame.document.body instanceof HTMLBodyElement) - frames.push(frame); - Array.forEach(frame.frames, arguments.callee); - })(window.content); - - if (frames.length == 0) // currently top is always included - return; - - // remove all unfocusable frames - // TODO: find a better way to do this - walking the tree is too slow - let start = document.commandDispatcher.focusedWindow; - frames = frames.filter(function (frame) { - frame.focus(); - return document.commandDispatcher.focusedWindow == frame; - }); - start.focus(); - - // find the currently focused frame index - // TODO: If the window is a frameset then the first _frame_ should be - // focused. Since this is not the current FF behaviour, - // we initalize current to -1 so the first call takes us to the - // first frame. - let current = frames.indexOf(document.commandDispatcher.focusedWindow); - - // calculate the next frame to focus - let next = current; - if (forward) - { - next = current + count; - - if (next > frames.length - 1) - { - if (current == frames.length - 1) - liberator.beep(); - next = frames.length - 1; // still allow the frame indicator to be activated - } - } - else - { - next = current - count; - - if (next < 0) - { - if (current == 0) - liberator.beep(); - next = 0; // still allow the frame indicator to be activated - } - } - - // focus next frame and scroll into view - frames[next].focus(); - if (frames[next] != window.content) - frames[next].frameElement.scrollIntoView(false); - - // add the frame indicator - let doc = frames[next].document; - let indicator = util.xmlToDom(
, doc); - doc.body.appendChild(indicator); - - setTimeout(function () { doc.body.removeChild(indicator); }, 500); - - // Doesn't unattach - //doc.body.setAttributeNS(NS.uri, "activeframe", "true"); - //setTimeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500); - }, - - // similar to pageInfo - // TODO: print more useful information, just like the DOM inspector - /** - * Displays information about the specified element. - * - * @param {Node} elem The element to query. - */ - showElementInfo: function (elem) - { - liberator.echo(<>Element:
{util.objectToString(elem, true)}, commandline.FORCE_MULTILINE); - }, - - /** - * Displays information about the current buffer. - * - * @param {boolean} verbose Display more verbose information. - * @param {string} sections A string limiting the displayed sections. - * @default The value of 'pageinfo'. - */ - showPageInfo: function (verbose, sections) - { - // Ctrl-g single line output - if (!verbose) - { - let file = content.document.location.pathname.split("/").pop() || "[No Name]"; - let title = content.document.title || "[No Title]"; - - let info = template.map("gf", function (opt) - template.map(pageInfo[opt][0](), util.identity, ", "), - ", "); - - if (bookmarks.isBookmarked(this.URL)) - info += ", bookmarked"; - - let pageInfoText = <>{file.quote()} [{info}] {title}; - liberator.echo(pageInfoText, commandline.FORCE_SINGLELINE); - return; - } - - let option = sections || options["pageinfo"]; - let list = template.map(option, function (option) { - let opt = pageInfo[option]; - if (opt) - return template.table(opt[1], opt[0](true)); - },
); - liberator.echo(list, commandline.FORCE_MULTILINE); - }, - - /** - * Opens a viewer to inspect the source of the currently selected - * range. - */ - viewSelectionSource: function () - { - // copied (and tuned somebit) from browser.jar -> nsContextMenu.js - let focusedWindow = document.commandDispatcher.focusedWindow; - if (focusedWindow == window) - focusedWindow = content; - - let docCharset = null; - if (focusedWindow) - docCharset = "charset=" + focusedWindow.document.characterSet; - - let reference = null; - reference = focusedWindow.getSelection(); - - let docUrl = null; - window.openDialog("chrome://global/content/viewPartialSource.xul", - "_blank", "scrollbars,resizable,chrome,dialog=no", - docUrl, docCharset, reference, "selection"); - }, - - /** - * Opens a viewer to inspect the source of the current buffer or the - * specified url. Either the default viewer or the configured - * external editor is used. - * - * @param {string} url The URL of the source. - * @default The current buffer. - * @param {boolean} useExternalEditor View the source in the external editor. - */ - viewSource: function (url, useExternalEditor) - { - url = url || buffer.URI; - - if (useExternalEditor) - editor.editFileExternally(url); - else - { - const PREFIX = "view-source:"; - if (url.indexOf(PREFIX) == 0) - url = url.substr(PREFIX.length); + mappings.add(myModes, ["%"], + "Scroll to {count} percent of the document", + function (count) { + if (count > 0 && count <= 100) + buffer.scrollToPercent(buffer.scrollXPercent, count); else - url = PREFIX + url; - liberator.open(url, { hide: true }); - } - }, + liberator.beep(); + }, + { count: true }); - /** - * Increases the zoom level of the current buffer. - * - * @param {number} steps The number of zoom levels to jump. - * @param {boolean} fullZoom Whether to use full zoom or text zoom. - */ - zoomIn: function (steps, fullZoom) - { - bumpZoomLevel(steps, fullZoom); - }, + mappings.add(myModes, [""], + "Scroll window downwards in the buffer", + function (count) { buffer._scrollByScrollSize(count, true); }, + { count: true }); - /** - * Decreases the zoom level of the current buffer. - * - * @param {number} steps The number of zoom levels to jump. - * @param {boolean} fullZoom Whether to use full zoom or text zoom. - */ - zoomOut: function (steps, fullZoom) - { - bumpZoomLevel(-steps, fullZoom); - } - }; - //}}} -} //}}} + mappings.add(myModes, [""], + "Scroll window upwards in the buffer", + function (count) { buffer._scrollByScrollSize(count, false); }, + { count: true }); -/** - * @instance marks - */ -function Marks() //{{{ -{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + mappings.add(myModes, ["", "", ""], + "Scroll up a full page", + function (count) { buffer.scrollPages(-Math.max(count, 1)); }, + { count: true }); - var localMarks = storage.newMap("local-marks", { store: true, privateData: true }); - var urlMarks = storage.newMap("url-marks", { store: true, privateData: true }); + mappings.add(myModes, ["", "", ""], + "Scroll down a full page", + function (count) { buffer.scrollPages(Math.max(count, 1)); }, + { count: true }); - var pendingJumps = []; - var appContent = document.getElementById("appcontent"); + mappings.add(myModes, ["]f"], + "Focus next frame", + function (count) { buffer.shiftFrameFocus(Math.max(count, 1), true); }, + { count: true }); - if (appContent) - appContent.addEventListener("load", onPageLoad, true); + mappings.add(myModes, ["[f"], + "Focus previous frame", + function (count) { buffer.shiftFrameFocus(Math.max(count, 1), false); }, + { count: true }); - function onPageLoad(event) - { - let win = event.originalTarget.defaultView; - for (let [i, mark] in Iterator(pendingJumps)) - { - if (win && win.location.href == mark.location) - { - buffer.scrollToPercent(mark.position.x * 100, mark.position.y * 100); - pendingJumps.splice(i, 1); - return; - } - } - } + mappings.add(myModes, ["]]"], + "Follow the link labeled 'next' or '>' if it exists", + function (count) { buffer.followDocumentRelationship("next"); }, + { count: true }); - function markToString(name, mark) - { - return name + ", " + mark.location + - ", (" + Math.round(mark.position.x * 100) + - "%, " + Math.round(mark.position.y * 100) + "%)" + - (("tab" in mark) ? ", tab: " + tabs.index(mark.tab) : ""); - } + mappings.add(myModes, ["[["], + "Follow the link labeled 'prev', 'previous' or '<' if it exists", + function (count) { buffer.followDocumentRelationship("previous"); }, + { count: true }); - function removeLocalMark(mark) - { - let localmark = localMarks.get(mark); - if (localmark) - { - let win = window.content; - for (let [i, ] in Iterator(localmark)) - { - if (localmark[i].location == win.location.href) - { - liberator.log("Deleting local mark: " + markToString(mark, localmark[i]), 5); - localmark.splice(i, 1); - if (localmark.length == 0) - localMarks.remove(mark); - break; + mappings.add(myModes, ["gf"], + "View source", + function () { buffer.viewSource(null, false); }); + + mappings.add(myModes, ["gF"], + "View source with an external editor", + function () { buffer.viewSource(null, true); }); + + mappings.add(myModes, ["|"], + "Toggle between rendered and source view", + function () { buffer.viewSource(null, false); }); + + mappings.add(myModes, ["gi"], + "Focus last used input field", + function (count) { + if (count < 1 && buffer.lastInputField) + buffer.focusElement(buffer.lastInputField); + else { + let xpath = ["input[not(@type) or @type='text' or @type='password' or @type='file']", + "textarea[not(@disabled) and not(@readonly)]"]; + + let elements = [m for (m in util.evaluateXPath(xpath))].filter(function (match) { + let computedStyle = util.computedStyle(match); + return computedStyle.visibility != "hidden" && computedStyle.display != "none"; + }); + + if (elements.length > 0) + buffer.focusElement(elements[util.Math.constrain(count, 1, elements.length) - 1]); + else + liberator.beep(); } - } - } - } + }, + { count: true }); - function removeURLMark(mark) - { - let urlmark = urlMarks.get(mark); - if (urlmark) - { - liberator.log("Deleting URL mark: " + markToString(mark, urlmark), 5); - urlMarks.remove(mark); - } - } + mappings.add(myModes, ["gP"], + "Open (put) a URL based on the current clipboard contents in a new buffer", + function () { + liberator.open(util.readFromClipboard(), + liberator[options.get("activate").has("paste") ? "NEW_BACKGROUND_TAB" : "NEW_TAB"]); + }); - function isLocalMark(mark) /^['`a-z]$/.test(mark); - function isURLMark(mark) /^[A-Z0-9]$/.test(mark); + mappings.add(myModes, ["p", ""], + "Open (put) a URL based on the current clipboard contents in the current buffer", + function () { + let url = util.readFromClipboard(); + if (url) + liberator.open(url); + else + liberator.beep(); + }); - function localMarkIter() - { - for (let [mark, value] in localMarks) - for (let [, val] in Iterator(value)) - yield [mark, val]; - } + mappings.add(myModes, ["P"], + "Open (put) a URL based on the current clipboard contents in a new buffer", + function () { + let url = util.readFromClipboard(); + if (url) + liberator.open(url, { from: "activate", where: liberator.NEW_TAB }); + else + liberator.beep(); + }); - function getSortedMarks() - { - // local marks - let location = window.content.location.href; - let lmarks = [i for (i in localMarkIter()) if (i[1].location == location)]; - lmarks.sort(); + // reloading + mappings.add(myModes, ["r"], + "Reload the current web page", + function () { tabs.reload(getBrowser().mCurrentTab, false); }); - // URL marks - // FIXME: why does umarks.sort() cause a "Component is not available = - // NS_ERROR_NOT_AVAILABLE" exception when used here? - let umarks = [i for (i in urlMarks)]; - umarks.sort(function (a, b) a[0].localeCompare(b[0])); + mappings.add(myModes, ["R"], + "Reload while skipping the cache", + function () { tabs.reload(getBrowser().mCurrentTab, true); }); - return lmarks.concat(umarks); - } + // yanking + mappings.add(myModes, ["Y"], + "Copy selected text or current word", + function () { + let sel = buffer.getCurrentWord(); - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + if (sel) + util.copyToClipboard(sel, true); + else + liberator.beep(); + }); - var myModes = config.browserModes; + // zooming + mappings.add(myModes, ["zi", "+"], + "Enlarge text zoom of current web page", + function (count) { buffer.zoomIn(Math.max(count, 1), false); }, + { count: true }); - mappings.add(myModes, - ["m"], "Set mark at the cursor position", - function (arg) - { - if (/[^a-zA-Z]/.test(arg)) - return void liberator.beep(); + mappings.add(myModes, ["zm"], + "Enlarge text zoom of current web page by a larger amount", + function (count) { buffer.zoomIn(Math.max(count, 1) * 3, false); }, + { count: true }); - marks.add(arg); - }, - { arg: true }); + mappings.add(myModes, ["zo", "-"], + "Reduce text zoom of current web page", + function (count) { buffer.zoomOut(Math.max(count, 1), false); }, + { count: true }); - mappings.add(myModes, - ["'", "`"], "Jump to the mark in the current buffer", - function (arg) { marks.jumpTo(arg); }, - { arg: true }); + mappings.add(myModes, ["zr"], + "Reduce text zoom of current web page by a larger amount", + function (count) { buffer.zoomOut(Math.max(count, 1) * 3, false); }, + { count: true }); - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + mappings.add(myModes, ["zz"], + "Set text zoom value of current web page", + function (count) { buffer.textZoom = count > 1 ? count : 100; }, + { count: true }); - commands.add(["delm[arks]"], - "Delete the specified marks", - function (args) - { - let special = args.bang; - args = args.string; + mappings.add(myModes, ["zI"], + "Enlarge full zoom of current web page", + function (count) { buffer.zoomIn(Math.max(count, 1), true); }, + { count: true }); - // assert(special ^ args) - liberator.assert( special || args, "E471: Argument required"); - liberator.assert(!special || !args, "E474: Invalid argument"); + mappings.add(myModes, ["zM"], + "Enlarge full zoom of current web page by a larger amount", + function (count) { buffer.zoomIn(Math.max(count, 1) * 3, true); }, + { count: true }); - let matches; - if (matches = args.match(/(?:(?:^|[^a-zA-Z0-9])-|-(?:$|[^a-zA-Z0-9])|[^a-zA-Z0-9 -]).*/)) + mappings.add(myModes, ["zO"], + "Reduce full zoom of current web page", + function (count) { buffer.zoomOut(Math.max(count, 1), true); }, + { count: true }); + + mappings.add(myModes, ["zR"], + "Reduce full zoom of current web page by a larger amount", + function (count) { buffer.zoomOut(Math.max(count, 1) * 3, true); }, + { count: true }); + + mappings.add(myModes, ["zZ"], + "Set full zoom value of current web page", + function (count) { buffer.fullZoom = count > 1 ? count : 100; }, + { count: true }); + + // page info + mappings.add(myModes, [""], + "Print the current file name", + function (count) { buffer.showPageInfo(false); }, + { count: true }); + + mappings.add(myModes, ["g"], + "Print file information", + function () { buffer.showPageInfo(true); }); + }, + options: function () { + options.add(["nextpattern"], // \u00BB is » (>> in a single char) + "Patterns to use when guessing the 'next' page in a document sequence", + "stringlist", "\\bnext\\b,^>$,^(>>|\u00BB)$,^(>|\u00BB),(>|\u00BB)$,\\bmore\\b"); + + options.add(["previouspattern"], // \u00AB is « (<< in a single char) + "Patterns to use when guessing the 'previous' page in a document sequence", + "stringlist", "\\bprev|previous\\b,^<$,^(<<|\u00AB)$,^(<|\u00AB),(<|\u00AB)$"); + + options.add(["pageinfo", "pa"], + "Desired info in the :pageinfo output", + "charlist", "gfm", { - // NOTE: this currently differs from Vim's behavior which - // deletes any valid marks in the arg list, up to the first - // invalid arg, as well as giving the error message. - liberator.echoerr("E475: Invalid argument: " + matches[0]); - return; - } - // check for illegal ranges - only allow a-z A-Z 0-9 - if (matches = args.match(/[a-zA-Z0-9]-[a-zA-Z0-9]/g)) + completer: function (context) [[k, v[1]] for ([k, v] in Iterator(this.pageInfo))], + validator: Option.validateCompleter + }); + + options.add(["scroll", "scr"], + "Number of lines to scroll with and commands", + "number", 0, + { validator: function (value) value >= 0 }); + + options.add(["showstatuslinks", "ssli"], + "Show the destination of the link under the cursor in the status bar", + "number", 1, { - for (let i = 0; i < matches.length; i++) - { - let start = matches[i][0]; - let end = matches[i][2]; - if (/[a-z]/.test(start) != /[a-z]/.test(end) || - /[A-Z]/.test(start) != /[A-Z]/.test(end) || - /[0-9]/.test(start) != /[0-9]/.test(end) || - start > end) - { - liberator.echoerr("E475: Invalid argument: " + args.match(matches[i] + ".*")[0]); - return; - } - } - } + completer: function (context) [ + ["0", "Don't show link destination"], + ["1", "Show the link in the status line"], + ["2", "Show the link in the command line"] + ], + validator: Option.validateCompleter + }); - marks.remove(args, special); - }, - { - bang: true, - completer: function (context) completion.mark(context) - }); - - commands.add(["ma[rk]"], - "Mark current location within the web page", - function (args) - { - let mark = args[0]; - - liberator.assert(mark.length <= 1, "E488: Trailing characters"); - liberator.assert(/[a-zA-Z]/.test(mark), - "E191: Argument must be a letter or forward/backward quote"); - - marks.add(mark); - }, - { argCount: "1" }); - - commands.add(["marks"], - "Show all location marks of current web page", - function (args) - { - args = args.string; - - // ignore invalid mark characters unless there are no valid mark chars - liberator.assert(!args || /[a-zA-Z]/.test(args), - "E283: No marks matching " + args.quote()); - - let filter = args.replace(/[^a-zA-Z]/g, ""); - marks.list(filter); - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMPLETIONS ///////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - completion.mark = function mark(context) { - function percent(i) Math.round(i * 100); - - // FIXME: Line/Column doesn't make sense with % - context.title = ["Mark", "Line Column File"]; - context.keys.description = function ([, m]) percent(m.position.y) + "% " + percent(m.position.x) + "% " + m.location; - context.completions = marks.all; - }; - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - - /** - * @property {Array} Returns all marks, both local and URL, in a sorted - * array. - */ - get all() getSortedMarks(), - - /** - * Add a named mark for the current buffer, at its current position. - * If mark matches [A-Z], it's considered a URL mark, and will jump to - * the same position at the same URL no matter what buffer it's - * selected from. If it matches [a-z'"], it's a local mark, and can - * only be recalled from a buffer with a matching URL. - * - * @param {string} mark The mark name. - * @param {boolean} silent Whether to output error messages. - */ - // TODO: add support for frameset pages - add: function (mark, silent) - { - let win = window.content; - let doc = win.document; - - if (!doc.body) - return; - if (doc.body instanceof HTMLFrameSetElement) + options.add(["usermode", "um"], + "Show current website with a minimal style sheet to make it easily accessible", + "boolean", false, { - if (!silent) - liberator.echoerr("Marks support for frameset pages not implemented yet"); - return; - } - - let x = win.scrollMaxX ? win.pageXOffset / win.scrollMaxX : 0; - let y = win.scrollMaxY ? win.pageYOffset / win.scrollMaxY : 0; - let position = { x: x, y: y }; - - if (isURLMark(mark)) - { - urlMarks.set(mark, { location: win.location.href, position: position, tab: tabs.getTab() }); - if (!silent) - liberator.log("Adding URL mark: " + markToString(mark, urlMarks.get(mark)), 5); - } - else if (isLocalMark(mark)) - { - // remove any previous mark of the same name for this location - removeLocalMark(mark); - if (!localMarks.get(mark)) - localMarks.set(mark, []); - let vals = { location: win.location.href, position: position }; - localMarks.get(mark).push(vals); - if (!silent) - liberator.log("Adding local mark: " + markToString(mark, vals), 5); - } - }, - - /** - * Remove all marks matching filter. If special is - * given, removes all local marks. - * - * @param {string} filter A string containing one character for each - * mark to be removed. - * @param {boolean} special Whether to delete all local marks. - */ - // FIXME: Shouldn't special be replaced with a null filter? - remove: function (filter, special) - { - if (special) - { - // :delmarks! only deletes a-z marks - for (let [mark, ] in localMarks) - removeLocalMark(mark); - } - else - { - for (let [mark, ] in urlMarks) - { - if (filter.indexOf(mark) >= 0) - removeURLMark(mark); - } - for (let [mark, ] in localMarks) - { - if (filter.indexOf(mark) >= 0) - removeLocalMark(mark); - } - } - }, - - /** - * Jumps to the named mark. See {@link #add} - * - * @param {string} mark The mark to jump to. - */ - jumpTo: function (mark) - { - let ok = false; - - if (isURLMark(mark)) - { - let slice = urlMarks.get(mark); - if (slice && slice.tab && slice.tab.linkedBrowser) - { - if (slice.tab.parentNode != getBrowser().tabContainer) - { - pendingJumps.push(slice); - // NOTE: this obviously won't work on generated pages using - // non-unique URLs :( - liberator.open(slice.location, liberator.NEW_TAB); - return; - } - let index = tabs.index(slice.tab); - if (index != -1) - { - tabs.select(index); - let win = slice.tab.linkedBrowser.contentWindow; - if (win.location.href != slice.location) - { - pendingJumps.push(slice); - win.location.href = slice.location; - return; - } - liberator.log("Jumping to URL mark: " + markToString(mark, slice), 5); - buffer.scrollToPercent(slice.position.x * 100, slice.position.y * 100); - ok = true; - } - } - } - else if (isLocalMark(mark)) - { - let win = window.content; - let slice = localMarks.get(mark) || []; - - for (let [, lmark] in Iterator(slice)) - { - if (win.location.href == lmark.location) - { - liberator.log("Jumping to local mark: " + markToString(mark, lmark), 5); - buffer.scrollToPercent(lmark.position.x * 100, lmark.position.y * 100); - ok = true; - break; - } - } - } - - if (!ok) - liberator.echoerr("E20: Mark not set"); - }, - - /** - * List all marks matching filter. - * - * @param {string} filter - */ - list: function (filter) - { - let marks = getSortedMarks(); - - liberator.assert(marks.length > 0, "No marks set"); - - if (filter.length > 0) - { - marks = marks.filter(function (mark) filter.indexOf(mark[0]) >= 0); - liberator.assert(marks.length > 0, "E283: No marks matching " + filter.quote()); - } - - let list = template.tabular( - ["Mark", "Line", "Column", "File"], - ["", "text-align: right", "text-align: right", "color: green"], - ([mark[0], - Math.round(mark[1].position.x * 100) + "%", - Math.round(mark[1].position.y * 100) + "%", - mark[1].location] - for ([, mark] in Iterator(marks)))); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } - - }; - //}}} -} //}}} + setter: function (value) getBrowser().markupDocumentViewer.authorStyleDisabled = value, + getter: function () getBrowser().markupDocumentViewer.authorStyleDisabled + }); + }, +}); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/commandline.js b/common/content/commandline.js new file mode 100644 index 00000000..6a66f63d --- /dev/null +++ b/common/content/commandline.js @@ -0,0 +1,1867 @@ +// Copyright (c) 2006-2009 by Martin Stubenschrott +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. + +/** @scope modules */ + +/** + * This class is used for prompting of user input and echoing of messages. + * + * It consists of a prompt and command field be sure to only create objects of + * this class when the chrome is ready. + */ +const CommandLine = Module("commandline", { + requires: ["liberator", "modes", "services", "storage", "template", "util"], + + init: function () { + const self = this; + + this._callbacks = {}; + + storage.newArray("history-search", { store: true, privateData: true }); + storage.newArray("history-command", { store: true, privateData: true }); + + // Really inideal. + let services = modules.services; // Storage objects are global to all windows, 'modules' isn't. + storage.newObject("sanitize", function () { + ({ + CLEAR: "browser:purge-session-history", + QUIT: "quit-application", + init: function () { + services.get("observer").addObserver(this, this.CLEAR, false); + services.get("observer").addObserver(this, this.QUIT, false); + }, + observe: function (subject, topic, data) { + switch (topic) { + case this.CLEAR: + ["search", "command"].forEach(function (mode) { + CommandLine.History(null, mode).sanitize(); + }); + break; + case this.QUIT: + services.get("observer").removeObserver(this, this.CLEAR); + services.get("observer").removeObserver(this, this.QUIT); + break; + } + } + }).init(); + }, { store: false }); + storage.addObserver("sanitize", + function (key, event, value) { + autocommands.trigger("Sanitize", {}); + }, window); + + this._messageHistory = { //{{{ + _messages: [], + get messages() { + let max = options["messages"]; + + // resize if 'messages' has changed + if (this._messages.length > max) + this._messages = this._messages.splice(this._messages.length - max); + + return this._messages; + }, + + get length() this._messages.length, + + clear: function clear() { + this._messages = []; + }, + + add: function add(message) { + if (!message) + return; + + if (this._messages.length >= options["messages"]) + this._messages.shift(); + + this._messages.push(message); + } + }; //}}} + + this._lastMowOutput = null; + + this._silent = false; + this._quiet = false; + this._keepCommand = false; + this._lastEcho = null; + + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// TIMERS ////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + this._statusTimer = new Timer(5, 100, function statusTell() { + if (self._completions == null) + return; + if (self._completions.selected == null) + statusline.updateProgress(""); + else + statusline.updateProgress("match " + (self._completions.selected + 1) + " of " + self._completions.items.length); + }); + + this._autocompleteTimer = new Timer(200, 500, function autocompleteTell(tabPressed) { + if (!events.feedingKeys && self._completions && options.get("wildoptions").has("auto")) { + self._completions.complete(true, false); + self._completions.itemList.show(); + } + }); + + // This timer just prevents s from queueing up when the + // system is under load (and, thus, giving us several minutes of + // the completion list scrolling). Multiple presses are + // still processed normally, as the time is flushed on "keyup". + this._tabTimer = new Timer(0, 0, function tabTell(event) { + if (self._completions) + self._completions.tab(event.shiftKey); + }); + + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// VARIABLES /////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + this._completionList = new ItemList("liberator-completions"); + this._completions = null; + this._history = null; + + this._startHints = false; // whether we're waiting to start hints mode + this._lastSubstring = ""; + + // the containing box for the this._promptWidget and this._commandWidget + this._commandlineWidget = document.getElementById("liberator-commandline"); + // the prompt for the current command, for example : or /. Can be blank + this._promptWidget = document.getElementById("liberator-commandline-prompt"); + // the command bar which contains the current command + this._commandWidget = document.getElementById("liberator-commandline-command"); + + this._messageBox = document.getElementById("liberator-message"); + + this._commandWidget.inputField.QueryInterface(Ci.nsIDOMNSEditableElement); + this._messageBox.inputField.QueryInterface(Ci.nsIDOMNSEditableElement); + + // the widget used for multiline output + this._multilineOutputWidget = document.getElementById("liberator-multiline-output"); + this._outputContainer = this._multilineOutputWidget.parentNode; + + this._multilineOutputWidget.contentDocument.body.id = "liberator-multiline-output-content"; + + // the widget used for multiline intput + this._multilineInputWidget = document.getElementById("liberator-multiline-input"); + + // we need to save the mode which were in before opening the command line + // this is then used if we focus the command line again without the "official" + // way of calling "open" + this._currentExtendedMode = null; // the extended mode which we last openend the command line for + this._currentPrompt = null; + this._currentCommand = null; + + // save the arguments for the inputMultiline method which are needed in the event handler + this._multilineRegexp = null; + this._multilineCallback = null; + + + this._input = {}; + + this.registerCallback("submit", modes.EX, function (command) { + commands.repeat = command; + liberator.execute(command); + }); + this.registerCallback("complete", modes.EX, function (context) { + context.fork("ex", 0, completion, "ex"); + }); + this.registerCallback("change", modes.EX, function (command) { + self._autocompleteTimer.tell(false); + }); + + this.registerCallback("cancel", modes.PROMPT, cancelPrompt); + this.registerCallback("submit", modes.PROMPT, closePrompt); + this.registerCallback("change", modes.PROMPT, function (str) { + if (self._input.complete) + self._autocompleteTimer.tell(false); + if (self._input.change) + return self._input.change.call(commandline, str); + }); + this.registerCallback("complete", modes.PROMPT, function (context) { + if (self._input.complete) + context.fork("input", 0, commandline, input.complete); + }); + + function cancelPrompt(value) { + let callback = self._input.cancel; + self._input = {}; + if (callback) + callback.call(self, value != null ? value : commandline.command); + } + + function closePrompt(value) { + let callback = self._input.submit; + self._input = {}; + if (callback) + callback.call(self, value != null ? value : commandline.command); + } + }, + + /** + * Highlight the messageBox according to group. + */ + _setHighlightGroup: function (group) { + this._messageBox.setAttributeNS(NS.uri, "highlight", group); + }, + + /** + * Determines whether the command line should be visible. + * + * @returns {boolean} + */ + _commandShown: function () modes.main == modes.COMMAND_LINE && + !(modes.extended & (modes.INPUT_MULTILINE | modes.OUTPUT_MULTILINE)), + + /** + * Set the command-line prompt. + * + * @param {string} val + * @param {string} highlightGroup + */ + _setPrompt: function (val, highlightGroup) { + this._promptWidget.value = val; + this._promptWidget.size = val.length; + this._promptWidget.collapsed = (val == ""); + this._promptWidget.setAttributeNS(NS.uri, "highlight", highlightGroup || commandline.HL_NORMAL); + }, + + /** + * Set the command-line input value. The caret is reset to the + * end of the line. + * + * @param {string} cmd + */ + _setCommand: function (cmd) { + this._commandWidget.value = cmd; + this._commandWidget.selectionStart = cmd.length; + this._commandWidget.selectionEnd = cmd.length; + }, + + /** + * Display a message in the command-line area. + * + * @param {string} str + * @param {string} highlightGroup + * @param {boolean} forceSingle If provided, don't let over-long + * messages move to the MOW. + */ + _echoLine: function (str, highlightGroup, forceSingle) { + this._setHighlightGroup(highlightGroup); + this._messageBox.value = str; + + liberator.triggerObserver("echoLine", str, highlightGroup, forceSingle); + + if (!this._commandShown()) + commandline.hide(); + + let field = this._messageBox.inputField; + if (!forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth) + this._echoMultiline({str}, highlightGroup); + }, + + /** + * Display a multiline message. + * + * @param {string} str + * @param {string} highlightGroup + */ + // TODO: resize upon a window resize + _echoMultiline: function (str, highlightGroup) { + let doc = this._multilineOutputWidget.contentDocument; + let win = this._multilineOutputWidget.contentWindow; + + liberator.triggerObserver("echoMultiline", str, highlightGroup); + + // If it's already XML, assume it knows what it's doing. + // Otherwise, white space is significant. + // The problem elsewhere is that E4X tends to insert new lines + // after interpolated data. + XML.ignoreWhitespace = typeof str != "xml"; + this._lastMowOutput =
{template.maybeXML(str)}
; + let output = util.xmlToDom(this._lastMowOutput, doc); + XML.ignoreWhitespace = true; + + // FIXME: need to make sure an open MOW is closed when commands + // that don't generate output are executed + if (this._outputContainer.collapsed) + doc.body.innerHTML = ""; + + doc.body.appendChild(output); + + commandline.updateOutputHeight(true); + + if (options["more"] && win.scrollMaxY > 0) { + // start the last executed command's output at the top of the screen + let elements = doc.getElementsByClassName("ex-command-output"); + elements[elements.length - 1].scrollIntoView(true); + } + else + win.scrollTo(0, doc.height); + + win.focus(); + + this._startHints = false; + modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE); + commandline.updateMorePrompt(); + }, + + /** + * Ensure that the multiline input widget is the correct size. + */ + _autosizeMultilineInputWidget: function () { + let lines = this._multilineInputWidget.value.split("\n").length - 1; + + this._multilineInputWidget.setAttribute("rows", Math.max(lines, 1)); + }, + + + + HL_NORMAL: "Normal", + HL_ERRORMSG: "ErrorMsg", + HL_MODEMSG: "ModeMsg", + HL_MOREMSG: "MoreMsg", + HL_QUESTION: "Question", + HL_INFOMSG: "InfoMsg", + HL_WARNINGMSG: "WarningMsg", + HL_LINENR: "LineNr", + + FORCE_MULTILINE : 1 << 0, + FORCE_SINGLELINE : 1 << 1, + DISALLOW_MULTILINE : 1 << 2, // if an echo() should try to use the single line + // but output nothing when the MOW is open; when also + // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence + APPEND_TO_MESSAGES : 1 << 3, // add the string to the message this._history + + get completionContext() this._completions.context, + + get mode() (modes.extended == modes.EX) ? "cmd" : "search", + + get silent() this._silent, + set silent(val) { + this._silent = val; + this._quiet = this._quiet; + }, + get quiet() this._quiet, + set quiet(val) { + this._quiet = val; + Array.forEach(document.getElementById("liberator-commandline").childNodes, function (node) { + node.style.opacity = this._quiet || this._silent ? "0" : ""; + }); + }, + + // @param type can be: + // "submit": when the user pressed enter in the command line + // "change" + // "cancel" + // "complete" + registerCallback: function (type, mode, func) { + if (!(type in this._callbacks)) + this._callbacks[type] = {}; + this._callbacks[type][mode] = func; + }, + + triggerCallback: function (type, mode, data) { + if (this._callbacks[type] && this._callbacks[type][mode]) + this._callbacks[type][mode].call(this, data); + }, + + runSilently: function (func, self) { + let wasSilent = this._silent; + this._silent = true; + try { + func.call(self); + } + finally { + this._silent = wasSilent; + } + }, + + get command() { + try { + // The long path is because of complications with the + // completion preview. + return this._commandWidget.inputField.editor.rootElement.firstChild.textContent; + } + catch (e) { + return this._commandWidget.value; + } + }, + set command(cmd) this._commandWidget.value = cmd, + + get message() this._messageBox.value, + + /** + * Open the command line. The main mode is set to + * COMMAND_LINE, the extended mode to extendedMode. + * Further, callbacks defined for extendedMode are + * triggered as appropriate (see {@link #registerCallback}). + * + * @param {string} prompt + * @param {string} cmd + * @param {number} extendedMode + */ + open: function open(prompt, cmd, extendedMode) { + // save the current prompts, we need it later if the command widget + // receives focus without calling the this.open() method + this._currentPrompt = prompt || ""; + this._currentCommand = cmd || ""; + this._currentExtendedMode = extendedMode || null; + this._keepCommand = false; + + this._setPrompt(this._currentPrompt); + this._setCommand(this._currentCommand); + this._commandlineWidget.collapsed = false; + + modes.set(modes.COMMAND_LINE, this._currentExtendedMode); + + this._commandWidget.focus(); + + this._history = CommandLine.History(this._commandWidget.inputField, (modes.extended == modes.EX) ? "command" : "search"); + this._completions = CommandLine.Completions(this._commandWidget.inputField); + + // open the completion list automatically if wanted + if (cmd.length) + commandline.triggerCallback("change", this._currentExtendedMode, cmd); + }, + + /** + * Closes the command line. This is ordinarily triggered automatically + * by a mode change. Will not hide the command line immediately if + * called directly after a successful command, otherwise it will. + */ + close: function close() { + let mode = this._currentExtendedMode; + this._currentExtendedMode = null; + commandline.triggerCallback("cancel", mode); + + if (this._history) + this._history.save(); + + this.resetCompletions(); // cancels any asynchronous completion still going on, must be before we set completions = null + this._completions = null; + this._history = null; + + statusline.updateProgress(""); // we may have a "match x of y" visible + liberator.focusContent(false); + + this._multilineInputWidget.collapsed = true; + this._completionList.hide(); + + if (!this._keepCommand || this._silent || this._quiet) { + this._outputContainer.collapsed = true; + commandline.updateMorePrompt(); + this.hide(); + } + if (!this._outputContainer.collapsed) { + modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE); + commandline.updateMorePrompt(); + } + this._keepCommand = false; + }, + + /** + * Hides the command line, and shows any status messages that + * are under it. + */ + hide: function hide() { + this._commandlineWidget.collapsed = true; + }, + + /** + * Output the given string onto the command line. With no flags, the + * message will be shown in the status line if it's short enough to + * fit, and contains no new lines, and isn't XML. Otherwise, it will be + * shown in the MOW. + * + * @param {string} str + * @param {string} highlightGroup The Highlight group for the + * message. + * @default "Normal" + * @param {number} flags Changes the behavior as follows: + * commandline.APPEND_TO_MESSAGES - Causes message to be added to the + * messages this._history, and shown by :messages. + * commandline.FORCE_SINGLELINE - Forbids the command from being + * pushed to the MOW if it's too long or of there are already + * status messages being shown. + * commandline.DISALLOW_MULTILINE - Cancels the operation if the MOW + * is already visible. + * commandline.FORCE_MULTILINE - Forces the message to appear in + * the MOW. + */ + echo: function echo(str, highlightGroup, flags) { + // liberator.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst + if (this._silent) + return; + + highlightGroup = highlightGroup || this.HL_NORMAL; + + if (flags & this.APPEND_TO_MESSAGES) + this._messageHistory.add({ str: str, highlight: highlightGroup }); + + // The DOM isn't threadsafe. It must only be accessed from the main thread. + liberator.callInMainThread(function () { + if ((flags & this.DISALLOW_MULTILINE) && !this._outputContainer.collapsed) + return; + + let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE); + let action = this._echoLine; + + // TODO: this is all a bit convoluted - clean up. + // assume that FORCE_MULTILINE output is fully styled + if (!(flags & this.FORCE_MULTILINE) && !single && (!this._outputContainer.collapsed || this._messageBox.value == this._lastEcho)) { + highlightGroup += " Message"; + action = this._echoMultiline; + } + + if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE)) + action = this._echoMultiline; + + if (single) + this._lastEcho = null; + else { + if (this._messageBox.value == this._lastEcho) + this._echoMultiline({this._lastEcho}, + this._messageBox.getAttributeNS(NS.uri, "highlight")); + this._lastEcho = (action == this._echoLine) && str; + } + + if (action) + action.call(this, str, highlightGroup, single); + }, this); + }, + + /** + * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may + * pop at any time to close the prompt. + * + * @param {string} prompt The input prompt to use. + * @param {function(string)} callback + * @param {Object} extra + * @... {function} onChange - A function to be called with the current + * input every time it changes. + * @... {function(CompletionContext)} completer - A completion function + * for the user's input. + * @... {string} promptHighlight - The HighlightGroup used for the + * prompt. @default "Question" + * @... {string} default - The initial value that will be returned + * if the user presses straightaway. @default "" + */ + input: function _input(prompt, callback, extra) { + extra = extra || {}; + + this._input = { + submit: callback, + change: extra.onChange, + complete: extra.completer, + cancel: extra.onCancel + }; + + modes.push(modes.COMMAND_LINE, modes.PROMPT); + this._currentExtendedMode = modes.PROMPT; + + this._setPrompt(prompt, extra.promptHighlight || this.HL_QUESTION); + this._setCommand(extra.default || ""); + this._commandlineWidget.collapsed = false; + this._commandWidget.focus(); + + this._completions = CommandLine.Completions(this._commandWidget.inputField); + }, + + /** + * Get a multiline input from a user, up to but not including the line + * which matches the given regular expression. Then execute the + * callback with that string as a parameter. + * + * @param {RegExp} untilRegexp + * @param {function(string)} callbackFunc + */ + // FIXME: Buggy, especially when pasting. Shouldn't use a RegExp. + inputMultiline: function inputMultiline(untilRegexp, callbackFunc) { + // Kludge. + let cmd = !this._commandWidget.collapsed && this.command; + modes.push(modes.COMMAND_LINE, modes.INPUT_MULTILINE); + if (cmd != false) + this._echoLine(cmd, this.HL_NORMAL); + + // save the arguments, they are needed in the event handler onEvent + this._multilineRegexp = untilRegexp; + this._multilineCallback = callbackFunc; + + this._multilineInputWidget.collapsed = false; + this._multilineInputWidget.value = ""; + this._autosizeMultilineInputWidget(); + + setTimeout(function () { this._multilineInputWidget.focus(); }, 10); + }, + + /** + * Handles all command-line events. All key events are passed here when + * COMMAND_LINE mode is active, as well as all input, keyup, focus, and + * blur events sent to the command-line XUL element. + * + * @param {Event} event + * @private + */ + onEvent: function onEvent(event) { + const self = this; + let command = this.command; + + if (event.type == "blur") { + // prevent losing focus, there should be a better way, but it just didn't work otherwise + setTimeout(function () { + if (self._commandShown() && event.originalTarget == self._commandWidget.inputField) + self._commandWidget.inputField.focus(); + }, 0); + } + else if (event.type == "focus") { + if (!self._commandShown() && event.target == self._commandWidget.inputField) { + event.target.blur(); + liberator.beep(); + } + } + else if (event.type == "input") { + this.resetCompletions(); + commandline.triggerCallback("change", this._currentExtendedMode, command); + } + else if (event.type == "keypress") { + let key = events.toString(event); + if (this._completions) + this._completions.previewClear(); + if (!this._currentExtendedMode) + return; + + // user pressed to carry out a command + // user pressing is handled in the global onEscape + // FIXME: should trigger "cancel" event + if (events.isAcceptKey(key)) { + let mode = this._currentExtendedMode; // save it here, as modes.pop() resets it + this._keepCommand = !userContext.hidden_option_no_command_afterimage; + this._currentExtendedMode = null; // Don't let modes.pop trigger "cancel" + modes.pop(!this._silent); + commandline.triggerCallback("submit", mode, command); + } + // user pressed or arrow to cycle this._history completion + else if (/^(|||||)$/.test(key)) { + // prevent tab from moving to the next field + event.preventDefault(); + event.stopPropagation(); + + if (this._history) + this._history.select(/Up/.test(key), !/(Page|S-)/.test(key)); + else + liberator.beep(); + } + // user pressed to get completions of a command + else if (key == "" || key == "") { + // prevent tab from moving to the next field + event.preventDefault(); + event.stopPropagation(); + + this._tabTimer.tell(event); + } + else if (key == "") { + // reset the tab completion + //this.resetCompletions(); + + // and blur the command line if there is no text left + if (command.length == 0) { + commandline.triggerCallback("cancel", this._currentExtendedMode); + modes.pop(); + } + } + else { // any other key + //this.resetCompletions(); + } + // allow this event to be handled by the host app + } + else if (event.type == "keyup") { + let key = events.toString(event); + if (key == "" || key == "") + this._tabTimer.flush(); + } + }, + + /** + * Multiline input events, they will come straight from + * #liberator-multiline-input in the XUL. + * + * @param {Event} event + */ + onMultilineInputEvent: function onMultilineInputEvent(event) { + if (event.type == "keypress") { + let key = events.toString(event); + if (events.isAcceptKey(key)) { + let text = this._multilineInputWidget.value.substr(0, this._multilineInputWidget.selectionStart); + if (text.match(this._multilineRegexp)) { + text = text.replace(this._multilineRegexp, ""); + modes.pop(); + this._multilineInputWidget.collapsed = true; + this._multilineCallback.call(this, text); + } + } + else if (events.isCancelKey(key)) { + modes.pop(); + this._multilineInputWidget.collapsed = true; + } + } + else if (event.type == "blur") { + if (modes.extended & modes.INPUT_MULTILINE) + setTimeout(function () { this._multilineInputWidget.inputField.focus(); }, 0); + } + else if (event.type == "input") + this._autosizeMultilineInputWidget(); + return true; + }, + + /** + * Handle events when we are in multiline output mode, these come from + * liberator when modes.extended & modes.MULTILINE_OUTPUT and also from + * #liberator-multiline-output in the XUL. + * + * @param {Event} event + */ + // FIXME: if 'more' is set and the MOW is not scrollable we should still + // allow a down motion after an up rather than closing + onMultilineOutputEvent: function onMultilineOutputEvent(event) { + let win = this._multilineOutputWidget.contentWindow; + + let showMoreHelpPrompt = false; + let showMorePrompt = false; + let closeWindow = false; + let passEvent = false; + + let key = events.toString(event); + + // TODO: Wouldn't multiple handlers be cleaner? --djk + if (event.type == "click" && event.target instanceof HTMLAnchorElement) { + function openLink(where) { + event.preventDefault(); + // FIXME: Why is this needed? --djk + if (event.target.getAttribute("href") == "#") + liberator.open(event.target.textContent, where); + else + liberator.open(event.target.href, where); + } + + switch (key) { + case "": + if (event.originalTarget.getAttributeNS(NS.uri, "highlight") == "URL buffer-list") { + event.preventDefault(); + tabs.select(parseInt(event.originalTarget.parentNode.parentNode.firstChild.textContent, 10) - 1); + } + else + openLink(liberator.CURRENT_TAB); + break; + case "": + case "": + case "": + openLink(liberator.NEW_BACKGROUND_TAB); + break; + case "": + case "": + case "": + openLink(liberator.NEW_TAB); + break; + case "": + openLink(liberator.NEW_WINDOW); + break; + } + + return; + } + + if (this._startHints) { + statusline.updateInputBuffer(""); + this._startHints = false; + hints.show(key, undefined, win); + return; + } + + function isScrollable() !win.scrollMaxY == 0; + function atEnd() win.scrollY / win.scrollMaxY >= 1; + + switch (key) { + case "": + closeWindow = true; + break; // handled globally in events.js:onEscape() + + case ":": + commandline.open(":", "", modes.EX); + return; + + // down a line + case "j": + case "": + if (options["more"] && isScrollable()) + win.scrollByLines(1); + else + passEvent = true; + break; + + case "": + case "": + case "": + if (options["more"] && isScrollable() && !atEnd()) + win.scrollByLines(1); + else + closeWindow = true; // don't propagate the event for accept keys + break; + + // up a line + case "k": + case "": + case "": + if (options["more"] && isScrollable()) + win.scrollByLines(-1); + else if (options["more"] && !isScrollable()) + showMorePrompt = true; + else + passEvent = true; + break; + + // half page down + case "d": + if (options["more"] && isScrollable()) + win.scrollBy(0, win.innerHeight / 2); + else + passEvent = true; + break; + + // TODO: on the prompt line should scroll one page + // page down + case "f": + if (options["more"] && isScrollable()) + win.scrollByPages(1); + else + passEvent = true; + break; + + case "": + case "": + if (options["more"] && isScrollable() && !atEnd()) + win.scrollByPages(1); + else + passEvent = true; + break; + + // half page up + case "u": + // if (more and scrollable) + if (options["more"] && isScrollable()) + win.scrollBy(0, -(win.innerHeight / 2)); + else + passEvent = true; + break; + + // page up + case "b": + if (options["more"] && isScrollable()) + win.scrollByPages(-1); + else if (options["more"] && !isScrollable()) + showMorePrompt = true; + else + passEvent = true; + break; + + case "": + if (options["more"] && isScrollable()) + win.scrollByPages(-1); + else + passEvent = true; + break; + + // top of page + case "g": + if (options["more"] && isScrollable()) + win.scrollTo(0, 0); + else if (options["more"] && !isScrollable()) + showMorePrompt = true; + else + passEvent = true; + break; + + // bottom of page + case "G": + if (options["more"] && isScrollable() && !atEnd()) + win.scrollTo(0, win.scrollMaxY); + else + passEvent = true; + break; + + // copy text to clipboard + case "": + util.copyToClipboard(win.getSelection()); + break; + + // close the window + case "q": + closeWindow = true; + break; + + case ";": + statusline.updateInputBuffer(";"); + this._startHints = true; + break; + + // unmapped key + default: + if (!options["more"] || !isScrollable() || atEnd() || events.isCancelKey(key)) + passEvent = true; + else + showMoreHelpPrompt = true; + } + + if (passEvent || closeWindow) { + modes.pop(); + + if (passEvent) + events.onKeyPress(event); + } + else + commandline.updateMorePrompt(showMorePrompt, showMoreHelpPrompt); + }, + + getSpaceNeeded: function getSpaceNeeded() { + let rect = this._commandlineWidget.getBoundingClientRect(); + let offset = rect.bottom - window.innerHeight; + return Math.max(0, offset); + }, + + /** + * Update or remove the multiline output widget's "MORE" prompt. + * + * @param {boolean} force If true, "-- More --" is shown even if we're + * at the end of the output. + * @param {boolean} showHelp When true, show the valid key sequences + * and what they do. + */ + updateMorePrompt: function updateMorePrompt(force, showHelp) { + if (this._outputContainer.collapsed) + return this._echoLine("", this.HL_NORMAL); + + let win = this._multilineOutputWidget.contentWindow; + function isScrollable() !win.scrollMaxY == 0; + function atEnd() win.scrollY / win.scrollMaxY >= 1; + + if (showHelp) + this._echoLine("-- More -- SPACE/d/j: screen/page/line down, b/u/k: up, q: quit", this.HL_MOREMSG, true); + else if (force || (options["more"] && isScrollable() && !atEnd())) + this._echoLine("-- More --", this.HL_MOREMSG, true); + else + this._echoLine("Press ENTER or type command to continue", this.HL_QUESTION, true); + }, + + /** + * Changes the height of the this._multilineOutputWidget to fit in the + * available space. + * + * @param {boolean} open If true, the widget will be opened if it's not + * already so. + */ + updateOutputHeight: function updateOutputHeight(open) { + if (!open && this._outputContainer.collapsed) + return; + + let doc = this._multilineOutputWidget.contentDocument; + + availableHeight = config.outputHeight; + if (!this._outputContainer.collapsed) + availableHeight += parseFloat(this._outputContainer.height); + doc.body.style.minWidth = this._commandlineWidget.scrollWidth + "px"; + this._outputContainer.height = Math.min(doc.height, availableHeight) + "px"; + doc.body.style.minWidth = ""; + this._outputContainer.collapsed = false; + }, + + resetCompletions: function resetCompletions() { + if (this._completions) { + this._completions.context.cancelAll(); + this._completions.wildIndex = -1; + this._completions.previewClear(); + } + if (this._history) + this._history.reset(); + } +}, { + /** + * A class for managing the this._history of an inputField. + * + * @param {HTMLInputElement} inputField + * @param {string} mode The mode for which we need this._history. + */ + History: Class("History", { + init: function (inputField, mode) { + this.mode = mode; + this.input = inputField; + this.store = storage["history-" + mode]; + this.reset(); + }, + /** + * Reset the this._history index to the first entry. + */ + reset: function () { + this.index = null; + }, + /** + * Save the last entry to the permanent store. All duplicate entries + * are removed and the list is truncated, if necessary. + */ + save: function () { + if (events.feedingKeys) + return; + let str = this.input.value; + if (/^\s*$/.test(str)) + return; + this.store.mutate("filter", function (line) (line.value || line) != str); + this.store.push({ value: str, timestamp: Date.now(), privateData: this.checkPrivate(str) }); + this.store.truncate(options["history"], true); + }, + /** + * @property {function} Returns whether a data item should be + * considered private. + */ + checkPrivate: function (str) { + // Not really the ideal place for this check. + if (this.mode == "command") + return (commands.get(commands.parseCommand(str)[1]) || {}).privateData; + return false; + }, + /** + * Removes any private data from this this._history. + */ + sanitize: function (timespan) { + let range = [0, Number.MAX_VALUE]; + if (liberator.has("sanitizer") && (timespan || options["sanitizetimespan"])) + range = sanitizer.getClearRange(timespan || options["sanitizetimespan"]); + + this.store.mutate("filter", function (item) { + let timestamp = (item.timestamp || Date.now()/1000) * 1000; + return !line.privateData || timestamp < self.range[0] || timestamp > self.range[1]; + }); + }, + /** + * Replace the current input field value. + * + * @param {string} val The new value. + */ + replace: function (val) { + this.input.value = val; + commandline.triggerCallback("change", this._currentExtendedMode, val); + }, + + /** + * Move forward or backward in this._history. + * + * @param {boolean} backward Direction to move. + * @param {boolean} matchCurrent Search for matches starting + * with the current input value. + */ + select: function (backward, matchCurrent) { + // always reset the tab completion if we use up/down keys + commandline._completions.reset(); + + let diff = backward ? -1 : 1; + + if (this.index == null) { + this.original = this.input.value; + this.index = this.store.length; + } + + // search the this._history for the first item matching the current + // commandline string + while (true) { + this.index += diff; + if (this.index < 0 || this.index > this.store.length) { + this.index = util.Math.constrain(this.index, 0, this.store.length); + liberator.beep(); + // I don't know why this kludge is needed. It + // prevents the caret from moving to the end of + // the input field. + if (this.input.value == "") { + this.input.value = " "; + this.input.value = ""; + } + break; + } + + let hist = this.store.get(this.index); + // user pressed DOWN when there is no newer this._history item + if (!hist) + hist = this.original; + else + hist = (hist.value || hist); + + if (!matchCurrent || hist.substr(0, this.original.length) == this.original) { + this.replace(hist); + break; + } + } + } + }), + + /** + * A class for tab this._completions on an input field. + * + * @param {Object} input + */ + Completions: Class("Completions", { + init: function (input) { + this.context = CompletionContext(input.editor); + this.context.onUpdate = this.closure._reset; + this.editor = input.editor; + this.selected = null; + this.wildmode = options.get("wildmode"); + this.itemList = commandline._completionList; + this.itemList.setItems(this.context); + this.reset(); + }, + + UP: {}, + DOWN: {}, + PAGE_UP: {}, + PAGE_DOWN: {}, + RESET: null, + + get completion() { + let str = commandline.command; + return str.substring(this.prefix.length, str.length - this.suffix.length); + }, + set completion set_completion(completion) { + this.previewClear(); + + // Change the completion text. + // The second line is a hack to deal with some substring + // preview corner cases. + commandline._commandWidget.value = this.prefix + completion + this.suffix; + this.editor.selection.focusNode.textContent = commandline._commandWidget.value; + + // Reset the caret to one position after the completion. + this.caret = this.prefix.length + completion.length; + }, + + get caret() this.editor.selection.focusOffset, + set caret(offset) { + commandline._commandWidget.selectionStart = offset; + commandline._commandWidget.selectionEnd = offset; + }, + + get start() this.context.allItems.start, + + get items() this.context.allItems.items, + + get substring() this.context.longestAllSubstring, + + get wildtype() this.wildtypes[this.wildIndex] || "", + + get type() ({ + list: this.wildmode.checkHas(this.wildtype, "list"), + longest: this.wildmode.checkHas(this.wildtype, "longest"), + first: this.wildmode.checkHas(this.wildtype, ""), + full: this.wildmode.checkHas(this.wildtype, "full") + }), + + complete: function complete(show, tabPressed) { + this.context.reset(); + this.context.tabPressed = tabPressed; + commandline.triggerCallback("complete", commandline._currentExtendedMode, this.context); + this.context.updateAsync = true; + this.reset(show, tabPressed); + this.wildIndex = 0; + }, + + preview: function preview() { + this.previewClear(); + if (this.wildIndex < 0 || this.suffix || !this.items.length) + return; + + let substring = ""; + switch (this.wildtype.replace(/.*:/, "")) { + case "": + substring = this.items[0].text; + break; + case "longest": + if (this.items.length > 1) { + substring = this.substring; + break; + } + // Fallthrough + case "full": + let item = this.items[this.selected != null ? this.selected + 1 : 0]; + if (item) + substring = item.text; + break; + } + + // Don't show 1-character substrings unless we've just hit backspace + if (substring.length < 2 && (!this._lastSubstring || this._lastSubstring.indexOf(substring) != 0)) + return; + this._lastSubstring = substring; + + let value = this.completion; + if (util.compareIgnoreCase(value, substring.substr(0, value.length))) + return; + substring = substring.substr(value.length); + this.removeSubstring = substring; + + let node = util.xmlToDom({substring}, + document); + let start = this.caret; + this.editor.insertNode(node, this.editor.rootElement, 1); + this.caret = start; + }, + + previewClear: function previewClear() { + let node = this.editor.rootElement.firstChild; + if (node && node.nextSibling) { + try { + this.editor.deleteNode(node.nextSibling); + } + catch (e) { + node.nextSibling.textContent = ""; + } + } + else if (this.removeSubstring) { + let str = this.removeSubstring; + let cmd = commandline._commandWidget.value; + if (cmd.substr(cmd.length - str.length) == str) + commandline._commandWidget.value = cmd.substr(0, cmd.length - str.length); + } + delete this.removeSubstring; + }, + + reset: function reset(show) { + this.wildIndex = -1; + + this.prefix = this.context.value.substring(0, this.start); + this.value = this.context.value.substring(this.start, this.caret); + this.suffix = this.context.value.substring(this.caret); + + if (show) { + this.itemList.reset(); + this.selected = null; + this.wildIndex = 0; + } + + this.wildtypes = this.wildmode.values; + this.preview(); + }, + + _reset: function _reset() { + this.prefix = this.context.value.substring(0, this.start); + this.value = this.context.value.substring(this.start, this.caret); + this.suffix = this.context.value.substring(this.caret); + + this.itemList.reset(); + this.itemList.selectItem(this.selected); + + this.preview(); + }, + + select: function select(idx) { + switch (idx) { + case this.UP: + if (this.selected == null) + idx = -2; + else + idx = this.selected - 1; + break; + case this.DOWN: + if (this.selected == null) + idx = 0; + else + idx = this.selected + 1; + break; + case this.RESET: + idx = null; + break; + default: + idx = util.Math.constrain(idx, 0, this.items.length - 1); + break; + } + + if (idx == -1 || this.items.length && idx >= this.items.length || idx == null) { + // Wrapped. Start again. + this.selected = null; + this.completion = this.value; + } + else { + // Wait for contexts to complete if necessary. + // FIXME: Need to make idx relative to individual contexts. + let list = this.context.contextList; + if (idx == -2) + list = list.slice().reverse(); + let n = 0; + try { + this.waiting = true; + for (let [, context] in Iterator(list)) { + function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length); + while (context.incomplete && !done()) + liberator.threadYield(false, true); + + if (done()) + break; + + n += context.items.length; + } + } + finally { + this.waiting = false; + } + + // See previous FIXME. This will break if new items in + // a previous context come in. + if (idx < 0) + idx = this.items.length - 1; + if (this.items.length == 0) + return; + + this.selected = idx; + this.completion = this.items[idx].text; + } + + this.itemList.selectItem(idx); + }, + + tabs: [], + + tab: function tab(reverse) { + commandline._autocompleteTimer.flush(); + // Check if we need to run the completer. + if (this.context.waitingForTab || this.wildIndex == -1) + this.complete(true, true); + + this.tabs.push(reverse); + if (this.waiting) + return; + + while (this.tabs.length) { + reverse = this.tabs.shift(); + switch (this.wildtype.replace(/.*:/, "")) { + case "": + this.select(0); + break; + case "longest": + if (this.items.length > 1) { + if (this.substring && this.substring != this.completion) + this.completion = this.substring; + break; + } + // Fallthrough + case "full": + this.select(reverse ? this.UP : this.DOWN); + break; + } + + if (this.type.list) + this.itemList.show(); + + this.wildIndex = util.Math.constrain(this.wildIndex + 1, 0, this.wildtypes.length - 1); + this.preview(); + + this._statusTimer.tell(); + } + + if (this.items.length == 0) + liberator.beep(); + } + }), + + /** + * eval() a JavaScript expression and return a string suitable + * to be echoed. + * + * @param {string} arg + * @param {boolean} useColor When true, the result is a + * highlighted XML object. + */ + echoArgumentToString: function (arg, useColor) { + if (!arg) + return ""; + + try { + arg = liberator.eval(arg); + } + catch (e) { + liberator.echoerr(e); + return null; + } + + if (typeof arg === "object") + arg = util.objectToString(arg, useColor); + else if (typeof arg == "string" && /\n/.test(arg)) + arg = {arg}; + else + arg = String(arg); + + return arg; + }, +}, { + commands: function () { + [ + { + name: "ec[ho]", + description: "Echo the expression", + action: liberator.echo + }, + { + name: "echoe[rr]", + description: "Echo the expression as an error message", + action: liberator.echoerr + }, + { + name: "echom[sg]", + description: "Echo the expression as an informational message", + action: liberator.echomsg + } + ].forEach(function (command) { + commands.add([command.name], + command.description, + function (args) { + let str = CommandLine.echoArgumentToString(args.string, true); + if (str != null) + command.action(str); + }, { + completer: function (context) completion.javascript(context), + literal: 0 + }); + }); + + commands.add(["mes[sages]"], + "Display previously given messages", + function () { + // TODO: are all messages single line? Some display an aggregation + // of single line messages at least. E.g. :source + if (this._messageHistory.length == 1) { + let message = this._messageHistory.messages[0]; + commandline.echo(message.str, message.highlight, commandline.FORCE_SINGLELINE); + } + else if (this._messageHistory.length > 1) { + XML.ignoreWhitespace = false; + let list = template.map(this._messageHistory.messages, function (message) +
{message.str}
); + liberator.echo(list, commandline.FORCE_MULTILINE); + } + }, + { argCount: "0" }); + + commands.add(["messc[lear]"], + "Clear the message this._history", + function () { this._messageHistory.clear(); }, + { argCount: "0" }); + + commands.add(["sil[ent]"], + "Run a command silently", + function (args) { + commandline.runSilently(function () liberator.execute(args[0], null, true)); + }, { + completer: function (context) completion.ex(context), + literal: 0 + }); + }, + mappings: function () { + var myModes = [modes.COMMAND_LINE]; + + // TODO: move "", "" here from mappings + mappings.add(myModes, + [""], "Focus content", + function () { events.onEscape(); }); + + // Any "non-keyword" character triggers abbreviation expansion + // TODO: Add "" and "" to this list + // At the moment, adding "" breaks tab completion. Adding + // "" has no effect. + // TODO: Make non-keyword recognition smarter so that there need not + // be two lists of the same characters (one here and a regex in + // mappings.js) + mappings.add(myModes, + ["", '"', "'"], "Expand command line abbreviation", + function () { + commandline.resetCompletions(); + return editor.expandAbbreviation("c"); + }, + { route: true }); + + mappings.add(myModes, + ["", ""], "Expand command line abbreviation", + function () { editor.expandAbbreviation("c"); }); + + mappings.add([modes.NORMAL], + ["g<"], "Redisplay the last command output", + function () { + if (this._lastMowOutput) + this._echoMultiline(this._lastMowOutput, commandline.HL_NORMAL); + else + liberator.beep(); + }); + }, + options: function () { + options.add(["history", "hi"], + "Number of Ex commands and search patterns to store in the command-line this._history", + "number", 500, + { validator: function (value) value >= 0 }); + + options.add(["maxitems"], + "Maximum number of items to display at once", + "number", 20, + { validator: function (value) value >= 1 }); + + options.add(["messages", "msgs"], + "Number of messages to store in the message this._history", + "number", 100, + { validator: function (value) value >= 0 }); + + options.add(["more"], + "Pause the message list window when more than one screen of listings is displayed", + "boolean", true); + + options.add(["showmode", "smd"], + "Show the current mode in the command line", + "boolean", true); + + options.add(["suggestengines"], + "Engine Alias which has a feature of suggest", + "stringlist", "google", + { + completer: function completer(value) { + let engines = services.get("browserSearch").getEngines({}) + .filter(function (engine) engine.supportsResponseType("application/x-suggestions+json")); + + return engines.map(function (engine) [engine.alias, engine.description]); + }, + validator: Option.validateCompleter + }); + + options.add(["complete", "cpt"], + "Items which are completed at the :open prompts", + "charlist", typeof(config.defaults["complete"]) == "string" ? config.defaults["complete"] : "slf", + { + completer: function (context) array(keys(completion.urlCompleters)), + validator: Option.validateCompleter + }); + + options.add(["wildcase", "wic"], + "Completion case matching mode", + "string", "smart", + { + completer: function () [ + ["smart", "Case is significant when capital letters are typed"], + ["match", "Case is always significant"], + ["ignore", "Case is never significant"] + ], + validator: Option.validateCompleter + }); + + options.add(["wildignore", "wig"], + "List of file patterns to ignore when completing files", + "stringlist", "", + { + validator: function validator(values) { + // TODO: allow for escaping the "," + try { + RegExp("^(" + values.join("|") + ")$"); + return true; + } + catch (e) { + return false; + } + } + }); + + options.add(["wildmode", "wim"], + "Define how command line completion works", + "stringlist", "list:full", + { + completer: function (context) [ + // Why do we need ""? + ["", "Complete only the first match"], + ["full", "Complete the next full match"], + ["longest", "Complete to longest common string"], + ["list", "If more than one match, list all matches"], + ["list:full", "List all and complete first match"], + ["list:longest", "List all and complete common string"] + ], + validator: Option.validateCompleter, + checkHas: function (value, val) { + let [first, second] = value.split(":", 2); + return first == val || second == val; + } + }); + + options.add(["wildoptions", "wop"], + "Change how command line completion is done", + "stringlist", "", + { + completer: function completer(value) { + return [ + ["", "Default completion that won't show or sort the results"], + ["auto", "Automatically show this._completions while you are typing"], + ["sort", "Always sort the completion list"] + ]; + }, + validator: Option.validateCompleter + }); + }, + styles: function () { + let fontSize = util.computedStyle(document.getElementById(config.mainWindowId)).fontSize; + styles.registerSheet("chrome://liberator/skin/liberator.css"); + let error = styles.addSheet(true, "font-size", "chrome://liberator/content/buffer.xhtml", + "body { font-size: " + fontSize + "; }"); + }, +}); + + +/** + * The list which is used for the completion box (and QuickFix window in + * future). + * + * @param {string} id The id of the