diff --git a/Donators b/Donators index 9c266865..d784f962 100644 --- a/Donators +++ b/Donators @@ -2,6 +2,8 @@ Note: If you don't wish to appear on this list when making a donation, please tell me. 2008: +* Anton Kovalenko +* Paulo Tanimoto * Paul Sobey * Olivier Guéry * Dotan Cohen diff --git a/NEWS b/NEWS index 874dc23a..948ec70e 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,8 @@ * IMPORTANT: input fields are not blured anymore by default after a page has loaded use :set [no]focuscontent to control the behavior (thanks Paul Sobey for the generous donation which made this behavior possible) + * new argument parser for ex commands, should tell better error messages when + you do things like :bmark -tag=a,b instead of :bmark -tags=a,b * :bdelete accepts an optional argument now * renamed some :autocmd, mainly BrowserStartup -> Startup and BrowserExit -> Quit * don't pass any ctrl- or alt- prefixed keys to firefox in insert mode diff --git a/TODO b/TODO index 2bab8c49..0d756023 100644 --- a/TODO +++ b/TODO @@ -47,6 +47,7 @@ FEATURES: 4 y and Y could maybe changed to, but probably not: Y, yy and yl=yank location, ys=yank selection, yd=yank domain name, yt=yank title, yw=yank current word, yf=yank filename, (other things to yank?) 4 Add -nargs, -complete, etc. to :command +4 :set urlseparators=,\s+ 4 } { should jump to the next paragraph of the page (maybe impossible) 3 Splitting Windows with [:sp :vsp ctrl-w,s ctrl-w,v] and closing with [ctrl-w,q], moving with [ctrl-w,w or tab] have a look into the split browser extension diff --git a/content/addressbook.js b/content/addressbook.js index d30c2825..e8495587 100644 --- a/content/addressbook.js +++ b/content/addressbook.js @@ -112,20 +112,10 @@ liberator.Addressbook = function () //{{{ "Add an address book entry", function (args) { - var res = liberator.commands.parseArgs(args, this.args); - if (!res) - return; - - if (res.args.length == 0) - { - liberator.echoerr("E474: Invalid argument"); - return; - } - - var mailAddr = res.args[0]; // TODO: support more than one email address - var firstName = liberator.commands.getOption(res.opts, "-firstname", null); - var lastName = liberator.commands.getOption(res.opts, "-lastname", null); - var displayName = liberator.commands.getOption(res.opts, "-name", null); + var mailAddr = args.arguments[0]; // TODO: support more than one email address + var firstName = args["-firstname"] || null; + var lastName = args["-lastname"] || null; + var displayName = args["-name"] || null; if (!displayName) displayName = generateDisplayName(firstName, lastName); @@ -136,9 +126,10 @@ liberator.Addressbook = function () //{{{ }, { - args: [[["-firstname", "-f"], liberator.commands.OPTION_STRING], - [["-lastname", "-l"], liberator.commands.OPTION_STRING], - [["-name", "-n"], liberator.commands.OPTION_STRING]] + options: [[["-firstname", "-f"], liberator.commands.OPTION_STRING], + [["-lastname", "-l"], liberator.commands.OPTION_STRING], + [["-name", "-n"], liberator.commands.OPTION_STRING]], + argCount: "+" }); liberator.commands.add(["contacts", "addr[essbook]"], diff --git a/content/bookmarks.js b/content/bookmarks.js index dd31fe96..83ed5ef3 100644 --- a/content/bookmarks.js +++ b/content/bookmarks.js @@ -133,16 +133,12 @@ liberator.Bookmarks = function () //{{{ "Add a bookmark", function (args) { - var res = liberator.commands.parseArgs(args, this.args); - if (!res) - return; - - var url = res.args.length == 0 ? liberator.buffer.URL : res.args[0]; - var title = liberator.commands.getOption(res.opts, "-title", res.args.length == 0 ? liberator.buffer.title : null); + var url = args.arguments.length == 0 ? liberator.buffer.URL : args.arguments[0]; + var title = args["-title"] || (args.arguments.length == 0 ? liberator.buffer.title : null); if (!title) title = url; - var keyword = liberator.commands.getOption(res.opts, "-keyword", null); - var tags = liberator.commands.getOption(res.opts, "-tags", []); + var keyword = args["-keyword"] || null; + var tags = args["-tags"] || []; if (liberator.bookmarks.add(false, title, url, keyword, tags)) { @@ -155,25 +151,21 @@ liberator.Bookmarks = function () //{{{ liberator.echoerr("Exxx: Could not add bookmark `" + title + "'", liberator.commandline.FORCE_SINGLELINE); }, { - args: [[["-title", "-t"], liberator.commands.OPTION_STRING], - [["-tags", "-T"], liberator.commands.OPTION_LIST], - [["-keyword", "-k"], liberator.commands.OPTION_STRING, function (arg) { return /\w/.test(arg); }]] + options: [[["-title", "-t"], liberator.commands.OPTION_STRING], + [["-tags", "-T"], liberator.commands.OPTION_LIST], + [["-keyword", "-k"], liberator.commands.OPTION_STRING, function (arg) { return /\w/.test(arg); }]], + argCount: "?" }); liberator.commands.add(["bmarks"], "List or open multiple bookmarks", function (args, special) { - var res = liberator.commands.parseArgs(args, this.args); - if (!res) - return; - - var tags = liberator.commands.getOption(res.opts, "-tags", []); - liberator.bookmarks.list(res.args.join(" "), tags, special); + liberator.bookmarks.list(args.arguments.join(" "), args["-tags"] || [], special); }, { completer: function (filter) { return [0, liberator.bookmarks.get(filter)]; }, - args: [[["-tags", "-T"], liberator.commands.OPTION_LIST]] + options: [[["-tags", "-T"], liberator.commands.OPTION_LIST]] }); liberator.commands.add(["delbm[arks]"], diff --git a/content/commands.js b/content/commands.js index ac7fca1f..2eb29d6e 100644 --- a/content/commands.js +++ b/content/commands.js @@ -75,7 +75,8 @@ liberator.Command = function (specs, description, action, extraInfo) //{{{ this.description = description || ""; this.action = action; this.completer = extraInfo.completer || null; - this.args = extraInfo.args || []; + this.options = extraInfo.options || []; + this.argCount = extraInfo.argCount || ""; this.isUserCommand = extraInfo.isUserCommand || false; }; @@ -83,6 +84,15 @@ liberator.Command.prototype = { execute: function (args, special, count, modifiers) { + // whenever the user specifies special options or fixed number of arguments + // we use our args parser instead of passing a string to the callback + if (this.options.length > 0 || this.argCount) + { + args = liberator.commands.parseArgs(args, this.options, this.argCount); + if (args == null) + return false; + } + return this.action.call(this, args, special, count, modifiers); }, @@ -128,292 +138,6 @@ liberator.Commands = function () //{{{ var exCommands = []; - // in '-quoted strings, only ' and \ itself are escaped - // in "-quoted strings, also ", \n and \t are translated - // in non-quoted strings everything is taken literally apart from "\ " and "\\" - // - // "options" is an array [name, type, validator, completions] and could look like: - // options = [[["-force"], OPTION_NOARG], - // [["-fullscreen", "-f"], OPTION_BOOL], - // [["-language"], OPTION_STRING, validateFunc, ["perl", "ruby"]], - // [["-speed"], OPTION_INT], - // [["-acceleration"], OPTION_FLOAT], - // [["-accessories"], OPTION_LIST, null, ["foo", "bar"]], - // [["-other"], OPTION_ANY]]; - // TODO: should it handle comments? - // TODO: should it return an error, if it contains arguments which look like options (beginning with -)? - function parseArgs(str, options) - { - // returns [count, parsed_argument] - function getNextArg(str) - { - var inSingleString = false; - var inDoubleString = false; - var inEscapeKey = false; - - var arg = ""; - - outer: - for (var i = 0; i < str.length; i++) - { - switch (str[i]) - { - case "\"": - if (inEscapeKey) - { - inEscapeKey = false; - break; - } - if (!inSingleString) - { - inDoubleString = !inDoubleString; - continue outer; - } - break; - - case "'": - if (inEscapeKey) - { - inEscapeKey = false; - break; - } - if (!inDoubleString) - { - inSingleString = !inSingleString; - continue outer; - } - break; - - // \ is an escape key for non quoted or "-quoted strings - // for '-quoted strings it is taken literally, apart from \' and \\ - case "\\": - if (inEscapeKey) - { - inEscapeKey = false; - break; - } - else - { - // only escape "\\" and "\ " in non quoted strings - if (!inSingleString && !inDoubleString && str[i + 1] != "\\" && str[i + 1] != " ") - continue outer; - // only escape "\\" and "\'" in single quoted strings - else if (inSingleString && str[i + 1] != "\\" && str[i + 1] != "'") - break; - else - { - inEscapeKey = true; - continue outer; - } - } - break; - - default: - if (inSingleString) - { - inEscapeKey = false; - break; - } - else if (inEscapeKey) - { - inEscapeKey = false; - switch (str[i]) - { - case "n": arg += "\n"; continue outer; - case "t": arg += "\t"; continue outer; - default: - break; // this makes "a\fb" -> afb; wanted or should we return ab? --mst - } - } - else if (!inDoubleString && /\s/.test(str[i])) - { - return [i, arg]; - } - else // a normal charcter - break; - } - arg += str[i]; - } - - // TODO: add parsing of a " comment here: - if (inDoubleString || inSingleString) - return [-1, "E114: Missing quote"]; - if (inEscapeKey) - return [-1, "trailing \\"]; - else - return [str.length, arg]; - } - - var args = []; // parsed arguments - var opts = []; // parsed options - if (!options) - options = []; - - var invalid = false; - var arg = null; - var count = 0; // the length of the argument - var i = 0; - outer: - while (i < str.length) - { - // skip whitespace - if (/\s/.test(str[i])) - { - i++; - continue; - } - - var sub = str.substr(i); - var optname = ""; - for (var opt = 0; opt < options.length; opt++) - { - for (var name = 0; name < options[opt][0].length; name++) - { - optname = options[opt][0][name]; - if (sub.indexOf(optname) == 0) - { - invalid = false; - arg = null; - // no value to the option - if (optname.length >= sub.length) - { - count = 0; - } - else if (sub[optname.length] == "=") - { - [count, arg] = getNextArg(sub.substr(optname.length + 1)); - if (count == -1) - { - liberator.echoerr("Invalid argument for option " + optname); - return null; - } - - count++; // to compensate the "=" character - } - else if (options[opt][1] != commandManager.OPTION_NOARG && /\s/.test(sub[optname.length])) - { - [count, arg] = getNextArg(sub.substr(optname.length + 1)); - if (count == -1) - { - liberator.echoerr("Invalid argument for option " + optname); - return null; - } - - // if we add the argument to an option after a space, it MUST not be empty - if (arg.length == 0) - arg = null; - - count++; // to compensate the " " character - } - else - { - // this isn't really an option as it has trailing characters, parse it as an argument - invalid = true; - } - - if (!invalid) - { - switch (options[opt][1]) // type - { - case commandManager.OPTION_NOARG: - if (arg != null) - { - liberator.echoerr("No argument allowed for option: " + optname); - return null; - } - break; - case commandManager.OPTION_BOOL: - if (arg == "true" || arg == "1" || arg == "on") - arg = true; - else if (arg == "false" || arg == "0" || arg == "off") - arg = false; - else - { - liberator.echoerr("Invalid argument for boolean option: " + optname); - return null; - } - break; - case commandManager.OPTION_STRING: - if (arg == null) - { - liberator.echoerr("Argument required for string option: " + optname); - return null; - } - break; - case commandManager.OPTION_INT: - arg = parseInt(arg, 10); - if (isNaN(arg)) - { - liberator.echoerr("Numeric argument required for integer option: " + optname); - return null; - } - break; - case commandManager.OPTION_FLOAT: - arg = parseFloat(arg); - if (isNaN(arg)) - { - liberator.echoerr("Numeric argument required for float option: " + optname); - return null; - } - break; - case commandManager.OPTION_LIST: - if (arg == null) - { - liberator.echoerr("Argument required for list option: " + optname); - return null; - } - arg = arg.split(/\s*,\s*/); - break; - } - - // we have a validator function - if (typeof options[opt][2] == "function") - { - if (options[opt][2].call(this, arg) == false) - { - liberator.echoerr("Invalid argument for option: " + optname); - return null; - } - } - - opts.push([options[opt][0][0], arg]); // always use the first name of the option - i += optname.length + count; - continue outer; - } - // if it is invalid, just fall through and try the next argument - } - } - } - - // if not an option, treat this token as an argument - var [count, arg] = getNextArg(sub); - if (count == -1) - { - liberator.echoerr("Error parsing arguments: " + arg); - return null; - } - - if (arg != null) - args.push(arg); - - i += count; // hopefully count is always >0, otherwise we get an endless loop - } - - return { opts: opts, args: args }; - } - - function getOption(opts, option, def) - { - for (var i = 0; i < opts.length; i++) - { - if (opts[i][0] == option) - return opts[i][1]; - } - - // no match found, return default - return def; - } - function getUserCommands(name) { var matches = []; @@ -457,9 +181,6 @@ liberator.Commands = function () //{{{ // Idea: If v.commands.add() specifies args or opts in extraInfo, don't call the function // with args as a string, but already pass an object like: // args = { -option: value, -anotheroption: true, arguments: [] } - getOption: function (opts, option, def) { return getOption(opts, option, def); }, - parseArgs: function (str, options) { return parseArgs(str, options); }, - OPTION_ANY: 0, // can be given no argument or an argument of any type, // caller is responsible for parsing the return value OPTION_NOARG: 1, @@ -539,8 +260,321 @@ liberator.Commands = function () //{{{ return null; }, - // TODO: generalized 0 count handling -> "Zero count" - // FIXME: doesn't really belong here... + // in '-quoted strings, only ' and \ itself are escaped + // in "-quoted strings, also ", \n and \t are translated + // in non-quoted strings everything is taken literally apart from "\ " and "\\" + // + // @param str: something like "-x=foo -opt=bar arg1 arg2" + // "options" is an array [name, type, validator, completions] and could look like: + // options = [[["-force"], OPTION_NOARG], + // [["-fullscreen", "-f"], OPTION_BOOL], + // [["-language"], OPTION_STRING, validateFunc, ["perl", "ruby"]], + // [["-speed"], OPTION_INT], + // [["-acceleration"], OPTION_FLOAT], + // [["-accessories"], OPTION_LIST, null, ["foo", "bar"]], + // [["-other"], OPTION_ANY]]; + // argCount can be: + // "0": no arguments + // "1": exactly one argument + // "+": one or more aguments + // "*": zero or more arguments + // "?": zero or one arguments + // TODO: should it handle comments? + parseArgs: function(str, options, argCount) + { + // returns [count, parsed_argument] + function getNextArg(str) + { + var inSingleString = false; + var inDoubleString = false; + var inEscapeKey = false; + + var arg = ""; + + outer: + for (var i = 0; i < str.length; i++) + { + switch (str[i]) + { + case "\"": + if (inEscapeKey) + { + inEscapeKey = false; + break; + } + if (!inSingleString) + { + inDoubleString = !inDoubleString; + continue outer; + } + break; + + case "'": + if (inEscapeKey) + { + inEscapeKey = false; + break; + } + if (!inDoubleString) + { + inSingleString = !inSingleString; + continue outer; + } + break; + + // \ is an escape key for non quoted or "-quoted strings + // for '-quoted strings it is taken literally, apart from \' and \\ + case "\\": + if (inEscapeKey) + { + inEscapeKey = false; + break; + } + else + { + // only escape "\\" and "\ " in non quoted strings + if (!inSingleString && !inDoubleString && str[i + 1] != "\\" && str[i + 1] != " ") + continue outer; + // only escape "\\" and "\'" in single quoted strings + else if (inSingleString && str[i + 1] != "\\" && str[i + 1] != "'") + break; + else + { + inEscapeKey = true; + continue outer; + } + } + break; + + default: + if (inSingleString) + { + inEscapeKey = false; + break; + } + else if (inEscapeKey) + { + inEscapeKey = false; + switch (str[i]) + { + case "n": arg += "\n"; continue outer; + case "t": arg += "\t"; continue outer; + default: + break; // this makes "a\fb" -> afb; wanted or should we return ab? --mst + } + } + else if (!inDoubleString && /\s/.test(str[i])) + { + return [i, arg]; + } + else // a normal charcter + break; + } + arg += str[i]; + } + + // TODO: add parsing of a " comment here: + if (inDoubleString || inSingleString) + return [-1, "E114: Missing quote"]; + if (inEscapeKey) + return [-1, "trailing \\"]; + else + return [str.length, arg]; + } + + if (!options) + options = []; + + if (!argCount) + argCount = "*"; + + var args = {}; // parsed options + args.arguments = []; // remaining arguments + + var invalid = false; + var onlyArgumentsRemaining = false; // after a -- has been found + var arg = null; + var count = 0; // the length of the argument + var i = 0; + outer: + while (i < str.length) + { + // skip whitespace + if (/\s/.test(str[i])) + { + i++; + continue; + } + + var sub = str.substr(i); + // dump(i + ": " + sub + " - " + onlyArgumentsRemaining + "\n"); + if ((!onlyArgumentsRemaining) && /^--(\s|$)/.test(sub)) + { + onlyArgumentsRemaining = true; + i+=2; + continue; + } + + var optname = ""; + if (!onlyArgumentsRemaining) + { + for (var opt = 0; opt < options.length; opt++) + { + for (var name = 0; name < options[opt][0].length; name++) + { + optname = options[opt][0][name]; + if (sub.indexOf(optname) == 0) + { + invalid = false; + arg = null; + // no value to the option + if (optname.length >= sub.length) + { + count = 0; + } + else if (sub[optname.length] == "=") + { + [count, arg] = getNextArg(sub.substr(optname.length + 1)); + if (count == -1) + { + liberator.echoerr("Invalid argument for option " + optname); + return null; + } + + count++; // to compensate the "=" character + } + else if (/\s/.test(sub[optname.length])) + { + if (options[opt][1] != this.OPTION_NOARG) + { + [count, arg] = getNextArg(sub.substr(optname.length + 1)); + if (count == -1) + { + liberator.echoerr("Invalid argument for option " + optname); + return null; + } + + // if we add the argument to an option after a space, it MUST not be empty + if (arg.length == 0) + arg = null; + + count++; // to compensate the " " character + } + else + count = 1; // the space + } + else + { + // this isn't really an option as it has trailing characters, parse it as an argument + invalid = true; + } + + if (!invalid) + { + switch (options[opt][1]) // type + { + case this.OPTION_NOARG: + if (arg != null) + { + liberator.echoerr("No argument allowed for option: " + optname); + return null; + } + break; + case this.OPTION_BOOL: + if (arg == "true" || arg == "1" || arg == "on") + arg = true; + else if (arg == "false" || arg == "0" || arg == "off") + arg = false; + else + { + liberator.echoerr("Invalid argument for boolean option: " + optname); + return null; + } + break; + case this.OPTION_STRING: + if (arg == null) + { + liberator.echoerr("Argument required for string option: " + optname); + return null; + } + break; + case this.OPTION_INT: + arg = parseInt(arg, 10); + if (isNaN(arg)) + { + liberator.echoerr("Numeric argument required for integer option: " + optname); + return null; + } + break; + case this.OPTION_FLOAT: + arg = parseFloat(arg); + if (isNaN(arg)) + { + liberator.echoerr("Numeric argument required for float option: " + optname); + return null; + } + break; + case this.OPTION_LIST: + if (arg == null) + { + liberator.echoerr("Argument required for list option: " + optname); + return null; + } + arg = arg.split(/\s*,\s*/); + break; + } + + // we have a validator function + if (typeof options[opt][2] == "function") + { + if (options[opt][2].call(this, arg) == false) + { + liberator.echoerr("Invalid argument for option: " + optname); + return null; + } + } + + args[options[opt][0][0]] = arg; // always use the first name of the option + i += optname.length + count; + continue outer; + } + // if it is invalid, just fall through and try the next argument + } + } + } + } + + // if not an option, treat this token as an argument + var [count, arg] = getNextArg(sub); + if (count == -1) + { + liberator.echoerr("Error parsing arguments: " + arg); + return null; + } + else if (!onlyArgumentsRemaining && /^-/.test(arg)) + { + liberator.echoerr("Invalid option: " + arg); + return null; + } + + if (arg != null) + args.arguments.push(arg); + + i += count; // hopefully count is always >0, otherwise we get an endless loop + } + + // check for correct number of arguments + if ((args.arguments.length == 0 && (argCount == "1" || argCount == "+")) || + (args.arguments.length == 1 && (argCount == "0")) || + (args.arguments.length > 1 && (argCount == "0" || argCount == "1" || argCount == "?"))) + { + liberator.echoerr("Invalid number of arguments: " + args.arguments.length); + return null; + } + + return args; + }, + // return [null, null, null, null, heredoc_tag || false]; // [count, cmd, special, args] = match; parseCommand: function (str, tag) @@ -629,7 +663,7 @@ liberator.Commands = function () //{{{ } }, { - /*args: [[["-nargs"], OPTION_STRING, function (arg) { return /^(0|1|\*|\?|\+)$/.test(arg); }], + /*options: [[["-nargs"], OPTION_STRING, function (arg) { return /^(0|1|\*|\?|\+)$/.test(arg); }], [["-bang"], OPTION_NOARG], [["-bar"], OPTION_NOARG]] */ }); diff --git a/content/liberator.js b/content/liberator.js index 2ca36601..2c9b5f49 100644 --- a/content/liberator.js +++ b/content/liberator.js @@ -733,7 +733,7 @@ const liberator = (function () //{{{ // liberator.options does not exist at the very beginning if (liberator.options) verbose = liberator.options["verbose"]; -dump("level: " + level + " - verbose: " + verbose + "\n"); + if (level > verbose) return; diff --git a/content/mail.js b/content/mail.js index 9fcc32ce..23b3327e 100644 --- a/content/mail.js +++ b/content/mail.js @@ -651,7 +651,7 @@ liberator.Mail = function () //{{{ try { if (liberator.mail.currentAccount.server.type == "rss") - OpenBrowserWithMessageId(gDBView.hdrForFirstSelectedMessage.messageId) + messenger.launchExternalURL(getRSSUrl()); // TODO: what to do for non-rss message? } catch (e) { liberator.beep(); } @@ -684,20 +684,15 @@ liberator.Mail = function () //{{{ "Write a new message", function (args, special, count) { - var res = liberator.commands.parseArgs(args, this.args); - if (!res) - return; - var mailargs = new Object(); - mailargs.subject = liberator.commands.getOption(res.opts, "-subject", undefined); - mailargs.bcc = liberator.commands.getOption(res.opts, "-bcc", undefined); - mailargs.cc = liberator.commands.getOption(res.opts, "-cc", undefined); - mailargs.body = liberator.commands.getOption(res.opts, "-text", undefined); - mailargs.attachments = liberator.commands.getOption(res.opts, "-attachment", []); + mailargs.to = args.arguments.join(", "); + mailargs.subject = args["-subject"]; + mailargs.bcc = args["-bcc"]; + mailargs.cc = args["-cc"]; + mailargs.body = args["-text"]; + mailargs.attachments = args["-attachment"] || []; - var addresses = []; - if (res.args) - addresses = addresses.concat(res.args); + var addresses = args.arguments; if (mailargs.bcc) addresses = addresses.concat(mailargs.bcc); if (mailargs.cc) @@ -710,19 +705,14 @@ liberator.Mail = function () //{{{ return; } - if (res.args.length > 0) - { - mailargs.to = res.args.join(", "); - } - liberator.mail.composeNewMail(mailargs); }, { - args: [[["-subject", "-s"], liberator.commands.OPTION_STRING], - [["-attachment", "-a"], liberator.commands.OPTION_LIST], - [["-bcc", "-b"], liberator.commands.OPTION_STRING], - [["-cc", "-c"], liberator.commands.OPTION_STRING], - [["-text", "-t"], liberator.commands.OPTION_STRING]] + options: [[["-subject", "-s"], liberator.commands.OPTION_STRING], + [["-attachment", "-a"], liberator.commands.OPTION_LIST], + [["-bcc", "-b"], liberator.commands.OPTION_STRING], + [["-cc", "-c"], liberator.commands.OPTION_STRING], + [["-text", "-t"], liberator.commands.OPTION_STRING]] }); liberator.commands.add(["copy[to]"],