1 // Copyright (c) 2008-2011 by 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.
7 Components.utils.import("resource://dactyl/bootstrap.jsm");
10 require: ["cache", "dom", "protocol", "services", "util"]
13 this.lazyRequire("completion", ["completion"]);
14 this.lazyRequire("overlay", ["overlay"]);
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(function (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(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);
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(<![CDATA[
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(function (m) m[1]).uniq().slice(-1)[0];
135 default xml namespace = NS;
136 function rec(text, level, li) {
137 XML.ignoreWhitespace = XML.prettyPrinting = false;
140 let list, space, i = 0;
143 for (let match in re.iterate(text)) {
146 else if (match.char) {
150 li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li);
153 else if (match.par) {
154 let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
156 tags = array(betas.iterate(tags)).map(function (m) m[1]);
158 let group = !tags.length ? "" :
159 !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew";
161 li.@highlight = group;
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>;
171 let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
172 res += <p highlight={group + " HelpNews"}>{
174 <hl key="HelpNewsTag">{tags.join(" ")}</hl>
176 a ? <hl key="HelpWarning">{a}</hl> : ""
178 template.linkifyHelp(b, true)
184 for each (let attr in res..@highlight) {
185 attr.parent().@NS::highlight = attr;
186 delete attr.parent().@highlight;
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";
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>
212 </document>.toXMLString()
216 initialize: function initialize() {
217 help.initialized = true;
220 flush: function flush(entries, time) {
221 cache.flushEntry("help.json", time);
223 for (let entry in values(Array.concat(entries || [])))
224 cache.flushEntry(entry, time);
227 get data() this._data || cache.get("help.json"),
229 get files() this.data.files,
230 get overlays() this.data.overlays,
231 get tags() this.data.tags,
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);
242 * Returns the URL of the specified help *topic* if it exists.
244 * @param {string} topic The help topic to look up.
245 * @param {boolean} consolidated Whether to search the consolidated help page.
248 findHelp: function (topic, consolidated) {
249 if (!consolidated && Set.has(help.files, topic))
251 let items = modules.completion._runCompleter("help", topic, null, !!consolidated).items;
252 let partialMatch = null;
254 function format(item) item.description + "#" + encodeURIComponent(item.text);
256 for (let [i, item] in Iterator(items)) {
257 if (item.text == topic)
259 else if (!partialMatch && topic)
264 return format(partialMatch);
269 * Opens the help page containing the specified *topic* if it exists.
271 * @param {string} topic The help topic to open.
272 * @param {boolean} consolidated Whether to use the consolidated help page.
274 help: function (topic, consolidated) {
278 let helpFile = consolidated ? "all" : modules.options["helpfile"];
280 if (Set.has(help.files, helpFile))
281 dactyl.open("dactyl://help/" + helpFile, { from: "help" });
283 dactyl.echomsg(_("help.noFile", helpFile.quote()));
287 let page = this.findHelp(topic, consolidated);
288 dactyl.assert(page != null, _("help.noTopic", topic));
290 dactyl.open("dactyl://help/" + page, { from: "help" });
293 exportHelp: function (path) {
294 const FILE = io.File(path);
295 const PATH = FILE.leafName.replace(/\..*/, "") + "/";
296 const TIME = Date.now();
298 if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
299 FILE.create(FILE.DIRECTORY_TYPE, octal(755));
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);
307 var zip = services.ZipWriter(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
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));
316 let empty = Set("area base basefont br col frame hr img input isindex link meta param"
319 switch(node.nodeType) {
320 case Ci.nsIDOMNode.ELEMENT_NODE:
321 if (isinstance(node, [Ci.nsIDOMHTMLBaseElement]))
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());
329 for (let { name, value } in array.iterValues(node.attributes)) {
330 if (name == "dactyl:highlight") {
331 Set.add(styles, value);
333 value = "hl-" + value;
335 if (name == "href") {
336 value = node.href || value;
337 if (value.indexOf("dactyl://help-tag/") == 0) {
339 let uri = services.io.newChannel(value, null, null).originalURI;
340 value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
343 util.dump("Magical tag thingy failure for: " + value);
344 dactyl.reportError(e);
347 if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
348 value = value.replace(/(#|$)/, ".xhtml$1");
350 if (name == "src" && value.indexOf(":") > 0) {
351 chromeFiles[value] = value.replace(/.*\//, "");
352 value = value.replace(/.*\//, "");
355 data.push(" ", name, '="',
356 <>{value}</>.toXMLString().replace(/"/g, """),
359 if (node.localName in empty)
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, ">");
369 case Ci.nsIDOMNode.TEXT_NODE:
370 data.push(<>{node.textContent}</>.toXMLString());
374 let { buffer, content, events } = modules;
375 let chromeFiles = {};
378 for (let [file, ] in Iterator(help.files)) {
379 let url = "dactyl://help/" + file;
381 util.waitFor(function () content.location.href == url && buffer.loaded
382 && content.document.documentElement instanceof Ci.nsIDOMHTMLHtmlElement,
384 events.waitForPageLoad();
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'
390 fix(content.document.documentElement);
391 addDataEntry(file + ".xhtml", data.join(""));
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 + "}")
399 addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
401 addDataEntry("tag-map.json", JSON.stringify(help.tags));
403 let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
404 while ((m = re.exec(data)))
405 chromeFiles[m[0]] = m[2];
407 for (let [uri, leaf] in Iterator(chromeFiles))
408 addURIEntry(leaf, uri);
417 commands: function init_commands(dactyl, modules, window) {
418 const { commands, completion, help } = modules;
423 description: "Open the introductory help page"
426 description: "Open the single consolidated help page"
428 ].forEach(function (command) {
429 let consolidated = command.name == "helpa[ll]";
431 commands.add([command.name],
434 dactyl.assert(!args.bang, _("help.dontPanic"));
435 help.help(args.literalArg, consolidated);
439 completer: function (context) completion.help(context, consolidated),
444 completion: function init_completion(dactyl, modules, window) {
445 const { completion } = modules;
447 completion.help = function completion_help(context, consolidated) {
449 context.title = ["Help"];
450 context.anchored = false;
451 context.completions = help.tags;
453 context.keys = { text: 0, description: function () "all" };
456 mappings: function init_mappings(dactyl, modules, window) {
457 const { help, mappings, modes } = modules;
459 mappings.add([modes.MAIN], ["<open-help>", "<F1>"],
460 "Open the introductory help page",
461 function () { help.help(); });
463 mappings.add([modes.MAIN], ["<open-single-help>", "<A-F1>"],
464 "Open the single, consolidated help page",
465 function () { modules.ex.helpall(); });
467 javascript: function init_javascript(dactyl, modules, window) {
468 modules.JavaScript.setCompleter([modules.help.exportHelp],
469 [function (context, args) overlay.activeModules.completion.file(context)]);
475 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: