b722a2367087d5beb554966c03e6a6fb60f1bb36
[conkeror.git] / modules / index-webjump.js
blobb722a2367087d5beb554966c03e6a6fb60f1bb36
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);
38     if (this.require_completions && ! this.file)
39         throw interactive_error("Index file not defined for " + this.key);
41 index_webjump.prototype = {
42     constructor: index_webjump,
43     toString: function () "#<index_webjump>",
44     mime_type: null,
45     xpath_expr: null,
46     make_completion: null,
47     require_completions: false,
48     completions: null,
49     file_time: 0,
50     tidy_command: null,
52     /* Extract full completion list from index file. */
53     extract_completions: function () {
54         /* Parse the index file. */
55         var stream = Cc["@mozilla.org/network/file-input-stream;1"]
56             .createInstance(Ci.nsIFileInputStream);
57         stream.init(this.file, MODE_RDONLY, 0644, false);
58         var parser = Cc["@mozilla.org/xmlextras/domparser;1"]
59             .createInstance(Ci.nsIDOMParser);
60         // todo: catch parser errors
61         var doc = parser.parseFromStream(stream, null,
62                                          this.file.fileSize, this.mime_type);
64         /* Extract the completion items. */
65         var cmpl = [], node, res;
66         res = doc.evaluate(
67             this.xpath_expr, doc, xpath_lookup_namespace,
68             Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
69         while ((node = res.iterateNext()))
70             cmpl.push(this.make_completion(node));
72         cmpl.sort(function(a, b) {
73             if (a[1] < b[1])  return -1;
74             if (a[1] > b[1])  return 1;
75             if (a[0] < b[0])  return -1;
76             if (a[0] > b[0])  return 1;
77             return 0;
78         });
80         this.completions = cmpl;
81     },
83     /* The guts of the completer. */
84     internal_completer: function (input, pos, conservative) {
85         if (pos == 0 && conservative)
86             yield co_return(undefined);
88         let require = this.require_completions;
90         /* Update full completion list if necessary. */
91         if (require && ! this.file.exists())
92             throw interactive_error("Index file missing for " + this.key);
93         if (this.file.exists() &&
94             this.file.lastModifiedTime > this.file_time)
95         {
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                 {
111                     return false;
112                 }
113             }
114             return true;
115         });
117         let c = { count: data.length,
118                   get_string: function (i) data[i][0],
119                   get_description: function (i) data[i][1],
120                   get_input_state: function (i) [data[i][0]],
121                   get_match_required: function() require
122                 };
123         yield co_return(c);
124     },
126     /* A completer suitable for supplying to define_webjump. */
127     make_completer: function () {
128         if (! this.file)
129             return null;
130         let jmp = this;
131         return function (input, pos, conservative) {
132             return jmp.internal_completer(input, pos, conservative);
133         };
134     },
136     /* Fetch and save the index for later use with completion.
137      * (buffer is used only to associate with the download) */
138     get_index: function (buffer) {
139         if (! this.file)
140             throw interactive_error("Index file not defined for " + this.key);
142         var info = save_uri(load_spec(this.url), this.file,
143                             $buffer = buffer, $use_cache = false,
144                             $temp_file = true);
146         // Note: it would be better to run this before the temp file
147         // is renamed; that requires support in save_uri.
148         if (this.tidy_command)
149             info.set_shell_command(this.tidy_command, index_webjumps_directory);
150     },
152     /* Try to make a suitable file object when the supplied file is a
153      * string or null. */
154     canonicalize_file: function (file) {
155         if (typeof file == 'string')
156             file = make_file(file);
157         if (! file && index_webjumps_directory) {
158             file = Cc["@mozilla.org/file/local;1"]
159                 .createInstance(Ci.nsILocalFile);
160             file.initWithFile(index_webjumps_directory);
161             file.appendRelativePath(this.key + ".index");
162         }
163         return file;
164     }
168 function index_webjump_xhtml (key, url, file, xpath_expr) {
169     index_webjump.call(this, key, url, file);
170     this.xpath_expr = xpath_expr;
172 index_webjump_xhtml.prototype = {
173     constructor: index_webjump_xhtml,
174     __proto__: index_webjump.prototype,
175     toString: function () "#<index_webjump_xhtml>",
176     require_completions: true,
177     mime_type: "application/xhtml+xml",
178     tidy_command: index_xpath_webjump_tidy_command,
180     make_completion: function (node) {
181         return [makeURLAbsolute(this.url, node.href), node.text];
182     },
184     make_handler: function () {
185         let jmp = this;
186         return function (term) {
187             if (!(jmp.completions && jmp.completions.length))
188                 throw interactive_error("Completions required for " + this.key);
189             return term;
190         };
191     }
195 function index_webjump_gitweb (key, url, file) {
196     index_webjump.call(this, key, url, file);
198 index_webjump_gitweb.prototype = {
199     constructor: index_webjump_gitweb,
200     __proto__: index_webjump.prototype,
201     toString: function () "#<index_webjump_gitweb>",
202     mime_type: "text/xml",
203     xpath_expr: '//outline[@type="rss"]',
204     make_completion: function (node) {
205         var name = node.getAttribute("text");
206         return [name.replace(/\.git$/, ""), ""];
207     }
211 interactive("webjump-get-index",
212     "Fetch and save the index URL corresponding to an index " +
213     "webjump.  It will then be available to the completer.",
214     function (I) {
215         var completions = [];
216         for (let i in index_webjumps)
217             completions.push(i);
218         completions.sort();
219         var key = yield I.minibuffer.read(
220             $prompt = "Fetch index for index webjump:",
221             $history = "webjump",
222             $completer = all_word_completer(
223                 $completions = completions),
224             $match_required = true);
225         var jmp = index_webjumps[key];
226         if (jmp)
227             jmp.get_index(I.buffer);
228     });
231  * Construct a webjump to visit URLs referenced from an index page.
233  * The index page must be able to be parsed as xhtml.  The anchor
234  * nodes indexed are those that match the given xpath_expr.  Don't
235  * forget to use xhtml: prefixes on the xpath steps.
237  * If an alternative is not specified then it is set to the index page.
239  * A completer is provided that uses the index page.  A local file for
240  * the index must be specified either with $index_file or via
241  * index_webjumps_directory.  The index must be manually downloaded;
242  * eg. using webjump-get-index.  Each time the completer is used it
243  * will check if the file has been updated and reload if necessary.
244  * This kind of webjump is not useful without the completions.
245  */
246 define_keywords("$alternative", "$index_file", "$description");
247 function define_xpath_webjump (key, index_url, xpath_expr) {
248     keywords(arguments);
249     let alternative = arguments.$alternative || index_url;
250     var jmp = new index_webjump_xhtml(key, index_url, arguments.$index_file,
251                                       xpath_expr);
252     index_webjumps[key] = jmp;
253     define_webjump(key, jmp.make_handler(),
254                    $completer = jmp.make_completer(),
255                    $alternative = alternative,
256                    $description = arguments.$description);
260  * Modify the xpath for an index webjump and show the resulting
261  * completions.  Useful for figuring out an appropriate xpath.  Either
262  * run using mozrepl or eval in the browser with the dump parameter
263  * set.
264  */
265 function index_webjump_try_xpath (key, xpath_expr, dump) {
266     jmp = index_webjumps[key];
267     if (xpath_expr)
268         jmp.xpath_expr = xpath_expr;
269     jmp.extract_completions();
270     if (dump)
271         dumpln(dump_obj(jmp.completions,
272                         "Completions for index webjump " + key));
273     return jmp.completions;
278  * Construct a webjump to visit repository summary pages at a gitweb
279  * server.
281  * If a repository name is supplied as $default then the alternative
282  * url is set to that repository at the gitweb site.  If an
283  * alternative is not specified by either $default or $alternative
284  * then it is set to the repository list page of the gitweb site.
286  * A completer is provided that uses the list of repositories from the
287  * OPML data on the gitweb server.  The completer is setup in the same
288  * way as for define_xpath_webjump, but the webjump will work without
289  * the completions.
290  */
291 define_keywords("$default", "$alternative", "$opml_file", "$description");
292 function define_gitweb_summary_webjump (key, base_url) {
293     keywords(arguments);
294     let alternative = arguments.$alternative;
295     let gitweb_url = base_url + "/gitweb.cgi";
296     let summary_url = gitweb_url + "?p=%s.git;a=summary";
297     let opml_url = gitweb_url + "?a=opml";
299     if (arguments.$default)
300         alternative = summary_url.replace("%s", arguments.$default);
301     if (! alternative)
302         alternative = gitweb_url;
304     var jmp = new index_webjump_gitweb(key, opml_url, arguments.$opml_file);
305     index_webjumps[key] = jmp;
307     define_webjump(key, summary_url,
308                    $completer = jmp.make_completer(),
309                    $alternative = alternative,
310                    $description = arguments.$description);
313 provide("index-webjump");