diff --git a/Donators b/Donators index cc072a07..ba4f8bc2 100644 --- a/Donators +++ b/Donators @@ -1,22 +1,25 @@
 Note: If you don't wish to appear on this list when making a donation, please tell me.
 
-* Andrew Pantyukhin
-* Ben Klemens
-* Sjoerd Siebinga
-* Cillian de Roiste
-* Miron Tewfik
+2007 (most recent donators first):
+
 * Robert Heckel
-* Stefan Krauth
-* Giuseppe Guida
-* Richard Dooling
-* Nigel McNie
-* Paulo Tanimoto
-* Nathan Saper
-* Albert Menkveld
-* Ian Taylor
-* Thomas Svensen
 * Ramana Kumar
+* Thomas Svensen
+* Ian Taylor
+* Albert Menkveld
+* Nathan Saper
+* Paulo Tanimoto
+* Nigel McNie
+* Richard Dooling
+* Giuseppe Guida
+* Stefan Krauth
+* Robert Heckel
+* Miron Tewfik
+* Cillian de Roiste
+* Sjoerd Siebinga
+* Ben Klemens
+* Andrew Pantyukhin
 
 I want to say a big THANK YOU for all people which supported this project in this way.
 
diff --git a/NEWS b/NEWS index 86bd8e98..91682b42 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,7 @@ 2007-xx-xx: * version 0.6 * THIS VERSION ONLY WORKS WITH FIREFOX 3.0 + * tags and keyword support for :bmark * added full zoom, and changed keybindings slightly for text zoom * :buffer partial_string works now as in vim, and with ! even better * improvements for scrollable -- more -- prompt diff --git a/content/bookmarks.js b/content/bookmarks.js index 4d618f70..55aea0f3 100644 --- a/content/bookmarks.js +++ b/content/bookmarks.js @@ -38,8 +38,8 @@ function Bookmarks() //{{{ .getService(Components.interfaces.nsINavBookmarksService); const tagging_service = Components.classes["@mozilla.org/browser/tagging-service;1"] .getService(Components.interfaces.nsITaggingService); - const search_service = Components.classes["@mozilla.org/browser/search-service;1"]. - getService(Components.interfaces.nsIBrowserSearchService); + const search_service = Components.classes["@mozilla.org/browser/search-service;1"] + .getService(Components.interfaces.nsIBrowserSearchService); const io_service = Components.classes['@mozilla.org/network/io-service;1'] .getService(Components.interfaces.nsIIOService); @@ -112,21 +112,39 @@ function Bookmarks() //{{{ return bookmarks; } - this.add = function (title, url, keyword) + this.add = function (title, url, keyword, tags) { if (!bookmarks) load(); - var uri = io_service.newURI(url, null, null); - var id = bookmarks_service.insertBookmark(bookmarks_service.bookmarksRoot, uri, -1, title); - if (id && keyword) + // if no protocol specified, default to http://, isn't there a better way? + if (/^\w+:/.test(url) == false) + url = "http://" + url; + + try { - bookmarks_service.setKeywordForBookmark(id, keyword); - keywords.unshift([keyword, title, url]); + var uri = io_service.newURI(url, null, null); + var id = bookmarks_service.insertBookmark(bookmarks_service.bookmarksRoot, uri, -1, title); + if (!id) + return false; + + if (keyword) + { + bookmarks_service.setKeywordForBookmark(id, keyword); + keywords.unshift([keyword, title, url]); + } + + if (tags) + tagging_service.tagURI(uri, tags); + } + catch (e) + { + vimperator.log(e); + return false; } //also update bookmark cache - bookmarks.unshift([url, title, null, []]); + bookmarks.unshift([url, title, keyword, tags || []]); return true; } @@ -136,12 +154,22 @@ function Bookmarks() //{{{ if (!url) return 0; - var uri = io_service.newURI(url, null, null); - var count = {}; - var bmarks = bookmarks_service.getBookmarkIdsForURI(uri, count); + var i = 0; + try + { + var uri = io_service.newURI(url, null, null); + var count = {}; + var bmarks = bookmarks_service.getBookmarkIdsForURI(uri, count); + + for (; i < bmarks.length; i++) + bookmarks_service.removeItem(bmarks[i]); + } + catch (e) + { + vimperator.log(e); + return i; + } - for (var i = 0; i < bmarks.length; i++) - bookmarks_service.removeItem(bmarks[i]); // also update bookmark cache, if we removed at least one bookmark if (count.value > 0) @@ -298,75 +326,6 @@ function Bookmarks() //{{{ vimperator.commandline.echo(list, vimperator.commandline.HL_NORMAL, vimperator.commandline.FORCE_MULTILINE); } } - - // res = parseBookmarkString("-t tag1,tag2 -T title http://www.orf.at"); - // res.tags is an array of tags - // res.title is the title or "" if no one was given - // res.url is the url as a string - // returns null, if parsing failed - Bookmarks.parseBookmarkString = function(str) - { - var res = {}; - res.tags = []; - res.title = null; - res.url = null; - - var re_title = /^\s*((-t|--title)\s+(\w+|\".*\"))(.*)/; - var re_tags = /^\s*((-T|--tags)\s+((\w+)(,\w+)*))(.*)/; - var re_url = /^\s*(\".+\"|\S+)(.*)/; - - var match_tags = null; - var match_title = null; - var match_url = null; - - while (!str.match(/^\s*$/)) - { - // first check for --tags - match_tags = str.match(re_tags); - if (match_tags != null) - { - str = match_tags[match_tags.length - 1]; // the last captured parenthesis is the rest of the string - tags = match_tags[3].split(","); - res.tags = res.tags.concat(tags); - } - else // then for --titles - { - - match_title = str.match(re_title); - if (match_title != null) - { - // only one title allowed - if (res.title != null) - return null; - - str = match_title[match_title.length - 1]; // the last captured parenthesis is the rest of the string - var title = match_title[3]; - if (title.charAt(0) == '"') - title = title.substring(1, title.length - 1); - res.title = title; - } - else // at last check for a URL - { - match_url = str.match(re_url); - if (match_url != null) - { - // only one url allowed - if (res.url != null) - return null; - - str = match_url[match_url.length - 1]; // the last captured parenthesis is the rest of the string - url = match_url[1]; - if (url.charAt(0) == '"') - url = url.substring(1, url.length - 1); - res.url = url; - } - else - return null; // no url, tag or title found but still text left, abort - } - } - } - return res; - } //}}} } //}}} diff --git a/content/commands.js b/content/commands.js index 4afc81d9..3bd4e000 100644 --- a/content/commands.js +++ b/content/commands.js @@ -82,9 +82,10 @@ function Command(specs, action, extra_info) //{{{ if (extra_info.usage) this.usage = extra_info.usage; - this.help = extra_info.help || null; + this.help = extra_info.help || null; this.short_help = extra_info.short_help || null; - this.completer = extra_info.completer || null; + this.completer = extra_info.completer || null; + this.args = extra_info.args || []; } } @@ -127,6 +128,14 @@ function Commands() //{{{ //////////////////////////////////////////////////////////////////////////////// ////////////////////// PRIVATE SECTION ///////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ + const OPTION_ANY = 0; // can be given no argument or an argument of any type, user is responsible + // for parsing the return value + const OPTION_NOARG = 1; + const OPTION_BOOL = 2; + const OPTION_STRING = 3; + const OPTION_INT = 4; + const OPTION_FLOAT = 5; + const OPTION_LIST = 6; var ex_commands = []; var last_run_command = ""; // updated whenever the users runs a command with :! @@ -140,6 +149,243 @@ function Commands() //{{{ } } + // in '-quoted strings, only ' and \ itself are escaped + // in "-quoted strings, also ", \n and \t are translated + // + // "options" is an array [name, type, validator, completions] and could look like: + // options = [[["-force"], OPTION_NOARG], + // [["-fullscreen"], 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 in_single_string = false; + var in_double_string = false; + var in_escape_key = false; + + var arg = ""; + + outer: + for (var i = 0; i < str.length; i++) + { + switch(str[i]) + { + case "\"": + if (in_escape_key) + { + in_escape_key = false; + break; + } + if (!in_single_string) + { + in_double_string = !in_double_string; + continue outer; + } + break; + + case "'": + if (in_escape_key) + { + in_escape_key = false; + break; + } + if (!in_double_string) + { + in_single_string = !in_single_string; + 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 (in_escape_key) + { + in_escape_key = false; + break; + } + else + { + in_escape_key = true; + if (in_single_string && str[i+1] != "\\" && str[i+1] != "'") + break; + else + continue outer; + } + break; + + default: + if (in_single_string) + { + in_escape_key = false; + break; + } + else if (in_escape_key) + { + in_escape_key = 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 (!in_double_string && /\s/.test(str[i])) + { + return [i, arg]; + } + else // a normal charcter + break; + } + arg += str[i]; + } + + // TODO: add parsing of a " comment here: + if (in_double_string || in_single_string) + return [-1, "unterminated string literal"]; + if (in_escape_key) + return [-1, "trailing \\"]; + else + return [str.length, arg]; + } + + + var args = []; // parsed arguments + var opts = []; // parsed options + if (!options) + options = []; + + var arg = null; + var count = 0; + 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) + { + var invalid = false; + // no value to the option + if (optname.length >= sub.length || /\s/.test(sub[optname.length])) + { + arg = null; + count = 0; + } + else if (sub[optname.length] == "=") + { + [count, arg] = getNextArg(sub.substr(optname.length + 1)); + if (count == -1) + return { error: "Invalid argument for option " + optname, opts: [], args: [] } + + 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 OPTION_NOARG: + if (arg != null) + return { error: "No argument allowed for option: " + optname, opts: [], args: [] } + break; + case OPTION_BOOL: + if (arg == "true" || arg == "1" || arg == "on") + arg = true; + else if (arg == "false" || arg == "0" || arg == "off") + arg = false; + else + return { error: "Invalid argument for boolean option: " + optname, opts: [], args: [] } + break; + case OPTION_STRING: + if (arg == null) + return { error: "Argument required for string option: " + optname, opts: [], args: [] } + break; + case OPTION_INT: + arg = parseInt(arg); + if (isNaN(arg)) + return { error: "Numeric argument required for integer option: " + optname, opts: [], args: [] } + break; + case OPTION_FLOAT: + arg = parseFloat(arg); + if (isNaN(arg)) + return { error: "Numeric argument required for float option: " + optname, opts: [], args: [] } + break; + case OPTION_LIST: + if (arg == null) + return { error: "Argument required for list option: " + optname, opts: [], args: [] } + 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) + return { error: "Invalid argument for option: " + optname, opts: [], args: [] } + } + + 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) + return { error: "Error parsing arguments: " + arg, opts: [], args: [] } + + if (arg != null) + args.push(arg); + + i += count; // hopefully count is always >0, otherwise we get an endless loop + } + + return { error: null, 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 commandsIterator() { for (var i = 0; i < ex_commands.length; i++) @@ -293,43 +539,39 @@ function Commands() //{{{ } )); addDefaultCommand(new Command(["bma[rk]"], - // takes: -t "foo" myurl - // converts that string to a useful url and title, and calls addBookmark function(args) { - var result = Bookmarks.parseBookmarkString(args); - - if (result) + var res = parseArgs(args, this.args); + if (res.error) { - if (result.url == null) - { - result.url = vimperator.buffer.URL; - // also guess title if the current url is :bmarked - if (result.title == null) - result.title = vimperator.buffer.title; - } - - if (result.title == null) // title could still be null - result.title = result.url; - - vimperator.bookmarks.add(result.title, result.url); - vimperator.echo("Bookmark `" + result.title + "' added with url `" + result.url + "'", vimperator.commandline.FORCE_SINGLELINE); + vimperator.echoerr(res.error); + return false; } + + var url = res.args.length == 0 ? vimperator.buffer.URL : res.args[0]; + var title = getOption(res.opts, "-title", res.args.length == 0 ? vimperator.buffer.title : null); + if (!title) + title = url; + var keyword = getOption(res.opts, "-keyword", null); + var tags = getOption(res.opts, "-tags", []); + + if (vimperator.bookmarks.add(title, url, keyword, tags)) + vimperator.echo("Bookmark `" + title + "' added with url `" + url + "'", vimperator.commandline.FORCE_SINGLELINE); else - { - vimperator.echoerr("E474: Invalid argument"); - } + vimperator.echoerr("Exxx: Could not add bookmark `" + title + "'", vimperator.commandline.FORCE_SINGLELINE); }, { - usage: ["bma[rk] [-t {title}] [url]"], + usage: ["bma[rk] [-title=title] [-keyword=kw] [-tags=tag1,tag2] [url]"], short_help: "Add a bookmark", help: "If you don't add a custom title, either the title of the web page or the URL will be taken as the title.
" + "You can omit the optional [url] argument, so just do :bmark to bookmark the currently loaded web page with a default title and without any tags.
" + - " -t \"custom title\"
" + - "The following options will be interpreted in the future:
" + - " -T comma,separated,tag,list
" + - " -k keyword
" + - "Tags WILL be some mechanism to classify bookmarks. Assume, you tag a URL with the tags \"linux\" and \"computer\" you'll be able to search for bookmarks containing these tags." + "The following options are interpreted:
" + + " -title=\"custom title\"
" + + " -tags=comma,separated,tag,list
" + + " -keyword=keyword
", + args: [[["-title", "-t"], OPTION_STRING], + [["-tags", "-T"], OPTION_LIST], + [["-keyword", "-k"], OPTION_STRING, function(arg) { return /\w/.test(arg); } ]] } )); addDefaultCommand(new Command(["bmarks"], @@ -405,25 +647,20 @@ function Commands() //{{{ addDefaultCommand(new Command(["delbm[arks]"], function(args, special) { - var result = Bookmarks.parseBookmarkString(args); + var url = args; - if (result) - { - if (result.url == null) - result.url = vimperator.buffer.URL; + if (!url) + url = vimperator.buffer.URL; - var deleted_count = vimperator.bookmarks.remove(result.url); - vimperator.echo(deleted_count + " bookmark(s) with url `" + result.url + "' deleted", vimperator.commandline.FORCE_SINGLELINE); - } - else - { - vimperator.echoerr("E488: Trailing characters"); - } + var deleted_count = vimperator.bookmarks.remove(url); + vimperator.echo(deleted_count + " bookmark(s) with url `" + url + "' deleted", vimperator.commandline.FORCE_SINGLELINE); }, { - usage: ["delbm[arks] {url}"], + usage: ["delbm[arks] [url]"], short_help: "Delete a bookmark", - help: "Deletes all bookmarks which match the {url}. Use <Tab> key on a string to complete the URL which you want to delete.
" + + help: "Deletes all bookmarks which match the [url]. " + + "If ommited, [url] defaults to the URL of the current buffer. " + + "Use <Tab> key on a string to complete the URL which you want to delete.
" + "The following options WILL be interpreted in the future:
" + " [!] a special version to delete ALL bookmarks
" + " -T comma,separated,tag,list
", @@ -433,11 +670,21 @@ function Commands() //{{{ addDefaultCommand(new Command(["com[mand]"], function(args) { - vimperator.echo(vimperator.util.colorize(window.argParser(args))); + var res = parseArgs(args, this.args); + if (res.error) + vimperator.echoerr(res.error); + else + { + vimperator.echo(vimperator.util.colorize(res)); + } }, { + usage: ["com[mand][!] [{attr}...] {cmd} {rep}"], short_help: "Temporarily used for testing args parser", - help: "" + help: "", + args: [[["-nargs"], OPTION_STRING, function(arg) { return /^(0|1|\*|\?|\+)$/.test(arg); } ], + [["-bang"], OPTION_NOARG], + [["-bar"], OPTION_NOARG]] } )); addDefaultCommand(new Command(["delm[arks]"], diff --git a/content/events.js b/content/events.js index 7884ad24..7bb349b8 100644 --- a/content/events.js +++ b/content/events.js @@ -733,7 +733,6 @@ function Events() //{{{ else { vimperator.input.buffer = ""; - // vimperator.log("executed: " + candidate_command + " in mode: " + mode, 8); map.execute(null, vimperator.input.count); } }