// 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: ["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) && Object.freeze) 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() cssUri(this.fullCSS), get enabled() this._enabled, set enabled(on) { if (on != this._enabled || this.uri != this._uri) { if (on) this.enabled = false; else if (!this._uri) return; let meth = on ? "registerSheet" : "unregisterSheet"; styles[meth](on ? this.uri : this._uri, on ? this.agent : this._agent); this._agent = this.agent; this._enabled = Boolean(on); this._uri = this.uri; } }, 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; if (filter[0] == "*") return namespace + css; let selectors = filter.map(function (part) (/[*]$/.test(part) ? "url-prefix" : /[\/:]/.test(part) ? "url" : "domain") + '("' + part.replace(/"/g, "%22").replace(/\*$/, "") + '")') .join(", "); return "/* Dactyl style #" + this.id + (this.agent ? " (agent)" : "") + " */ " + namespace + " @-moz-document " + selectors + "{\n" + css + "\n}\n"; } }); var Hive = Class("Hive", { init: function () { this.sheets = []; this.names = {}; }, 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); } 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]; } this.sheets = this.sheets.filter(function (s) matches.indexOf(s) == -1); return matches.length; }, }); try { /** * Manages named and unnamed user style sheets, which apply to both * chrome and content pages. * * @author Kris Maglione */ var Styles = Module("Styles", { init: function () { this._id = 0; this.user = Hive(); this.system = Hive(); }, cleanup: function cleanup() { for each (let hive in [this.user, this.system]) hive.cleanup(); }, __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) }), 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; return Object.keys(props)[sort ? "sort" : "slice"]() .map(function (prop) prop + ": " + props[prop] + ";") .join(" "); }, completeSite: function (context, content) { context.anchored = false; try { context.fork("current", 0, this, function (context) { context.title = ["Current Site"]; context.completions = [ [content.location.host, "Current Host"], [content.location.href, "Current URL"] ]; }); } catch (e) {} context.fork("others", 0, this, function (context) { context.title = ["Site"]; context.completions = [[s, ""] for ([, s] in Iterator(styles.user.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 (filter[0] == "^") { let re = RegExp(filter[0]); 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]); }, propertyIter: function (str, always) { let i = 0; for (let match in this.propertyPattern.iterate(str)) if (always && !i++ || match[0] && match.value) yield match; }, propertyPattern: util.regexp( *) (?P [-a-z]*) (?: * : \s* (?P (?: [-\w]+ (?: \s* \( \s* (?: | [^)]* ) \s* (?: \) | $) )? \s* | \s* \s* | * | [^;}]* )* ) )? ) (?P * (?: ; | $) ) ]]>, "gi", { space: /(?: \s | \/\* .*? \*\/ )/, string: /(?:" (?:[^\\"]|\\.)* (?:"|$) | '(?:[^\\']|\\.)* (?:'|$) )/ }), patterns: memoize({ get property() util.regexp( *) (?P [-a-z]*) (?: * : \s* (?P * ) )? ) (?P * (?: ; | $) ) ]]>, "gi", this), get function() util.regexp( \s* \( \s* (?: | [^)]* ) \s* (?: \) | $) ) ]]>, "g", this), space: /(?: \s | \/\* .*? \*\/ )/, get string() util.regexp( " (?:[^\\"]|\\.)* (?:"|$) | ' (?:[^\\']|\\.)* (?:'|$) ) ]]>, "g", this), get token() util.regexp( (?P [-\w]+) ? \s* | (?P !important\b) | \s* \s* | + | [^;}\s]+ ) ]]>, "gi", this) }) }, { commands: function (dactyl, modules, window) { const commands = modules.commands; commands.add(["sty[le]"], "Add or list user styles", function (args) { let [filter, css] = args; if (css) { if ("-append" in args) { let sheet = styles.user.get(args["-name"]); if (sheet) { filter = sheet.sites.concat(filter).join(","); css = sheet.css + " " + css; } } styles.user.add(args["-name"], filter, css, args["-agent"]); } else { let list = styles.user.sheets.slice() .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(window.content); let name = args["-name"]; modules.commandline.commandOutput( template.tabular(["", "Name", "Filter", "CSS"], ["min-width: 1em; text-align: center; color: red; font-weight: bold;", "padding: 0 1em 0 1ex; vertical-align: top;", "padding: 0 1em 0 0; vertical-align: top;"], ([sheet.enabled ? "" : UTF8("×"), sheet.name || styles.user.sheets.indexOf(sheet), sheet.formatSites(uris), sheet.css] for (sheet in values(list)) if ((!filter || sheet.sites.indexOf(filter) >= 0) && (!name || sheet.name == name))))); } }, { bang: true, completer: function (context, args) { let compl = []; let sheet = styles.user.get(args["-name"]); if (args.completeArg == 0) { if (sheet) context.completions = [[sheet.sites.join(","), "Current Value"]]; context.fork("sites", 0, Styles, "completeSite", window.content); } else if (args.completeArg == 1) { if (sheet) context.completions = [[sheet.css, "Current Value"]]; 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" }, { names: ["-name", "-n"], description: "The name of this stylesheet", completer: function () [[k, v.css] for ([k, v] in Iterator(styles.user.names))], type: modules.CommandOption.STRING } ], serialize: function () [ { command: this.name, arguments: [sty.sites.join(",")], bang: true, literalArg: sty.css, options: sty.name ? { "-name": sty.name } : {} } for ([k, sty] in Iterator(styles.user.sheets.slice().sort(function (a, b) String.localeCompare(a.name || "", b.name || "")))) ] }); [ { 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) { function splitContext(context, generate) { for (let item in Iterator({ Active: true, Inactive: false })) { let [name, active] = item; context.split(name, null, function (context) { context.title[0] = name + " Sheets"; context.filters.push(function (item) item.active == active); }); } } function sheets(context) { let uris = util.visibleURIs(window.content); context.compare = modules.CompletionContext.Sort.number; context.generate = function () styles.user.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 (cmd.filter) context.filters.push(function ({ item }) cmd.filter(item)); splitContext(context); } commands.add(cmd.name, cmd.desc, function (args) { styles.user.find(args["-name"], args[0], args.literalArg, args["-index"]) .forEach(cmd.action); }, { completer: function (context) { let uris = util.visibleURIs(window.content); context.generate = function () styles.user.sites; context.keys.text = util.identity; context.keys.description = function (site) this.sheets.length + " sheet" + (this.sheets.length == 1 ? "" : "s") + ": " + array.compact(this.sheets.map(function (s) s.name)).join(", "); context.keys.sheets = function (site) styles.user.sheets.filter(function (s) s.sites.indexOf(site) >= 0); context.keys.active = function (site) uris.some(Styles.matchFilter(site)); if (cmd.filter) context.filters.push(function ({ sheets }) sheets.some(cmd.filter)); splitContext(context); }, literal: 1, options: [ { names: ["-index", "-i"], type: modules.CommandOption.INT, completer: function (context) { context.keys.text = function (sheet) styles.user.sheets.indexOf(sheet); sheets(context); }, }, { names: ["-name", "-n"], type: modules.CommandOption.STRING, completer: function (context) { context.keys.text = function (sheet) sheet.name; context.filters.push(function ({ item }) item.name); sheets(context); } } ] }); }); }, 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: