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"]
13 function cssUri(css) "chrome-data:text/css," + encodeURI(css);
14 var namespace = "@namespace html " + XHTML.uri.quote() + ";\n" +
15 "@namespace xul " + XUL.uri.quote() + ";\n" +
16 "@namespace dactyl " + NS.uri.quote() + ";\n";
18 var Sheet = Struct("name", "id", "sites", "css", "hive", "agent");
19 Sheet.liveProperty = function (name) {
20 let i = this.prototype.members[name];
21 this.prototype.__defineGetter__(name, function () this[i]);
22 this.prototype.__defineSetter__(name, function (val) {
24 val = Array.slice(val);
28 this.enabled = this.enabled;
31 Sheet.liveProperty("agent");
32 Sheet.liveProperty("css");
33 Sheet.liveProperty("sites");
34 update(Sheet.prototype, {
35 formatSites: function (uris)
36 template.map(this.sites,
37 function (filter) <span highlight={uris.some(Styles.matchFilter(filter)) ? "Filter" : ""}>{filter}</span>,
40 remove: function () { this.hive.remove(this); },
42 get uri() "dactyl://style/" + this.id + "/" + this.hive.name + "/" + (this.name || ""),
44 get enabled() this._enabled,
46 if (on != this._enabled || this.fullCSS != this._fullCSS) {
49 else if (!this._fullCSS)
52 let meth = on ? "registerSheet" : "unregisterSheet";
53 styles[meth](this.uri, on ? this.agent : this._agent);
55 this._agent = this.agent;
56 this._enabled = Boolean(on);
57 this._fullCSS = this.fullCSS;
61 match: function (uri) {
63 uri = util.newURI(uri);
64 return this.sites.some(function (site) Styles.matchFilter(site, uri));
68 let filter = this.sites;
71 let preamble = "/* " + this.uri + (this.agent ? " (agent)" : "") + " */\n\n" + namespace + "\n";
73 return preamble + css;
75 let selectors = filter.map(function (part)
76 !/^(?:[a-z-]+[:*]|[a-z-.]+$)/i.test(part) ? "regexp(" + Styles.quote(".*(?:" + part + ").*") + ")" :
77 (/[*]$/.test(part) ? "url-prefix" :
78 /[\/:]/.test(part) ? "url"
80 + '(' + Styles.quote(part.replace(/\*$/, "")) + ')')
83 return preamble + "@-moz-document " + selectors + " {\n\n" + css + "\n\n}\n";
87 var Hive = Class("Hive", {
88 init: function (name, persist) {
93 this.persist = persist;
96 get modifiable() this.name !== "system",
98 addRef: function (obj) {
99 this.refs.push(util.weakReference(obj));
102 dropRef: function (obj) {
103 this.refs = this.refs.filter(function (ref) ref.get() && ref.get() !== obj);
104 if (!this.refs.length) {
106 styles.hives = styles.hives.filter(function (h) h !== this, this);
110 cleanup: function cleanup() {
111 for (let sheet in values(this.sheets))
112 sheet.enabled = false;
115 __iterator__: function () Iterator(this.sheets),
117 get sites() array(this.sheets).map(function (s) s.sites).flatten().uniq().array,
120 * Add a new style sheet.
122 * @param {string} name The name given to the style sheet by
123 * which it may be later referenced.
124 * @param {string} filter The sites to which this sheet will
125 * apply. Can be a domain name or a URL. Any URL ending in
126 * "*" is matched as a prefix.
127 * @param {string} css The CSS to be applied.
128 * @param {boolean} agent If true, the sheet is installed as an
130 * @param {boolean} lazy If true, the sheet is not initially enabled.
133 add: function add(name, filter, css, agent, lazy) {
135 if (!isArray(filter))
136 filter = filter.split(",");
137 if (name && name in this.names) {
138 var sheet = this.names[name];
140 sheet.css = String(css);
141 sheet.sites = filter;
144 sheet = Sheet(name, styles._id++, filter.filter(util.identity), String(css), this, agent);
145 this.sheets.push(sheet);
148 styles.allSheets[sheet.id] = sheet;
151 sheet.enabled = true;
154 this.names[name] = sheet;
159 * Get a sheet with a given name or index.
161 * @param {string or number} sheet The sheet to retrieve. Strings indicate
162 * sheet names, while numbers indicate indices.
164 get: function get(sheet) {
165 if (typeof sheet === "number")
166 return this.sheets[sheet];
167 return this.names[sheet];
171 * Find sheets matching the parameters. See {@link #addSheet}
174 * @param {string} name
175 * @param {string} filter
176 * @param {string} css
177 * @param {number} index
179 find: function find(name, filter, css, index) {
180 // Grossly inefficient.
181 let matches = [k for ([k, v] in Iterator(this.sheets))];
183 matches = String(index).split(",").filter(function (i) i in this.sheets, this);
185 matches = matches.filter(function (i) this.sheets[i].name == name, this);
187 matches = matches.filter(function (i) this.sheets[i].css == css, this);
189 matches = matches.filter(function (i) this.sheets[i].sites.indexOf(filter) >= 0, this);
190 return matches.map(function (i) this.sheets[i], this);
194 * Remove a style sheet. See {@link #addSheet} for parameters.
195 * In cases where *filter* is supplied, the given filters are removed from
196 * matching sheets. If any remain, the sheet is left in place.
198 * @param {string} name
199 * @param {string} filter
200 * @param {string} css
201 * @param {number} index
203 remove: function remove(name, filter, css, index) {
205 if (arguments.length == 1) {
206 var matches = [name];
210 if (filter && filter.indexOf(",") > -1)
211 return filter.split(",").reduce(
212 function (n, f) n + self.removeSheet(name, f, index), 0);
214 if (filter == undefined)
218 matches = this.findSheets(name, filter, css, index);
219 if (matches.length == 0)
222 for (let [, sheet] in Iterator(matches.reverse())) {
224 let sites = sheet.sites.filter(function (f) f != filter);
230 sheet.enabled = false;
232 delete this.names[sheet.name];
233 delete styles.allSheets[sheet.id];
235 this.sheets = this.sheets.filter(function (s) matches.indexOf(s) == -1);
236 return matches.length;
241 * Manages named and unnamed user style sheets, which apply to both
242 * chrome and content pages.
244 * @author Kris Maglione <maglione.k@gmail.com>
246 var Styles = Module("Styles", {
247 Local: function (dactyl, modules, window) ({
248 cleanup: function () {}
256 update(services["dactyl:"].providers, {
257 "style": function styleProvider(uri, path) {
258 let id = parseInt(path);
259 if (Set.has(styles.allSheets, id))
260 return ["text/css", styles.allSheets[id].fullCSS];
266 cleanup: function cleanup() {
267 for each (let hive in this.hives)
268 util.trapErrors("cleanup", hive);
270 this.user = this.addHive("user", this, true);
271 this.system = this.addHive("system", this, false);
274 addHive: function addHive(name, ref, persist) {
275 let hive = array.nth(this.hives, function (h) h.name === name, 0);
277 hive = Hive(name, persist);
278 this.hives.push(hive);
280 hive.persist = persist;
286 __iterator__: function () Iterator(this.user.sheets.concat(this.system.sheets)),
288 _proxy: function (name, args)
289 let (obj = this[args[0] ? "system" : "user"])
290 obj[name].apply(obj, Array.slice(args, 1)),
292 addSheet: deprecated("Styles#{user,system}.add", function addSheet() this._proxy("add", arguments)),
293 findSheets: deprecated("Styles#{user,system}.find", function findSheets() this._proxy("find", arguments)),
294 get: deprecated("Styles#{user,system}.get", function get() this._proxy("get", arguments)),
295 removeSheet: deprecated("Styles#{user,system}.remove", function removeSheet() this._proxy("remove", arguments)),
297 userSheets: Class.Property({ get: deprecated("Styles#user.sheets", function userSheets() this.user.sheets) }),
298 systemSheets: Class.Property({ get: deprecated("Styles#system.sheets", function systemSheets() this.system.sheets) }),
299 userNames: Class.Property({ get: deprecated("Styles#user.names", function userNames() this.user.names) }),
300 systemNames: Class.Property({ get: deprecated("Styles#system.names", function systemNames() this.system.names) }),
301 sites: Class.Property({ get: deprecated("Styles#user.sites", function sites() this.user.sites) }),
303 list: function list(content, sites, name, hives) {
304 const { commandline, dactyl } = this.modules;
306 hives = hives || styles.hives.filter(function (h) h.modifiable && h.sheets.length);
308 function sheets(group)
310 .filter(function (sheet) (!name || sheet.name === name) &&
311 (!sites || sites.every(function (s) sheet.sites.indexOf(s) >= 0)))
312 .sort(function (a, b) a.name && b.name ? String.localeCompare(a.name, b.name)
313 : !!b.name - !!a.name || a.id - b.id);
315 let uris = util.visibleURIs(content);
318 <tr highlight="Title">
321 <td style="padding-right: 1em;">{_("title.Name")}</td>
322 <td style="padding-right: 1em;">{_("title.Filter")}</td>
323 <td style="padding-right: 1em;">{_("title.CSS")}</td>
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;"/>
330 template.map(hives, function (hive) let (i = 0)
331 <tr style="height: .5ex;"/> +
332 template.map(sheets(hive), function (sheet)
334 <td highlight="Title">{!i++ ? hive.name : ""}</td>
335 <td>{sheet.enabled ? "" : UTF8("×")}</td>
336 <td>{sheet.name || hive.sheets.indexOf(sheet)}</td>
337 <td>{sheet.formatSites(uris)}</td>
340 <tr style="height: .5ex;"/>)
344 // TODO: Move this to an ItemList to show this automatically
345 if (list.*.length() === list.text().length() + 5)
346 dactyl.echomsg(_("style.none"));
348 commandline.commandOutput(list);
351 registerSheet: function registerSheet(url, agent, reload) {
352 let uri = services.io.newURI(url, null, null);
354 this.unregisterSheet(url, agent);
356 let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
357 if (reload || !services.stylesheet.sheetRegistered(uri, type))
358 services.stylesheet.loadAndRegisterSheet(uri, type);
361 unregisterSheet: function unregisterSheet(url, agent) {
362 let uri = services.io.newURI(url, null, null);
363 let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
364 if (services.stylesheet.sheetRegistered(uri, type))
365 services.stylesheet.unregisterSheet(uri, type);
368 append: function (dest, src, sort) {
370 for each (let str in [dest, src])
371 for (let prop in Styles.propertyIter(str))
372 props[prop.name] = prop.value;
374 let val = Object.keys(props)[sort ? "sort" : "slice"]()
375 .map(function (prop) prop + ": " + props[prop] + ";")
378 if (/^\s*(\/\*.*?\*\/)/.exec(src))
379 val = RegExp.$1 + " " + val;
383 completeSite: function (context, content, group) {
384 group = group || styles.user;
385 context.anchored = false;
387 context.fork("current", 0, this, function (context) {
388 context.title = ["Current Site"];
389 context.completions = [
390 [content.location.host, /*L*/"Current Host"],
391 [content.location.href, /*L*/"Current URL"]
397 let uris = util.visibleURIs(content);
399 context.generate = function () values(group.sites);
401 context.keys.text = util.identity;
402 context.keys.description = function (site) this.sheets.length + /*L*/" sheet" + (this.sheets.length == 1 ? "" : "s") + ": " +
403 array.compact(this.sheets.map(function (s) s.name)).join(", ");
404 context.keys.sheets = function (site) group.sheets.filter(function (s) s.sites.indexOf(site) >= 0);
405 context.keys.active = function (site) uris.some(Styles.matchFilter(site));
407 Styles.splitContext(context, "Sites");
411 * A curried function which determines which host names match a
412 * given stylesheet filter. When presented with one argument,
413 * returns a matcher function which, given one nsIURI argument,
414 * returns true if that argument matches the given filter. When
415 * given two arguments, returns true if the second argument matches
418 * @param {string} filter The URI filter to match against.
419 * @param {nsIURI} uri The location to test.
420 * @returns {nsIURI -> boolean}
422 matchFilter: function (filter) {
423 filter = filter.trim();
426 var test = function test(uri) true;
427 else if (!/^(?:[a-z-]+:|[a-z-.]+$)/.test(filter)) {
428 let re = util.regexp(filter);
429 test = function test(uri) re.test(uri.spec);
431 else if (/[*]$/.test(filter)) {
432 let re = RegExp("^" + util.regexp.escape(filter.substr(0, filter.length - 1)));
433 test = function test(uri) re.test(uri.spec);
435 else if (/[\/:]/.test(filter))
436 test = function test(uri) uri.spec === filter;
438 test = function test(uri) { try { return util.isSubdomain(uri.host, filter); } catch (e) { return false; } };
439 test.toString = function toString() filter;
441 if (arguments.length < 2)
443 return test(arguments[1]);
446 splitContext: function splitContext(context, title) {
447 for (let item in Iterator({ Active: true, Inactive: false })) {
448 let [name, active] = item;
449 context.split(name, null, function (context) {
450 context.title[0] = /*L*/name + " " + (title || "Sheets");
451 context.filters.push(function (item) !!item.active == active);
456 propertyIter: function (str, always) {
458 for (let match in this.propertyPattern.iterate(str)) {
459 if (match.value || always && match.name || match.wholeMatch === match.preSpace && always && !i++)
461 if (!/;/.test(match.postSpace))
466 propertyPattern: util.regexp(<![CDATA[
468 (?P<preSpace> <space>*)
471 <space>* : \s* (?P<value>
476 (?: <string> | [^)]* )
487 (?P<postSpace> <space>* (?: ; | $) )
490 space: /(?: \s | \/\* .*? \*\/ )/,
491 string: /(?:" (?:[^\\"]|\\.)* (?:"|$) | '(?:[^\\']|\\.)* (?:'|$) )/
495 get property() util.regexp(<![CDATA[
497 (?P<preSpace> <space>*)
500 <space>* : \s* (?P<value>
505 (?P<postSpace> <space>* (?: ; | $) )
508 get function() util.regexp(<![CDATA[
511 (?: <string> | [^)]* )
516 space: /(?: \s | \/\* .*? \*\/ )/,
518 get string() util.regexp(<![CDATA[
520 " (?:[^\\"]|\\.)* (?:"|$) |
521 ' (?:[^\\']|\\.)* (?:'|$)
525 get token() util.regexp(<![CDATA[
530 | (?P<important> !important\b)
539 * Quotes a string for use in CSS stylesheets.
541 * @param {string} str
544 quote: function quote(str) {
545 return '"' + str.replace(/([\\"])/g, "\\$1").replace(/\n/g, "\\00000a") + '"';
548 commands: function (dactyl, modules, window) {
549 const { commands, contexts, styles } = modules;
551 function sheets(context, args, filter) {
552 let uris = util.visibleURIs(window.content);
553 context.compare = modules.CompletionContext.Sort.number;
554 context.generate = function () args["-group"].sheets;
555 context.keys.active = function (sheet) uris.some(sheet.closure.match);
556 context.keys.description = function (sheet) <>{sheet.formatSites(uris)}: {sheet.css.replace("\n", "\\n")}</>
558 context.filters.push(function ({ item }) filter(item));
559 Styles.splitContext(context);
562 function nameFlag(filter) ({
563 names: ["-name", "-n"],
564 description: "The name of this stylesheet",
565 type: modules.CommandOption.STRING,
566 completer: function (context, args) {
567 context.keys.text = function (sheet) sheet.name;
568 context.filters.unshift(function ({ item }) item.name);
569 sheets(context, args, filter);
573 commands.add(["sty[le]"],
574 "Add or list user styles",
576 let [filter, css] = args;
579 styles.list(window.content, filter ? filter.split(",") : null, args["-name"], args.explicitOpts["-group"] ? [args["-group"]] : null);
581 util.assert(args["-group"].modifiable && args["-group"].hive.modifiable,
582 _("group.cantChangeBuiltin", _("style.styles")));
584 if (args["-append"]) {
585 let sheet = args["-group"].get(args["-name"]);
587 filter = array(sheet.sites).concat(filter).uniq().join(",");
588 css = sheet.css + " " + css;
591 let style = args["-group"].add(args["-name"], filter, css, args["-agent"]);
593 if (args["-nopersist"] || !args["-append"] || style.persist === undefined)
594 style.persist = !args["-nopersist"];
598 completer: function (context, args) {
600 let sheet = args["-group"].get(args["-name"]);
601 if (args.completeArg == 0) {
603 context.completions = [[sheet.sites.join(","), "Current Value"]];
604 context.fork("sites", 0, Styles, "completeSite", window.content, args["-group"]);
606 else if (args.completeArg == 1) {
608 context.completions = [
609 [sheet.css, _("option.currentValue")]
611 context.fork("css", 0, modules.completion, "css");
617 { names: ["-agent", "-A"], description: "Apply style as an Agent sheet" },
618 { names: ["-append", "-a"], description: "Append site filter and css to an existing, matching sheet" },
619 contexts.GroupFlag("styles"),
621 { names: ["-nopersist", "-N"], description: "Do not save this style to an auto-generated RC file" }
623 serialize: function ()
625 .filter(function (hive) hive.persist)
627 hive.sheets.filter(function (style) style.persist)
628 .sort(function (a, b) String.localeCompare(a.name || "", b.name || ""))
629 .map(function (style) ({
631 arguments: [style.sites.join(",")],
632 literalArg: style.css,
634 "-group": hive.name == "user" ? undefined : hive.name,
635 "-name": style.name || undefined
643 name: ["stylee[nable]", "stye[nable]"],
644 desc: "Enable a user style sheet",
645 action: function (sheet) sheet.enabled = true,
646 filter: function (sheet) !sheet.enabled
649 name: ["styled[isable]", "styd[isable]"],
650 desc: "Disable a user style sheet",
651 action: function (sheet) sheet.enabled = false,
652 filter: function (sheet) sheet.enabled
655 name: ["stylet[oggle]", "styt[oggle]"],
656 desc: "Toggle a user style sheet",
657 action: function (sheet) sheet.enabled = !sheet.enabled
660 name: ["dels[tyle]"],
661 desc: "Remove a user style sheet",
662 action: function (sheet) sheet.remove(),
664 ].forEach(function (cmd) {
665 commands.add(cmd.name, cmd.desc,
667 dactyl.assert(args.bang ^ !!(args[0] || args[1] || args["-name"] || args["-index"]),
668 _("error.argumentOrBang"));
670 args["-group"].find(args["-name"], args[0], args.literalArg, args["-index"])
671 .forEach(cmd.action);
674 completer: function (context, args) {
675 let uris = util.visibleURIs(window.content);
677 Styles.completeSite(context, window.content, args["-group"]);
679 context.filters.push(function ({ sheets }) sheets.some(cmd.filter));
683 contexts.GroupFlag("styles"),
685 names: ["-index", "-i"],
686 type: modules.CommandOption.INT,
687 completer: function (context, args) {
688 context.keys.text = function (sheet) args["-group"].sheets.indexOf(sheet);
689 sheets(context, args, cmd.filter);
697 contexts: function (dactyl, modules, window) {
698 modules.contexts.Hives("styles",
699 Class("LocalHive", Contexts.Hive, {
700 init: function init(group) {
701 init.superapply(this, arguments);
702 this.hive = styles.addHive(group.name, this, this.persist);
705 get names() this.hive.names,
706 get sheets() this.hive.sheets,
707 get sites() this.hive.sites,
709 __noSuchMethod__: function __noSuchMethod__(meth, args) {
710 return this.hive[meth].apply(this.hive, args);
713 destroy: function () {
714 this.hive.dropRef(this);
718 completion: function (dactyl, modules, window) {
719 const names = Array.slice(DOM(<div/>, window.document).style);
720 modules.completion.css = function (context) {
721 context.title = ["CSS Property"];
722 context.keys = { text: function (p) p + ":", description: function () "" };
724 for (let match in Styles.propertyIter(context.filter, true))
725 var lastMatch = match;
727 if (lastMatch != null && !lastMatch.value && !lastMatch.postSpace) {
728 context.advance(lastMatch.index + lastMatch.preSpace.length);
729 context.completions = names;
733 javascript: function (dactyl, modules, window) {
734 modules.JavaScript.setCompleter(["get", "add", "remove", "find"].map(function (m) Hive.prototype[m]),
735 [ // Prototype: (name, filter, css, index)
736 function (context, obj, args) this.names,
737 function (context, obj, args) Styles.completeSite(context, window.content),
739 function (context, obj, args) this.sheets
742 template: function () {
743 let patterns = Styles.patterns;
745 template.highlightCSS = function highlightCSS(css) {
746 XML.prettyPrinting = XML.ignoreWhitespace = false;
748 return this.highlightRegexp(css, patterns.property, function (match) {
751 return <>{match.preSpace}{template.filter(match.name)}: {
753 template.highlightRegexp(match.value, patterns.token, function (match) {
755 return <>{template.filter(match.word)}{
756 template.highlightRegexp(match.function, patterns.string,
757 function (match) <span highlight="String">{match.string}</span>)
759 if (match.important == "!important")
760 return <span highlight="String">{match.important}</span>;
762 return <span highlight="String">{match.string}</span>;
763 return template.highlightRegexp(match.wholeMatch, /^(\d+)(em|ex|px|in|cm|mm|pt|pc)?/g,
764 function (m, n, u) <><span highlight="Number">{n}</span><span highlight="Object">{u || ""}</span></>);
767 }{ match.postSpace }</>
775 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
777 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: