1 // Copyright (c) 2008-2013 Kris Maglione <maglione.k@gmail.com>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
9 require: ["cache", "dom", "protocol", "services", "util"]
12 lazyRequire("completion", ["completion"]);
13 lazyRequire("overlay", ["overlay"]);
14 lazyRequire("template", ["template"]);
16 var HelpBuilder = Class("HelpBuilder", {
17 init: function init() {
19 // The versions munger will need to access the tag map
20 // during this process and without this we'll get an
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(doc =>
31 [f.value for (f in DOM.XPath("//dactyl:include/@href", doc))]);
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);
46 toJSON: function toJSON() ({
48 overlays: this.overlays,
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;
59 bases: ["dactyl://locale-local/", "dactyl://locale/", "dactyl://cache/help/"],
61 // Find help and overlay files with the given name.
62 findHelpFile: function findHelpFile(file) {
64 for (let base in values(this.bases)) {
65 let url = [base, file, ".xml"].join("");
66 let res = util.httpGet(url, { quiet: true });
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);
79 var Help = Module("Help", {
80 init: function init() {
81 this.initialized = false;
84 function (uri, path) {
85 if (!help.initialized)
86 return RedirectChannel(uri.spec, uri, 2,
87 "Initializing. Please wait...");
89 return fn.apply(this, arguments);
92 update(services["dactyl:"].providers, {
93 "help": Loop((uri, path) => help.files[path]),
94 "help-overlay": Loop((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);
100 return RedirectChannel("dactyl://help/" + help.tags[tag] + "#" + tag.replace(/#/g, encodeURIComponent), uri);
104 cache.register("help.json", HelpBuilder);
106 cache.register("help/versions.xml", function () {
107 let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec,
108 { mimeType: "text/plain;charset=UTF-8" })
111 let re = util.regexp(UTF8(literal(/*
112 ^ (?P<comment> \s* # .*\n)
115 (?P<char> [-•*+]) \ //
117 (?: \2\ \ .*\n | \s*\n)* )
121 (?:[^-•*+\s] | [-•*+]\S)
126 | (?: ^ [^\S\n]* \n) +
129 let betas = util.regexp(/\[((?:b|rc)\d)\]/, "gx");
131 let beta = array(betas.iterate(NEWS))
132 .map(m => m[1]).uniq().slice(-1)[0];
134 function rec(text, level, li) {
136 let list, space, i = 0;
138 for (let match in re.iterate(text)) {
141 else if (match.char) {
143 res.push(list = ["ul", {}]);
145 li.push(rec(match.content
146 .replace(RegExp("^" + match.space, "gm"), ""),
151 else if (match.par) {
152 let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
154 tags = array(betas.iterate(tags)).map(m => m[1]);
156 let group = !tags.length ? "" :
157 !tags.some(t => t == beta) ? "HelpNewsOld" : "HelpNewsNew";
159 li[1]["dactyl:highlight"] = group;
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)]);
170 let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
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)]);
184 let body = rec(NEWS, 0);
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";
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" }],
207 initialize: function initialize() {
208 help.initialized = true;
211 flush: function flush(entries, time) {
212 cache.flushEntry("help.json", time);
214 for (let entry in values(Array.concat(entries || [])))
215 cache.flushEntry(entry, time);
218 get data() this._data || cache.get("help.json"),
220 get files() this.data.files,
221 get overlays() this.data.overlays,
222 get tags() this.data.tags,
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);
233 * Returns the URL of the specified help *topic* if it exists.
235 * @param {string} topic The help topic to look up.
236 * @param {boolean} consolidated Whether to search the consolidated help page.
239 findHelp: function (topic, consolidated) {
240 if (!consolidated && Set.has(help.files, topic))
242 let items = modules.completion._runCompleter("help", topic, null, !!consolidated).items;
243 let partialMatch = null;
245 function format(item) item.description + "#" + encodeURIComponent(item.text);
247 for (let [i, item] in Iterator(items)) {
248 if (item.text == topic)
250 else if (!partialMatch && topic)
255 return format(partialMatch);
260 * Opens the help page containing the specified *topic* if it exists.
262 * @param {string} topic The help topic to open.
263 * @param {boolean} consolidated Whether to use the consolidated help page.
265 help: function (topic, consolidated) {
269 let helpFile = consolidated ? "all" : modules.options["helpfile"];
271 if (Set.has(help.files, helpFile))
272 dactyl.open("dactyl://help/" + helpFile, { from: "help" });
274 dactyl.echomsg(_("help.noFile", helpFile.quote()));
278 let page = this.findHelp(topic, consolidated);
279 dactyl.assert(page != null, _("help.noTopic", topic));
281 dactyl.open("dactyl://help/" + page, { from: "help" });
284 exportHelp: function (path) {
285 const FILE = io.File(path);
286 const PATH = FILE.leafName.replace(/\..*/, "") + "/";
287 const TIME = Date.now();
289 if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
290 FILE.create(FILE.DIRECTORY_TYPE, octal(755));
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);
298 var zip = services.ZipWriter(FILE.file, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
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));
307 let empty = Set("area base basefont br col frame hr img input isindex link meta param"
310 switch (node.nodeType) {
311 case Ci.nsIDOMNode.ELEMENT_NODE:
312 if (isinstance(node, [Ci.nsIDOMHTMLBaseElement]))
315 data.push("<", node.localName);
316 if (node instanceof Ci.nsIDOMHTMLHtmlElement)
317 data.push(" xmlns=" + XHTML.quote(),
318 " xmlns:dactyl=" + NS.quote());
320 for (let { name, value } in array.iterValues(node.attributes)) {
321 if (name == "dactyl:highlight") {
322 Set.add(styles, value);
324 value = "hl-" + value;
326 if (name == "href") {
327 value = node.href || value;
328 if (value.indexOf("dactyl://help-tag/") == 0) {
330 let uri = services.io.newChannel(value, null, null).originalURI;
331 value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
334 util.dump("Magical tag thingy failure for: " + value);
335 dactyl.reportError(e);
338 if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
339 value = value.replace(/(#|$)/, ".xhtml$1");
341 if (name == "src" && value.indexOf(":") > 0) {
342 chromeFiles[value] = value.replace(/.*\//, "");
343 value = value.replace(/.*\//, "");
346 data.push(" ", name, '="', DOM.escapeHTML(value), '"');
348 if (node.localName in empty)
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, ">");
358 case Ci.nsIDOMNode.TEXT_NODE:
359 data.push(DOM.escapeHTML(node.textContent, true));
363 let { buffer, content, events } = modules;
364 let chromeFiles = {};
367 for (let [file, ] in Iterator(help.files)) {
368 let url = "dactyl://help/" + file;
370 util.waitFor(() => (content.location.href == url && buffer.loaded &&
371 content.document.documentElement instanceof Ci.nsIDOMHTMLHtmlElement),
373 events.waitForPageLoad();
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'
379 fix(content.document.documentElement);
380 addDataEntry(file + ".xhtml", data.join(""));
383 let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
385 .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
386 .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
388 addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
390 addDataEntry("tag-map.json", JSON.stringify(help.tags));
392 let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
393 while ((m = re.exec(data)))
394 chromeFiles[m[0]] = m[2];
396 for (let [uri, leaf] in Iterator(chromeFiles))
397 addURIEntry(leaf, uri);
406 commands: function initCommands(dactyl, modules, window) {
407 const { commands, completion, help } = modules;
412 description: "Open the introductory help page"
415 description: "Open the single consolidated help page"
417 ].forEach(function (command) {
418 let consolidated = command.name == "helpa[ll]";
420 commands.add([command.name],
423 dactyl.assert(!args.bang, _("help.dontPanic"));
424 help.help(args.literalArg, consolidated);
428 completer: function (context) completion.help(context, consolidated),
433 completion: function initCompletion(dactyl, modules, window) {
434 const { completion } = modules;
436 completion.help = function completion_help(context, consolidated) {
438 context.title = ["Help"];
439 context.anchored = false;
440 context.completions = help.tags;
442 context.keys = { text: 0, description: function () "all" };
445 javascript: function initJavascript(dactyl, modules, window) {
446 modules.JavaScript.setCompleter([modules.help.exportHelp],
447 [(context, args) => overlay.activeModules.completion.file(context)]);
449 options: function initOptions(dactyl, modules, window) {
450 const { options } = modules;
452 options.add(["helpfile", "hf"],
453 "Name of the main help file",
460 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: