var setTimeout = require("timers").setTimeout

module.exports = signal

// ((A -> void) -> void) -> A -> Signal A
function signal(generator, defaultValue) {
    var value = defaultValue
    var listeners = []

    setTimeout(function () {
        generator(set)
    }, 0)

    return observable

    function observable(listener) {
        if (isGet(listener)) {
            return value
        } else if (isSet(listener)) {
            throw new Error("read-only")
        } else {
            listeners.push(listener)
        }
    }

    function set(v) {
        value = v

        for (var i = 0; i < listeners.length; i++) {
            var listener = listeners[i]
            listener(value)
        }
    }
}

function isGet(x) { return x === undefined }
function isSet(x) { return typeof x !== "function" }


'use strict';

const constants = require('haraka-constants');
const ipaddr    = require('ipaddr.js');

exports.register = function () {
    const plugin = this;
    plugin.inherits('haraka-plugin-redis');

    plugin.register_hook('init_master',  'init_redis_plugin');
    plugin.register_hook('init_child',   'init_redis_plugin');

    plugin.load_limit_ini();

    if (plugin.cfg.concurrency.enabled) {
        plugin.register_hook('connect_init', 'conn_concur_incr');
        plugin.register_hook('connect',      'check_concurrency');
        plugin.register_hook('disconnect',   'conn_concur_decr');
    }

    if (plugin.cfg.errors.enabled) {
        ['helo','ehlo','mail','rcpt','data'].forEach(hook => {
            plugin.register_hook(hook, 'max_errors');
        })
    }

    if (plugin.cfg.recipients.enabled) {
        plugin.register_hook('rcpt', 'max_recipients');
    }

    if (plugin.cfg.unrecognized_commands.enabled) {
        plugin.register_hook('unrecognized_command', 'max_unrecognized_commands');
    }

    if (plugin.cfg.rate_conn.enabled) {
        plugin.register_hook('connect_init', 'rate_conn_incr');
        plugin.register_hook('connect',      'rate_conn_enforce');
    }
    if (plugin.cfg.rate_rcpt_host.enabled) {
        plugin.register_hook('connect', 'rate_rcpt_host_enforce');
        plugin.register_hook('rcpt',    'rate_rcpt_host_incr');
    }
    if (plugin.cfg.rate_rcpt_sender.enabled) {
        plugin.register_hook('rcpt', 'rate_rcpt_sender');
    }
    if (plugin.cfg.rate_rcpt_null.enabled) {
        plugin.register_hook('rcpt', 'rate_rcpt_null');
    }
    if (plugin.cfg.rate_rcpt.enabled) {
        plugin.register_hook('rcpt', 'rate_rcpt');
    }

    if (plugin.cfg.outbound.enabled) {
        plugin.register_hook('send_email', 'outbound_increment');
        plugin.register_hook('delivered',  'outbound_decrement');
        plugin.register_hook('deferred',   'outbound_decrement');
        plugin.register_hook('bounce',     'outbound_decrement');
    }
}

exports.load_limit_ini = function () {
    const plugin = this;
    plugin.cfg = plugin.config.get('limit.ini', {
        booleans: [
            '-outbound.enabled',
            '-recipients.enabled',
            '-unrecognized_commands.enabled',
            '-errors.enabled',
            '-rate_conn.enabled',
            '-rate_rcpt.enabled',
            '-rate_rcpt_host.enabled',
            '-rate_rcpt_sender.enabled',
            '-rate_rcpt_null.enabled',
        ]
    },
    function () {
        plugin.load_limit_ini();
    });

    if (!plugin.cfg.concurrency) {   // no config file
        plugin.cfg.concurrency = {};
    }

    plugin.merge_redis_ini();
};

exports.shutdown = function () {
    if (this.db) this.db.quit();
}

exports.max_unrecognized_commands = function (next, connection, cmd) {
    const plugin = this;
    if (!plugin.cfg.unrecognized_commands) return next();

    connection.results.push(plugin, {unrec_cmds: cmd, emit: true});

    const max = parseFloat(plugin.cfg.unrecognized_commands.max);
    if (!max || isNaN(max)) return next();

    const uc = connection.results.get(plugin).unrec_cmds;
    if (!uc || !uc.length) return next();

    if (uc.length <= max) return next();

    connection.results.add(plugin, { fail: 'unrec_cmds.max' });
    plugin.penalize(connection, true, 'Too many unrecognized commands', next);
};

exports.max_errors = function (next, connection) {
    const plugin = this;
    if (!plugin.cfg.errors) return next();  // disabled in config

    const max = parseFloat(plugin.cfg.errors.max);
    if (!max || isNaN(max)) return next();

    if (connection.errors <= max) return next();

    connection.results.add(plugin, {fail: 'errors.max'});
    plugin.penalize(connection, true, 'Too many errors', next);
};

exports.max_recipients = function (next, connection, params) {
    const plugin = this;
    if (!plugin.cfg.recipients) return next(); // disabled in config

    const max = plugin.get_limit('recipients', connection);
    if (!max || isNaN(max)) return next();

    const c = connection.rcpt_count;
    const count = c.accept + c.tempfail + c.reject + 1;
    if (count <= max) return next();

    connection.results.add(plugin, { fail: 'recipients.max' });
    plugin.penalize(connection, false, 'Too many recipient attempts', next);
};

exports.get_history_limit = function (type, connection) {
    const plugin = this;

    const history_cfg = `${type}_history`;
    if (!plugin.cfg[history_cfg]) return;

    const history_plugin = plugin.cfg[history_cfg].plugin;
    if (!history_plugin) return;

    const results = connection.results.get(history_plugin);
    if (!results) {
        connection.logerror(plugin, `no ${history_plugin} results, disabling history due to misconfiguration`);
        delete plugin.cfg[history_cfg];
        return;
    }

    if (results.history === undefined) {
        connection.logdebug(plugin, `no history from : ${history_plugin}`);
        return;
    }

    const history = parseFloat(results.history);
    connection.logdebug(plugin, `history: ${history}`);
    if (isNaN(history)) return;

    if (history > 0) return plugin.cfg[history_cfg].good;
    if (history < 0) return plugin.cfg[history_cfg].bad;
    return plugin.cfg[history_cfg].none;
};

exports.get_limit = function (type, connection) {
    const plugin = this;

    if (type === 'recipients') {
        if (connection.relaying && plugin.cfg.recipients.max_relaying) {
            return plugin.cfg.recipients.max_relaying;
        }
    }

    if (plugin.cfg[`${type}_history`]) {
        const history = plugin.get_history_limit(type, connection);
        if (history) return history;
    }

    return plugin.cfg[type].max || plugin.cfg[type].default;
};

exports.conn_concur_incr = function (next, connection) {
    const plugin = this;
    if (!plugin.db) return next();
    if (!plugin.cfg.concurrency) return next();

    const dbkey = plugin.get_concurrency_key(connection);

    plugin.db.incr(dbkey, (err, count) => {
        if (err) {
            connection.results.add(plugin, { err: `conn_concur_incr:${err}` });
            return next();
        }

        if (isNaN(count)) {
            connection.results.add(plugin, {err: 'conn_concur_incr got isNaN'});
            return next();
        }

        connection.results.add(plugin, { concurrent_count: count });

        // repair negative concurrency counters
        if (count < 1) {
            connection.results.add(plugin, {
                msg: `resetting concurrent ${count} to 1`
            });
            plugin.db.set(dbkey, 1);
        }

        plugin.db.expire(dbkey, 3 * 60); // 3 minute lifetime
        next();
    });
};

exports.get_concurrency_key = function (connection) {
    return `concurrency|${connection.remote.ip}`;
};

exports.check_concurrency = function (next, connection) {
    const plugin = this;

    const max = plugin.get_limit('concurrency', connection);
    if (!max || isNaN(max)) {
        connection.results.add(plugin, {err: "concurrency: no limit?!"});
        return next();
    }

    const count = parseInt(connection.results.get(plugin.name).concurrent_count);
    if (isNaN(count)) {
        connection.results.add(plugin, { err: 'concurrent.unset' });
        return next();
    }

    connection.results.add(plugin, { concurrent: `${count}/${max}` });

    if (count <= max) return next();

    connection.results.add(plugin, { fail: 'concurrency.max' });

    plugin.penalize(connection, true, 'Too many concurrent connections', next);
};

exports.penalize = function (connection, disconnect, msg, next) {
    const plugin = this;
    const code = disconnect ? constants.DENYSOFTDISCONNECT : constants.DENYSOFT;

    if (!plugin.cfg.main.tarpit_delay) {
        return next(code, msg);
    }

    const delay = plugin.cfg.main.tarpit_delay;
    connection.loginfo(plugin, `tarpitting for ${delay}s`);

    setTimeout(() => {
        if (!connection) return;
        next(code, msg);
    }, delay * 1000);
}

exports.conn_concur_decr = function (next, connection) {
    const plugin = this;
    if (!plugin.db) return next();
    if (!plugin.cfg.concurrency) return next();

    const dbkey = plugin.get_concurrency_key(connection);
    plugin.db.incrby(dbkey, -1, (err, concurrent) => {
        if (err) connection.results.add(plugin, { err: `conn_concur_decr:${err}` })
        return next();
    });
};

exports.get_host_key = function (type, connection, cb) {
    const plugin = this;
    if (!plugin.cfg[type]) {
        return cb(new Error(`${type}: not configured`));
    }

    let ip;
    try {
        ip = ipaddr.parse(connection.remote.ip);
        if (ip.kind === 'ipv6') {
            ip = ipaddr.toNormalizedString();
        }
        else {
            ip = ip.toString();
        }
    }
    catch (err) {
        return cb(err);
    }

    const ip_array = ((ip.kind === 'ipv6') ? ip.split(':') : ip.split('.'));
    while (ip_array.length) {
        const part = ((ip.kind === 'ipv6') ? ip_array.join(':') : ip_array.join('.'));
        if (plugin.cfg[type][part] || plugin.cfg[type][part] === 0) {
            return cb(null, part, plugin.cfg[type][part]);
        }
        ip_array.pop();
    }

    // rDNS
    if (connection.remote.host) {
        const rdns_array = connection.remote.host.toLowerCase().split('.');
        while (rdns_array.length) {
            const part2 = rdns_array.join('.');
            if (plugin.cfg[type][part2] || plugin.cfg[type][part2] === 0) {
                return cb(null, part2, plugin.cfg[type][part2]);
            }
            rdns_array.pop();
        }
    }

    if (plugin.cfg[`${type}_history`]) {
        const history = plugin.get_history_limit(type, connection);
        if (history) return cb(null, ip, history);
    }

    // Custom Default
    if (plugin.cfg[type].default) {
        return cb(null, ip, plugin.cfg[type].default);
    }

    // Default 0 = unlimited
    cb(null, ip, 0);
};

exports.get_mail_key = function (type, mail, cb) {
    const plugin = this;
    if (!plugin.cfg[type] || !mail) return cb();

    // Full e-mail address (e.g. smf@fsl.com)
    const email = mail.address();
    if (plugin.cfg[type][email] || plugin.cfg[type][email] === 0) {
        return cb(email, plugin.cfg[type][email]);
    }

    // RHS parts e.g. host.sub.sub.domain.com
    if (mail.host) {
        const rhs_split = mail.host.toLowerCase().split('.');
        while (rhs_split.length) {
            const part = rhs_split.join('.');
            if (plugin.cfg[type][part] || plugin.cfg[type][part] === 0) {
                return cb(part, plugin.cfg[type][part]);
            }
            rhs_split.pop();
        }
    }

    // Custom Default
    if (plugin.cfg[type].default) {
        return cb(email, plugin.cfg[type].default);
    }

    // Default 0 = unlimited
    cb(email, 0);
};

function getTTL (value) {

    const match = /^(\d+)(?:\/(\d+)(\S)?)?$/.exec(value);
    if (!match) return;

    const qty = match[2];
    const units = match[3];

    let ttl = qty ? qty : 60;  // Default 60s
    if (!units) return ttl;

    // Unit
    switch (units.toLowerCase()) {
        case 's':               // Default is seconds
            break;
        case 'm':
            ttl *= 60;          // minutes
            break;
        case 'h':
            ttl *= (60*60);     // hours
            break;
        case 'd':
            ttl *= (60*60*24);  // days
            break;
        default:
            return;
    }
    return ttl;
}

function getLimit (value) {
    const match = /^([\d]+)/.exec(value);
    if (!match) return 0;
    return parseInt(match[1], 10);
}

exports.rate_limit = function (connection, key, value, cb) {
    const plugin = this;

    if (value === 0) {     // Limit disabled for this host
        connection.loginfo(this, `rate limit disabled for: ${key}`);
        return cb(null, false);
    }

    // CAUTION: !value would match that 0 value -^
    if (!key || !value) return cb();
    if (!plugin.db) return cb();

    const limit = getLimit(value);
    const ttl = getTTL(value);

    if (!limit || ! ttl) {
        return cb(new Error(`syntax error: key=${key} value=${value}`));
    }

    connection.logdebug(plugin, `key=${key} limit=${limit} ttl=${ttl}`);

    plugin.db.incr(key, (err, newval) => {
        if (err) return cb(err);

        if (newval === 1) plugin.db.expire(key, ttl);
        cb(err, parseInt(newval, 10) > limit); // boolean true/false
    })
}

exports.rate_rcpt_host_incr = function (next, connection) {
    const plugin = this;
    if (!plugin.db) return next();

    plugin.get_host_key('rate_rcpt_host', connection, (err, key, value) => {
        if (!key || !value) return next();

        key = `rate_rcpt_host:${key}`;
        plugin.db.incr(key, (err2, newval) => {
            if (newval === 1) plugin.db.expire(key, getTTL(value));
            next();
        })
    })
}

exports.rate_rcpt_host_enforce = function (next, connection) {
    const plugin = this;
    if (!plugin.db) return next();

    plugin.get_host_key('rate_rcpt_host', connection, (err, key, value) => {
        if (err) {
            connection.results.add(plugin, { err: `rate_rcpt_host:${err}` });
            return next();
        }

        if (!key || !value) return next();

        const match = /^(\d+)/.exec(value);
        const limit = parseInt(match[0], 10);
        if (!limit) return next();

        plugin.db.get(`rate_rcpt_host:${key}`, (err2, result) => {
            if (err2) {
                connection.results.add(plugin, { err: `rate_rcpt_host:${err2}` });
                return next();
            }

            if (!result) return next();
            connection.results.add(plugin, {
                rate_rcpt_host: `${key}:${result}:${value}`
            });

            if (result <= limit) return next();

            connection.results.add(plugin, { fail: 'rate_rcpt_host' });
            plugin.penalize(connection, false, 'recipient rate limit exceeded', next);
        });
    });
}

exports.rate_conn_incr = function (next, connection) {
    const plugin = this;
    if (!plugin.db) return next();

    plugin.get_host_key('rate_conn', connection, (err, key, value) => {
        if (!key || !value) return next();

        key = `rate_conn:${key}`;
        plugin.db.hincrby(key, + new Date(), 1, (err2, newval) => {
            // extend key expiration on every new connection
            plugin.db.expire(key, getTTL(value) * 2);
            next();
        });
    });
}

exports.rate_conn_enforce = function (next, connection) {
    const plugin = this;
    if (!plugin.db) return next();

    plugin.get_host_key('rate_conn', connection, (err, key, value) => {
        if (err) {
            connection.results.add(plugin, { err: `rate_conn:${err}` });
            return next();
        }

        if (!key || !value) return next();

        const limit = getLimit(value);
        if (!limit) {
            connection.results.add(plugin, { err: `rate_conn:syntax:${value}` });
            return next();
        }

        plugin.db.hgetall('rate_conn:' + key, (err2, tstamps) => {
            if (err2) {
                connection.results.add(plugin, { err: `rate_conn:${err}` });
                return next();
            }

            if (!tstamps) {
                connection.results.add(plugin, { err: 'rate_conn:no_tstamps' });
                return next();
            }

            const d = new Date();
            d.setMinutes(d.getMinutes() - (getTTL(value) / 60));
            const periodStartTs = + d;  // date as integer

            let connections_in_ttl_period = 0;
            Object.keys(tstamps).forEach(ts => {
                if (parseInt(ts, 10) < periodStartTs) return; // older than ttl
                connections_in_ttl_period = connections_in_ttl_period + parseInt(tstamps[ts], 10);
            })
            connection.results.add(plugin, { rate_conn: connections_in_ttl_period + ':' + value});

            if (connections_in_ttl_period <= limit) return next();

            connection.results.add(plugin, { fail: 'rate_conn' });

            plugin.penalize(connection, true, 'connection rate limit exceeded', next);
        });
    });
};

exports.rate_rcpt_sender = function (next, connection, params) {
    const plugin = this;

    plugin.get_mail_key('rate_rcpt_sender', connection.transaction.mail_from, (key, value) => {

        plugin.rate_limit(connection, `rate_rcpt_sender:${key}`, value, (err, over) => {
            if (err) {
                connection.results.add(plugin, { err: `rate_rcpt_sender:${err}` });
                return next();
            }

            connection.results.add(plugin, { rate_rcpt_sender: value });

            if (!over) return next();

            connection.results.add(plugin, { fail: 'rate_rcpt_sender' });
            plugin.penalize(connection, false, 'rcpt rate limit exceeded', next);
        });
    });
};

exports.rate_rcpt_null = function (next, connection, params) {
    const plugin = this;

    if (!params) return next();
    if (Array.isArray(params)) params = params[0];
    if (params.user) return next();

    // Message from the null sender
    plugin.get_mail_key('rate_rcpt_null', params, (key, value) => {

        plugin.rate_limit(connection, `rate_rcpt_null:${key}`, value, (err2, over) => {
            if (err2) {
                connection.results.add(plugin, { err: `rate_rcpt_null:${err2}` });
                return next();
            }

            connection.results.add(plugin, { rate_rcpt_null: value });

            if (!over) return next();

            connection.results.add(plugin, { fail: 'rate_rcpt_null' });
            plugin.penalize(connection, false, 'null recip rate limit', next);
        });
    });
};

exports.rate_rcpt = function (next, connection, params) {
    const plugin = this;
    if (Array.isArray(params)) params = params[0];
    plugin.get_mail_key('rate_rcpt', params, (key, value) => {

        plugin.rate_limit(connection, `rate_rcpt:${key}`, value, (err2, over) => {
            if (err2) {
                connection.results.add(plugin, { err: `rate_rcpt:${err2}` });
                return next();
            }

            connection.results.add(plugin, { rate_rcpt: value });

            if (!over) return next();

            connection.results.add(plugin, { fail: 'rate_rcpt' });
            plugin.penalize(connection, false, 'rate limit exceeded', next);
        });
    });
};

/*
 *        Outbound Rate Limits
 *
 */

function getOutDom (hmail) {
    // outbound isn't internally consistent in the use of hmail.domain
    // vs hmail.todo.domain.
    // TODO: fix haraka/Haraka/outbound/HMailItem to be internally consistent.
    if (hmail.todo && hmail.todo.domain) return hmail.todo.domain;
    return hmail.domain;
}

function getOutKey (domain) {
    return 'outbound-rate:' + domain;
}

exports.outbound_increment = function (next, hmail) {
    const plugin = this;
    if (!plugin.db) return next();

    const outDom = getOutDom(hmail);
    const outKey = getOutKey(outDom);

    plugin.db.hincrby(outKey, 'TOTAL', 1, (err, count) => {
        if (err) {
            plugin.logerror(`outbound_increment: ${err}`);
            return next(); // just deliver
        }

        plugin.db.expire(outKey, 300);  // 5 min expire

        if (!plugin.cfg.outbound[outDom]) return next();
        const limit = parseInt(plugin.cfg.outbound[outDom], 10);
        if (!limit) return next();

        count = parseInt(count, 10);
        if (count <= limit) return next();

        const delay = plugin.cfg.outbound.delay || 30;
        next(constants.delay, delay);
    })
}

exports.outbound_decrement = function (next, hmail) {
    const plugin = this;
    if (!plugin.db) return next();

    plugin.db.hincrby(getOutKey(getOutDom(hmail)), 'TOTAL', -1);
    return next();
}


'use strict';

var constants = require('../dreamscreen').constants;
var utils = require('../dreamscreen').utils;

/**
 * A representation of a light bulb
 * DreamScreen HD, DreamScreen 4K, SideKick
 *
 * @class
 * @param {Obj} constr constructor object
 * @param {DreamScreen/Client} constr.client the client the light belongs to
 * @param {String} constr.ipAddress ip address of the light
 */
function Light(constr) {
    this.client = constr.client;

    this.ipAddress = constr.ipAddress;
    this.serialNumber = constr.serialNumber;
    this.productId = constr.productId;                  //devicetype

    this.lastSeen = constr.lastSeen;
    this.isReachable = constr.isReachable;

    this.name = constr.name;                            //devicename
    this.groupName = constr.groupName;                  //groupname
    this.groupNumber = constr.groupNumber;              //groupnumber

    this.mode = constr.mode;                            //mode
    this.brightness = constr.brightness;                //brightness
    this.ambientColor = constr.ambientColor;            //ambientr ambientg ambientb
    this.ambientShow = constr.ambientShow;              //ambientscene
    this.ambientModeType = constr.ambientModeType;      //
    this.hdmiInput = constr.hdmiInput;                  //hdmiinput
    this.hdmiInputName1 = constr.hdmiInputName1;        //hdminame1
    this.hdmiInputName2 = constr.hdmiInputName2;        //hdminame2
    this.hdmiInputName3 = constr.hdmiInputName3;        //hdminame3
}

/**
 * Mode that specifies the behavior of the backlighting LEDs
 * @param {*} mode 1 byte, range 0-3
 * 0 Sleep (Off)
 * 1 Video
 * 2 Music
 * 3 Ambient
 */
Light.prototype.setMode = function (mode, callback) {
    if (mode !== undefined && typeof mode !== 'number') {
        throw new RangeError('light, setMode method expects mode to be a number');
    } else if (mode < 0 || mode > 3) {
        throw new RangeError('light, setMode method expects mode to be in range 0-3');
    }

    this.mode = mode;
    if (this.client.debug) {
        console.log(`${this.name}, setMode ${mode}`);
    }
    utils.sendUDPWrite(this, 0x03, 0x01, [mode], false, callback);
};

/**
 * Brightness of the backlighting LEDs
 * @param {*} brightness 1 byte, range 0-100
 */
Light.prototype.setBrightness = function (brightness, callback) {
    if (brightness !== undefined && typeof brightness !== 'number') {
        throw new RangeError('light, setBrightness method expects brightness to be a number');
    } else if (brightness < 0 || brightness > 100) {
        throw new RangeError('light, setBrightness method expects brightness to be in range 0-100');
    }

    this.brightness = brightness;
    if (this.client.debug) {
        console.log(`${this.name}, setBrightness ${brightness}`);
    }
    utils.sendUDPWrite(this, 0x03, 0x02, [brightness], false, callback);
};

/**
 * Sets the Ambient mode type, either static or show
 * @param {*} ambientModeType 1 byte, range 0-1
 * 0 Static, 1 Show
 */
Light.prototype.setAmbientModeType = function (ambientModeType, callback) {
    if (ambientModeType !== undefined && typeof ambientModeType !== 'number') {
        throw new RangeError('light, setAmbientModeType method expects ambientModeType to be a number');
    } else if (ambientModeType < 0 || ambientModeType > 1) {
        throw new RangeError('light, setAmbientModeType method expects ambientModeType to be in range 0-1');
    }

    this.ambientModeType = ambientModeType;
    if (this.client.debug) {
        console.log(`${this.name}, setAmbientModeType ${ambientModeType}`);
    }
    utils.sendUDPWrite(this, 0x03, 0x08, [ambientModeType], false, callback);
};

/**
 * The static color displayed in Ambient-static mode
 * @param {*} ambientColor 3 bytes as RGB array, range 0-255
 */
Light.prototype.setAmbientColor = function (ambientColor, callback) {
    if (ambientColor !== undefined && !Array.isArray(ambientColor)) {
        throw new RangeError('light, setAmbientColor method expects ambientColor to be an array');
    } else if (ambientColor.length != 3) {
        throw new RangeError('light, setAmbientColor method expects ambientColor to be an array of length 3');
    }

    this.ambientColor = ambientColor;
    if (this.client.debug) {
        console.log(`${this.name}, setAmbientColor ${ambientColor}`);
    }
    utils.sendUDPWrite(this, 0x03, 0x05, ambientColor, false, callback);
};

/**
 * The show/scene displayed in Ambient-scene mode
 * @param {*} ambientShow 1 byte, range 0-8
 * 0 Random color
 * 1 Fireside
 * 2 Twinkle
 * 3 Ocean
 * 4 Rainbow
 * 5 July 4th
 * 6 Holiday
 * 7 Pop
 * 8 Enchanted Forest
 */
Light.prototype.setAmbientShow = function (ambientShow, callback) {
    if (ambientShow !== undefined && typeof ambientShow !== 'number') {
        throw new RangeError('light, setAmbientShow method expects ambientShow to be a number');
    } else if (ambientShow < 0 || ambientShow > 8) {
        throw new RangeError('light, setAmbientShow method expects ambientShow to be in range 0-8');
    }

    this.ambientShow = ambientShow;
    if (this.client.debug) {
        console.log(`${this.name}, setAmbientShow ${ambientShow}`);
    }
    utils.sendUDPWrite(this, 0x03, 0x0D, [ambientShow], false, callback);
};

/**
 * The active HDMI port that DreamScreen is displaying
 * @param {*} hdmiInput 1 byte, range 0-2
 * value 0: HDMI 1
 * value 1: HDMI 2
 * value 2: HDMI 3
 */
Light.prototype.setHdmiInput = function (hdmiInput, callback) {
    if (hdmiInput !== undefined && typeof hdmiInput !== 'number') {
        throw new RangeError('light, setHdmiInput method expects hdmiInput to be a number');
    } else if (hdmiInput < 0 || hdmiInput > 2) {
        throw new RangeError('light, setHdmiInput method expects hdmiInput to be in range 0-2');
    }

    this.hdmiInput = hdmiInput;
    if (this.client.debug) {
        console.log(`${this.name}, setHdmiInput ${hdmiInput}`);
    }
    utils.sendUDPWrite(this, 0x03, 0x20, [hdmiInput], false, callback);
};

exports.Light = Light;


var document = require("global/document")

var signal = require("../signal/signal")

var KEYS = {
    "37": "left"
    , "38": "up"
    , "39": "right"
    , "40": "down"
}

module.exports = KeyboardArrows

// Signal { x: Number, y: Number }
function KeyboardArrows() {
    var validKeys = [37, 38, 39, 40]

    return signal(function (next) {
        var down = {}

        document.addEventListener("keyup", function onup(ev) {
            if (ev.which in KEYS) {
                var key = KEYS[ev.which]
                down[key] = false

                next(getState())
            }
        })

        document.addEventListener("keydown", function ondown(ev) {
            if (ev.which in KEYS) {
                var key = KEYS[ev.which]
                down[key] = true

                next(getState())
            }
        })

        function getState() {
            var x = 0, y = 0
            if (down.up) {
                y = 1
            } else if (down.down) {
                y = -1
            }

            if (down.left) {
                x = -1
            } else if (down.right) {
                x = 1
            }

            return { x: x, y: y }
        }

    }, { x: 0, y: 0 })
}


var setTimeout = require("timers").setTimeout

var signal = require("../signal/signal")

module.exports = fps

// Signal Number
function fps(desiredFps) {
    var msPerFrame = 1000 / desiredFps

    return signal(function (next) {
        var prev = Date.now()

        setTimeout(tick, msPerFrame)

        function tick() {
            var curr = Date.now()
            var diff = curr - prev
            prev = curr

            next(diff)

            setTimeout(tick, msPerFrame)
        }
    }, 0)
}


var document = require("global/document")
var process = require("process")

var signal = require("../signal/signal")
var TransformStorage = require("./transform-storage")

var ENTER = 13

module.exports = EventPool

/*
String -> {
    signal: Signal<Any>,
    on: Element -> String -> (EventTarget -> Any) -> Element,
    submit: Element -> (valueof EventTarget -> Any) -> Element,
    change: Element -> (valueof EventTarget -> Any) -> Element
}
*/
function EventPool(name){
    var storage = TransformStorage(name)
    var events = {}
    var _next
    var nextTick = function (value, elem) {
        process.nextTick(function () {
            _next(value, elem)
        })
    }

    return {
        signal: signal(function (next) {
            handleSubmit(next)
            handleChange(next)

            Object.keys(events).forEach(function (event) {
                handleEvent(event, next)
            })

            _next = next
        }),
        submit: function (elem, transform) {
            return storage.set("submit", elem, transform)
        },
        change: function (elem, transform) {
            return storage.set("change", elem, transform)
        },
        on: function (elem, event, transform) {
            if (!events[event]) {
                events[event] = true

                if (_next) {
                    handleEvent(event, _next)
                }
            }

            return storage.set(event, elem, transform)
        }
    }

    function handleSubmit(next) {
        document.addEventListener("keypress", function (ev) {
            var target = ev.target
            var fn = storage.get("submit", target)

            var validEvent = fn && target.type === "text" &&
                ev.keyCode === ENTER && !ev.shiftKey

            if (!validEvent) {
                return
            }

            var item = fn(target.value, target)
            nextTick(item)
        })

        document.addEventListener("click", function (ev) {
            var target = ev.target
            var fn = storage.get("submit", target)

            if (!fn || target.tagName !== "BUTTON") {
                return
            }

            nextTick(fn(), target)
        })
    }

    function handleChange(next) {
        document.addEventListener("keypress", function (ev) {
            var target = ev.target
            var fn = storage.get("change", target)

            if (!fn || target.type !== "text") {
                return
            }

            nextTick(fn(target.value), target)
        })

        document.addEventListener("change", function (ev) {
            var target = ev.target
            var fn = storage.get("change", target)

            if (!fn || target.type !== "checkbox") {
                return
            }

            nextTick(fn(target.checked), target)
        })
    }

    function handleEvent(event, next) {
        document.addEventListener(event, function (ev) {
            var target = ev.target
            var fn = storage.get(event, target)

            if (fn) {
                nextTick(fn(target.value, target))
            }
        }, true)
    }
}


var document = require("global/document")

var getTextSize = require("./get-text-size")
var Element = require("./element")

function TextElement(textAlign, textContent) {
    this.textAlign = textAlign
    this.textContent = textContent
}

TextElement.prototype.type = "TextElement"

TextElement.prototype.create = function _TextElement_create() {
    var div = document.createElement("div")
    div.textContent = this.textContent
    div.style.textAlign = this.textAlign
    return div
}

TextElement.prototype.update = function _TextElement_update(elem, previous) {
    if (previous.textContent !== this.textContent) {
        elem.textContent = this.textContent
    }
}

module.exports = plainText

// String -> Element
function plainText(content) {
    var textSize = getTextSize(content)

    return new Element(new TextElement("left", content)
        , textSize.width, textSize.height)
}


var window = require("global/window")

var signal = require("../signal/signal")

module.exports = WindowDimensions

// Signal { width: Number, height: Number }
function WindowDimensions() {
    return signal(function (next) {
        window.addEventListener("resize", function (e) {
            next({
                width: window.innerWidth
                , height: window.innerHeight
            })
        })
    }, {
        width: window.innerWidth
        , height: window.innerHeight
    })
}


var document = require("global/document")

// JSONFormatter json->HTML prototype straight from Firefox JSONView
// For reference: http://code.google.com/p/jsonview
function JSONFormatter() {
    // No magic required.
}

JSONFormatter.prototype = {
    htmlEncode: function (t) {
        return (t !== null && t !== undefined) ? t.toString()
            .replace(/&/g,"&")
            .replace(/"/g,""")
            .replace(/</g,"<")
            .replace(/>/g,">") : '';
    },

    decorateWithSpan: function (value, className) {
        return '' +
            this.htmlEncode(value) + '';
    },

    // Convert a basic JSON datatype (number, string, boolean, null,
    // object, array) into an HTML fragment.
    valueToHTML: function (value) {
        var valueType = typeof value;

        var output = "";
        if (value === null || value === undefined) {
            output += this.decorateWithSpan('null', 'null');
        }
        else if (value && value.constructor === Array) {
            output += this.arrayToHTML(value);
        }
        else if (valueType === 'object') {
            output += this.objectToHTML(value);
        }
        else if (valueType === 'number') {
            output += this.decorateWithSpan(value, 'num');
        }
        else if (valueType === 'string') {
            if (/^(http|https):\/\/[^\s]+$/.test(value)) {
                output += '' +
                    this.htmlEncode(value) + '';
            } else {
                output += this.decorateWithSpan('"' + value + '"', 'string');
            }
        }
        else if (valueType === 'boolean') {
            output += this.decorateWithSpan(value, 'bool');
        }

        return output;
    },

    // Convert an array into an HTML fragment
    arrayToHTML: function(json) {
      var output = '[
';
      var hasContents = false;
      for ( var prop in json ) {
        hasContents = true;
        output += '
';
        output += this.valueToHTML(json[prop]);
        output += '
';
      }
      output += '
]';

      if ( ! hasContents ) {
        output = "[ ]";
      }

      return output;
    },

    // Convert a JSON object to an HTML fragment
    objectToHTML: function(json) {
        var output = '{
';
        var hasContents = false;
        for ( var prop in json ) {
            hasContents = true;
            output += '
';
            output += '' +
                this.htmlEncode(prop) + ': ';
            output += this.valueToHTML(json[prop]);
            output += '
';
        }
        output += '
}';

        if ( ! hasContents ) {
            output = "{ }";
        }

        return output;
    },

    // Convert a whole JSON object into a formatted HTML document.
    jsonToHTML: function(json) {
        var output = '';
        output += '
';
        output += this.valueToHTML(json);
        output += '
';
        return output;
    },

    // Produce an error document for when parsing fails.
    errorPage: function(error, data, uri) {
        // var output = '
' +
        //    this.stringbundle.GetStringFromName('errorParsing') + '
';
        // output += '
' +
        //    this.stringbundle.GetStringFromName('docContents') + ':
';
        var output = '
Error parsing JSON: ' +
        error.message+'
'
        output += '
'+error.stack+':
';
        output += '
' + this.htmlEncode(data) + '
';
        return this.toHTML(output, uri + ' - Error');
    }
}

var collapseStates = {}

module.exports = JSONFormat

function JSONFormat(jsonObj, id) {
    var collapseState = collapseStates[id]

    if (!collapseState) {
        collapseState = collapseStates[id] = {}
    }

    // TODO remember collapsal path

    function collapse(evt) {
        var collapser = evt.target;

        var target = collapser.parentNode.getElementsByClassName('collapsible');

        if ( ! target.length ) {
            return
        }

        target = target[0];

        if ( target.style.display === 'none' ) {
            var ellipsis = target.parentNode
                .getElementsByClassName('ellipsis')[0]
            target.parentNode.removeChild(ellipsis);
            target.style.display = '';
            collapser.innerHTML = '-';
        } else {
            target.style.display = 'none';

            var ellipsis = document.createElement('span');
            ellipsis.className = 'ellipsis';
            ellipsis.innerHTML = ' … ';
            target.parentNode.insertBefore(ellipsis, target);
            collapser.innerHTML = '+';
        }
    }

    function addCollapser(item) {
        // This mainly filters out the root object
        // (which shouldn't be collapsible)
        if ( item.nodeName !== 'LI' ) {
            return
        }

        var collapser = document.createElement('div');
        collapser.className = 'collapser';
        collapser.innerHTML = '-';
        collapser.addEventListener('click', collapse, false);
        item.insertBefore(collapser, item.firstChild);
    }

    var jsonFormatter = new JSONFormatter;

    // Covert, and catch exceptions on failure
    var outputDoc = jsonFormatter.jsonToHTML(jsonObj);

    var div = document.createElement("div")
    div.innerHTML = outputDoc

    var items = div.getElementsByClassName('collapsible');
    for( var i = 0; i < items.length; i++) {
        addCollapser(items[i].parentNode);
    }

    return div
}


var document = require("global/document")

var Element = require("./element")

function FlowElement(direction, elements) {
    this.direction = direction
    this.elements = elements
}

FlowElement.prototype.type = "FlowElement"

FlowElement.prototype.create = function _FlowElement_create() {
    var elements = this.elements
    if (this.direction === "down" ||
        this.direction === "right" ||
        this.direction === "outward"
    ) {
        elements = elements.slice().reverse()
    }

    var container = document.createElement("div")

    for (var i = elements.length; i--;) {
        var element = elements[i]
        var domElement = element.create ? element.create() : element

        if (this.direction === "right" || this.direction === "left") {
            domElement.style.styleFloat = "left"
            domElement.style.cssFloat = "left"
        } else if (this.direction === "inward" ||
            this.direction === "outward"
        ) {
            domElement.style.position = "absolute"
        }

        container.appendChild(domElement)
    }

    return container
}

FlowElement.prototype.update = function _FlowElement_update(elem, previous) {
    var parentElem = elem.parentNode
    if (this.direction !== previous.direction) {
        return parentElem.replaceChild(this.create(), elem)
    }

    var nextElements = this.elements
    var childNodes = elem.childNodes

    if (childNodes.length !== nextElements.length) {
        return parentElem.replaceChild(this.create(), elem)
    }

    var previousElements = previous.elements

    for (var i = childNodes.length; i--;) {
        nextElements[i].update(childNodes[i], previousElements[i])
        var domElement = childNodes[i]

        if (this.direction === "right" || this.direction === "left") {
            domElement.style.styleFloat = "left"
            domElement.style.cssFloat = "left"
        } else if (this.direction === "inward" ||
            this.direction === "outward"
        ) {
            domElement.style.position = "absolute"
        }
    }
}

module.exports = flow

// String -> [Element] -> Element
function flow(direction, elements) {
    var widths = elements.map(widthOf)
    var heights = elements.map(heightOf)

    var width = direction === "left" ?
        sum(widths) : direction === "right" ?
        sum(widths) : maximum(widths)
    var height = direction === "down" ?
        sum(heights) : direction === "right" ?
        sum(heights) : maximum(heights)

    return new Element(new FlowElement(direction, elements)
        , width, height)
}

function widthOf(elem) {
    return elem.width
}

function heightOf(elem) {
    return elem.height
}

function sum(list) {
    var total = 0
    for (var i = 0; i < list.length; i++) {
        total += list[i]
    }
    return total
}

function maximum(list) {
    var max = 0
    for (var i = 0; i < list.length; i++) {
        var item = list[i]
        if (item > max) {
            max = item
        }
    }
    return max
}


var document = require("global/document")

var Element = require("./element")
var flow = require("./flow")

var MATH_PI = Math.PI

function CollageElement(forms, width, height) {
    var formGroups = []
    var group = []

    for (var i = forms.length; i--; ) {
        var form = forms[i]

        if (form.basicForm.type === "FormElement") {
            if (group.length > 0) {
                formGroups.push(group)
                group = []
            }

            formGroups.push(form)
        } else {
            group.push(form)
        }
    }

    if (group.length > 0) {
        formGroups.push(group)
    }

    this.width = width
    this.height = height
    this.formGroups = formGroups
}

CollageElement.prototype.create = function _ContainerElement_create() {
    // console.log("CollageElement.create")
    var formGroups = this.formGroups
    var width = this.width
    var height = this.height

    if (formGroups.length === 0) {
        return createCollageForForms(width, height, [])
    }

    var elems = []

    for (var i = formGroups.length; i--; ) {
        var group = formGroups[i]

        if (!Array.isArray(group)) {
            elems[i] = createCollageForElements(width, height, group)
        } else {
            // console.log("CREATE COLLAGE FOR FORMS", formGroups)
            elems[i] = createCollageForForms(width, height, group)
        }
    }

    if (elems.length === 1) {
        return elems[0]
    }

    return flow("inward", elems).create()

}

CollageElement.prototype.update = function (elem, previous) {
    var current = this
    var currentFormGroups = this.formGroups
    var previousFormGroups = previous.formGroups

    var width = current.width
    var height = current.height

    if (currentFormGroups.length === 1) {
        return updateFormSet(elem, previousFormGroups[0]
            , currentFormGroups[0], width, height)
    }

    var kids = elem.childNodes
    var len = kids.length

    for (var i = len; i--; ) {
        var childElem = kids[len - i - 1]

        // childElem.style.width = (~~current.width) + "px"
        // childElem.style.height = (~~current.height) + "px"

        // console.log("current", current, elem.style.height)

        updateFormSet(kids[len - i - 1], previousFormGroups[i]
            , currentFormGroups[i], width, height)
    }
}

module.exports = collage

// Int -> Int -> [Form] -> Element
function collage(width, height, forms) {
    return new Element(new CollageElement(forms, width, height)
        , width, height)
}

function updateFormSet(elem, previousGroup, currentGroup, width, height) {
    // var width = elem.style.width.slice(0, -2) - 0
    // var height = elem.style.height.slice(0, -2) - 0

    elem.style.width = (~~width) + "px"
    elem.style.height = (~~height) + "px"

    if (elem.tagName === "CANVAS") {
        elem.width = width
        elem.height = height
    }

    if (Array.isArray(currentGroup) && currentGroup.length > 0) {
        if (Array.isArray(previousGroup) && previousGroup.length > 0) {
            if (elem.getContext) {
                var context = elem.getContext("2d")

                return renderForms(context, currentGroup, width, height)
            }
        }

        var newElem = createCollageForForms(width, height, currentGroup)
        newElem.style.position = "absolute"
        return elem.parentNode.replaceChild(newElem, elem)
    }

    var currentForm = currentGroup
    var previousForm = previousGroup

    var currentElement = currentForm.basicForm.element
    var previousElement = previousForm.basicForm.element
    var domElement = elem.firstChild
    currentElement.update(domElement, previousElement)

    applyTransformations(currentForm, currentElement.width
        , currentElement.height, domElement)
}

function createCollageForForms(width, height, forms) {
    var canvas = document.createElement("canvas")
    width = ~~width
    height = ~~height
    canvas.style.width = width + "px"
    canvas.style.height = height + "px"
    canvas.style.display = "block"
    canvas.width = width
    canvas.height = height

    if (canvas.getContext) {
        var context = canvas.getContext("2d")

        renderForms(context, forms, width, height)

        return canvas
    }

    canvas.textContent = "Your browser does not support the canvas element."
    return canvas
}

function createCollageForElements(width, height, form) {
    var element = form.basicForm.element
    var domElement = element.create()

    applyTransformations(form, element.width
        , element.height, domElement)

    var div = document.createElement("div")
    div.appendChild(domElement)

    div.style.width = (~~width) + "px"
    div.style.height = (~~height) + "px"
    div.style.overflow = "hidden"

    return div
}

function renderForms(context, forms, width, height) {
    context.clearRect(0, 0, width, height)

    for (var i = forms.length; i--; ) {
        var form = forms[i]
        context.save()
        var x = form.position.x
        var y = form.position.y

        if (x !== 0 || y !== 0) {
            context.translate(x, y)
        }

        var theta = form.theta

        if (theta !== ~~theta) {
            context.rotate(2 * MATH_PI * theta)
        }

        var scale = form.scale

        if (scale !== 1) {
            context.scale(scale, scale)
        }

        context.beginPath()

        var basicForm = form.basicForm
        // console.log("Calling create", forms)
        basicForm.create(context)

        context.restore()
    }
}

function applyTransformations(form, w, h, elem) {
    var theta = form.theta
    var scale = form.scale
    var position = form.position
    var x = position.x
    var y = position.y

    var t = "translate(" + (x - w / 2) + "px,"+ (y - h / 2) + "px)"
    var r = theta === (~~theta) ? "" : "rotate(" + theta*360 + "deg)"
    var s = scale === 1 ? "" : "scale(" + scale + "," + scale + ")"
    var transforms = t + " " + s + " " + r
    elem.style.transform = transforms
    elem.style.msTransform = transforms
    elem.style.MozTransform = transforms
    elem.style.webkitTransform = transforms
    elem.style.OTransform = transforms
}


var document = require("global/document")
var ClassList = require("class-list")
var DataSet = require("data-set")
var setTimeout = require("timers").setTimeout

var plainText = require("./plain-text")
var Element = require("./element")

var NOT_FOUND = -1
var splitSelectorRegex = /([\.#]?[a-zA-Z0-9_-]+)/

function TextElement(text) {
    this.text = text
}

TextElement.prototype.type = "TextElement"

TextElement.prototype.create = function _TextElement_create() {
    return document.createTextNode(this.text)
}

TextElement.prototype.update = function _TextElement_update(elem, previous) {
    if (this.text !== previous.text) {
        elem.data = this.text
    }
}

/* a HtmlElement has a tagName, a hash of special attributes and a hash
    of general attributes.

    The special attributes have special rendering & updating logic
    The general attributes are just set on the DOM element

    The children array contains a list of Element instances that are appended
        to this particular element when rendered

*/
function HtmlElement(tagName, special, general, children) {
    this.tagName = tagName
    this.specialProperties = special
    this.generalProperties = general
    this.children = children
    this.cl = null // elem.classList
    this.ds = null // elem.dataset
}

HtmlElement.prototype.type = "HtmlElement"

HtmlElement.prototype.create = function _HtmlElement_create() {
    var elem = document.createElement(this.tagName)

    var generalProperties = this.generalProperties
    var generalKeys = Object.keys(generalProperties)

    for (var i = 0; i < generalKeys.length; i++) {
        var generalKey = generalKeys[i]
        var generalProp = generalProperties[generalKey]

        elem[generalKey] = generalProp
    }

    var specialProperties = this.specialProperties
    var classes = specialProperties.classes
    var cl = this.cl = ClassList(elem)

    for (var j = 0; j < classes.length; j++) {
        cl.add(classes[j])
    }

    var dataset = specialProperties.dataset
    var datasetKeys = Object.keys(dataset)
    var ds = this.ds = DataSet(elem)

    for (var k = 0; k < datasetKeys.length; k++) {
        var datasetKey = datasetKeys[k]
        var datasetAttr = dataset[datasetKey]

        ds[datasetKey] = datasetAttr
    }

    var children = this.children

    for (var l = 0; l < children.length; l++) {
        var child = children[l]
        var domElement = child.create ? child.create() : child

        elem.appendChild(domElement)
    }

    handleFocus(elem, specialProperties)

    return elem
}

function handleFocus(elem, specialProperties) {
    // Many hacks :(
    if (specialProperties.focus) {
        setTimeout(function () {
            elem.focus()
        }, 0)
    }
}

// TODO: Be more efficient
HtmlElement.prototype.update = function _HtmlElement_update(elem, previous) {
    var parentElem = elem.parentNode
    var current = this

    if (current.tagName !== previous.tagName) {
        var replacement = current.create()
        return parentElem.replaceChild(replacement, elem)
    }

    handleProperties(previous, current, elem)
    handleClasses(previous, current)
    handleDataSet(previous, current)

    var currChildren = current.children
    var currLength = currChildren.length
    var prevChildren = previous.children
    var prevLength = prevChildren.length
    var childNodes = elem.childNodes

    var smallestLength = prevLength < currLength ?
        prevLength : currLength

    // if curr list smaller then prev then nuke excess DOM elems
    for (var i = prevLength; i > currLength; i--) {
        elem.removeChild(childNodes[currLength])
    }

    // for all elements that are in both arrays
    for (var j = 0; j < smallestLength; j++) {
        var currChild = currChildren[j]
        var prevChild = prevChildren[j]

        currChild.update(childNodes[j], prevChild)
    }

    // for all elements that are in curr but not prev
    for (var k = prevLength; k < currLength; k++) {
        var child = currChildren[k]
        var domElement = child.create ? child.create() : child

        elem.appendChild(domElement)
    }

    handleFocus(elem, current.specialProperties)
}

function handleDataSet(previous, current) {
    var currDataset = current.specialProperties.dataset
    var prevDataset = current.specialProperties.dataset
    var ds = current.ds = previous.ds

    var prevDataSetKeys = Object.keys(prevDataset)
    for (var i = 0; i < prevDataSetKeys.length; i++) {
        var key = prevDataSetKeys[i]
        if (!(key in currDataset)) {
            delete ds[key]
        }
    }

    var currDataSetKeys = Object.keys(currDataset)

    for (var j = 0; j < currDataSetKeys.length; j++) {
        var datasetKey = currDataSetKeys[j]
        var datasetAttr = currDataset[datasetKey]

        ds[datasetKey] = datasetAttr
    }
}

function handleProperties(previous, current, elem) {
    var currGeneral = current.generalProperties
    var prevGeneral = previous.generalProperties

    var currGeneralKeys = Object.keys(currGeneral)
    for (var i = 0; i < currGeneralKeys.length; i++) {
        var currGeneralKey = currGeneralKeys[i]
        var currGeneralProp = currGeneral[currGeneralKey]
        var prevGeneralProp = prevGeneral[currGeneralKey]

        if (currGeneralProp !== prevGeneralProp) {
            elem[currGeneralKey] = currGeneralProp
        }
    }
}

function handleClasses(previous, current) {
    var currClasses = current.specialProperties.classes
    var prevClasses = previous.specialProperties.classes
    var cl = current.cl = previous.cl

    for (var j = 0; j < prevClasses.length; j++) {
        var className = prevClasses[j]
        var index = currClasses.indexOf(className)

        if (index === NOT_FOUND) {
            cl.remove(className)
        }
    }

    for (var k = 0; k < currClasses.length; k++) {
        var className = currClasses[k]
        var index = prevClasses.indexOf(className)

        if (index === NOT_FOUND) {
            cl.add(className)
        }
    }
}

h.HtmlElement = HtmlElement

module.exports = h

function h(selector, attributes, children) {
    if (typeof attributes === "string" || Array.isArray(attributes)) {
        children = attributes
        attributes = {}
    }

    var hElement = new HtmlElement("", {
        classes: [], dataset: {}, focus: false
    }, {}, [])

    unpackAttributes(hElement, attributes)
    unpackSelector(hElement, selector)
    unpackChildren(hElement, children)

    return new Element(hElement, -1, -1)
}

function unpackAttributes(hElement, attributes) {
    if (!attributes) {
        return
    }

    var special = hElement.specialProperties
    var dataset = special.dataset
    var classes = special.classes
    var general = hElement.generalProperties

    // TODO: Handle special attributes and put them in special hash
    var attributesKeys = Object.keys(attributes)
    for (var i = 0; i < attributesKeys.length; i++) {
        var key = attributesKeys[i]
        var attribute = attributes[key]

        if (key.indexOf("data-") === 0) {
            var attr = key.substr(5)
            dataset[attr] = attribute
        } else if (key === "focus") {
            special.focus = attribute
        } else {
            general[key] = attribute
        }
    }
}

function unpackSelector(hElement, selector) {
    var matches = selector.split(splitSelectorRegex)
    var classes = hElement.specialProperties.classes
    var general = hElement.generalProperties

    for (var j = 0; j < matches.length; j++) {
        var match = matches[j]
        var value = match.substring(1, match.length)

        if (match[0] === ".") {
            classes.push(value)
        } else if (match[0] === "#") {
            general.id = value
        } else if (match.length > 0) {
            hElement.tagName = match
        }
    }
}

function unpackChildren(hElement, children) {
    if (!children) {
        return
    }

    if (typeof children === "string") {
        children = [children]
    }

    var elChildren = hElement.children

    for (var k = 0; k < children.length; k++) {
        var child = children[k]

        if (typeof child === "string") {
            children[k] = new TextElement(child)
        }
    }

    hElement.children = children
}


/// <reference path="common.js"/>
function createLexer(g) {
	function Token(tag, text, index, subMatches, end, pos) {
		this.tag = tag;
		this.text = text;
		this.index = index;
		this.subMatches = subMatches;
		this.end = end;
		this.pos = pos;
	}
	Token.prototype.toString = function() {
		return this.text;
	};
	function emptyFunc() { }
	function tofn(f) {
		if (typeof f == 'function') return f;
		return function() {
			return f;
		};
	}
	function buildScanner(a) {
		var n = 1;
		var b = [];
		var matchIndexes = [1];
		var fa = [];
		for (var i = 0; i < a.length; ++i) {
			matchIndexes.push(n += RegExp('|' + a[i][0].source).exec('').length);
			fa.push(a[i][1] ? tofn(a[i][1]) : emptyFunc);
			b.push('(' + a[i][0].source + ')');
		}

		var re = RegExp(b.join('|') + '|', 'g');
		return [re, matchIndexes, fa];
	}

	var endTag = g.$ || '$';
	var scanner = {};
	for (var i in g) {
		if (i.charAt(0) != '$')
			scanner[i] = buildScanner(g[i]);
	}

	return Lexer;
	function Lexer(s) {
		/// <param name="s" type="String"></param>
		var Length = s.length;
		var i = 0;
		var stateStack = [''];

		var obj = {
			text: '',
			index: 0,
			source: s,
			pushState: function(s) {
				stateStack.push(s);
			},
			popState: function() {
				stateStack.pop();
			},
			retract: function(n) {
				i -= n;
			}
		};
		var currentPos = new Position(1, 1);

		function scan() {
			var st = stateStack[stateStack.length - 1];
			var rule = scanner[st];
			var re = rule[0];
			re.lastIndex = i;
			var t = re.exec(s);
			if (t[0] == '') {
				if (i < Length) {
					throw Error('lexer error: ' + currentPos +
						'\n' + s.slice(i, i + 50));
				}
				return new Token(endTag, '', i, null, i, currentPos);
			}
			obj.index = i;
			i = re.lastIndex;
			var idx = rule[1];
			for (var j = 0; j < idx.length; ++j)
				if (t[idx[j]]) {
					var tag = rule[2][j].apply(obj, t.slice(idx[j], idx[j + 1]));
					//if (tag == null) return null;
					return new Token(tag, t[0], obj.index, t.slice(idx[j] + 1, idx[j + 1]), i, currentPos);
				}
		}

		var re_newLine = /\r\n?|\n/g;
		var re_lastLine = /[^\r\n\u2028\u2029]*$/;
		return {
			scan: function() {
				do {
					var t = scan();
					var _row = currentPos.row;
					var _col = currentPos.col;
					var ms = t.text.match(re_newLine);
					var h = ms ? ms.length : 0;
					_row += h;
					if (h == 0) _col += t.text.length;
					else _col = re_lastLine.exec(t.text)[0].length + 1;
					currentPos = new Position(_row, _col);
					if (t.tag != null) {
						return t;
					}
				} while (true);
			},
			GetCurrentPosition: function() {
				return currentPos;
			},
			getPos: function(i) {
				return getPos(s, i);
			}
		};
	}
}


/**
 * @preserve Crox v1.4.9
 * https://github.com/thx/crox
 *
 * Released under the MIT license
 * md5: 2c4b3337e9079e6be8238404a8181725
 */
(function(root) {var Crox = (function() {
function Class(base, constructor, methods) {
	/// <param name="base" type="Function"></param>
	/// <param name="constructor" type="Function"></param>
	/// <param name="prototype" type="Object" optional="true"></param>
	function f() { }
	f.prototype = base.prototype;
	var t = new f;
	if (methods) {
		for (var i in methods)
			t[i] = methods[i];
	}
	if (!constructor)
		constructor = f;
	constructor.prototype = t;
	return constructor;
}

function Position(row, col) {
	this.row = row;
	this.col = col;
}
Position.prototype.toString = function() {
	return '(' + this.row + ',' + this.col + ')';
};

function getPos(s, index) {
	/// <summary>取得字符串中某个位置所在的行列</summary>
	/// <param name="s" type="String"></param>
	/// <param name="index" type="Number"></param>
	var t = s.substring(0, index);
	var re_nl = /\r\n?|\n/g;
	var m = t.match(re_nl);
	var row = 1;
	if (m) {
		row += m.length;
	}
	var col = 1 + /[^\r\n]*$/.exec(t)[0].length;
	return new Position(row, col);
}

function Enum(arr) {
	/// <param name="arr" type="Array"></param>
	var obj = {};
	for (var i = 0; i < arr.length; ++i)
		obj[arr[i]] = arr[i];
	return obj;
}

function inArr(a, t) {
	/// <param name="a" type="Array"></param>
	for (var i = 0; i < a.length; ++i)
		if (a[i] == t)
			return i;
	return -1;
}
function inArr_strict(a, t) {
	/// <param name="a" type="Array"></param>
	for (var i = 0; i < a.length; ++i)
		if (a[i] === t)
			return i;
	return -1;
}
function nodup(a, eq) {
	/// <param name="a" type="Array"></param>
	/// <param name="eq" type="Function" optional="true">比较函数</param>
	if (!eq) eq = function(a, b) { return a === b; };
	var b = [];
	var n = a.length;
	for (var i = 0; i < n; i++) {
		for (var j = i + 1; j < n; j++)
			if (eq(a[i], a[j]))
				j = ++i;
		b.push(a[i]);
	}
	return b;
}
function htmlEncode(s) {
	/// <param name="s" type="String"></param>
	/// <returns type="String" />
	return String(s).replace(/[&<>"]/g, function(a) {
		switch (a) {
			case '&': return '&';
			case '<': return '<';
			case '>': return '>';
			default: return '"';
		}
	});
}
function quote(s) {
	/// <param name="s" type="String"></param>
	/// <returns type="String" />
	return '"' + (s).replace(/[\x00-\x1f"\\\u2028\u2029]/g, function(a) {
		switch (a) {
			case '"': return '\\"';
			case '\\': return '\\\\';
			case '\b': return '\\b';
			case '\f': return '\\f';
			case '\n': return '\\n';
			case '\r': return '\\r';
			case '\t': return '\\t';
		}
		return '\\u' + ('000' + a.charCodeAt(0).toString(16)).slice(-4);
	}) + '"';
}
function singleQuote(s) {
	/// <param name="s" type="String"></param>
	/// <returns type="String" />
	return "'" + (s).replace(/[\x00-\x1f'\\\u2028\u2029]/g, function(a) {
		switch (a) {
			case "'": return "\\'";
			case '\\': return '\\\\';
			case '\b': return '\\b';
			case '\f': return '\\f';
			case '\n': return '\\n';
			case '\r': return '\\r';
			case '\t': return '\\t';
		}
		return '\\u' + ('000' + a.charCodeAt(0).toString(16)).slice(-4);
	}) + "'";
}
function phpQuote(s) {
	/// <param name="s" type="String"></param>
	/// <returns type="String" />
	return "'" + String(s).replace(/['\\]/g, '\\$&') + "'";
}
function evalNum(s) {
	return +s;
}
function evalStr(s) {
	return eval(s);
}

function encodeCommonName(s) {
	/// <param name="s" type="String"></param>
	return s.replace(/^_+/, '$&$&');
}

/// <reference path="common.js"/>
function createLexer(g) {
	function Token(tag, text, index, subMatches, end, pos) {
		this.tag = tag;
		this.text = text;
		this.index = index;
		this.subMatches = subMatches;
		this.end = end;
		this.pos = pos;
	}
	Token.prototype.toString = function() {
		return this.text;
	};
	function emptyFunc() { }
	function tofn(f) {
		if (typeof f == 'function') return f;
		return function() {
			return f;
		};
	}
	function buildScanner(a) {
		var n = 1;
		var b = [];
		var matchIndexes = [1];
		var fa = [];
		for (var i = 0; i < a.length; ++i) {
			matchIndexes.push(n += RegExp('|' + a[i][0].source).exec('').length);
			fa.push(a[i][1] ? tofn(a[i][1]) : emptyFunc);
			b.push('(' + a[i][0].source + ')');
		}

		var re = RegExp(b.join('|') + '|', 'g');
		return [re, matchIndexes, fa];
	}

	var endTag = g.$ || '$';
	var scanner = {};
	for (var i in g) {
		if (i.charAt(0) != '$')
			scanner[i] = buildScanner(g[i]);
	}

	return Lexer;
	function Lexer(s) {
		/// <param name="s" type="String"></param>
		var Length = s.length;
		var i = 0;
		var stateStack = [''];

		var obj = {
			text: '',
			index: 0,
			source: s,
			pushState: function(s) {
				stateStack.push(s);
			},
			popState: function() {
				stateStack.pop();
			},
			retract: function(n) {
				i -= n;
			}
		};
		var currentPos = new Position(1, 1);

		function scan() {
			var st = stateStack[stateStack.length - 1];
			var rule = scanner[st];
			var re = rule[0];
			re.lastIndex = i;
			var t = re.exec(s);
			if (t[0] == '') {
				if (i < Length) {
					throw Error('lexer error: ' + currentPos +
						'\n' + s.slice(i, i + 50));
				}
				return new Token(endTag, '', i, null, i, currentPos);
			}
			obj.index = i;
			i = re.lastIndex;
			var idx = rule[1];
			for (var j = 0; j < idx.length; ++j)
				if (t[idx[j]]) {
					var tag = rule[2][j].apply(obj, t.slice(idx[j], idx[j + 1]));
					//if (tag == null) return null;
					return new Token(tag, t[0], obj.index, t.slice(idx[j] + 1, idx[j + 1]), i, currentPos);
				}
		}

		var re_newLine = /\r\n?|\n/g;
		var re_lastLine = /[^\r\n\u2028\u2029]*$/;
		return {
			scan: function() {
				do {
					var t = scan();
					var _row = currentPos.row;
					var _col = currentPos.col;
					var ms = t.text.match(re_newLine);
					var h = ms ? ms.length : 0;
					_row += h;
					if (h == 0) _col += t.text.length;
					else _col = re_lastLine.exec(t.text)[0].length + 1;
					currentPos = new Position(_row, _col);
					if (t.tag != null) {
						return t;
					}
				} while (true);
			},
			GetCurrentPosition: function() {
				return currentPos;
			},
			getPos: function(i) {
				return getPos(s, i);
			}
		};
	}
}

/// <reference path="createLexer.js"/>
var Lexer = function() {
	var re_id = /[A-Za-z_]\w*/;
	var re_str = /"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'/;
	var re_num = /\d+(?:\.\d+)?(?:e-?\d+)?/;

	function isReserved(s) {
		return " abstract boolean break byte case catch char class const continue debugger default delete do double else enum export extends final finally float for function goto if implements import in instanceof int interface let long native new package private protected public return short static super switch synchronized this throw throws transient try typeof var void volatile while with yield ".indexOf(' ' + s + ' ') != -1;
	}
	var code = [
		[/\s+/],
		[/\/\/[^\r\n]*|\/\*[\s\S]*?\*\//],
		[re_id, function(a) {
			switch (a) {
				case 'true':
				case 'false':
					return 'boolean';
				case 'set':
				case 'include':
				case 'null':
					return a;
				default:
					if (isReserved(a) || a == 'null') throw Error("Reserved: " + a + ' ' + getPos(this.source, this.index));
					return 'realId';
			}
		}],
		[re_str, function(a) {
			return 'string';
		}],
		[re_num, function(a) {
			return 'number';
		}],
		[/{(?!{)/, function(a) { return '{'; }],
		[/}(?!})/, function(a) { return '}'; }],
		[function(a) {
			a.sort().reverse();
			for (var i = 0; i < a.length; ++i)
				a[i] = a[i].replace(/[()*+?.[\]|]/g, '\\$&');
			return RegExp(a.join('|'));
		}(["!", "%", "&&", "(", ")", "*", "+", "-", ".", "/", "<", "<=", "=", ">", ">=", "[", "]", "||", "===", "!==", "==", "!=", ",", ":", "?"]), function(a) {
			return /[*/%]/.test(a) ? 'mul' : /[<>]/.test(a) ? 'rel' : /[!=]=/.test(a) ? 'eq' : a;
		}]
	];

	var Lexer = createLexer({
		'': [
			[/(?:(?!{{)[\s\S])+/, function(a) {
				return 'text';
			}],
			[/{{{/, function(a) {
				this.pushState(a);
				return a;
			}],
			[/{{(?:\/if|else|\/each|\/forin|\/raw)}}/, function(a) {
				return a;
			}],
			[/{{#raw}}/, function(a) {
				this.pushState('raw');
				return a;
			}],
			[/{{<script>[\s\S]*?<\/script>}}/, function(a) {
				return 'script';
			}],
			[/{{\*[\s\S]*?\*}}/],
			[/{{(?!\*)(?:#(?:if|each|forin)(?=\s))?/, function(a) {
				this.pushState('{{');
				return a;
			}]
		],
		raw: [
			[/(?:(?!{{\/raw}})[\s\S])+/, function(a) {
				this.popState();
				return 'rawtext';
			}]
		],
		'{{': code.concat([
			[/}}/, function(a) {
				this.popState();
				return a;
			}]
		]),
		'{{{': code.concat([
			[/}}}/, function(a) {
				this.popState();
				return a;
			}]
		])
	});
	return Lexer;
}();

var parse = function() {
	var table = { "nStart": 43, "tSymbols": ["$", "!", "\u0026\u0026", "(", ")", "+", ",", "-", ".", ":", "=", "?", "[", "]", "boolean", "eq", "include", "mul", "null", "number", "rawtext", "realId", "rel", "script", "set", "string", "text", "{", "{{", "{{#each", "{{#forin", "{{#if", "{{#raw}}", "{{/each}}", "{{/forin}}", "{{/if}}", "{{/raw}}", "{{else}}", "{{{", "||", "}", "}}", "}}}", "AdditiveExpression", "ArrayLiteral", "ConditionalExpression", "ElementList", "Elision", "EqualityExpression", "LogicalAndExpression", "LogicalOrExpression", "MemberExpression", "MultiplicativeExpression", "ObjectLiteral", "PrimaryExpression", "PropertyAssignment", "PropertyNameAndValueList", "RelationalExpression", "UnaryExpression", "_text", "args", "empty", "expr", "id", "name", "program", "statement", "statements", "texts", "program\u0027"], "actionList": [{ "_": -1 }, { "23": 10, "26": 11, "28": 8, "29": 6, "30": 7, "31": 5, "32": 12, "38": 9, "_": -32768 }, { "_": -2 }, { "26": 11, "32": 12, "_": -14 }, { "_": -19 }, { "1": 38, "3": 34, "7": 39, "12": 36, "14": 33, "16": 29, "18": 35, "19": 32, "21": 31, "24": 28, "25": 30, "27": 37, "_": 0 }, { "1": 38, "3": 34, "7": 39, "12": 36, "14": 33, "16": 44, "18": 35, "19": 32, "21": 31, "24": 43, "25": 30, "27": 37, "_": 0 }, { "_": -16 }, { "_": -21 }, { "20": 46, "_": 0 }, { "_": -20 }, { "_": -29 }, { "_": -47 }, { "_": -31 }, { "_": -32 }, { "3": 47, "8": 49, "12": 48, "_": -54 }, { "_": -57 }, { "17": 50, "_": -59 }, { "5": 52, "7": 51, "_": -62 }, { "22": 53, "_": -64 }, { "15": 54, "_": -66 }, { "2": 55, "_": -68 }, { "11": 57, "39": 56, "_": -70 }, { "_": -72 }, { "41": 58, "_": 0 }, { "_": -24 }, { "_": -25 }, { "_": -26 }, { "_": -23 }, { "_": -27 }, { "_": -28 }, { "_": -33 }, { "6": 64, "13": 63, "_": -73 }, { "16": 29, "21": 31, "24": 28, "25": 69, "40": 70, "_": 0 }, { "16": 29, "21": 31, "24": 28, "25": 69, "_": 0 }, { "41": 75, "_": 0 }, { "3": 34, "12": 36, "14": 33, "16": 29, "18": 35, "19": 32, "21": 31, "24": 28, "25": 30, "27": 37, "_": -24 }, { "25": 77, "_": -25 }, { "42": 78, "_": 0 }, { "36": 79, "_": 0 }, { "1": 38, "3": 34, "7": 39, "12": 36, "14": 33, "16": 29, "18": 35, "19": 32, "21": 31, "24": 28, "25": 30, "27": 37, "_": -73 }, { "16": 29, "21": 31, "24": 28, "_": 0 }, { "_": -3 }, { "4": 94, "_": 0 }, { "6": 96, "13": 95, "_": 0 }, { "1": 38, "3": 34, "6": 98, "7": 39, "12": 36, "14": 33, "16": 29, "18": 35, "19": 32, "21": 31, "24": 28, "25": 30, "27": 37, "_": 0 }, { "_": -34 }, { "_": -40 }, { "9": 100, "_": 0 }, { "_": -18 }, { "6": 101, "40": 102, "_": 0 }, { "_": -44 }, { "_": -17 }, { "_": -42 }, { "_": -55 }, { "_": -56 }, { "16": 29, "21": 31, "24": 28, "25": 69, "_": -73 }, { "_": -12 }, { "3": 47, "8": 49, "10": 107, "12": 48, "_": 0 }, { "41": 108, "_": 0 }, { "_": -13 }, { "_": -22 }, { "4": 109, "6": 110, "_": 0 }, { "_": -52 }, { "4": 111, "_": 0 }, { "13": 112, "_": 0 }, { "_": -48 }, { "_": -58 }, { "17": 50, "_": -61 }, { "17": 50, "_": -60 }, { "5": 52, "7": 51, "_": -63 }, { "22": 53, "_": -65 }, { "15": 54, "_": -67 }, { "2": 55, "_": -69 }, { "9": 113, "_": 0 }, { "23": 10, "26": 11, "28": 8, "29": 6, "30": 7, "31": 5, "32": 12, "35": 115, "37": 116, "38": 9, "_": 0 }, { "_": -30 }, { "_": -35 }, { "6": 64, "_": -73 }, { "_": -36 }, { "_": -41 }, { "_": -37 }, { "_": -43 }, { "41": 121, "_": 0 }, { "41": 122, "_": 0 }, { "41": 123, "_": 0 }, { "41": 124, "_": 0 }, { "_": -15 }, { "_": -50 }, { "_": -51 }, { "_": -49 }, { "_": -4 }, { "_": -5 }, { "_": -3 }, { "_": -46 }, { "_": -45 }, { "_": -3 }, { "_": -3 }, { "41": 135, "_": 0 }, { "_": -53 }, { "_": -71 }, { "23": 10, "26": 11, "28": 8, "29": 6, "30": 7, "31": 5, "32": 12, "35": 136, "38": 9, "_": 0 }, { "_": -38 }, { "_": -39 }, { "23": 10, "26": 11, "28": 8, "29": 6, "30": 7, "31": 5, "32": 12, "33": 137, "38": 9, "_": 0 }, { "23": 10, "26": 11, "28": 8, "29": 6, "30": 7, "31": 5, "32": 12, "33": 138, "38": 9, "_": 0 }, { "23": 10, "26": 11, "28": 8, "29": 6, "30": 7, "31": 5, "32": 12, "34": 139, "38": 9, "_": 0 }, { "23": 10, "26": 11, "28": 8, "29": 6, "30": 7, "31": 5, "32": 12, "34": 140, "38": 9, "_": 0 }, { "_": -11 }, { "_": -6 }, { "_": -7 }, { "_": -8 }, { "_": -9 }, { "_": -10 }], "actionIndex": [0, 1, 2, 3, 4, 5, 5, 5, 6, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 5, 31, 32, 33, 5, 5, 34, 34, 35, 36, 37, 38, 39, 40, 5, 41, 5, 5, 5, 5, 5, 5, 5, 5, 42, 43, 44, 45, 5, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 5, 34, 82, 83, 84, 85, 86, 5, 87, 88, 5, 89, 90, 5, 91, 92, 93, 45, 5, 94, 95, 96, 96, 97, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113], "tGoto": [{ "65": 1 }, { "66": 2, "68": 3, "59": 4 }, {}, { "59": 13 }, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 27 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 40 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 41 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 42 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 45 }, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 59 }, {}, { "46": 60, "47": 61, "61": 62 }, { "64": 65, "63": 66, "56": 67, "55": 68 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 71 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 72 }, { "64": 73, "63": 66 }, { "64": 74, "63": 66 }, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 76 }, {}, {}, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "60": 80, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 81, "61": 82 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 83 }, { "63": 84 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 85 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 86 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 87 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 88 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 89 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 90 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 91 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 92 }, { "67": 93 }, {}, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 97 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 99 }, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, { "64": 103, "63": 66, "61": 104 }, { "64": 105, "63": 66, "61": 106 }, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, { "66": 114, "68": 3, "59": 4 }, {}, {}, { "47": 117, "61": 118 }, {}, {}, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 119 }, { "64": 65, "63": 66, "55": 120 }, {}, {}, {}, {}, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 125 }, {}, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 126 }, {}, {}, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 127 }, {}, {}, { "67": 128 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 129 }, { "63": 14, "54": 15, "44": 16, "53": 17, "51": 18, "58": 19, "52": 20, "43": 21, "57": 22, "48": 23, "49": 24, "50": 25, "45": 26, "62": 130 }, {}, {}, { "67": 131 }, { "67": 132 }, { "67": 133 }, { "67": 134 }, {}, {}, {}, { "66": 114, "68": 3, "59": 4 }, {}, {}, { "66": 114, "68": 3, "59": 4 }, { "66": 114, "68": 3, "59": 4 }, { "66": 114, "68": 3, "59": 4 }, { "66": 114, "68": 3, "59": 4 }, {}, {}, {}, {}, {}, {}], "tRules": [[69, 65], [65], [65, 65, 66], [67], [67, 67, 66], [66, 31, 62, 41, 67, 35], [66, 31, 62, 41, 67, 37, 67, 35], [66, 29, 62, 64, 64, 41, 67, 33], [66, 29, 62, 64, 61, 41, 67, 33], [66, 30, 62, 64, 64, 41, 67, 34], [66, 30, 62, 64, 61, 41, 67, 34], [66, 28, 24, 51, 10, 62, 41], [66, 28, 62, 41], [66, 38, 62, 42], [66, 68], [66, 28, 16, 25, 41], [66, 23], [64, 25], [64, 63], [68, 59], [68, 68, 59], [59, 26], [59, 32, 20, 36], [63, 21], [63, 24], [63, 16], [54, 25], [54, 19], [54, 14], [54, 63], [54, 3, 62, 4], [54, 44], [54, 53], [54, 18], [44, 12, 13], [44, 12, 46, 13], [46, 47, 62], [46, 61, 62], [46, 46, 6, 47, 62], [46, 46, 6, 61, 62], [47, 6], [47, 47, 6], [53, 27, 40], [53, 27, 56, 40], [56, 55], [56, 56, 6, 55], [55, 64, 9, 62], [51, 54], [51, 51, 8, 63], [51, 51, 12, 62, 13], [51, 51, 3, 60, 4], [51, 51, 3, 61, 4], [60, 62], [60, 60, 6, 62], [58, 51], [58, 1, 58], [58, 7, 58], [52, 58], [52, 52, 17, 58], [43, 52], [43, 43, 5, 52], [43, 43, 7, 52], [57, 43], [57, 57, 22, 43], [48, 57], [48, 48, 15, 57], [49, 48], [49, 49, 2, 48], [50, 49], [50, 50, 39, 49], [45, 50], [45, 50, 11, 45, 9, 45], [62, 45], [61]], "objCharset": null };
	function parse(lexer, extra) {
		var nStart = table.nStart;
		var tSymbols = table.tSymbols;
		var tSymbolIndex = {};
		for (var i = 0; i < tSymbols.length; ++i)
			tSymbolIndex[tSymbols[i]] = i;
		var tAction = table.tAction || table.actionList;
		var tGoto = table.tGoto;
		var tRules = table.tRules;
		var tCodes = table.tCodes;
		var actionIndex = table.actionIndex;
		function $f0($1, $2, $3, $4, $5, $6, $7) { var $$; { $$ = ['each', $2, $6, $4, $3, true]; } return $$; }
		function $f1($1, $2, $3, $4, $5, $6, $7) { var $$; { $$ = ['each', $2, $6, $4, $3, false]; } return $$; }
		function $f2($1) { var $$; { $$ = $1.text; } return $$; }
		function $f3($1, $2) { var $$; { $$ = Array($1 || 0); $$.push($2); } return $$; }
		function $f4($1, $2, $3, $4) { var $$; { if ($3) $1.length += $3; ($$ = $1).push($4); } return $$; }
		function $f5($1) { var $$; { $$ = [$1]; } return $$; }
		function $f6($1, $2, $3, $4) { var $$; { $$ = ['()', $1, $3]; } return $$; }
		function $f7($1, $2, $3) { var $$; { $$ = [$2.text, $1, $3]; } return $$; } var tFuncs = [, function() { var $$; { $$ = ['prog', [], []]; } return $$; }, function($1, $2) { var $$; { $1[1].push($2); $$ = $1; } return $$; }, function() { var $$; { $$ = []; } return $$; }, function($1, $2) { var $$; { $1.push($2); $$ = $1; } return $$; }, function($1, $2, $3, $4, $5) { var $$; { $$ = ['if', $2, $4]; } return $$; }, function($1, $2, $3, $4, $5, $6, $7) { var $$; { $$ = ['if', $2, $4, $6]; } return $$; }, $f0, $f0, $f1, $f1, function($1, $2, $3, $4, $5, $6) { var $$; { $$ = ['set', $3[0] == 'id' ? $3[1] : $3, $5]; } return $$; }, function($1, $2, $3) { var $$; { $$ = ['eval', $2, false]; } return $$; }, function($1, $2, $3) { var $$; { $$ = ['eval', $2, true]; } return $$; }, function($1) { var $$; { $$ = ['text', $1]; } return $$; }, function($1, $2, $3, $4) { var $$; { $$ = ['inc', evalStr($3.text)]; } return $$; }, function($1) { var $$; { $$ = ['script', $1.text.slice(10, -11)] } return $$; }, function($1) { var $$; { $$ = evalStr($1.text); } return $$; }, $f2, function($1) { var $$; { $$ = $1; } return $$; }, function($1, $2) { var $$; { $$ = $1 + $2; } return $$; }, $f2, function($1, $2, $3) { var $$; { $$ = $2.text; } return $$; }, , , , function($1) { var $$; { $$ = ['lit', evalStr($1.text)]; } return $$; }, function($1) { var $$; { $$ = ['lit', evalNum($1.text)]; } return $$; }, function($1) { var $$; { $$ = ['lit', $1.text == 'true']; } return $$; }, function($1) { var $$; { $$ = ['id', $1.text]; } return $$; }, function($1, $2, $3) { var $$; { $$ = $2; } return $$; }, , , function($1) { var $$; { $$ = ['null']; } return $$; }, function($1, $2) { var $$; { $$ = ['array', []]; } return $$; }, function($1, $2, $3) { var $$; { $$ = ['array', $2]; } return $$; }, $f3, $f3, $f4, $f4, function($1) { var $$; { $$ = 1; } return $$; }, function($1, $2) { var $$; { $$ = $1 + 1; } return $$; }, function($1, $2) { var $$; { $$ = ['object', []]; } return $$; }, function($1, $2, $3) { var $$; { $$ = ['object', $2]; } return $$; }, $f5, function($1, $2, $3) { var $$; { $1.push($3); $$ = $1; } return $$; }, function($1, $2, $3) { var $$; { $$ = ['init', $1, $3]; } return $$; }, , function($1, $2, $3) { var $$; { $$ = ['.', $1, $3.text]; } return $$; }, function($1, $2, $3, $4) { var $$; { $$ = ['[]', $1, $3]; } return $$; }, $f6, $f6, $f5, function($1, $2, $3) { var $$; { ($$ = $1).push($3); } return $$; }, , function($1, $2) { var $$; { $$ = ['!', $2]; } return $$; }, function($1, $2) { var $$; { $$ = ['u-', $2]; } return $$; }, , $f7, , $f7, $f7, , $f7, , $f7, , $f7, , $f7, , function($1, $2, $3, $4, $5) { var $$; { $$ = ['cond', $1, $3, $5]; } return $$; }];
		function getAction(x, y) {
			return tAction[x][y];
		}
		var stateNum = tAction.length;

		if (actionIndex) {
			getAction = function(x, y) {
				var list = tAction[actionIndex[x]];
				return list[y] || list._;
			};
			stateNum = actionIndex.length;
		}

		function getToken() {
			var t = lexer.scan();
			return t;
		}

		var s = 0;
		var stateStack = [0];
		var a = getToken();
		var valueStack = [];
		var obj = {
			get: function(i) {
				return valueStack[valueStack.length + i];
			},
			set: function(i, v) {
				valueStack[valueStack.length + i] = v;
			}
		};
		if (extra) for (var i in extra)
			obj[i] = extra[i];
		var $smb = valueStack;

		while (1) {
			var $top = $smb.length - 1;
			var t = getAction(s, tSymbolIndex[a.tag]);
			if (!t) {
				break;
			}
			else if (t > 0) {
				stateStack.push(s = t);
				valueStack.push(a);
				a = getToken();
			}
			else if (t < 0 && t > -32768) {
				var idx = -t;
				var p = tRules[idx];
				var num = p.length - 1;
				stateStack.length -= num;
				s = tGoto[stateStack[stateStack.length - 1]][p[0]];
				stateStack.push(s);

				if (tFuncs[idx]) {
					var val = tFuncs[idx].apply(obj, valueStack.splice(valueStack.length - num, num));
					valueStack.push(val);
				}
				else if (num != 1) {
					valueStack.splice(valueStack.length - num, num, null);
				}
			}
			else {
				if (a.tag != tSymbols[0]) break;
				return valueStack[0];
			}
		}
		var okTokens = [];
		for (var j = 0; j < nStart; ++j) {
			if (getAction(s, j))
				okTokens.push(tSymbols[j]);
		}
		throw Error('Syntax error: ' + a.pos + '\nfind ' + a.tag + '\nexpect ' + okTokens.join(' '));
	}
	return parse;
}();

function isAtom(op) {
	switch (op) {
		case 'id':
		case 'lit':
		case 't':
			return true;
	}
	return false;
}
function isMember(op) {
	return isAtom(op) || op == '.' || op == '[]';
}

function isUnary(op) {
	return isMember(op) || op == '!' || op == 'u-';
}
function isMul(op) {
	if (isUnary(op)) return true;
	switch (op) {
		case '*': case '/': case '%':
			return true;
	}
	return false;
}
function isAdd(op) {
	if (isMul(op)) return true;
	switch (op) {
		case '+': case '-':
			return true;
	}
	return false;
}
function isRel(op) {
	if (isAdd(op)) return true;
	switch (op) {
		case '<': case '>': case '<=': case '>=':
			return true;
	}
	return false;
}
function isEquality(op) {
	if (isRel(op)) return true;
	switch (op) {
		case 'eq': case 'ne':
			return true;
	}
	return false;
}
function isLogicalAnd(op) {
	return isEquality(op) || op == '&&';
}
function isLogicalOr(op) {
	return isLogicalAnd(op) || op == '||';
}
function changeExt(s, ext) {
	return s.replace(/\.\w+$/, '.' + ext);
}
function isCond(op) {
	return isLogicalOr(op) || op == 'cond';
}

/// <reference path="common.js"/>
/// <reference path="codegen_common.js"/>
function codegen_js_tran(prog, encodeName, defaultEncode, ignoreWhitespace) {
	/// <param name="prog" type="Array">AST</param>
	/// <param name="encodeName" type="String"></param>
	/// <param name="defaultEncode" type="Boolean"></param>
	/// <returns type="String" />

	var i_tmake = 0;
	function TMake() {
		return '_' + (i_tmake++);
	}

	function emit(s) {
		body.push(s);
	}
	function nodeWithPos(node, pos) {
		node.pos = pos;
		return node;
	}

	function stmtGen(a) {
		switch (a[0]) {
			case 'if':
				emit('if(');
				emit(exprGen(a[1]));
				emit('){');
				stmtsGen(a[2]);
				emit('}');
				if (a[3]) {
					emit('else{');
					stmtsGen(a[3]);
					emit('}');
				}
				break;
			case 'each':
				var keyName = a[3] ? encodeCommonName(a[3]) : TMake();
				var tmpExpr = exprGen(a[1]);
				var tmpStr = joinCode(tmpExpr);
				if (/^\w+$/.test(tmpStr)) {
					var listName = tmpStr;
				}
				else {
					listName = TMake();
					emit('var ' + listName + ' = ');
					emit(tmpExpr);
					emit(';');
				}
				if (a[5]) {
					emit('for(var ' + keyName + '=0;' + keyName + '<');
					//listName + '.length'
					emit(exprGen(['.', nodeWithPos(['t', listName], a[1].pos), 'length']));
					emit(';' + keyName + '++){');
				}
				else emit('for(var ' + keyName + ' in ' + listName + ') {');
				emit('var ' + a[4] + ' = ');
				//listName + '[' + keyName + ']'
				emit(exprGen(['[]', nodeWithPos(['t', listName], a[1].pos), ['t', keyName]]));
				emit(';');
				stmtsGen(a[2]);
				emit('}');
				break;
			case 'set':
				if (typeof a[1] == 'string')
					emit('var ' + encodeCommonName(a[1]) + '=');
				else {
					emit(exprGen(a[1]));
					emit('=');
				}
				emit(exprGen(a[2]));
				emit(';');
				break;
			case 'eval':
				var tmpExpr = exprGen(a[1]);
				var tmpStr = joinCode(tmpExpr);
				if (/^\w+$/.test(tmpStr))
					var tName = tmpStr;
				else {
					tName = '_t';
					emit('_t = ');
					emit(tmpExpr);
					emit(';');
				}
				emit('if(' + tName + ' !=null)_s += ' + ((defaultEncode ? !a[2] : a[2]) ? encodeName + '(' + tName + ')' : tName) + ';');
				break;
			case 'text':
				if (ignoreWhitespace) {
					if (/^\s+$/.test(a[1])) break;
				}
				emit('_s += ' + quote(a[1]) + ';');
				break;
			case 'inc':
				//stmtsGen(a[2][1]);
				break;
			case 'script':
				scripts.push(a[1]);
				break;
			default:
				throw Error('unknown stmt: ' + a[0]);
		}
	}
	function stmtsGen(a) {
		for (var i = 0; i < a.length; ++i)
			stmtGen(a[i]);
	}

	function joinCode(a) {
		if (typeof a == 'string')
			return a;
		if (a instanceof Array) {
			var r = [];
			for (var i = 0; i < a.length; ++i) {
				r.push(joinCode(a[i]));
			}
			return r.join('');
		}
		throw new Error("unknown type");
	}
	function exprToStr(x, check) {
		var t = exprGen(x);
		if (check && !check(x[0])) t = ['(', t, ')'];
		return t;
	}
	function exprGen(x) {
		return nodeWithPos(exprGen_original(x), x.pos);
	}
	function exprGen_original(x) {
		switch (x[0]) {
			case 't':
				return x[1]; //临时变量直接返回
			case 'id':
				return encodeCommonName(x[1]);
			case 'lit':
				return (typeof x[1] == 'string')
					? quote(x[1])
				: String(x[1]);
			case 'array':
				var tmp = ['['];
				for (var i = 0; i < x[1].length; ++i) {
					if (i > 0) tmp.push(",");
					tmp.push(exprGen(x[1][i]));
				}
				tmp.push(']');
				return tmp;
			case 'object':
				var tmp = ['{'];
				for (var i = 0; i < x[1].length; ++i) {
					if (i > 0) tmp.push(",");
					tmp.push(quote(x[1][i][1]));
					tmp.push(':');
					tmp.push(exprGen(x[1][i][2]));
				}
				tmp.push('}');
				return tmp;
			case 'null':
				return ['null'];
			case '.':
				return [exprToStr(x[1], isMember), '.', x[2]];
			case '[]':
				return [exprToStr(x[1], isMember), '[', exprGen(x[2]), ']'];
			case '()':
				var a = [exprToStr(x[1], isMember), '('];
				if (x[2]) {
					for (var i = 0; i < x[2].length; ++i) {
						if (i > 0) a.push(',');
						a.push(exprGen(x[2][i]));
					}
				}
				a.push(')');
				return a;
			case '!':
				return ['!', exprToStr(x[1], isUnary)];
			case 'u-':
				return ['- ', exprToStr(x[1], isUnary)];
			case '*': case '/': case '%':
				return [exprToStr(x[1], isMul), x[0], exprToStr(x[2], isUnary)];
			case '+': case '-':
				return [exprToStr(x[1], isAdd), x[0], ' ', exprToStr(x[2], isMul)];
			case '<': case '>': case '<=': case '>=':
				return [exprToStr(x[1], isRel), x[0], exprToStr(x[2], isAdd)];
			case '==':
			case '!=':
			case '===':
			case '!==':
				return [exprToStr(x[1], isEquality), x[0], exprToStr(x[2], isRel)];
			case '&&':
				return [exprToStr(x[1], isLogicalAnd), '&&', exprToStr(x[2], isEquality)];
			case '||':
				return [exprToStr(x[1], isLogicalOr), '||', exprToStr(x[2], isLogicalAnd)];
			case 'cond':
				return [exprToStr(x[1], isLogicalOr), '?', exprToStr(x[2], isCond), ':', exprToStr(x[3], isCond)];
			default:
				throw Error("unknown expr: " + x[0]);
		}
	}

	var body = [];
	var scripts = [];
	stmtsGen(prog[1]);

	var posLog = [];

	var jsStr = '';
	function joinJsStr(a) {
		if (typeof a == 'string')
			jsStr += a;
		if (a instanceof Array) {
			if (a.pos) {
				posLog.push([jsStr.length, a.pos]);
			}
			for (var i = 0; i < a.length; ++i) {
				joinJsStr(a[i]);
			}
		}
	}
	joinJsStr(body);

	if (scripts.length) {
		jsStr += scripts.join(';');
	}
	//alert(posLog.join('\n'));
	var strObj = new String(jsStr);
	strObj.posLog = posLog;
	return strObj;
}

/// <reference path="codegen_js.js"/>
function parsetmpl(s) {
	/// <summary>解析模板，得到语法树</summary>
	/// <param name="s" type="String">模板</param>
	var ast = parse(Lexer(s));
	return ast;
}
function compile2jsfn(s, config) {
	/// <summary>编译模板，得到一个 js 函数</summary>
	/// <param name="config" type="Object" optional="true"></param>
	/// <param name="s" type="String">模板</param>
	/// <returns type="Function" />
	var ast = parsetmpl(s);
	var encodeName;
	if (config) encodeName = config.htmlEncode;
	var ignoreWhitespace = !!(config && config.ignoreWhitespace);
	s = codegen_js_tran(ast, encodeName || '_htmlEncode', true, ignoreWhitespace);
	var body = '';
	if (!encodeName)
		body = "var _obj = { '<': '<', '>': '>', '&': '&', '\"': '"' };\
	function _htmlEncode(s) {\
		return String(s).replace(/[<>&\"]/g, function(c) {\
			return _obj[c];\
		});\
	}";
	body += "var _t,_s = '';";
	if (config && config.debug) {
		var posLog = s.posLog;
		body += 'try{\n';
		body += 'eval(' + JSON.stringify(s) + ');';
		body += '}catch(_e){throw ' + function(e, posLog) {
			var msg = e.stack;
			var msgLines = msg.split(/\r\n?|\n/);
			var re_pos = /:(\d+):(\d+)\)$/m;
			var m = re_pos.exec(msgLines[1]);
			//var row = +m[1];
			//if (row != 1) throw Error("row != 1");
			var col = +m[2];
			var pos = null;
			for (var i = 0; i < posLog.length; ++i) {
				if (posLog[i][0] + 1 >= col) {
					pos = posLog[i][1];
					break;
				}
			}
			//if (pos == null) throw Error("pos == null");
			function posToString(pos) {
				return '(' + pos.row + ',' + pos.col + ')';
			}
			return Error('CroxError: ' + posToString(pos.pos) + '\n' + msg);
		} + '(_e,' + JSON.stringify(posLog) + ');}';
	}
	else {
		body += s;
	}
	body += "return _s;";

	var f = Function('root', body);
	return f;
}
var Crox = {
	parse: parsetmpl,
	compile: compile2jsfn,
	render: function(s, data) {
		/// <summary>将数据 data 填充到模板 s</summary>
		/// <param name="s" type="String">模板</param>
		/// <returns type="String" />
		var fn = compile2jsfn(s);
		return fn(data);
	}
};

/// <reference path="common.js"/>
/// <reference path="codegen_common.js"/>
function codegen_php_tran(prog, defaultEncode) {
	/// <param name="prog" type="Array">AST</param>
	/// <param name="defaultEncode" type="Boolean"></param>
	/// <returns type="String" />

	//用户变量名 都奇数个下划线开头
	function encodeId(s) {
		return '$crox_' + encodeCommonName(s);
	}
	function emit(t) {
		s += t;
	}
	function compileEval(stmt) {
		var t = walkExpr(stmt[1]);
		emit('crox_echo(' + t + ', ' + (defaultEncode ? !stmt[2] : stmt[2]) + ');');
	}
	function compileContent(stmt) {
		var t = stmt[1];
		if (/<\?(?:php)?|\?>/.test(t))
			emit('echo ' + phpQuote(stmt[1]) + ';');
		else {
			emit('?>' + t + '<?php ');
		}
	}
	function compileIf(stmt) {
		emit('if(' + walkExpr(stmt[1]) + '){');
		compileStmts(stmt[2]);
		emit('}');
		if (stmt[3]) {
			emit('else{');
			compileStmts(stmt[3]);
			emit('}');
		}
	}
	function compileEach(stmt) {
		emit('foreach(' + walkExpr(stmt[1]) + ' as ' + (stmt[3] ? encodeId(stmt[3]) + '=>' : '') + encodeId(stmt[4]) + ')');
		emit('{');
		compileStmts(stmt[2]);
		emit('}');
	}
	function compileSet(stmt) {
		emit(encodeId(stmt[1]) + ' = ' + walkExpr(stmt[2]) + ';');
	}
	function compileStmt(a) {
		switch (a[0]) {
			case 'if': compileIf(a); break;
			case 'each': compileEach(a); break;
			case 'set': compileSet(a); break;
			case 'eval': compileEval(a); break;
			case 'text': compileContent(a); break;
			case 'inc':
				emit("include '" + changeExt(a[1], 'php') + "';");
				break;
			default: throw Error('unknown stmt: ' + a[0]);
		}
	}
	function compileStmts(a) {
		for (var i = 0; i < a.length; ++i)
			compileStmt(a[i]);
	}

	function exprToStr(x, check) {
		var t = walkExpr(x);
		if (check && !check(x[0])) t = '(' + t + ')';
		return t;
	}
	function walkExpr(x) {
		switch (x[0]) {
			case 'id':
				return encodeId(x[1]);
			case 'lit':
				if (typeof x[1] == 'string')
					return phpQuote(x[1]);
				return String(x[1]);
			case '.':
				return exprToStr(x[1], isMember) + "->" + x[2];
			case '[]':
				return exprToStr(x[1], isMember) + '[' + walkExpr(x[2]) + ']';
			case '!':
				return '!crox_ToBoolean(' + exprToStr(x[1], isUnary) + ')';
			case 'u-':
				return '- ' + exprToStr(x[1], isUnary);
			case '*': case '/': case '%':
				return exprToStr(x[1], isMul) + x[0] + exprToStr(x[2], isUnary);
			case '+':
				return 'crox_plus(' + exprToStr(x[1], null) + ', ' + exprToStr(x[2], null) + ')';
			case '-':
				return exprToStr(x[1], isAdd) + '- ' + exprToStr(x[2], isMul);
			case '<': case '>': case '<=': case '>=':
				return exprToStr(x[1], isRel) + x[0] + exprToStr(x[2], isAdd);
			case '==':
			case '!=':
			case '===':
			case '!==':
				return exprToStr(x[1], isEquality) + x[0] + exprToStr(x[2], isRel);
			case '&&':
				return 'crox_logical_and(' + exprToStr(x[1], null) + ', ' + exprToStr(x[2], null) + ')';
			case '||':
				return 'crox_logical_or(' + exprToStr(x[1], null) + ', ' + exprToStr(x[2], null) + ')';
			default:
				throw Error("unknown expr: " + x[0]);
		}
	}

	var s = "";
	compileStmts(prog[1]);
	if (s.slice(0, 2) == '?>')
		s = s.slice(2);
	else s = '<?php ' + s;
	if (s.slice(-6) == '<?php ')
		s = s.slice(0, -6);
	else s += '?>';
	return s;
}

/// <reference path="common.js"/>
/// <reference path="codegen_common.js"/>
function codegen_vm_tran(prog) {
	/// <param name="prog" type="Array">AST</param>
	/// <returns type="String" />

	//用户变量名 都奇数个下划线开头，临时变量都不下划线开头
	function encodeId(s) {
		return '$crox_' + encodeCommonName(s);
	}
	function isName(s) {
		return /^$\w+$/.test(s);
	}
	function emit(s) {
		body += s;
	}
	var i_each = 0;
	function stmtGen(a) {
		switch (a[0]) {
			case 'if':
				emit('#if(' + exprGen(a[1]) + ')');
				stmtsGen(a[2]);
				if (a[3]) {
					emit('#{else}');
					stmtsGen(a[3]);
				}
				emit('#{end}');
				break;
			case 'each':
				++i_each;
				var sExpr = exprGen(a[1]);
				if (isName(sExpr))
					var listName = sExpr;
				else {
					listName = '$list' + (i_each == 1 ? '' : i_each);
					emit('#set (' + listName + ' = ' + sExpr + ')');
				}
				if (a[5]) { //array
					emit('#foreach(' + encodeId(a[4]) + ' in ' + listName + ')');
					if (a[3]) {
						emit('#set(' + encodeId(a[3]) + ' = $velocityCount - 1)');
					}
				}
				else { //object
					if (a[3]) {
						emit('#foreach(' + encodeId(a[3]) + ' in ' + listName + '.keySet())');
						emit('#set(' + encodeId(a[4]) + ' =' + listName + '.get(' + encodeId(a[3]) + '))');
					}
					else {
						emit('#foreach(' + encodeId(a[4]) + ' in ' + listName + ')');
					}
				}
				stmtsGen(a[2]);
				emit('#{end}');
				--i_each;
				break;
			case 'set':
				emit('#set (' + encodeId(a[1]) + '=' + exprGen(a[2]) + ')');
				break;
			case 'eval':
				var s = exprGen(a[1]);
				if (isName(s))
					emit('$!{' + s.slice(1) + '}');
				else {
					emit('#set($t = ' + s + ')$!{t}');
				}
				break;
			case 'text':
				emit(a[1].replace(/\$/g, '$${dollar}').replace(/#/g, '$${sharp}'));
				break;
			case 'inc':
				emit("#parse('" + changeExt(a[1], 'vm') + "')");
				break;
			default:
				throw Error('unknown stmt: ' + a[0]);
		}
	}
	function stmtsGen(a) {
		for (var i = 0; i < a.length; ++i)
			stmtGen(a[i]);
	}

	function exprToStr(x, check) {
		var t = exprGen(x);
		if (check && !check(x[0])) t = '(' + t + ')';
		return t;
	}
	function exprGen(x) {
		switch (x[0]) {
			case 'id':
				return encodeId(x[1]);
			case 'lit':
				if (typeof x[1] == 'string')
					return vmQuote(x[1]);
				return String(x[1]);
			case '.':
				return exprToStr(x[1], isMember) + '.' + x[2];
			case '[]':
				return exprToStr(x[1], isMember) + '[' + exprGen(x[2]) + ']';
			case '!':
				return '!' + exprToStr(x[1], isUnary);
			case 'u-':
				if (x[1][0] == 'u-') throw Error("禁止两个负号连用");
				return '-' + exprToStr(x[1], isUnary);
			case '*': case '/': case '%':
				return exprToStr(x[1], isMul) + x[0] + exprToStr(x[2], isUnary);
			case '+': case '-':
				return exprToStr(x[1], isAdd) + x[0] + ' ' + exprToStr(x[2], isMul);
			case '<': case '>': case '<=': case '>=':
				return exprToStr(x[1], isRel) + x[0] + exprToStr(x[2], isAdd);
			case '==':
			case '!=':
			case '===':
			case '!==':
				return exprToStr(x[1], isEquality) + x[0].slice(0, 2) + exprToStr(x[2], isRel);
			case '&&':
				return exprToStr(x[1], isLogicalAnd) + '&&' + exprToStr(x[2], isEquality);
			case '||':
				return exprToStr(x[1], isLogicalOr) + '||' + exprToStr(x[2], isLogicalAnd);
			default:
				throw Error("unknown expr: " + x[0]);
		}
	}
	function vmQuote(s) {
		/// <param name="s" type="String"></param>
		if (s.indexOf("'") == -1) return "'" + s + "'";
		var a = s.split("'");
		return "('" + a.join("'+\"'\"+'") + "')";
	}
	var body = "#set($dollar='$')#set($sharp='#')";
	stmtsGen(prog[1]);

	return body;
}

/// <reference path="crox_js.js"/>
/// <reference path="codegen_php.js"/>
/// <reference path="codegen_vm.js"/>
Crox.compileToPhp = function(s) {
	/// <summary>返回编译后的 php</summary>
	/// <param name="s" type="String"></param>
	/// <returns type="String" />
	return codegen_php_tran(parsetmpl(s), true);
};
Crox.compileToVM = function(s, currentPath) {
	/// <summary>返回编译后的 VM 模板</summary>
	/// <param name="s" type="String"></param>
	/// <returns type="String" />
	return codegen_vm_tran(parsetmpl(s));
};

Crox.version = "1.4.9";return Crox;})();if ( typeof module == "object" && module && typeof module.exports == "object" ) module.exports = Crox;else if (typeof define == "function" && (define.amd || define.cmd) ) define(function () { return Crox; } );else if (typeof KISSY != "undefined") KISSY.add(function(){ return Crox; });if (root) root.Crox = Crox; })(this);