]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/highlight.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[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         let (self = this)
222            class_.replace(/(^|[>\s])([A-Z][\w-]+)\b/g,
223             function (m, n1, hl) n1 +
224                 (self.highlight[hl] && self.highlight[hl].class != class_
225                     ? self.highlight[hl].selector : "[dactyl|highlight~=" + hl + "]")),
226
227     groupRegexp: util.regexp(literal(/*
228         ^
229         (\s* (?:\S|\s\S)+ \s+)
230         \{ ([^}]*) \}
231         \s*
232         $
233     */), "gmx"),
234     sheetRegexp: util.regexp(literal(/*
235         ^\s*
236         !? \*?
237              (?P<group>    (?:[^;\s]|\s[^;\s])+ )
238         (?:; (?P<selector> (?:[^;\s]|\s[^;\s])+ )? )?
239         (?:; (?P<sites>    (?:[^;\s]|\s[^;\s])+ )? )?
240         (?:; (?P<extends>  (?:[^;\s]|\s[^;\s])+ )? )?
241         \s*  (?P<css>      .*)
242         $
243     */), "x"),
244     // </css>
245
246     /**
247      * Bulk loads new CSS rules, in the format of,
248      *
249      *   Rules     ::= Rule | Rule "\n" Rule
250      *   Rule      ::= Bang? Star? MatchSpec Space Space+ Css
251      *               | Comment
252      *   Comment   ::= Space* "//" *
253      *   Bang      ::= "!"
254      *   Star      ::= "*"
255      *   MatchSpec ::= Class
256      *               | Class ";" Selector
257      *               | Class ";" Selector ";" Sites
258      *               | Class ";" Selector ";" Sites ";" Extends
259      *   CSS       ::= CSSLine | "{" CSSLines "}"
260      *   CSSLines  ::= CSSLine | CSSLine "\n" CSSLines
261      *
262      * Where Class is the name of the sheet, Selector is the CSS
263      * selector for the style, Sites is the comma-separated list of site
264      * filters to apply the style to.
265      *
266      * If Selector is not provided, it defaults to [dactyl|highlight~={Class}].
267      * If it is provided and begins with any of "+", ">" or " ", it is
268      * appended to the default.
269      *
270      * If Sites is not provided, it defaults to the chrome documents of
271      * the main application window, dactyl help files, and any other
272      * dactyl-specific documents.
273      *
274      * If Star is provided, the style is applied as an agent sheet.
275      *
276      * The new styles are lazily activated unless Bang or *eager* is
277      * provided. See {@link Util#xmlToDom}.
278      *
279      * @param {string} css The rules to load. See {@link Highlights#css}.
280      * @param {boolean} eager When true, load all provided rules immediately.
281      */
282     loadCSS: function loadCSS(css, eager) {
283         String.replace(css, /\\\n/g, "")
284               .replace(this.groupRegexp, function (m, m1, m2) m1 + " " + m2.replace(/\n\s*/g, " "))
285               .split("\n").filter(function (s) /\S/.test(s) && !/^\s*\/\//.test(s))
286               .forEach(function (highlight) {
287
288             let bang = eager || /^\s*!/.test(highlight);
289             let star = /^\s*!?\*/.test(highlight);
290             highlight = this._create(star, this.sheetRegexp.exec(highlight).slice(1));
291             if (bang)
292                 highlight.style.enabled = true;
293        }, this);
294        for (let h in this)
295            h.style.css = h.css;
296     }
297 }, {
298 }, {
299     commands: function initCommands(dactyl, modules) {
300         const { autocommands, commands, completion, CommandOption, config, io } = modules;
301
302         let lastScheme;
303         commands.add(["colo[rscheme]"],
304             "Load a color scheme",
305             function (args) {
306                 let scheme = args[0];
307                 if (lastScheme)
308                     lastScheme.unload();
309
310                 if (scheme == "default")
311                     highlight.clear();
312                 else {
313                     lastScheme = modules.io.sourceFromRuntimePath(["colors/" + scheme + "." + config.fileExtension]);
314                     dactyl.assert(lastScheme, _("command.colorscheme.notFound", scheme));
315                 }
316                 autocommands.trigger("ColorScheme", { name: scheme });
317             },
318             {
319                 argCount: "1",
320                 completer: function (context) completion.colorScheme(context)
321             });
322
323         commands.add(["hi[ghlight]"],
324             "Set the style of certain display elements",
325             function (args) {
326                 let style = literal(/*
327                     ;
328                     display: inline-block !important;
329                     position: static !important;
330                     margin: 0px !important; padding: 0px !important;
331                     width: 3em !important; min-width: 3em !important; max-width: 3em !important;
332                     height: 1em !important; min-height: 1em !important; max-height: 1em !important;
333                     overflow: hidden !important;
334                 */);
335                 let clear = args[0] == "clear";
336                 if (clear)
337                     args.shift();
338
339                 let [key, css] = args;
340                 let modify = css || clear || args["-append"] || args["-link"];
341
342                 if (!modify && /&$/.test(key))
343                     [clear, modify, key] = [true, true, key.replace(/&$/, "")];
344
345                 dactyl.assert(!(clear && css), _("error.trailingCharacters"));
346
347                 if (!modify)
348                     modules.commandline.commandOutput(
349                         template.tabular(["Key", "Sample", "Link", "CSS"],
350                             ["padding: 0 1em 0 0; vertical-align: top; max-width: 16em; overflow: hidden;",
351                              "text-align: center"],
352                             ([h.class,
353                               ["span", { style: "text-align: center; line-height: 1em;" + h.value + style }, "XXX"],
354                               template.map(h.extends, function (s) template.highlight(s), ","),
355                               template.highlightRegexp(h.value, /\b[-\w]+(?=:)|\/\*.*?\*\//g,
356                                                        function (match) ["span", { highlight: match[0] == "/" ? "Comment" : "Key" }, match])
357                              ]
358                              for (h in highlight)
359                              if (!key || h.class.indexOf(key) > -1))));
360                 else if (!key && clear)
361                     highlight.clear();
362                 else if (key)
363                     highlight.set(key, css, clear, "-append" in args, args["-link"]);
364                 else
365                     util.assert(false, _("error.invalidArgument"));
366             },
367             {
368                 // TODO: add this as a standard highlight completion function?
369                 completer: function (context, args) {
370                     // Complete a highlight group on :hi clear ...
371                     if (args.completeArg > 0 && args[0] == "clear")
372                         args.completeArg = args.completeArg > 1 ? -1 : 0;
373
374                     if (args.completeArg == 0)
375                         completion.highlightGroup(context);
376                     else if (args.completeArg == 1) {
377                         let hl = highlight.get(args[0]);
378                         if (hl)
379                             context.completions = [
380                                 [hl.value, _("option.currentValue")],
381                                 [hl.defaultValue || "", _("option.defaultValue")]
382                             ];
383                         context.fork("css", 0, completion, "css");
384                     }
385                 },
386                 hereDoc: true,
387                 literal: 1,
388                 options: [
389                     { names: ["-append", "-a"], description: "Append new CSS to the existing value" },
390                     {
391                         names: ["-link", "-l"],
392                         description: "Link this group to another",
393                         type: CommandOption.LIST,
394                         completer: function (context, args) {
395                             let group = args[0] && highlight.get(args[0]);
396                             if (group)
397                                 context.fork("extra", 0, this, function (context) [
398                                      [String(group.extends), _("option.currentValue")],
399                                      [String(group.defaultExtends) || "", _("option.defaultValue")]
400                                 ]);
401                             context.fork("groups", 0, completion, "highlightGroup");
402                         }
403                     }
404                 ],
405                 serialize: function () [
406                     {
407                         command: this.name,
408                         arguments: [v.class],
409                         literalArg: v.value,
410                         options: {
411                             "-link": v.extends.length ? v.extends : undefined
412                         }
413                     }
414                     for (v in Iterator(highlight))
415                     if (v.value != v.defaultValue)
416                 ]
417             });
418     },
419     completion: function initCompletion(dactyl, modules) {
420         const { completion, config, io } = modules;
421
422         completion.colorScheme = function colorScheme(context) {
423             let extRe = RegExp("\\." + config.fileExtension + "$");
424
425             context.title = ["Color Scheme", "Runtime Path"];
426             context.keys = { text: function (f) f.leafName.replace(extRe, ""), description: ".parent.path" };
427             context.completions =
428                 array.flatten(
429                         io.getRuntimeDirectories("colors").map(
430                             function (dir) dir.readDirectory().filter(
431                                 function (file) extRe.test(file.leafName))))
432                      .concat([
433                         { leafName: "default", parent: { path: /*L*/"Revert to builtin colorscheme" } }
434                      ]);
435
436         };
437
438         completion.highlightGroup = function highlightGroup(context) {
439             context.title = ["Highlight Group", "Value"];
440             context.completions = [[v.class, v.value] for (v in highlight)];
441         };
442     },
443     javascript: function initJavascript(dactyl, modules, window) {
444         modules.JavaScript.setCompleter(["get", "set"].map(function (m) highlight[m]),
445             [ function (context, obj, args) Iterator(highlight.highlight) ]);
446         modules.JavaScript.setCompleter(["highlightNode"].map(function (m) highlight[m]),
447             [ null, function (context, obj, args) Iterator(highlight.highlight) ]);
448     }
449 });
450
451 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
452
453 endModule();
454
455 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: