// Copyright (c) 2008-2015 Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";

defineModule("styles", {
    exports: ["Style", "Styles", "styles"],
    require: ["services", "util"]
});

lazyRequire("contexts", ["Contexts"]);
lazyRequire("template", ["template"]);

var namespace = "@namespace html " + JSON.stringify(XHTML) + ";\n" +
                "@namespace xul " + JSON.stringify(XUL) + ";\n" +
                "@namespace dactyl " + JSON.stringify(NS) + ";\n";

var Sheet = Struct("name", "id", "sites", "css", "hive", "agent");
Sheet.liveProperty = function (name) {
    let i = this.prototype.members[name];
    this.prototype.__defineGetter__(name, function () { return this[i]; });
    this.prototype.__defineSetter__(name, function (val) {
        if (isArray(val))
            val = Array.slice(val);
        if (isArray(val))
            Object.freeze(val);
        this[i] = val;
        this.enabled = this.enabled;
    });
};
Sheet.liveProperty("agent");
Sheet.liveProperty("css");
Sheet.liveProperty("sites");
update(Sheet.prototype, {
    formatSites: function (uris) {
          return template.map(this.sites,
                              filter => ["span", { highlight: uris.some(Styles.matchFilter(filter)) ? "Filter" : "" }, filter],
                              ",");
    },

    remove: function () { this.hive.remove(this); },

    get uri() {
        return "dactyl://style/" + this.id + "/" +
            this.hive.name + "/" + (this.name || "");
    },

    get enabled() { return this._enabled; },
    set enabled(on) {
        if (on != this._enabled || this.fullCSS != this._fullCSS) {
            if (on)
                this.enabled = false;
            else if (!this._fullCSS)
                return;

            let meth = on ? "registerSheet" : "unregisterSheet";
            styles[meth](this.uri, on ? this.agent : this._agent);

            this._agent = this.agent;
            this._enabled = Boolean(on);
            this._fullCSS = this.fullCSS;
        }
    },

    match: function (uri) {
        if (isString(uri))
            uri = util.newURI(uri);
        return this.sites.some(site => Styles.matchFilter(site, uri));
    },

    get fullCSS() {
        let filter = this.sites;
        let css = this.css;

        let preamble = "/* " + this.uri + (this.agent ? " (agent)" : "") + " */\n\n" + namespace + "\n";
        if (filter[0] == "*")
            return preamble + css;

        let selectors = filter.map(part =>
                                    !/^(?:[a-z-]+[:*]|[a-z-.]+$)/i.test(part) ? "regexp(" + Styles.quote(".*(?:" + part + ").*") + ")" :
                                       (/[*]$/.test(part)   ? "url-prefix" :
                                        /[\/:]/.test(part)  ? "url"
                                                            : "domain")
                                       + '(' + Styles.quote(part.replace(/\*$/, "")) + ')')
                              .join(",\n               ");

        return preamble + "@-moz-document " + selectors + " {\n\n" + css + "\n\n}\n";
    }
});

var Hive = Class("Hive", {
    init: function (name, persist) {
        this.name = name;
        this.sheets = [];
        this.names = {};
        this.refs = [];
        this.persist = persist;
    },

    get modifiable() { return this.name !== "system"; },

    addRef: function (obj) {
        this.refs.push(util.weakReference(obj));
        this.dropRef(null);
    },
    dropRef: function (obj) {
        this.refs = this.refs.filter(ref => (ref.get() && ref.get() !== obj));

        if (!this.refs.length) {
            this.cleanup();
            styles.hives = styles.hives.filter(h => h !== this);
        }
    },

    cleanup: function cleanup() {
        for (let sheet of this.sheets)
            util.trapErrors(() => {
                sheet.enabled = false;
            });
    },

    "@@iterator": function () {
        return iter(this.sheets);
    },

    get sites() {
        let sites = this.sheets.flatMap(s => s.sites);

        return [...new RealSet(sites)];
    },

    /**
     * Add a new style sheet.
     *
     * @param {string} name The name given to the style sheet by
     *     which it may be later referenced.
     * @param {string} filter The sites to which this sheet will
     *     apply. Can be a domain name or a URL. Any URL ending in
     *     "*" is matched as a prefix.
     * @param {string} css The CSS to be applied.
     * @param {boolean} agent If true, the sheet is installed as an
     *     agent sheet.
     * @param {boolean} lazy If true, the sheet is not initially enabled.
     * @returns {Sheet}
     */
    add: function add(name, filter, css, agent, lazy) {

        if (isArray(filter))
            // Need an array from the same compartment.
            filter = Array.slice(filter);
        else
            filter = filter.split(",");

        if (name && name in this.names) {
            var sheet = this.names[name];
            sheet.agent = agent;
            sheet.css = String(css);
            sheet.sites = filter;
        }
        else {
            sheet = Sheet(name, styles._id++, filter.filter(identity), String(css), this, agent);
            this.sheets.push(sheet);
        }

        styles.allSheets[sheet.id] = sheet;

        if (!lazy)
            sheet.enabled = true;

        if (name)
            this.names[name] = sheet;
        return sheet;
    },

    /**
     * Get a sheet with a given name or index.
     *
     * @param {string or number} sheet The sheet to retrieve. Strings indicate
     *     sheet names, while numbers indicate indices.
     */
    get: function get(sheet) {
        if (typeof sheet === "number")
            return this.sheets[sheet];
        return this.names[sheet];
    },

    /**
     * Find sheets matching the parameters. See {@link #addSheet}
     * for parameters.
     *
     * @param {string} name
     * @param {string} filter
     * @param {string} css
     * @param {number} index
     */
    find: function find(name, filter, css, index) {
        // Grossly inefficient.
        let matches = Object.keys(this.sheets);
        if (index)
            matches = String(index).split(",").filter(i => i in this.sheets);
        if (name)
            matches = matches.filter(i => this.sheets[i].name == name);
        if (css)
            matches = matches.filter(i => this.sheets[i].css == css);
        if (filter)
            matches = matches.filter(i => this.sheets[i].sites.indexOf(filter) >= 0);

        return matches.map(i => this.sheets[i]);
    },

    /**
     * Remove a style sheet. See {@link #addSheet} for parameters.
     * In cases where *filter* is supplied, the given filters are removed from
     * matching sheets. If any remain, the sheet is left in place.
     *
     * @param {string} name
     * @param {string} filter
     * @param {string} css
     * @param {number} index
     */
    remove: function remove(name, filter, css, index) {
        if (arguments.length == 1) {
            var matches = [name];
            name = null;
        }

        if (filter && filter.includes(","))
            return filter.split(",").reduce(
                (n, f) => n + this.removeSheet(name, f, index), 0);

        if (filter == undefined)
            filter = "";

        if (!matches)
            matches = this.findSheets(name, filter, css, index);
        if (matches.length == 0)
            return null;

        for (let sheet of matches.reverse()) {
            if (filter) {
                let sites = sheet.sites.filter(f => f != filter);
                if (sites.length) {
                    sheet.sites = sites;
                    continue;
                }
            }
            sheet.enabled = false;
            if (sheet.name)
                delete this.names[sheet.name];
            delete styles.allSheets[sheet.id];
        }
        this.sheets = this.sheets.filter(s => matches.indexOf(s) == -1);
        return matches.length;
    }
});

/**
 * Manages named and unnamed user style sheets, which apply to both
 * chrome and content pages.
 *
 * @author Kris Maglione <maglione.k@gmail.com>
 */
var Styles = Module("Styles", {
    Local: function (dactyl, modules, window) {
        return { cleanup: function () {} };
    },

    init: function () {
        this._id = 0;
        this.cleanup();
        this.allSheets = {};

        update(services["dactyl:"].providers, {
            "style": function styleProvider(uri, path) {
                let id = parseInt(path);
                if (hasOwnProp(styles.allSheets, id))
                    return ["text/css", styles.allSheets[id].fullCSS];
                return null;
            }
        });
    },

    cleanup: function cleanup() {
        for (let hive of this.hives || [])
            util.trapErrors("cleanup", hive);
        this.hives = [];
        this.user = this.addHive("user", this, true);
        this.system = this.addHive("system", this, false);
    },

    addHive: function addHive(name, ref, persist) {
        let hive = this.hives.find(h => h.name === name);
        if (!hive) {
            hive = Hive(name, persist);
            this.hives.push(hive);
        }
        hive.persist = persist;
        if (ref)
            hive.addRef(ref);
        return hive;
    },

    "@@iterator": function () {
        return iter(this.user.sheets.concat(this.system.sheets));
    },

    _proxy: function (name, args) {
        let obj = this[args[0] ? "system" : "user"];

        return apply(obj, name, Array.slice(args, 1));
    },

    addSheet: deprecated("Styles#{user,system}.add", function addSheet() { return this._proxy("add", arguments); }),
    findSheets: deprecated("Styles#{user,system}.find", function findSheets() { return this._proxy("find", arguments); }),
    get: deprecated("Styles#{user,system}.get", function get() { return this._proxy("get", arguments); }),
    removeSheet: deprecated("Styles#{user,system}.remove", function removeSheet() { return this._proxy("remove", arguments); }),

    userSheets: Class.Property({ get: deprecated("Styles#user.sheets", function userSheets() { return this.user.sheets; }) }),
    systemSheets: Class.Property({ get: deprecated("Styles#system.sheets", function systemSheets() { return this.system.sheets; }) }),
    userNames: Class.Property({ get: deprecated("Styles#user.names", function userNames() { return this.user.names; }) }),
    systemNames: Class.Property({ get: deprecated("Styles#system.names", function systemNames() { return this.system.names; }) }),
    sites: Class.Property({ get: deprecated("Styles#user.sites", function sites() { return this.user.sites; }) }),

    list: function list(content, sites, name, hives) {
        const { commandline, dactyl } = this.modules;

        hives = hives || styles.hives.filter(h => (h.modifiable && h.sheets.length));

        function sheets(group) {
            return group.sheets.slice()
                        .filter(sheet => ((!name || sheet.name === name) &&
                                          (!sites || sites.every(s => sheet.sites.indexOf(s) >= 0))))
                        .sort((a, b) => (a.name && b.name ? String.localeCompare(a.name, b.name)
                                                          : !!b.name - !!a.name || a.id - b.id));
        }

        let uris = util.visibleURIs(content);

        let list = ["table", {},
                ["tr", { highlight: "Title" },
                    ["td"],
                    ["td"],
                    ["td", { style: "padding-right: 1em;" }, _("title.Name")],
                    ["td", { style: "padding-right: 1em;" }, _("title.Filter")],
                    ["td", { style: "padding-right: 1em;" }, _("title.CSS")]],
                ["col", { style: "min-width: 4em; padding-right: 1em;" }],
                ["col", { style: "min-width: 1em; text-align: center; color: red; font-weight: bold;" }],
                ["col", { style: "padding: 0 1em 0 1ex; vertical-align: top;" }],
                ["col", { style: "padding: 0 1em 0 0; vertical-align: top;" }],
                template.map(hives, hive => {
                    let i = 0;
                    return [
                        ["tr", { style: "height: .5ex;" }],
                        template.map(sheets(hive), sheet =>
                            ["tr", {},
                                ["td", { highlight: "Title" }, !i++ ? hive.name : ""],
                                ["td", {}, sheet.enabled ? "" : UTF8("×")],
                                ["td", {}, sheet.name || hive.sheets.indexOf(sheet)],
                                ["td", {}, sheet.formatSites(uris)],
                                ["td", {}, sheet.css]]),
                        ["tr", { style: "height: .5ex;" }]];
                })];

        // E4X-FIXME
        // // TODO: Move this to an ItemList to show this automatically
        // if (list.*.length() === list.text().length() + 5)
        //     dactyl.echomsg(_("style.none"));
        // else
        commandline.commandOutput(list);
    },

    registerSheet: function registerSheet(url, agent, reload) {
        let uri = services.io.newURI(url, null, null);
        if (reload)
            this.unregisterSheet(url, agent);

        let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
        if (reload || !services.stylesheet.sheetRegistered(uri, type)) {
            services.stylesheet.loadAndRegisterSheet(uri, type);
        }
    },

    unregisterSheet: function unregisterSheet(url, agent) {
        let uri = services.io.newURI(url, null, null);
        let type = services.stylesheet[agent ? "AGENT_SHEET" : "USER_SHEET"];
        if (services.stylesheet.sheetRegistered(uri, type))
            services.stylesheet.unregisterSheet(uri, type);
    }
}, {
    append: function (dest, src, sort) {
        let props = {};
        for (let str of [dest, src])
            for (let prop of Styles.propertyIter(str))
                props[prop.name] = prop.value;

        let val = Object.keys(props)[sort ? "sort" : "slice"]()
                        .map(prop => prop + ": " + props[prop] + ";")
                        .join(" ");

        if (/^\s*(\/\*.*?\*\/)/.exec(src))
            val = RegExp.$1 + " " + val;
        return val;
    },

    completeSite: function (context, content, group=styles.user) {
        context.anchored = false;
        try {
            context.fork("current", 0, this, context => {
                context.title = ["Current Site"];
                context.completions = [
                    [content.location.host, /*L*/"Current Host"],
                    [content.location.href, /*L*/"Current URL"]
                ];
            });
        }
        catch (e) {}

        let uris = util.visibleURIs(content);

        context.generate = () => values(group.sites);

        context.keys.text = identity;
        context.keys.description = function (site) {
            return this.sheets.length + /*L*/" sheet" +
                   (this.sheets.length == 1 ? "" : "s") + ": " +
                   Ary.compact(this.sheets.map(s => s.name)).join(", ");
        };
        context.keys.sheets = site => group.sheets.filter(s => s.sites.indexOf(site) >= 0);
        context.keys.active = site => uris.some(Styles.matchFilter(site));

        Styles.splitContext(context, "Sites");
    },

    /**
     * A curried function which determines which host names match a
     * given stylesheet filter. When presented with one argument,
     * returns a matcher function which, given one nsIURI argument,
     * returns true if that argument matches the given filter. When
     * given two arguments, returns true if the second argument matches
     * the given filter.
     *
     * @param {string} filter The URI filter to match against.
     * @param {nsIURI} uri The location to test.
     * @returns {nsIURI -> boolean}
     */
    matchFilter: function (filter) {
        filter = filter.trim();

        if (filter === "*")
            var test = function test(uri) { return true; };
        else if (!/^(?:[a-z-]+:|[a-z-.]+$)/.test(filter)) {
            let re = util.regexp(filter);
            test = function test(uri) { return re.test(uri.spec); };
        }
        else if (/[*]$/.test(filter)) {
            let re = RegExp("^" + util.regexp.escape(filter.substr(0, filter.length - 1)));
            test = function test(uri) { return re.test(uri.spec); };
        }
        else if (/[\/:]/.test(filter))
            test = function test(uri) { return uri.spec === filter; };
        else
            test = function test(uri) {
                try {
                    return util.isSubdomain(uri.host, filter);
                }
                catch (e) { return false; }
            };
        test.toString = function toString() { return filter; }
        test.key = filter;
        if (arguments.length < 2)
            return test;
        return test(arguments[1]);
    },

    splitContext: function splitContext(context, title) {
        for (let item of iter({ Active: true, Inactive: false })) {
            let [name, active] = item;
            context.split(name, null, context => {
                context.title[0] = /*L*/name + " " + (title || "Sheets");
                context.filters.push(item => !!item.active == active);
            });
        }
    },

    propertyIter: function* (str, always) {
        let i = 0;
        let x = /The status bar/.test(str);
        for (let match of this.propertyPattern.iterate(str)) {
            if (match.value || always && match.name || match.wholeMatch === match.preSpace && always && !i++)
                yield match;
            if (!/;/.test(match.postSpace))
                break;
        }
    },

    propertyPattern: util.regexp(String.raw`
            (?:
                (?P<preSpace> <space>*)
                (?P<name> [-a-z]*)
                (?:
                    <space>* : \s* (?P<value>
                        (?:
                            [-\w]+
                            (?:
                                \s* \( \s*
                                    (?: <string> | [^)]*  )
                                \s* (?: \) | $)
                            )?
                            \s*
                            | \s* <string> \s*
                            | <space>*
                            | [^;}]*
                        )*
                    )
                )?
            )
            (?P<postSpace> <space>* (?: ; | $) )
        `, "gix",
        {
            space: /(?: \s | \/\* .*? \*\/ )/,
            string: /(?:" (?:[^\\"]|\\.)* (?:"|$) | '(?:[^\\']|\\.)* (?:'|$) )/
        }),

    patterns: memoize({
        get property() {
            return util.regexp(String.raw`
                (?:
                    (?P<preSpace> <space>*)
                    (?P<name> [-a-z]*)
                    (?:
                        <space>* : \s* (?P<value>
                            <token>*
                        )
                    )?
                )
                (?P<postSpace> <space>* (?: ; | $) )
            `, "gix", this);
        },

        get function() {
            return util.regexp(String.raw`
                (?P<function>
                    \s* \( \s*
                        (?: <string> | [^)]*  )
                    \s* (?: \) | $)
                )
            `, "gx", this);
        },

        space: /(?: \s | \/\* .*? \*\/ )/,

        get string() {
            return util.regexp(String.raw`
                (?P<string>
                    " (?:[^\\"]|\\.)* (?:"|$) |
                    ' (?:[^\\']|\\.)* (?:'|$)
                )
            `, "gx", this);
            // " Vim.
        },

        get token() {
            return util.regexp(String.raw`
                (?P<token>
                    (?P<word> [-\w]+)
                    <function>?
                    \s*
                    | (?P<important> !important\b)
                    | \s* <string> \s*
                    | <space>+
                    | [^;}\s]+
                )
            `, "gix", this);
        }
    }),

    /**
     * Quotes a string for use in CSS stylesheets.
     *
     * @param {string} str
     * @returns {string}
     */
    quote: function quote(str) {
        return '"' + str.replace(/([\\"])/g, "\\$1").replace(/\n/g, "\\00000a") + '"';
    }
}, {
    commands: function initCommands(dactyl, modules, window) {
        const { commands, contexts, styles } = modules;

        function sheets(context, args, filter) {
            let uris = util.visibleURIs(window.content);
            context.compare = modules.CompletionContext.Sort.number;
            context.generate = () => args["-group"].sheets;
            context.keys.active = sheet => uris.some(sheet.bound.match);
            context.keys.description = sheet => [sheet.formatSites(uris), ": ", sheet.css.replace("\n", "\\n")];
            if (filter)
                context.filters.push(({ item }) => filter(item));
            Styles.splitContext(context);
        }

        function nameFlag(filter) {
            return {
                names: ["-name", "-n"],
                description: "The name of this stylesheet",
                type: modules.CommandOption.STRING,
                completer: function (context, args) {
                    context.keys.text = sheet => sheet.name;
                    context.filters.unshift(({ item }) => item.name);
                    sheets(context, args, filter);
                }
            };
        }

        commands.add(["sty[le]"],
            "Add or list user styles",
            function (args) {
                let [filter, css] = args;

                if (!css)
                    styles.list(window.content,
                                filter ? filter.split(",") : null,
                                args["-name"],
                                args.explicitOpts["-group"] ? [args["-group"]]
                                                            : null);
                else {
                    util.assert(args["-group"].modifiable && args["-group"].hive.modifiable,
                                _("group.cantChangeBuiltin", _("style.styles")));

                    if (args["-append"]) {
                        let sheet = args["-group"].get(args["-name"]);
                        if (sheet) {
                            filter = Ary(sheet.sites).concat(filter).uniq().join(",");
                            css = sheet.css + " " + css;
                        }
                    }
                    let style = args["-group"].add(args["-name"], filter, css, args["-agent"]);

                    if (args["-nopersist"] || !args["-append"] || style.persist === undefined)
                        style.persist = !args["-nopersist"];
                }
            }, {
                completer: function (context, args) {
                    let sheet = args["-group"].get(args["-name"]);
                    if (args.completeArg == 0) {
                        if (sheet)
                            context.completions = [[sheet.sites.join(","), _("option.currentValue")]];
                        context.fork("sites", 0, Styles, "completeSite", window.content, args["-group"]);
                    }
                    else if (args.completeArg == 1) {
                        if (sheet)
                            context.completions = [
                                [sheet.css, _("option.currentValue")]
                            ];
                        context.fork("css", 0, modules.completion, "css");
                    }
                },
                hereDoc: true,
                literal: 1,
                options: [
                    { names: ["-agent", "-A"],  description: "Apply style as an Agent sheet" },
                    { names: ["-append", "-a"], description: "Append site filter and css to an existing, matching sheet" },
                    contexts.GroupFlag("styles"),
                    nameFlag(),
                    { names: ["-nopersist", "-N"], description: "Do not save this style to an auto-generated RC file" }
                ],
                serialize: function () {
                    return styles.hives
                        .filter(hive => hive.persist)
                        .flatMap(hive =>
                             hive.sheets.filter(style => style.persist)
                                 .sort((a, b) => String.localeCompare(a.name || "",
                                                                      b.name || ""))
                                 .map(style => ({
                                    command: "style",
                                    arguments: [style.sites.join(",")],
                                    literalArg: style.css,
                                    options: {
                                        "-group": hive.name == "user" ? undefined : hive.name,
                                        "-name": style.name || undefined
                                    }
                                })));
                }
            });

        [
            {
                name: ["stylee[nable]", "stye[nable]"],
                desc: "Enable a user style sheet",
                action: function (sheet) {
                    sheet.enabled = true;
                },
                filter: function (sheet) { return !sheet.enabled; }
            },
            {
                name: ["styled[isable]", "styd[isable]"],
                desc: "Disable a user style sheet",
                action: function (sheet) {
                    sheet.enabled = false;
                },
                filter: function (sheet) { return sheet.enabled; }
            },
            {
                name: ["stylet[oggle]", "styt[oggle]"],
                desc: "Toggle a user style sheet",
                action: function (sheet) {
                    sheet.enabled = !sheet.enabled;
                }
            },
            {
                name: ["dels[tyle]"],
                desc: "Remove a user style sheet",
                action: function (sheet) { sheet.remove(); }
            }
        ].forEach(function (cmd) {
            commands.add(cmd.name, cmd.desc,
                function (args) {
                    dactyl.assert(args.bang ^ !!(args[0] || args[1] || args["-name"] || args["-index"]),
                                  _("error.argumentOrBang"));

                    args["-group"].find(args["-name"], args[0], args.literalArg, args["-index"])
                                  .forEach(cmd.action);
                }, {
                    bang: true,
                    completer: function (context, args) {
                        Styles.completeSite(context, window.content, args["-group"]);
                        if (cmd.filter)
                            context.filters.push(({ sheets }) => sheets.some(cmd.filter));
                    },
                    literal: 1,
                    options: [
                        contexts.GroupFlag("styles"),
                        {
                            names: ["-index", "-i"],
                            type: modules.CommandOption.INT,
                            completer: function (context, args) {
                                context.keys.text = sheet => args["-group"].sheets.indexOf(sheet);
                                sheets(context, args, cmd.filter);
                            }
                        },
                        nameFlag(cmd.filter)
                    ]
                });
        });
    },
    contexts: function initContexts(dactyl, modules, window) {
        modules.contexts.Hives("styles",
            Class("LocalHive", Contexts.Hive, {
                init: function init(group) {
                    init.superapply(this, arguments);
                    this.hive = styles.addHive(group.name, this, this.persist);

                    return new Proxy(this, {
                        get(target, prop) {
                            if (prop in target)
                                return target[prop];

                            return target.hive[prop];
                        },
                    });
                },

                get names() { return this.hive.names; },
                get sheets() { return this.hive.sheets; },
                get sites() { return this.hive.sites; },

                destroy: function () {
                    this.hive.dropRef(this);
                }
            }));
    },
    completion: function initCompletion(dactyl, modules, window) {
        const names = Array.slice(DOM(["div"], window.document).style);
        modules.completion.css = context => {
            context.title = ["CSS Property"];
            context.keys = {
                text: function (p) { return p + ":"; },
                description: function () { return ""; }
            };

            for (let match of Styles.propertyIter(context.filter, true))
                var lastMatch = match;

            if (lastMatch != null && !lastMatch.value && !lastMatch.postSpace) {
                context.advance(lastMatch.index + lastMatch.preSpace.length);
                context.completions = names;
            }
        };
    },
    javascript: function initJavascript(dactyl, modules, window) {
        modules.JavaScript.setCompleter(["get", "add", "remove", "find"].map(m => Hive.prototype[m]),
            [ // Prototype: (name, filter, css, index)
                function (context, obj, args) { return this.names; },
                (context, obj, args) => Styles.completeSite(context, window.content),
                null,
                function (context, obj, args) { return this.sheets; }
            ]);
    },
    template: function initTemplate() {
        let patterns = Styles.patterns;

        template.highlightCSS = function highlightCSS(css) {
            return this.highlightRegexp(css, patterns.property, match => {
                if (!match.length)
                    return [];
                return ["", match.preSpace, template.filter(match.name), ": ",

                    template.highlightRegexp(match.value, patterns.token, match => {
                        if (match.function)
                            return ["", template.filter(match.word),
                                template.highlightRegexp(match.function, patterns.string,
                                                         match => ["span", { highlight: "String" },
                                                                       match.string])
                            ];
                        if (match.important == "!important")
                            return ["span", { highlight: "String" }, match.important];
                        if (match.string)
                            return ["span", { highlight: "String" }, match.string];
                        return template._highlightRegexp(match.wholeMatch, /^(\d+)(em|ex|px|in|cm|mm|pt|pc)?/g,
                                                         (m, n, u) => [
                                                             ["span", { highlight: "Number" }, n],
                                                             ["span", { highlight: "Object" }, u || ""]
                                                         ]);
                    }),
                    match.postSpace
                ];
            });
        };
    }
});

endModule();

// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}

// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript:
