1 // Copyright (c) 2008-2014 Kris Maglione <maglione.k at Gmail>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
7 defineModule("highlight", {
8 exports: ["Highlight", "Highlights", "highlight"],
9 require: ["services", "util"]
12 lazyRequire("styles", ["Styles", "styles"]);
13 lazyRequire("template", ["template"]);
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") {
24 val = Array.slice(val);
26 val = update({}, val);
31 if (name === "value" || name === "extends")
32 for (let h in highlight)
33 if (h.extends.indexOf(this.class) >= 0)
36 this.style[prop || name] = this[prop || name];
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");
48 Highlight.defaultValue("baseClass", function () /^(\w*)/.exec(this.class)[0]);
50 Highlight.defaultValue("selector", function () highlight.selector(this.class));
52 Highlight.defaultValue("sites", function ()
53 this.base ? this.base.sites
54 : ["resource://dactyl*", "dactyl:*", "file://*"].concat(
55 highlight.styleableChrome));
57 Highlight.defaultValue("style", function ()
58 styles.system.add("highlight:" + this.class, this.sites, this.css, this.agent, true));
60 Highlight.defaultValue("defaultExtends", () => []);
61 Highlight.defaultValue("defaultValue", () => "");
62 Highlight.defaultValue("extends", function () this.defaultExtends);
63 Highlight.defaultValue("value", function () this.defaultValue);
65 update(Highlight.prototype, {
66 get base() this.baseClass != this.class && highlight.highlight[this.baseClass] || null,
68 get bases() array.compact(this.extends.map(name => highlight.get(name))),
74 this.gettingCSS = true;
75 return this.bases.map(b => b.cssText.replace(/;?\s*$/, "; ")).join("");
78 this.gettingCSS = false;
82 get css() this.selector + "{" + this.cssText + "}",
84 get cssText() this.inheritedCSS + this.value,
86 toString: function () "Highlight(" + this.class + ")\n\t" +
87 [k + ": " + String(v).quote() for ([k, v] in this)] .join("\n\t")
91 * A class to manage highlighting rules.
93 * @author Kris Maglione <maglione.k@gmail.com>
95 var Highlights = Module("Highlight", {
101 keys: function keys() Object.keys(this.highlight).sort(),
103 __iterator__: function () values(this.highlight).sort((a, b) => String.localeCompare(a.class, b.class))
106 _create: function _create(agent, args) {
107 let obj = Highlight.apply(Highlight, args);
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);
115 obj.set("defaultValue", Styles.append("", obj.get("defaultValue")));
117 let old = this.highlight[obj.class];
118 this.highlight[obj.class] = obj;
119 // This *must* come before any other property changes.
121 obj.selector = old.selector;
122 obj.style = old.style;
125 if (/^[[>+: ]/.test(args[1]))
126 obj.selector = this.selector(obj.class) + args[1];
128 obj.selector = this.selector(args[1].replace(/^,/, ""));
130 if (old && old.value != old.defaultValue)
131 obj.value = old.value;
133 if (!old && obj.base && obj.base.style.enabled)
134 obj.style.enabled = true;
136 this.loaded.__defineSetter__(obj.class, function () {
137 Object.defineProperty(this, obj.class, {
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;
153 get: function get(k) this.highlight[k],
155 set: function set(key, newStyle, force, append, extend) {
156 let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/);
158 let highlight = this.highlight[key] || this._create(false, [key]);
160 let bases = extend || highlight.extends;
162 newStyle = Styles.append(highlight.value || "", newStyle);
163 bases = highlight.extends.concat(bases);
166 if (/^\s*$/.test(newStyle))
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];
175 newStyle = highlight.defaultValue;
176 bases = highlight.defaultExtends;
179 highlight.set("value", newStyle || "");
180 highlight.extends = array.uniq(bases, true);
182 highlight.style.enabled = true;
183 this.highlight[highlight.class] = highlight;
188 * Clears all highlighting rules. Rules with default values are
191 clear: function clear() {
192 for (let [k, v] in Iterator(this.highlight))
193 this.set(k, null, true);
197 * Highlights a node with the given group, and ensures that said
201 * @param {string} group
203 highlightNode: function highlightNode(node, group, applyBindings) {
204 node.setAttributeNS(NS, "highlight", group);
206 let groups = group.split(" ");
207 for (let group of groups)
208 this.loaded[group] = true;
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);
220 * Gets a CSS selector given a highlight group.
222 * @param {string} class
224 selector: function selector(class_)
225 class_.replace(/(^|[>\s])([A-Z][\w-]+)\b/g,
227 if (this.highlight[hl] && this.highlight[hl].class != class_)
228 return n1 + this.highlight[hl].selector;
229 return n1 + "[dactyl|highlight~=" + hl + "]";
232 groupRegexp: util.regexp(literal(/*
234 (\s* (?:\S|\s\S)+ \s+)
239 sheetRegexp: util.regexp(literal(/*
242 (?P<group> (?:[^;\s]|\s[^;\s])+ )
243 (?:; (?P<selector> (?:[^;\s]|\s[^;\s])+ )? )?
244 (?:; (?P<sites> (?:[^;\s]|\s[^;\s])+ )? )?
245 (?:; (?P<extends> (?:[^;\s]|\s[^;\s])+ )? )?
252 * Bulk loads new CSS rules, in the format of,
254 * Rules ::= Rule | Rule "\n" Rule
255 * Rule ::= Bang? Star? MatchSpec Space Space+ Css
257 * Comment ::= Space* "//" *
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
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.
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.
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.
279 * If Star is provided, the style is applied as an agent sheet.
281 * The new styles are lazily activated unless Bang or *eager* is
284 * @param {string} css The rules to load. See {@link Highlights#css}.
285 * @param {boolean} eager When true, load all provided rules immediately.
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) {
293 let bang = eager || /^\s*!/.test(highlight);
294 let star = /^\s*!?\*/.test(highlight);
295 highlight = this._create(star, this.sheetRegexp.exec(highlight).slice(1));
297 highlight.style.enabled = true;
304 commands: function initCommands(dactyl, modules) {
305 const { autocommands, commands, completion, CommandOption, config, io } = modules;
308 commands.add(["colo[rscheme]"],
309 "Load a color scheme",
311 let scheme = args[0];
315 if (scheme == "default")
318 lastScheme = modules.io.sourceFromRuntimePath(["colors/" + scheme + "." + config.fileExtension]);
319 dactyl.assert(lastScheme, _("command.colorscheme.notFound", scheme));
321 autocommands.trigger("ColorScheme", { name: scheme });
325 completer: function (context) completion.colorScheme(context)
328 commands.add(["hi[ghlight]"],
329 "Set the style of certain display elements",
331 let style = literal(/*
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;
340 let clear = args[0] == "clear";
344 let [key, css] = args;
345 let modify = css || clear || args["-append"] || args["-link"];
347 if (!modify && /&$/.test(key))
348 [clear, modify, key] = [true, true, key.replace(/&$/, "")];
350 dactyl.assert(!(clear && css), _("error.trailingCharacters"));
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"],
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])
364 if (!key || h.class.indexOf(key) > -1))));
365 else if (!key && clear)
368 highlight.set(key, css, clear, "-append" in args, args["-link"]);
370 util.assert(false, _("error.invalidArgument"));
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;
379 if (args.completeArg == 0)
380 completion.highlightGroup(context);
381 else if (args.completeArg == 1) {
382 let hl = highlight.get(args[0]);
384 context.completions = [
385 [hl.value, _("option.currentValue")],
386 [hl.defaultValue || "", _("option.defaultValue")]
388 context.fork("css", 0, completion, "css");
394 { names: ["-append", "-a"], description: "Append new CSS to the existing value" },
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]);
402 context.fork("extra", 0, this, context => [
403 [String(group.extends), _("option.currentValue")],
404 [String(group.defaultExtends) || "", _("option.defaultValue")]
406 context.fork("groups", 0, completion, "highlightGroup");
410 serialize: function () [
413 arguments: [v.class],
416 "-link": v.extends.length ? v.extends : undefined
419 for (v in Iterator(highlight))
420 if (v.value != v.defaultValue)
424 completion: function initCompletion(dactyl, modules) {
425 const { completion, config, io } = modules;
427 completion.colorScheme = function colorScheme(context) {
428 let extRe = RegExp("\\." + config.fileExtension + "$");
430 context.title = ["Color Scheme", "Runtime Path"];
431 context.keys = { text: f => f.leafName.replace(extRe, ""),
432 description: ".parent.path" };
433 context.completions =
435 io.getRuntimeDirectories("colors").map(
436 dir => dir.readDirectory()
437 .filter(file => extRe.test(file.leafName))))
439 { leafName: "default", parent: { path: /*L*/"Revert to builtin colorscheme" } }
444 completion.highlightGroup = function highlightGroup(context) {
445 context.title = ["Highlight Group", "Value"];
446 context.completions = [[v.class, v.value] for (v in highlight)];
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) ]);
457 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
461 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: