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