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