]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/styles.jsm
Import 1.0 supporting Firefox up to 14.*
[dactyl.git] / common / modules / styles.jsm
1 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
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("styles", {
9     exports: ["Style", "Styles", "styles"],
10     require: ["services", "util"]
11 }, this);
12
13 function cssUri(css) "chrome-data:text/css," + encodeURI(css);
14 var namespace = "@namespace html " + XHTML.uri.quote() + ";\n" +
15                 "@namespace xul " + XUL.uri.quote() + ";\n" +
16                 "@namespace dactyl " + NS.uri.quote() + ";\n";
17
18 var Sheet = Struct("name", "id", "sites", "css", "hive", "agent");
19 Sheet.liveProperty = function (name) {
20     let i = this.prototype.members[name];
21     this.prototype.__defineGetter__(name, function () this[i]);
22     this.prototype.__defineSetter__(name, function (val) {
23         if (isArray(val))
24             val = Array.slice(val);
25         if (isArray(val))
26             Object.freeze(val);
27         this[i] = val;
28         this.enabled = this.enabled;
29     });
30 }
31 Sheet.liveProperty("agent");
32 Sheet.liveProperty("css");
33 Sheet.liveProperty("sites");
34 update(Sheet.prototype, {
35     formatSites: function (uris)
36           template.map(this.sites,
37                        function (filter) <span highlight={uris.some(Styles.matchFilter(filter)) ? "Filter" : ""}>{filter}</span>,
38                        <>,</>),
39
40     remove: function () { this.hive.remove(this); },
41
42     get uri() "dactyl://style/" + this.id + "/" + this.hive.name + "/" + (this.name || ""),
43
44     get enabled() this._enabled,
45     set enabled(on) {
46         if (on != this._enabled || this.fullCSS != this._fullCSS) {
47             if (on)
48                 this.enabled = false;
49             else if (!this._fullCSS)
50                 return;
51
52             let meth = on ? "registerSheet" : "unregisterSheet";
53             styles[meth](this.uri, on ? this.agent : this._agent);
54
55             this._agent = this.agent;
56             this._enabled = Boolean(on);
57             this._fullCSS = this.fullCSS;
58         }
59     },
60
61     match: function (uri) {
62         if (isString(uri))
63             uri = util.newURI(uri);
64         return this.sites.some(function (site) Styles.matchFilter(site, uri));
65     },
66
67     get fullCSS() {
68         let filter = this.sites;
69         let css = this.css;
70
71         let preamble = "/* " + this.uri + (this.agent ? " (agent)" : "") + " */\n\n" + namespace + "\n";
72         if (filter[0] == "*")
73             return preamble + css;
74
75         let selectors = filter.map(function (part)
76                                     !/^(?:[a-z-]+[:*]|[a-z-.]+$)/i.test(part) ? "regexp(" + Styles.quote(".*(?:" + part + ").*") + ")" :
77                                        (/[*]$/.test(part)   ? "url-prefix" :
78                                         /[\/:]/.test(part)  ? "url"
79                                                             : "domain")
80                                        + '(' + Styles.quote(part.replace(/\*$/, "")) + ')')
81                               .join(",\n               ");
82
83         return preamble + "@-moz-document " + selectors + " {\n\n" + css + "\n\n}\n";
84     }
85 });
86
87 var Hive = Class("Hive", {
88     init: function (name, persist) {
89         this.name = name;
90         this.sheets = [];
91         this.names = {};
92         this.refs = [];
93         this.persist = persist;
94     },
95
96     get modifiable() this.name !== "system",
97
98     addRef: function (obj) {
99         this.refs.push(util.weakReference(obj));
100         this.dropRef(null);
101     },
102     dropRef: function (obj) {
103         this.refs = this.refs.filter(function (ref) ref.get() && ref.get() !== obj);
104         if (!this.refs.length) {
105             this.cleanup();
106             styles.hives = styles.hives.filter(function (h) h !== this, this);
107         }
108     },
109
110     cleanup: function cleanup() {
111         for (let sheet in values(this.sheets))
112             sheet.enabled = false;
113     },
114
115     __iterator__: function () Iterator(this.sheets),
116
117     get sites() array(this.sheets).map(function (s) s.sites).flatten().uniq().array,
118
119     /**
120      * Add a new style sheet.
121      *
122      * @param {string} name The name given to the style sheet by
123      *     which it may be later referenced.
124      * @param {string} filter The sites to which this sheet will
125      *     apply. Can be a domain name or a URL. Any URL ending in
126      *     "*" is matched as a prefix.
127      * @param {string} css The CSS to be applied.
128      * @param {boolean} agent If true, the sheet is installed as an
129      *     agent sheet.
130      * @param {boolean} lazy If true, the sheet is not initially enabled.
131      * @returns {Sheet}
132      */
133     add: function add(name, filter, css, agent, lazy) {
134
135         if (!isArray(filter))
136             filter = filter.split(",");
137         if (name && name in this.names) {
138             var sheet = this.names[name];
139             sheet.agent = agent;
140             sheet.css = String(css);
141             sheet.sites = filter;
142         }
143         else {
144             sheet = Sheet(name, styles._id++, filter.filter(util.identity), String(css), this, agent);
145             this.sheets.push(sheet);
146         }
147
148         styles.allSheets[sheet.id] = sheet;
149
150         if (!lazy)
151             sheet.enabled = true;
152
153         if (name)
154             this.names[name] = sheet;
155         return sheet;
156     },
157
158     /**
159      * Get a sheet with a given name or index.
160      *
161      * @param {string or number} sheet The sheet to retrieve. Strings indicate
162      *     sheet names, while numbers indicate indices.
163      */
164     get: function get(sheet) {
165         if (typeof sheet === "number")
166             return this.sheets[sheet];
167         return this.names[sheet];
168     },
169
170     /**
171      * Find sheets matching the parameters. See {@link #addSheet}
172      * for parameters.
173      *
174      * @param {string} name
175      * @param {string} filter
176      * @param {string} css
177      * @param {number} index
178      */
179     find: function find(name, filter, css, index) {
180         // Grossly inefficient.
181         let matches = [k for ([k, v] in Iterator(this.sheets))];
182         if (index)
183             matches = String(index).split(",").filter(function (i) i in this.sheets, this);
184         if (name)
185             matches = matches.filter(function (i) this.sheets[i].name == name, this);
186         if (css)
187             matches = matches.filter(function (i) this.sheets[i].css == css, this);
188         if (filter)
189             matches = matches.filter(function (i) this.sheets[i].sites.indexOf(filter) >= 0, this);
190         return matches.map(function (i) this.sheets[i], this);
191     },
192
193     /**
194      * Remove a style sheet. See {@link #addSheet} for parameters.
195      * In cases where *filter* is supplied, the given filters are removed from
196      * matching sheets. If any remain, the sheet is left in place.
197      *
198      * @param {string} name
199      * @param {string} filter
200      * @param {string} css
201      * @param {number} index
202      */
203     remove: function remove(name, filter, css, index) {
204         let self = this;
205         if (arguments.length == 1) {
206             var matches = [name];
207             name = null;
208         }
209
210         if (filter && filter.indexOf(",") > -1)
211             return filter.split(",").reduce(
212                 function (n, f) n + self.removeSheet(name, f, index), 0);
213
214         if (filter == undefined)
215             filter = "";
216
217         if (!matches)
218             matches = this.findSheets(name, filter, css, index);
219         if (matches.length == 0)
220             return null;
221
222         for (let [, sheet] in Iterator(matches.reverse())) {
223             if (filter) {
224                 let sites = sheet.sites.filter(function (f) f != filter);
225                 if (sites.length) {
226                     sheet.sites = sites;
227                     continue;
228                 }
229             }
230             sheet.enabled = false;
231             if (sheet.name)
232                 delete this.names[sheet.name];
233             delete styles.allSheets[sheet.id];
234         }
235         this.sheets = this.sheets.filter(function (s) matches.indexOf(s) == -1);
236         return matches.length;
237     },
238 });
239
240 /**
241  * Manages named and unnamed user style sheets, which apply to both
242  * chrome and content pages.
243  *
244  * @author Kris Maglione <maglione.k@gmail.com>
245  */
246 var Styles = Module("Styles", {
247     Local: function (dactyl, modules, window) ({
248         cleanup: function () {}
249     }),
250
251     init: function () {
252         this._id = 0;
253         this.cleanup();
254         this.allSheets = {};
255
256         update(services["dactyl:"].providers, {
257             "style": function styleProvider(uri, path) {
258                 let id = parseInt(path);
259                 if (Set.has(styles.allSheets, id))
260                     return ["text/css", styles.allSheets[id].fullCSS];
261                 return null;
262             }
263         });
264     },
265
266     cleanup: function cleanup() {
267         for each (let hive in this.hives)
268             util.trapErrors("cleanup", hive);
269         this.hives = [];
270         this.user = this.addHive("user", this, true);
271         this.system = this.addHive("system", this, false);
272     },
273
274     addHive: function addHive(name, ref, persist) {
275         let hive = array.nth(this.hives, function (h) h.name === name, 0);
276         if (!hive) {
277             hive = Hive(name, persist);
278             this.hives.push(hive);
279         }
280         hive.persist = persist;
281         if (ref)
282             hive.addRef(ref);
283         return hive;
284     },
285
286     __iterator__: function () Iterator(this.user.sheets.concat(this.system.sheets)),
287
288     _proxy: function (name, args)
289         let (obj = this[args[0] ? "system" : "user"])
290             obj[name].apply(obj, Array.slice(args, 1)),
291
292     addSheet: deprecated("Styles#{user,system}.add", function addSheet() this._proxy("add", arguments)),
293     findSheets: deprecated("Styles#{user,system}.find", function findSheets() this._proxy("find", arguments)),
294     get: deprecated("Styles#{user,system}.get", function get() this._proxy("get", arguments)),
295     removeSheet: deprecated("Styles#{user,system}.remove", function removeSheet() this._proxy("remove", arguments)),
296
297     userSheets: Class.Property({ get: deprecated("Styles#user.sheets", function userSheets() this.user.sheets) }),
298     systemSheets: Class.Property({ get: deprecated("Styles#system.sheets", function systemSheets() this.system.sheets) }),
299     userNames: Class.Property({ get: deprecated("Styles#user.names", function userNames() this.user.names) }),
300     systemNames: Class.Property({ get: deprecated("Styles#system.names", function systemNames() this.system.names) }),
301     sites: Class.Property({ get: deprecated("Styles#user.sites", function sites() this.user.sites) }),
302
303     list: function list(content, sites, name, hives) {
304         const { commandline, dactyl } = this.modules;
305
306         hives = hives || styles.hives.filter(function (h) h.modifiable && h.sheets.length);
307
308         function sheets(group)
309             group.sheets.slice()
310                  .filter(function (sheet) (!name || sheet.name === name) &&
311                                           (!sites || sites.every(function (s) sheet.sites.indexOf(s) >= 0)))
312                  .sort(function (a, b) a.name && b.name ? String.localeCompare(a.name, b.name)
313                                                         : !!b.name - !!a.name || a.id - b.id);
314
315         let uris = util.visibleURIs(content);
316
317         let list = <table>
318                 <tr highlight="Title">
319                     <td/>
320                     <td/>
321                     <td style="padding-right: 1em;">{_("title.Name")}</td>
322                     <td style="padding-right: 1em;">{_("title.Filter")}</td>
323                     <td style="padding-right: 1em;">{_("title.CSS")}</td>
324                 </tr>
325                 <col style="min-width: 4em; padding-right: 1em;"/>
326                 <col style="min-width: 1em; text-align: center; color: red; font-weight: bold;"/>
327                 <col style="padding: 0 1em 0 1ex; vertical-align: top;"/>
328                 <col style="padding: 0 1em 0 0; vertical-align: top;"/>
329                 {
330                     template.map(hives, function (hive) let (i = 0)
331                         <tr style="height: .5ex;"/> +
332                         template.map(sheets(hive), function (sheet)
333                             <tr>
334                                 <td highlight="Title">{!i++ ? hive.name : ""}</td>
335                                 <td>{sheet.enabled ? "" : UTF8("×")}</td>
336                                 <td>{sheet.name || hive.sheets.indexOf(sheet)}</td>
337                                 <td>{sheet.formatSites(uris)}</td>
338                                 <td>{sheet.css}</td>
339                             </tr>) +
340                         <tr style="height: .5ex;"/>)
341                 }
342                 </table>;
343
344         // TODO: Move this to an ItemList to show this automatically
345         if (list.*.length() === list.text().length() + 5)
346             dactyl.echomsg(_("style.none"));
347         else
348             commandline.commandOutput(list);
349     },
350
351     registerSheet: function registerSheet(url, agent, reload) {
352         let uri = services.io.newURI(url, null, null);
353         if (reload)
354             this.unregisterSheet(url, agent);
355
356         let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
357         if (reload || !services.stylesheet.sheetRegistered(uri, type))
358             services.stylesheet.loadAndRegisterSheet(uri, type);
359     },
360
361     unregisterSheet: function unregisterSheet(url, agent) {
362         let uri = services.io.newURI(url, null, null);
363         let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
364         if (services.stylesheet.sheetRegistered(uri, type))
365             services.stylesheet.unregisterSheet(uri, type);
366     },
367 }, {
368     append: function (dest, src, sort) {
369         let props = {};
370         for each (let str in [dest, src])
371             for (let prop in Styles.propertyIter(str))
372                 props[prop.name] = prop.value;
373
374         let val = Object.keys(props)[sort ? "sort" : "slice"]()
375                         .map(function (prop) prop + ": " + props[prop] + ";")
376                         .join(" ");
377
378         if (/^\s*(\/\*.*?\*\/)/.exec(src))
379             val = RegExp.$1 + " " + val;
380         return val;
381     },
382
383     completeSite: function (context, content, group) {
384         group = group || styles.user;
385         context.anchored = false;
386         try {
387             context.fork("current", 0, this, function (context) {
388                 context.title = ["Current Site"];
389                 context.completions = [
390                     [content.location.host, /*L*/"Current Host"],
391                     [content.location.href, /*L*/"Current URL"]
392                 ];
393             });
394         }
395         catch (e) {}
396
397         let uris = util.visibleURIs(content);
398
399         context.generate = function () values(group.sites);
400
401         context.keys.text = util.identity;
402         context.keys.description = function (site) this.sheets.length + /*L*/" sheet" + (this.sheets.length == 1 ? "" : "s") + ": " +
403             array.compact(this.sheets.map(function (s) s.name)).join(", ");
404         context.keys.sheets = function (site) group.sheets.filter(function (s) s.sites.indexOf(site) >= 0);
405         context.keys.active = function (site) uris.some(Styles.matchFilter(site));
406
407         Styles.splitContext(context, "Sites");
408     },
409
410     /**
411      * A curried function which determines which host names match a
412      * given stylesheet filter. When presented with one argument,
413      * returns a matcher function which, given one nsIURI argument,
414      * returns true if that argument matches the given filter. When
415      * given two arguments, returns true if the second argument matches
416      * the given filter.
417      *
418      * @param {string} filter The URI filter to match against.
419      * @param {nsIURI} uri The location to test.
420      * @returns {nsIURI -> boolean}
421      */
422     matchFilter: function (filter) {
423         filter = filter.trim();
424
425         if (filter === "*")
426             var test = function test(uri) true;
427         else if (!/^(?:[a-z-]+:|[a-z-.]+$)/.test(filter)) {
428             let re = util.regexp(filter);
429             test = function test(uri) re.test(uri.spec);
430         }
431         else if (/[*]$/.test(filter)) {
432             let re = RegExp("^" + util.regexp.escape(filter.substr(0, filter.length - 1)));
433             test = function test(uri) re.test(uri.spec);
434         }
435         else if (/[\/:]/.test(filter))
436             test = function test(uri) uri.spec === filter;
437         else
438             test = function test(uri) { try { return util.isSubdomain(uri.host, filter); } catch (e) { return false; } };
439         test.toString = function toString() filter;
440         test.key = filter;
441         if (arguments.length < 2)
442             return test;
443         return test(arguments[1]);
444     },
445
446     splitContext: function splitContext(context, title) {
447         for (let item in Iterator({ Active: true, Inactive: false })) {
448             let [name, active] = item;
449             context.split(name, null, function (context) {
450                 context.title[0] = /*L*/name + " " + (title || "Sheets");
451                 context.filters.push(function (item) !!item.active == active);
452             });
453         }
454     },
455
456     propertyIter: function (str, always) {
457         let i = 0;
458         for (let match in this.propertyPattern.iterate(str)) {
459             if (match.value || always && match.name || match.wholeMatch === match.preSpace && always && !i++)
460                 yield match;
461             if (!/;/.test(match.postSpace))
462                 break;
463         }
464     },
465
466     propertyPattern: util.regexp(<![CDATA[
467             (?:
468                 (?P<preSpace> <space>*)
469                 (?P<name> [-a-z]*)
470                 (?:
471                     <space>* : \s* (?P<value>
472                         (?:
473                             [-\w]+
474                             (?:
475                                 \s* \( \s*
476                                     (?: <string> | [^)]*  )
477                                 \s* (?: \) | $)
478                             )?
479                             \s*
480                             | \s* <string> \s*
481                             | <space>*
482                             | [^;}]*
483                         )*
484                     )
485                 )?
486             )
487             (?P<postSpace> <space>* (?: ; | $) )
488         ]]>, "gix",
489         {
490             space: /(?: \s | \/\* .*? \*\/ )/,
491             string: /(?:" (?:[^\\"]|\\.)* (?:"|$) | '(?:[^\\']|\\.)* (?:'|$) )/
492         }),
493
494     patterns: memoize({
495         get property() util.regexp(<![CDATA[
496                 (?:
497                     (?P<preSpace> <space>*)
498                     (?P<name> [-a-z]*)
499                     (?:
500                         <space>* : \s* (?P<value>
501                             <token>*
502                         )
503                     )?
504                 )
505                 (?P<postSpace> <space>* (?: ; | $) )
506             ]]>, "gix", this),
507
508         get function() util.regexp(<![CDATA[
509                 (?P<function>
510                     \s* \( \s*
511                         (?: <string> | [^)]*  )
512                     \s* (?: \) | $)
513                 )
514             ]]>, "gx", this),
515
516         space: /(?: \s | \/\* .*? \*\/ )/,
517
518         get string() util.regexp(<![CDATA[
519                 (?P<string>
520                     " (?:[^\\"]|\\.)* (?:"|$) |
521                     ' (?:[^\\']|\\.)* (?:'|$)
522                 )
523             ]]>, "gx", this),
524
525         get token() util.regexp(<![CDATA[
526             (?P<token>
527                 (?P<word> [-\w]+)
528                 <function>?
529                 \s*
530                 | (?P<important> !important\b)
531                 | \s* <string> \s*
532                 | <space>+
533                 | [^;}\s]+
534             )
535         ]]>, "gix", this)
536     }),
537
538     /**
539      * Quotes a string for use in CSS stylesheets.
540      *
541      * @param {string} str
542      * @returns {string}
543      */
544     quote: function quote(str) {
545         return '"' + str.replace(/([\\"])/g, "\\$1").replace(/\n/g, "\\00000a") + '"';
546     },
547 }, {
548     commands: function (dactyl, modules, window) {
549         const { commands, contexts, styles } = modules;
550
551         function sheets(context, args, filter) {
552             let uris = util.visibleURIs(window.content);
553             context.compare = modules.CompletionContext.Sort.number;
554             context.generate = function () args["-group"].sheets;
555             context.keys.active = function (sheet) uris.some(sheet.closure.match);
556             context.keys.description = function (sheet) <>{sheet.formatSites(uris)}: {sheet.css.replace("\n", "\\n")}</>
557             if (filter)
558                 context.filters.push(function ({ item }) filter(item));
559             Styles.splitContext(context);
560         }
561
562         function nameFlag(filter) ({
563             names: ["-name", "-n"],
564             description: "The name of this stylesheet",
565             type: modules.CommandOption.STRING,
566             completer: function (context, args) {
567                 context.keys.text = function (sheet) sheet.name;
568                 context.filters.unshift(function ({ item }) item.name);
569                 sheets(context, args, filter);
570             }
571         });
572
573         commands.add(["sty[le]"],
574             "Add or list user styles",
575             function (args) {
576                 let [filter, css] = args;
577
578                 if (!css)
579                     styles.list(window.content, filter ? filter.split(",") : null, args["-name"], args.explicitOpts["-group"] ? [args["-group"]] : null);
580                 else {
581                     util.assert(args["-group"].modifiable && args["-group"].hive.modifiable,
582                                 _("group.cantChangeBuiltin", _("style.styles")));
583
584                     if (args["-append"]) {
585                         let sheet = args["-group"].get(args["-name"]);
586                         if (sheet) {
587                             filter = array(sheet.sites).concat(filter).uniq().join(",");
588                             css = sheet.css + " " + css;
589                         }
590                     }
591                     let style = args["-group"].add(args["-name"], filter, css, args["-agent"]);
592
593                     if (args["-nopersist"] || !args["-append"] || style.persist === undefined)
594                         style.persist = !args["-nopersist"];
595                 }
596             },
597             {
598                 completer: function (context, args) {
599                     let compl = [];
600                     let sheet = args["-group"].get(args["-name"]);
601                     if (args.completeArg == 0) {
602                         if (sheet)
603                             context.completions = [[sheet.sites.join(","), "Current Value"]];
604                         context.fork("sites", 0, Styles, "completeSite", window.content, args["-group"]);
605                     }
606                     else if (args.completeArg == 1) {
607                         if (sheet)
608                             context.completions = [
609                                 [sheet.css, _("option.currentValue")]
610                             ];
611                         context.fork("css", 0, modules.completion, "css");
612                     }
613                 },
614                 hereDoc: true,
615                 literal: 1,
616                 options: [
617                     { names: ["-agent", "-A"],  description: "Apply style as an Agent sheet" },
618                     { names: ["-append", "-a"], description: "Append site filter and css to an existing, matching sheet" },
619                     contexts.GroupFlag("styles"),
620                     nameFlag(),
621                     { names: ["-nopersist", "-N"], description: "Do not save this style to an auto-generated RC file" }
622                 ],
623                 serialize: function ()
624                     array(styles.hives)
625                         .filter(function (hive) hive.persist)
626                         .map(function (hive)
627                              hive.sheets.filter(function (style) style.persist)
628                                  .sort(function (a, b) String.localeCompare(a.name || "", b.name || ""))
629                                  .map(function (style) ({
630                                     command: "style",
631                                     arguments: [style.sites.join(",")],
632                                     literalArg: style.css,
633                                     options: {
634                                         "-group": hive.name == "user" ? undefined : hive.name,
635                                         "-name": style.name || undefined
636                                     }
637                                 })))
638                         .flatten().array
639             });
640
641         [
642             {
643                 name: ["stylee[nable]", "stye[nable]"],
644                 desc: "Enable a user style sheet",
645                 action: function (sheet) sheet.enabled = true,
646                 filter: function (sheet) !sheet.enabled
647             },
648             {
649                 name: ["styled[isable]", "styd[isable]"],
650                 desc: "Disable a user style sheet",
651                 action: function (sheet) sheet.enabled = false,
652                 filter: function (sheet) sheet.enabled
653             },
654             {
655                 name: ["stylet[oggle]", "styt[oggle]"],
656                 desc: "Toggle a user style sheet",
657                 action: function (sheet) sheet.enabled = !sheet.enabled
658             },
659             {
660                 name: ["dels[tyle]"],
661                 desc: "Remove a user style sheet",
662                 action: function (sheet) sheet.remove(),
663             }
664         ].forEach(function (cmd) {
665             commands.add(cmd.name, cmd.desc,
666                 function (args) {
667                     dactyl.assert(args.bang ^ !!(args[0] || args[1] || args["-name"] || args["-index"]),
668                                   _("error.argumentOrBang"));
669
670                     args["-group"].find(args["-name"], args[0], args.literalArg, args["-index"])
671                                   .forEach(cmd.action);
672                 }, {
673                     bang: true,
674                     completer: function (context, args) {
675                         let uris = util.visibleURIs(window.content);
676
677                         Styles.completeSite(context, window.content, args["-group"]);
678                         if (cmd.filter)
679                             context.filters.push(function ({ sheets }) sheets.some(cmd.filter));
680                     },
681                     literal: 1,
682                     options: [
683                         contexts.GroupFlag("styles"),
684                         {
685                             names: ["-index", "-i"],
686                             type: modules.CommandOption.INT,
687                             completer: function (context, args) {
688                                 context.keys.text = function (sheet) args["-group"].sheets.indexOf(sheet);
689                                 sheets(context, args, cmd.filter);
690                             }
691                         },
692                         nameFlag(cmd.filter)
693                     ]
694                 });
695         });
696     },
697     contexts: function (dactyl, modules, window) {
698         modules.contexts.Hives("styles",
699             Class("LocalHive", Contexts.Hive, {
700                 init: function init(group) {
701                     init.superapply(this, arguments);
702                     this.hive = styles.addHive(group.name, this, this.persist);
703                 },
704
705                 get names() this.hive.names,
706                 get sheets() this.hive.sheets,
707                 get sites() this.hive.sites,
708
709                 __noSuchMethod__: function __noSuchMethod__(meth, args) {
710                     return this.hive[meth].apply(this.hive, args);
711                 },
712
713                 destroy: function () {
714                     this.hive.dropRef(this);
715                 }
716             }));
717     },
718     completion: function (dactyl, modules, window) {
719         const names = Array.slice(DOM(<div/>, window.document).style);
720         modules.completion.css = function (context) {
721             context.title = ["CSS Property"];
722             context.keys = { text: function (p) p + ":", description: function () "" };
723
724             for (let match in Styles.propertyIter(context.filter, true))
725                 var lastMatch = match;
726
727             if (lastMatch != null && !lastMatch.value && !lastMatch.postSpace) {
728                 context.advance(lastMatch.index + lastMatch.preSpace.length);
729                 context.completions = names;
730             }
731         };
732     },
733     javascript: function (dactyl, modules, window) {
734         modules.JavaScript.setCompleter(["get", "add", "remove", "find"].map(function (m) Hive.prototype[m]),
735             [ // Prototype: (name, filter, css, index)
736                 function (context, obj, args) this.names,
737                 function (context, obj, args) Styles.completeSite(context, window.content),
738                 null,
739                 function (context, obj, args) this.sheets
740             ]);
741     },
742     template: function () {
743         let patterns = Styles.patterns;
744
745         template.highlightCSS = function highlightCSS(css) {
746             XML.prettyPrinting = XML.ignoreWhitespace = false;
747
748             return this.highlightRegexp(css, patterns.property, function (match) {
749                 if (!match.length)
750                     return <></>;
751                 return <>{match.preSpace}{template.filter(match.name)}: {
752
753                     template.highlightRegexp(match.value, patterns.token, function (match) {
754                         if (match.function)
755                             return <>{template.filter(match.word)}{
756                                 template.highlightRegexp(match.function, patterns.string,
757                                     function (match) <span highlight="String">{match.string}</span>)
758                             }</>;
759                         if (match.important == "!important")
760                             return <span highlight="String">{match.important}</span>;
761                         if (match.string)
762                             return <span highlight="String">{match.string}</span>;
763                         return template.highlightRegexp(match.wholeMatch, /^(\d+)(em|ex|px|in|cm|mm|pt|pc)?/g,
764                                                         function (m, n, u) <><span highlight="Number">{n}</span><span highlight="Object">{u || ""}</span></>);
765                     })
766
767                 }{ match.postSpace }</>
768             })
769         }
770     },
771 });
772
773 endModule();
774
775 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
776
777 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: