1 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
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"]
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";
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) {
25 val = Array.slice(val);
29 this.enabled = this.enabled;
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>,
41 remove: function () { this.hive.remove(this); },
43 get uri() "dactyl://style/" + this.id + "/" + this.hive.name + "/" + (this.name || ""),
45 get enabled() this._enabled,
47 if (on != this._enabled || this.fullCSS != this._fullCSS) {
50 else if (!this._fullCSS)
53 let meth = on ? "registerSheet" : "unregisterSheet";
54 styles[meth](this.uri, on ? this.agent : this._agent);
56 this._agent = this.agent;
57 this._enabled = Boolean(on);
58 this._fullCSS = this.fullCSS;
62 match: function (uri) {
64 uri = util.newURI(uri);
65 return this.sites.some(function (site) Styles.matchFilter(site, uri));
69 let filter = this.sites;
72 return namespace + css;
74 let selectors = filter.map(function (part)
75 (/[*]$/.test(part) ? "url-prefix" :
76 /[\/:]/.test(part) ? "url"
78 + '("' + part.replace(/"/g, "%22").replace(/\*$/, "") + '")')
80 return "/* " + this.uri + (this.agent ? " (agent)" : "") + " */\n\n"
81 + namespace + "\n@-moz-document " + selectors + " {\n\n" + css + "\n\n}\n";
85 var Hive = Class("Hive", {
86 init: function (name, persist) {
91 this.persist = persist;
94 get modifiable() this.name !== "system",
96 addRef: function (obj) {
97 this.refs.push(Cu.getWeakReference(obj));
100 dropRef: function (obj) {
101 this.refs = this.refs.filter(function (ref) ref.get() && ref.get() !== obj);
102 if (!this.refs.length) {
104 styles.hives = styles.hives.filter(function (h) h !== this, this);
108 cleanup: function cleanup() {
109 for (let sheet in values(this.sheets))
110 sheet.enabled = false;
113 __iterator__: function () Iterator(this.sheets),
115 get sites() array(this.sheets).map(function (s) s.sites).flatten().uniq().array,
118 * Add a new style sheet.
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
128 * @param {boolean} lazy If true, the sheet is not initially enabled.
131 add: function add(name, filter, css, agent, lazy) {
133 if (!isArray(filter))
134 filter = filter.split(",");
135 if (name && name in this.names) {
136 var sheet = this.names[name];
138 sheet.css = String(css);
139 sheet.sites = filter;
142 sheet = Sheet(name, styles._id++, filter.filter(util.identity), String(css), this, agent);
143 this.sheets.push(sheet);
146 styles.allSheets[sheet.id] = sheet;
149 sheet.enabled = true;
152 this.names[name] = sheet;
157 * Get a sheet with a given name or index.
159 * @param {string or number} sheet The sheet to retrieve. Strings indicate
160 * sheet names, while numbers indicate indices.
162 get: function get(sheet) {
163 if (typeof sheet === "number")
164 return this.sheets[sheet];
165 return this.names[sheet];
169 * Find sheets matching the parameters. See {@link #addSheet}
172 * @param {string} name
173 * @param {string} filter
174 * @param {string} css
175 * @param {number} index
177 find: function find(name, filter, css, index) {
178 // Grossly inefficient.
179 let matches = [k for ([k, v] in Iterator(this.sheets))];
181 matches = String(index).split(",").filter(function (i) i in this.sheets, this);
183 matches = matches.filter(function (i) this.sheets[i].name == name, this);
185 matches = matches.filter(function (i) this.sheets[i].css == css, this);
187 matches = matches.filter(function (i) this.sheets[i].sites.indexOf(filter) >= 0, this);
188 return matches.map(function (i) this.sheets[i], this);
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.
196 * @param {string} name
197 * @param {string} filter
198 * @param {string} css
199 * @param {number} index
201 remove: function remove(name, filter, css, index) {
203 if (arguments.length == 1) {
204 var matches = [name];
208 if (filter && filter.indexOf(",") > -1)
209 return filter.split(",").reduce(
210 function (n, f) n + self.removeSheet(name, f, index), 0);
212 if (filter == undefined)
216 matches = this.findSheets(name, filter, css, index);
217 if (matches.length == 0)
220 for (let [, sheet] in Iterator(matches.reverse())) {
222 let sites = sheet.sites.filter(function (f) f != filter);
228 sheet.enabled = false;
230 delete this.names[sheet.name];
231 delete styles.allSheets[sheet.id];
233 this.sheets = this.sheets.filter(function (s) matches.indexOf(s) == -1);
234 return matches.length;
239 * Manages named and unnamed user style sheets, which apply to both
240 * chrome and content pages.
242 * @author Kris Maglione <maglione.k@gmail.com>
244 var Styles = Module("Styles", {
245 Local: function (dactyl, modules, window) ({
246 cleanup: function () {}
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))];
262 cleanup: function cleanup() {
263 for each (let hive in this.hives)
264 util.trapErrors("cleanup", hive);
266 this.user = this.addHive("user", this, true);
267 this.system = this.addHive("system", this, false);
270 addHive: function addHive(name, ref, persist) {
271 let hive = array.nth(this.hives, function (h) h.name === name, 0);
273 hive = Hive(name, persist);
274 this.hives.push(hive);
276 hive.persist = persist;
282 __iterator__: function () Iterator(this.user.sheets.concat(this.system.sheets)),
284 _proxy: function (name, args)
285 let (obj = this[args[0] ? "system" : "user"])
286 obj[name].apply(obj, Array.slice(args, 1)),
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)),
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) }),
299 list: function list(content, filter, name, hives) {
300 const { commandline, dactyl } = this.modules;
302 hives = hives || styles.hives.filter(function (h) h.modifiable && h.sheets.length);
304 function sheets(group)
306 .sort(function (a, b) a.name && b.name ? String.localeCompare(a.name, b.name)
307 : !!b.name - !!a.name || a.id - b.id);
309 let uris = util.visibleURIs(content);
312 <tr highlight="Title">
315 <td style="padding-right: 1em;">Name</td>
316 <td style="padding-right: 1em;">Filter</td>
317 <td style="padding-right: 1em;">CSS</td>
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;"/>
324 template.map(hives, function (hive) let (i = 0)
325 <tr style="height: .5ex;"/> +
326 template.map(sheets(hive), function (sheet)
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>
334 <tr style="height: .5ex;"/>)
338 // TODO: Move this to an ItemList to show this automatically
339 if (list.*.length() === list.text().length() + 5)
340 dactyl.echomsg(_("style.none"));
342 commandline.commandOutput(list);
345 registerSheet: function registerSheet(url, agent, reload) {
346 let uri = services.io.newURI(url, null, null);
348 this.unregisterSheet(url, agent);
350 let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
351 if (reload || !services.stylesheet.sheetRegistered(uri, type))
352 services.stylesheet.loadAndRegisterSheet(uri, type);
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);
362 append: function (dest, src, sort) {
364 for each (let str in [dest, src])
365 for (let prop in Styles.propertyIter(str))
366 props[prop.name] = prop.value;
368 return Object.keys(props)[sort ? "sort" : "slice"]()
369 .map(function (prop) prop + ": " + props[prop] + ";")
373 completeSite: function (context, content, group) {
374 group = group || styles.user;
375 context.anchored = false;
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"]
387 let uris = util.visibleURIs(content);
389 context.generate = function () values(group.sites);
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));
397 Styles.splitContext(context, "Sites");
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
408 * @param {string} filter The URI filter to match against.
409 * @param {nsIURI} uri The location to test.
410 * @returns {nsIURI -> boolean}
412 matchFilter: function (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);
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);
423 else if (/[\/:]/.test(filter))
424 test = function test(uri) uri.spec === filter;
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)
430 return test(arguments[1]);
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);
443 propertyIter: function (str, always) {
445 for (let match in this.propertyPattern.iterate(str)) {
446 if (match.value || always && match.name || match.wholeMatch === match.preSpace && always && !i++)
448 if (!/;/.test(match.postSpace))
453 propertyPattern: util.regexp(<![CDATA[
455 (?P<preSpace> <space>*)
458 <space>* : \s* (?P<value>
463 (?: <string> | [^)]* )
474 (?P<postSpace> <space>* (?: ; | $) )
477 space: /(?: \s | \/\* .*? \*\/ )/,
478 string: /(?:" (?:[^\\"]|\\.)* (?:"|$) | '(?:[^\\']|\\.)* (?:'|$) )/
482 get property() util.regexp(<![CDATA[
484 (?P<preSpace> <space>*)
487 <space>* : \s* (?P<value>
492 (?P<postSpace> <space>* (?: ; | $) )
495 get function() util.regexp(<![CDATA[
498 (?: <string> | [^)]* )
503 space: /(?: \s | \/\* .*? \*\/ )/,
505 get string() util.regexp(<![CDATA[
507 " (?:[^\\"]|\\.)* (?:"|$) |
508 ' (?:[^\\']|\\.)* (?:'|$)
512 get token() util.regexp(<![CDATA[
517 | (?P<important> !important\b)
525 commands: function (dactyl, modules, window) {
526 const { commands, contexts, styles } = modules;
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")}</>
535 context.filters.push(function ({ item }) filter(item));
536 Styles.splitContext(context);
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);
550 commands.add(["sty[le]"],
551 "Add or list user styles",
553 let [filter, css] = args;
556 styles.list(window.content, filter, args["-name"], args.explicitOpts["-group"] ? [args["-group"]] : null);
558 util.assert(args["-group"].modifiable && args["-group"].hive.modifiable,
559 "Cannot modify styles in the builtin group");
561 if (args["-append"]) {
562 let sheet = args["-group"].get(args["-name"]);
564 filter = sheet.sites.concat(filter).join(",");
565 css = sheet.css + " " + css;
569 let style = args["-group"].add(args["-name"], filter, css, args["-agent"]);
571 if (args["-nopersist"] || !args["-append"] || style.persist === undefined)
572 style.persist = !args["-nopersist"];
577 completer: function (context, args) {
579 let sheet = args["-group"].get(args["-name"]);
580 if (args.completeArg == 0) {
582 context.completions = [[sheet.sites.join(","), "Current Value"]];
583 context.fork("sites", 0, Styles, "completeSite", window.content, args["-group"]);
585 else if (args.completeArg == 1) {
587 context.completions = [[sheet.css, "Current Value"]];
588 context.fork("css", 0, modules.completion, "css");
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"),
598 { names: ["-nopersist", "-N"], description: "Do not save this style to an auto-generated RC file" }
600 serialize: function ()
602 .filter(function (hive) hive.persist)
604 hive.sheets.filter(function (style) style.persist)
605 .sort(function (a, b) String.localeCompare(a.name || "", b.name || ""))
606 .map(function (style) ({
608 arguments: [style.sites.join(",")],
609 literalArg: style.css,
613 style.name ? { "-name": style.name } : {})
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
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
632 name: ["stylet[oggle]", "styt[oggle]"],
633 desc: "Toggle a user style sheet",
634 action: function (sheet) sheet.enabled = !sheet.enabled
637 name: ["dels[tyle]"],
638 desc: "Remove a user style sheet",
639 action: function (sheet) sheet.remove(),
641 ].forEach(function (cmd) {
642 commands.add(cmd.name, cmd.desc,
644 dactyl.assert(args.bang ^ !!(args[0] || args[1] || args["-name"] || args["-index"]),
645 "Argument or ! required");
647 args["-group"].find(args["-name"], args[0], args.literalArg, args["-index"])
648 .forEach(cmd.action);
651 completer: function (context, args) {
652 let uris = util.visibleURIs(window.content);
654 Styles.completeSite(context, window.content, args["-group"]);
656 context.filters.push(function ({ sheets }) sheets.some(cmd.filter));
660 contexts.GroupFlag("styles"),
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);
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);
682 get names() this.hive.names,
683 get sheets() this.hive.sheets,
684 get sites() this.hive.sites,
686 __noSuchMethod__: function __noSuchMethod__(meth, args) {
687 return this.hive[meth].apply(this.hive, args);
690 destroy: function () {
691 this.hive.dropRef(this);
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 () "" };
701 for (let match in Styles.propertyIter(context.filter, true))
702 var lastMatch = match;
704 if (lastMatch != null && !lastMatch.value && !lastMatch.postSpace) {
705 context.advance(lastMatch.index + lastMatch.preSpace.length);
706 context.completions = names;
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),
716 function (context, obj, args) this.sheets
719 template: function () {
720 let patterns = Styles.patterns;
722 template.highlightCSS = function highlightCSS(css) {
723 XML.prettyPrinting = XML.ignoreWhitespace = false;
725 return this.highlightRegexp(css, patterns.property, function (match) <>{
726 match.preSpace}{template.filter(match.name)}: {
728 template.highlightRegexp(match.value, patterns.token, function (match) {
730 return <>{template.filter(match.word)}{
731 template.highlightRegexp(match.function, patterns.string,
732 function (match) <span highlight="String">{match.string}</span>)
734 if (match.important == "!important")
735 return <span highlight="String">{match.important}</span>;
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></>);
742 }{ match.postSpace }</>
750 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
752 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: