]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/highlight.jsm
fca93478b3f7ce514cabba374e700eaf5bde62a8
[dactyl.git] / common / modules / highlight.jsm
1 // Copyright (c) 2008-2012 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 defineModule("highlight", {
8     exports: ["Highlight", "Highlights", "highlight"],
9     require: ["services", "util"]
10 });
11
12 lazyRequire("styles", ["Styles", "styles"]);
13 lazyRequire("template", ["template"]);
14
15 var Highlight = Struct("class", "selector", "sites",
16                        "defaultExtends", "defaultValue",
17                        "value", "extends", "agent",
18                        "base", "baseClass", "style");
19 Highlight.liveProperty = function (name, prop) {
20     this.prototype.__defineGetter__(name, function () this.get(name));
21     this.prototype.__defineSetter__(name, function (val) {
22         if (isObject(val) && name !== "style") {
23             if (isArray(val))
24                 val = Array.slice(val);
25             else
26                 val = update({}, val);
27             Object.freeze(val);
28         }
29         this.set(name, val);
30
31         if (name === "value" || name === "extends")
32             for (let h in highlight)
33                 if (h.extends.indexOf(this.class) >= 0)
34                     h.style.css = h.css;
35
36         this.style[prop || name] = this[prop || name];
37         if (this.onChange)
38             this.onChange();
39     });
40 }
41 Highlight.liveProperty("agent");
42 Highlight.liveProperty("extends", "css");
43 Highlight.liveProperty("value", "css");
44 Highlight.liveProperty("selector", "css");
45 Highlight.liveProperty("sites");
46 Highlight.liveProperty("style", "css");
47
48 Highlight.defaultValue("baseClass", function () /^(\w*)/.exec(this.class)[0]);
49
50 Highlight.defaultValue("selector", function () highlight.selector(this.class));
51
52 Highlight.defaultValue("sites", function ()
53     this.base ? this.base.sites
54               : ["resource://dactyl*", "dactyl:*", "file://*"].concat(
55                     highlight.styleableChrome));
56
57 Highlight.defaultValue("style", function ()
58     styles.system.add("highlight:" + this.class, this.sites, this.css, this.agent, true));
59
60 Highlight.defaultValue("defaultExtends", function () []);
61 Highlight.defaultValue("defaultValue", function () "");
62 Highlight.defaultValue("extends", function () this.defaultExtends);
63 Highlight.defaultValue("value", function () this.defaultValue);
64
65 update(Highlight.prototype, {
66     get base() this.baseClass != this.class && highlight.highlight[this.baseClass] || null,
67
68     get bases() array.compact(this.extends.map(function (name) highlight.get(name))),
69
70     get inheritedCSS() {
71         if (this.gettingCSS)
72             return "";
73         try {
74             this.gettingCSS = true;
75             return this.bases.map(function (b) b.cssText.replace(/;?\s*$/, "; ")).join("");
76         }
77         finally {
78             this.gettingCSS = false;
79         }
80     },
81
82     get css() this.selector + "{" + this.cssText + "}",
83
84     get cssText() this.inheritedCSS + this.value,
85
86     toString: function () "Highlight(" + this.class + ")\n\t" +
87         [k + ": " + String(v).quote() for ([k, v] in this)] .join("\n\t")
88 });
89
90 /**
91  * A class to manage highlighting rules.
92  *
93  * @author Kris Maglione <maglione.k@gmail.com>
94  */
95 var Highlights = Module("Highlight", {
96     init: function () {
97         this.highlight = {};
98         this.loaded = {};
99     },
100
101     keys: function keys() Object.keys(this.highlight).sort(),
102
103     __iterator__: function () values(this.highlight).sort(function (a, b) String.localeCompare(a.class, b.class))
104                                                     .iterValues(),
105
106     _create: function _create(agent, args) {
107         let obj = Highlight.apply(Highlight, args);
108
109         if (!isArray(obj.sites))
110             obj.set("sites", obj.sites.split(","));
111         if (!isArray(obj.defaultExtends))
112             obj.set("defaultExtends", obj.defaultExtends.split(","));
113         obj.set("agent", agent);
114
115         obj.set("defaultValue", Styles.append("", obj.get("defaultValue")));
116
117         let old = this.highlight[obj.class];
118         this.highlight[obj.class] = obj;
119         // This *must* come before any other property changes.
120         if (old) {
121             obj.selector = old.selector;
122             obj.style = old.style;
123         }
124
125         if (/^[[>+: ]/.test(args[1]))
126             obj.selector = this.selector(obj.class) + args[1];
127         else if (args[1])
128             obj.selector = this.selector(args[1]);
129
130         if (old && old.value != old.defaultValue)
131             obj.value = old.value;
132
133         if (!old && obj.base && obj.base.style.enabled)
134             obj.style.enabled = true;
135         else
136             this.loaded.__defineSetter__(obj.class, function () {
137                 delete this[obj.class];
138                 this[obj.class] = true;
139
140                 if (obj.class === obj.baseClass)
141                     for (let h in highlight)
142                         if (h.baseClass === obj.class)
143                             this[h.class] = true;
144                 obj.style.enabled = true;
145             });
146         return obj;
147     },
148
149     get: function get(k) this.highlight[k],
150
151     set: function set(key, newStyle, force, append, extend) {
152         let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/);
153
154         let highlight = this.highlight[key] || this._create(false, [key]);
155
156         let bases = extend || highlight.extends;
157         if (append) {
158             newStyle = Styles.append(highlight.value || "", newStyle);
159             bases = highlight.extends.concat(bases);
160         }
161
162         if (/^\s*$/.test(newStyle))
163             newStyle = null;
164         if (newStyle == null && extend == null) {
165             if (highlight.defaultValue == null && highight.defaultExtends.length == 0) {
166                 highlight.style.enabled = false;
167                 delete this.loaded[highlight.class];
168                 delete this.highlight[highlight.class];
169                 return null;
170             }
171             newStyle = highlight.defaultValue;
172             bases = highlight.defaultExtends;
173         }
174
175         highlight.set("value", newStyle || "");
176         highlight.extends = array.uniq(bases, true);
177         if (force)
178             highlight.style.enabled = true;
179         this.highlight[highlight.class] = highlight;
180         return highlight;
181     },
182
183     /**
184      * Clears all highlighting rules. Rules with default values are
185      * reset.
186      */
187     clear: function clear() {
188         for (let [k, v] in Iterator(this.highlight))
189             this.set(k, null, true);
190     },
191
192     /**
193      * Highlights a node with the given group, and ensures that said
194      * group is loaded.
195      *
196      * @param {Node} node
197      * @param {string} group
198      */
199     highlightNode: function highlightNode(node, group, applyBindings) {
200         node.setAttributeNS(NS, "highlight", group);
201
202         let groups = group.split(" ");
203         for each (let group in groups)
204             this.loaded[group] = true;
205
206         if (applyBindings)
207             for each (let group in groups) {
208                 if (applyBindings.bindings && group in applyBindings.bindings)
209                     applyBindings.bindings[group](node, applyBindings);
210                 else if (group in template.bindings)
211                     template.bindings[group](node, applyBindings);
212             }
213     },
214
215     /**
216      * Gets a CSS selector given a highlight group.
217      *
218      * @param {string} class
219      */
220     selector: function selector(class_)
221         class_.replace(/(^|[>\s])([A-Z][\w-]+)\b/g,
222             (m, n1, hl) => {
223                 if (this.highlight[hl] && this.highlight[hl].class != class_)
224                     return n1 + this.highlight[hl].selector;
225                 return n1 + "[dactyl|highlight~=" + hl + "]";
226             }),
227
228     groupRegexp: util.regexp(literal(/*
229         ^
230         (\s* (?:\S|\s\S)+ \s+)
231         \{ ([^}]*) \}
232         \s*
233         $
234     */), "gmx"),
235     sheetRegexp: util.regexp(literal(/*
236         ^\s*
237         !? \*?
238              (?P<group>    (?:[^;\s]|\s[^;\s])+ )
239         (?:; (?P<selector> (?:[^;\s]|\s[^;\s])+ )? )?
240         (?:; (?P<sites>    (?:[^;\s]|\s[^;\s])+ )? )?
241         (?:; (?P<extends>  (?:[^;\s]|\s[^;\s])+ )? )?
242         \s*  (?P<css>      .*)
243         $
244     */), "x"),
245     // </css>
246
247     /**
248      * Bulk loads new CSS rules, in the format of,
249      *
250      *   Rules     ::= Rule | Rule "\n" Rule
251      *   Rule      ::= Bang? Star? MatchSpec Space Space+ Css
252      *               | Comment
253      *   Comment   ::= Space* "//" *
254      *   Bang      ::= "!"
255      *   Star      ::= "*"
256      *   MatchSpec ::= Class
257      *               | Class ";" Selector
258      *               | Class ";" Selector ";" Sites
259      *               | Class ";" Selector ";" Sites ";" Extends
260      *   CSS       ::= CSSLine | "{" CSSLines "}"
261      *   CSSLines  ::= CSSLine | CSSLine "\n" CSSLines
262      *
263      * Where Class is the name of the sheet, Selector is the CSS
264      * selector for the style, Sites is the comma-separated list of site
265      * filters to apply the style to.
266      *
267      * If Selector is not provided, it defaults to [dactyl|highlight~={Class}].
268      * If it is provided and begins with any of "+", ">" or " ", it is
269      * appended to the default.
270      *
271      * If Sites is not provided, it defaults to the chrome documents of
272      * the main application window, dactyl help files, and any other
273      * dactyl-specific documents.
274      *
275      * If Star is provided, the style is applied as an agent sheet.
276      *
277      * The new styles are lazily activated unless Bang or *eager* is
278      * provided. See {@link Util#xmlToDom}.
279      *
280      * @param {string} css The rules to load. See {@link Highlights#css}.
281      * @param {boolean} eager When true, load all provided rules immediately.
282      */
283     loadCSS: function loadCSS(css, eager) {
284         String.replace(css, /\\\n/g, "")
285               .replace(this.groupRegexp, function (m, m1, m2) m1 + " " + m2.replace(/\n\s*/g, " "))
286               .split("\n").filter(function (s) /\S/.test(s) && !/^\s*\/\//.test(s))
287               .forEach(function (highlight) {
288
289             let bang = eager || /^\s*!/.test(highlight);
290             let star = /^\s*!?\*/.test(highlight);
291             highlight = this._create(star, this.sheetRegexp.exec(highlight).slice(1));
292             if (bang)
293                 highlight.style.enabled = true;
294        }, this);
295        for (let h in this)
296            h.style.css = h.css;
297     }
298 }, {
299 }, {
300     commands: function initCommands(dactyl, modules) {
301         const { autocommands, commands, completion, CommandOption, config, io } = modules;
302
303         let lastScheme;
304         commands.add(["colo[rscheme]"],
305             "Load a color scheme",
306             function (args) {
307                 let scheme = args[0];
308                 if (lastScheme)
309                     lastScheme.unload();
310
311                 if (scheme == "default")
312                     highlight.clear();
313                 else {
314                     lastScheme = modules.io.sourceFromRuntimePath(["colors/" + scheme + "." + config.fileExtension]);
315                     dactyl.assert(lastScheme, _("command.colorscheme.notFound", scheme));
316                 }
317                 autocommands.trigger("ColorScheme", { name: scheme });
318             },
319             {
320                 argCount: "1",
321                 completer: function (context) completion.colorScheme(context)
322             });
323
324         commands.add(["hi[ghlight]"],
325             "Set the style of certain display elements",
326             function (args) {
327                 let style = literal(/*
328                     ;
329                     display: inline-block !important;
330                     position: static !important;
331                     margin: 0px !important; padding: 0px !important;
332                     width: 3em !important; min-width: 3em !important; max-width: 3em !important;
333                     height: 1em !important; min-height: 1em !important; max-height: 1em !important;
334                     overflow: hidden !important;
335                 */);
336                 let clear = args[0] == "clear";
337                 if (clear)
338                     args.shift();
339
340                 let [key, css] = args;
341                 let modify = css || clear || args["-append"] || args["-link"];
342
343                 if (!modify && /&$/.test(key))
344                     [clear, modify, key] = [true, true, key.replace(/&$/, "")];
345
346                 dactyl.assert(!(clear && css), _("error.trailingCharacters"));
347
348                 if (!modify)
349                     modules.commandline.commandOutput(
350                         template.tabular(["Key", "Sample", "Link", "CSS"],
351                             ["padding: 0 1em 0 0; vertical-align: top; max-width: 16em; overflow: hidden;",
352                              "text-align: center"],
353                             ([h.class,
354                               ["span", { style: "text-align: center; line-height: 1em;" + h.value + style }, "XXX"],
355                               template.map(h.extends, function (s) template.highlight(s), ","),
356                               template.highlightRegexp(h.value, /\b[-\w]+(?=:)|\/\*.*?\*\//g,
357                                                        function (match) ["span", { highlight: match[0] == "/" ? "Comment" : "Key" }, match])
358                              ]
359                              for (h in highlight)
360                              if (!key || h.class.indexOf(key) > -1))));
361                 else if (!key && clear)
362                     highlight.clear();
363                 else if (key)
364                     highlight.set(key, css, clear, "-append" in args, args["-link"]);
365                 else
366                     util.assert(false, _("error.invalidArgument"));
367             },
368             {
369                 // TODO: add this as a standard highlight completion function?
370                 completer: function (context, args) {
371                     // Complete a highlight group on :hi clear ...
372                     if (args.completeArg > 0 && args[0] == "clear")
373                         args.completeArg = args.completeArg > 1 ? -1 : 0;
374
375                     if (args.completeArg == 0)
376                         completion.highlightGroup(context);
377                     else if (args.completeArg == 1) {
378                         let hl = highlight.get(args[0]);
379                         if (hl)
380                             context.completions = [
381                                 [hl.value, _("option.currentValue")],
382                                 [hl.defaultValue || "", _("option.defaultValue")]
383                             ];
384                         context.fork("css", 0, completion, "css");
385                     }
386                 },
387                 hereDoc: true,
388                 literal: 1,
389                 options: [
390                     { names: ["-append", "-a"], description: "Append new CSS to the existing value" },
391                     {
392                         names: ["-link", "-l"],
393                         description: "Link this group to another",
394                         type: CommandOption.LIST,
395                         completer: function (context, args) {
396                             let group = args[0] && highlight.get(args[0]);
397                             if (group)
398                                 context.fork("extra", 0, this, function (context) [
399                                      [String(group.extends), _("option.currentValue")],
400                                      [String(group.defaultExtends) || "", _("option.defaultValue")]
401                                 ]);
402                             context.fork("groups", 0, completion, "highlightGroup");
403                         }
404                     }
405                 ],
406                 serialize: function () [
407                     {
408                         command: this.name,
409                         arguments: [v.class],
410                         literalArg: v.value,
411                         options: {
412                             "-link": v.extends.length ? v.extends : undefined
413                         }
414                     }
415                     for (v in Iterator(highlight))
416                     if (v.value != v.defaultValue)
417                 ]
418             });
419     },
420     completion: function initCompletion(dactyl, modules) {
421         const { completion, config, io } = modules;
422
423         completion.colorScheme = function colorScheme(context) {
424             let extRe = RegExp("\\." + config.fileExtension + "$");
425
426             context.title = ["Color Scheme", "Runtime Path"];
427             context.keys = { text: function (f) f.leafName.replace(extRe, ""), description: ".parent.path" };
428             context.completions =
429                 array.flatten(
430                         io.getRuntimeDirectories("colors").map(
431                             function (dir) dir.readDirectory().filter(
432                                 function (file) extRe.test(file.leafName))))
433                      .concat([
434                         { leafName: "default", parent: { path: /*L*/"Revert to builtin colorscheme" } }
435                      ]);
436
437         };
438
439         completion.highlightGroup = function highlightGroup(context) {
440             context.title = ["Highlight Group", "Value"];
441             context.completions = [[v.class, v.value] for (v in highlight)];
442         };
443     },
444     javascript: function initJavascript(dactyl, modules, window) {
445         modules.JavaScript.setCompleter(["get", "set"].map(function (m) highlight[m]),
446             [ function (context, obj, args) Iterator(highlight.highlight) ]);
447         modules.JavaScript.setCompleter(["highlightNode"].map(function (m) highlight[m]),
448             [ null, function (context, obj, args) Iterator(highlight.highlight) ]);
449     }
450 });
451
452 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
453
454 endModule();
455
456 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: