X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fmodules%2Fhelp.jsm;fp=common%2Fmodules%2Fhelp.jsm;h=d12312bd678834124a0edb4b1809532ffb65acd7;hb=9044153cb63835e39b9de8ec4ade237c03e3888a;hp=0000000000000000000000000000000000000000;hpb=70740024f9c028c1fd63e1a1850ab062ff956054;p=dactyl.git diff --git a/common/modules/help.jsm b/common/modules/help.jsm new file mode 100644 index 0000000..d12312b --- /dev/null +++ b/common/modules/help.jsm @@ -0,0 +1,467 @@ +// Copyright (c) 2008-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +/* use strict */ + +Components.utils.import("resource://dactyl/bootstrap.jsm"); +defineModule("help", { + exports: ["help"], + require: ["cache", "dom", "protocol", "services", "util"] +}, this); + +this.lazyRequire("completion", ["completion"]); +this.lazyRequire("overlay", ["overlay"]); + +var HelpBuilder = Class("HelpBuilder", { + init: function init() { + try { + // The versions munger will need to access the tag map + // during this process and without this we'll get an + // infinite loop. + help._data = this; + + this.files = {}; + this.tags = {}; + this.overlays = {}; + + // Scrape the list of help files from all.xml + this.tags["all"] = this.tags["all.xml"] = "all"; + let files = this.findHelpFile("all").map(function (doc) + [f.value for (f in DOM.XPath("//dactyl:include/@href", doc))]); + + // Scrape the tags from the rest of the help files. + array.flatten(files).forEach(function (file) { + this.tags[file + ".xml"] = file; + this.findHelpFile(file).forEach(function (doc) { + this.addTags(file, doc); + }, this); + }, this); + } + finally { + delete help._data; + } + }, + + toJSON: function toJSON() ({ + files: this.files, + overlays: this.overlays, + tags: this.tags + }), + + // Find the tags in the document. + addTags: function addTags(file, doc) { + for (let elem in DOM.XPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc)) + for (let tag in values((elem.value || elem.textContent).split(/\s+/))) + this.tags[tag] = file; + }, + + bases: ["dactyl://locale-local/", "dactyl://locale/", "dactyl://cache/help/"], + + // Find help and overlay files with the given name. + findHelpFile: function findHelpFile(file) { + let result = []; + for (let base in values(this.bases)) { + let url = [base, file, ".xml"].join(""); + let res = util.httpGet(url, { quiet: true }); + if (res) { + if (res.responseXML.documentElement.localName == "document") + this.files[file] = url; + if (res.responseXML.documentElement.localName == "overlay") + this.overlays[file] = url; + result.push(res.responseXML); + } + } + return result; + } +}); + +var Help = Module("Help", { + init: function init() { + this.initialized = false; + + function Loop(fn) + function (uri, path) { + if (!help.initialized) + return RedirectChannel(uri.spec, uri, 2, + "Initializing. Please wait..."); + + return fn.apply(this, arguments); + } + + update(services["dactyl:"].providers, { + "help": Loop(function (uri, path) help.files[path]), + "help-overlay": Loop(function (uri, path) help.overlays[path]), + "help-tag": Loop(function (uri, path) { + let tag = decodeURIComponent(path); + if (tag in help.files) + return RedirectChannel("dactyl://help/" + tag, uri); + if (tag in help.tags) + return RedirectChannel("dactyl://help/" + help.tags[tag] + "#" + tag.replace(/#/g, encodeURIComponent), uri); + }) + }); + + cache.register("help.json", HelpBuilder); + + cache.register("help/versions.xml", function () { + let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec, + { mimeType: "text/plain;charset=UTF-8" }) + .responseText; + + let re = util.regexp(UTF8( \s* # .*\n) + + | ^ (?P \s*) + (?P [-•*+]) \ // + (?P .*\n + (?: \2\ \ .*\n | \s*\n)* ) + + | (?P + (?: ^ [^\S\n]* + (?:[^-•*+\s] | [-•*+]\S) + .*\n + )+ + ) + + | (?: ^ [^\S\n]* \n) + + ]]>), "gmxy"); + + let betas = util.regexp(/\[(b\d)\]/, "gx"); + + let beta = array(betas.iterate(NEWS)) + .map(function (m) m[1]).uniq().slice(-1)[0]; + + + default xml namespace = NS; + function rec(text, level, li) { + XML.ignoreWhitespace = XML.prettyPrinting = false; + + let res = <>; + let list, space, i = 0; + + + for (let match in re.iterate(text)) { + if (match.comment) + continue; + else if (match.char) { + if (!list) + res += list =
    ; + let li =
  • ; + li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li); + list.* += li; + } + else if (match.par) { + let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par); + let t = tags; + tags = array(betas.iterate(tags)).map(function (m) m[1]); + + let group = !tags.length ? "" : + !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew"; + if (i === 0 && li) { + li.@highlight = group; + group = ""; + } + + list = null; + if (level == 0 && /^.*:\n$/.test(match.par)) { + let text = par.slice(0, -1); + res +=

    {template.linkifyHelp(text, true)}

    ; + } + else { + let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par); + res +=

    { + !tags.length ? "" : + {tags.join(" ")} + }{ + a ? {a} : "" + }{ + template.linkifyHelp(b, true) + }

    ; + } + } + i++; + } + for each (let attr in res..@highlight) { + attr.parent().@NS::highlight = attr; + delete attr.parent().@highlight; + } + return res; + } + + XML.ignoreWhitespace = XML.prettyPrinting = false; + let body = rec(NEWS, 0); + for each (let li in body..li) { + let list = li..li.(@NS::highlight == "HelpNewsOld"); + if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) { + for each (let li in list) + li.@NS::highlight = ""; + li.@NS::highlight = "HelpNewsOld"; + } + } + + + return '\n' + + '\n' + + '\n' + + +

    {config.appName} Versions

    + + + {body} +
    .toXMLString() + }); + }, + + initialize: function initialize() { + help.initialized = true; + }, + + flush: function flush(entries, time) { + cache.flushEntry("help.json", time); + + for (let entry in values(Array.concat(entries || []))) + cache.flushEntry(entry, time); + }, + + get data() this._data || cache.get("help.json"), + + get files() this.data.files, + get overlays() this.data.overlays, + get tags() this.data.tags, + + Local: function Local(dactyl, modules, window) ({ + init: function init() { + dactyl.commands["dactyl.help"] = function (event) { + let elem = event.originalTarget; + help.help(elem.getAttribute("tag") || elem.textContent); + }; + }, + + /** + * Returns the URL of the specified help *topic* if it exists. + * + * @param {string} topic The help topic to look up. + * @param {boolean} consolidated Whether to search the consolidated help page. + * @returns {string} + */ + findHelp: function (topic, consolidated) { + if (!consolidated && Set.has(help.files, topic)) + return topic; + let items = modules.completion._runCompleter("help", topic, null, !!consolidated).items; + let partialMatch = null; + + function format(item) item.description + "#" + encodeURIComponent(item.text); + + for (let [i, item] in Iterator(items)) { + if (item.text == topic) + return format(item); + else if (!partialMatch && topic) + partialMatch = item; + } + + if (partialMatch) + return format(partialMatch); + return null; + }, + + /** + * Opens the help page containing the specified *topic* if it exists. + * + * @param {string} topic The help topic to open. + * @param {boolean} consolidated Whether to use the consolidated help page. + */ + help: function (topic, consolidated) { + dactyl.initHelp(); + + if (!topic) { + let helpFile = consolidated ? "all" : modules.options["helpfile"]; + + if (Set.has(help.files, helpFile)) + dactyl.open("dactyl://help/" + helpFile, { from: "help" }); + else + dactyl.echomsg(_("help.noFile", helpFile.quote())); + return; + } + + let page = this.findHelp(topic, consolidated); + dactyl.assert(page != null, _("help.noTopic", topic)); + + dactyl.open("dactyl://help/" + page, { from: "help" }); + }, + + exportHelp: function (path) { + const FILE = io.File(path); + const PATH = FILE.leafName.replace(/\..*/, "") + "/"; + const TIME = Date.now(); + + if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName))) + FILE.create(FILE.DIRECTORY_TYPE, octal(755)); + + dactyl.initHelp(); + if (FILE.isDirectory()) { + var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data); + var addURIEntry = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText); + } + else { + var zip = services.ZipWriter(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE); + + addURIEntry = function addURIEntry(file, uri) + zip.addEntryChannel(PATH + file, TIME, 9, + services.io.newChannel(uri, null, null), false); + addDataEntry = function addDataEntry(file, data) // Unideal to an extreme. + addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data)); + } + + let empty = Set("area base basefont br col frame hr img input isindex link meta param" + .split(" ")); + function fix(node) { + switch(node.nodeType) { + case Node.ELEMENT_NODE: + if (isinstance(node, [HTMLBaseElement])) + return; + + data.push("<"); data.push(node.localName); + if (node instanceof HTMLHtmlElement) + data.push(" xmlns=" + XHTML.uri.quote(), + " xmlns:dactyl=" + NS.uri.quote()); + + for (let { name, value } in array.iterValues(node.attributes)) { + if (name == "dactyl:highlight") { + Set.add(styles, value); + name = "class"; + value = "hl-" + value; + } + if (name == "href") { + value = node.href || value; + if (value.indexOf("dactyl://help-tag/") == 0) { + let uri = services.io.newChannel(value, null, null).originalURI; + value = uri.spec == value ? "javascript:;" : uri.path.substr(1); + } + if (!/^#|[\/](#|$)|^[a-z]+:/.test(value)) + value = value.replace(/(#|$)/, ".xhtml$1"); + } + if (name == "src" && value.indexOf(":") > 0) { + chromeFiles[value] = value.replace(/.*\//, ""); + value = value.replace(/.*\//, ""); + } + + data.push(" ", name, '="', + <>{value}.toXMLString().replace(/"/g, """), + '"'); + } + if (node.localName in empty) + data.push(" />"); + else { + data.push(">"); + if (node instanceof HTMLHeadElement) + data.push(.toXMLString()); + Array.map(node.childNodes, fix); + data.push(""); + } + break; + case Node.TEXT_NODE: + data.push(<>{node.textContent}.toXMLString()); + } + } + + let chromeFiles = {}; + let styles = {}; + for (let [file, ] in Iterator(help.files)) { + let url = "dactyl://help/" + file; + dactyl.open(url); + util.waitFor(function () content.location.href == url && buffer.loaded + && content.document.documentElement instanceof HTMLHtmlElement, + 15000); + events.waitForPageLoad(); + var data = [ + '\n', + '\n' + ]; + fix(content.document.documentElement); + addDataEntry(file + ".xhtml", data.join("")); + } + + let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))] + .map(function (h) h.selector + .replace(/^\[.*?=(.*?)\]/, ".hl-$1") + .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}") + .join("\n"); + addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, "")); + + addDataEntry("tag-map.json", JSON.stringify(help.tags)); + + let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g; + while ((m = re.exec(data))) + chromeFiles[m[0]] = m[2]; + + for (let [uri, leaf] in Iterator(chromeFiles)) + addURIEntry(leaf, uri); + + if (zip) + zip.close(); + } + + }) +}, { +}, { + commands: function init_commands(dactyl, modules, window) { + const { commands, completion, help } = modules; + + [ + { + name: "h[elp]", + description: "Open the introductory help page" + }, { + name: "helpa[ll]", + description: "Open the single consolidated help page" + } + ].forEach(function (command) { + let consolidated = command.name == "helpa[ll]"; + + commands.add([command.name], + command.description, + function (args) { + dactyl.assert(!args.bang, _("help.dontPanic")); + help.help(args.literalArg, consolidated); + }, { + argCount: "?", + bang: true, + completer: function (context) completion.help(context, consolidated), + literal: 0 + }); + }); + }, + completion: function init_completion(dactyl, modules, window) { + const { completion } = modules; + + completion.help = function completion_help(context, consolidated) { + dactyl.initHelp(); + context.title = ["Help"]; + context.anchored = false; + context.completions = help.tags; + if (consolidated) + context.keys = { text: 0, description: function () "all" }; + }; + }, + mappings: function init_mappings(dactyl, modules, window) { + const { help, mappings, modes } = modules; + + mappings.add([modes.MAIN], ["", ""], + "Open the introductory help page", + function () { help.help(); }); + + mappings.add([modes.MAIN], ["", ""], + "Open the single, consolidated help page", + function () { modules.ex.helpall(); }); + }, + javascript: function init_javascript(dactyl, modules, window) { + modules.JavaScript.setCompleter([modules.help.exportHelp], + [function (context, args) overlay.activeModules.completion.file(context)]); + } +}); + +endModule(); + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: