1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-19 07:10:18 +01:00
Files
pentadactyl-pm/common/modules/styles.jsm
2015-07-23 01:15:41 +10:00

814 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2008-2015 Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";
defineModule("styles", {
exports: ["Style", "Styles", "styles"],
require: ["services", "util"]
});
lazyRequire("contexts", ["Contexts"]);
lazyRequire("template", ["template"]);
var namespace = "@namespace html " + JSON.stringify(XHTML) + ";\n" +
"@namespace xul " + JSON.stringify(XUL) + ";\n" +
"@namespace dactyl " + JSON.stringify(NS) + ";\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 () { return 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,
filter => ["span", { highlight: uris.some(Styles.matchFilter(filter)) ? "Filter" : "" }, filter],
","),
remove: function () { this.hive.remove(this); },
get uri() {
return "dactyl://style/" + this.id + "/" +
this.hive.name + "/" + (this.name || "");
},
get enabled() { return 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(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(part =>
!/^(?:[a-z-]+[:*]|[a-z-.]+$)/i.test(part) ? "regexp(" + Styles.quote(".*(?:" + part + ").*") + ")" :
(/[*]$/.test(part) ? "url-prefix" :
/[\/:]/.test(part) ? "url"
: "domain")
+ '(' + Styles.quote(part.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() { return this.name !== "system"; },
addRef: function (obj) {
this.refs.push(util.weakReference(obj));
this.dropRef(null);
},
dropRef: function (obj) {
this.refs = this.refs.filter(ref => (ref.get() && ref.get() !== obj));
if (!this.refs.length) {
this.cleanup();
styles.hives = styles.hives.filter(h => h !== this);
}
},
cleanup: function cleanup() {
for (let sheet of this.sheets)
util.trapErrors(() => {
sheet.enabled = false;
});
},
"@@iterator": function () iter(this.sheets),
get sites() {
return Ary(this.sheets).map(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))
// Need an array from the same compartment.
filter = Array.slice(filter);
else
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(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] of iter(this.sheets))];
if (index)
matches = String(index).split(",").filter(i => i in this.sheets);
if (name)
matches = matches.filter(i => this.sheets[i].name == name);
if (css)
matches = matches.filter(i => this.sheets[i].css == css);
if (filter)
matches = matches.filter(i => this.sheets[i].sites.indexOf(filter) >= 0);
return matches.map(i => this.sheets[i]);
},
/**
* 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) {
if (arguments.length == 1) {
var matches = [name];
name = null;
}
if (filter && filter.contains(","))
return filter.split(",").reduce(
(n, f) => n + this.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 of matches.reverse()) {
if (filter) {
let sites = sheet.sites.filter(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(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 <maglione.k@gmail.com>
*/
var Styles = Module("Styles", {
Local: function (dactyl, modules, window) ({
cleanup: function () {}
}),
init: function () {
this._id = 0;
this.cleanup();
this.allSheets = {};
update(services["dactyl:"].providers, {
"style": function styleProvider(uri, path) {
let id = parseInt(path);
if (hasOwnProperty(styles.allSheets, id))
return ["text/css", styles.allSheets[id].fullCSS];
return null;
}
});
},
cleanup: function cleanup() {
for (let hive of 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 = this.hives.find(h => h.name === name);
if (!hive) {
hive = Hive(name, persist);
this.hives.push(hive);
}
hive.persist = persist;
if (ref)
hive.addRef(ref);
return hive;
},
"@@iterator": function () iter(this.user.sheets.concat(this.system.sheets)),
_proxy: function (name, args) {
let obj = this[args[0] ? "system" : "user"];
return apply(obj, name, 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(h => (h.modifiable && h.sheets.length));
function sheets(group) {
return group.sheets.slice()
.filter(sheet => ((!name || sheet.name === name) &&
(!sites || sites.every(s => sheet.sites.indexOf(s) >= 0))))
.sort((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 = ["table", {},
["tr", { highlight: "Title" },
["td"],
["td"],
["td", { style: "padding-right: 1em;" }, _("title.Name")],
["td", { style: "padding-right: 1em;" }, _("title.Filter")],
["td", { style: "padding-right: 1em;" }, _("title.CSS")]],
["col", { style: "min-width: 4em; padding-right: 1em;" }],
["col", { style: "min-width: 1em; text-align: center; color: red; font-weight: bold;" }],
["col", { style: "padding: 0 1em 0 1ex; vertical-align: top;" }],
["col", { style: "padding: 0 1em 0 0; vertical-align: top;" }],
template.map(hives, hive => {
let i = 0;
return [
["tr", { style: "height: .5ex;" }],
template.map(sheets(hive), sheet =>
["tr", {},
["td", { highlight: "Title" }, !i++ ? hive.name : ""],
["td", {}, sheet.enabled ? "" : UTF8("×")],
["td", {}, sheet.name || hive.sheets.indexOf(sheet)],
["td", {}, sheet.formatSites(uris)],
["td", {}, sheet.css]]),
["tr", { style: "height: .5ex;" }]];
})];
// E4X-FIXME
// // 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 (let str of [dest, src])
for (let prop of Styles.propertyIter(str))
props[prop.name] = prop.value;
let val = Object.keys(props)[sort ? "sort" : "slice"]()
.map(prop => prop + ": " + props[prop] + ";")
.join(" ");
if (/^\s*(\/\*.*?\*\/)/.exec(src))
val = RegExp.$1 + " " + val;
return val;
},
completeSite: function (context, content, group=styles.user) {
context.anchored = false;
try {
context.fork("current", 0, this, 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 = () => values(group.sites);
context.keys.text = identity;
context.keys.description = function (site) {
return this.sheets.length + /*L*/" sheet" +
(this.sheets.length == 1 ? "" : "s") + ": " +
Ary.compact(this.sheets.map(s => s.name)).join(", ");
};
context.keys.sheets = site => group.sheets.filter(s => s.sites.indexOf(site) >= 0);
context.keys.active = 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) {
filter = filter.trim();
if (filter === "*")
var test = function test(uri) { return true; };
else if (!/^(?:[a-z-]+:|[a-z-.]+$)/.test(filter)) {
let re = util.regexp(filter);
test = function test(uri) { return re.test(uri.spec); };
}
else if (/[*]$/.test(filter)) {
let re = RegExp("^" + util.regexp.escape(filter.substr(0, filter.length - 1)));
test = function test(uri) { return re.test(uri.spec); };
}
else if (/[\/:]/.test(filter))
test = function test(uri) { return uri.spec === filter; };
else
test = function test(uri) {
try {
return util.isSubdomain(uri.host, filter);
}
catch (e) { return false; }
};
test.toString = function toString() { return filter; }
test.key = filter;
if (arguments.length < 2)
return test;
return test(arguments[1]);
},
splitContext: function splitContext(context, title) {
for (let item of iter({ Active: true, Inactive: false })) {
let [name, active] = item;
context.split(name, null, context => {
context.title[0] = /*L*/name + " " + (title || "Sheets");
context.filters.push(item => !!item.active == active);
});
}
},
propertyIter: function* (str, always) {
let i = 0;
let x = /The status bar/.test(str);
for (let match of 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(literal(function () /*
(?:
(?P<preSpace> <space>*)
(?P<name> [-a-z]*)
(?:
<space>* : \s* (?P<value>
(?:
[-\w]+
(?:
\s* \( \s*
(?: <string> | [^)]* )
\s* (?: \) | $)
)?
\s*
| \s* <string> \s*
| <space>*
| [^;}]*
)*
)
)?
)
(?P<postSpace> <space>* (?: ; | $) )
*/$), "gix",
{
space: /(?: \s | \/\* .*? \*\/ )/,
string: /(?:" (?:[^\\"]|\\.)* (?:"|$) | '(?:[^\\']|\\.)* (?:'|$) )/
}),
patterns: memoize({
get property() {
return util.regexp(literal(function () /*
(?:
(?P<preSpace> <space>*)
(?P<name> [-a-z]*)
(?:
<space>* : \s* (?P<value>
<token>*
)
)?
)
(?P<postSpace> <space>* (?: ; | $) )
*/$), "gix", this);
},
get function() {
return util.regexp(literal(function () /*
(?P<function>
\s* \( \s*
(?: <string> | [^)]* )
\s* (?: \) | $)
)
*/$), "gx", this);
},
space: /(?: \s | \/\* .*? \*\/ )/,
get string() {
return util.regexp(literal(function () /*
(?P<string>
" (?:[^\\"]|\\.)* (?:"|$) |
' (?:[^\\']|\\.)* (?:'|$)
)
*/$), "gx", this);
},
get token() {
return util.regexp(literal(function () /*
(?P<token>
(?P<word> [-\w]+)
<function>?
\s*
| (?P<important> !important\b)
| \s* <string> \s*
| <space>+
| [^;}\s]+
)
*/$), "gix", this);
}
}),
/**
* Quotes a string for use in CSS stylesheets.
*
* @param {string} str
* @returns {string}
*/
quote: function quote(str) {
return '"' + str.replace(/([\\"])/g, "\\$1").replace(/\n/g, "\\00000a") + '"';
}
}, {
commands: function initCommands(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 = () => args["-group"].sheets;
context.keys.active = sheet => uris.some(sheet.bound.match);
context.keys.description = sheet => [sheet.formatSites(uris), ": ", sheet.css.replace("\n", "\\n")];
if (filter)
context.filters.push(({ item }) => filter(item));
Styles.splitContext(context);
}
function nameFlag(filter) {
return {
names: ["-name", "-n"],
description: "The name of this stylesheet",
type: modules.CommandOption.STRING,
completer: function (context, args) {
context.keys.text = sheet => sheet.name;
context.filters.unshift(({ 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 = Ary(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 sheet = args["-group"].get(args["-name"]);
if (args.completeArg == 0) {
if (sheet)
context.completions = [[sheet.sites.join(","), _("option.currentValue")]];
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 () {
return Ary(styles.hives)
.filter(hive => hive.persist)
.map(hive =>
hive.sheets.filter(style => style.persist)
.sort((a, b) => String.localeCompare(a.name || "",
b.name || ""))
.map(style => ({
command: "style",
arguments: [style.sites.join(",")],
literalArg: style.css,
options: {
"-group": hive.name == "user" ? undefined : hive.name,
"-name": style.name || undefined
}
})))
.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) {
Styles.completeSite(context, window.content, args["-group"]);
if (cmd.filter)
context.filters.push(({ 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 = sheet => args["-group"].sheets.indexOf(sheet);
sheets(context, args, cmd.filter);
}
},
nameFlag(cmd.filter)
]
});
});
},
contexts: function initContexts(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() { return this.hive.names; },
get sheets() { return this.hive.sheets; },
get sites() { return this.hive.sites; },
__noSuchMethod__: function __noSuchMethod__(meth, args) {
return apply(this.hive, meth, args);
},
destroy: function () {
this.hive.dropRef(this);
}
}));
},
completion: function initCompletion(dactyl, modules, window) {
const names = Array.slice(DOM(["div"], window.document).style);
modules.completion.css = context => {
context.title = ["CSS Property"];
context.keys = { text: function (p) p + ":",
description: function () "" };
for (let match of 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 initJavascript(dactyl, modules, window) {
modules.JavaScript.setCompleter(["get", "add", "remove", "find"].map(m => Hive.prototype[m]),
[ // Prototype: (name, filter, css, index)
function (context, obj, args) this.names,
(context, obj, args) => Styles.completeSite(context, window.content),
null,
function (context, obj, args) this.sheets
]);
},
template: function initTemplate() {
let patterns = Styles.patterns;
template.highlightCSS = function highlightCSS(css) {
return this.highlightRegexp(css, patterns.property, match => {
if (!match.length)
return [];
return ["", match.preSpace, template.filter(match.name), ": ",
template.highlightRegexp(match.value, patterns.token, match => {
if (match.function)
return ["", template.filter(match.word),
template.highlightRegexp(match.function, patterns.string,
match => ["span", { highlight: "String" },
match.string])
];
if (match.important == "!important")
return ["span", { highlight: "String" }, match.important];
if (match.string)
return ["span", { highlight: "String" }, match.string];
return template._highlightRegexp(match.wholeMatch, /^(\d+)(em|ex|px|in|cm|mm|pt|pc)?/g,
(m, n, u) => [
["span", { highlight: "Number" }, n],
["span", { highlight: "Object" }, u || ""]
]);
}),
match.postSpace
];
});
};
}
});
endModule();
// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: