]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/highlight.jsm
35288942f54c24afb5b97cfa1e8b09a65fee63da
[dactyl.git] / common / modules / highlight.jsm
1 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
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"]
12 }, this);
13
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") {
22             if (isArray(val))
23                 val = Array.slice(val);
24             else
25                 val = update({}, val);
26             Object.freeze(val);
27         }
28         this.set(name, val);
29
30         if (name === "value" || name === "extends")
31             for (let h in highlight)
32                 if (h.extends.indexOf(this.class) >= 0)
33                     h.style.css = h.css;
34
35         this.style[prop || name] = this[prop || name];
36     });
37 }
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");
44
45 Highlight.defaultValue("baseClass", function () /^(\w*)/.exec(this.class)[0]);
46
47 Highlight.defaultValue("selector", function () highlight.selector(this.class));
48
49 Highlight.defaultValue("sites", function ()
50     this.base ? this.base.sites
51               : ["resource://dactyl*", "dactyl:*", "file://*"].concat(
52                     highlight.styleableChrome));
53
54 Highlight.defaultValue("style", function ()
55     styles.system.add("highlight:" + this.class, this.sites, this.css, this.agent, true));
56
57 Highlight.defaultValue("defaultExtends", function () []);
58 Highlight.defaultValue("defaultValue", function () "");
59 Highlight.defaultValue("extends", function () this.defaultExtends);
60 Highlight.defaultValue("value", function () this.defaultValue);
61
62 update(Highlight.prototype, {
63     get base() this.baseClass != this.class && highlight.highlight[this.baseClass] || null,
64
65     get bases() array.compact(this.extends.map(function (name) highlight.get(name))),
66
67     get inheritedCSS() {
68         if (this.gettingCSS)
69             return "";
70         try {
71             this.gettingCSS = true;
72             return this.bases.map(function (b) b.cssText.replace(/;?\s*$/, "; ")).join("");
73         }
74         finally {
75             this.gettingCSS = false;
76         }
77     },
78
79     get css() this.selector + "{" + this.cssText + "}",
80
81     get cssText() this.inheritedCSS + this.value,
82
83     toString: function () "Highlight(" + this.class + ")\n\t" +
84         [k + ": " + String(v).quote() for ([k, v] in this)] .join("\n\t")
85 });
86
87 /**
88  * A class to manage highlighting rules.
89  *
90  * @author Kris Maglione <maglione.k@gmail.com>
91  */
92 var Highlights = Module("Highlight", {
93     init: function () {
94         this.highlight = {};
95         this.loaded = {};
96     },
97
98     keys: function keys() Object.keys(this.highlight).sort(),
99
100     __iterator__: function () values(this.highlight).sort(function (a, b) String.localeCompare(a.class, b.class))
101                                                     .iterValues(),
102
103     _create: function (agent, args) {
104         let obj = Highlight.apply(Highlight, args);
105
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);
111
112         obj.set("defaultValue", Styles.append("", obj.get("defaultValue")));
113
114         let old = this.highlight[obj.class];
115         this.highlight[obj.class] = obj;
116         // This *must* come before any other property changes.
117         if (old) {
118             obj.selector = old.selector;
119             obj.style = old.style;
120         }
121
122         if (/^[[>+: ]/.test(args[1]))
123             obj.selector = this.selector(obj.class) + args[1];
124         else if (args[1])
125             obj.selector = this.selector(args[1]);
126
127         if (old && old.value != old.defaultValue)
128             obj.value = old.value;
129
130         if (!old && obj.base && obj.base.style.enabled)
131             obj.style.enabled = true;
132         else
133             this.loaded.__defineSetter__(obj.class, function () {
134                 delete this[obj.class];
135                 this[obj.class] = true;
136
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;
142             });
143         return obj;
144     },
145
146     get: function (k) this.highlight[k],
147
148     set: function (key, newStyle, force, append, extend) {
149         let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/);
150
151         let highlight = this.highlight[key] || this._create(false, [key]);
152
153         let bases = extend || highlight.extend;
154         if (append) {
155             newStyle = Styles.append(highlight.value || "", newStyle);
156             bases = highlight.extends.concat(bases);
157         }
158
159         if (/^\s*$/.test(newStyle))
160             newStyle = null;
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];
166                 return null;
167             }
168             newStyle = highlight.defaultValue;
169             bases = highlight.defaultExtends;
170         }
171
172         highlight.set("value", newStyle || "");
173         highlight.extends = array.uniq(bases, true);
174         if (force)
175             highlight.style.enabled = true;
176         this.highlight[highlight.class] = highlight;
177         return highlight;
178     },
179
180     /**
181      * Clears all highlighting rules. Rules with default values are
182      * reset.
183      */
184     clear: function () {
185         for (let [k, v] in Iterator(this.highlight))
186             this.set(k, null, true);
187     },
188
189     /**
190      * Highlights a node with the given group, and ensures that said
191      * group is loaded.
192      *
193      * @param {Node} node
194      * @param {string} group
195      */
196     highlightNode: function (node, group, applyBindings) {
197         node.setAttributeNS(NS.uri, "highlight", group);
198
199         let groups = group.split(" ");
200         for each (let group in groups)
201             this.loaded[group] = true;
202
203         if (applyBindings)
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);
209             }
210     },
211
212     /**
213      * Gets a CSS selector given a highlight group.
214      *
215      * @param {string} class
216      */
217     selector: function (class_)
218         let (self = this)
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 + "]")),
223
224     groupRegexp: util.regexp(<![CDATA[
225         ^
226         (\s* (?:\S|\s\S)+ \s+)
227         \{ ([^}]*) \}
228         \s*
229         $
230     ]]>, "gmx"),
231     sheetRegexp: util.regexp(<![CDATA[
232         ^\s*
233         !? \*?
234              (?P<group>    (?:[^;\s]|\s[^;\s])+ )
235         (?:; (?P<selector> (?:[^;\s]|\s[^;\s])+ )? )?
236         (?:; (?P<sites>    (?:[^;\s]|\s[^;\s])+ )? )?
237         (?:; (?P<extends>  (?:[^;\s]|\s[^;\s])+ )? )?
238         \s*  (?P<css>      .*)
239         $
240     ]]>, "x"),
241
242     /**
243      * Bulk loads new CSS rules, in the format of,
244      *
245      *   Rules     ::= Rule | Rule "\n" Rule
246      *   Rule      ::= Bang? Star? MatchSpec Space Space+ Css
247      *               | Comment
248      *   Comment   ::= Space* "//" *
249      *   Bang      ::= "!"
250      *   Star      ::= "*"
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
257      *
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.
261      *
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.
265      *
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.
269      *
270      * If Star is provided, the style is applied as an agent sheet.
271      *
272      * The new styles are lazily activated unless Bang or *eager* is
273      * provided. See {@link Util#xmlToDom}.
274      *
275      * @param {string} css The rules to load. See {@link Highlights#css}.
276      * @param {boolean} eager When true, load all provided rules immediately.
277      */
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) {
282
283             let bang = eager || /^\s*!/.test(highlight);
284             let star = /^\s*!?\*/.test(highlight);
285             highlight = this._create(star, this.sheetRegexp.exec(highlight).slice(1));
286             if (bang)
287                 highlight.style.enabled = true;
288        }, this);
289        for (let h in this)
290            h.style.css = h.css;
291     }
292 }, {
293 }, {
294     commands: function (dactyl, modules) {
295         const { autocommands, commands, completion, CommandOption, config, io } = modules;
296
297         let lastScheme;
298         commands.add(["colo[rscheme]"],
299             "Load a color scheme",
300             function (args) {
301                 let scheme = args[0];
302                 if (lastScheme)
303                     lastScheme.unload();
304
305                 if (scheme == "default")
306                     highlight.clear();
307                 else {
308                     lastScheme = modules.io.sourceFromRuntimePath(["colors/" + scheme + "." + config.fileExtension]);
309                     dactyl.assert(lastScheme, _("command.colorscheme.notFound", scheme));
310                 }
311                 autocommands.trigger("ColorScheme", { name: scheme });
312             },
313             {
314                 argCount: "1",
315                 completer: function (context) completion.colorScheme(context)
316             });
317
318         commands.add(["hi[ghlight]"],
319             "Set the style of certain display elements",
320             function (args) {
321                 let style = <![CDATA[
322                     ;
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;
329                 ]]>;
330                 let clear = args[0] == "clear";
331                 if (clear)
332                     args.shift();
333
334                 let [key, css] = args;
335                 let modify = css || clear || args["-append"] || args["-link"];
336
337                 if (!modify && /&$/.test(key))
338                     [clear, modify, key] = [true, true, key.replace(/&$/, "")];
339
340                 dactyl.assert(!(clear && css), _("error.trailing"));
341
342                 if (!modify)
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"],
347                             ([h.class,
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)]
351                                 for (h in highlight)
352                                 if (!key || h.class.indexOf(key) > -1))));
353                 else if (!key && clear)
354                     highlight.clear();
355                 else if (key)
356                     highlight.set(key, css, clear, "-append" in args, args["-link"]);
357                 else
358                     util.assert(false, _("error.invalidArgument"));
359             },
360             {
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;
366
367                     if (args.completeArg == 0)
368                         completion.highlightGroup(context);
369                     else if (args.completeArg == 1) {
370                         let hl = highlight.get(args[0]);
371                         if (hl)
372                             context.completions = [[hl.value, "Current Value"], [hl.defaultValue || "", "Default Value"]];
373                         context.fork("css", 0, completion, "css");
374                     }
375                 },
376                 hereDoc: true,
377                 literal: 1,
378                 options: [
379                     { names: ["-append", "-a"], description: "Append new CSS to the existing value" },
380                     {
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]);
386                             if (group)
387                                 context.fork("extra", 0, this, function (context) [
388                                      [String(group.extends), "Current Value"],
389                                      [String(group.defaultExtends) || "", "Default Value"]
390                                 ]);
391                             context.fork("groups", 0, completion, "highlightGroup");
392                         }
393                     }
394                 ],
395                 serialize: function () [
396                     {
397                         command: this.name,
398                         arguments: [v.class],
399                         literalArg: v.value
400                     }
401                     for (v in Iterator(highlight))
402                     if (v.value != v.defaultValue)
403                 ]
404             });
405     },
406     completion: function (dactyl, modules) {
407         const { completion, config, io } = modules;
408         completion.colorScheme = function colorScheme(context) {
409             let extRe = RegExp("\\." + config.fileExtension + "$");
410
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))));
417
418         };
419
420         completion.highlightGroup = function highlightGroup(context) {
421             context.title = ["Highlight Group", "Value"];
422             context.completions = [[v.class, v.value] for (v in highlight)];
423         };
424     },
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) ]);
430     }
431 });
432
433 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
434
435 endModule();
436
437 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: