]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/highlight.jsm
finalize changelog for 7904
[dactyl.git] / common / modules / highlight.jsm
1 // Copyright (c) 2008-2014 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", () => []);
61 Highlight.defaultValue("defaultValue", () => "");
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(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(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((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].replace(/^,/, ""));
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                 Object.defineProperty(this, obj.class, {
138                     value: true,
139                     configurable: true,
140                     enumerable: true,
141                     writable: true
142                 });
143
144                 if (obj.class === obj.baseClass)
145                     for (let h in highlight)
146                         if (h.baseClass === obj.class)
147                             this[h.class] = true;
148                 obj.style.enabled = true;
149             });
150         return obj;
151     },
152
153     get: function get(k) this.highlight[k],
154
155     set: function set(key, newStyle, force, append, extend) {
156         let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/);
157
158         let highlight = this.highlight[key] || this._create(false, [key]);
159
160         let bases = extend || highlight.extends;
161         if (append) {
162             newStyle = Styles.append(highlight.value || "", newStyle);
163             bases = highlight.extends.concat(bases);
164         }
165
166         if (/^\s*$/.test(newStyle))
167             newStyle = null;
168         if (newStyle == null && extend == null) {
169             if (highlight.defaultValue == null && highight.defaultExtends.length == 0) {
170                 highlight.style.enabled = false;
171                 delete this.loaded[highlight.class];
172                 delete this.highlight[highlight.class];
173                 return null;
174             }
175             newStyle = highlight.defaultValue;
176             bases = highlight.defaultExtends;
177         }
178
179         highlight.set("value", newStyle || "");
180         highlight.extends = array.uniq(bases, true);
181         if (force)
182             highlight.style.enabled = true;
183         this.highlight[highlight.class] = highlight;
184         return highlight;
185     },
186
187     /**
188      * Clears all highlighting rules. Rules with default values are
189      * reset.
190      */
191     clear: function clear() {
192         for (let [k, v] in Iterator(this.highlight))
193             this.set(k, null, true);
194     },
195
196     /**
197      * Highlights a node with the given group, and ensures that said
198      * group is loaded.
199      *
200      * @param {Node} node
201      * @param {string} group
202      */
203     highlightNode: function highlightNode(node, group, applyBindings) {
204         node.setAttributeNS(NS, "highlight", group);
205
206         let groups = group.split(" ");
207         for (let group of groups)
208             this.loaded[group] = true;
209
210         if (applyBindings)
211             for (let group of groups) {
212                 if (applyBindings.bindings && group in applyBindings.bindings)
213                     applyBindings.bindings[group](node, applyBindings);
214                 else if (group in template.bindings)
215                     template.bindings[group](node, applyBindings);
216             }
217     },
218
219     /**
220      * Gets a CSS selector given a highlight group.
221      *
222      * @param {string} class
223      */
224     selector: function selector(class_)
225         class_.replace(/(^|[>\s])([A-Z][\w-]+)\b/g,
226             (m, n1, hl) => {
227                 if (this.highlight[hl] && this.highlight[hl].class != class_)
228                     return n1 + this.highlight[hl].selector;
229                 return n1 + "[dactyl|highlight~=" + hl + "]";
230             }),
231
232     groupRegexp: util.regexp(literal(/*
233         ^
234         (\s* (?:\S|\s\S)+ \s+)
235         \{ ([^}]*) \}
236         \s*
237         $
238     */), "gmx"),
239     sheetRegexp: util.regexp(literal(/*
240         ^\s*
241         !? \*?
242              (?P<group>    (?:[^;\s]|\s[^;\s])+ )
243         (?:; (?P<selector> (?:[^;\s]|\s[^;\s])+ )? )?
244         (?:; (?P<sites>    (?:[^;\s]|\s[^;\s])+ )? )?
245         (?:; (?P<extends>  (?:[^;\s]|\s[^;\s])+ )? )?
246         \s*  (?P<css>      .*)
247         $
248     */), "x"),
249     // </css>
250
251     /**
252      * Bulk loads new CSS rules, in the format of,
253      *
254      *   Rules     ::= Rule | Rule "\n" Rule
255      *   Rule      ::= Bang? Star? MatchSpec Space Space+ Css
256      *               | Comment
257      *   Comment   ::= Space* "//" *
258      *   Bang      ::= "!"
259      *   Star      ::= "*"
260      *   MatchSpec ::= Class
261      *               | Class ";" Selector
262      *               | Class ";" Selector ";" Sites
263      *               | Class ";" Selector ";" Sites ";" Extends
264      *   CSS       ::= CSSLine | "{" CSSLines "}"
265      *   CSSLines  ::= CSSLine | CSSLine "\n" CSSLines
266      *
267      * Where Class is the name of the sheet, Selector is the CSS
268      * selector for the style, Sites is the comma-separated list of site
269      * filters to apply the style to.
270      *
271      * If Selector is not provided, it defaults to [dactyl|highlight~={Class}].
272      * If it is provided and begins with any of "+", ">" or " ", it is
273      * appended to the default.
274      *
275      * If Sites is not provided, it defaults to the chrome documents of
276      * the main application window, dactyl help files, and any other
277      * dactyl-specific documents.
278      *
279      * If Star is provided, the style is applied as an agent sheet.
280      *
281      * The new styles are lazily activated unless Bang or *eager* is
282      * provided.
283      *
284      * @param {string} css The rules to load. See {@link Highlights#css}.
285      * @param {boolean} eager When true, load all provided rules immediately.
286      */
287     loadCSS: function loadCSS(css, eager) {
288         String.replace(css, /\\\n/g, "")
289               .replace(this.groupRegexp, (m, m1, m2) => (m1 + " " + m2.replace(/\n\s*/g, " ")))
290               .split("\n").filter(s => (/\S/.test(s) && !/^\s*\/\//.test(s)))
291               .forEach(function (highlight) {
292
293             let bang = eager || /^\s*!/.test(highlight);
294             let star = /^\s*!?\*/.test(highlight);
295             highlight = this._create(star, this.sheetRegexp.exec(highlight).slice(1));
296             if (bang)
297                 highlight.style.enabled = true;
298        }, this);
299        for (let h in this)
300            h.style.css = h.css;
301     }
302 }, {
303 }, {
304     commands: function initCommands(dactyl, modules) {
305         const { autocommands, commands, completion, CommandOption, config, io } = modules;
306
307         let lastScheme;
308         commands.add(["colo[rscheme]"],
309             "Load a color scheme",
310             function (args) {
311                 let scheme = args[0];
312                 if (lastScheme)
313                     lastScheme.unload();
314
315                 if (scheme == "default")
316                     highlight.clear();
317                 else {
318                     lastScheme = modules.io.sourceFromRuntimePath(["colors/" + scheme + "." + config.fileExtension]);
319                     dactyl.assert(lastScheme, _("command.colorscheme.notFound", scheme));
320                 }
321                 autocommands.trigger("ColorScheme", { name: scheme });
322             },
323             {
324                 argCount: "1",
325                 completer: function (context) completion.colorScheme(context)
326             });
327
328         commands.add(["hi[ghlight]"],
329             "Set the style of certain display elements",
330             function (args) {
331                 let style = literal(/*
332                     ;
333                     display: inline-block !important;
334                     position: static !important;
335                     margin: 0px !important; padding: 0px !important;
336                     width: 3em !important; min-width: 3em !important; max-width: 3em !important;
337                     height: 1em !important; min-height: 1em !important; max-height: 1em !important;
338                     overflow: hidden !important;
339                 */);
340                 let clear = args[0] == "clear";
341                 if (clear)
342                     args.shift();
343
344                 let [key, css] = args;
345                 let modify = css || clear || args["-append"] || args["-link"];
346
347                 if (!modify && /&$/.test(key))
348                     [clear, modify, key] = [true, true, key.replace(/&$/, "")];
349
350                 dactyl.assert(!(clear && css), _("error.trailingCharacters"));
351
352                 if (!modify)
353                     modules.commandline.commandOutput(
354                         template.tabular(["Key", "Sample", "Link", "CSS"],
355                             ["padding: 0 1em 0 0; vertical-align: top; max-width: 16em; overflow: hidden;",
356                              "text-align: center"],
357                             ([h.class,
358                               ["span", { style: "text-align: center; line-height: 1em;" + h.value + style }, "XXX"],
359                               template.map(h.extends, s => template.highlight(s), ","),
360                               template.highlightRegexp(h.value, /\b[-\w]+(?=:)|\/\*.*?\*\//g,
361                                                        match => ["span", { highlight: match[0] == "/" ? "Comment" : "Key" }, match])
362                              ]
363                              for (h in highlight)
364                              if (!key || h.class.indexOf(key) > -1))));
365                 else if (!key && clear)
366                     highlight.clear();
367                 else if (key)
368                     highlight.set(key, css, clear, "-append" in args, args["-link"]);
369                 else
370                     util.assert(false, _("error.invalidArgument"));
371             },
372             {
373                 // TODO: add this as a standard highlight completion function?
374                 completer: function (context, args) {
375                     // Complete a highlight group on :hi clear ...
376                     if (args.completeArg > 0 && args[0] == "clear")
377                         args.completeArg = args.completeArg > 1 ? -1 : 0;
378
379                     if (args.completeArg == 0)
380                         completion.highlightGroup(context);
381                     else if (args.completeArg == 1) {
382                         let hl = highlight.get(args[0]);
383                         if (hl)
384                             context.completions = [
385                                 [hl.value, _("option.currentValue")],
386                                 [hl.defaultValue || "", _("option.defaultValue")]
387                             ];
388                         context.fork("css", 0, completion, "css");
389                     }
390                 },
391                 hereDoc: true,
392                 literal: 1,
393                 options: [
394                     { names: ["-append", "-a"], description: "Append new CSS to the existing value" },
395                     {
396                         names: ["-link", "-l"],
397                         description: "Link this group to another",
398                         type: CommandOption.LIST,
399                         completer: function (context, args) {
400                             let group = args[0] && highlight.get(args[0]);
401                             if (group)
402                                 context.fork("extra", 0, this, context => [
403                                      [String(group.extends), _("option.currentValue")],
404                                      [String(group.defaultExtends) || "", _("option.defaultValue")]
405                                 ]);
406                             context.fork("groups", 0, completion, "highlightGroup");
407                         }
408                     }
409                 ],
410                 serialize: function () [
411                     {
412                         command: this.name,
413                         arguments: [v.class],
414                         literalArg: v.value,
415                         options: {
416                             "-link": v.extends.length ? v.extends : undefined
417                         }
418                     }
419                     for (v in Iterator(highlight))
420                     if (v.value != v.defaultValue)
421                 ]
422             });
423     },
424     completion: function initCompletion(dactyl, modules) {
425         const { completion, config, io } = modules;
426
427         completion.colorScheme = function colorScheme(context) {
428             let extRe = RegExp("\\." + config.fileExtension + "$");
429
430             context.title = ["Color Scheme", "Runtime Path"];
431             context.keys = { text: f => f.leafName.replace(extRe, ""),
432                              description: ".parent.path" };
433             context.completions =
434                 array.flatten(
435                         io.getRuntimeDirectories("colors").map(
436                             dir => dir.readDirectory()
437                                       .filter(file => extRe.test(file.leafName))))
438                      .concat([
439                         { leafName: "default", parent: { path: /*L*/"Revert to builtin colorscheme" } }
440                      ]);
441
442         };
443
444         completion.highlightGroup = function highlightGroup(context) {
445             context.title = ["Highlight Group", "Value"];
446             context.completions = [[v.class, v.value] for (v in highlight)];
447         };
448     },
449     javascript: function initJavascript(dactyl, modules, window) {
450         modules.JavaScript.setCompleter(["get", "set"].map(m => highlight[m]),
451             [ (context, obj, args) => Iterator(highlight.highlight) ]);
452         modules.JavaScript.setCompleter(["highlightNode"].map(m => highlight[m]),
453             [ null, (context, obj, args) => Iterator(highlight.highlight) ]);
454     }
455 });
456
457 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
458
459 endModule();
460
461 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: