index-webjump: New module to define webjumps for index pages.
[conkeror.git] / modules / index-webjump.js
blob1c402f451a19b8b0840d6343ff34696b5f907b28
1 /**
2  * (C) Copyright 2009 David Kettler
3  *
4  * Use, modification, and distribution are subject to the terms specified in the
5  * COPYING file.
6  *
7  * Construct a webjump (with completer) to visit URLs referenced from
8  * an index page.  An xpath expression is used to extract the indexed
9  * URLs.  A specialized form is also provided for gitweb summary
10  * pages.
11 **/
13 require("webjump.js");
15 /* Objects with completion data for index webjumps. */
16 index_webjumps = {};
18 define_variable("index_webjumps_directory", null,
19                 "A directory (instance of nsILocalFile) for storing the " +
20                 "index files corresponding to index webjumps; the index " +
21                 "data can be downloaded from the index URL using " +
22                 "webjump-get-index.  " +
23                 "If the index file is available for an index webjump then " +
24                 "the webjump will provide completions for the indexed URLs.");
26 define_variable("index_xpath_webjump_tidy_command",
27                 "tidy -asxhtml -wrap 0  -numeric --clean yes" +
28                 " -modify -quiet --show-warnings no",
29                 "A command to run on the downloaded index.  The xulrunner " +
30                 "parser is quite fussy and specifically requires xhtml (or " +
31                 "other xml).  Running something like html tidy can avoid " +
32                 "parser problems.");
34 function index_webjump(key, url, file) {
35     this.key = key;
36     this.url = url;
37     this.file = this.canonicalize_file(file);
39     if (this.require_completions && !this.file)
40         throw interactive_error("Index file not defined for " + this.key);
42 index_webjump.prototype = {
43     constructor : index_webjump,
45     mime_type : null,
46     xpath_expr : null,
47     make_completion : null,
48     require_completions : false,
49     completions : null,
50     file_time : 0,
51     tidy_command : null,
53     /* Extract full completion list from index file. */
54     extract_completions : function () {
55         /* Parse the index file. */
56         var stream = Cc["@mozilla.org/network/file-input-stream;1"]
57             .createInstance(Ci.nsIFileInputStream);
58         stream.init(this.file, MODE_RDONLY, 0644, false);
59         var parser = Cc["@mozilla.org/xmlextras/domparser;1"]
60             .createInstance(Ci.nsIDOMParser);
61         // todo: catch parser errors
62         var doc = parser.parseFromStream(stream, null,
63                                          this.file.fileSize, this.mime_type);
65         /* Extract the completion items. */
66         var cmpl = [], node, res;
67         res = doc.evaluate(
68             this.xpath_expr, doc, xpath_lookup_namespace,
69             Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
70         while ((node = res.iterateNext()))
71             cmpl.push(this.make_completion(node));
73         cmpl.sort(function(a, b) {
74             if (a[1] < b[1])  return -1;
75             if (a[1] > b[1])  return 1;
76             if (a[0] < b[0])  return -1;
77             if (a[0] > b[0])  return 1;
78             return 0;
79         });
81         this.completions = cmpl;
82     },
84     /* The guts of the completer. */
85     internal_completer : function (input, pos, conservative) {
86         if (pos == 0 && conservative)
87             yield co_return(undefined);
89         let require = this.require_completions;
91         /* Update full completion list if necessary. */
92         if (require && !this.file.exists())
93             throw interactive_error("Index file missing for " + this.key);
94         if (this.file.exists() &&
95             this.file.lastModifiedTime > this.file_time) {
96             this.file_time = this.file.lastModifiedTime;
97             this.extract_completions();
98         }
99         if (require && !(this.completions && this.completions.length))
100             throw interactive_error("No completions for " + this.key);
101         if (!this.completions)
102             yield co_return(null);
104         /* Match completions against input. */
105         let words = trim_whitespace(input.toLowerCase()).split(/\s+/);
106         let data = this.completions.filter(function (x) {
107             for (var i = 0; i < words.length; ++i)
108                 if (x[0].toLowerCase().indexOf(words[i]) == -1 &&
109                     x[1].toLowerCase().indexOf(words[i]) == -1)
110                     return false;
111             return true;
112         });
114         let c = { count: data.length,
115                   get_string: function (i) data[i][0],
116                   get_description: function (i) data[i][1],
117                   get_input_state: function (i) [data[i][0]],
118                   get_match_required: function() require
119                 };
120         yield co_return(c);
121     },
123     /* A completer suitable for supplying to define_webjump. */
124     make_completer : function() {
125         if (!this.file)
126             return null;
127         let jmp = this;
128         return function (input, pos, conservative) {
129             return jmp.internal_completer(input, pos, conservative);
130         };
131     },
133     /* Fetch and save the index for later use with completion.
134      * (buffer is used only to associate with the download) */
135     get_index : function (buffer) {
136         if (!this.file)
137             throw interactive_error("Index file not defined for " + this.key);
139         var info = save_uri(load_spec(this.url), this.file,
140                             $buffer = buffer, $use_cache = false,
141                             $temp_file = true);
143         // Note: it would be better to run this before the temp file
144         // is renamed; that requires support in save_uri.
145         if (this.tidy_command)
146             info.set_shell_command(this.tidy_command, index_webjumps_directory);
147     },
149     /* Try to make a suitable file object when the supplied file is a
150      * string or null. */
151     canonicalize_file : function (file) {
152         if (typeof file == 'string')
153             file = make_file(file);
154         if (!file && index_webjumps_directory) {
155             file = Cc["@mozilla.org/file/local;1"]
156                 .createInstance(Ci.nsILocalFile);
157             file.initWithFile(index_webjumps_directory);
158             file.appendRelativePath(this.key + ".index");
159         }
160         return file;
161     }
165 function index_webjump_xhtml(key, url, file, xpath_expr) {
166     index_webjump.call(this, key, url, file);
167     this.xpath_expr = xpath_expr;
169 index_webjump_xhtml.prototype = {
170     constructor : index_webjump_xhtml,
172     require_completions : true,
173     mime_type : "application/xhtml+xml",
174     tidy_command : index_xpath_webjump_tidy_command,
176     make_completion : function (node) {
177         return [makeURLAbsolute(this.url, node.href), node.text];
178     },
180     __proto__ : index_webjump.prototype
184 function index_webjump_gitweb(key, url, file) {
185     index_webjump.call(this, key, url, file);
187 index_webjump_gitweb.prototype = {
188     constructor : index_webjump_gitweb,
190     mime_type : "text/xml",
191     xpath_expr : '//outline[@type="rss"]',
193     make_completion : function (node) {
194         var name = node.getAttribute("text");
195         return [name.replace(/\.git$/, ""), ""];
196     },
198     __proto__ : index_webjump.prototype
202 interactive("webjump-get-index",
203             "Fetch and save the index URL corresponding to an index " +
204             "webjump.  It will then be available to the completer.",
205             function (I) {
206                 var completions = [];
207                 for (let i in index_webjumps)
208                     completions.push(i);
209                 completions.sort();
211                 var key = yield I.minibuffer.read(
212                     $prompt = "Fetch index for index webjump:",
213                     $history = "webjump",
214                     $completer =
215                         all_word_completer($completions = completions),
216                     $match_required = true);
218                 var jmp = index_webjumps[key];
219                 if (jmp)
220                     jmp.get_index(I.buffer);
221             });
224  * Construct a webjump to visit URLs referenced from an index page.
226  * The index page must be able to be parsed as xhtml.  The anchor
227  * nodes indexed are those that match the given xpath_expr.  Don't
228  * forget to use xhtml: prefixes on the xpath steps.
230  * If an alternative is not specified then it is set to the index page.
232  * A completer is provided that uses the index page.  A local file for
233  * the index must be specified either with $index_file or via
234  * index_webjumps_directory.  The index must be manually downloaded;
235  * eg. using webjump-get-index.  Each time the completer is used it
236  * will check if the file has been updated and reload if necessary.
237  * This kind of webjump is not useful without the completions.
238  */
239 define_keywords("$alternative", "$index_file", "$description");
240 function define_xpath_webjump(key, index_url, xpath_expr) {
241     keywords(arguments);
242     let alternative = arguments.$alternative || index_url;
244     var jmp = new index_webjump_xhtml(key, index_url, arguments.$index_file,
245                                       xpath_expr);
246     index_webjumps[key] = jmp;
248     define_webjump(key, function (term) {return term;},
249                    $completer = jmp.make_completer(),
250                    $alternative = alternative,
251                    $description = arguments.$description);
255  * Modify the xpath for an index webjump and show the resulting
256  * completions.  Useful for figuring out an appropriate xpath.  Either
257  * run using mozrepl or eval in the browser with the dump parameter
258  * set.
259  */
260 function index_webjump_try_xpath(key, xpath_expr, dump) {
261     jmp = index_webjumps[key];
262     if (xpath_expr)
263         jmp.xpath_expr = xpath_expr;
264     jmp.extract_completions();
265     if (dump)
266         dumpln(dump_obj(jmp.completions,
267                         "Completions for index webjump " + key));
268     return jmp.completions;
273  * Construct a webjump to visit repository summary pages at a gitweb
274  * server.
276  * If a repository name is supplied as $default then the alternative
277  * url is set to that repository at the gitweb site.  If an
278  * alternative is not specified by either $default or $alternative
279  * then it is set to the repository list page of the gitweb site.
281  * A completer is provided that uses the list of repositories from the
282  * OPML data on the gitweb server.  The completer is setup in the same
283  * way as for define_xpath_webjump, but the webjump will work without
284  * the completions.
285  */
286 define_keywords("$default", "$alternative", "$opml_file", "$description");
287 function define_gitweb_summary_webjump(key, base_url) {
288     keywords(arguments);
289     let alternative = arguments.$alternative;
290     let gitweb_url = base_url + "/gitweb.cgi";
291     let summary_url = gitweb_url + "?p=%s.git;a=summary";
292     let opml_url = gitweb_url + "?a=opml";
294     if (arguments.$default)
295         alternative = summary_url.replace("%s", arguments.$default);
296     if (!alternative)
297         alternative = gitweb_url;
299     var jmp = new index_webjump_gitweb(key, opml_url, arguments.$opml_file);
300     index_webjumps[key] = jmp;
302     define_webjump(key, summary_url,
303                    $completer = jmp.make_completer(),
304                    $alternative = alternative,
305                    $description = arguments.$description);