1 // Copyright (c) 2008-2014 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 defineModule("styles", {
8 exports: ["Style", "Styles", "styles"],
9 require: ["services", "util"]
12 lazyRequire("contexts", ["Contexts"]);
13 lazyRequire("template", ["template"]);
15 function cssUri(css) "chrome-data:text/css," + encodeURI(css);
16 var namespace = "@namespace html " + XHTML.quote() + ";\n" +
17 "@namespace xul " + XUL.quote() + ";\n" +
18 "@namespace dactyl " + NS.quote() + ";\n";
20 var Sheet = Struct("name", "id", "sites", "css", "hive", "agent");
21 Sheet.liveProperty = function (name) {
22 let i = this.prototype.members[name];
23 this.prototype.__defineGetter__(name, function () this[i]);
24 this.prototype.__defineSetter__(name, function (val) {
26 val = Array.slice(val);
30 this.enabled = this.enabled;
33 Sheet.liveProperty("agent");
34 Sheet.liveProperty("css");
35 Sheet.liveProperty("sites");
36 update(Sheet.prototype, {
37 formatSites: function (uris)
38 template.map(this.sites,
39 filter => ["span", { highlight: uris.some(Styles.matchFilter(filter)) ? "Filter" : "" }, filter],
42 remove: function () { this.hive.remove(this); },
44 get uri() "dactyl://style/" + this.id + "/" + this.hive.name + "/" + (this.name || ""),
46 get enabled() this._enabled,
48 if (on != this._enabled || this.fullCSS != this._fullCSS) {
51 else if (!this._fullCSS)
54 let meth = on ? "registerSheet" : "unregisterSheet";
55 styles[meth](this.uri, on ? this.agent : this._agent);
57 this._agent = this.agent;
58 this._enabled = Boolean(on);
59 this._fullCSS = this.fullCSS;
63 match: function (uri) {
65 uri = util.newURI(uri);
66 return this.sites.some(site => Styles.matchFilter(site, uri));
70 let filter = this.sites;
73 let preamble = "/* " + this.uri + (this.agent ? " (agent)" : "") + " */\n\n" + namespace + "\n";
75 return preamble + css;
77 let selectors = filter.map(part =>
78 !/^(?:[a-z-]+[:*]|[a-z-.]+$)/i.test(part) ? "regexp(" + Styles.quote(".*(?:" + part + ").*") + ")" :
79 (/[*]$/.test(part) ? "url-prefix" :
80 /[\/:]/.test(part) ? "url"
82 + '(' + Styles.quote(part.replace(/\*$/, "")) + ')')
85 return preamble + "@-moz-document " + selectors + " {\n\n" + css + "\n\n}\n";
89 var Hive = Class("Hive", {
90 init: function (name, persist) {
95 this.persist = persist;
98 get modifiable() this.name !== "system",
100 addRef: function (obj) {
101 this.refs.push(util.weakReference(obj));
104 dropRef: function (obj) {
105 this.refs = this.refs.filter(ref => (ref.get() && ref.get() !== obj));
107 if (!this.refs.length) {
109 styles.hives = styles.hives.filter(h => h !== this);
113 cleanup: function cleanup() {
114 for (let sheet of this.sheets)
115 util.trapErrors(() => {
116 sheet.enabled = false;
120 __iterator__: function () Iterator(this.sheets),
122 get sites() array(this.sheets).map(s => s.sites)
127 * Add a new style sheet.
129 * @param {string} name The name given to the style sheet by
130 * which it may be later referenced.
131 * @param {string} filter The sites to which this sheet will
132 * apply. Can be a domain name or a URL. Any URL ending in
133 * "*" is matched as a prefix.
134 * @param {string} css The CSS to be applied.
135 * @param {boolean} agent If true, the sheet is installed as an
137 * @param {boolean} lazy If true, the sheet is not initially enabled.
140 add: function add(name, filter, css, agent, lazy) {
143 // Need an array from the same compartment.
144 filter = Array.slice(filter);
146 filter = filter.split(",");
148 if (name && name in this.names) {
149 var sheet = this.names[name];
151 sheet.css = String(css);
152 sheet.sites = filter;
155 sheet = Sheet(name, styles._id++, filter.filter(util.identity), String(css), this, agent);
156 this.sheets.push(sheet);
159 styles.allSheets[sheet.id] = sheet;
162 sheet.enabled = true;
165 this.names[name] = sheet;
170 * Get a sheet with a given name or index.
172 * @param {string or number} sheet The sheet to retrieve. Strings indicate
173 * sheet names, while numbers indicate indices.
175 get: function get(sheet) {
176 if (typeof sheet === "number")
177 return this.sheets[sheet];
178 return this.names[sheet];
182 * Find sheets matching the parameters. See {@link #addSheet}
185 * @param {string} name
186 * @param {string} filter
187 * @param {string} css
188 * @param {number} index
190 find: function find(name, filter, css, index) {
191 // Grossly inefficient.
192 let matches = [k for ([k, v] in Iterator(this.sheets))];
194 matches = String(index).split(",").filter(i => i in this.sheets);
196 matches = matches.filter(i => this.sheets[i].name == name);
198 matches = matches.filter(i => this.sheets[i].css == css);
200 matches = matches.filter(i => this.sheets[i].sites.indexOf(filter) >= 0);
202 return matches.map(i => this.sheets[i]);
206 * Remove a style sheet. See {@link #addSheet} for parameters.
207 * In cases where *filter* is supplied, the given filters are removed from
208 * matching sheets. If any remain, the sheet is left in place.
210 * @param {string} name
211 * @param {string} filter
212 * @param {string} css
213 * @param {number} index
215 remove: function remove(name, filter, css, index) {
216 if (arguments.length == 1) {
217 var matches = [name];
221 if (filter && filter.contains(","))
222 return filter.split(",").reduce(
223 (n, f) => n + this.removeSheet(name, f, index), 0);
225 if (filter == undefined)
229 matches = this.findSheets(name, filter, css, index);
230 if (matches.length == 0)
233 for (let [, sheet] in Iterator(matches.reverse())) {
235 let sites = sheet.sites.filter(f => f != filter);
241 sheet.enabled = false;
243 delete this.names[sheet.name];
244 delete styles.allSheets[sheet.id];
246 this.sheets = this.sheets.filter(s => matches.indexOf(s) == -1);
247 return matches.length;
252 * Manages named and unnamed user style sheets, which apply to both
253 * chrome and content pages.
255 * @author Kris Maglione <maglione.k@gmail.com>
257 var Styles = Module("Styles", {
258 Local: function (dactyl, modules, window) ({
259 cleanup: function () {}
267 update(services["dactyl:"].providers, {
268 "style": function styleProvider(uri, path) {
269 let id = parseInt(path);
270 if (hasOwnProperty(styles.allSheets, id))
271 return ["text/css", styles.allSheets[id].fullCSS];
277 cleanup: function cleanup() {
278 for (let hive of this.hives || [])
279 util.trapErrors("cleanup", hive);
281 this.user = this.addHive("user", this, true);
282 this.system = this.addHive("system", this, false);
285 addHive: function addHive(name, ref, persist) {
286 let hive = this.hives.find(h => h.name === name);
288 hive = Hive(name, persist);
289 this.hives.push(hive);
291 hive.persist = persist;
297 __iterator__: function () Iterator(this.user.sheets.concat(this.system.sheets)),
299 _proxy: function (name, args)
300 let (obj = this[args[0] ? "system" : "user"])
301 obj[name].apply(obj, Array.slice(args, 1)),
303 addSheet: deprecated("Styles#{user,system}.add", function addSheet() this._proxy("add", arguments)),
304 findSheets: deprecated("Styles#{user,system}.find", function findSheets() this._proxy("find", arguments)),
305 get: deprecated("Styles#{user,system}.get", function get() this._proxy("get", arguments)),
306 removeSheet: deprecated("Styles#{user,system}.remove", function removeSheet() this._proxy("remove", arguments)),
308 userSheets: Class.Property({ get: deprecated("Styles#user.sheets", function userSheets() this.user.sheets) }),
309 systemSheets: Class.Property({ get: deprecated("Styles#system.sheets", function systemSheets() this.system.sheets) }),
310 userNames: Class.Property({ get: deprecated("Styles#user.names", function userNames() this.user.names) }),
311 systemNames: Class.Property({ get: deprecated("Styles#system.names", function systemNames() this.system.names) }),
312 sites: Class.Property({ get: deprecated("Styles#user.sites", function sites() this.user.sites) }),
314 list: function list(content, sites, name, hives) {
315 const { commandline, dactyl } = this.modules;
317 hives = hives || styles.hives.filter(h => (h.modifiable && h.sheets.length));
319 function sheets(group)
321 .filter(sheet => ((!name || sheet.name === name) &&
322 (!sites || sites.every(s => sheet.sites.indexOf(s) >= 0))))
323 .sort((a, b) => (a.name && b.name ? String.localeCompare(a.name, b.name)
324 : !!b.name - !!a.name || a.id - b.id));
326 let uris = util.visibleURIs(content);
328 let list = ["table", {},
329 ["tr", { highlight: "Title" },
332 ["td", { style: "padding-right: 1em;" }, _("title.Name")],
333 ["td", { style: "padding-right: 1em;" }, _("title.Filter")],
334 ["td", { style: "padding-right: 1em;" }, _("title.CSS")]],
335 ["col", { style: "min-width: 4em; padding-right: 1em;" }],
336 ["col", { style: "min-width: 1em; text-align: center; color: red; font-weight: bold;" }],
337 ["col", { style: "padding: 0 1em 0 1ex; vertical-align: top;" }],
338 ["col", { style: "padding: 0 1em 0 0; vertical-align: top;" }],
339 template.map(hives, hive => let (i = 0) [
340 ["tr", { style: "height: .5ex;" }],
341 template.map(sheets(hive), sheet =>
343 ["td", { highlight: "Title" }, !i++ ? hive.name : ""],
344 ["td", {}, sheet.enabled ? "" : UTF8("×")],
345 ["td", {}, sheet.name || hive.sheets.indexOf(sheet)],
346 ["td", {}, sheet.formatSites(uris)],
347 ["td", {}, sheet.css]]),
348 ["tr", { style: "height: .5ex;" }]])];
351 // // TODO: Move this to an ItemList to show this automatically
352 // if (list.*.length() === list.text().length() + 5)
353 // dactyl.echomsg(_("style.none"));
355 commandline.commandOutput(list);
358 registerSheet: function registerSheet(url, agent, reload) {
359 let uri = services.io.newURI(url, null, null);
361 this.unregisterSheet(url, agent);
363 let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
364 if (reload || !services.stylesheet.sheetRegistered(uri, type))
365 services.stylesheet.loadAndRegisterSheet(uri, type);
368 unregisterSheet: function unregisterSheet(url, agent) {
369 let uri = services.io.newURI(url, null, null);
370 let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
371 if (services.stylesheet.sheetRegistered(uri, type))
372 services.stylesheet.unregisterSheet(uri, type);
375 append: function (dest, src, sort) {
377 for (let str of [dest, src])
378 for (let prop in Styles.propertyIter(str))
379 props[prop.name] = prop.value;
381 let val = Object.keys(props)[sort ? "sort" : "slice"]()
382 .map(prop => prop + ": " + props[prop] + ";")
385 if (/^\s*(\/\*.*?\*\/)/.exec(src))
386 val = RegExp.$1 + " " + val;
390 completeSite: function (context, content, group=styles.user) {
391 context.anchored = false;
393 context.fork("current", 0, this, function (context) {
394 context.title = ["Current Site"];
395 context.completions = [
396 [content.location.host, /*L*/"Current Host"],
397 [content.location.href, /*L*/"Current URL"]
403 let uris = util.visibleURIs(content);
405 context.generate = () => values(group.sites);
407 context.keys.text = util.identity;
408 context.keys.description = function (site) this.sheets.length + /*L*/" sheet" + (this.sheets.length == 1 ? "" : "s") + ": " +
409 array.compact(this.sheets.map(s => s.name)).join(", ");
410 context.keys.sheets = site => group.sheets.filter(s => s.sites.indexOf(site) >= 0);
411 context.keys.active = site => uris.some(Styles.matchFilter(site));
413 Styles.splitContext(context, "Sites");
417 * A curried function which determines which host names match a
418 * given stylesheet filter. When presented with one argument,
419 * returns a matcher function which, given one nsIURI argument,
420 * returns true if that argument matches the given filter. When
421 * given two arguments, returns true if the second argument matches
424 * @param {string} filter The URI filter to match against.
425 * @param {nsIURI} uri The location to test.
426 * @returns {nsIURI -> boolean}
428 matchFilter: function (filter) {
429 filter = filter.trim();
432 var test = function test(uri) true;
433 else if (!/^(?:[a-z-]+:|[a-z-.]+$)/.test(filter)) {
434 let re = util.regexp(filter);
435 test = function test(uri) re.test(uri.spec);
437 else if (/[*]$/.test(filter)) {
438 let re = RegExp("^" + util.regexp.escape(filter.substr(0, filter.length - 1)));
439 test = function test(uri) re.test(uri.spec);
441 else if (/[\/:]/.test(filter))
442 test = function test(uri) uri.spec === filter;
444 test = function test(uri) { try { return util.isSubdomain(uri.host, filter); } catch (e) { return false; } };
445 test.toString = function toString() filter;
447 if (arguments.length < 2)
449 return test(arguments[1]);
452 splitContext: function splitContext(context, title) {
453 for (let item in Iterator({ Active: true, Inactive: false })) {
454 let [name, active] = item;
455 context.split(name, null, function (context) {
456 context.title[0] = /*L*/name + " " + (title || "Sheets");
457 context.filters.push(item => !!item.active == active);
462 propertyIter: function (str, always) {
464 for (let match in this.propertyPattern.iterate(str)) {
465 if (match.value || always && match.name || match.wholeMatch === match.preSpace && always && !i++)
467 if (!/;/.test(match.postSpace))
472 propertyPattern: util.regexp(literal(/*
474 (?P<preSpace> <space>*)
477 <space>* : \s* (?P<value>
482 (?: <string> | [^)]* )
493 (?P<postSpace> <space>* (?: ; | $) )
496 space: /(?: \s | \/\* .*? \*\/ )/,
497 string: /(?:" (?:[^\\"]|\\.)* (?:"|$) | '(?:[^\\']|\\.)* (?:'|$) )/
501 get property() util.regexp(literal(/*
503 (?P<preSpace> <space>*)
506 <space>* : \s* (?P<value>
511 (?P<postSpace> <space>* (?: ; | $) )
514 get function() util.regexp(literal(/*
517 (?: <string> | [^)]* )
522 space: /(?: \s | \/\* .*? \*\/ )/,
524 get string() util.regexp(literal(/*
526 " (?:[^\\"]|\\.)* (?:"|$) |
527 ' (?:[^\\']|\\.)* (?:'|$)
531 get token() util.regexp(literal(/*
536 | (?P<important> !important\b)
545 * Quotes a string for use in CSS stylesheets.
547 * @param {string} str
550 quote: function quote(str) {
551 return '"' + str.replace(/([\\"])/g, "\\$1").replace(/\n/g, "\\00000a") + '"';
554 commands: function initCommands(dactyl, modules, window) {
555 const { commands, contexts, styles } = modules;
557 function sheets(context, args, filter) {
558 let uris = util.visibleURIs(window.content);
559 context.compare = modules.CompletionContext.Sort.number;
560 context.generate = () => args["-group"].sheets;
561 context.keys.active = sheet => uris.some(sheet.bound.match);
562 context.keys.description = sheet => [sheet.formatSites(uris), ": ", sheet.css.replace("\n", "\\n")];
564 context.filters.push(({ item }) => filter(item));
565 Styles.splitContext(context);
568 function nameFlag(filter) ({
569 names: ["-name", "-n"],
570 description: "The name of this stylesheet",
571 type: modules.CommandOption.STRING,
572 completer: function (context, args) {
573 context.keys.text = sheet => sheet.name;
574 context.filters.unshift(({ item }) => item.name);
575 sheets(context, args, filter);
579 commands.add(["sty[le]"],
580 "Add or list user styles",
582 let [filter, css] = args;
585 styles.list(window.content, filter ? filter.split(",") : null, args["-name"], args.explicitOpts["-group"] ? [args["-group"]] : null);
587 util.assert(args["-group"].modifiable && args["-group"].hive.modifiable,
588 _("group.cantChangeBuiltin", _("style.styles")));
590 if (args["-append"]) {
591 let sheet = args["-group"].get(args["-name"]);
593 filter = array(sheet.sites).concat(filter).uniq().join(",");
594 css = sheet.css + " " + css;
597 let style = args["-group"].add(args["-name"], filter, css, args["-agent"]);
599 if (args["-nopersist"] || !args["-append"] || style.persist === undefined)
600 style.persist = !args["-nopersist"];
604 completer: function (context, args) {
606 let sheet = args["-group"].get(args["-name"]);
607 if (args.completeArg == 0) {
609 context.completions = [[sheet.sites.join(","), "Current Value"]];
610 context.fork("sites", 0, Styles, "completeSite", window.content, args["-group"]);
612 else if (args.completeArg == 1) {
614 context.completions = [
615 [sheet.css, _("option.currentValue")]
617 context.fork("css", 0, modules.completion, "css");
623 { names: ["-agent", "-A"], description: "Apply style as an Agent sheet" },
624 { names: ["-append", "-a"], description: "Append site filter and css to an existing, matching sheet" },
625 contexts.GroupFlag("styles"),
627 { names: ["-nopersist", "-N"], description: "Do not save this style to an auto-generated RC file" }
629 serialize: function ()
631 .filter(hive => hive.persist)
633 hive.sheets.filter(style => style.persist)
634 .sort((a, b) => String.localeCompare(a.name || "",
638 arguments: [style.sites.join(",")],
639 literalArg: style.css,
641 "-group": hive.name == "user" ? undefined : hive.name,
642 "-name": style.name || undefined
650 name: ["stylee[nable]", "stye[nable]"],
651 desc: "Enable a user style sheet",
652 action: function (sheet) sheet.enabled = true,
653 filter: function (sheet) !sheet.enabled
656 name: ["styled[isable]", "styd[isable]"],
657 desc: "Disable a user style sheet",
658 action: function (sheet) sheet.enabled = false,
659 filter: function (sheet) sheet.enabled
662 name: ["stylet[oggle]", "styt[oggle]"],
663 desc: "Toggle a user style sheet",
664 action: function (sheet) sheet.enabled = !sheet.enabled
667 name: ["dels[tyle]"],
668 desc: "Remove a user style sheet",
669 action: function (sheet) sheet.remove(),
671 ].forEach(function (cmd) {
672 commands.add(cmd.name, cmd.desc,
674 dactyl.assert(args.bang ^ !!(args[0] || args[1] || args["-name"] || args["-index"]),
675 _("error.argumentOrBang"));
677 args["-group"].find(args["-name"], args[0], args.literalArg, args["-index"])
678 .forEach(cmd.action);
681 completer: function (context, args) {
682 let uris = util.visibleURIs(window.content);
684 Styles.completeSite(context, window.content, args["-group"]);
686 context.filters.push(({ sheets }) => sheets.some(cmd.filter));
690 contexts.GroupFlag("styles"),
692 names: ["-index", "-i"],
693 type: modules.CommandOption.INT,
694 completer: function (context, args) {
695 context.keys.text = sheet => args["-group"].sheets.indexOf(sheet);
696 sheets(context, args, cmd.filter);
704 contexts: function initContexts(dactyl, modules, window) {
705 modules.contexts.Hives("styles",
706 Class("LocalHive", Contexts.Hive, {
707 init: function init(group) {
708 init.superapply(this, arguments);
709 this.hive = styles.addHive(group.name, this, this.persist);
712 get names() this.hive.names,
713 get sheets() this.hive.sheets,
714 get sites() this.hive.sites,
716 __noSuchMethod__: function __noSuchMethod__(meth, args) {
717 return this.hive[meth].apply(this.hive, args);
720 destroy: function () {
721 this.hive.dropRef(this);
725 completion: function initCompletion(dactyl, modules, window) {
726 const names = Array.slice(DOM(["div"], window.document).style);
727 modules.completion.css = function (context) {
728 context.title = ["CSS Property"];
729 context.keys = { text: function (p) p + ":",
730 description: function () "" };
732 for (let match in Styles.propertyIter(context.filter, true))
733 var lastMatch = match;
735 if (lastMatch != null && !lastMatch.value && !lastMatch.postSpace) {
736 context.advance(lastMatch.index + lastMatch.preSpace.length);
737 context.completions = names;
741 javascript: function initJavascript(dactyl, modules, window) {
742 modules.JavaScript.setCompleter(["get", "add", "remove", "find"].map(m => Hive.prototype[m]),
743 [ // Prototype: (name, filter, css, index)
744 function (context, obj, args) this.names,
745 (context, obj, args) => Styles.completeSite(context, window.content),
747 function (context, obj, args) this.sheets
750 template: function initTemplate() {
751 let patterns = Styles.patterns;
753 template.highlightCSS = function highlightCSS(css) {
754 return this.highlightRegexp(css, patterns.property, function (match) {
757 return ["", match.preSpace, template.filter(match.name), ": ",
759 template.highlightRegexp(match.value, patterns.token, function (match) {
761 return ["", template.filter(match.word),
762 template.highlightRegexp(match.function, patterns.string,
763 match => ["span", { highlight: "String" },
766 if (match.important == "!important")
767 return ["span", { highlight: "String" }, match.important];
769 return ["span", { highlight: "String" }, match.string];
770 return template._highlightRegexp(match.wholeMatch, /^(\d+)(em|ex|px|in|cm|mm|pt|pc)?/g,
772 ["span", { highlight: "Number" }, n],
773 ["span", { highlight: "Object" }, u || ""]
785 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
787 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: