]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/help.jsm
Import r6948 from upstream hg supporting Firefox up to 24.*
[dactyl.git] / common / modules / help.jsm
1 // Copyright (c) 2008-2012 Kris Maglione <maglione.k@gmail.com>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 defineModule("help", {
8     exports: ["help"],
9     require: ["cache", "dom", "protocol", "services", "util"]
10 });
11
12 lazyRequire("completion", ["completion"]);
13 lazyRequire("overlay", ["overlay"]);
14 lazyRequire("template", ["template"]);
15
16 var HelpBuilder = Class("HelpBuilder", {
17     init: function init() {
18         try {
19             // The versions munger will need to access the tag map
20             // during this process and without this we'll get an
21             // infinite loop.
22             help._data = this;
23
24             this.files = {};
25             this.tags = {};
26             this.overlays = {};
27
28             // Scrape the list of help files from all.xml
29             this.tags["all"] = this.tags["all.xml"] = "all";
30             let files = this.findHelpFile("all").map(function (doc)
31                     [f.value for (f in DOM.XPath("//dactyl:include/@href", doc))]);
32
33             // Scrape the tags from the rest of the help files.
34             array.flatten(files).forEach(function (file) {
35                 this.tags[file + ".xml"] = file;
36                 this.findHelpFile(file).forEach(function (doc) {
37                     this.addTags(file, doc);
38                 }, this);
39             }, this);
40         }
41         finally {
42             delete help._data;
43         }
44     },
45
46     toJSON: function toJSON() ({
47         files: this.files,
48         overlays: this.overlays,
49         tags: this.tags
50     }),
51
52     // Find the tags in the document.
53     addTags: function addTags(file, doc) {
54         for (let elem in DOM.XPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc))
55                 for (let tag in values((elem.value || elem.textContent).split(/\s+/)))
56             this.tags[tag] = file;
57     },
58
59     bases: ["dactyl://locale-local/", "dactyl://locale/", "dactyl://cache/help/"],
60
61     // Find help and overlay files with the given name.
62     findHelpFile: function findHelpFile(file) {
63         let result = [];
64         for (let base in values(this.bases)) {
65             let url = [base, file, ".xml"].join("");
66             let res = util.httpGet(url, { quiet: true });
67             if (res) {
68                 if (res.responseXML.documentElement.localName == "document")
69                     this.files[file] = url;
70                 if (res.responseXML.documentElement.localName == "overlay")
71                     this.overlays[file] = url;
72                 result.push(res.responseXML);
73             }
74         }
75         return result;
76     }
77 });
78
79 var Help = Module("Help", {
80     init: function init() {
81         this.initialized = false;
82
83         function Loop(fn)
84             function (uri, path) {
85                 if (!help.initialized)
86                     return RedirectChannel(uri.spec, uri, 2,
87                                            "Initializing. Please wait...");
88
89                 return fn.apply(this, arguments);
90             }
91
92         update(services["dactyl:"].providers, {
93             "help": Loop(function (uri, path) help.files[path]),
94             "help-overlay": Loop(function (uri, path) help.overlays[path]),
95             "help-tag": Loop(function (uri, path) {
96                 let tag = decodeURIComponent(path);
97                 if (tag in help.files)
98                     return RedirectChannel("dactyl://help/" + tag, uri);
99                 if (tag in help.tags)
100                     return RedirectChannel("dactyl://help/" + help.tags[tag] + "#" + tag.replace(/#/g, encodeURIComponent), uri);
101             })
102         });
103
104         cache.register("help.json", HelpBuilder);
105
106         cache.register("help/versions.xml", function () {
107             let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec,
108                                     { mimeType: "text/plain;charset=UTF-8" })
109                            .responseText;
110
111             let re = util.regexp(UTF8(literal(/*
112                   ^ (?P<comment> \s* # .*\n)
113
114                 | ^ (?P<space> \s*)
115                     (?P<char>  [-•*+]) \ //
116                   (?P<content> .*\n
117                      (?: \2\ \ .*\n | \s*\n)* )
118
119                 | (?P<par>
120                       (?: ^ [^\S\n]*
121                           (?:[^-•*+\s] | [-•*+]\S)
122                           .*\n
123                       )+
124                   )
125
126                 | (?: ^ [^\S\n]* \n) +
127             */)), "gmxy");
128
129             let betas = util.regexp(/\[((?:b|rc)\d)\]/, "gx");
130
131             let beta = array(betas.iterate(NEWS))
132                         .map(function (m) m[1]).uniq().slice(-1)[0];
133
134             function rec(text, level, li) {
135                 let res = [];
136                 let list, space, i = 0;
137
138                 for (let match in re.iterate(text)) {
139                     if (match.comment)
140                         continue;
141                     else if (match.char) {
142                         if (!list)
143                             res.push(list = ["ul", {}]);
144                         let li = ["li", {}];
145                         li.push(rec(match.content
146                                          .replace(RegExp("^" + match.space, "gm"), ""),
147                                     level + 1,
148                                     li));
149                         list.push(li);
150                     }
151                     else if (match.par) {
152                         let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
153                         let t = tags;
154                         tags = array(betas.iterate(tags)).map(function (m) m[1]);
155
156                         let group = !tags.length                       ? "" :
157                                     !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew";
158                         if (i === 0 && li) {
159                             li[1]["dactyl:highlight"] = group;
160                             group = "";
161                         }
162
163                         list = null;
164                         if (level == 0 && /^.*:\n$/.test(match.par)) {
165                             let text = par.slice(0, -1);
166                             res.push(["h2", { tag: "news-" + text },
167                                           template.linkifyHelp(text, true)]);
168                         }
169                         else {
170                             let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
171
172                             res.push(["p", { "dactyl:highlight": group + " HelpNews" },
173                                 !tags.length ? "" : ["hl", { key: "HelpNewsTag" }, tags.join(" ")],
174                                 a ? ["hl", { key: "HelpWarning" }, a] : "",
175                                 template.linkifyHelp(b, true)]);
176                         }
177                     }
178                     i++;
179                 }
180
181                 return res;
182             }
183
184             let body = rec(NEWS, 0);
185
186             // E4X-FIXME
187             // for each (let li in body..li) {
188             //     let list = li..li.(@NS::highlight == "HelpNewsOld");
189             //     if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) {
190             //         for each (let li in list)
191             //             li.@NS::highlight = "";
192             //         li.@NS::highlight = "HelpNewsOld";
193             //     }
194             // }
195
196             return '<?xml version="1.0"?>\n' +
197                    '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
198                    DOM.toXML(["document", { xmlns: "dactyl", name: "versions",
199                                   title: config.appName + " Versions" },
200                        ["h1", { tag: "versions news NEWS" }, config.appName + " Versions"],
201                        ["toc", { start: "2" }],
202
203                        body]);
204         });
205     },
206
207     initialize: function initialize() {
208         help.initialized = true;
209     },
210
211     flush: function flush(entries, time) {
212         cache.flushEntry("help.json", time);
213
214         for (let entry in values(Array.concat(entries || [])))
215             cache.flushEntry(entry, time);
216     },
217
218     get data() this._data || cache.get("help.json"),
219
220     get files() this.data.files,
221     get overlays() this.data.overlays,
222     get tags() this.data.tags,
223
224     Local: function Local(dactyl, modules, window) ({
225         init: function init() {
226             dactyl.commands["dactyl.help"] = function (event) {
227                 let elem = event.originalTarget;
228                 modules.help.help(elem.getAttribute("tag") || elem.textContent);
229             };
230         },
231
232         /**
233          * Returns the URL of the specified help *topic* if it exists.
234          *
235          * @param {string} topic The help topic to look up.
236          * @param {boolean} consolidated Whether to search the consolidated help page.
237          * @returns {string}
238          */
239         findHelp: function (topic, consolidated) {
240             if (!consolidated && Set.has(help.files, topic))
241                 return topic;
242             let items = modules.completion._runCompleter("help", topic, null, !!consolidated).items;
243             let partialMatch = null;
244
245             function format(item) item.description + "#" + encodeURIComponent(item.text);
246
247             for (let [i, item] in Iterator(items)) {
248                 if (item.text == topic)
249                     return format(item);
250                 else if (!partialMatch && topic)
251                     partialMatch = item;
252             }
253
254             if (partialMatch)
255                 return format(partialMatch);
256             return null;
257         },
258
259         /**
260          * Opens the help page containing the specified *topic* if it exists.
261          *
262          * @param {string} topic The help topic to open.
263          * @param {boolean} consolidated Whether to use the consolidated help page.
264          */
265         help: function (topic, consolidated) {
266             dactyl.initHelp();
267
268             if (!topic) {
269                 let helpFile = consolidated ? "all" : modules.options["helpfile"];
270
271                 if (Set.has(help.files, helpFile))
272                     dactyl.open("dactyl://help/" + helpFile, { from: "help" });
273                 else
274                     dactyl.echomsg(_("help.noFile", helpFile.quote()));
275                 return;
276             }
277
278             let page = this.findHelp(topic, consolidated);
279             dactyl.assert(page != null, _("help.noTopic", topic));
280
281             dactyl.open("dactyl://help/" + page, { from: "help" });
282         },
283
284         exportHelp: function (path) {
285             const FILE = io.File(path);
286             const PATH = FILE.leafName.replace(/\..*/, "") + "/";
287             const TIME = Date.now();
288
289             if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
290                 FILE.create(FILE.DIRECTORY_TYPE, octal(755));
291
292             dactyl.initHelp();
293             if (FILE.isDirectory()) {
294                 var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data);
295                 var addURIEntry  = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText);
296             }
297             else {
298                 var zip = services.ZipWriter(FILE.file, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
299
300                 addURIEntry = function addURIEntry(file, uri)
301                     zip.addEntryChannel(PATH + file, TIME, 9,
302                         services.io.newChannel(uri, null, null), false);
303                 addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
304                     addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
305             }
306
307             let empty = Set("area base basefont br col frame hr img input isindex link meta param"
308                                 .split(" "));
309             function fix(node) {
310                 switch(node.nodeType) {
311                 case Ci.nsIDOMNode.ELEMENT_NODE:
312                     if (isinstance(node, [Ci.nsIDOMHTMLBaseElement]))
313                         return;
314
315                     data.push("<", node.localName);
316                     if (node instanceof Ci.nsIDOMHTMLHtmlElement)
317                         data.push(" xmlns=" + XHTML.quote(),
318                                   " xmlns:dactyl=" + NS.quote());
319
320                     for (let { name, value } in array.iterValues(node.attributes)) {
321                         if (name == "dactyl:highlight") {
322                             Set.add(styles, value);
323                             name = "class";
324                             value = "hl-" + value;
325                         }
326                         if (name == "href") {
327                             value = node.href || value;
328                             if (value.indexOf("dactyl://help-tag/") == 0) {
329                                 try {
330                                     let uri = services.io.newChannel(value, null, null).originalURI;
331                                     value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
332                                 }
333                                 catch (e) {
334                                     util.dump("Magical tag thingy failure for: " + value);
335                                     dactyl.reportError(e);
336                                 }
337                             }
338                             if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
339                                 value = value.replace(/(#|$)/, ".xhtml$1");
340                         }
341                         if (name == "src" && value.indexOf(":") > 0) {
342                             chromeFiles[value] = value.replace(/.*\//, "");
343                             value = value.replace(/.*\//, "");
344                         }
345
346                         data.push(" ", name, '="', DOM.escapeHTML(value), '"');
347                     }
348                     if (node.localName in empty)
349                         data.push(" />");
350                     else {
351                         data.push(">");
352                         if (node instanceof Ci.nsIDOMHTMLHeadElement)
353                             data.push('<link rel="stylesheet" type="text/css" href="help.css"/>');
354                         Array.map(node.childNodes, fix);
355                         data.push("</", node.localName, ">");
356                     }
357                     break;
358                 case Ci.nsIDOMNode.TEXT_NODE:
359                     data.push(DOM.escapeHTML(node.textContent, true));
360                 }
361             }
362
363             let { buffer, content, events } = modules;
364             let chromeFiles = {};
365             let styles = {};
366
367             for (let [file, ] in Iterator(help.files)) {
368                 let url = "dactyl://help/" + file;
369                 dactyl.open(url);
370                 util.waitFor(function () content.location.href == url && buffer.loaded
371                                 && content.document.documentElement instanceof Ci.nsIDOMHTMLHtmlElement,
372                              15000);
373                 events.waitForPageLoad();
374                 var data = [
375                     '<?xml version="1.0" encoding="UTF-8"?>\n',
376                     '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
377                     '          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
378                 ];
379                 fix(content.document.documentElement);
380                 addDataEntry(file + ".xhtml", data.join(""));
381             }
382
383             let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
384                 .map(function (h) h.selector
385                                    .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
386                                    .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
387                 .join("\n");
388             addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
389
390             addDataEntry("tag-map.json", JSON.stringify(help.tags));
391
392             let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
393             while ((m = re.exec(data)))
394                 chromeFiles[m[0]] = m[2];
395
396             for (let [uri, leaf] in Iterator(chromeFiles))
397                 addURIEntry(leaf, uri);
398
399             if (zip)
400                 zip.close();
401         }
402
403     })
404 }, {
405 }, {
406     commands: function initCommands(dactyl, modules, window) {
407         const { commands, completion, help } = modules;
408
409         [
410             {
411                 name: "h[elp]",
412                 description: "Open the introductory help page"
413             }, {
414                 name: "helpa[ll]",
415                 description: "Open the single consolidated help page"
416             }
417         ].forEach(function (command) {
418             let consolidated = command.name == "helpa[ll]";
419
420             commands.add([command.name],
421                 command.description,
422                 function (args) {
423                     dactyl.assert(!args.bang, _("help.dontPanic"));
424                     help.help(args.literalArg, consolidated);
425                 }, {
426                     argCount: "?",
427                     bang: true,
428                     completer: function (context) completion.help(context, consolidated),
429                     literal: 0
430                 });
431         });
432     },
433     completion: function initCompletion(dactyl, modules, window) {
434         const { completion } = modules;
435
436         completion.help = function completion_help(context, consolidated) {
437             dactyl.initHelp();
438             context.title = ["Help"];
439             context.anchored = false;
440             context.completions = help.tags;
441             if (consolidated)
442                 context.keys = { text: 0, description: function () "all" };
443         };
444     },
445     javascript: function initJavascript(dactyl, modules, window) {
446         modules.JavaScript.setCompleter([modules.help.exportHelp],
447             [function (context, args) overlay.activeModules.completion.file(context)]);
448     },
449     options: function initOptions(dactyl, modules, window) {
450         const { options } = modules;
451
452         options.add(["helpfile", "hf"],
453             "Name of the main help file",
454             "string", "intro");
455     }
456 });
457
458 endModule();
459
460 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: