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("highlight", {
9 exports: ["Highlight", "Highlights", "highlight"],
10 require: ["services", "styles", "util"],
11 use: ["messages", "template"]
14 var Highlight = Struct("class", "selector", "sites",
15 "defaultExtends", "defaultValue",
16 "value", "extends", "agent",
17 "base", "baseClass", "style");
18 Highlight.liveProperty = function (name, prop) {
19 this.prototype.__defineGetter__(name, function () this.get(name));
20 this.prototype.__defineSetter__(name, function (val) {
21 if (isObject(val) && name !== "style") {
23 val = Array.slice(val);
25 val = update({}, val);
30 if (name === "value" || name === "extends")
31 for (let h in highlight)
32 if (h.extends.indexOf(this.class) >= 0)
35 this.style[prop || name] = this[prop || name];
38 Highlight.liveProperty("agent");
39 Highlight.liveProperty("extends", "css");
40 Highlight.liveProperty("value", "css");
41 Highlight.liveProperty("selector", "css");
42 Highlight.liveProperty("sites");
43 Highlight.liveProperty("style", "css");
45 Highlight.defaultValue("baseClass", function () /^(\w*)/.exec(this.class)[0]);
47 Highlight.defaultValue("selector", function () highlight.selector(this.class));
49 Highlight.defaultValue("sites", function ()
50 this.base ? this.base.sites
51 : ["resource://dactyl*", "dactyl:*", "file://*"].concat(
52 highlight.styleableChrome));
54 Highlight.defaultValue("style", function ()
55 styles.system.add("highlight:" + this.class, this.sites, this.css, this.agent, true));
57 Highlight.defaultValue("defaultExtends", function () []);
58 Highlight.defaultValue("defaultValue", function () "");
59 Highlight.defaultValue("extends", function () this.defaultExtends);
60 Highlight.defaultValue("value", function () this.defaultValue);
62 update(Highlight.prototype, {
63 get base() this.baseClass != this.class && highlight.highlight[this.baseClass] || null,
65 get bases() array.compact(this.extends.map(function (name) highlight.get(name))),
71 this.gettingCSS = true;
72 return this.bases.map(function (b) b.cssText.replace(/;?\s*$/, "; ")).join("");
75 this.gettingCSS = false;
79 get css() this.selector + "{" + this.cssText + "}",
81 get cssText() this.inheritedCSS + this.value,
83 toString: function () "Highlight(" + this.class + ")\n\t" +
84 [k + ": " + String(v).quote() for ([k, v] in this)] .join("\n\t")
88 * A class to manage highlighting rules.
90 * @author Kris Maglione <maglione.k@gmail.com>
92 var Highlights = Module("Highlight", {
98 keys: function keys() Object.keys(this.highlight).sort(),
100 __iterator__: function () values(this.highlight).sort(function (a, b) String.localeCompare(a.class, b.class))
103 _create: function (agent, args) {
104 let obj = Highlight.apply(Highlight, args);
106 if (!isArray(obj.sites))
107 obj.set("sites", obj.sites.split(","));
108 if (!isArray(obj.defaultExtends))
109 obj.set("defaultExtends", obj.defaultExtends.split(","));
110 obj.set("agent", agent);
112 obj.set("defaultValue", Styles.append("", obj.get("defaultValue")));
114 let old = this.highlight[obj.class];
115 this.highlight[obj.class] = obj;
116 // This *must* come before any other property changes.
118 obj.selector = old.selector;
119 obj.style = old.style;
122 if (/^[[>+: ]/.test(args[1]))
123 obj.selector = this.selector(obj.class) + args[1];
125 obj.selector = this.selector(args[1]);
127 if (old && old.value != old.defaultValue)
128 obj.value = old.value;
130 if (!old && obj.base && obj.base.style.enabled)
131 obj.style.enabled = true;
133 this.loaded.__defineSetter__(obj.class, function () {
134 delete this[obj.class];
135 this[obj.class] = true;
137 if (obj.class === obj.baseClass)
138 for (let h in highlight)
139 if (h.baseClass === obj.class)
140 this[h.class] = true;
141 obj.style.enabled = true;
146 get: function (k) this.highlight[k],
148 set: function (key, newStyle, force, append, extend) {
149 let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/);
151 let highlight = this.highlight[key] || this._create(false, [key]);
153 let bases = extend || highlight.extend;
155 newStyle = Styles.append(highlight.value || "", newStyle);
156 bases = highlight.extends.concat(bases);
159 if (/^\s*$/.test(newStyle))
161 if (newStyle == null && extend == null) {
162 if (highlight.defaultValue == null && highight.defaultExtends.length == 0) {
163 highlight.style.enabled = false;
164 delete this.loaded[highlight.class];
165 delete this.highlight[highlight.class];
168 newStyle = highlight.defaultValue;
169 bases = highlight.defaultExtends;
172 highlight.set("value", newStyle || "");
173 highlight.extends = array.uniq(bases, true);
175 highlight.style.enabled = true;
176 this.highlight[highlight.class] = highlight;
181 * Clears all highlighting rules. Rules with default values are
185 for (let [k, v] in Iterator(this.highlight))
186 this.set(k, null, true);
190 * Highlights a node with the given group, and ensures that said
194 * @param {string} group
196 highlightNode: function (node, group, applyBindings) {
197 node.setAttributeNS(NS.uri, "highlight", group);
199 let groups = group.split(" ");
200 for each (let group in groups)
201 this.loaded[group] = true;
204 for each (let group in groups) {
205 if (applyBindings.bindings && group in applyBindings.bindings)
206 applyBindings.bindings[group](node, applyBindings);
207 else if (group in template.bindings)
208 template.bindings[group](node, applyBindings);
213 * Gets a CSS selector given a highlight group.
215 * @param {string} class
217 selector: function (class_)
219 class_.replace(/(^|[>\s])([A-Z][\w-]+)\b/g,
220 function (m, n1, hl) n1 +
221 (self.highlight[hl] && self.highlight[hl].class != class_
222 ? self.highlight[hl].selector : "[dactyl|highlight~=" + hl + "]")),
224 groupRegexp: util.regexp(<![CDATA[
226 (\s* (?:\S|\s\S)+ \s+)
231 sheetRegexp: util.regexp(<![CDATA[
234 (?P<group> (?:[^;\s]|\s[^;\s])+ )
235 (?:; (?P<selector> (?:[^;\s]|\s[^;\s])+ )? )?
236 (?:; (?P<sites> (?:[^;\s]|\s[^;\s])+ )? )?
237 (?:; (?P<extends> (?:[^;\s]|\s[^;\s])+ )? )?
243 * Bulk loads new CSS rules, in the format of,
245 * Rules ::= Rule | Rule "\n" Rule
246 * Rule ::= Bang? Star? MatchSpec Space Space+ Css
248 * Comment ::= Space* "//" *
251 * MatchSpec ::= Class
252 * | Class ";" Selector
253 * | Class ";" Selector ";" Sites
254 * | Class ";" Selector ";" Sites ";" Extends
255 * CSS ::= CSSLine | "{" CSSLines "}"
256 * CSSLines ::= CSSLine | CSSLine "\n" CSSLines
258 * Where Class is the name of the sheet, Selector is the CSS
259 * selector for the style, Sites is the comma-separated list of site
260 * filters to apply the style to.
262 * If Selector is not provided, it defaults to [dactyl|highlight~={Class}].
263 * If it is provided and begins with any of "+", ">" or " ", it is
264 * appended to the default.
266 * If Sites is not provided, it defaults to the chrome documents of
267 * the main application window, dactyl help files, and any other
268 * dactyl-specific documents.
270 * If Star is provided, the style is applied as an agent sheet.
272 * The new styles are lazily activated unless Bang or *eager* is
273 * provided. See {@link Util#xmlToDom}.
275 * @param {string} css The rules to load. See {@link Highlights#css}.
276 * @param {boolean} eager When true, load all provided rules immediately.
278 loadCSS: function (css, eager) {
279 String.replace(css, this.groupRegexp, function (m, m1, m2) m1 + " " + m2.replace(/\n\s*/g, " "))
280 .split("\n").filter(function (s) /\S/.test(s) && !/^\s*\/\//.test(s))
281 .forEach(function (highlight) {
283 let bang = eager || /^\s*!/.test(highlight);
284 let star = /^\s*!?\*/.test(highlight);
285 highlight = this._create(star, this.sheetRegexp.exec(highlight).slice(1));
287 highlight.style.enabled = true;
294 commands: function (dactyl, modules) {
295 const { autocommands, commands, completion, CommandOption, config, io } = modules;
298 commands.add(["colo[rscheme]"],
299 "Load a color scheme",
301 let scheme = args[0];
305 if (scheme == "default")
308 lastScheme = modules.io.sourceFromRuntimePath(["colors/" + scheme + "." + config.fileExtension]);
309 dactyl.assert(lastScheme, _("command.colorscheme.notFound", scheme));
311 autocommands.trigger("ColorScheme", { name: scheme });
315 completer: function (context) completion.colorScheme(context)
318 commands.add(["hi[ghlight]"],
319 "Set the style of certain display elements",
321 let style = <![CDATA[
323 display: inline-block !important;
324 position: static !important;
325 margin: 0px !important; padding: 0px !important;
326 width: 3em !important; min-width: 3em !important; max-width: 3em !important;
327 height: 1em !important; min-height: 1em !important; max-height: 1em !important;
328 overflow: hidden !important;
330 let clear = args[0] == "clear";
334 let [key, css] = args;
335 let modify = css || clear || args["-append"] || args["-link"];
337 if (!modify && /&$/.test(key))
338 [clear, modify, key] = [true, true, key.replace(/&$/, "")];
340 dactyl.assert(!(clear && css), _("error.trailing"));
343 modules.commandline.commandOutput(
344 template.tabular(["Key", "Sample", "Link", "CSS"],
345 ["padding: 0 1em 0 0; vertical-align: top; max-width: 16em; overflow: hidden;",
346 "text-align: center"],
348 <span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>,
349 template.map(h.extends, template.highlight),
350 template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g)]
352 if (!key || h.class.indexOf(key) > -1))));
353 else if (!key && clear)
356 highlight.set(key, css, clear, "-append" in args, args["-link"]);
358 util.assert(false, _("error.invalidArgument"));
361 // TODO: add this as a standard highlight completion function?
362 completer: function (context, args) {
363 // Complete a highlight group on :hi clear ...
364 if (args.completeArg > 0 && args[0] == "clear")
365 args.completeArg = args.completeArg > 1 ? -1 : 0;
367 if (args.completeArg == 0)
368 completion.highlightGroup(context);
369 else if (args.completeArg == 1) {
370 let hl = highlight.get(args[0]);
372 context.completions = [[hl.value, "Current Value"], [hl.defaultValue || "", "Default Value"]];
373 context.fork("css", 0, completion, "css");
379 { names: ["-append", "-a"], description: "Append new CSS to the existing value" },
381 names: ["-link", "-l"],
382 description: "Link this group to another",
383 type: CommandOption.LIST,
384 completer: function (context, args) {
385 let group = args[0] && highlight.get(args[0]);
387 context.fork("extra", 0, this, function (context) [
388 [String(group.extends), "Current Value"],
389 [String(group.defaultExtends) || "", "Default Value"]
391 context.fork("groups", 0, completion, "highlightGroup");
395 serialize: function () [
398 arguments: [v.class],
401 for (v in Iterator(highlight))
402 if (v.value != v.defaultValue)
406 completion: function (dactyl, modules) {
407 const { completion, config, io } = modules;
408 completion.colorScheme = function colorScheme(context) {
409 let extRe = RegExp("\\." + config.fileExtension + "$");
411 context.title = ["Color Scheme", "Runtime Path"];
412 context.keys = { text: function (f) f.leafName.replace(extRe, ""), description: ".parent.path" };
413 context.completions = array.flatten(
414 io.getRuntimeDirectories("colors").map(
415 function (dir) dir.readDirectory().filter(
416 function (file) extRe.test(file.leafName))));
420 completion.highlightGroup = function highlightGroup(context) {
421 context.title = ["Highlight Group", "Value"];
422 context.completions = [[v.class, v.value] for (v in highlight)];
425 javascript: function (dactyl, modules, window) {
426 modules.JavaScript.setCompleter(["get", "set"].map(function (m) highlight[m]),
427 [ function (context, obj, args) Iterator(highlight.highlight) ]);
428 modules.JavaScript.setCompleter(["highlightNode"].map(function (m) highlight[m]),
429 [ null, function (context, obj, args) Iterator(highlight.highlight) ]);
433 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
437 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: