1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
9 var DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png";
11 // also includes methods for dealing with keywords and search engines
12 var Bookmarks = Module("bookmarks", {
14 this.timer = Timer(0, 100, function () {
15 this.checkBookmarked(buffer.uri);
18 storage.addObserver("bookmark-cache", function (key, event, arg) {
19 if (["add", "change", "remove"].indexOf(event) >= 0)
20 autocommands.trigger("Bookmark" + event[0].toUpperCase() + event.substr(1),
23 toString: function () "bookmarkcache.bookmarks[" + arg.id + "]",
24 valueOf: function () arg
27 bookmarks.timer.tell();
32 "browser.locationChange": function (webProgress, request, uri) {
33 statusline.bookmarked = false;
34 this.checkBookmarked(uri);
40 title: ["URL", "Info"],
41 keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" },
42 process: [template.icon, template.bookmarkDescription]
45 // TODO: why is this a filter? --djk
46 get: function get(filter, tags, maxItems, extra) {
47 return completion.runCompleter("bookmark", filter, maxItems, tags, extra);
51 * Adds a new bookmark. The first parameter should be an object with
52 * any of the following properties:
54 * @param {boolean} unfiled If true, the bookmark is added to the
55 * Unfiled Bookmarks Folder.
56 * @param {string} title The title of the new bookmark.
57 * @param {string} url The URL of the new bookmark.
58 * @param {string} keyword The keyword of the new bookmark.
60 * @param {[string]} tags The tags for the new bookmark.
62 * @param {boolean} force If true, a new bookmark is always added.
63 * Otherwise, if a bookmark for the given URL exists it is
66 * @returns {boolean} True if the bookmark was added or updated
69 add: function add(unfiled, title, url, keyword, tags, force) {
71 if (isObject(unfiled))
72 var { unfiled, title, url, keyword, tags, post, charset, force } = unfiled;
75 let uri = util.createURI(url);
76 if (!force && bookmarkcache.isBookmarked(uri))
77 for (var bmark in bookmarkcache)
78 if (bmark.url == uri.spec) {
85 PlacesUtils.tagging.untagURI(uri, null);
86 PlacesUtils.tagging.tagURI(uri, tags);
88 if (bmark == undefined)
89 bmark = bookmarkcache.bookmarks[
90 services.bookmarks.insertBookmark(
91 services.bookmarks[unfiled ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"],
92 uri, -1, title || url)];
96 if (charset !== undefined)
97 bmark.charset = charset;
98 if (post !== undefined)
101 bmark.keyword = keyword;
112 * Opens the command line in Ex mode pre-filled with a :bmark
113 * command to add a new search keyword for the given form element.
115 * @param {Element} elem A form element for which to add a keyword.
117 addSearchKeyword: function addSearchKeyword(elem) {
118 if (elem instanceof HTMLFormElement || elem.form)
119 var [url, post, charset] = util.parseForm(elem);
121 var [url, post, charset] = [elem.href || elem.src, null, elem.ownerDocument.characterSet];
123 let options = { "-title": "Search " + elem.ownerDocument.title };
125 options["-post"] = post;
126 if (charset != null && charset !== "UTF-8")
127 options["-charset"] = charset;
129 CommandExMode().open(
130 commands.commandToString({ command: "bmark", options: options, arguments: [url] }) + " -keyword ");
133 checkBookmarked: function checkBookmarked(uri) {
134 if (PlacesUtils.asyncGetBookmarkIds)
135 PlacesUtils.asyncGetBookmarkIds(uri, function (ids) {
136 statusline.bookmarked = ids.length;
139 this.timeout(function () {
140 statusline.bookmarked = bookmarkcache.isBookmarked(uri);
145 * Toggles the bookmarked state of the given URL. If the URL is
146 * bookmarked, all bookmarks for said URL are removed.
147 * If it is not, a new bookmark is added to the Unfiled Bookmarks
148 * Folder. The new bookmark has the title of the current buffer if
149 * its URL is identical to *url*, otherwise its title will be the
152 * @param {string} url The URL of the bookmark to toggle.
154 toggle: function toggle(url) {
158 let count = this.remove(url);
160 dactyl.echomsg({ domains: [util.getHost(url)], message: _("bookmark.removed", url) });
162 let title = buffer.uri.spec == url && buffer.title || url;
165 extra = " (" + title + ")";
166 this.add({ unfiled: true, title: title, url: url });
167 dactyl.echomsg({ domains: [util.getHost(url)], message: _("bookmark.added", url + extra) });
171 isBookmarked: deprecated("bookmarkcache.isBookmarked", { get: function isBookmarked() bookmarkcache.closure.isBookmarked }),
174 * Remove a bookmark or bookmarks. If *ids* is an array, removes the
175 * bookmarks with those IDs. If it is a string, removes all
176 * bookmarks whose URLs match that string.
178 * @param {string|[number]} ids The IDs or URL of the bookmarks to
180 * @returns {number} The number of bookmarks removed.
182 remove: function remove(ids) {
185 let uri = util.newURI(ids);
186 ids = services.bookmarks
187 .getBookmarkIdsForURI(uri, {})
188 .filter(bookmarkcache.closure.isRegularBookmark);
190 ids.forEach(function (id) {
191 let bmark = bookmarkcache.bookmarks[id];
193 PlacesUtils.tagging.untagURI(bmark.uri, null);
194 bmark.charset = null;
196 services.bookmarks.removeItem(id);
201 dactyl.reportError(e, true);
206 getSearchEngines: deprecated("bookmarks.searchEngines", function getSearchEngines() this.searchEngines),
208 * Returns a list of all visible search engines in the search
209 * services, augmented with keyword, title, and icon properties for
210 * use in completion functions.
212 get searchEngines() {
213 let searchEngines = [];
215 return iter(services.browserSearch.getVisibleEngines({})).map(function ([, engine]) {
216 let alias = engine.alias;
217 if (!alias || !/^[a-z0-9-]+$/.test(alias))
218 alias = engine.name.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/, "").toLowerCase();
220 alias = "search"; // for search engines which we can't find a suitable alias
222 if (Set.has(aliases, alias))
223 alias += ++aliases[alias];
227 return [alias, { keyword: alias, __proto__: engine, title: engine.description, icon: engine.iconURI && engine.iconURI.spec }];
232 * Retrieves a list of search suggestions from the named search
233 * engine based on the given *query*. The results are always in the
234 * form of an array of strings. If *callback* is provided, the
235 * request is executed asynchronously and *callback* is called on
236 * completion. Otherwise, the request is executed synchronously and
237 * the results are returned.
239 * @param {string} engineName The name of the search engine from
240 * which to request suggestions.
241 * @param {string} query The query string for which to request
243 * @param {function([string])} callback The function to call when
244 * results are returned.
245 * @returns {[string] | null}
247 getSuggestions: function getSuggestions(engineName, query, callback) {
248 const responseType = "application/x-suggestions+json";
250 let engine = Set.has(this.searchEngines, engineName) && this.searchEngines[engineName];
251 if (engine && engine.supportsResponseType(responseType))
252 var queryURI = engine.getSubmission(query, responseType).uri.spec;
254 return (callback || util.identity)([]);
256 function process(req) {
259 results = JSON.parse(req.responseText)[1].filter(isString);
263 return callback(results);
267 let req = util.httpGet(queryURI, callback && process);
274 * Returns an array containing a search URL and POST data for the
275 * given search string. If *useDefsearch* is true, the string is
276 * always passed to the default search engine. If it is not, the
277 * search engine name is retrieved from the first space-separated
278 * token of the given string.
280 * Returns null if no search engine is found for the passed string.
282 * @param {string} text The text for which to retrieve a search URL.
283 * @param {boolean} useDefsearch Whether to use the default search
285 * @returns {[string, string | null] | null}
287 getSearchURL: function getSearchURL(text, useDefsearch) {
288 let query = (useDefsearch ? options["defsearch"] + " " : "") + text;
290 // ripped from Firefox
293 var offset = query.indexOf(" ");
295 keyword = query.substr(0, offset);
296 param = query.substr(offset + 1);
299 var engine = Set.has(bookmarks.searchEngines, keyword) && bookmarks.searchEngines[keyword];
301 if (engine.searchForm && !param)
302 return engine.searchForm;
303 let submission = engine.getSubmission(param, null);
304 return [submission.uri.spec, submission.postData];
307 let [url, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword);
311 let data = window.unescape(postData || "");
312 if (/%s/i.test(url) || /%s/i.test(data)) {
314 var matches = url.match(/^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/);
316 [, url, charset] = matches;
319 charset = services.history.getCharsetForURI(util.newURI(url));
324 var encodedParam = escape(window.convertFromUnicode(charset, param));
326 encodedParam = bookmarkcache.keywords[keyword].encodeURIComponent(param);
328 url = url.replace(/%s/g, encodedParam).replace(/%S/g, param);
329 if (/%s/i.test(data))
330 postData = window.getPostDataStream(data, param, encodedParam, "application/x-www-form-urlencoded");
336 return [url, postData];
341 * Lists all bookmarks whose URLs match *filter*, tags match *tags*,
342 * and other properties match the properties of *extra*. If
343 * *openItems* is true, the items are opened in tabs rather than
346 * @param {string} filter A URL filter string which the URLs of all
347 * matched items must contain.
348 * @param {[string]} tags An array of tags each of which all matched
349 * items must contain.
350 * @param {boolean} openItems If true, items are opened rather than
352 * @param {object} extra Extra properties which must be matched.
354 list: function list(filter, tags, openItems, maxItems, extra) {
355 // FIXME: returning here doesn't make sense
356 // Why the hell doesn't it make sense? --Kris
357 // Because it unconditionally bypasses the final error message
358 // block and does so only when listing items, not opening them. In
359 // short it breaks the :bmarks command which doesn't make much
360 // sense to me but I'm old-fashioned. --djk
362 return completion.listCompleter("bookmark", filter, maxItems, tags, extra);
363 let items = completion.runCompleter("bookmark", filter, maxItems, tags, extra);
366 return dactyl.open(items.map(function (i) i.url), dactyl.NEW_TAB);
368 if (filter.length > 0 && tags.length > 0)
369 dactyl.echoerr(_("bookmark.noMatching", tags.map(String.quote), filter.quote()));
370 else if (filter.length > 0)
371 dactyl.echoerr(_("bookmark.noMatchingString", filter.quote()));
372 else if (tags.length > 0)
373 dactyl.echoerr(_("bookmark.noMatchingTags", tags.map(String.quote)));
375 dactyl.echoerr(_("bookmark.none"));
380 commands: function () {
381 commands.add(["ju[mps]"],
384 let sh = history.session;
385 commandline.commandOutput(template.jumps(sh.index, sh));
389 // TODO: Clean this up.
391 names: ["-tags", "-T"],
392 description: "A comma-separated list of tags",
393 completer: function tags(context, args) {
394 context.generate = function () array(b.tags for (b in bookmarkcache) if (b.tags)).flatten().uniq().array;
395 context.keys = { text: util.identity, description: util.identity };
397 type: CommandOption.LIST
401 names: ["-title", "-t"],
402 description: "Bookmark page title or description",
403 completer: function title(context, args) {
404 let frames = buffer.allFrames();
407 [win.document.title, frames.length == 1 ? /*L*/"Current Location" : /*L*/"Frame: " + win.location.href]
408 for ([, win] in Iterator(frames))];
409 context.keys.text = "title";
410 context.keys.description = "url";
411 return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter });
413 type: CommandOption.STRING
417 names: ["-post", "-p"],
418 description: "Bookmark POST data",
419 completer: function post(context, args) {
420 context.keys.text = "post";
421 context.keys.description = "url";
422 return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], post: context.filter });
424 type: CommandOption.STRING
428 names: ["-keyword", "-k"],
429 description: "Keyword by which this bookmark may be opened (:open {keyword})",
430 completer: function keyword(context, args) {
431 context.keys.text = "keyword";
432 return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] });
434 type: CommandOption.STRING,
435 validator: function (arg) /^\S+$/.test(arg)
438 commands.add(["bma[rk]"],
444 keyword: args["-keyword"] || null,
445 charset: args["-charset"],
447 tags: args["-tags"] || [],
448 title: args["-title"] || (args.length === 0 ? buffer.title : null),
449 url: args.length === 0 ? buffer.uri.spec : args[0]
452 if (bookmarks.add(opts)) {
453 let extra = (opts.title == opts.url) ? "" : " (" + opts.title + ")";
454 dactyl.echomsg({ domains: [util.getHost(opts.url)], message: _("bookmark.added", opts.url + extra) },
455 1, commandline.FORCE_SINGLELINE);
458 dactyl.echoerr(_("bookmark.cantAdd", opts.title.quote()));
462 completer: function (context, args) {
464 context.title = ["Page URL"];
465 let frames = buffer.allFrames();
466 context.completions = [
467 [win.document.documentURI, frames.length == 1 ? /*L*/"Current Location" : /*L*/"Frame: " + win.document.title]
468 for ([, win] in Iterator(frames))];
471 completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
473 options: [keyword, title, tags, post,
475 names: ["-charset", "-c"],
476 description: "The character encoding of the bookmark",
477 type: CommandOption.STRING,
478 completer: function (context) completion.charset(context),
479 validator: Option.validateCompleter
484 commands.add(["bmarks"],
485 "List or open multiple bookmarks",
487 bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"],
488 { keyword: args["-keyword"], title: args["-title"] });
492 completer: function completer(context, args) {
493 context.filter = args.join(" ");
494 completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
496 options: [tags, keyword, title,
498 names: ["-max", "-m"],
499 description: "The maximum number of items to list or open",
500 type: CommandOption.INT
503 // Not privateData, since we don't treat bookmarks as private
506 commands.add(["delbm[arks]"],
510 commandline.input(_("bookmark.prompt.deleteAll") + " ",
512 if (resp && resp.match(/^y(es)?$/i)) {
513 bookmarks.remove(Object.keys(bookmarkcache.bookmarks));
514 dactyl.echomsg(_("bookmark.allDeleted"));
518 if (!(args.length || args["-tags"] || args["-keyword"] || args["-title"]))
519 var deletedCount = bookmarks.remove(buffer.uri.spec);
521 let context = CompletionContext(args.join(" "));
522 context.fork("bookmark", 0, completion, "bookmark",
523 args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
524 deletedCount = bookmarks.remove(context.allItems.items.map(function (item) item.item.id));
527 dactyl.echomsg({ message: _("bookmark.deleted", deletedCount) });
534 completer: function completer(context, args)
535 completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }),
536 domains: function (args) array.compact(args.map(util.getHost)),
538 options: [tags, title, keyword],
542 mappings: function () {
543 var myModes = config.browserModes;
545 mappings.add(myModes, ["a"],
546 "Open a prompt to bookmark the current URL",
550 let url = buffer.uri.spec;
551 let bmarks = bookmarks.get(url).filter(function (bmark) bmark.url == url);
553 if (bmarks.length == 1) {
554 let bmark = bmarks[0];
556 options["-title"] = bmark.title;
558 options["-charset"] = bmark.charset;
560 options["-keyword"] = bmark.keyword;
562 options["-post"] = bmark.post;
563 if (bmark.tags.length > 0)
564 options["-tags"] = bmark.tags;
567 if (buffer.title != buffer.uri.spec)
568 options["-title"] = buffer.title;
569 if (content.document.characterSet !== "UTF-8")
570 options["-charset"] = content.document.characterSet;
573 CommandExMode().open(
574 commands.commandToString({ command: "bmark", options: options, arguments: [buffer.uri.spec] }));
577 mappings.add(myModes, ["A"],
578 "Toggle bookmarked state of current URL",
579 function () { bookmarks.toggle(buffer.uri.spec); });
581 options: function () {
582 options.add(["defsearch", "ds"],
583 "The default search engine",
586 completer: function completer(context) {
587 completion.search(context, true);
588 context.completions = [{ keyword: "", title: "Don't perform searches by default" }].concat(context.completions);
592 options.add(["suggestengines"],
593 "Search engines used for search suggestions",
594 "stringlist", "google",
595 { completer: function completer(context) completion.searchEngine(context, true), });
598 completion: function () {
599 completion.bookmark = function bookmark(context, tags, extra) {
600 context.title = ["Bookmark", "Title"];
601 context.format = bookmarks.format;
602 iter(extra || {}).forEach(function ([k, v]) {
604 context.filters.push(function (item) item.item[k] != null && this.matchString(v, item.item[k]));
606 context.generate = function () values(bookmarkcache.bookmarks);
607 completion.urls(context, tags);
610 completion.search = function search(context, noSuggest) {
611 let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/);
612 let keywords = bookmarkcache.keywords;
613 let engines = bookmarks.searchEngines;
615 context.title = ["Search Keywords"];
616 context.completions = iter(values(keywords), values(engines));
617 context.keys = { text: "keyword", description: "title", icon: "icon" };
619 if (!space || noSuggest)
622 context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest",
625 let item = keywords[keyword];
626 if (item && item.url.indexOf("%s") > -1)
627 context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) {
628 context.format = history.format;
629 context.title = [/*L*/keyword + " Quick Search"];
630 // context.background = true;
631 context.compare = CompletionContext.Sort.unsorted;
632 context.generate = function () {
633 let [begin, end] = item.url.split("%s");
635 return history.get({ uri: util.newURI(begin), uriIsPrefix: true }).map(function (item) {
636 let rest = item.url.length - end.length;
637 let query = item.url.substring(begin.length, rest);
638 if (item.url.substr(rest) == end && query.indexOf("&") == -1)
640 item.url = decodeURIComponent(query.replace(/#.*/, "").replace(/\+/g, " "));
645 }).filter(util.identity);
650 completion.searchEngine = function searchEngine(context, suggest) {
651 context.title = ["Suggest Engine", "Description"];
652 context.keys = { text: "keyword", description: "title", icon: "icon" };
653 context.completions = values(bookmarks.searchEngines);
655 context.filters.push(function ({ item }) item.supportsResponseType("application/x-suggestions+json"));
659 completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) {
663 let engineList = (engineAliases || options["suggestengines"].join(",") || "google").split(",");
665 engineList.forEach(function (name) {
666 let engine = bookmarks.searchEngines[name];
669 let [, word] = /^\s*(\S+)/.exec(context.filter) || [];
670 if (!kludge && word == name) // FIXME: Check for matching keywords
672 let ctxt = context.fork(name, 0);
674 ctxt.title = [/*L*/engine.description + " Suggestions"];
675 ctxt.keys = { text: util.identity, description: function () "" };
676 ctxt.compare = CompletionContext.Sort.unsorted;
677 ctxt.filterFunc = null;
679 let words = ctxt.filter.toLowerCase().split(/\s+/g);
680 ctxt.completions = ctxt.completions.filter(function (i) words.every(function (w) i.toLowerCase().indexOf(w) >= 0));
682 ctxt.hasItems = ctxt.completions.length;
683 ctxt.incomplete = true;
684 ctxt.cache.request = bookmarks.getSuggestions(name, ctxt.filter, function (compl) {
685 ctxt.incomplete = false;
686 ctxt.completions = array.uniq(ctxt.completions.filter(function (c) compl.indexOf(c) >= 0)
687 .concat(compl), true);
692 completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest);
693 completion.addUrlCompleter("b", "Bookmarks", completion.bookmark);
694 completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search);
698 // vim: set fdm=marker sw=4 ts=4 et: