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