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