<commit_before>if (jQuery) {
    (function($j) {

        String.prototype.format = String.prototype.f = function(arg) {
            var s = this,
                i = arg.length;
            while (i--) {
                s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arg[i]);
            }
            return s;
        };

        var sale_assistant = window.sale_assistant = window.sale_assistant || {};
        sale_assistant.__gender_list = [{id:'M', text : "Male"}, {id:'F', text : "Female"}, {id:'U', text : "Unknown"}];

        sale_assistant.genders = function(){
            return sale_assistant.__gender_list;
        };

        sale_assistant.render_gender = function(record, index, col_index){
            var cellvalue = this.getCellValue(index, col_index);
            var html = "";
            var gs = sale_assistant.__gender_list;
            for (var p in gs) {
                if (gs[p].id === cellvalue){
                    html = sale_assistant.l(gs[p].text);
                    break;
                }
            }
            return html;
        };

        sale_assistant.renderLink = function(record, index, col_index){
            var cellvalue = this.getCellValue(index, col_index);
            var html = "";
            //Visit W3Schools.com!
            return ""+cellvalue+"";
        };

        sale_assistant.render_link  = sale_assistant.renderLink ;
        sale_assistant.error = function(d) {
            if (console) {
                console.error(d);
            }
        };
        sale_assistant.log = function(d) {
            if (console) {
                console.log(d);
            }
        };
        sale_assistant.debug = function(d) {
            if (console) {
                console.debug("*********");
                console.debug(d);
            }
        };

        sale_assistant.safeEval = function(sr){
            var fu = new Function(sr);
            fu();
        };

        sale_assistant.autoUpdate = function(data) {
            if (!data)
                return;
            var zones = data.contentRefreshZone;
            if (zones) {
                for (var z in zones) {
                    try {
                        $j("#" + z).innerHTML = zones[z];
                    } catch (ex) {
                        sale_assistant.log(z);
                    }
                }
            }
            var scripts = data.scriptRefreshZone;
            if (scripts) {
                for (var i = 0; i < scripts.length; i++) {
                    try {
                        sale_assistant.safeEval(scripts[i]);
                    } catch (ex) {
                        sale_assistant.log(ex);
                    }
                }
            }
            var clearZones = data.clearRefreshZone;
            if (clearZones) {
                for (var m = 0; m < clearZones.length; m++) {
                    try {
                        $(clearZones[m]).innerHTML = '';
                    } catch (e) {
                        sale_assistant.log(e);
                    }
                }
            }
            if (data.data) {
                sale_assistant.autoUpdate(data.data);
            }
        };

        sale_assistant.callbackFunction = function(func, transport) {
            if (func && typeof (func) === 'function') {
                var callBack = func;
                callBack(transport);
                return true;
            } else {
                return false;
            }
        };

        sale_assistant.queryString2JSON = function(qry) {
            var pairs = qry.split('&');
            var result = {};
            pairs.forEach(function(pair) {
                pair = pair.split('=');
                if (pair[1]) {
                    var va = decodeURIComponent(pair[1]);
                    var curr = result[pair[0]];
                    if (curr) {
                        if (Array.isArray(curr)) {
                            curr.push(va);
                        } else {
                            result[pair[0]] = [curr, va];
                        }
                    } else {
                        result[pair[0]] = decodeURIComponent(va);
                    }
                }
            });
            return JSON.parse(JSON.stringify(result));
        };

        sale_assistant.url = function(navUri, action, type){
            return navUri + '&' + sale_assistant.BTN_OPTION +'='+action+"&" + sale_assistant.REFRESH_TYPE +"=" + type;
        };

        sale_assistant.init = function(uri, butName, formName, refreshTypeName) {
            sale_assistant.baseURLWithButton = uri;
            sale_assistant.BTN_OPTION = butName;
            sale_assistant.form = formName;
            sale_assistant.REFRESH_TYPE = refreshTypeName;
        };

        sale_assistant.serialize = function(zone) {
            if(!zone){
                return "";
            }
            if(zone.indexOf("=") >= 0){
                return zone;
            }
            var zones = zone.split(/;|,/);
            var params = "";
            for (var index = 0; index < zones.length; index++) {
                var el = $j("#" + zones[index]);
                var serialized = el.serialize();
                if (!serialized) // not a form
                    serialized = el.
                      find('input,select,textarea').serialize();
                params += serialized;
                if (index !== zones.length - 1) {
                    params += "&";
                }
            }
            return params;
        };

        sale_assistant.get = function(action, paraZone, sucessCallback, errorCallback, autoUpdate, sync) {
            var paras = sale_assistant.serialize(paraZone);
            $j.ajax({
                url: sale_assistant.baseURLWithButton + action,
                header: {
                    "Access-Control-Allow-Origin": "*",
                    "Access-Control-Allow-Methods": "GET"
                },
                async: sync ? false : true,
                type: "GET",
                dataType: "json",
                contentType: "application/json; charset=utf-8",
                data: paras,
                success: function(data, state) {
                    sale_assistant.callbackFunction(sucessCallback, data, state);
                    if (autoUpdate) {
                        sale_assistant.autoUpdate(data);
                    }
                },
                error: function(data, state) {
                    sale_assistant.callbackFunction(errorCallback, data, state);
                }
            });
        };

        sale_assistant.post = function(action, paraZone, sucessCallback, errorCallback, autoUpdate) {
            var paras = sale_assistant.serialize(paraZone);
            alert(paras);
            $j.ajax({
                url: sale_assistant.baseURLWithButton + action,
                header: {
                    "Access-Control-Allow-Origin": "*",
                    "Access-Control-Allow-Methods": "POST"
                },
                async: true,
                type: "POST",
                dataType: "json",
                contentType: "application/json; charset=utf-8",
                data: paras,
                success: function(data, state) {
                    sale_assistant.log(data);
                    sale_assistant.log(state);
                    sale_assistant.callbackFunction(sucessCallback, data, state);
                    if (autoUpdate) {
                        sale_assistant.autoUpdate(data);
                    }
                },
                error: function(data, state) {
                    sale_assistant.error(data);
                    sale_assistant.error(state);
                    sale_assistant.callbackFunction(errorCallback, data, state);
                }
            });
        };

        sale_assistant.sumbit = function(action) {
            $j("#" + sale_assistant.actionButton).value = action;
            $j("#" + sale_assistant.form).submit();
        };

        sale_assistant.loadContent = function(url, title, navTier){

        };

        sale_assistant.runApp = function(et){
            if(!et) return;
            var url = et["data-url"];
            var title = et.text;
            var navTier = et.navTier;
            if(url && url !== '' && (navTier && navTier !== '' && navTier !== sale_assistant.currentNavTier) ){
                sale_assistant.currentNavTier = navTier;
                sale_assistant.log(url);
                document.title = title;
                sale_assistant.loadContent(url, title);
            }
        };

        sale_assistant.generateSidebarID = function(prefix, nav){
            if(nav && nav !== "" ){
                return prefix + "-" +  nav.replace(/_/g, '-');
            }
            return null;
        };

        sale_assistant.expandSidebar = function(id, parent){
            if(parent && parent.id && parent.parent){
                  window.w2ui[id].expand(parent.id);
                  if(parent.parent){
                      sale_assistant.expandSidebar(id, parent.parent);
                  }
            }
        };

        sale_assistant.selectSidebar = function (id, item){
            if(item){
                window.w2ui[id].select(item);
                var _sel = window.w2ui[id].get(item);
                if(_sel){
                    sale_assistant.expandSidebar(id ,_sel.parent);
                }
            }
        };

        sale_assistant.max_recid = {};
        sale_assistant.generateRecid = function(grid, app){
            var rs = grid.records;
            sale_assistant.max_recid[app]  = sale_assistant.max_recid[app] || 0;
            for(var i =0; i< rs.length; i++){
                var recid = rs[i].recid;
                if(sale_assistant.max_recid[app] < recid){
                    sale_assistant.max_recid[app] = recid;
                }
            }
            sale_assistant.max_recid[app] += 1;
            return sale_assistant.max_recid[app];
        };

        sale_assistant.initMaxRecId = function(grid, app){
            var rs = grid.records;
            sale_assistant.max_recid[app]  =  0;
            for(var i =0; i< rs.length; i++){
                var recid = rs[i].recid;
                if(sale_assistant.max_recid[app] < recid){
                    sale_assistant.max_recid[app] = recid;
                }
            }
            //sa.log(sale_assistant.max_recid[app]);
        };

        sale_assistant.eventData = function(evt){
            var data;
            try { data = $.parseJSON(evt.xhr.responseText) } catch (e) {}
            sa.log(data);
            return data;
        };

        sale_assistant.add_record = function(record){
            if (!$.isArray(record)) record = [record];
            var added = 0;
            for (var o in record) {
                if (!this.recid && typeof record[o].recid == 'undefined') record[o].recid = record[o][this.recid];
                if (record[o].recid == null || typeof record[o].recid == 'undefined') {
                    console.log('ERROR: Cannot add record without recid. (obj: '+ this.name +')');
                    continue;
                }
                this.records.unshift(record[o]);
                added++;
            }
            var url = (typeof this.url != 'object' ? this.url : this.url.get);
            if (!url) {
                this.total = this.records.length;
                this.localSort();
                this.localSearch();
            }
            this.refresh(); // ??  should it be reload?
            return added;

        }

    })(jQuery);
}<commit_msg>fix render link<commit_after>if (jQuery) {
    (function($j) {

        String.prototype.format = String.prototype.f = function(arg) {
            var s = this,
                i = arg.length;
            while (i--) {
                s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arg[i]);
            }
            return s;
        };

        var sale_assistant = window.sale_assistant = window.sale_assistant || {};
        sale_assistant.__gender_list = [{id:'M', text : "Male"}, {id:'F', text : "Female"}, {id:'U', text : "Unknown"}];

        sale_assistant.genders = function(){
            return sale_assistant.__gender_list;
        };

        sale_assistant.render_gender = function(record, index, col_index){
            var cellvalue = this.getCellValue(index, col_index);
            var html = "";
            var gs = sale_assistant.__gender_list;
            for (var p in gs) {
                if (gs[p].id === cellvalue){
                    html = sale_assistant.l(gs[p].text);
                    break;
                }
            }
            return html;
        };

        sale_assistant.renderLink = function(record, index, col_index){
            var cellvalue = this.getCellValue(index, col_index);
            var html = "";
            //Visit W3Schools.com!
            return "<a href='"+(cellvalue.indexOf('http') < 0 ? 'http://': '' )+cellvalue+"' target='_blank'>"+cellvalue+"";
        };

        sale_assistant.render_link  = sale_assistant.renderLink ;
        sale_assistant.error = function(d) {
            if (console) {
                console.error(d);
            }
        };
        sale_assistant.log = function(d) {
            if (console) {
                console.log(d);
            }
        };
        sale_assistant.debug = function(d) {
            if (console) {
                console.debug("*********");
                console.debug(d);
            }
        };

        sale_assistant.safeEval = function(sr){
            var fu = new Function(sr);
            fu();
        };

        sale_assistant.autoUpdate = function(data) {
            if (!data)
                return;
            var zones = data.contentRefreshZone;
            if (zones) {
                for (var z in zones) {
                    try {
                        $j("#" + z).innerHTML = zones[z];
                    } catch (ex) {
                        sale_assistant.log(z);
                    }
                }
            }
            var scripts = data.scriptRefreshZone;
            if (scripts) {
                for (var i = 0; i < scripts.length; i++) {
                    try {
                        sale_assistant.safeEval(scripts[i]);
                    } catch (ex) {
                        sale_assistant.log(ex);
                    }
                }
            }
            var clearZones = data.clearRefreshZone;
            if (clearZones) {
                for (var m = 0; m < clearZones.length; m++) {
                    try {
                        $(clearZones[m]).innerHTML = '';
                    } catch (e) {
                        sale_assistant.log(e);
                    }
                }
            }
            if (data.data) {
                sale_assistant.autoUpdate(data.data);
            }
        };

        sale_assistant.callbackFunction = function(func, transport) {
            if (func && typeof (func) === 'function') {
                var callBack = func;
                callBack(transport);
                return true;
            } else {
                return false;
            }
        };

        sale_assistant.queryString2JSON = function(qry) {
            var pairs = qry.split('&');
            var result = {};
            pairs.forEach(function(pair) {
                pair = pair.split('=');
                if (pair[1]) {
                    var va = decodeURIComponent(pair[1]);
                    var curr = result[pair[0]];
                    if (curr) {
                        if (Array.isArray(curr)) {
                            curr.push(va);
                        } else {
                            result[pair[0]] = [curr, va];
                        }
                    } else {
                        result[pair[0]] = decodeURIComponent(va);
                    }
                }
            });
            return JSON.parse(JSON.stringify(result));
        };

        sale_assistant.url = function(navUri, action, type){
            return navUri + '&' + sale_assistant.BTN_OPTION +'='+action+"&" + sale_assistant.REFRESH_TYPE +"=" + type;
        };

        sale_assistant.init = function(uri, butName, formName, refreshTypeName) {
            sale_assistant.baseURLWithButton = uri;
            sale_assistant.BTN_OPTION = butName;
            sale_assistant.form = formName;
            sale_assistant.REFRESH_TYPE = refreshTypeName;
        };

        sale_assistant.serialize = function(zone) {
            if(!zone){
                return "";
            }
            if(zone.indexOf("=") >= 0){
                return zone;
            }
            var zones = zone.split(/;|,/);
            var params = "";
            for (var index = 0; index < zones.length; index++) {
                var el = $j("#" + zones[index]);
                var serialized = el.serialize();
                if (!serialized) // not a form
                    serialized = el.
                      find('input,select,textarea').serialize();
                params += serialized;
                if (index !== zones.length - 1) {
                    params += "&";
                }
            }
            return params;
        };

        sale_assistant.get = function(action, paraZone, sucessCallback, errorCallback, autoUpdate, sync) {
            var paras = sale_assistant.serialize(paraZone);
            $j.ajax({
                url: sale_assistant.baseURLWithButton + action,
                header: {
                    "Access-Control-Allow-Origin": "*",
                    "Access-Control-Allow-Methods": "GET"
                },
                async: sync ? false : true,
                type: "GET",
                dataType: "json",
                contentType: "application/json; charset=utf-8",
                data: paras,
                success: function(data, state) {
                    sale_assistant.callbackFunction(sucessCallback, data, state);
                    if (autoUpdate) {
                        sale_assistant.autoUpdate(data);
                    }
                },
                error: function(data, state) {
                    sale_assistant.callbackFunction(errorCallback, data, state);
                }
            });
        };

        sale_assistant.post = function(action, paraZone, sucessCallback, errorCallback, autoUpdate) {
            var paras = sale_assistant.serialize(paraZone);
            alert(paras);
            $j.ajax({
                url: sale_assistant.baseURLWithButton + action,
                header: {
                    "Access-Control-Allow-Origin": "*",
                    "Access-Control-Allow-Methods": "POST"
                },
                async: true,
                type: "POST",
                dataType: "json",
                contentType: "application/json; charset=utf-8",
                data: paras,
                success: function(data, state) {
                    sale_assistant.log(data);
                    sale_assistant.log(state);
                    sale_assistant.callbackFunction(sucessCallback, data, state);
                    if (autoUpdate) {
                        sale_assistant.autoUpdate(data);
                    }
                },
                error: function(data, state) {
                    sale_assistant.error(data);
                    sale_assistant.error(state);
                    sale_assistant.callbackFunction(errorCallback, data, state);
                }
            });
        };

        sale_assistant.sumbit = function(action) {
            $j("#" + sale_assistant.actionButton).value = action;
            $j("#" + sale_assistant.form).submit();
        };

        sale_assistant.loadContent = function(url, title, navTier){

        };

        sale_assistant.runApp = function(et){
            if(!et) return;
            var url = et["data-url"];
            var title = et.text;
            var navTier = et.navTier;
            if(url && url !== '' && (navTier && navTier !== '' && navTier !== sale_assistant.currentNavTier) ){
                sale_assistant.currentNavTier = navTier;
                sale_assistant.log(url);
                document.title = title;
                sale_assistant.loadContent(url, title);
            }
        };

        sale_assistant.generateSidebarID = function(prefix, nav){
            if(nav && nav !== "" ){
                return prefix + "-" +  nav.replace(/_/g, '-');
            }
            return null;
        };

        sale_assistant.expandSidebar = function(id, parent){
            if(parent && parent.id && parent.parent){
                  window.w2ui[id].expand(parent.id);
                  if(parent.parent){
                      sale_assistant.expandSidebar(id, parent.parent);
                  }
            }
        };

        sale_assistant.selectSidebar = function (id, item){
            if(item){
                window.w2ui[id].select(item);
                var _sel = window.w2ui[id].get(item);
                if(_sel){
                    sale_assistant.expandSidebar(id ,_sel.parent);
                }
            }
        };

        sale_assistant.max_recid = {};
        sale_assistant.generateRecid = function(grid, app){
            var rs = grid.records;
            sale_assistant.max_recid[app]  = sale_assistant.max_recid[app] || 0;
            for(var i =0; i< rs.length; i++){
                var recid = rs[i].recid;
                if(sale_assistant.max_recid[app] < recid){
                    sale_assistant.max_recid[app] = recid;
                }
            }
            sale_assistant.max_recid[app] += 1;
            return sale_assistant.max_recid[app];
        };

        sale_assistant.initMaxRecId = function(grid, app){
            var rs = grid.records;
            sale_assistant.max_recid[app]  =  0;
            for(var i =0; i< rs.length; i++){
                var recid = rs[i].recid;
                if(sale_assistant.max_recid[app] < recid){
                    sale_assistant.max_recid[app] = recid;
                }
            }
            //sa.log(sale_assistant.max_recid[app]);
        };

        sale_assistant.eventData = function(evt){
            var data;
            try { data = $.parseJSON(evt.xhr.responseText) } catch (e) {}
            sa.log(data);
            return data;
        };

        sale_assistant.add_record = function(record){
            if (!$.isArray(record)) record = [record];
            var added = 0;
            for (var o in record) {
                if (!this.recid && typeof record[o].recid == 'undefined') record[o].recid = record[o][this.recid];
                if (record[o].recid == null || typeof record[o].recid == 'undefined') {
                    console.log('ERROR: Cannot add record without recid. (obj: '+ this.name +')');
                    continue;
                }
                this.records.unshift(record[o]);
                added++;
            }
            var url = (typeof this.url != 'object' ? this.url : this.url.get);
            if (!url) {
                this.total = this.records.length;
                this.localSort();
                this.localSearch();
            }
            this.refresh(); // ??  should it be reload?
            return added;

        }

    })(jQuery);
}<|endoftext|>


<commit_before>import { Filemanager } from './filemanager.js';
import { Acorn } from './acorn.js';
import { Mod } from './mod.js';
import { UI } from './ui.js';
import { Loader } from './loader.js';

const CCLOADER_VERSION = '2.10.4';

export class ModLoader {
	constructor() {
		this.filemanager = new Filemanager(this);
		this.ui = new UI(this);
		this.acorn = new Acorn();
		this.loader = new Loader(this.filemanager);

		this.frame = document.getElementById('frame');
		this.overlay = document.getElementById('overlay');
		this.status = document.getElementById('status');

		this.modsLoaded = 0;
		/** @type {Mod[]} */
		this.mods = [];

		this._buildCrosscodeVersion();
		this._initializeTable();
	}

	/**
	 * Loads and starts the game. It then loads the definitions and mods
	 */
	async startGame() {
		await this.loader.initialize();

		await this._loadModPackages();
		this._orderCheckMods();
		this._registerMods();

		await this._initializeGame();

		this._setStatus('Loading Game');
		this._setupGamewindow();

		await this._waitForGame();

		this._executeMainDb();
		this._setStatus('Initializing Mods');
		this._executeDb();

		await this._initializeMods();

		this._getGameWindow().document.body.dispatchEvent(new Event('modsLoaded'));
		this._removeOverlay();
	}

	/**
	 * Reloads all definitions
	 */
	reloadTables() {
		this.modTables = {};
		this._createTable(this.filemanager.getTableName());
		this.table.execute(this._getGameWindow(), this._getGameWindow());
		for (const mod of this.mods) {
			mod.executeTable(this);
		}
	}

	/**
	 * Orders mods and checks their dependencies
	 */
	_orderCheckMods() {
		const mods = [];

		let lastCount = 0;
		while (this.mods.length != lastCount) {
			lastCount = this.mods.length;
			for (let i = this.mods.length - 1; i >= 0; i--) {
				const mod = this.mods[i];
				if (!mod.isEnabled || this._canLoad(mod, mods)) {
					mods.push(mod);
					this.mods.splice(i, 1);
				}
			}
		}

		if (this.mods.length > 0) {
			console.log(`Could not load mods due to missing dependencies: "${this.mods.map(m => m.name).join('", "')}"`);
			for (const mod of this.mods) {
				mod.disabled = true;
				mods.push(mod);
			}
		}

		this.mods = mods;
	}

	/**
	 *
	 * @param {string} text
	 */
	_setStatus(text) {
		if (this.status && this.status.isConnected) {
			this.status.innerHTML = text;
		}
	}

	/**
	 *
	 * @returns {window}
	 */
	_getGameWindow() {
		return this.frame.contentWindow;
	}

	/**
	 * Loads a cached table if available and creates a new one otherwise
	 */
	_initializeTable() {
		if (this._definitionsNeeded()) {
			const tableName = this.filemanager.getTableName();
			if (this.filemanager.tableExists(tableName)) {
				this._loadTable(tableName);
			} else {
				this._createTable(tableName);
			}
		} else {
			this._loadTable('final.table');
		}
	}

	/**
	 * Creates the table from the definitions.db. It will also generate a cached table if possible.
	 * @param {string} tableName
	 */
	_createTable(tableName) {
		this._setStatus('Initializing Mapping');
		console.log('Reading files...');
		const jscode = this.filemanager.getResource('assets/js/game.compiled.js');
		const dbtext = this.filemanager.getResource('ccloader/data/definitions.db');

		try {
			const dbdef = JSON.parse(dbtext);
			console.log('Parsing...');
			this.acorn.parse(jscode);
			console.log('Analysing...');
			this.table = this.acorn.analyse(dbdef);
			console.log('Writing...');
			this.filemanager.saveTable(tableName, this.table);
			console.log('Finished!');
		} catch (e) {
			console.error('Could not load definitions.', e);
		}
	}

	/**
	 * Loads the cached table
	 * @param {string} tableName
	 */
	_loadTable(tableName) {
		const hash = this._definitionsNeeded() ? this.filemanager.getDefintionHash() : 'final';
		this.table = this.filemanager.loadTable(tableName, hash);
		if(!this.table) {
			this._createTable(tableName);
		}
	}

	/**
	 * Loads the mod tables
	 * @param {{[key: string]: string}} entries
	 */
	_executeModTables(entries) {
		for (const mod of this.mods) {
			if (mod.isEnabled) {
				const table = mod.initializeTable(this);
				if (table) {
					Object.assign(entries, table.entries);
					mod.executeTable(this);
				}
			}
		}
	}

	/**
	 * @param {{[key: string]: string}} entries
	 */
	_finalizeEntries(entries) {
		Object.defineProperty(this._getGameWindow(), 'entries', {
			value : Object.freeze(entries),
			writable : false
		});
	}

	_executeMainDb() {
		if (!this.table) {
			return this._removeOverlay();
		}

		this.table.execute(this._getGameWindow(), this._getGameWindow());
	}

	/**
	 * Applies all definitions and loads the mods
	 */
	_executeDb() {
		const entries = Object.assign({}, this.table.entries);

		try {
			this._executeModTables(entries);
			this._finalizeEntries(entries);
		} catch (err) {
			console.error('An error occured while loading mod tables', err);
		}
	}

	/**
	 * Sets up all global objects from ccloader in the game window
	 * @returns {{[key: string]: string}} entries
	 */
	_setupGamewindow() {
		this.ui.applyBindings(this._getGameWindow().console);

		Object.assign(this._getGameWindow(), {
			reloadTables: () => this.reloadTables(),
			getEntry: name => this._getGameWindow().entries[name],
			getEntryName: value =>
				Object.keys(this._getGameWindow().entries)
					.find(key => this._getGameWindow().entries[key] === value)
		});

		this.versions = this._getGameWindow().versions = {
			ccloader: CCLOADER_VERSION,
			crosscode: this.ccVersion
		};

		this._getGameWindow().document.createEvent('Event').initEvent('modsLoaded', true, true);
	}

	/**
	 * Searches for mods and stores them in this.mods
	 */
	_getModPackages() {
		const modFiles = this.filemanager.getAllModsFiles();
		this.mods = [];
		for (const modFile of modFiles) {
			this.mods.push(new Mod(this, modFile));
		}
	}

	_registerMods() {
		this._getGameWindow().inactiveMods = [];
		this._getGameWindow().activeMods = [];

		for (const mod of this.mods) {
			if (mod.isEnabled) {
				this._getGameWindow().activeMods.push(mod);
				if (mod.preload) {
					this.loader.addPreload(mod.preload, mod.module);
				}
				if (mod.postload) {
					this.loader.addPostload(mod.postload, mod.module);
				}
			} else {
				this._getGameWindow().inactiveMods.push(mod);
			}
		}
	}

	/**
	 * Loads the package.json of the mods. This makes sure all necessary data needed for loading the mod is available
	 */
	_loadModPackages() {
		this._getModPackages();
		return Promise.all(this.mods.map((mod) => mod.onload()));
	}

	async _initializeMods() {
		for (const mod of this.mods) {
			if (mod.isEnabled) {
				this.versions[mod.name] = mod.version;

				try {
					await mod.load();
				} catch (e) {
					console.warn(`Could not load "${mod.name}": ${e}`);
				}
			}
		}
	}

	/**
	 * @returns {Promise<void>}
	 */
	_initializeGame() {
		return new Promise((resolve, reject) => {
			if (typeof global !== 'undefined') {
				this._enableNode();
			}

			this.frame.onload = () => {
				this._getGameWindow().onbeforeunload = () => this.startGame();
				resolve();
			};
			this.frame.onerror = event => reject(event);
			this.loader.startGame(this.frame);
		});
	}

	_enableNode() {
		Object.assign(this._getGameWindow(), {
			require,
			process,
			global,
			Buffer,
			root: global.root
		});
	}

	/**
	 * Waits for the game to be completely loaded
	 * @returns {Promise<void>}
	 */
	_waitForGame() {
		return new Promise(resolve => {
			const intervalid = setInterval(() => {
				if (this._getGameWindow().ig && this._getGameWindow().ig.ready) {
					clearInterval(intervalid);
					resolve();
				}}, 1000);
		});
	}

	_removeOverlay() {
		if (this.status && this.overlay && this.status.isConnected && this.overlay.isConnected) {
			this.status.outerHTML = '';
			this.overlay.outerHTML = '';
		}
	}

	_buildCrosscodeVersion(){
		try {
			const json = JSON.parse(localStorage.getItem('cc.version'));
			this.ccVersion = json.major + '.' + json.minor + '.' + json.patch;
		} catch (e) {
			console.error('Could not find crosscode version. Assuming "0.0.0".', e);
			this.ccVersion = '0.0.0';
		}
	}

	//Requires bind
	/**
	 *
	 * @param {Mod} mod
	 * @param {Mod[]} mods
	 */
	_canLoad(mod, mods) {
		const deps = mod.dependencies;
		if(!deps) {
			return true;
		}

		for (const depName in deps){
			if(!deps.hasOwnProperty(depName))
				continue;

			const depRange = semver.validRange(deps[depName]);
			if(!depRange){
				console.warn('Invalid dependency version "' + deps[depName] + '" of "' + depName + '" of "' + mod.name + '"');
			}

			let satisfied = false;

			if(depName == 'ccloader' && semver.satisfies(CCLOADER_VERSION, depRange)) {
				satisfied = true;
			}
			if(depName == 'crosscode' && semver.satisfies(this.ccVersion, depRange)) {
				satisfied = true;
			}

			for(let i = 0; i < mods.length && !satisfied; i++){
				if(mods[i].isEnabled && mods[i].name === depName){
					if(semver.satisfies(semver.valid(mods[i].version), depRange)){
						satisfied = true;
					}
				}
			}

			if(!satisfied){
				return false;
			}
		}

		return true;
	}

	_definitionsNeeded() {
		return !semver.satisfies(this.ccVersion, '^1.0.4');
	}
}

<commit_msg>Bumped ccloader version number to 2.11.0<commit_after>import { Filemanager } from './filemanager.js';
import { Acorn } from './acorn.js';
import { Mod } from './mod.js';
import { UI } from './ui.js';
import { Loader } from './loader.js';

const CCLOADER_VERSION = '2.11.0';

export class ModLoader {
	constructor() {
		this.filemanager = new Filemanager(this);
		this.ui = new UI(this);
		this.acorn = new Acorn();
		this.loader = new Loader(this.filemanager);

		this.frame = document.getElementById('frame');
		this.overlay = document.getElementById('overlay');
		this.status = document.getElementById('status');

		this.modsLoaded = 0;
		/** @type {Mod[]} */
		this.mods = [];

		this._buildCrosscodeVersion();
		this._initializeTable();
	}

	/**
	 * Loads and starts the game. It then loads the definitions and mods
	 */
	async startGame() {
		await this.loader.initialize();

		await this._loadModPackages();
		this._orderCheckMods();
		this._registerMods();

		await this._initializeGame();

		this._setStatus('Loading Game');
		this._setupGamewindow();

		await this._waitForGame();

		this._executeMainDb();
		this._setStatus('Initializing Mods');
		this._executeDb();

		await this._initializeMods();

		this._getGameWindow().document.body.dispatchEvent(new Event('modsLoaded'));
		this._removeOverlay();
	}

	/**
	 * Reloads all definitions
	 */
	reloadTables() {
		this.modTables = {};
		this._createTable(this.filemanager.getTableName());
		this.table.execute(this._getGameWindow(), this._getGameWindow());
		for (const mod of this.mods) {
			mod.executeTable(this);
		}
	}

	/**
	 * Orders mods and checks their dependencies
	 */
	_orderCheckMods() {
		const mods = [];

		let lastCount = 0;
		while (this.mods.length != lastCount) {
			lastCount = this.mods.length;
			for (let i = this.mods.length - 1; i >= 0; i--) {
				const mod = this.mods[i];
				if (!mod.isEnabled || this._canLoad(mod, mods)) {
					mods.push(mod);
					this.mods.splice(i, 1);
				}
			}
		}

		if (this.mods.length > 0) {
			console.log(`Could not load mods due to missing dependencies: "${this.mods.map(m => m.name).join('", "')}"`);
			for (const mod of this.mods) {
				mod.disabled = true;
				mods.push(mod);
			}
		}

		this.mods = mods;
	}

	/**
	 *
	 * @param {string} text
	 */
	_setStatus(text) {
		if (this.status && this.status.isConnected) {
			this.status.innerHTML = text;
		}
	}

	/**
	 *
	 * @returns {window}
	 */
	_getGameWindow() {
		return this.frame.contentWindow;
	}

	/**
	 * Loads a cached table if available and creates a new one otherwise
	 */
	_initializeTable() {
		if (this._definitionsNeeded()) {
			const tableName = this.filemanager.getTableName();
			if (this.filemanager.tableExists(tableName)) {
				this._loadTable(tableName);
			} else {
				this._createTable(tableName);
			}
		} else {
			this._loadTable('final.table');
		}
	}

	/**
	 * Creates the table from the definitions.db. It will also generate a cached table if possible.
	 * @param {string} tableName
	 */
	_createTable(tableName) {
		this._setStatus('Initializing Mapping');
		console.log('Reading files...');
		const jscode = this.filemanager.getResource('assets/js/game.compiled.js');
		const dbtext = this.filemanager.getResource('ccloader/data/definitions.db');

		try {
			const dbdef = JSON.parse(dbtext);
			console.log('Parsing...');
			this.acorn.parse(jscode);
			console.log('Analysing...');
			this.table = this.acorn.analyse(dbdef);
			console.log('Writing...');
			this.filemanager.saveTable(tableName, this.table);
			console.log('Finished!');
		} catch (e) {
			console.error('Could not load definitions.', e);
		}
	}

	/**
	 * Loads the cached table
	 * @param {string} tableName
	 */
	_loadTable(tableName) {
		const hash = this._definitionsNeeded() ? this.filemanager.getDefintionHash() : 'final';
		this.table = this.filemanager.loadTable(tableName, hash);
		if(!this.table) {
			this._createTable(tableName);
		}
	}

	/**
	 * Loads the mod tables
	 * @param {{[key: string]: string}} entries
	 */
	_executeModTables(entries) {
		for (const mod of this.mods) {
			if (mod.isEnabled) {
				const table = mod.initializeTable(this);
				if (table) {
					Object.assign(entries, table.entries);
					mod.executeTable(this);
				}
			}
		}
	}

	/**
	 * @param {{[key: string]: string}} entries
	 */
	_finalizeEntries(entries) {
		Object.defineProperty(this._getGameWindow(), 'entries', {
			value : Object.freeze(entries),
			writable : false
		});
	}

	_executeMainDb() {
		if (!this.table) {
			return this._removeOverlay();
		}

		this.table.execute(this._getGameWindow(), this._getGameWindow());
	}

	/**
	 * Applies all definitions and loads the mods
	 */
	_executeDb() {
		const entries = Object.assign({}, this.table.entries);

		try {
			this._executeModTables(entries);
			this._finalizeEntries(entries);
		} catch (err) {
			console.error('An error occured while loading mod tables', err);
		}
	}

	/**
	 * Sets up all global objects from ccloader in the game window
	 * @returns {{[key: string]: string}} entries
	 */
	_setupGamewindow() {
		this.ui.applyBindings(this._getGameWindow().console);

		Object.assign(this._getGameWindow(), {
			reloadTables: () => this.reloadTables(),
			getEntry: name => this._getGameWindow().entries[name],
			getEntryName: value =>
				Object.keys(this._getGameWindow().entries)
					.find(key => this._getGameWindow().entries[key] === value)
		});

		this.versions = this._getGameWindow().versions = {
			ccloader: CCLOADER_VERSION,
			crosscode: this.ccVersion
		};

		this._getGameWindow().document.createEvent('Event').initEvent('modsLoaded', true, true);
	}

	/**
	 * Searches for mods and stores them in this.mods
	 */
	_getModPackages() {
		const modFiles = this.filemanager.getAllModsFiles();
		this.mods = [];
		for (const modFile of modFiles) {
			this.mods.push(new Mod(this, modFile));
		}
	}

	_registerMods() {
		this._getGameWindow().inactiveMods = [];
		this._getGameWindow().activeMods = [];

		for (const mod of this.mods) {
			if (mod.isEnabled) {
				this._getGameWindow().activeMods.push(mod);
				if (mod.preload) {
					this.loader.addPreload(mod.preload, mod.module);
				}
				if (mod.postload) {
					this.loader.addPostload(mod.postload, mod.module);
				}
			} else {
				this._getGameWindow().inactiveMods.push(mod);
			}
		}
	}

	/**
	 * Loads the package.json of the mods. This makes sure all necessary data needed for loading the mod is available
	 */
	_loadModPackages() {
		this._getModPackages();
		return Promise.all(this.mods.map((mod) => mod.onload()));
	}

	async _initializeMods() {
		for (const mod of this.mods) {
			if (mod.isEnabled) {
				this.versions[mod.name] = mod.version;

				try {
					await mod.load();
				} catch (e) {
					console.warn(`Could not load "${mod.name}": ${e}`);
				}
			}
		}
	}

	/**
	 * @returns {Promise<void>}
	 */
	_initializeGame() {
		return new Promise((resolve, reject) => {
			if (typeof global !== 'undefined') {
				this._enableNode();
			}

			this.frame.onload = () => {
				this._getGameWindow().onbeforeunload = () => this.startGame();
				resolve();
			};
			this.frame.onerror = event => reject(event);
			this.loader.startGame(this.frame);
		});
	}

	_enableNode() {
		Object.assign(this._getGameWindow(), {
			require,
			process,
			global,
			Buffer,
			root: global.root
		});
	}

	/**
	 * Waits for the game to be completely loaded
	 * @returns {Promise<void>}
	 */
	_waitForGame() {
		return new Promise(resolve => {
			const intervalid = setInterval(() => {
				if (this._getGameWindow().ig && this._getGameWindow().ig.ready) {
					clearInterval(intervalid);
					resolve();
				}}, 1000);
		});
	}

	_removeOverlay() {
		if (this.status && this.overlay && this.status.isConnected && this.overlay.isConnected) {
			this.status.outerHTML = '';
			this.overlay.outerHTML = '';
		}
	}

	_buildCrosscodeVersion(){
		try {
			const json = JSON.parse(localStorage.getItem('cc.version'));
			this.ccVersion = json.major + '.' + json.minor + '.' + json.patch;
		} catch (e) {
			console.error('Could not find crosscode version. Assuming "0.0.0".', e);
			this.ccVersion = '0.0.0';
		}
	}

	//Requires bind
	/**
	 *
	 * @param {Mod} mod
	 * @param {Mod[]} mods
	 */
	_canLoad(mod, mods) {
		const deps = mod.dependencies;
		if(!deps) {
			return true;
		}

		for (const depName in deps){
			if(!deps.hasOwnProperty(depName))
				continue;

			const depRange = semver.validRange(deps[depName]);
			if(!depRange){
				console.warn('Invalid dependency version "' + deps[depName] + '" of "' + depName + '" of "' + mod.name + '"');
			}

			let satisfied = false;

			if(depName == 'ccloader' && semver.satisfies(CCLOADER_VERSION, depRange)) {
				satisfied = true;
			}
			if(depName == 'crosscode' && semver.satisfies(this.ccVersion, depRange)) {
				satisfied = true;
			}

			for(let i = 0; i < mods.length && !satisfied; i++){
				if(mods[i].isEnabled && mods[i].name === depName){
					if(semver.satisfies(semver.valid(mods[i].version), depRange)){
						satisfied = true;
					}
				}
			}

			if(!satisfied){
				return false;
			}
		}

		return true;
	}

	_definitionsNeeded() {
		return !semver.satisfies(this.ccVersion, '^1.0.4');
	}
}

<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';
import arrayFindIndex from 'array-find-index';
import classNames from 'classnames';

import getDisplayText from './utils/getDisplayText';
import getControlComponent from './utils/getControlComponent';

import './index.scss';

const log = console.log;
const logError = console.error || log;
const logWarning = console.warn || log;

/*
 * AudioPlayer
 *
 * Accepts 'playlist' prop of the form:
 *
 * [{ "url": "./path/to/file.mp3",
 *    "displayText": "ArtistA - Track 1" },
 *  { "url": "https://domain.com/track2.ogg",
 *    "displayText": "ArtistB - Track 2" }]
 *
 * Accepts 'autoplay' prop (true/[false]).
 *
 * Accepts 'autoplayDelayInSeconds' prop (default 0).
 *
 * Accepts 'gapLengthInSeconds' prop (default 0).
 * Specifies gap at end of one track before next
 * track begins (ignored for manual skip).
 *
 * Accepts 'cycle' prop (default true,
 * starts playing at the beginning of the playlist
 * when finished if true).
 *
 * Accepts 'pauseOnSeekPreview' prop (default true,
 * pauses audio while user is selecting new time
 * for playback)
 *
 * Accepts 'disableSeek' prop (default false,
 * disables seeking through the audio if true).
 *
 * Accepts 'stayOnBackSkipThreshold' prop, default 5,
 * is number of seconds to progress until pressing back skip
 * restarts the current track.
 *
 * Accepts 'style' prop, object, is applied to
 * outermost div (React styles).
 *
 * Accepts 'onMediaEvent' prop, an object used for
 * listening to media events on the underlying audio element.
 *
 * Accepts 'audioElementRef' prop, a function called after
 * the component mounts and before it unmounts with the
 * internally-referenced HTML audio element as its only parameter.
 * Similar to: https://facebook.github.io/react/docs/refs-and-the-dom.html
 */
class AudioPlayer extends Component {

  constructor (props) {
    super(props);

    this.defaultState = {
      /* activeTrackIndex will change to match
       * this.currentTrackIndex once metadata has loaded
       */
      activeTrackIndex: -1,
      // indicates whether audio player should be paused
      paused: true,
      // elapsed time for current track, in seconds
      currentTime: 0,
      // The most recent targeted time, in seconds, for seek preview
      seekPreviewTime: 0,
      /* true if the user is currently dragging the mouse
       * to seek a new track position
       */
      seekInProgress: false,
      /* true if audio was playing when seek previewing began,
       * it was paused, and it should be resumed on seek
       * complete
       */
      awaitingResumeOnSeekComplete: false,
      // the latest volume of the audio, between 0 and 1.
      volume: 0,
      // true if the audio has been muted
      muted: false,
      // the duration in seconds of the loaded track
      duration: 0,
      /* the TimeRanges object representing the buffered sections of the
       * loaded track
       */
      buffered: null,
      /* the TimeRanges object representing the played sections of the
       * loaded track
       */
      played: null,
      // whether to loop the current track
      loop: true,
      // Rate at which audio should be played. 1.0 is normal speed.
      playbackRate: 1.0
    };

    this.state = this.defaultState;

    // index matching requested track (whether track has loaded or not)
    this.currentTrackIndex = 0;

    // html audio element used for playback
    this.audio = null;

    // bind methods fired on React events
    this.togglePause = this.togglePause.bind(this);
    this.skipToNextTrack = this.skipToNextTrack.bind(this);
    this.backSkip = this.backSkip.bind(this);
    this.seekPreview = this.seekPreview.bind(this);
    this.seekComplete = this.seekComplete.bind(this);

    // bind audio event listeners to add on mount and remove on unmount
    this.handleAudioPlay = this.handleAudioPlay.bind(this);
    this.handleAudioPause = this.handleAudioPause.bind(this);
    this.handleAudioEnded = this.handleAudioEnded.bind(this);
    this.handleAudioStalled = this.handleAudioStalled.bind(this);
    this.handleAudioTimeupdate = this.handleAudioTimeupdate.bind(this);
    this.handleAudioLoadedmetadata = this.handleAudioLoadedmetadata.bind(this);
    this.handleAudioVolumechange = this.handleAudioVolumechange.bind(this);
    this.handleAudioDurationchange = this.handleAudioDurationchange.bind(this);
    this.handleAudioProgress = this.handleAudioProgress.bind(this);
    this.handleAudioRatechange = this.handleAudioRatechange.bind(this);
  }

  componentDidMount () {
    const audio = this.audio = document.createElement('audio');

    // add event listeners on the audio element
    audio.preload = 'metadata';
    audio.addEventListener('play', this.handleAudioPlay);
    audio.addEventListener('pause', this.handleAudioPause);
    audio.addEventListener('ended', this.handleAudioEnded);
    audio.addEventListener('stalled', this.handleAudioStalled);
    audio.addEventListener('timeupdate', this.handleAudioTimeupdate);
    audio.addEventListener('loadedmetadata', this.handleAudioLoadedmetadata);
    audio.addEventListener('volumechange', this.handleAudioVolumechange);
    audio.addEventListener('durationchange', this.handleAudioDurationchange);
    audio.addEventListener('progress', this.handleAudioProgress);
    audio.addEventListener('ratechange', this.handleAudioRatechange);
    this.addMediaEventListeners(this.props.onMediaEvent);

    if (this.props.playlist && this.props.playlist.length) {
      this.updateSource();
      if (this.props.autoplay) {
        clearTimeout(this.delayTimeout);
        this.delayTimeout = setTimeout(() => {
          this.togglePause(false);
        }, this.props.autoplayDelayInSeconds * 1000);
      }
    }

    if (this.props.audioElementRef) {
      this.props.audioElementRef(audio);
    }
  }

  componentWillReceiveProps (nextProps) {
    // Update media event listeners that may have changed
    this.removeMediaEventListeners(this.props.onMediaEvent);
    this.addMediaEventListeners(nextProps.onMediaEvent);

    const newPlaylist = nextProps.playlist;
    if (!newPlaylist || !newPlaylist.length) {
      this.audio.src = '';
      this.currentTrackIndex = 0;
      return this.setState(this.defaultState);
    }

    const oldPlaylist = this.props.playlist;

    const currentTrackUrl = ((oldPlaylist || [])[this.currentTrackIndex] || {}).url;
    this.currentTrackIndex = arrayFindIndex(newPlaylist, track => {
      return track.url && currentTrackUrl === track.url;
    });
    /* if the track we're already playing is in the new playlist, update the
     * activeTrackIndex.
     */
    if (this.currentTrackIndex !== -1) {
      this.setState({
        activeTrackIndex: this.currentTrackIndex
      });
    }
  }

  componentDidUpdate () {
    /* if we loaded a new playlist and reset the current track marker, we
     * should load up the first one.
     */
    if (this.currentTrackIndex === -1) {
      this.skipToNextTrack(false);
    }
  }

  componentWillUnmount () {
    const { audio } = this;
    // remove event listeners on the audio element
    audio.removeEventListener('play', this.handleAudioPlay);
    audio.removeEventListener('pause', this.handleAudioPause);
    audio.removeEventListener('ended', this.handleAudioEnded);
    audio.removeEventListener('stalled', this.handleAudioStalled);
    audio.removeEventListener('timeupdate', this.handleAudioTimeupdate);
    audio.removeEventListener('loadedmetadata', this.handleAudioLoadedmetadata);
    audio.removeEventListener('volumechange', this.handleAudioVolumechange);
    audio.removeEventListener('durationchange', this.handleAudioDurationchange);
    audio.removeEventListener('progress', this.handleAudioProgress);
    audio.removeEventListener('ratechange', this.handleAudioRatechange);
    removeMediaEventListeners(this.props.onMediaEvent);

    clearTimeout(this.gapLengthTimeout);
    clearTimeout(this.delayTimeout);

    // pause the audio element before we unmount
    audio.pause();

    if (this.props.audioElementRef) {
      this.props.audioElementRef(audio);
    }
  }

  addMediaEventListeners (mediaEvents) {
    if (!mediaEvents) {
      return;
    }
    Object.keys(mediaEvents).forEach((type) => {
      if (typeof mediaEvents[type] !== 'function') {
        return;
      }
      this.audio.addEventListener(type, mediaEvents[type]);
    });
  }

  removeMediaEventListeners (mediaEvents) {
    if (!mediaEvents) {
      return;
    }
    Object.keys(mediaEvents).forEach((type) => {
      if (typeof mediaEvents[type] !== 'function') {
        return;
      }
      this.audio.removeEventListener(type, mediaEvents[type]);
    });
  }

  handleAudioPlay () {
    this.setState({ paused: false });
  }

  handleAudioPause () {
    this.setState({ paused: true });
  }

  handleAudioEnded () {
    clearTimeout(this.gapLengthTimeout);
    this.gapLengthTimeout = setTimeout(
      this.skipToNextTrack,
      this.props.gapLengthInSeconds * 1000
    );
  }

  handleAudioStalled () {
    this.togglePause(true);
  }

  handleAudioTimeupdate () {
    const { currentTime, played } = this.audio;
    this.setState({ currentTime, played });
  }

  handleAudioLoadedmetadata () {
    this.setState({
      activeTrackIndex: this.currentTrackIndex
    });
  }

  handleAudioVolumechange () {
    const { volume, muted } = this.audio;
    this.setState({ volume, muted });
  }

  handleAudioDurationchange () {
    const { duration } = this.audio;
    this.setState({ duration });
  }

  handleAudioProgress () {
    const { buffered } = this.audio;
    this.setState({ buffered });
  }

  handleAudioRatechange () {
    const { playbackRate } = this.audio;
    this.setState({ playbackRate });
  }

  updateSource () {
    this.audio.src = this.props.playlist[this.currentTrackIndex].url;
  }

  togglePause (value) {
    const { audio } = this;
    if (!audio) {
      return;
    }
    const pause = typeof value === 'boolean' ? value : !this.state.paused;
    if (pause) {
      return audio.pause();
    }
    const { playlist } = this.props;
    if (!playlist || !playlist.length) {
      return;
    }
    try {
      audio.play();
    } catch (error) {
      logError(error);
      const warningMessage =
        'Audio playback failed at ' +
        new Date().toLocaleTimeString() +
        '! (Perhaps autoplay is disabled in this browser.)';
      logWarning(warningMessage);
    }
  }

  skipToNextTrack (shouldPlay) {
    const { audio } = this;
    if (!audio) {
      return;
    }
    audio.pause();
    const { playlist, cycle } = this.props;
    if (!playlist || !playlist.length) {
      return;
    }
    let i = this.currentTrackIndex + 1;
    if (i >= playlist.length) {
      i = 0;
    }
    this.currentTrackIndex = i;
    this.setState({
      activeTrackIndex: -1,
      currentTime: 0
    }, () => {
      this.updateSource();
      const shouldPauseOnCycle = (!cycle && i === 0);
      const shouldPause = shouldPauseOnCycle || (typeof shouldPlay === 'boolean' ? !shouldPlay : false);
      this.togglePause(shouldPause);
    });
  }

  backSkip () {
    const { playlist, stayOnBackSkipThreshold } = this.props;
    const { audio } = this;
    if (!playlist || !playlist.length) {
      return;
    }
    if (audio.currentTime >= stayOnBackSkipThreshold) {
      return audio.currentTime = 0;
    }
    let i = this.currentTrackIndex - 1;
    if (i < 0) {
      i = playlist.length - 1;
    }
    this.currentTrackIndex = i - 1;
    this.skipToNextTrack();
  }

  seekPreview (progress) {
    const { paused, awaitingResumeOnSeekComplete } = this.state;
    if (this.isSeekUnavailable()) {
      return;
    }
    if (!paused && this.props.pauseOnSeekPreview && !awaitingResumeOnSeekComplete) {
      this.setState({
        awaitingResumeOnSeekComplete: true
      });
      this.togglePause(true);
    }
    const progressInBounds = Math.max(0, Math.min(progress, 1));
    this.setState({
      seekPreviewTime: progressInBounds * this.audio.duration,
      seekInProgress: true
    });
  }

  seekComplete () {
    const { seekPreviewTime, awaitingResumeOnSeekComplete } = this.state;
    this.setState({
      seekInProgress: false,
    });
    if (isNaN(seekPreviewTime)) {
      return;
    }
    this.setState({
      /* we'll update currentTime on the audio listener hook anyway,
       * but the optimistic update helps avoid a visual glitch in
       * the progress bar, if seekInProgress changes before currentTime.
       */
      currentTime: seekPreviewTime
    });
    const { audio } = this;
    audio.currentTime = seekPreviewTime;
    if (awaitingResumeOnSeekComplete) {
      audio.play();
      this.setState({
        awaitingResumeOnSeekComplete: false
      });
    }
  }

  isSeekUnavailable () {
    return Boolean(
      !this.props.playlist ||
      !this.props.playlist.length ||
      this.props.disableSeek
    );
  }

  render () {
    const { playlist, controls, style } = this.props;
    return (
      <div
        className="audio_player"
        title={getDisplayText(playlist, this.state.activeTrackIndex)}
        style={style}
      >
        {controls.map((control, index) => {
          const ControlComponent = getControlComponent(control);
          if (!ControlComponent) {
            return null;
          }
          return (
            <ControlComponent
              key={index}
              playlist={playlist}
              seekUnavailable={this.isSeekUnavailable()}
              onTogglePause={this.togglePause}
              onBackSkip={this.backSkip}
              onForwardSkip={this.skipToNextTrack}
              onSeekPreview={this.seekPreview}
              onSeekComplete={this.seekComplete}
              {...this.state}
            />
          );
        })}
      </div>
    );
  }

}

AudioPlayer.propTypes = {
  playlist: PropTypes.arrayOf(PropTypes.shape({
    url: PropTypes.string.isRequired,
    displayText: PropTypes.string.isRequired
  }).isRequired),
  controls: PropTypes.arrayOf(PropTypes.oneOfType([
    PropTypes.element,
    PropTypes.oneOf([
      'playpause',
      'backskip',
      'forwardskip',
      'progress',
      'spacer'
    ])
  ]).isRequired),
  autoplay: PropTypes.bool,
  autoplayDelayInSeconds: PropTypes.number,
  gapLengthInSeconds: PropTypes.number,
  cycle: PropTypes.bool,
  pauseOnSeekPreview: PropTypes.bool,
  disableSeek: PropTypes.bool,
  stayOnBackSkipThreshold: PropTypes.number,
  style: PropTypes.object,
  onMediaEvent: PropTypes.objectOf(PropTypes.func.isRequired),
  audioElementRef: PropTypes.func
};

AudioPlayer.defaultProps = {
  controls: [
    'spacer',
    'backskip',
    'playpause',
    'forwardskip',
    'spacer',
    'progress'
  ],
  autoplay: false,
  autoplayDelayInSeconds: 0,
  gapLengthInSeconds: 0,
  cycle: true,
  pauseOnSeekPreview: false,
  disableSeek: false,
  stayOnBackSkipThreshold: 5
};

module.exports = AudioPlayer;
<commit_msg>Restore binding on console log shortcuts for older browser compatibility.<commit_after>import React, { Component, PropTypes } from 'react';
import arrayFindIndex from 'array-find-index';
import classNames from 'classnames';

import getDisplayText from './utils/getDisplayText';
import getControlComponent from './utils/getControlComponent';

import './index.scss';

const log = console.log.bind(console);
const logError = console.error ? console.error.bind(console) : log;
const logWarning = console.warn ? console.warn.bind(console) : log;

/*
 * AudioPlayer
 *
 * Accepts 'playlist' prop of the form:
 *
 * [{ "url": "./path/to/file.mp3",
 *    "displayText": "ArtistA - Track 1" },
 *  { "url": "https://domain.com/track2.ogg",
 *    "displayText": "ArtistB - Track 2" }]
 *
 * Accepts 'autoplay' prop (true/[false]).
 *
 * Accepts 'autoplayDelayInSeconds' prop (default 0).
 *
 * Accepts 'gapLengthInSeconds' prop (default 0).
 * Specifies gap at end of one track before next
 * track begins (ignored for manual skip).
 *
 * Accepts 'cycle' prop (default true,
 * starts playing at the beginning of the playlist
 * when finished if true).
 *
 * Accepts 'pauseOnSeekPreview' prop (default true,
 * pauses audio while user is selecting new time
 * for playback)
 *
 * Accepts 'disableSeek' prop (default false,
 * disables seeking through the audio if true).
 *
 * Accepts 'stayOnBackSkipThreshold' prop, default 5,
 * is number of seconds to progress until pressing back skip
 * restarts the current track.
 *
 * Accepts 'style' prop, object, is applied to
 * outermost div (React styles).
 *
 * Accepts 'onMediaEvent' prop, an object used for
 * listening to media events on the underlying audio element.
 *
 * Accepts 'audioElementRef' prop, a function called after
 * the component mounts and before it unmounts with the
 * internally-referenced HTML audio element as its only parameter.
 * Similar to: https://facebook.github.io/react/docs/refs-and-the-dom.html
 */
class AudioPlayer extends Component {

  constructor (props) {
    super(props);

    this.defaultState = {
      /* activeTrackIndex will change to match
       * this.currentTrackIndex once metadata has loaded
       */
      activeTrackIndex: -1,
      // indicates whether audio player should be paused
      paused: true,
      // elapsed time for current track, in seconds
      currentTime: 0,
      // The most recent targeted time, in seconds, for seek preview
      seekPreviewTime: 0,
      /* true if the user is currently dragging the mouse
       * to seek a new track position
       */
      seekInProgress: false,
      /* true if audio was playing when seek previewing began,
       * it was paused, and it should be resumed on seek
       * complete
       */
      awaitingResumeOnSeekComplete: false,
      // the latest volume of the audio, between 0 and 1.
      volume: 0,
      // true if the audio has been muted
      muted: false,
      // the duration in seconds of the loaded track
      duration: 0,
      /* the TimeRanges object representing the buffered sections of the
       * loaded track
       */
      buffered: null,
      /* the TimeRanges object representing the played sections of the
       * loaded track
       */
      played: null,
      // whether to loop the current track
      loop: true,
      // Rate at which audio should be played. 1.0 is normal speed.
      playbackRate: 1.0
    };

    this.state = this.defaultState;

    // index matching requested track (whether track has loaded or not)
    this.currentTrackIndex = 0;

    // html audio element used for playback
    this.audio = null;

    // bind methods fired on React events
    this.togglePause = this.togglePause.bind(this);
    this.skipToNextTrack = this.skipToNextTrack.bind(this);
    this.backSkip = this.backSkip.bind(this);
    this.seekPreview = this.seekPreview.bind(this);
    this.seekComplete = this.seekComplete.bind(this);

    // bind audio event listeners to add on mount and remove on unmount
    this.handleAudioPlay = this.handleAudioPlay.bind(this);
    this.handleAudioPause = this.handleAudioPause.bind(this);
    this.handleAudioEnded = this.handleAudioEnded.bind(this);
    this.handleAudioStalled = this.handleAudioStalled.bind(this);
    this.handleAudioTimeupdate = this.handleAudioTimeupdate.bind(this);
    this.handleAudioLoadedmetadata = this.handleAudioLoadedmetadata.bind(this);
    this.handleAudioVolumechange = this.handleAudioVolumechange.bind(this);
    this.handleAudioDurationchange = this.handleAudioDurationchange.bind(this);
    this.handleAudioProgress = this.handleAudioProgress.bind(this);
    this.handleAudioRatechange = this.handleAudioRatechange.bind(this);
  }

  componentDidMount () {
    const audio = this.audio = document.createElement('audio');

    // add event listeners on the audio element
    audio.preload = 'metadata';
    audio.addEventListener('play', this.handleAudioPlay);
    audio.addEventListener('pause', this.handleAudioPause);
    audio.addEventListener('ended', this.handleAudioEnded);
    audio.addEventListener('stalled', this.handleAudioStalled);
    audio.addEventListener('timeupdate', this.handleAudioTimeupdate);
    audio.addEventListener('loadedmetadata', this.handleAudioLoadedmetadata);
    audio.addEventListener('volumechange', this.handleAudioVolumechange);
    audio.addEventListener('durationchange', this.handleAudioDurationchange);
    audio.addEventListener('progress', this.handleAudioProgress);
    audio.addEventListener('ratechange', this.handleAudioRatechange);
    this.addMediaEventListeners(this.props.onMediaEvent);

    if (this.props.playlist && this.props.playlist.length) {
      this.updateSource();
      if (this.props.autoplay) {
        clearTimeout(this.delayTimeout);
        this.delayTimeout = setTimeout(() => {
          this.togglePause(false);
        }, this.props.autoplayDelayInSeconds * 1000);
      }
    }

    if (this.props.audioElementRef) {
      this.props.audioElementRef(audio);
    }
  }

  componentWillReceiveProps (nextProps) {
    // Update media event listeners that may have changed
    this.removeMediaEventListeners(this.props.onMediaEvent);
    this.addMediaEventListeners(nextProps.onMediaEvent);

    const newPlaylist = nextProps.playlist;
    if (!newPlaylist || !newPlaylist.length) {
      this.audio.src = '';
      this.currentTrackIndex = 0;
      return this.setState(this.defaultState);
    }

    const oldPlaylist = this.props.playlist;

    const currentTrackUrl = ((oldPlaylist || [])[this.currentTrackIndex] || {}).url;
    this.currentTrackIndex = arrayFindIndex(newPlaylist, track => {
      return track.url && currentTrackUrl === track.url;
    });
    /* if the track we're already playing is in the new playlist, update the
     * activeTrackIndex.
     */
    if (this.currentTrackIndex !== -1) {
      this.setState({
        activeTrackIndex: this.currentTrackIndex
      });
    }
  }

  componentDidUpdate () {
    /* if we loaded a new playlist and reset the current track marker, we
     * should load up the first one.
     */
    if (this.currentTrackIndex === -1) {
      this.skipToNextTrack(false);
    }
  }

  componentWillUnmount () {
    const { audio } = this;
    // remove event listeners on the audio element
    audio.removeEventListener('play', this.handleAudioPlay);
    audio.removeEventListener('pause', this.handleAudioPause);
    audio.removeEventListener('ended', this.handleAudioEnded);
    audio.removeEventListener('stalled', this.handleAudioStalled);
    audio.removeEventListener('timeupdate', this.handleAudioTimeupdate);
    audio.removeEventListener('loadedmetadata', this.handleAudioLoadedmetadata);
    audio.removeEventListener('volumechange', this.handleAudioVolumechange);
    audio.removeEventListener('durationchange', this.handleAudioDurationchange);
    audio.removeEventListener('progress', this.handleAudioProgress);
    audio.removeEventListener('ratechange', this.handleAudioRatechange);
    removeMediaEventListeners(this.props.onMediaEvent);

    clearTimeout(this.gapLengthTimeout);
    clearTimeout(this.delayTimeout);

    // pause the audio element before we unmount
    audio.pause();

    if (this.props.audioElementRef) {
      this.props.audioElementRef(audio);
    }
  }

  addMediaEventListeners (mediaEvents) {
    if (!mediaEvents) {
      return;
    }
    Object.keys(mediaEvents).forEach((type) => {
      if (typeof mediaEvents[type] !== 'function') {
        return;
      }
      this.audio.addEventListener(type, mediaEvents[type]);
    });
  }

  removeMediaEventListeners (mediaEvents) {
    if (!mediaEvents) {
      return;
    }
    Object.keys(mediaEvents).forEach((type) => {
      if (typeof mediaEvents[type] !== 'function') {
        return;
      }
      this.audio.removeEventListener(type, mediaEvents[type]);
    });
  }

  handleAudioPlay () {
    this.setState({ paused: false });
  }

  handleAudioPause () {
    this.setState({ paused: true });
  }

  handleAudioEnded () {
    clearTimeout(this.gapLengthTimeout);
    this.gapLengthTimeout = setTimeout(
      this.skipToNextTrack,
      this.props.gapLengthInSeconds * 1000
    );
  }

  handleAudioStalled () {
    this.togglePause(true);
  }

  handleAudioTimeupdate () {
    const { currentTime, played } = this.audio;
    this.setState({ currentTime, played });
  }

  handleAudioLoadedmetadata () {
    this.setState({
      activeTrackIndex: this.currentTrackIndex
    });
  }

  handleAudioVolumechange () {
    const { volume, muted } = this.audio;
    this.setState({ volume, muted });
  }

  handleAudioDurationchange () {
    const { duration } = this.audio;
    this.setState({ duration });
  }

  handleAudioProgress () {
    const { buffered } = this.audio;
    this.setState({ buffered });
  }

  handleAudioRatechange () {
    const { playbackRate } = this.audio;
    this.setState({ playbackRate });
  }

  updateSource () {
    this.audio.src = this.props.playlist[this.currentTrackIndex].url;
  }

  togglePause (value) {
    const { audio } = this;
    if (!audio) {
      return;
    }
    const pause = typeof value === 'boolean' ? value : !this.state.paused;
    if (pause) {
      return audio.pause();
    }
    const { playlist } = this.props;
    if (!playlist || !playlist.length) {
      return;
    }
    try {
      audio.play();
    } catch (error) {
      logError(error);
      const warningMessage =
        'Audio playback failed at ' +
        new Date().toLocaleTimeString() +
        '! (Perhaps autoplay is disabled in this browser.)';
      logWarning(warningMessage);
    }
  }

  skipToNextTrack (shouldPlay) {
    const { audio } = this;
    if (!audio) {
      return;
    }
    audio.pause();
    const { playlist, cycle } = this.props;
    if (!playlist || !playlist.length) {
      return;
    }
    let i = this.currentTrackIndex + 1;
    if (i >= playlist.length) {
      i = 0;
    }
    this.currentTrackIndex = i;
    this.setState({
      activeTrackIndex: -1,
      currentTime: 0
    }, () => {
      this.updateSource();
      const shouldPauseOnCycle = (!cycle && i === 0);
      const shouldPause = shouldPauseOnCycle || (typeof shouldPlay === 'boolean' ? !shouldPlay : false);
      this.togglePause(shouldPause);
    });
  }

  backSkip () {
    const { playlist, stayOnBackSkipThreshold } = this.props;
    const { audio } = this;
    if (!playlist || !playlist.length) {
      return;
    }
    if (audio.currentTime >= stayOnBackSkipThreshold) {
      return audio.currentTime = 0;
    }
    let i = this.currentTrackIndex - 1;
    if (i < 0) {
      i = playlist.length - 1;
    }
    this.currentTrackIndex = i - 1;
    this.skipToNextTrack();
  }

  seekPreview (progress) {
    const { paused, awaitingResumeOnSeekComplete } = this.state;
    if (this.isSeekUnavailable()) {
      return;
    }
    if (!paused && this.props.pauseOnSeekPreview && !awaitingResumeOnSeekComplete) {
      this.setState({
        awaitingResumeOnSeekComplete: true
      });
      this.togglePause(true);
    }
    const progressInBounds = Math.max(0, Math.min(progress, 1));
    this.setState({
      seekPreviewTime: progressInBounds * this.audio.duration,
      seekInProgress: true
    });
  }

  seekComplete () {
    const { seekPreviewTime, awaitingResumeOnSeekComplete } = this.state;
    this.setState({
      seekInProgress: false,
    });
    if (isNaN(seekPreviewTime)) {
      return;
    }
    this.setState({
      /* we'll update currentTime on the audio listener hook anyway,
       * but the optimistic update helps avoid a visual glitch in
       * the progress bar, if seekInProgress changes before currentTime.
       */
      currentTime: seekPreviewTime
    });
    const { audio } = this;
    audio.currentTime = seekPreviewTime;
    if (awaitingResumeOnSeekComplete) {
      audio.play();
      this.setState({
        awaitingResumeOnSeekComplete: false
      });
    }
  }

  isSeekUnavailable () {
    return Boolean(
      !this.props.playlist ||
      !this.props.playlist.length ||
      this.props.disableSeek
    );
  }

  render () {
    const { playlist, controls, style } = this.props;
    return (
      <div
        className="audio_player"
        title={getDisplayText(playlist, this.state.activeTrackIndex)}
        style={style}
      >
        {controls.map((control, index) => {
          const ControlComponent = getControlComponent(control);
          if (!ControlComponent) {
            return null;
          }
          return (
            <ControlComponent
              key={index}
              playlist={playlist}
              seekUnavailable={this.isSeekUnavailable()}
              onTogglePause={this.togglePause}
              onBackSkip={this.backSkip}
              onForwardSkip={this.skipToNextTrack}
              onSeekPreview={this.seekPreview}
              onSeekComplete={this.seekComplete}
              {...this.state}
            />
          );
        })}
      </div>
    );
  }

}

AudioPlayer.propTypes = {
  playlist: PropTypes.arrayOf(PropTypes.shape({
    url: PropTypes.string.isRequired,
    displayText: PropTypes.string.isRequired
  }).isRequired),
  controls: PropTypes.arrayOf(PropTypes.oneOfType([
    PropTypes.element,
    PropTypes.oneOf([
      'playpause',
      'backskip',
      'forwardskip',
      'progress',
      'spacer'
    ])
  ]).isRequired),
  autoplay: PropTypes.bool,
  autoplayDelayInSeconds: PropTypes.number,
  gapLengthInSeconds: PropTypes.number,
  cycle: PropTypes.bool,
  pauseOnSeekPreview: PropTypes.bool,
  disableSeek: PropTypes.bool,
  stayOnBackSkipThreshold: PropTypes.number,
  style: PropTypes.object,
  onMediaEvent: PropTypes.objectOf(PropTypes.func.isRequired),
  audioElementRef: PropTypes.func
};

AudioPlayer.defaultProps = {
  controls: [
    'spacer',
    'backskip',
    'playpause',
    'forwardskip',
    'spacer',
    'progress'
  ],
  autoplay: false,
  autoplayDelayInSeconds: 0,
  gapLengthInSeconds: 0,
  cycle: true,
  pauseOnSeekPreview: false,
  disableSeek: false,
  stayOnBackSkipThreshold: 5
};

module.exports = AudioPlayer;
<|endoftext|>


<commit_before>var Table = require('cli-table');

var data,
    log = console.log,
    fileColWidth = 50;

data = {
    assertions: [],
    tests: [],
    summaries: [],
    coverages: []
};

/**
 * Add data to the log report.
 *
 * @param {String} type
 * @param {Object} obj
 * @return {Array}
 */
exports.add = function(type, obj) {
    if (obj) {
        data[type].push(obj);
    }
    return data[type];
};

/**
 * Get global tests stats in unified format
 */
exports.stats = function() {
    var stats = {
        files: 0,
        assertions: 0,
        failed: 0,
        passed: 0,
        runtime: 0
    };

    data.summaries.forEach(function(file) {
        stats.files++;
        stats.assertions += file.total;
        stats.failed += file.failed;
        stats.passed += file.passed;
        stats.runtime += file.runtime;
    });

    stats.tests = data.tests.length;

    stats.coverage = {
        files: 0,
        statements: { covered: 0, total: 0 },
        branches: { covered: 0, total: 0 },
        functions: { covered: 0, total: 0 },
        lines: { covered: 0, total: 0 }
    };

    data.coverages.forEach(function(file) {
        stats.coverage.files++;
        stats.coverage.statements.covered += file.statements.covered;
        stats.coverage.statements.total += file.statements.total;
        stats.coverage.branches.covered += file.branches.covered;
        stats.coverage.branches.total += file.branches.total;
        stats.coverage.functions.covered += file.functions.covered;
        stats.coverage.functions.total += file.functions.total;
        stats.coverage.lines.covered += file.lines.covered;
        stats.coverage.lines.total += file.lines.total;
    });

    return stats;
};

/**
 * Reset global stats data
 */
exports.reset = function() {
    data = {
        assertions: [],
        tests: [],
        summaries: [],
        coverages: []
    };
};

var print = exports.print = {};

print.assertions = function() {
    var table,
        currentModule, module,
        currentTest, test;

    table = new Table({
        head: ['Module', 'Test', 'Assertion', 'Result']
    });

    data.assertions.forEach(function(data) {
        // just easier to read the table
        if (data.module === currentModule) {
            module = '';
        } else {
            module = currentModule = data.module;
        }

        // just easier to read the table
        if (data.test === currentTest) {
            test = '';
        } else {
            test = currentTest = data.test;
        }

        table.push([module, test, data.message || '', data.result ? 'ok' : 'fail']);
    });

    log('\nAssertions:\n' + table.toString());
};

print.errors = function() {
    var errors = [];

    data.assertions.forEach(function(data) {
        if (!data.result) {
            errors.push(data);
        }
    });

    if (errors.length) {
        log('\n\nErrors:');
        errors.forEach(function(data) {
            log('\nModule: ' + data.module + ' Test: ' + data.test);
            if (data.message) {
                log(data.message);
            }

            if (data.source) {
                log(data.source);
            }

            if (data.expected != null || data.actual != null) {
                //it will be an error if data.expected !== data.actual, but if they're
                //both undefined, it means that they were just not filled out because
                //no assertions were hit (likely due to code error that would have been logged as source or message).
                log('Actual value:');
                log(data.actual);
                log('Expected value:');
                log(data.expected);
            }
        });
    }
};

print.tests = function() {
    var table,
        currentModule, module;

    table = new Table({
        head: ['Module', 'Test', 'Failed', 'Passed', 'Total']
    });

    data.tests.forEach(function(data) {
        // just easier to read the table
        if (data.module === currentModule) {
            module = '';
        } else {
            module = currentModule = data.module;
        }

        table.push([module, data.name, data.failed, data.passed, data.total]);
    });

    log('\nTests:\n' + table.toString());
};

// truncate file name
function truncFile(code) {
    if (code && code.length > fileColWidth) {
        code = '...' + code.slice(code.length - fileColWidth + 3);
    }
    return code;
}

print.summary = function() {
    var table;

    table = new Table({
        head: ['File', 'Failed', 'Passed', 'Total', 'Runtime']
    });

    data.summaries.forEach(function(data) {
        table.push([truncFile(data.code), data.failed, data.passed, data.total, data.runtime]);
    });

    log('\nSummary:\n' + table.toString());
};

print.globalSummary = function() {
    var table,
        data = exports.stats();

    table = new Table({
        head: ['Files', 'Tests', 'Assertions', 'Failed', 'Passed', 'Runtime']
    });

    table.push([data.files, data.tests, data.assertions, data.failed,
        data.passed, data.runtime]);

    log('\nGlobal summary:\n' + table.toString());
};

function getMet(metric) {
    function percent(covered, total) {
        var tmp;
        if (total > 0) {
            tmp = 1000 * 100 * covered / total + 5;
            return Math.floor(tmp / 10) / 100;
        } else {
            return 100.00;
        }
    }
    if (!metric.pct) metric.pct = percent(metric.covered, metric.total);
    return metric.pct + '% (' + metric.covered + '/' + metric.total + ')';
}

print.coverage = function() {
    var table;

    if (!data.coverages.length) return;

    table = new Table({
        head: ['File', 'Statements', 'Branches', 'Functions', 'Lines']
    });

    data.coverages.forEach(function(coverage) {
        table.push([
            truncFile(coverage.code),
            getMet(coverage.statements),
            getMet(coverage.branches),
            getMet(coverage.functions),
            getMet(coverage.lines)]);
    });

    log('\nCoverage:\n' + table.toString());
};

print.globalCoverage = function() {
    var coverage, table;

    if (!data.coverages.length) return;

    coverage = exports.stats().coverage;
    table = new Table({
        head: ['Files', 'Statements', 'Branches', 'Functions', 'Lines']
    });

    table.push([
        coverage.files,
        getMet(coverage.statements),
        getMet(coverage.branches),
        getMet(coverage.functions),
        getMet(coverage.lines)
    ]);

    log('\nGlobal coverage:\n' + table.toString());
};
<commit_msg>Sort by module before logging the assertions and the tests<commit_after>var Table = require('cli-table');

var data,
    log = console.log,
    fileColWidth = 50;

data = {
    assertions: [],
    tests: [],
    summaries: [],
    coverages: []
};

/**
 * Add data to the log report.
 *
 * @param {String} type
 * @param {Object} obj
 * @return {Array}
 */
exports.add = function(type, obj) {
    if (obj) {
        data[type].push(obj);
    }
    return data[type];
};

/**
 * Get global tests stats in unified format
 */
exports.stats = function() {
    var stats = {
        files: 0,
        assertions: 0,
        failed: 0,
        passed: 0,
        runtime: 0
    };

    data.summaries.forEach(function(file) {
        stats.files++;
        stats.assertions += file.total;
        stats.failed += file.failed;
        stats.passed += file.passed;
        stats.runtime += file.runtime;
    });

    stats.tests = data.tests.length;

    stats.coverage = {
        files: 0,
        statements: { covered: 0, total: 0 },
        branches: { covered: 0, total: 0 },
        functions: { covered: 0, total: 0 },
        lines: { covered: 0, total: 0 }
    };

    data.coverages.forEach(function(file) {
        stats.coverage.files++;
        stats.coverage.statements.covered += file.statements.covered;
        stats.coverage.statements.total += file.statements.total;
        stats.coverage.branches.covered += file.branches.covered;
        stats.coverage.branches.total += file.branches.total;
        stats.coverage.functions.covered += file.functions.covered;
        stats.coverage.functions.total += file.functions.total;
        stats.coverage.lines.covered += file.lines.covered;
        stats.coverage.lines.total += file.lines.total;
    });

    return stats;
};

/**
 * Reset global stats data
 */
exports.reset = function() {
    data = {
        assertions: [],
        tests: [],
        summaries: [],
        coverages: []
    };
};

var print = exports.print = {};

print.assertions = function() {
    var table,
        currentModule, module,
        currentTest, test;

    table = new Table({
        head: ['Module', 'Test', 'Assertion', 'Result']
    });

    data.assertions.sort(function(a,b){if(a.module > b.module) return 1; if(a.module < b.module) return -1; return 0});

    data.assertions.forEach(function(data) {
        // just easier to read the table
        if (data.module === currentModule) {
            module = '';
        } else {
            module = currentModule = data.module;
        }

        // just easier to read the table
        if (data.test === currentTest) {
            test = '';
        } else {
            test = currentTest = data.test;
        }

        table.push([module, test, data.message || '', data.result ? 'ok' : 'fail']);
    });

    log('\nAssertions:\n' + table.toString());
};

print.errors = function() {
    var errors = [];

    data.assertions.forEach(function(data) {
        if (!data.result) {
            errors.push(data);
        }
    });

    if (errors.length) {
        log('\n\nErrors:');
        errors.forEach(function(data) {
            log('\nModule: ' + data.module + ' Test: ' + data.test);
            if (data.message) {
                log(data.message);
            }

            if (data.source) {
                log(data.source);
            }

            if (data.expected != null || data.actual != null) {
                //it will be an error if data.expected !== data.actual, but if they're
                //both undefined, it means that they were just not filled out because
                //no assertions were hit (likely due to code error that would have been logged as source or message).
                log('Actual value:');
                log(data.actual);
                log('Expected value:');
                log(data.expected);
            }
        });
    }
};

print.tests = function() {
    var table,
        currentModule, module;

    table = new Table({
        head: ['Module', 'Test', 'Failed', 'Passed', 'Total']
    });

    data.tests.sort(function(a,b){if(a.module > b.module) return 1; if(a.module < b.module) return -1; return 0});

    data.tests.forEach(function(data) {
        // just easier to read the table
        if (data.module === currentModule) {
            module = '';
        } else {
            module = currentModule = data.module;
        }

        table.push([module, data.name, data.failed, data.passed, data.total]);
    });

    log('\nTests:\n' + table.toString());
};

// truncate file name
function truncFile(code) {
    if (code && code.length > fileColWidth) {
        code = '...' + code.slice(code.length - fileColWidth + 3);
    }
    return code;
}

print.summary = function() {
    var table;

    table = new Table({
        head: ['File', 'Failed', 'Passed', 'Total', 'Runtime']
    });

    data.summaries.forEach(function(data) {
        table.push([truncFile(data.code), data.failed, data.passed, data.total, data.runtime]);
    });

    log('\nSummary:\n' + table.toString());
};

print.globalSummary = function() {
    var table,
        data = exports.stats();

    table = new Table({
        head: ['Files', 'Tests', 'Assertions', 'Failed', 'Passed', 'Runtime']
    });

    table.push([data.files, data.tests, data.assertions, data.failed,
        data.passed, data.runtime]);

    log('\nGlobal summary:\n' + table.toString());
};

function getMet(metric) {
    function percent(covered, total) {
        var tmp;
        if (total > 0) {
            tmp = 1000 * 100 * covered / total + 5;
            return Math.floor(tmp / 10) / 100;
        } else {
            return 100.00;
        }
    }
    if (!metric.pct) metric.pct = percent(metric.covered, metric.total);
    return metric.pct + '% (' + metric.covered + '/' + metric.total + ')';
}

print.coverage = function() {
    var table;

    if (!data.coverages.length) return;

    table = new Table({
        head: ['File', 'Statements', 'Branches', 'Functions', 'Lines']
    });

    data.coverages.forEach(function(coverage) {
        table.push([
            truncFile(coverage.code),
            getMet(coverage.statements),
            getMet(coverage.branches),
            getMet(coverage.functions),
            getMet(coverage.lines)]);
    });

    log('\nCoverage:\n' + table.toString());
};

print.globalCoverage = function() {
    var coverage, table;

    if (!data.coverages.length) return;

    coverage = exports.stats().coverage;
    table = new Table({
        head: ['Files', 'Statements', 'Branches', 'Functions', 'Lines']
    });

    table.push([
        coverage.files,
        getMet(coverage.statements),
        getMet(coverage.branches),
        getMet(coverage.functions),
        getMet(coverage.lines)
    ]);

    log('\nGlobal coverage:\n' + table.toString());
};
<|endoftext|>


<commit_before>/**
 * var connection = new Connection({ tag:        'groonga',
 *                                   host:       'localhost',
 *                                   port:       24224,
 *                                   listenPort: 10030 });
 *
 * // for REST APIs
 * connection.emitMessage(
 *   { command: 'search',
 *     query:   'foobar' },
 *   function callback(result) {
 *     // codes to handle results from the backend
 *   }
 * );
 *
 * // for socket.io APIs
 * connection.on('notification', function listener(messageBody) { ... });
 * // volatile message
 * connection.emitMessage({ command: 'reindex' });
 */

var EventEmitter = require('events').EventEmitter;
var socketIo = require('socket.io');
var fluent = require('fluent-logger');

var DEFAULT_FLUENT_TAG  = 'groonga';
var DEFAULT_FLUENT_HOST = 'localhost';
var DEFAULT_FLUENT_PORT = 24224;
var DEFAULT_LISTEN_PORT = 10030;

function Connection(params) {
  this._params = params || {};
  this._init();
}

Connection.prototype = new EventEmitter();

Connection.prototype._init = function() {
  this._callbacks = {};

  this._sender = (
    this._params.sender ||
    fluent.createFluentSender(this._params.tag || DEFAULT_FLUENT_TAG,
                              { host: this._params.host || DEFAULT_FLUENT_HOST,
                                port: this._params.port || DEFAULT_FLUENT_PORT })
  );
  this._socket = socketIo.listen(this._params.listenPort || DEFAULT_LISTEN_PORT);

  var self = this;
  this._socket.sockets.on('connection', function(socket) {
    socket.on((self._params.tag || DEFAULT_FLUENT_TAG)+ '.message',
              self._handleMessage.bind(self));
  });
  this._socket.set('log level', 1);
};

Connection.prototype._handleMessage = function(message) {
  var replyTo = message['reply_to'];
  if (replyTo) {
    this.emit('replyTo:' + replyTo, message.body);
  }
};

function createId() {
  return Date.now() + ':' + Math.round(Math.random() * 65536);
}

Connection.prototype.emitMessage = function(message, callback) {
  var id = createId();
  var message = {
    id:   id,
    body: message
  };
  if (callback) {
    this.once('replyTo:' + id, callback);
  }
  this._sender.emit('message', message);
};

export.Connection = Connection;
<commit_msg>Make receiver socket injectable<commit_after>/**
 * var connection = new Connection({ tag:        'groonga',
 *                                   host:       'localhost',
 *                                   port:       24224,
 *                                   listenPort: 10030 });
 *
 * // for REST APIs
 * connection.emitMessage(
 *   { command: 'search',
 *     query:   'foobar' },
 *   function callback(result) {
 *     // codes to handle results from the backend
 *   }
 * );
 *
 * // for socket.io APIs
 * connection.on('notification', function listener(messageBody) { ... });
 * // volatile message
 * connection.emitMessage({ command: 'reindex' });
 */

var EventEmitter = require('events').EventEmitter;
var socketIo = require('socket.io');
var fluent = require('fluent-logger');

var DEFAULT_FLUENT_TAG  = 'groonga';
var DEFAULT_FLUENT_HOST = 'localhost';
var DEFAULT_FLUENT_PORT = 24224;
var DEFAULT_LISTEN_PORT = 10030;

function Connection(params) {
  this._params = params || {};
  this._init();
}

Connection.prototype = new EventEmitter();

Connection.prototype._init = function() {
  this._callbacks = {};
  this._initSender();
  this._initReceiver();
};

Connection.prototype._initSender = function() {
  this._sender = (
    this._params.sender ||
    fluent.createFluentSender(this._params.tag || DEFAULT_FLUENT_TAG,
                              { host: this._params.host || DEFAULT_FLUENT_HOST,
                                port: this._params.port || DEFAULT_FLUENT_PORT })
  );
};

Connection.prototype._initReceiver = function() {
  this._receiver = (
    this._params.receiver ||
    socketIo.listen(this._params.listenPort || DEFAULT_LISTEN_PORT)
  );

  this._receiver.sockets.on(
    'connection',
    (function(socket) {
      socket.on((this._params.tag || DEFAULT_FLUENT_TAG)+ '.message',
                this._handleMessage.bind(this));
    }).bind(this)
  );
  this._receiver.set('log level', 1);
};

Connection.prototype._handleMessage = function(message) {
  var replyTo = message['reply_to'];
  if (replyTo) {
    this.emit('replyTo:' + replyTo, message.body);
  }
};

function createId() {
  return Date.now() + ':' + Math.round(Math.random() * 65536);
}

Connection.prototype.emitMessage = function(message, callback) {
  var id = createId();
  var message = {
    id:   id,
    body: message
  };
  if (callback) {
    this.once('replyTo:' + id, callback);
  }
  this._sender.emit('message', message);
};

export.Connection = Connection;
<|endoftext|>