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