about summary refs log tree commit diff
path: root/vimperator/.vimperator/colors/style.js
diff options
context:
space:
mode:
Diffstat (limited to 'vimperator/.vimperator/colors/style.js')
-rwxr-xr-xvimperator/.vimperator/colors/style.js842
1 files changed, 842 insertions, 0 deletions
diff --git a/vimperator/.vimperator/colors/style.js b/vimperator/.vimperator/colors/style.js
new file mode 100755
index 0000000..7dae145
--- /dev/null
+++ b/vimperator/.vimperator/colors/style.js
@@ -0,0 +1,842 @@
+// Copyright (c) 2008-2009 by Kris Maglione <maglione.k at Gmail>
+// Copyright (c) 2008-2010 by Martin Stubenschrott <stubenschrott@vimperator.org>
+//
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the License.txt file included with this file.
+
+/** @scope modules */
+
+/**
+ * @constant
+ * @property {string} The default highlighting rules. They have the
+ * form:
+ *    rule ::= selector space space+ css
+ *    selector ::= group
+ *               | group "," css-selector
+ *               | group "," css-selector "," scope
+ *    group ::= groupname
+ *            | groupname css-selector
+ */
+// <css>
+Highlights.prototype.CSS = `
+    Boolean     color: red;
+    Function    color: navy;
+    Null        color: blue;
+    Number      color: blue;
+    Object      color: maroon;
+    String      color: green;
+    Mapping     color: magenta;
+
+    Key         font-weight: bold;
+
+    Enabled     color: green;
+    Disabled    color: red;
+
+    Normal          color: black; background: white;
+    ErrorMsg        color: white; background: red; font-weight: bold;
+    InfoMsg         color: magenta; background: white;
+    ModeMsg         color: white; background: green; border-radius: 1px; padding: 0px 5px;
+    MoreMsg         color: green; background: white;
+    WarningMsg      color: red; background: white;
+    Message         white-space: normal; min-width: 100%; padding-left: 2em; text-indent: -2em; display: block;
+    NonText         color: blue; min-height: 16px; padding-left: 2px;
+    Preview         color: gray;
+    Prompt          background: url("chrome://liberator/skin/prompt.png"); width: 10px; background-position: center; background-repeat: no-repeat;
+    PromptText      color: white; background: purple;
+
+    CmdOutput             white-space: pre;
+    CmdLine               background: white; color: black; transition: all 0.25s;
+    CmdLine>*             font-family: monospace;
+
+    ContentSeparator      border-top: 1px dotted gray; display: -moz-box;
+
+    CompGroup
+    CompGroup:not(:first-of-type)  margin-top: 1ex;
+    CompTitle            font-weight: bold; background: linear-gradient(to top, #DBDBDB 19%, #D9D9D9, #E7E7E7 100%);
+    CompTitle>*          color: #333; border-top: 1px solid gray; border-bottom: 1px solid #BBB; padding: 1px 0.5ex; text-shadow: 1px 1px 0px #E0E0E0;
+    CompMsg              font-style: italic; margin-left: 16px;
+    CompItem
+    CompItem[selected]   background: #FFEC8B; box-shadow: 0px 0px 1px #CC0;
+    CompItem>*           height: 18px; min-height: 18px; padding: 0 0.5ex;
+    CompIcon             width: 16px; min-width: 16px; display: inline-block; margin-right: .5ex;
+    CompIcon>img         max-width: 16px; max-height: 16px; vertical-align: middle;
+    CompResult           width: 500px; max-width: 500px; overflow: hidden;
+    CompDesc             width: 500px; max-width: 500px; color: gray;
+
+    Indicator   color: blue;
+    Filter      border-radius: 2px; background: #ffec8b; border: 1px solid orange;
+
+    Keyword     color: red;
+    Tag         color: blue;
+
+    LineNr      color: orange; background: white;
+    Question    color: green; background: white; font-weight: bold;
+
+    StatusLine  color: gray; background: transparent; font-weight: normal;
+
+    TabNumber      font-weight: bold; margin: 0px; padding-right: .3ex;
+
+    Title       color: magenta; background: white; font-weight: bold;
+    URL         text-decoration: none; color: green;
+    URL:hover   text-decoration: underline; cursor: pointer;
+
+    FrameIndicator,,* {
+        background-color: red;
+        opacity: 0.5;
+        z-index: 999;
+        position: fixed;
+        top: 0;
+        bottom: 0;
+        left: 0;
+        right: 0;
+    }
+
+    Bell         border: none; background-color: black;
+    Hint,,* {
+        font-family: monospace;
+        font-size: 10px;
+        font-weight: bold;
+        text-transform: uppercase;
+        color: white;
+        background-color: red;
+        border-color: ButtonShadow;
+        border-width: 0px;
+        border-style: solid;
+        padding: 0px 1px 0px 1px;
+    }
+    Hint::after,,*  content: attr(number);
+    HintElem,,*     background-color: yellow;  color: black;
+    HintActive,,*   background-color: #88FF00; color: black;
+    HintImage,,*    opacity: .5;
+
+    Help                                        font-size: 8pt; line-height: 1.4em; font-family: -moz-fixed;
+
+    HelpArg                                     color: #6A97D4;
+    HelpOptionalArg                             color: #6A97D4;
+
+    HelpBody                                    display: block; margin: 1em auto; max-width: 100ex;
+    HelpBorder,*,liberator://help/*             border-color: silver; border-width: 0px; border-style: solid;
+    HelpCode                                    display: block; white-space: pre; margin-left: 2em; font-family: Terminus, Fixed, monospace;
+
+    HelpDefault                                 margin-right: 1ex; white-space: pre;
+
+    HelpDescription                             display: block;
+    HelpEm,html|em,liberator://help/*           font-weight: bold; font-style: normal;
+
+    HelpEx                                      display: inline-block; color: #527BBD; font-weight: bold;
+
+    HelpExample                                 display: block; margin: 1em 0;
+    HelpExample::before                         content: "Example: "; font-weight: bold;
+
+    HelpInfo                                    display: block; width: 20em; margin-left: auto;
+    HelpInfoLabel                               display: inline-block; width: 6em;  color: magenta; font-weight: bold; vertical-align: text-top;
+    HelpInfoValue                               display: inline-block; width: 14em; text-decoration: none;             vertical-align: text-top;
+
+    HelpItem                                    display: block; margin: 1em 1em 1em 10em; clear: both;
+
+    HelpKey                                     color: #102663;
+
+    HelpLink,html|a,liberator://help/*          text-decoration: none;
+    HelpLink[href]:hover                        text-decoration: underline;
+
+    HelpList,html|ul,liberator://help/*         display: block; list-style: outside disc;
+    HelpOrderedList,html|ol,liberator://help/*  display: block; list-style: outside decimal;
+    HelpListItem,html|li,liberator://help/*     display: list-item;
+
+    HelpNote                                    color: red; font-weight: bold;
+
+    HelpOpt                                     color: #106326;
+    HelpOptInfo                                 display: inline-block; margin-bottom: 1ex;
+
+    HelpParagraph,html|p,liberator://help/*     display: block; margin: 1em 0em;
+    HelpParagraph:first-child                   margin-top: 0;
+    HelpSpec                                    display: block; margin-left: -10em; float: left; clear: left; color: #FF00FF;
+
+    HelpString                                  display: inline-block; color: green; font-weight: normal; vertical-align: text-top;
+    HelpString::before                          content: '"';
+    HelpString::after                           content: '"';
+
+    HelpHead,html|h1,liberator://help/* {
+        display: block;
+        margin: 1em 0;
+        padding-bottom: .2ex;
+        border-bottom-width: 1px;
+        font-size: 2em;
+        font-weight: bold;
+        color: #527BBD;
+        clear: both;
+    }
+    HelpSubhead,html|h2,liberator://help/* {
+        display: block;
+        margin: 1em 0;
+        padding-bottom: .2ex;
+        border-bottom-width: 1px;
+        font-size: 1.2em;
+        font-weight: bold;
+        color: #527BBD;
+        clear: both;
+    }
+    HelpSubsubhead,html|h3,liberator://help/* {
+        display: block;
+        margin: 1em 0;
+        padding-bottom: .2ex;
+        font-size: 1.1em;
+        font-weight: bold;
+        color: #527BBD;
+        clear: both;
+    }
+
+    HelpTOC
+    HelpTOC>ol ol                               margin-left: -1em;
+
+    HelpTab,html|dl,liberator://help/*             display: table; width: 100%; margin: 1em 0; border-bottom-width: 1px; border-top-width: 1px; padding: .5ex 0; table-layout: fixed;
+    HelpTabColumn,html|column,liberator://help/*   display: table-column;
+    HelpTabColumn:first-child                      width: 25%;
+    HelpTabTitle,html|dt,liberator://help/*        display: table-cell; padding: .1ex 1ex; font-weight: bold;
+    HelpTabDescription,html|dd,liberator://help/*  display: table-cell; padding: .1ex 1ex; border-width: 0px;
+    HelpTabRow,html|dl>html|tr,liberator://help/*  display: table-row;
+
+    HelpTag                                     display: inline-block; color: #999; margin-left: 1ex; font-size: 8pt; font-weight: bold;
+    HelpTags                                    display: block; float: right; clear: right;
+    HelpTopic                                   color: #102663;
+    HelpType                                    margin-right: 2ex;
+
+    HelpWarning                                 color: red; font-weight: bold;
+
+    Logo
+    `;
+
+/**
+ * A class to manage highlighting rules. The parameters are the
+ * standard parameters for any {@link Storage} object.
+ *
+ * @author Kris Maglione <maglione.k@gmail.com>
+ */
+function Highlights(name, store) {
+    let self = this;
+    let highlight = {};
+    let styles = storage.styles;
+
+    const Highlight = Struct("class", "selector", "filter", "default", "value", "base");
+    Highlight.defaultValue("filter", function ()
+        this.base ? this.base.filter :
+        ["chrome://liberator/*",
+         "liberator:*",
+         "file://*"].concat(config.styleableChrome).join(","));
+    Highlight.defaultValue("selector", function () self.selector(this.class));
+    Highlight.defaultValue("value", function () this.default);
+    Highlight.defaultValue("base", function () {
+        let base = this.class.match(/^(\w*)/)[0];
+        return base != this.class && base in highlight ? highlight[base] : null;
+    });
+
+    let highlightKeys = [];
+    for ([k, v] in this)
+        highlightKeys.push(k + ": " + util.escapeString(v || "undefined"));
+    Highlight.prototype.toString = function () "Highlight(" + this.class + ")\n\t" + highlightKeys.join("\n\t");
+
+    function keys() {
+        return Object.keys(highlight).sort();
+    }
+
+    this.__iterator__ = function () iter(keys().map(v => highlight[v]));
+
+    this.get = function (k) highlight[k];
+    this.set = function (key, newStyle, force, append) {
+        let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/);
+
+        if (!(class_ in highlight))
+            return "Unknown highlight keyword: " + class_;
+
+        let style = highlight[key] || Highlight(key);
+        styles.removeSheet(true, style.selector);
+
+        if (append)
+            newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle);
+        if (/^\s*$/.test(newStyle))
+            newStyle = null;
+        if (newStyle == null) {
+            if (style.default == null) {
+                delete highlight[style.class];
+                styles.removeSheet(true, style.selector);
+                return null;
+            }
+            newStyle = style.default;
+            force = true;
+        }
+
+        let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;")
+                          .replace(/;!important;/g, ";"); // Seeming Spidermonkey bug
+        if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) {
+            css = style.selector + " { " + css + " }";
+
+            let error = styles.addSheet(true, "highlight:" + style.class, style.filter, css, true);
+            if (error)
+                return error;
+        }
+        style.value = newStyle;
+        highlight[style.class] = style;
+        return null;
+    };
+
+    /**
+     * Gets a CSS selector given a highlight group.
+     *
+     * @param {string} class
+     */
+    this.selector = function (class_) {
+        let [, hl, rest] = class_.match(/^(\w*)(.*)/);
+        let pattern = "[liberator|highlight~=" + hl + "]"
+        if (highlight[hl] && highlight[hl].class != class_)
+            pattern = highlight[hl].selector;
+        return pattern + rest;
+    };
+
+    /**
+     * Clears all highlighting rules. Rules with default values are
+     * reset.
+     */
+    this.clear = function () {
+        for (let [k, v] in Iterator(highlight))
+            this.set(k, null, true);
+    };
+
+    /**
+     * Bulk loads new CSS rules.
+     *
+     * @param {string} css The rules to load. See {@link Highlights#css}.
+     */
+    this.loadCSS = function (css) {
+        css.replace(/^(\s*\S*\s+)\{((?:.|\n)*?)\}\s*$/gm, function (_, _1, _2) _1 + " " + _2.replace(/\n\s*/g, " "))
+           .split("\n").filter(function (s) /\S/.test(s))
+           .forEach(function (style) {
+                style = Highlight.apply(Highlight, Array.slice(style.match(/^\s*((?:[^,\s]|\s\S)+)(?:,((?:[^,\s]|\s\S)+)?)?(?:,((?:[^,\s]|\s\S)+))?\s*(.*)$/), 1));
+                if (/^[>+ ]/.test(style.selector))
+                    style.selector = self.selector(style.class) + style.selector;
+
+                let old = highlight[style.class];
+                highlight[style.class] = style;
+                if (old && old.value != old.default)
+                    style.value = old.value;
+        });
+        for (let [class_, hl] in Iterator(highlight)) {
+            if (hl.value == hl.default)
+                this.set(class_);
+        }
+    };
+    this.loadCSS(this.CSS);
+}
+
+/**
+ * Manages named and unnamed user style sheets, which apply to both
+ * chrome and content pages. The parameters are the standard
+ * parameters for any {@link Storage} object.
+ *
+ * @author Kris Maglione <maglione.k@gmail.com>
+ */
+function Styles(name, store) {
+    // Can't reference liberator or Components inside Styles --
+    // they're members of the window object, which disappear
+    // with this window.
+    const self = this;
+    const util = modules.util;
+    const sleep = liberator.sleep;
+    const storage = modules.storage;
+    const ios = services.get("io");
+    const sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
+    const namespace = "@namespace html " + JSON.stringify(XHTML.uri) + ";\n" +
+                      "@namespace xul " + JSON.stringify(XUL.uri) + ";\n" +
+                      "@namespace liberator " + JSON.stringify(NS.uri) + ";\n";
+
+    const Sheet = Struct("name", "id", "sites", "css", "system", "agent");
+    Sheet.prototype.__defineGetter__("fullCSS", function wrapCSS() {
+        let filter = this.sites;
+        let css = this.css;
+        if (filter[0] == "*")
+            return namespace + css;
+        let selectors = filter.map(function (part) (/[*]$/.test(part)   ? "url-prefix" :
+                                                    /[\/:]/.test(part)  ? "url"
+                                                                        : "domain")
+                                            + '("' + part.replace(/"/g, "%22").replace(/[*]$/, "") + '")')
+                              .join(", ");
+        return namespace + "/* Liberator style #" + this.id + " */ @-moz-document " + selectors + "{\n" + css + "\n}\n";
+    });
+    Sheet.prototype.__defineGetter__("enabled", function () this._enabled);
+    Sheet.prototype.__defineSetter__("enabled", function (on) {
+        this._enabled = Boolean(on);
+        if (on) {
+            self.registerSheet(cssUri(this.fullCSS));
+            if (this.agent)
+                self.registerSheet(cssUri(this.fullCSS), true);
+        }
+        else {
+            self.unregisterSheet(cssUri(this.fullCSS));
+            self.unregisterSheet(cssUri(this.fullCSS), true);
+        }
+    });
+
+    let cssUri = function (css) "chrome-data:text/css," + window.encodeURIComponent(css);
+
+    let userSheets = [];
+    let systemSheets = [];
+    let userNames = {};
+    let systemNames = {};
+
+    let id = 0;
+
+    this.__iterator__ = function () Iterator(userSheets.concat(systemSheets));
+    this.__defineGetter__("systemSheets", function () Iterator(systemSheets));
+    this.__defineGetter__("userSheets", function () Iterator(userSheets));
+    this.__defineGetter__("systemNames", function () Iterator(systemNames));
+    this.__defineGetter__("userNames", function () Iterator(userNames));
+
+    /**
+     * Add a new style sheet.
+     *
+     * @param {boolean} system Declares whether this is a system or
+     *     user sheet. System sheets are used internally by
+     *     @liberator.
+     * @param {string} name The name given to the style sheet by
+     *     which it may be later referenced.
+     * @param {string} filter The sites to which this sheet will
+     *     apply. Can be a domain name or a URL. Any URL ending in
+     *     "*" is matched as a prefix.
+     * @param {string} css The CSS to be applied.
+     */
+    this.addSheet = function (system, name, filter, css, agent) {
+        let sheets = system ? systemSheets : userSheets;
+        let names = system ? systemNames : userNames;
+        let sites = filter.split(",").filter(util.identity);
+        if (name && name in names) {
+            let s = names[name];
+            if (s.css === css && s.sites.join(",") === sites.join(",")) {
+                if (!s.enabled) s.enabled = true;
+                return null;
+            }
+            this.removeSheet(system, name);
+        }
+
+        let sheet = Sheet(name, id++, sites, String(css), system, agent);
+
+        try {
+            sheet.enabled = true;
+        }
+        catch (e) {
+            return e.echoerr || e;
+        }
+        sheets.push(sheet);
+
+        if (name)
+            names[name] = sheet;
+        return null;
+    };
+
+    /**
+     * Get a sheet with a given name or index.
+     *
+     * @param {boolean} system
+     * @param {string or number} sheet The sheet to retrieve. Strings indicate
+     *     sheet names, while numbers indicate indices.
+     */
+    this.get = function get(system, sheet) {
+        let sheets = system ? systemSheets : userSheets;
+        let names = system ? systemNames : userNames;
+        if (typeof sheet === "number")
+            return sheets[sheet];
+        return names[sheet];
+    };
+
+    /**
+     * Find sheets matching the parameters. See {@link #addSheet}
+     * for parameters.
+     *
+     * @param {boolean} system
+     * @param {string} name
+     * @param {string} filter
+     * @param {string} css
+     * @param {number} index
+     */
+    this.findSheets = function (system, name, filter, css, index) {
+        let sheets = system ? systemSheets : userSheets;
+        let names = system ? systemNames : userNames;
+
+        // Grossly inefficient.
+        let matches = Object.keys(sheets);
+        if (index)
+            matches = String(index).split(",").filter(function (i) i in sheets);
+        if (name)
+            matches = matches.filter(function (i) sheets[i] == names[name]);
+        if (css)
+            matches = matches.filter(function (i) sheets[i].css == css);
+        if (filter)
+            matches = matches.filter(function (i) sheets[i].sites.indexOf(filter) >= 0);
+        return matches.map(function (i) sheets[i]);
+    };
+
+    /**
+     * Remove a style sheet. See {@link #addSheet} for parameters.
+     * In cases where <b>filter</b> is supplied, the given filters
+     * are removed from matching sheets. If any remain, the sheet is
+     * left in place.
+     *
+     * @param {boolean} system
+     * @param {string} name
+     * @param {string} filter
+     * @param {string} css
+     * @param {number} index
+     */
+    this.removeSheet = function (system, name, filter, css, index) {
+        let self = this;
+        if (arguments.length == 1) {
+            var matches = [system];
+            system = matches[0].system;
+        }
+        let sheets = system ? systemSheets : userSheets;
+        let names = system ? systemNames : userNames;
+
+        if (filter && filter.indexOf(",") > -1)
+            return filter.split(",").reduce(
+                function (n, f) n + self.removeSheet(system, name, f, index), 0);
+
+        if (filter == undefined)
+            filter = "";
+
+        if (!matches)
+            matches = this.findSheets(system, name, filter, css, index);
+        if (matches.length == 0)
+            return null;
+
+        for (let sheet of matches.reverse()) {
+            sheet.enabled = false;
+            if (name)
+                delete names[name];
+            if (sheets.indexOf(sheet) > -1)
+                sheets.splice(sheets.indexOf(sheet), 1);
+
+            /* Re-add if we're only changing the site filter. */
+            if (filter) {
+                let sites = sheet.sites.filter(function (f) f != filter);
+                if (sites.length)
+                    this.addSheet(system, name, sites.join(","), css, sheet.agent);
+            }
+        }
+        return matches.length;
+    };
+
+    /**
+     * Register a user style sheet at the given URI.
+     *
+     * @param {string} uri The URI of the sheet to register.
+     * @param {boolean} agent If true, sheet is registered as an agent sheet.
+     * @param {boolean} reload Whether to reload any sheets that are
+     *     already registered.
+     */
+    this.registerSheet = function (uri, agent, reload) {
+        if (reload)
+            this.unregisterSheet(uri, agent);
+        uri = ios.newURI(uri, null, null);
+        if (reload || !sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET))
+            sss.loadAndRegisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET);
+    };
+
+    /**
+     * Unregister a sheet at the given URI.
+     *
+     * @param {string} uri The URI of the sheet to unregister.
+     */
+    this.unregisterSheet = function (uri, agent) {
+        uri = ios.newURI(uri, null, null);
+        if (sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET))
+            sss.unregisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET);
+    };
+}
+
+Module("styles", {
+    requires: ["config", "liberator", "storage", "util"],
+
+    init: function () {
+        let array = util.Array;
+        update(Styles.prototype, {
+            get sites() {
+                return array(Array.from(this.userSheets).map(([k, v]) => v.sites)).flatten().uniq().__proto__;
+            },
+            completeSite: function (context, content) {
+                context.anchored = false;
+                try {
+                    context.fork("current", 0, this, function (context) {
+                        context.title = ["Current Site"];
+                        context.completions = [
+                            [content.location.host, "Current Host"],
+                            [content.location.href, "Current URL"]
+                        ];
+                    });
+                }
+                catch (e) {}
+                context.fork("others", 0, this, function (context) {
+                    context.title = ["Site"];
+                    context.completions = styles.sites.map(s => [s, ""]);
+                });
+            }
+        });
+        return storage.newObject("styles", Styles, { store: false });
+    }
+}, {
+}, {
+    commands: function () {
+        commands.add(["sty[le]"],
+            "Add or list user styles",
+            function (args) {
+                let [filter, css] = args;
+                let name = args["-name"];
+
+                if (!css) {
+                    let list = Array.concat(Array.from(styles.userNames),
+                                            Array.from(styles.userSheets).filter(i => !i[1].name));
+                    let str = template.tabular([{ header: "", style: "min-width: 1em; text-align: center; font-weight: bold;", highlight: "Disabled" }, "Name", "Filter", "CSS"],
+                        iter(list.filter(([key, sheet]) =>
+                                     (!filter || sheet.sites.indexOf(filter) >= 0) &&
+                                     (!name || sheet.name == name))
+                                 .map(([key, sheet]) => [
+                                     sheet.enabled ? "" : "\u00d7",
+                                     key,
+                                     sheet.sites.join(","),
+                                     sheet.css
+                                 ])
+                        )
+                    );
+
+                    commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+                }
+                else {
+                    if ("-append" in args) {
+                        let sheet = styles.get(false, name);
+                        if (sheet) {
+                            filter = sheet.sites.concat(filter).join(",");
+                            css = sheet.css + " " + css;
+                        }
+                    }
+                    let err = styles.addSheet(false, name, filter, css);
+                    if (err)
+                        liberator.echoerr(err);
+                }
+            },
+            {
+                bang: true,
+                completer: function (context, args) {
+                    let compl = [];
+                    if (args.completeArg == 0)
+                        styles.completeSite(context, content);
+                    else if (args.completeArg == 1) {
+                        let sheet = styles.get(false, args["-name"]);
+                        if (sheet)
+                            context.completions = [[sheet.css.replace(/\n+/g, " "), "Current Value"]];
+                    }
+                },
+                hereDoc: true,
+                literal: 1,
+                options: [[["-name", "-n"], commands.OPTION_STRING, null, function () Array.from(styles.userNames).map(([k, v]) => [k, v.css])],
+                          [["-append", "-a"], commands.OPTION_NOARG]],
+                serial: function () {
+                    return Array.from(styles.userSheets)
+                                .map(([k, sty]) => ({
+                                   command: this.name,
+                                   bang: true,
+                                   options: sty.name ? { "-name": sty.name } : {},
+                                   arguments: [sty.sites.join(",")],
+                                   literalArg: sty.css
+                                }));
+                }
+            });
+
+        [
+            {
+                name: ["stylee[nable]", "stye[nable]"],
+                desc: "Enable a user style sheet",
+                action: function (sheet) sheet.enabled = true,
+                filter: function (sheet) !sheet.enabled
+            },
+            {
+                name: ["styled[isable]", "styd[isable]"],
+                desc: "Disable a user style sheet",
+                action: function (sheet) sheet.enabled = false,
+                filter: function (sheet) sheet.enabled
+            },
+            {
+                name: ["stylet[oggle]", "styt[oggle]"],
+                desc: "Toggle a user style sheet",
+                action: function (sheet) sheet.enabled = !sheet.enabled
+            },
+            {
+                name: ["dels[tyle]"],
+                desc: "Remove a user style sheet",
+                action: function (sheet) styles.removeSheet(sheet)
+            }
+        ].forEach(function (cmd) {
+            commands.add(cmd.name, cmd.desc,
+                function (args) {
+                    styles.findSheets(false, args["-name"], args[0], args.literalArg, args["-index"])
+                          .forEach(cmd.action);
+                },
+            {
+                completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); },
+                literal: 1,
+                options: [[["-index", "-i"], commands.OPTION_INT, null,
+                            function (context) {
+                                context.compare = CompletionContext.Sort.number;
+                                return Array.from(styles.userSheets)
+                                            .filter(([i, sheet]) => !cmd.filter && cmd.filter(sheet))
+                                            .map(([i, sheet]) =>
+                                                [i, `${sheet.sites.join(",")}: ${sheet.css.replace("\n", "\\n")}`]
+                                            );
+                            }],
+                          [["-name", "-n"],  commands.OPTION_STRING, null,
+                            function () Array.from(styles.userNames)
+                                             .filter(([name, sheet]) => !cmd.filter || cmd.filter(sheet))
+                                             .map(([name, sheet]) => [name, sheet.css])]]
+            });
+        });
+    },
+    completion: function () {
+        JavaScript.setCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]),
+            [ // Prototype: (system, name, filter, css, index)
+                null,
+                function (context, obj, args) args[0] ? styles.systemNames : styles.userNames,
+                function (context, obj, args) styles.completeSite(context, content),
+                null,
+                function (context, obj, args) args[0] ? styles.systemSheets : styles.userSheets
+            ]);
+    }
+});
+
+Module("highlight", {
+    requires: ["config", "styles"],
+
+    init: function () {
+        const self = storage.newObject("highlight", Highlights, { store: false });
+
+        if (self.CSS != Highlights.prototype.CSS) {
+            self.CSS = Highlights.prototype.CSS;
+            self.loadCSS(self.CSS);
+        }
+        return self;
+    }
+}, {
+}, {
+    commands: function () {
+        commands.add(["colo[rscheme]"],
+            "Load a color scheme",
+            function (args) {
+                let scheme = args[0];
+
+                if (scheme == "default")
+                    highlight.clear();
+                else
+                    liberator.assert(io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]),
+                        "No such color scheme: " + scheme);
+                autocommands.trigger("ColorScheme", { name: scheme });
+            },
+            {
+                argCount: "1",
+                completer: function (context) {
+                    context.title = ["Extra Completions"];
+                    context.completions = [
+                        ["default", "Clear all highlights"]
+                    ];
+                    context.fork("colorScheme", 0, completion, "colorScheme");
+                }
+            });
+
+        commands.add(["hi[ghlight]"],
+            "Set the style of certain display elements",
+            function (args) {
+                let style = `
+                    ;
+                    display: inline-block !important;
+                    position: static !important;
+                    margin: 0px !important; padding: 0px !important;
+                    width: 3em !important; min-width: 3em !important; max-width: 3em !important;
+                    height: 1em !important; min-height: 1em !important; max-height: 1em !important;
+                    overflow: hidden !important;
+                `;
+                let clear = args[0] == "clear";
+                if (clear)
+                    args.shift();
+
+                let [key, css] = args;
+                liberator.assert(!(clear && css), "Trailing characters");
+
+                if (!css && !clear) {
+                    // List matching keys
+                    let str = template.tabular(["Key", { header: "Sample", style: "text-align: center" }, "CSS"],
+                        iter(
+                            Array.from(iter(highlight))
+                                 .filter(h => !key || h.class.indexOf(key) > -1)
+                                 .map(h => [
+                                    h.class,
+                                    xml`<span style=${h.value + style}>XXX</span>`,
+                                    template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g, function (str) xml`<span style="font-weight: bold;">${str}</span>`)
+                                 ])
+                        )
+                    );
+
+                    commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+                    return;
+                }
+                if (!key && clear)
+                    highlight.clear();
+                else {
+                    let error = highlight.set(key, css, clear, "-append" in args);
+                    if (error)
+                        liberator.echoerr(error);
+                }
+            },
+            {
+                // TODO: add this as a standard highlight completion function?
+                completer: function (context, args) {
+                    // Complete a highlight group on :hi clear ...
+                    if (args.completeArg > 0 && args[0] == "clear")
+                        args.completeArg = args.completeArg > 1 ? -1 : 0;
+
+                    if (args.completeArg == 0)
+                        context.completions = Array.from(iter(highlight))
+                                                   .map(v => [v.class, v.value]);
+                    else if (args.completeArg == 1) {
+                        let hl = highlight.get(args[0]);
+                        if (hl)
+                            context.completions = [[hl.value.replace(/\n+/g, " "), "Current Value"], [hl.default || "", "Default Value"]];
+                    }
+                },
+                hereDoc: true,
+                literal: 1,
+                options: [[["-append", "-a"], commands.OPTION_NOARG]],
+                serial: function () {
+                    return Array.from(iter(highlight))
+                                .filter(v => v.value != v.default)
+                                .map(v => ({
+                                    command: this.name,
+                                    arguments: [v.class],
+                                    literalArg: v.value
+                                }));
+                }
+            });
+    },
+    completion: function () {
+        completion.colorScheme = function colorScheme(context) {
+            context.title = ["Color Scheme", "Runtime Path"];
+            context.keys = { text: function (f) f.leafName.replace(/\.vimp$/, ""), description: ".parent.path" };
+            context.completions = util.Array.flatten(
+                io.getRuntimeDirectories("colors").map(
+                    function (dir) dir.readDirectory().filter(
+                        function (file) /\.vimp$/.test(file.leafName))))
+
+        };
+
+        completion.highlightGroup = function highlightGroup(context) {
+            context.title = ["Highlight Group", "Value"];
+            context.completions = Array.from(iter(highlight)).map(v => [v.class, v.value]);
+        };
+    }
+});
+
+// vim: set fdm=marker sw=4 ts=4 et: