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