]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/help.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[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
135             function rec(text, level, li) {
136                 let res = [];
137                 let list, space, i = 0;
138
139
140                 for (let match in re.iterate(text)) {
141                     if (match.comment)
142                         continue;
143                     else if (match.char) {
144                         if (!list)
145                             res.push(list = ["ul", {}]);
146                         let li = ["li", {}];
147                         li.push(rec(match.content
148                                          .replace(RegExp("^" + match.space, "gm"), ""),
149                                     level + 1,
150                                     li));
151                         list.push(li);
152                     }
153                     else if (match.par) {
154                         let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
155                         let t = tags;
156                         tags = array(betas.iterate(tags)).map(function (m) m[1]);
157
158                         let group = !tags.length                       ? "" :
159                                     !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew";
160                         if (i === 0 && li) {
161                             li[1]["dactyl:highlight"] = group;
162                             group = "";
163                         }
164
165                         list = null;
166                         if (level == 0 && /^.*:\n$/.test(match.par)) {
167                             let text = par.slice(0, -1);
168                             res.push(["h2", { tag: "news-" + text },
169                                           template.linkifyHelp(text, true)]);
170                         }
171                         else {
172                             let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
173
174                             res.push(["p", { "dactyl:highlight": group + " HelpNews" },
175                                 !tags.length ? "" : ["hl", { key: "HelpNewsTag" }, tags.join(" ")],
176                                 a ? ["hl", { key: "HelpWarning" }, a] : "",
177                                 template.linkifyHelp(b, true)]);
178                         }
179                     }
180                     i++;
181                 }
182
183                 return res;
184             }
185
186             let body = rec(NEWS, 0);
187
188             // E4X-FIXME
189             // for each (let li in body..li) {
190             //     let list = li..li.(@NS::highlight == "HelpNewsOld");
191             //     if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) {
192             //         for each (let li in list)
193             //             li.@NS::highlight = "";
194             //         li.@NS::highlight = "HelpNewsOld";
195             //     }
196             // }
197
198
199             return '<?xml version="1.0"?>\n' +
200                    '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
201                    DOM.toXML(["document", { xmlns: "dactyl", name: "versions",
202                                   title: config.appName + " Versions" },
203                        ["h1", { tag: "versions news NEWS" }, config.appName + " Versions"],
204                        ["toc", { start: "2" }],
205
206                        body]);
207         });
208     },
209
210     initialize: function initialize() {
211         help.initialized = true;
212     },
213
214     flush: function flush(entries, time) {
215         cache.flushEntry("help.json", time);
216
217         for (let entry in values(Array.concat(entries || [])))
218             cache.flushEntry(entry, time);
219     },
220
221     get data() this._data || cache.get("help.json"),
222
223     get files() this.data.files,
224     get overlays() this.data.overlays,
225     get tags() this.data.tags,
226
227     Local: function Local(dactyl, modules, window) ({
228         init: function init() {
229             dactyl.commands["dactyl.help"] = function (event) {
230                 let elem = event.originalTarget;
231                 modules.help.help(elem.getAttribute("tag") || elem.textContent);
232             };
233         },
234
235         /**
236          * Returns the URL of the specified help *topic* if it exists.
237          *
238          * @param {string} topic The help topic to look up.
239          * @param {boolean} consolidated Whether to search the consolidated help page.
240          * @returns {string}
241          */
242         findHelp: function (topic, consolidated) {
243             if (!consolidated && Set.has(help.files, topic))
244                 return topic;
245             let items = modules.completion._runCompleter("help", topic, null, !!consolidated).items;
246             let partialMatch = null;
247
248             function format(item) item.description + "#" + encodeURIComponent(item.text);
249
250             for (let [i, item] in Iterator(items)) {
251                 if (item.text == topic)
252                     return format(item);
253                 else if (!partialMatch && topic)
254                     partialMatch = item;
255             }
256
257             if (partialMatch)
258                 return format(partialMatch);
259             return null;
260         },
261
262         /**
263          * Opens the help page containing the specified *topic* if it exists.
264          *
265          * @param {string} topic The help topic to open.
266          * @param {boolean} consolidated Whether to use the consolidated help page.
267          */
268         help: function (topic, consolidated) {
269             dactyl.initHelp();
270
271             if (!topic) {
272                 let helpFile = consolidated ? "all" : modules.options["helpfile"];
273
274                 if (Set.has(help.files, helpFile))
275                     dactyl.open("dactyl://help/" + helpFile, { from: "help" });
276                 else
277                     dactyl.echomsg(_("help.noFile", helpFile.quote()));
278                 return;
279             }
280
281             let page = this.findHelp(topic, consolidated);
282             dactyl.assert(page != null, _("help.noTopic", topic));
283
284             dactyl.open("dactyl://help/" + page, { from: "help" });
285         },
286
287         exportHelp: function (path) {
288             const FILE = io.File(path);
289             const PATH = FILE.leafName.replace(/\..*/, "") + "/";
290             const TIME = Date.now();
291
292             if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
293                 FILE.create(FILE.DIRECTORY_TYPE, octal(755));
294
295             dactyl.initHelp();
296             if (FILE.isDirectory()) {
297                 var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data);
298                 var addURIEntry  = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText);
299             }
300             else {
301                 var zip = services.ZipWriter(FILE.file, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
302
303                 addURIEntry = function addURIEntry(file, uri)
304                     zip.addEntryChannel(PATH + file, TIME, 9,
305                         services.io.newChannel(uri, null, null), false);
306                 addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
307                     addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
308             }
309
310             let empty = Set("area base basefont br col frame hr img input isindex link meta param"
311                                 .split(" "));
312             function fix(node) {
313                 switch(node.nodeType) {
314                 case Ci.nsIDOMNode.ELEMENT_NODE:
315                     if (isinstance(node, [Ci.nsIDOMHTMLBaseElement]))
316                         return;
317
318                     data.push("<", node.localName);
319                     if (node instanceof Ci.nsIDOMHTMLHtmlElement)
320                         data.push(" xmlns=" + XHTML.quote(),
321                                   " xmlns:dactyl=" + NS.quote());
322
323                     for (let { name, value } in array.iterValues(node.attributes)) {
324                         if (name == "dactyl:highlight") {
325                             Set.add(styles, value);
326                             name = "class";
327                             value = "hl-" + value;
328                         }
329                         if (name == "href") {
330                             value = node.href || value;
331                             if (value.indexOf("dactyl://help-tag/") == 0) {
332                                 try {
333                                     let uri = services.io.newChannel(value, null, null).originalURI;
334                                     value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
335                                 }
336                                 catch (e) {
337                                     util.dump("Magical tag thingy failure for: " + value);
338                                     dactyl.reportError(e);
339                                 }
340                             }
341                             if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
342                                 value = value.replace(/(#|$)/, ".xhtml$1");
343                         }
344                         if (name == "src" && value.indexOf(":") > 0) {
345                             chromeFiles[value] = value.replace(/.*\//, "");
346                             value = value.replace(/.*\//, "");
347                         }
348
349                         data.push(" ", name, '="', DOM.escapeHTML(value), '"');
350                     }
351                     if (node.localName in empty)
352                         data.push(" />");
353                     else {
354                         data.push(">");
355                         if (node instanceof Ci.nsIDOMHTMLHeadElement)
356                             data.push('<link rel="stylesheet" type="text/css" href="help.css"/>');
357                         Array.map(node.childNodes, fix);
358                         data.push("</", node.localName, ">");
359                     }
360                     break;
361                 case Ci.nsIDOMNode.TEXT_NODE:
362                     data.push(DOM.escapeHTML(node.textContent, true));
363                 }
364             }
365
366             let { buffer, content, events } = modules;
367             let chromeFiles = {};
368             let styles = {};
369
370             for (let [file, ] in Iterator(help.files)) {
371                 let url = "dactyl://help/" + file;
372                 dactyl.open(url);
373                 util.waitFor(function () content.location.href == url && buffer.loaded
374                                 && content.document.documentElement instanceof Ci.nsIDOMHTMLHtmlElement,
375                              15000);
376                 events.waitForPageLoad();
377                 var data = [
378                     '<?xml version="1.0" encoding="UTF-8"?>\n',
379                     '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
380                     '          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
381                 ];
382                 fix(content.document.documentElement);
383                 addDataEntry(file + ".xhtml", data.join(""));
384             }
385
386             let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
387                 .map(function (h) h.selector
388                                    .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
389                                    .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
390                 .join("\n");
391             addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
392
393             addDataEntry("tag-map.json", JSON.stringify(help.tags));
394
395             let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
396             while ((m = re.exec(data)))
397                 chromeFiles[m[0]] = m[2];
398
399             for (let [uri, leaf] in Iterator(chromeFiles))
400                 addURIEntry(leaf, uri);
401
402             if (zip)
403                 zip.close();
404         }
405
406     })
407 }, {
408 }, {
409     commands: function initCommands(dactyl, modules, window) {
410         const { commands, completion, help } = modules;
411
412         [
413             {
414                 name: "h[elp]",
415                 description: "Open the introductory help page"
416             }, {
417                 name: "helpa[ll]",
418                 description: "Open the single consolidated help page"
419             }
420         ].forEach(function (command) {
421             let consolidated = command.name == "helpa[ll]";
422
423             commands.add([command.name],
424                 command.description,
425                 function (args) {
426                     dactyl.assert(!args.bang, _("help.dontPanic"));
427                     help.help(args.literalArg, consolidated);
428                 }, {
429                     argCount: "?",
430                     bang: true,
431                     completer: function (context) completion.help(context, consolidated),
432                     literal: 0
433                 });
434         });
435     },
436     completion: function initCompletion(dactyl, modules, window) {
437         const { completion } = modules;
438
439         completion.help = function completion_help(context, consolidated) {
440             dactyl.initHelp();
441             context.title = ["Help"];
442             context.anchored = false;
443             context.completions = help.tags;
444             if (consolidated)
445                 context.keys = { text: 0, description: function () "all" };
446         };
447     },
448     javascript: function initJavascript(dactyl, modules, window) {
449         modules.JavaScript.setCompleter([modules.help.exportHelp],
450             [function (context, args) overlay.activeModules.completion.file(context)]);
451     }
452 });
453
454 endModule();
455
456 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: