// Copyright (c) 2008-2011 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. "use strict"; Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("styles", { exports: ["Style", "Styles", "styles"], require: ["services", "util"], use: ["contexts", "messages", "template"] }, this); function cssUri(css) "chrome-data:text/css," + encodeURI(css); var namespace = "@namespace html " + XHTML.uri.quote() + ";\n" + "@namespace xul " + XUL.uri.quote() + ";\n" + "@namespace dactyl " + NS.uri.quote() + ";\n"; var Sheet = Struct("name", "id", "sites", "css", "hive", "agent"); Sheet.liveProperty = function (name) { let i = this.prototype.members[name]; this.prototype.__defineGetter__(name, function () this[i]); this.prototype.__defineSetter__(name, function (val) { if (isArray(val)) val = Array.slice(val); if (isArray(val)) Object.freeze(val); this[i] = val; this.enabled = this.enabled; }); } Sheet.liveProperty("agent"); Sheet.liveProperty("css"); Sheet.liveProperty("sites"); update(Sheet.prototype, { formatSites: function (uris) template.map(this.sites, function (filter) {filter}, <>,), remove: function () { this.hive.remove(this); }, get uri() "dactyl://style/" + this.id + "/" + this.hive.name + "/" + (this.name || ""), get enabled() this._enabled, set enabled(on) { if (on != this._enabled || this.fullCSS != this._fullCSS) { if (on) this.enabled = false; else if (!this._fullCSS) return; let meth = on ? "registerSheet" : "unregisterSheet"; styles[meth](this.uri, on ? this.agent : this._agent); this._agent = this.agent; this._enabled = Boolean(on); this._fullCSS = this.fullCSS; } }, match: function (uri) { if (isString(uri)) uri = util.newURI(uri); return this.sites.some(function (site) Styles.matchFilter(site, uri)); }, get fullCSS() { let filter = this.sites; let css = this.css; let preamble = "/* " + this.uri + (this.agent ? " (agent)" : "") + " */\n\n" + namespace + "\n"; if (filter[0] == "*") return preamble + css; let selectors = filter.map(function (part) !/^(?:[a-z-]+[:*]|[a-z-.]+$)/i.test(part) ? "regexp(" + (".*(?:" + part + ").*").quote() + ")" : (/[*]$/.test(part) ? "url-prefix" : /[\/:]/.test(part) ? "url" : "domain") + '("' + part.replace(/"/g, "%22").replace(/\*$/, "") + '")') .join(",\n "); return preamble + "@-moz-document " + selectors + " {\n\n" + css + "\n\n}\n"; } }); var Hive = Class("Hive", { init: function (name, persist) { this.name = name; this.sheets = []; this.names = {}; this.refs = []; this.persist = persist; }, get modifiable() this.name !== "system", addRef: function (obj) { this.refs.push(Cu.getWeakReference(obj)); this.dropRef(null); }, dropRef: function (obj) { this.refs = this.refs.filter(function (ref) ref.get() && ref.get() !== obj); if (!this.refs.length) { this.cleanup(); styles.hives = styles.hives.filter(function (h) h !== this, this); } }, cleanup: function cleanup() { for (let sheet in values(this.sheets)) sheet.enabled = false; }, __iterator__: function () Iterator(this.sheets), get sites() array(this.sheets).map(function (s) s.sites).flatten().uniq().array, /** * Add a new style sheet. * * @param {string} name The name given to the style sheet by * which it may be later referenced. * @param {string} filter The sites to which this sheet will * apply. Can be a domain name or a URL. Any URL ending in * "*" is matched as a prefix. * @param {string} css The CSS to be applied. * @param {boolean} agent If true, the sheet is installed as an * agent sheet. * @param {boolean} lazy If true, the sheet is not initially enabled. * @returns {Sheet} */ add: function add(name, filter, css, agent, lazy) { if (!isArray(filter)) filter = filter.split(","); if (name && name in this.names) { var sheet = this.names[name]; sheet.agent = agent; sheet.css = String(css); sheet.sites = filter; } else { sheet = Sheet(name, styles._id++, filter.filter(util.identity), String(css), this, agent); this.sheets.push(sheet); } styles.allSheets[sheet.id] = sheet; if (!lazy) sheet.enabled = true; if (name) this.names[name] = sheet; return sheet; }, /** * Get a sheet with a given name or index. * * @param {string or number} sheet The sheet to retrieve. Strings indicate * sheet names, while numbers indicate indices. */ get: function get(sheet) { if (typeof sheet === "number") return this.sheets[sheet]; return this.names[sheet]; }, /** * Find sheets matching the parameters. See {@link #addSheet} * for parameters. * * @param {string} name * @param {string} filter * @param {string} css * @param {number} index */ find: function find(name, filter, css, index) { // Grossly inefficient. let matches = [k for ([k, v] in Iterator(this.sheets))]; if (index) matches = String(index).split(",").filter(function (i) i in this.sheets, this); if (name) matches = matches.filter(function (i) this.sheets[i].name == name, this); if (css) matches = matches.filter(function (i) this.sheets[i].css == css, this); if (filter) matches = matches.filter(function (i) this.sheets[i].sites.indexOf(filter) >= 0, this); return matches.map(function (i) this.sheets[i], this); }, /** * Remove a style sheet. See {@link #addSheet} for parameters. * In cases where *filter* is supplied, the given filters are removed from * matching sheets. If any remain, the sheet is left in place. * * @param {string} name * @param {string} filter * @param {string} css * @param {number} index */ remove: function remove(name, filter, css, index) { let self = this; if (arguments.length == 1) { var matches = [name]; name = null; } if (filter && filter.indexOf(",") > -1) return filter.split(",").reduce( function (n, f) n + self.removeSheet(name, f, index), 0); if (filter == undefined) filter = ""; if (!matches) matches = this.findSheets(name, filter, css, index); if (matches.length == 0) return null; for (let [, sheet] in Iterator(matches.reverse())) { if (filter) { let sites = sheet.sites.filter(function (f) f != filter); if (sites.length) { sheet.sites = sites; continue; } } sheet.enabled = false; if (sheet.name) delete this.names[sheet.name]; delete styles.allSheets[sheet.id]; } this.sheets = this.sheets.filter(function (s) matches.indexOf(s) == -1); return matches.length; }, }); /** * Manages named and unnamed user style sheets, which apply to both * chrome and content pages. * * @author Kris Maglione */ var Styles = Module("Styles", { Local: function (dactyl, modules, window) ({ cleanup: function () {} }), init: function () { this._id = 0; this.cleanup(); this.allSheets = {}; services["dactyl:"].providers["style"] = function styleProvider(uri) { let id = /^\/(\d*)/.exec(uri.path)[1]; if (Set.has(styles.allSheets, id)) return ["text/css", styles.allSheets[id].fullCSS]; return null; }; }, cleanup: function cleanup() { for each (let hive in this.hives) util.trapErrors("cleanup", hive); this.hives = []; this.user = this.addHive("user", this, true); this.system = this.addHive("system", this, false); }, addHive: function addHive(name, ref, persist) { let hive = array.nth(this.hives, function (h) h.name === name, 0); if (!hive) { hive = Hive(name, persist); this.hives.push(hive); } hive.persist = persist; if (ref) hive.addRef(ref); return hive; }, __iterator__: function () Iterator(this.user.sheets.concat(this.system.sheets)), _proxy: function (name, args) let (obj = this[args[0] ? "system" : "user"]) obj[name].apply(obj, Array.slice(args, 1)), addSheet: deprecated("Styles#{user,system}.add", function addSheet() this._proxy("add", arguments)), findSheets: deprecated("Styles#{user,system}.find", function findSheets() this._proxy("find", arguments)), get: deprecated("Styles#{user,system}.get", function get() this._proxy("get", arguments)), removeSheet: deprecated("Styles#{user,system}.remove", function removeSheet() this._proxy("remove", arguments)), userSheets: Class.Property({ get: deprecated("Styles#user.sheets", function userSheets() this.user.sheets) }), systemSheets: Class.Property({ get: deprecated("Styles#system.sheets", function systemSheets() this.system.sheets) }), userNames: Class.Property({ get: deprecated("Styles#user.names", function userNames() this.user.names) }), systemNames: Class.Property({ get: deprecated("Styles#system.names", function systemNames() this.system.names) }), sites: Class.Property({ get: deprecated("Styles#user.sites", function sites() this.user.sites) }), list: function list(content, sites, name, hives) { const { commandline, dactyl } = this.modules; hives = hives || styles.hives.filter(function (h) h.modifiable && h.sheets.length); function sheets(group) group.sheets.slice() .filter(function (sheet) (!name || sheet.name === name) && (!sites || sites.every(function (s) sheet.sites.indexOf(s) >= 0))) .sort(function (a, b) a.name && b.name ? String.localeCompare(a.name, b.name) : !!b.name - !!a.name || a.id - b.id); let uris = util.visibleURIs(content); let list = { template.map(hives, function (hive) let (i = 0) + template.map(sheets(hive), function (sheet) ) + ) }
{_("title.Name")} {_("title.Filter")} {_("title.CSS")}
{!i++ ? hive.name : ""} {sheet.enabled ? "" : UTF8("×")} {sheet.name || hive.sheets.indexOf(sheet)} {sheet.formatSites(uris)} {sheet.css}
; // TODO: Move this to an ItemList to show this automatically if (list.*.length() === list.text().length() + 5) dactyl.echomsg(_("style.none")); else commandline.commandOutput(list); }, registerSheet: function registerSheet(url, agent, reload) { let uri = services.io.newURI(url, null, null); if (reload) this.unregisterSheet(url, agent); let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"]; if (reload || !services.stylesheet.sheetRegistered(uri, type)) services.stylesheet.loadAndRegisterSheet(uri, type); }, unregisterSheet: function unregisterSheet(url, agent) { let uri = services.io.newURI(url, null, null); let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"]; if (services.stylesheet.sheetRegistered(uri, type)) services.stylesheet.unregisterSheet(uri, type); }, }, { append: function (dest, src, sort) { let props = {}; for each (let str in [dest, src]) for (let prop in Styles.propertyIter(str)) props[prop.name] = prop.value; let val = Object.keys(props)[sort ? "sort" : "slice"]() .map(function (prop) prop + ": " + props[prop] + ";") .join(" "); if (/^\s*(\/\*.*?\*\/)/.exec(src)) val = RegExp.$1 + " " + val; return val; }, completeSite: function (context, content, group) { group = group || styles.user; context.anchored = false; try { context.fork("current", 0, this, function (context) { context.title = ["Current Site"]; context.completions = [ [content.location.host, /*L*/"Current Host"], [content.location.href, /*L*/"Current URL"] ]; }); } catch (e) {} let uris = util.visibleURIs(content); context.generate = function () values(group.sites); context.keys.text = util.identity; context.keys.description = function (site) this.sheets.length + /*L*/" sheet" + (this.sheets.length == 1 ? "" : "s") + ": " + array.compact(this.sheets.map(function (s) s.name)).join(", "); context.keys.sheets = function (site) group.sheets.filter(function (s) s.sites.indexOf(site) >= 0); context.keys.active = function (site) uris.some(Styles.matchFilter(site)); Styles.splitContext(context, "Sites"); }, /** * A curried function which determines which host names match a * given stylesheet filter. When presented with one argument, * returns a matcher function which, given one nsIURI argument, * returns true if that argument matches the given filter. When * given two arguments, returns true if the second argument matches * the given filter. * * @param {string} filter The URI filter to match against. * @param {nsIURI} uri The location to test. * @returns {nsIURI -> boolean} */ matchFilter: function (filter) { if (filter === "*") var test = function test(uri) true; else if (!/^(?:[a-z-]+:|[a-z-.]+$)/.test(filter)) { let re = util.regexp(filter); test = function test(uri) re.test(uri.spec); } else if (/[*]$/.test(filter)) { let re = RegExp("^" + util.regexp.escape(filter.substr(0, filter.length - 1))); test = function test(uri) re.test(uri.spec); } else if (/[\/:]/.test(filter)) test = function test(uri) uri.spec === filter; else test = function test(uri) { try { return util.isSubdomain(uri.host, filter); } catch (e) { return false; } }; test.toString = function toString() filter; if (arguments.length < 2) return test; return test(arguments[1]); }, splitContext: function splitContext(context, title) { for (let item in Iterator({ Active: true, Inactive: false })) { let [name, active] = item; context.split(name, null, function (context) { context.title[0] = /*L*/name + " " + (title || "Sheets"); context.filters.push(function (item) !!item.active == active); }); } }, propertyIter: function (str, always) { let i = 0; for (let match in this.propertyPattern.iterate(str)) { if (match.value || always && match.name || match.wholeMatch === match.preSpace && always && !i++) yield match; if (!/;/.test(match.postSpace)) break; } }, propertyPattern: util.regexp( *) (?P [-a-z]*) (?: * : \s* (?P (?: [-\w]+ (?: \s* \( \s* (?: | [^)]* ) \s* (?: \) | $) )? \s* | \s* \s* | * | [^;}]* )* ) )? ) (?P * (?: ; | $) ) ]]>, "gix", { space: /(?: \s | \/\* .*? \*\/ )/, string: /(?:" (?:[^\\"]|\\.)* (?:"|$) | '(?:[^\\']|\\.)* (?:'|$) )/ }), patterns: memoize({ get property() util.regexp( *) (?P [-a-z]*) (?: * : \s* (?P * ) )? ) (?P * (?: ; | $) ) ]]>, "gix", this), get function() util.regexp( \s* \( \s* (?: | [^)]* ) \s* (?: \) | $) ) ]]>, "gx", this), space: /(?: \s | \/\* .*? \*\/ )/, get string() util.regexp( " (?:[^\\"]|\\.)* (?:"|$) | ' (?:[^\\']|\\.)* (?:'|$) ) ]]>, "gx", this), get token() util.regexp( (?P [-\w]+) ? \s* | (?P !important\b) | \s* \s* | + | [^;}\s]+ ) ]]>, "gix", this) }) }, { commands: function (dactyl, modules, window) { const { commands, contexts, styles } = modules; function sheets(context, args, filter) { let uris = util.visibleURIs(window.content); context.compare = modules.CompletionContext.Sort.number; context.generate = function () args["-group"].sheets; context.keys.active = function (sheet) uris.some(sheet.closure.match); context.keys.description = function (sheet) <>{sheet.formatSites(uris)}: {sheet.css.replace("\n", "\\n")} if (filter) context.filters.push(function ({ item }) filter(item)); Styles.splitContext(context); } function nameFlag(filter) ({ names: ["-name", "-n"], description: "The name of this stylesheet", type: modules.CommandOption.STRING, completer: function (context, args) { context.keys.text = function (sheet) sheet.name; context.filters.unshift(function ({ item }) item.name); sheets(context, args, filter); } }); commands.add(["sty[le]"], "Add or list user styles", function (args) { let [filter, css] = args; if (!css) styles.list(window.content, filter ? filter.split(",") : null, args["-name"], args.explicitOpts["-group"] ? [args["-group"]] : null); else { util.assert(args["-group"].modifiable && args["-group"].hive.modifiable, _("group.cantChangeBuiltin", _("style.styles"))); if (args["-append"]) { let sheet = args["-group"].get(args["-name"]); if (sheet) { filter = array(sheet.sites).concat(filter).uniq().join(","); css = sheet.css + " " + css; } } let style = args["-group"].add(args["-name"], filter, css, args["-agent"]); if (args["-nopersist"] || !args["-append"] || style.persist === undefined) style.persist = !args["-nopersist"]; } }, { completer: function (context, args) { let compl = []; let sheet = args["-group"].get(args["-name"]); if (args.completeArg == 0) { if (sheet) context.completions = [[sheet.sites.join(","), "Current Value"]]; context.fork("sites", 0, Styles, "completeSite", window.content, args["-group"]); } else if (args.completeArg == 1) { if (sheet) context.completions = [ [sheet.css, _("option.currentValue")] ]; context.fork("css", 0, modules.completion, "css"); } }, hereDoc: true, literal: 1, options: [ { names: ["-agent", "-A"], description: "Apply style as an Agent sheet" }, { names: ["-append", "-a"], description: "Append site filter and css to an existing, matching sheet" }, contexts.GroupFlag("styles"), nameFlag(), { names: ["-nopersist", "-N"], description: "Do not save this style to an auto-generated RC file" } ], serialize: function () array(styles.hives) .filter(function (hive) hive.persist) .map(function (hive) hive.sheets.filter(function (style) style.persist) .sort(function (a, b) String.localeCompare(a.name || "", b.name || "")) .map(function (style) ({ command: "style", arguments: [style.sites.join(",")], literalArg: style.css, options: update( hive.name == "user" ? {} : { "-group": hive.name }, style.name ? { "-name": style.name } : {}) }))) .flatten().array }); [ { name: ["stylee[nable]", "stye[nable]"], desc: "Enable a user style sheet", action: function (sheet) sheet.enabled = true, filter: function (sheet) !sheet.enabled }, { name: ["styled[isable]", "styd[isable]"], desc: "Disable a user style sheet", action: function (sheet) sheet.enabled = false, filter: function (sheet) sheet.enabled }, { name: ["stylet[oggle]", "styt[oggle]"], desc: "Toggle a user style sheet", action: function (sheet) sheet.enabled = !sheet.enabled }, { name: ["dels[tyle]"], desc: "Remove a user style sheet", action: function (sheet) sheet.remove(), } ].forEach(function (cmd) { commands.add(cmd.name, cmd.desc, function (args) { dactyl.assert(args.bang ^ !!(args[0] || args[1] || args["-name"] || args["-index"]), _("error.argumentOrBang")); args["-group"].find(args["-name"], args[0], args.literalArg, args["-index"]) .forEach(cmd.action); }, { bang: true, completer: function (context, args) { let uris = util.visibleURIs(window.content); Styles.completeSite(context, window.content, args["-group"]); if (cmd.filter) context.filters.push(function ({ sheets }) sheets.some(cmd.filter)); }, literal: 1, options: [ contexts.GroupFlag("styles"), { names: ["-index", "-i"], type: modules.CommandOption.INT, completer: function (context, args) { context.keys.text = function (sheet) args["-group"].sheets.indexOf(sheet); sheets(context, args, cmd.filter); } }, nameFlag(cmd.filter) ] }); }); }, contexts: function (dactyl, modules, window) { modules.contexts.Hives("styles", Class("LocalHive", Contexts.Hive, { init: function init(group) { init.superapply(this, arguments); this.hive = styles.addHive(group.name, this, this.persist); }, get names() this.hive.names, get sheets() this.hive.sheets, get sites() this.hive.sites, __noSuchMethod__: function __noSuchMethod__(meth, args) { return this.hive[meth].apply(this.hive, args); }, destroy: function () { this.hive.dropRef(this); } })); }, completion: function (dactyl, modules, window) { const names = Array.slice(util.computedStyle(window.document.createElement("div"))); modules.completion.css = function (context) { context.title = ["CSS Property"]; context.keys = { text: function (p) p + ":", description: function () "" }; for (let match in Styles.propertyIter(context.filter, true)) var lastMatch = match; if (lastMatch != null && !lastMatch.value && !lastMatch.postSpace) { context.advance(lastMatch.index + lastMatch.preSpace.length); context.completions = names; } }; }, javascript: function (dactyl, modules, window) { modules.JavaScript.setCompleter(["get", "add", "remove", "find"].map(function (m) styles.user[m]), [ // Prototype: (name, filter, css, index) function (context, obj, args) this.names, function (context, obj, args) Styles.completeSite(context, window.content), null, function (context, obj, args) this.sheets ]); }, template: function () { let patterns = Styles.patterns; template.highlightCSS = function highlightCSS(css) { XML.prettyPrinting = XML.ignoreWhitespace = false; return this.highlightRegexp(css, patterns.property, function (match) <>{ match.preSpace}{template.filter(match.name)}: { template.highlightRegexp(match.value, patterns.token, function (match) { if (match.function) return <>{template.filter(match.word)}{ template.highlightRegexp(match.function, patterns.string, function (match) {match.string}) }; if (match.important == "!important") return {match.important}; if (match.string) return {match.string}; return template.highlightRegexp(match.wholeMatch, /^(\d+)(em|ex|px|in|cm|mm|pt|pc)?/g, function (m, n, u) <>{n}{u || ""}); }) }{ match.postSpace } ) } }, }); endModule(); // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);} // vim: set fdm=marker sw=4 ts=4 et ft=javascript: