<commit_before>import { isQName } from "./qname";

import { isSeq } from "./seq";

// faux VNode
function vnode(inode,type,name,value){
	return {
		inode:inode,
		type:type,
		name:name,
		value:value,
		__is_VNode:true
	};
}

function _n(type, name, children){
	if(children === undefined) {
		children = [];
	} else if(isSeq(children)) {
		children = children.toArray();
	} else if(children.constructor != Array) {
		if(!children.__is_VNode) children = x(children);
		children = [children];
	}
	return vnode(function (parent, ref) {
		var ns;
		if(type == 1) {
			if(isQName(name)) {
				ns = name;
				name = name.name;
			} else if(/:/.test(name)){
				// TODO where are the namespaces?
			}
		}
		// convert to real VNode instance
		var node = parent.vnode(
			parent.emptyINode(type, name, type == 1 ? parent.emptyAttrMap() : undefined, ns),
			parent,
			parent.depth + 1,
			-1,
			type
		);
		for (let i = 0; i < children.length; i++) {
			let child = children[i];
			child = child.inode(node);
		}
		node = node.finalize();
		// insert into the parent means: update all parents until we come to the root
		// but the parents of my parent will be updated elsewhere
		// we just mutate the parent, because it was either cloned or newly created
		node.parent = parent.modify(node, ref);
		return node;
	}, type, name);
}

function _a(name, child) {
	return vnode(function (parent) {
		let node = parent.vnode(parent.ituple(name, child), parent);
		child = child.inode(node);
		node = node.finalize();
		if(parent.type == 1){
			// TODO conversion rules!
			parent.attr(name,node.value+"");
		} else if(parent.type == 6){
			// tuple
			parent.push(node);
		}
		return node;
	}, 2, name);
}

function _v(type,val) {
	return vnode(function (parent, ref) {
		// reuse insertIndex here to create a named map entry
		let node = parent.vnode(parent.ivalue(type, val), parent);
		// we don't want to do checks here
		// we just need to call a function that will insert the node into the parent
		node.parent = parent.modify(node,ref);
		return node;
	}, type, null, val);
}

/**
 * Create a provisional element VNode.
 * Once the VNode's inode function is called, the node is inserted into the parent at the specified index
 * @param  {[type]} name     [description]
 * @param  {[type]} children [description]
 * @return {[type]}          [description]
 */
export function e(name, children) {
	return _n(1, name, children);
}

export function l(children) {
	return _n(5, null, children);
}

export function m(children){
	return _n(6, null, children);
}

export function a(name, value){
	return _a(name,value);
}

export function p(target, content){
	return _v(7, target+" "+content);
}

export function x(value = null) {
	return _v(typeof value == "string" ? 3 : 12, value);
}

export function c(value){
	return _v(8, value);
}
<commit_msg>construct tuples from a()<commit_after>import { isQName } from "./qname";

import { first, isSeq } from "./seq";

// faux VNode
function vnode(inode,type,name,value){
	return {
		inode:inode,
		type:type,
		name:name,
		value:value,
		__is_VNode:true
	};
}

function _n(type, name, children){
	if(children === undefined) {
		children = [];
	} else if(isSeq(children)) {
		children = children.toArray();
	} else if(children.constructor != Array) {
		if(!children.__is_VNode) children = x(children);
		children = [children];
	}
	return vnode(function (parent, ref) {
		var ns;
		if(type == 1) {
			if(isQName(name)) {
				ns = name;
				name = name.name;
			} else if(/:/.test(name)){
				// TODO where are the namespaces?
			}
		}
		// convert to real VNode instance
		var node = parent.vnode(
			parent.emptyINode(type, name, type == 1 ? parent.emptyAttrMap() : undefined, ns),
			parent,
			parent.depth + 1,
			0,
			type
		);
		for (let i = 0; i < children.length; i++) {
			let child = children[i];
			child = child.inode(node);
		}
		node = node.finalize();
		// insert into the parent means: update all parents until we come to the root
		// but the parents of my parent will be updated elsewhere
		// we just mutate the parent, because it was either cloned or newly created
		node.parent = parent.modify(node, ref);
		return node;
	}, type, name);
}

function _a(name, child) {
	if(isSeq(child)) {
		child = first(child);
	} else if(Array.isArray(child)){
		child = child[0];
	} else if(!child.__is_VNode) {
		child = x(child);
	}
	return vnode(function (parent) {
		var node = parent.vnode(parent.ituple(name, child), parent);
		// node is an attr node /w child as $val
		// push node to
		child = child.inode(node);
		//node.type = child.type;
		node = node.finalize();
		if (parent.type == 1) {
			// TODO conversion rules!
			parent.attr(name, node.value + "");
		} else if (parent.type == 6) {
			// tuple
			parent.push([node.inode.$name,node.inode.$value]);
		}
		return node;
	}, 2, name);
}

function _v(type,val) {
	return vnode(function (parent, ref) {
		// reuse insertIndex here to create a named map entry
		let node = parent.vnode(parent.ivalue(type, val), parent);
		// we don't want to do checks here
		// we just need to call a function that will insert the node into the parent
		node.parent = parent.modify(node,ref);
		return node;
	}, type, null, val);
}

/**
 * Create a provisional element VNode.
 * Once the VNode's inode function is called, the node is inserted into the parent at the specified index
 * @param  {[type]} name     [description]
 * @param  {[type]} children [description]
 * @return {[type]}          [description]
 */
export function e(name, children) {
	return _n(1, name, children);
}

export function l(children) {
	return _n(5, null, children);
}

export function m(children){
	return _n(6, null, children);
}

export function a(name, value){
	return _a(name,value);
}

export function p(target, content){
	return _v(7, target+" "+content);
}

export function x(value = null) {
	return _v(typeof value == "string" ? 3 : 12, value);
}

export function c(value){
	return _v(8, value);
}
<|endoftext|>


<commit_before>/**
 *
 * @package     Converter
 * @version     1.0
 * @license     http://mit-license.org/
 * @author      Tapakan https://github.com/Tapakan
 * @coder       Alexander Oganov <t_tapak@yahoo.com>
 */

var Converter = function() {};

(function (Converter) {
  "use strict";

  /**
   * Init with currencies params.
   * @param settings
   */
  Converter.prototype.init = function (settings) {
    if (typeof settings != 'object') {
      throw new Error('Settings must be an instance of object.');
    }

    this.settings = settings;
  };

  /**
   * Returns symbol for currency.
   * @param currency
   * @returns {string|string|string|*}
   */
  Converter.prototype.getSymbol = function (currency) {
    if (!this._checkCurrency(currency)) {
      throw new Error("Currency " + currency + " does\'t exists.");
    }

    return this.settings[currency].symbol;
  };

  /**
   * Returns rate for currency.
   * @param currency
   * @returns {number}
   */
  Converter.prototype.getRate = function (currency) {
    if (!this._checkCurrency(currency)) {
      throw new Error("Currency " + currency + " does\'t exists.");
    }

    return this.settings[currency].rate;
  };

  /**
   * Convert value from currency to currency.
   * Before converting check currency.
   * @param {string} from
   * @param {string} to
   * @param {float|int} value
   */
  Converter.prototype.value = function (from, to, value) {
    if (!this._checkCurrency(from) || !this._checkCurrency(to)) {
      throw new Error("Invalid currency.");
    }

    return this._convert(from, to, value);
  };

  /**
   * Check currency for existing.
   * @param {string} currency
   * @returns {boolean}
   * @private
   */
  Converter.prototype._checkCurrency = function(currency) {
    return this.settings.hasOwnProperty(currency);
  };

  /**
   * Converting value from one currency to other currency.
   * @param {string} from
   * @param {string} to
   * @param {float|int} value
   * @private
   */
  Converter.prototype._convert = function(from, to, value) {
    var newValue = value;
    if (from != to) {
      newValue = value / this.settings[from].rate;
      newValue = newValue * this.settings[to].rate;
    }

    return newValue;
  }

})(Converter);

window.Converter = new Converter();
<commit_msg>Refactoring<commit_after>/**
 * Converter.
 *
 * @package     Converter
 * @version     1.0
 * @license     http://mit-license.org/
 * @author      Tapakan https://github.com/Tapakan
 * @coder       Alexander Oganov <t_tapak@yahoo.com>
 */

/**
 * Converter lib.
 */
(function (window, document) {

    "use strict";

    var Converter = function () {
    };

    Converter.prototype = {

        /**
         * Init with currencies params.
         *
         *
         * @param settings
         */
        init: function (settings) {
            if (typeof settings != 'object') {
                throw new Error('Settings must be an instance of object.');
            }

            this.settings = settings;
        },

        /**
         * Returns symbol for currency.
         *
         * @param currency
         * @returns {string|string|string|*}
         */
        getSymbol: function (currency) {
            if (!this._checkCurrency(currency)) {
                throw new Error("Currency " + currency + " does\'t exists.");
            }

            return this.settings[currency].symbol;
        },

        /**
         * Returns rate for currency.
         *
         * @param currency
         * @returns {number}
         */
        getRate: function (currency) {
            if (!this._checkCurrency(currency)) {
                throw new Error("Currency " + currency + " does\'t exists.");
            }

            return this.settings[currency].rate;
        },

        /**
         * Convert value from currency to currency.
         * Before converting check currency.
         *
         * @param {string} from
         * @param {string} to
         * @param {float|int} value
         */
        value: function (from, to, value) {
            if (!this._checkCurrency(from) || !this._checkCurrency(to)) {
                throw new Error("Invalid currency.");
            }

            return this._convert(from, to, value);
        },

        /**
         * Check currency for existing.
         *
         * @param {string} currency
         * @returns {boolean}
         * @private
         */
        _checkCurrency: function (currency) {
            return this.settings.hasOwnProperty(currency);
        },

        /**
         * Converting value from one currency to other currency.
         *
         * @param {string} from
         * @param {string} to
         * @param {float|int} value
         * @private
         */
        _convert: function (from, to, value) {
            var newValue = value;
            if (from != to) {
                newValue = value / this.settings[from].rate;
                newValue = newValue * this.settings[to].rate;
            }

            return newValue;
        }

    };

    window.Converter = new Converter();

})(window, document);
<|endoftext|>


<commit_before>/**
 * @author bhouston / http://exocortex.com
 */

THREE.Box3 = function ( min, max ) {

	if ( min === undefined && max === undefined ) {

		this.min = new THREE.Vector3();
		this.max = new THREE.Vector3();
		this.makeEmpty();

	} else {

		this.min = min || new THREE.Vector3();
		this.max = max || new THREE.Vector3().copy( this.min ); // This is done on purpose so you can make a box using a single point and then expand it.

	}

};

THREE.Box3.prototype = {

	constructor: THREE.Box3,

	set: function ( min, max ) {

		this.min = min;
		this.max = max;

		return this;

	},

	setFromPoints: function ( points ) {

		if ( points.length > 0 ) {

			var p = points[ 0 ];

			this.min.copy( p );
			this.max.copy( p );

			for ( var i = 1, il = points.length; i < il; i ++ ) {

				p = points[ i ];

				if ( p.x < this.min.x ) {

					this.min.x = p.x;

				} else if ( p.x > this.max.x ) {

					this.max.x = p.x;

				}

				if ( p.y < this.min.y ) {

					this.min.y = p.y;

				} else if ( p.y > this.max.y ) {

					this.max.y = p.y;

				}

				if ( p.z < this.min.z ) {

					this.min.z = p.z;

				} else if ( p.z > this.max.z ) {

					this.max.z = p.z;

				}

			}

		} else {

			this.makeEmpty();

		}

		return this;

	},

	setFromCenterAndSize: function ( center, size ) {

		var halfSize = THREE.Box3.__v1.copy( size ).multiplyScalar( 0.5 );

		this.min.copy( center ).subSelf( halfSize );
		this.max.copy( center ).addSelf( halfSize );

		return this;

	},

	copy: function ( box ) {

		this.min.copy( box.min );
		this.max.copy( box.max );

		return this;

	},

	makeEmpty: function () {

		this.min.x = this.min.y = this.min.z = Infinity;
		this.max.x = this.max.y = this.max.z = -Infinity;

		return this;

	},

	empty: function () {

		// this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes

		return ( this.max.x < this.min.x ) || ( this.max.y < this.min.y ) || ( this.max.z < this.min.z );

	},

	volume: function () {

		return ( this.max.x - this.min.x ) * ( this.max.y - this.min.y ) * ( this.max.z - this.min.z );

	},

	center: function () {

		return new THREE.Vector3().add( this.min, this.max ).multiplyScalar( 0.5 );

	},

	size: function () {

		return new THREE.Vector3().sub( this.max, this.min );

	},

	expandByPoint: function ( point ) {

		this.min.minSelf( point );
		this.max.maxSelf( point );

		return this;

	},

	expandByVector: function ( vector ) {

		this.min.subSelf( vector );
		this.max.addSelf( vector );

		return this;

	},

	expandByScalar: function ( scalar ) {

		this.min.addScalar( -scalar );
		this.max.addScalar( scalar );

		return this;

	},

	containsPoint: function ( point ) {

		if ( ( this.min.x <= point.x ) && ( point.x <= this.max.x ) &&
			 ( this.min.y <= point.y ) && ( point.y <= this.max.y ) &&
			 ( this.min.z <= point.z ) && ( point.z <= this.max.z ) ) {

			return true;

		}

		return false;

	},

	containsBox: function ( box ) {

		if ( ( this.min.x <= box.min.x ) && ( box.max.x <= this.max.x ) &&
			 ( this.min.y <= box.min.y ) && ( box.max.y <= this.max.y ) &&
			 ( this.min.z <= box.min.z ) && ( box.max.z <= this.max.z ) ) {

			return true;

		}

		return false;

	},

	getParameter: function ( point ) {

		// This can potentially have a divide by zero if the box
		// has a size dimension of 0.
		return new THREE.Vector3(
			( point.x - this.min.x ) / ( this.max.x - this.min.x ),
			( point.y - this.min.y ) / ( this.max.y - this.min.y ),
			( point.z - this.min.z ) / ( this.max.z - this.min.z )
		);

	},

	isIntersection: function ( box ) {

		// using 6 splitting planes to rule out intersections.

		if ( ( this.max.x < box.min.x ) || ( box.min.x > this.max.x ) ||
			 ( this.max.y < box.min.y ) || ( box.min.y > this.max.y ) ||
			 ( this.max.z < box.min.z ) || ( box.min.z > this.max.z ) ) {

			return false;

		}

		return true;

	},

	clampPoint: function ( point ) {

		return new THREE.Vector3().copy( point ).clampSelf( this.min, this.max );

	},

	distanceToPoint: function ( point ) {

		return this.clampPoint( point ).subSelf( point ).length();

	},

	intersect: function ( box ) {

		this.min.maxSelf( box.min );
		this.max.minSelf( box.max );

		return this;

	},

	union: function ( box ) {

		this.min.minSelf( box.min );
		this.max.maxSelf( box.max );

		return this;

	},

	translate: function ( offset ) {

		this.min.addSelf( offset );
		this.max.addSelf( offset );

		return this;

	},

	scale: function ( factor ) {

		var sizeDeltaHalf = this.size().multiplyScalar( ( 1 - factor )  * 0.5 );
		this.expandByVector( sizeDeltaHalf );

		return this;

	},

	equals: function ( box ) {

		return box.min.equals( this.min ) && box.max.equals( this.max );

	},

	clone: function () {

		return new THREE.Box3().copy( this );

	}

};

THREE.Box3.__v1 = new THREE.Vector3();
<commit_msg>Merging with @bhouston.<commit_after>/**
 * @author bhouston / http://exocortex.com
 */

THREE.Box3 = function ( min, max ) {

	if( min == undefined && max === undefined ) {

		this.min = new THREE.Vector3();
		this.max = new THREE.Vector3();
		this.makeEmpty();
	}
	else {
		this.min = min || new THREE.Vector3();
		this.max = max || new THREE.Vector3().copy( this.min ); // This is done on purpose so you can make a box using a single point and then expand it.
	}
};

THREE.Box3.prototype = {

	constructor: THREE.Box3,

	set: function ( min, max ) {

		this.min = min;
		this.max = max;

		return this;

	},

	setFromPoints: function ( points ) {

		if ( points.length > 0 ) {

			var p = points[ 0 ];

			this.min.copy( p );
			this.max.copy( p );

			for ( var i = 1, il = points.length; i < il; i ++ ) {

				p = points[ i ];

				if ( p.x < this.min.x ) {

					this.min.x = p.x;

				} else if ( p.x > this.max.x ) {

					this.max.x = p.x;

				}

				if ( p.y < this.min.y ) {

					this.min.y = p.y;

				} else if ( p.y > this.max.y ) {

					this.max.y = p.y;

				}

				if ( p.z < this.min.z ) {

					this.min.z = p.z;

				} else if ( p.z > this.max.z ) {

					this.max.z = p.z;

				}

			}

		} else {

			this.makeEmpty();

		}

		return this;
	},

	setFromCenterAndSize: function ( center, size ) {

		var halfSize = THREE.Box3.__v1.copy( size ).multiplyScalar( 0.5 );

		this.min.copy( center ).subSelf( halfSize );
		this.max.copy( center ).addSelf( halfSize );

		return this;

	},

	copy: function ( box ) {

		this.min.copy( box.min );
		this.max.copy( box.max );

		return this;

	},

	makeEmpty: function () {

		this.min.x = this.min.y = this.min.z = Infinity;
		this.max.x = this.max.y = this.max.z = -Infinity;

		return this;

	},

	empty: function () {

		// this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes
		return ( this.max.x < this.min.x ) || ( this.max.y < this.min.y ) || ( this.max.z < this.min.z );

	},

	volume: function () {

		return ( this.max.x - this.min.x ) * ( this.max.y - this.min.y ) * ( this.max.z - this.min.z );

	},

	center: function () {

		return new THREE.Vector3().add( this.min, this.max ).multiplyScalar( 0.5 );

	},

	size: function () {

		return new THREE.Vector3().sub( this.max, this.min );

	},

	expandByPoint: function ( point ) {

		this.min.minSelf( point );
		this.max.maxSelf( point );

		return this;

	},

	expandByVector: function ( vector ) {

		this.min.subSelf( vector );
		this.max.addSelf( vector );

		return this;

	},

	expandByScalar: function ( scalar ) {

		this.min.addScalar( -scalar );
		this.max.addScalar( scalar );

		return this;

	},

	containsPoint: function ( point ) {

		if ( ( this.min.x <= point.x ) && ( point.x <= this.max.x ) &&
			( this.min.y <= point.y ) && ( point.y <= this.max.y ) &&
			( this.min.z <= point.z ) && ( point.z <= this.max.z ) ) {

			return true;

		}

		return false;

	},

	containsBox: function ( box ) {

		if ( ( this.min.x <= box.min.x ) && ( box.max.x <= this.max.x ) &&
			( this.min.y <= box.min.y ) && ( box.max.y <= this.max.y ) &&
			( this.min.z <= box.min.z ) && ( box.max.z <= this.max.z ) ) {

			return true;

		}

		return false;

	},

	getParameter: function ( point ) {

		// This can potentially have a divide by zero if the box
		// has a size dimension of 0.
		return new THREE.Vector3(
			( point.x - this.min.x ) / ( this.max.x - this.min.x ),
			( point.y - this.min.y ) / ( this.max.y - this.min.y ),
			( point.z - this.min.z ) / ( this.max.z - this.min.z )
		);

	},

	isIntersection: function ( box ) {

		// using 6 splitting planes to rule out intersections.

		if ( ( this.max.x < box.min.x ) || ( box.min.x > this.max.x ) ||
			( this.max.y < box.min.y ) || ( box.min.y > this.max.y ) ||
			( this.max.z < box.min.z ) || ( box.min.z > this.max.z ) ) {

			return false;

		}

		return true;

	},

	clampPoint: function ( point ) {

		return new THREE.Vector3().copy( point ).clampSelf( this.min, this.max );

	},

	distanceToPoint: function ( point ) {

		return this.clampPoint( point ).subSelf( point ).length();

	},

	intersect: function ( box ) {

		this.min.maxSelf( box.min );
		this.max.minSelf( box.max );

		return this;

	},

	union: function ( box ) {

		this.min.minSelf( box.min );
		this.max.maxSelf( box.max );

		return this;

	},

	translate: function ( offset ) {

		this.min.addSelf( offset );
		this.max.addSelf( offset );

		return this;

	},

	scale: function ( factor ) {

		var sizeDeltaHalf = this.size().multiplyScalar( ( 1 - factor )  * 0.5 );
		this.expandByVector( sizeDeltaHalf );

		return this;

	},

	equals: function ( box ) {

		return box.min.equals( this.min ) && box.max.equals( this.max );

	},

	clone: function () {

		return new THREE.Box3().copy( this );

	}

};

THREE.Box3.__v1 = new THREE.Vector3();
<|endoftext|>


<commit_before>var CONST = require('./const'),
    utils = require('./utils'),
    config = require('./config'),
    AssetLoader = require('../loader/AssetLoader'),
    DataManager = require('../extra/DataManager'),
    InputManager = require('../input/InputManager'),
    AudioManager = require('../audio/AudioManager'),
    autoDetectRenderer = require('../../lib/pixi/src/core').autoDetectRenderer,
    SceneManager = require('./SceneManager'),
    GameTime = require('./GameTime'),
    Device = require('./Device');

/**
 * The main object of your game.
 * @class
 * @memberof PQ
 * @param [width=800]
 * @param [height=600]
 * @param [options] {DEFAULT_GAME_OPTIONS}
 */
function Game(width, height, options){
    this._init(width, height, options);
}

Game.prototype.constructor = Game;

Game.prototype._init = function(width, height, options){
    if(typeof width === "object"){
        options = width;
        width = null;
        height = null;
    }
    /**
     * The config of the game
     *
     * @member {object}
     * @default CONST.DEFAULT_GAME_OPTIONS
     */
    this.config = parseConfig(options);
    var rendererOptions = parseRendererConfig(CONST.DEFAULT_RENDER_OPTIONS, this.config);

    /**
     * The renderer width
     * @member {number}
     */
    this.width = width || this.config.game.width;

    /**
     * The renderer height
     * @member {number}
     */
    this.height = height || this.config.game.height;

    /**
     * Bundle id for this game
     * @member {string}
     */
    this.id = this.config.id;

    /**
     * Version of this game
     * @member {string}
     */
    this.version = this.config.version;

    /**
     * Renderer in use
     * @member {WebGLRenderer|CanvasRenderer}
     */
    this.renderer = getRenderer(this.width, this.height, rendererOptions, !this.config.game.useWebGL);
    this.resize(this.width, this.height);

    /**
     * Canvas element
     * @member {HTMLCanvasElement}
     */
    this.canvas = this.renderer.view;

    this.time = new GameTime(this, this.animate.bind(this));

    /**
     * Whether the renderer is a webgl
     * @member {boolean}
     */
    this.isWebGL = (this.renderer.type === CONST.RENDERER_TYPE.WEBGL);

    /**
     * Is webAudio using in this game
     * @type {boolean}
     */
    this.isWebAudio = (Device.hasWebAudio && this.config.audio.useWebAudio);

    /**
     * The scene manager for this game
     * @member {SceneManager}
     */
    this.sceneManager = new SceneManager(this);

    /**
     * The audio manager for this game
     * @type {AudioManager}
     */
    this.audioManager = new AudioManager(this);

    /**
     * Basic loader
     * @member {AssetLoader}
     */
    this.assetLoader = new AssetLoader(this);

    /**
     * Manage all data, using localstorage or not
     * @type {DataManager}
     */
    this.dataManager = new DataManager(this);

    /**
     * The manager for mouse, keyboard, etc...
     * @type {InputManager}
     */
    this.inputManager = new InputManager(this);

    /**
     * Store the resize method
     * @member {null|function}
     * @private
     */
    this._gameResizeListener = null;

    /**
     * Stop the game when it lost the focus
     */
    if(this.config.game.stopAtVisibilityChange){
        utils.watchVisibilityChanges(this);
    }

    /**
     * Autoscale the gamescreen
     */
    if(this.config.game.scaleType !== CONST.GAME_SCALE_TYPE.NONE){
        this.enableAutoResize(true);
    }

    if(this.config.sayHello){
        this.sayHello();
    }
};

/**
 * Start the request animation frame
 * @returns {Game}
 */
Game.prototype.start = function(){
    this.time.start();
    this.audioManager.resumeAllLines();
    return this;
};

/**
 * Stop the request animation frame
 * @returns {Game}
 */
Game.prototype.stop = function(){
    this.time.stop();
    this.audioManager.pauseAllLines();
    return this;
};

Game.prototype.update = function(gameTime){

};

Game.prototype.postUpdate = function(gameTime){

};

/**
 * Draw and animate all the actors in the scene
 * @returns {Game}
 */
Game.prototype.animate = function(){
    this.update(this.time);

    this.renderer.render(this.sceneManager);
    this.sceneManager.animate(this.time);
    cleanObjects();

    this.inputManager.update(this.time);

    this.postUpdate(this.time);
    return this;
};

/**
 * Resize the view using DOM Style
 * @param width {number}
 * @param height {number}
 * @param renderer {boolean}
 * @returns {Game}
 */
Game.prototype.resize = function(width, height, renderer){
    var canvas = this.renderer.view;

    if(renderer){
        this.renderer.resize(width, height);
        this.width = width;
        this.height = height;
    }

    canvas.style.width = width + 'px';
    canvas.style.height = height + 'px';

    if(this.sceneManager)this._dispatchOnResize(width, height);
    return this;
};

Game.prototype._dispatchOnResize = function(width, height){
    if(this.onResize){
        this.onResize(width, height);
    }

    var len = this.sceneManager.scenes.length;
    for(var i = 0; i < len; i++){
        if(this.sceneManager.scenes[i].onResize){
            this.sceneManager.scenes[i].onResize(width, height);
        }
    }
};

/**
 * Stop or start the game when the focus is in or out
 * @param hidden {boolean}
 * @returns {Game}
 */
Game.prototype.visibilityChange = function(hidden){
    if(this.config.game.stopAtVisibilityChange){
        if(hidden){
            this.stop();
        }else{
            this.start();
        }
    }
    return this;
};

/**
 * Rules to define how to resize the game screen
 * @param [value] {boolean}
 * @param [mode] {GAME_SCALE_TYPE}
 * @returns {Game}
 */
Game.prototype.enableAutoResize = function(value, mode){
    mode = mode || this.config.game.scaleType;
    value = (value !== false);
    var scope = this,
        canvas = this.renderer.view;

    //Remove previous listeners
    if(this._gameResizeListener){
        window.removeEventListener('resize', this._gameResizeListener);
        this._gameResizeListener = null;
    }

    //Nothing to do here
    if(mode === CONST.GAME_SCALE_TYPE.NONE||!value){
        return this;
    }

    switch(mode){
        case CONST.GAME_SCALE_TYPE.ASPECT_FIT:
            this._gameResizeListener = function(e){
                var ww = parseInt(canvas.style.width,10) || canvas.width;
                var hh = parseInt(canvas.style.height,10) || canvas.height;

                if (window.innerWidth < ww || window.innerHeight < hh || window.innerWidth > ww || window.innerHeight > hh) {
                    var scale = Math.min(window.innerWidth/scope.width, window.innerHeight/scope.height);
                    scope.resize(scope.width*scale, scope.height*scale);
                }
            };
            break;
        case CONST.GAME_SCALE_TYPE.ASPECT_FILL:
            this._gameResizeListener = function(e){
                //TODO: Revisar en moviles
                var ww = parseInt(canvas.style.width,10) || canvas.width;
                var hh = parseInt(canvas.style.height,10) || canvas.height;

                if (window.innerWidth < ww || window.innerHeight < hh || window.innerWidth > ww || window.innerHeight > hh) {
                    var scale = Math.max(window.innerWidth/scope.width, window.innerHeight/scope.height);
                    var width = scope.width*scale,
                        height = scope.height*scale;
                    scope.resize(width, height);

                    var topMargin = (window.innerHeight-height)/2;
                    var leftMargin = (window.innerWidth-width)/2;

                    canvas.style['margin-top'] = topMargin + 'px';
                    canvas.style['margin-left'] = leftMargin + 'px';
                }
            };
            break;
        case CONST.GAME_SCALE_TYPE.FILL:
            this._gameResizeListener = function(e){
                var ww = parseInt(canvas.style.width,10) || canvas.width;
                var hh = parseInt(canvas.style.height,10) || canvas.height;

                if (window.innerWidth !== ww || window.innerHeight !== hh) {
                    scope.resize(window.innerWidth, window.innerHeight);
                }
            };
            break;
    }

    window.addEventListener('resize', this._gameResizeListener);
    this._gameResizeListener();

    return this;
};

Game.prototype.setFullScreen = function(value){
    if(!Device.hasFullScreen)return this;
    value = (value !== false);

    if(value){
        this.canvas[Device.fullScreenRequest]();
    }else{
        document[Device.fullScreenCancel]();
    }

    return this;

};

Game.prototype.sayHello = function(){
    var renderer = this.isWebGL ? "WebGL" : "Canvas",
        audio = this.isWebAudio ? "WebAudio" : "HTMLAudio";

    //console.log('Powered by Perenquen v'+CONST.VERSION + ' (' + renderer + ' - ' + audio + ') [http://perenquen.tarentoladigital.com]');
    console.log(
        "Powered by Perenquen v" + CONST.VERSION + " [http://perenquen.tarentoladigital.com]\n" +
        "and Pixi.js v" + CONST.PIXI_VERSION + " [http://www.pixijs.com]\n\n\n" +
        "Renderer: " + renderer + "\n" +
        "Audio: " + audio
    );
    return this;
};

module.exports = Game;

Object.defineProperties(Game.prototype, {
    scene : {
        get: function(){
            return this.sceneManager.currentScene;
        }
    },

    mouse: {
        get: function(){
            return this.inputManager.mouse;
        }
    },

    keyboard: {
        get: function(){
            return this.inputManager.keyboard;
        }
    },

    gamepad: {
        get: function(){
            return this.inputManager.gamepad;
        }
    }
});

function getRenderer(width, height, options, noWebGL){
    if(navigator.isCocoonJS&&!options.view){
        options.view = window.document.createElement("screencanvas");
    }

    var renderer = new autoDetectRenderer(width, height, options, noWebGL);
    if(!options.view)window.document.body.appendChild(renderer.view);

    return renderer;
}

function parseConfig(options){
    var cfg = utils.defaultObject(CONST.DEFAULT_GAME_OPTIONS, options);
    config.useSortChildrenByDepth = cfg.useSortChildrenByDepth;
    return cfg;
}

function parseRendererConfig(defaultConfig, config){
    var cfg = JSON.parse(JSON.stringify(defaultConfig));

    for(var key in config.game){
        switch(key){
            case "autoResize":
            case "clearBeforeRenderer":
            case "forceFXAA":
            case "preserveDrawingBuffer":
            case "backgroundColor":
            case "resolution":
                cfg[key] = config.game[key];
                break;
            case "canvas":
                cfg.view = config.game[key];
                break;
            case "transparentBackground":
                cfg.transparent = config.game[key];
                break;
            case "useAntialias":
                cfg.antialias = config.game[key];
                break;
        }
    }

    return cfg;
}

function cleanObjects(){
    if(config._killedObjects.length){
        for(var i = 0; i < config._killedObjects.length; i++){
            config._killedObjects[i].remove();
        }
        config._killedObjects.length = 0;
    }
}<commit_msg>move draw after animate<commit_after>var CONST = require('./const'),
    utils = require('./utils'),
    config = require('./config'),
    AssetLoader = require('../loader/AssetLoader'),
    DataManager = require('../extra/DataManager'),
    InputManager = require('../input/InputManager'),
    AudioManager = require('../audio/AudioManager'),
    autoDetectRenderer = require('../../lib/pixi/src/core').autoDetectRenderer,
    SceneManager = require('./SceneManager'),
    GameTime = require('./GameTime'),
    Device = require('./Device');

/**
 * The main object of your game.
 * @class
 * @memberof PQ
 * @param [width=800]
 * @param [height=600]
 * @param [options] {DEFAULT_GAME_OPTIONS}
 */
function Game(width, height, options){
    this._init(width, height, options);
}

Game.prototype.constructor = Game;

Game.prototype._init = function(width, height, options){
    if(typeof width === "object"){
        options = width;
        width = null;
        height = null;
    }
    /**
     * The config of the game
     *
     * @member {object}
     * @default CONST.DEFAULT_GAME_OPTIONS
     */
    this.config = parseConfig(options);
    var rendererOptions = parseRendererConfig(CONST.DEFAULT_RENDER_OPTIONS, this.config);

    /**
     * The renderer width
     * @member {number}
     */
    this.width = width || this.config.game.width;

    /**
     * The renderer height
     * @member {number}
     */
    this.height = height || this.config.game.height;

    /**
     * Bundle id for this game
     * @member {string}
     */
    this.id = this.config.id;

    /**
     * Version of this game
     * @member {string}
     */
    this.version = this.config.version;

    /**
     * Renderer in use
     * @member {WebGLRenderer|CanvasRenderer}
     */
    this.renderer = getRenderer(this.width, this.height, rendererOptions, !this.config.game.useWebGL);
    this.resize(this.width, this.height);

    /**
     * Canvas element
     * @member {HTMLCanvasElement}
     */
    this.canvas = this.renderer.view;

    this.time = new GameTime(this, this.animate.bind(this));

    /**
     * Whether the renderer is a webgl
     * @member {boolean}
     */
    this.isWebGL = (this.renderer.type === CONST.RENDERER_TYPE.WEBGL);

    /**
     * Is webAudio using in this game
     * @type {boolean}
     */
    this.isWebAudio = (Device.hasWebAudio && this.config.audio.useWebAudio);

    /**
     * The scene manager for this game
     * @member {SceneManager}
     */
    this.sceneManager = new SceneManager(this);

    /**
     * The audio manager for this game
     * @type {AudioManager}
     */
    this.audioManager = new AudioManager(this);

    /**
     * Basic loader
     * @member {AssetLoader}
     */
    this.assetLoader = new AssetLoader(this);

    /**
     * Manage all data, using localstorage or not
     * @type {DataManager}
     */
    this.dataManager = new DataManager(this);

    /**
     * The manager for mouse, keyboard, etc...
     * @type {InputManager}
     */
    this.inputManager = new InputManager(this);

    /**
     * Store the resize method
     * @member {null|function}
     * @private
     */
    this._gameResizeListener = null;

    /**
     * Stop the game when it lost the focus
     */
    if(this.config.game.stopAtVisibilityChange){
        utils.watchVisibilityChanges(this);
    }

    /**
     * Autoscale the gamescreen
     */
    if(this.config.game.scaleType !== CONST.GAME_SCALE_TYPE.NONE){
        this.enableAutoResize(true);
    }

    if(this.config.sayHello){
        this.sayHello();
    }
};

/**
 * Start the request animation frame
 * @returns {Game}
 */
Game.prototype.start = function(){
    this.time.start();
    this.audioManager.resumeAllLines();
    return this;
};

/**
 * Stop the request animation frame
 * @returns {Game}
 */
Game.prototype.stop = function(){
    this.time.stop();
    this.audioManager.pauseAllLines();
    return this;
};

Game.prototype.update = function(gameTime){

};

Game.prototype.postUpdate = function(gameTime){

};

/**
 * Draw and animate all the actors in the scene
 * @returns {Game}
 */
Game.prototype.animate = function(){
    this.update(this.time);

    this.sceneManager.animate(this.time);
    cleanObjects();

    this.renderer.render(this.sceneManager);
    this.inputManager.update(this.time);

    this.postUpdate(this.time);
    return this;
};

/**
 * Resize the view using DOM Style
 * @param width {number}
 * @param height {number}
 * @param renderer {boolean}
 * @returns {Game}
 */
Game.prototype.resize = function(width, height, renderer){
    var canvas = this.renderer.view;

    if(renderer){
        this.renderer.resize(width, height);
        this.width = width;
        this.height = height;
    }

    canvas.style.width = width + 'px';
    canvas.style.height = height + 'px';

    if(this.sceneManager)this._dispatchOnResize(width, height);
    return this;
};

Game.prototype._dispatchOnResize = function(width, height){
    if(this.onResize){
        this.onResize(width, height);
    }

    var len = this.sceneManager.scenes.length;
    for(var i = 0; i < len; i++){
        if(this.sceneManager.scenes[i].onResize){
            this.sceneManager.scenes[i].onResize(width, height);
        }
    }
};

/**
 * Stop or start the game when the focus is in or out
 * @param hidden {boolean}
 * @returns {Game}
 */
Game.prototype.visibilityChange = function(hidden){
    if(this.config.game.stopAtVisibilityChange){
        if(hidden){
            this.stop();
        }else{
            this.start();
        }
    }
    return this;
};

/**
 * Rules to define how to resize the game screen
 * @param [value] {boolean}
 * @param [mode] {GAME_SCALE_TYPE}
 * @returns {Game}
 */
Game.prototype.enableAutoResize = function(value, mode){
    mode = mode || this.config.game.scaleType;
    value = (value !== false);
    var scope = this,
        canvas = this.renderer.view;

    //Remove previous listeners
    if(this._gameResizeListener){
        window.removeEventListener('resize', this._gameResizeListener);
        this._gameResizeListener = null;
    }

    //Nothing to do here
    if(mode === CONST.GAME_SCALE_TYPE.NONE||!value){
        return this;
    }

    switch(mode){
        case CONST.GAME_SCALE_TYPE.ASPECT_FIT:
            this._gameResizeListener = function(e){
                var ww = parseInt(canvas.style.width,10) || canvas.width;
                var hh = parseInt(canvas.style.height,10) || canvas.height;

                if (window.innerWidth < ww || window.innerHeight < hh || window.innerWidth > ww || window.innerHeight > hh) {
                    var scale = Math.min(window.innerWidth/scope.width, window.innerHeight/scope.height);
                    scope.resize(scope.width*scale, scope.height*scale);
                }
            };
            break;
        case CONST.GAME_SCALE_TYPE.ASPECT_FILL:
            this._gameResizeListener = function(e){
                //TODO: Revisar en moviles
                var ww = parseInt(canvas.style.width,10) || canvas.width;
                var hh = parseInt(canvas.style.height,10) || canvas.height;

                if (window.innerWidth < ww || window.innerHeight < hh || window.innerWidth > ww || window.innerHeight > hh) {
                    var scale = Math.max(window.innerWidth/scope.width, window.innerHeight/scope.height);
                    var width = scope.width*scale,
                        height = scope.height*scale;
                    scope.resize(width, height);

                    var topMargin = (window.innerHeight-height)/2;
                    var leftMargin = (window.innerWidth-width)/2;

                    canvas.style['margin-top'] = topMargin + 'px';
                    canvas.style['margin-left'] = leftMargin + 'px';
                }
            };
            break;
        case CONST.GAME_SCALE_TYPE.FILL:
            this._gameResizeListener = function(e){
                var ww = parseInt(canvas.style.width,10) || canvas.width;
                var hh = parseInt(canvas.style.height,10) || canvas.height;

                if (window.innerWidth !== ww || window.innerHeight !== hh) {
                    scope.resize(window.innerWidth, window.innerHeight);
                }
            };
            break;
    }

    window.addEventListener('resize', this._gameResizeListener);
    this._gameResizeListener();

    return this;
};

Game.prototype.setFullScreen = function(value){
    if(!Device.hasFullScreen)return this;
    value = (value !== false);

    if(value){
        this.canvas[Device.fullScreenRequest]();
    }else{
        document[Device.fullScreenCancel]();
    }

    return this;

};

Game.prototype.sayHello = function(){
    var renderer = this.isWebGL ? "WebGL" : "Canvas",
        audio = this.isWebAudio ? "WebAudio" : "HTMLAudio";

    //console.log('Powered by Perenquen v'+CONST.VERSION + ' (' + renderer + ' - ' + audio + ') [http://perenquen.tarentoladigital.com]');
    console.log(
        "Powered by Perenquen v" + CONST.VERSION + " [http://perenquen.tarentoladigital.com]\n" +
        "and Pixi.js v" + CONST.PIXI_VERSION + " [http://www.pixijs.com]\n\n\n" +
        "Renderer: " + renderer + "\n" +
        "Audio: " + audio
    );
    return this;
};

module.exports = Game;

Object.defineProperties(Game.prototype, {
    scene : {
        get: function(){
            return this.sceneManager.currentScene;
        }
    },

    mouse: {
        get: function(){
            return this.inputManager.mouse;
        }
    },

    keyboard: {
        get: function(){
            return this.inputManager.keyboard;
        }
    },

    gamepad: {
        get: function(){
            return this.inputManager.gamepad;
        }
    }
});

function getRenderer(width, height, options, noWebGL){
    if(navigator.isCocoonJS&&!options.view){
        options.view = window.document.createElement("screencanvas");
    }

    var renderer = new autoDetectRenderer(width, height, options, noWebGL);
    if(!options.view)window.document.body.appendChild(renderer.view);

    return renderer;
}

function parseConfig(options){
    var cfg = utils.defaultObject(CONST.DEFAULT_GAME_OPTIONS, options);
    config.useSortChildrenByDepth = cfg.useSortChildrenByDepth;
    return cfg;
}

function parseRendererConfig(defaultConfig, config){
    var cfg = JSON.parse(JSON.stringify(defaultConfig));

    for(var key in config.game){
        switch(key){
            case "autoResize":
            case "clearBeforeRenderer":
            case "forceFXAA":
            case "preserveDrawingBuffer":
            case "backgroundColor":
            case "resolution":
                cfg[key] = config.game[key];
                break;
            case "canvas":
                cfg.view = config.game[key];
                break;
            case "transparentBackground":
                cfg.transparent = config.game[key];
                break;
            case "useAntialias":
                cfg.antialias = config.game[key];
                break;
        }
    }

    return cfg;
}

function cleanObjects(){
    if(config._killedObjects.length){
        for(var i = 0; i < config._killedObjects.length; i++){
            config._killedObjects[i].remove();
        }
        config._killedObjects.length = 0;
    }
}<|endoftext|>


<commit_before>/*
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
// JSON Support
//
// TODO:
//   - don't output default classes
//   - don't output transient properties
//   - pretty printing
//   - allow for custom Property JSON support
//   - compact output
//   - reading shortNames
*/

/**
  A short-name is an optional shorter name for a property.
  It is used by foam.json.Outputer when 'useShortNames'
  is enabled. Short-names enable JSON output to be smaller,
  which can save disk space and/or network bandwidth.
  Ex.

  properties: [
    { name: 'firstName', shortName: 'fn' },
    { name: 'lastName',  shortName: 'ln' }
  ]

*/
foam.CLASS({
  refines: 'foam.core.Property',

  properties: [
    { class: 'String', name: 'shortName' }
  ]
});

foam.CLASS({
  refines: 'foam.core.FObject',

  methods: [
    function toJSON() {
      return foam.json.Outputer.create({nlStr:null, postColonStr:null, indentStr:null}).stringify(this);
    }
  ]
});

foam.CLASS({
  package: 'foam.json',
  name: 'Outputer',

  properties: [
    {
      class: 'String',
      name: 'buf_',
      value: ''
    },
    {
      class: 'Int',
      name: 'indentLevel_',
      value: 0
    },
    {
      class: 'String',
      name: 'indentStr',
      value: '\t'
    },
    {
      class: 'String',
      name: 'nlStr',
      value: '\n'
    },
    {
      class: 'String',
      name: 'postColonStr',
      value: ' '
    },
    {
      class: 'Boolean',
      name: 'alwaysQuoteKeys',
      value: false
    },
    {
      class: 'Boolean',
      name: 'outputDefaultValues',
      value: false
    },
    {
      class: 'Function',
      name: 'propertyPredicate',
      value: function(o, p) { return ! p.transient; }
    },
    {
      class: 'Boolean',
      name: 'useShortNames',
      value: false
    },
    {
      class: 'Boolean',
      name: 'pretty',
      value: true,
      postSet: function(_, p) {
        if ( p ) {
          this.clearProperty('indentStr');
          this.clearProperty('nlStr');
          this.clearProperty('postColonStr');
          this.clearProperty('useShortNames');
        } else {
          this.indentStr = this.nlStr = this.postColonStr = null;
        }
      }
    }
    /*
    {
      class: 'Boolean',
      name: 'functionFormat',
      value: false
    },
    */
  ],

  methods: [
    function reset() {
      this.indentLevel_ = 0;
      this.buf_ = '';
      return this;
    },

    function escape(str) {
      return str
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"')
        .replace(/[\x00-\x1f]/g, function(c) {
          return "\\u00" + ((c.charCodeAt(0) < 0x10) ?
                            '0' + c.charCodeAt(0).toString(16) :
                            c.charCodeAt(0).toString(16));
        });
    },

    function maybeEscapeKey(str) {
      return this.alwaysQuoteKeys || ! /^[a-zA-Z\$_][0-9a-zA-Z$_]*$/.test(str) ?
          '"' + str + '"' :
          str ;
    },

    function out() {
      for ( var i = 0 ; i < arguments.length ; i++ ) this.buf_ += arguments[i];
      return this;
    },

    function start(c) {
      if ( c ) this.out(c).nl();
      if ( this.indentStr ) {
        this.indentLevel_++;
        this.ind();
      }
      return this;
    },

    function end(c) {
      if ( c ) this.out(c);
      if ( this.indent ) {
        this.indentLevel_--;
      }
      return this.nl();
    },

    function nl() {
      if ( this.nlStr && this.nlStr.length ) {
        this.out(this.nlStr);
      }
      return this;
    },

    function ind() {
      for ( var i = 0 ; i < this.indentLevel_ ; i++ ) this.out(this.indentStr);
      return this;
    },

    function isDefaultValue(o, p) {
      if ( typeof p.value === 'undefined' ) return false;
      return ( ! o.hasOwnProperty(p.name) ) || foam.util.compare(p.value, p.get(o));
    },

    function outputPropertyName(p) {
      this.out(this.maybeEscapeKey(this.useShortNames && p.shortName ? p.shortName : p.name));
      return this;
    },

    function outputProperty(o, p) {
      if ( ! this.propertyPredicate(o, p ) ) return;
      if ( ! this.outputDefaultValues && this.isDefaultValue(o, p) ) return;

      var v = o[p.name];
      if ( Array.isArray(v) && ! v.length ) return;

      this.out(',').nl().ind().outputPropertyName(p).out(':', this.postColonStr);
      this.output(v);
    },

    {
      name: 'output',
      code: foam.mmethod({
        Undefined: function(o) { this.out('undefined'); },
        Null:      function(o) { this.out('null'); },
        String:    function(o) { this.out('"', this.escape(o), '"'); },
        Number:    function(o) { this.out(o); },
        Boolean:   function(o) { this.out(o); },
        Function:  function(o) { this.out(o); },
        FObject:   function(o) {
          this.start('{');
          this.out(
              this.maybeEscapeKey('class'),
              ':',
              this.postColonStr,
              '"',
              o.cls_.id,
              '"');
          var ps = o.cls_.getAxiomsByClass(foam.core.Property);
          for ( var i = 0 ; i < ps.length ; i++ ) {
            this.outputProperty(o, ps[i]);
          }
          this.nl().end('}');
        },
        Array:     function(o) {
          this.start('[');
          for ( var i = 0 ; i < o.length ; i++ ) {
            this.output(o[i], this);
            if ( i < o.length -1 ) this.out(',').nl();
          }
          this.end(']')
        },
        Object:    function(o) {
          if ( o.outputJSON2 ) {
            o.outputJSON2(this)
          } else {
            this.out('undefined');
          }
        }
      })
    },

    function stringify(o) {
      this.output(o);
      var ret = this.toString();
      this.reset(); // reset to avoid retaining garbage
      return ret;
    },

    function toString() { return this.buf_; }
  ]
});

foam.LIB({
  name: 'foam.json',

  constants: {

    Pretty: foam.json.Outputer.create(),

    Strict: foam.json.Outputer.create({
      pretty: false,
      alwaysQuoteKeys: true
    }),

    PrettyStrict: foam.json.Outputer.create({
      alwaysQuoteKeys: true
    }),

    Compact: foam.json.Outputer.create({
      pretty: false,
      outputDefaultValues: false
    }),

    Short: foam.json.Outputer.create({
      pretty: false,
      outputDefaultValues: false,
      useShortNames: true
    }),

    Network: foam.json.Outputer.create({
      pretty: false,
      outputDefaultValues: false,
      useShortNames: true,
      propertyPredicate: function(o, p) { return ! p.networkTransient; }
    }),

    Storage: foam.json.Outputer.create({
      pretty: false,
      outputDefaultValues: false,
      useShortNames: true,
      propertyPredicate: function(o, p) { return ! p.storageTransient; }
    })
  },

  methods: [
    function parse(json, opt_class) {
      // recurse into sub-objects
      for ( var key in json ) {
        var o = json[key];
        if ( typeof o === 'object' && ! o.cls_ ) { // traverse plain old objects only
          json[key] = this.parse(o);
        }
      }

      if ( json.class ) {
        var cls = foam.lookup(json.class);
        foam.X.assert(cls, 'Unknown class "', json.class, '" in foam.json.parse.');
        return cls.create(json);
      }
      if ( opt_class ) return opt_class.create(json);

      return json;
    },

    function parseArray(a, opt_class) {
      return a.map(function(e) { return foam.json.parse(e, opt_class); });
    },

    function parseString(jsonStr) {
      return eval('(' + jsonStr + ')');
    },

    function stringify(o) {
      return foam.json.Outputer.create({nlStr:null, postColonStr:null, indentStr:null}).stringify(o);
    }
  ]
});
<commit_msg>JSON work.<commit_after>/*
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
// JSON Support
//
// TODO:
//   - don't output default classes
//   - don't output transient properties
//   - pretty printing
//   - allow for custom Property JSON support
//   - compact output
//   - reading shortNames
//   - date support
*/

/**
  A short-name is an optional shorter name for a property.
  It is used by foam.json.Outputer when 'useShortNames'
  is enabled. Short-names enable JSON output to be smaller,
  which can save disk space and/or network bandwidth.
  Ex.

  properties: [
    { name: 'firstName', shortName: 'fn' },
    { name: 'lastName',  shortName: 'ln' }
  ]

*/
foam.CLASS({
  refines: 'foam.core.Property',

  properties: [
    { class: 'String', name: 'shortName' }
  ]
});

foam.CLASS({
  refines: 'foam.core.FObject',

  methods: [
    function toJSON() {
      return foam.json.Outputer.create({nlStr:null, postColonStr:null, indentStr:null}).stringify(this);
    }
  ]
});

foam.CLASS({
  package: 'foam.json',
  name: 'Outputer',

  properties: [
    {
      class: 'String',
      name: 'buf_',
      value: ''
    },
    {
      class: 'Int',
      name: 'indentLevel_',
      value: 0
    },
    {
      class: 'String',
      name: 'indentStr',
      value: '\t'
    },
    {
      class: 'String',
      name: 'nlStr',
      value: '\n'
    },
    {
      class: 'String',
      name: 'postColonStr',
      value: ' '
    },
    {
      class: 'Boolean',
      name: 'alwaysQuoteKeys',
      value: false
    },
    {
      class: 'Boolean',
      name: 'outputDefaultValues',
      value: false
    },
    {
      class: 'Function',
      name: 'propertyPredicate',
      value: function(o, p) { return ! p.transient; }
    },
    {
      class: 'Boolean',
      name: 'useShortNames',
      value: false
    },
    {
      class: 'Boolean',
      name: 'pretty',
      value: true,
      postSet: function(_, p) {
        if ( p ) {
          this.clearProperty('indentStr');
          this.clearProperty('nlStr');
          this.clearProperty('postColonStr');
          this.clearProperty('useShortNames');
        } else {
          this.indentStr = this.nlStr = this.postColonStr = null;
        }
      }
    }
    /*
    {
      class: 'Boolean',
      name: 'functionFormat',
      value: false
    },
    */
  ],

  methods: [
    function reset() {
      this.indentLevel_ = 0;
      this.buf_ = '';
      return this;
    },

    function escape(str) {
      return str
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"')
        .replace(/[\x00-\x1f]/g, function(c) {
          return "\\u00" + ((c.charCodeAt(0) < 0x10) ?
                            '0' + c.charCodeAt(0).toString(16) :
                            c.charCodeAt(0).toString(16));
        });
    },

    function maybeEscapeKey(str) {
      return this.alwaysQuoteKeys || ! /^[a-zA-Z\$_][0-9a-zA-Z$_]*$/.test(str) ?
          '"' + str + '"' :
          str ;
    },

    function out() {
      for ( var i = 0 ; i < arguments.length ; i++ ) this.buf_ += arguments[i];
      return this;
    },

    function start(c) {
      if ( c ) this.out(c).nl();
      if ( this.indentStr ) {
        this.indentLevel_++;
        this.ind();
      }
      return this;
    },

    function end(c) {
      if ( c ) this.out(c);
      if ( this.indent ) {
        this.indentLevel_--;
      }
      return this.nl();
    },

    function nl() {
      if ( this.nlStr && this.nlStr.length ) {
        this.out(this.nlStr);
      }
      return this;
    },

    function ind() {
      for ( var i = 0 ; i < this.indentLevel_ ; i++ ) this.out(this.indentStr);
      return this;
    },

    function isDefaultValue(o, p) {
      if ( typeof p.value === 'undefined' ) return false;
      return ( ! o.hasOwnProperty(p.name) ) || foam.util.compare(p.value, p.get(o));
    },

    function outputPropertyName(p) {
      this.out(this.maybeEscapeKey(this.useShortNames && p.shortName ? p.shortName : p.name));
      return this;
    },

    function outputProperty(o, p) {
      if ( ! this.propertyPredicate(o, p ) ) return;
      if ( ! this.outputDefaultValues && this.isDefaultValue(o, p) ) return;

      var v = o[p.name];
      if ( Array.isArray(v) && ! v.length ) return;

      this.out(',').nl().ind().outputPropertyName(p).out(':', this.postColonStr);
      this.output(v);
    },

    {
      name: 'output',
      code: foam.mmethod({
        Undefined: function(o) { this.out('undefined'); },
        Null:      function(o) { this.out('null'); },
        String:    function(o) { this.out('"', this.escape(o), '"'); },
        Number:    function(o) { this.out(o); },
        Boolean:   function(o) { this.out(o); },
        Function:  function(o) { this.out(o); },
        FObject:   function(o) {
          this.start('{');
          this.out(
              this.maybeEscapeKey('class'),
              ':',
              this.postColonStr,
              '"',
              o.cls_.id,
              '"');
          var ps = o.cls_.getAxiomsByClass(foam.core.Property);
          for ( var i = 0 ; i < ps.length ; i++ ) {
            this.outputProperty(o, ps[i]);
          }
          this.nl().end('}');
        },
        Array:     function(o) {
          this.start('[');
          for ( var i = 0 ; i < o.length ; i++ ) {
            this.output(o[i], this);
            if ( i < o.length -1 ) this.out(',').nl();
          }
          this.end(']')
        },
        Object:    function(o) {
          if ( o.outputJSON2 ) {
            o.outputJSON2(this)
          } else {
            this.out('undefined');
          }
        }
      })
    },

    function stringify(o) {
      this.output(o);
      var ret = this.buf_;
      this.reset(); // reset to avoid retaining garbage
      return ret;
    }
  ]
});

foam.LIB({
  name: 'foam.json',

  constants: {

    Pretty: foam.json.Outputer.create(),

    Strict: foam.json.Outputer.create({
      pretty: false,
      alwaysQuoteKeys: true
    }),

    PrettyStrict: foam.json.Outputer.create({
      alwaysQuoteKeys: true
    }),

    Compact: foam.json.Outputer.create({
      pretty: false,
      outputDefaultValues: false
    }),

    Short: foam.json.Outputer.create({
      pretty: false,
      outputDefaultValues: false,
      useShortNames: true
    }),

    Network: foam.json.Outputer.create({
      pretty: false,
      outputDefaultValues: false,
      useShortNames: true,
      propertyPredicate: function(o, p) { return ! p.networkTransient; }
    }),

    Storage: foam.json.Outputer.create({
      pretty: false,
      outputDefaultValues: false,
      useShortNames: true,
      propertyPredicate: function(o, p) { return ! p.storageTransient; }
    })
  },

  methods: [
    function parse(json, opt_class) {
      // recurse into sub-objects
      for ( var key in json ) {
        var o = json[key];
        if ( typeof o === 'object' && ! o.cls_ ) { // traverse plain old objects only
          json[key] = this.parse(o);
        }
      }

      if ( json.class ) {
        var cls = foam.lookup(json.class);
        foam.X.assert(cls, 'Unknown class "', json.class, '" in foam.json.parse.');
        return cls.create(json);
      }
      if ( opt_class ) return opt_class.create(json);

      return json;
    },

    function parseArray(a, opt_class) {
      return a.map(function(e) { return foam.json.parse(e, opt_class); });
    },

    function parseString(jsonStr) {
      return eval('(' + jsonStr + ')');
    },

    function stringify(o) {
      return foam.json.Outputer.create({nlStr:null, postColonStr:null, indentStr:null}).stringify(o);
    }
  ]
});
<|endoftext|>


<commit_before>var Case = require('Case');

var Test = (function () {

  /**
   * A collection of Cases.
   */
  function Test (name, attributes) {
    return new Test.fn.init(name, attributes);
  }

  // Prototype object of the Test.
  Test.fn = Test.prototype = {
    constructor: Test,
    init: function (name, attributes) {
      this.listeners = {};
      this.length = 0;
      if (!name) {
        return this;
      }
      this.attributes = attributes || {};
      this.attributes.name = name;
      this.attributes.status = 'untested';
      this.attributes.complete = false;

      return this;
    },
    // Setting a length property makes it behave like an array.
    length: 0,
    // Details of the test.
    attributes: null,
    // Execute a callback for every element in the matched set.
    each: function (iterator) {
      var args = [].slice.call(arguments, 1);
      for (var i = 0, len = this.length; i < len; ++i) {
        args.unshift(this[i]);
        args.unshift(i);
        iterator.apply(this[i], args);
      }
      return this;
    },
    get: function (attr) {
      return this.attributes[attr];
    },
    set: function (attr, value) {
      var isStatusChanged = false;
      // Allow an object of attributes to be passed in.
      if (typeof attr === 'object') {
        for (var prop in attr) {
          if (attr.hasOwnProperty(prop)) {
            if (prop === 'status') {
              isStatusChanged = true;
            }
            this.attributes[prop] = attr[prop];
          }
        }
      }
      // Assign a single attribute value.
      else {
        if (attr === 'status') {
          isStatusChanged = true;
        }
        this.attributes[attr] = value;
      }

      if (isStatusChanged) {
        this.resolve();
      }
      return this;
    },
    add: function (_case) {
      this.listenTo(_case, 'resolve', this.caseResponded);
      this.listenTo(_case, 'timeout', this.caseResponded);
      // If the case is already resolved because it has a status, then trigger
      // its resolve event.
      if (_case.status) {
        _case.dispatch('resolve', _case);
      }
      this.push(_case);
      return _case;
    },
    invoke: function () {
      var name = this.get('name');
      // This test is already running.
      if (this.testComplete) {
        throw new Error('The test ' + name + ' is already running.');
      }
      // This test has already been run.
      if (this.attributes.complete) {
        throw new Error('The test ' + name + ' has already been run.');
      }

      var options = this.get('options');
      var callback = this.get('callback');
      var self = this;

      // Set the test complete method to the closure function that dispatches
      // the complete event. This method needs to be debounced so it is only
      // called after a pause of invocations.
      this.testComplete = debounce(testComplete.bind(this), 400);

      // Invoke the complete dispatcher to prevent the test from never
      // completing in the off chance that no Cases are created.
      this.testComplete(false);

      // Record the time the test started for performance monitoring.
      var start = new Date();
      this.set('startTime', start);

      if (callback && typeof callback.call === 'function') {
        try {
          callback.call(self, self, options);
        }
        catch (error) {
          if (window.console && window.console.error) {
            window.console.error(error.stack);
          }
        }
      }
      else {
        window.console.log('Skipping ' + name + ' because it does not have an associated method on Quail');
      }

      // Invoke the complete dispatcher to prevent the test from never
      // completing in the off chance that no Cases are created.
      this.testComplete();

      return this;
    },
    /**
     * Finds cases by their status.
     */
    findByStatus: function (statuses) {
      if (!statuses) {
        return;
      }
      var test = new Test();
      // A single status or an array of statuses is allowed. Always act on an
      // array.
      if (typeof statuses === 'string') {
        statuses = [statuses];
      }
      // Loop the through the statuses and find tests with them.
      for (var i = 0, il = statuses.length; i < il; ++i) {
        var status = statuses[i];
        // Loop through the cases.
        this.each(function (index, _case) {
          var caseStatus = _case.get('status');
          if (caseStatus === status) {
            test.add(_case);
          }
        });
      }
      return test;
    },
    /**
     * Returns a set of cases with corresponding to th supplied selector.
     */
    findCasesBySelector: function (selector) {
      var cases = this.groupCasesBySelector();
      if (cases.hasOwnProperty(selector)) {
        return cases[selector];
      }
      return new Test();
    },
    /**
     * Returns a single Case object the matches the supplied HTML.
     *
     * We make the assumption, rightly or wrongly, that if the HTML is the
     * same for a number of cases in a Test, then the outcome will also
     * be the same, so only use this method if you are probing the result
     * of the case, not other specifics of it.
     *
     * @param string html
     *   A string representing an HTML structure.
     *
     * @needstests
     */
    findCaseByHtml: function (html) {
      var _case;
      for (var i = 0, il = this.length; i < il; ++i) {
        _case = this[i];
        if (html === _case.get('html')) {
          return _case;
        }
      }
      // Always return a Case object.
      return Case();
    },
    /**
     * Groups the cases by element selector.
     *
     * @return object
     *  A hash of cases, keyed by the element selector.
     */
    groupCasesBySelector: function () {
      var casesBySelector = {};
      // Loop through the cases.
      this.each(function (index, _case) {
        var selector = _case.get('selector');
        if (!casesBySelector[selector]) {
          casesBySelector[selector] = new Test();
        }
        casesBySelector[selector].add(_case);
      });
      return casesBySelector;
    },
    /**
     * Groups the cases by serialized HTML string.
     *
     * @todo, the html string index needs to be hashed to a uniform length.
     *
     * @return object
     *  A hash of cases, keyed by the element selector.
     */
    groupCasesByHtml: function () {
      var casesByHtml = {};
      // Loop through the cases.
      this.each(function (index, _case) {
        var html = _case.get('html');
        if (!casesByHtml[html]) {
          casesByHtml[html] = new Test();
        }
        casesByHtml[html].add(_case);
      });
      return casesByHtml;
    },
    /**
     * @needsdoc
     */
    getGuidelineCoverage: function (name) {
      var config = this.get('guidelines');
      return config && config[name] || {};
    },
    /**
     * Adds the test that owns the Case to the set of arguments passed up to
     * listeners of this test's cases.
     */
    caseResponded: function (eventName, _case) {
      this.dispatch(eventName, this, _case);
      // Attempt to declare the Test complete.
      if (typeof this.testComplete === 'function') {
        this.testComplete();
      }
    },
    /**
     * Evaluates the test's cases and sets the test's status.
     */
    determineStatus: function () {
      // CantTell.
      if (this.findByStatus(['cantTell']).length === this.length) {
        this.set({
          status: 'cantTell'
        });
      }
      // inapplicable.
      else if (this.findByStatus(['inapplicable']).length === this.length) {
        this.set({
          status: 'inapplicable'
        });
      }
      // Failed.
      else if (this.findByStatus(['failed', 'untested']).length) {
        this.set({
          status: 'failed'
        });
      }
      else {
        this.set({
          status: 'passed'
        });
      }
    },
    resolve: function () {
      this.dispatch('complete', this);
    },
    /**
     * A stub method implementation.
     *
     * It is assigned a function value when the Test is invoked. See the
     * testComplete function in outer scope.
     */
    testComplete: null,
    // @todo, make this a set of methods that all classes extend.
    listenTo: function (dispatcher, eventName, handler) {
      handler = handler.bind(this);
      dispatcher.registerListener.call(dispatcher, eventName, handler);
    },
    registerListener: function (eventName, handler) {
      // nb: 'this' is the dispatcher object, not the one that invoked listenTo.
      if (!this.listeners[eventName]) {
        this.listeners[eventName] = [];
      }

      this.listeners[eventName].push(handler);
    },
    dispatch: function (eventName) {
      if (this.listeners[eventName] && this.listeners[eventName].length) {
        var eventArgs = [].slice.call(arguments);
        this.listeners[eventName].forEach(function (handler) {
          // Pass any additional arguments from the event dispatcher to the
          // handler function.
          handler.apply(null, eventArgs);
        });
      }
    },
    push: [].push,
    sort: [].sort,
    concat: [].concat,
    splice: [].splice
  };

  /**
   * Dispatches the complete event.
   *
   * This function is meant to be bound to a Test as a method through
   * a debounced proxy function.
   */
  function testComplete (complete) {
    complete = (typeof complete === 'undefined') ? true : complete;
    // @todo, this iteration would be faster with _.findWhere, that breaks on
    // the first match.
    this.each(function (index, _case) {
      if (!_case.get('status')) {
        complete = false;
      }
    });
    // If all the Cases have been evaluated, dispatch the event.
    if (complete) {
      // Set the end time for performance monitoring.
      var end = new Date();
      this.set('endTime', end);
      // Null out the testComplete callback on this test.
      this.testComplete = null;
      // @todo, this should be set with the set method and a silent flag.
      this.attributes.complete = true;
      this.determineStatus();
    }
    // Otherwise attempt to the complete the Test again after the debounce
    // period has expired.
    else {
      this.testComplete();
    }
  }

  /**
   * Limits the invocations of a function in a given time frame.
   *
   * Adapted from underscore.js. Replace with debounce from underscore once class
   * loading with modules is in place.
   *
   * @param {Function} callback
   *   The function to be invoked.
   *
   * @param {Number} wait
   *   The time period within which the callback function should only be
   *   invoked once. For example if the wait period is 250ms, then the callback
   *   will only be called at most 4 times per second.
   */
  function debounce (func, wait, immediate) {
    'use strict';

    var timeout, result;
    return function () {
      var self = this;
      var args = arguments;
      var later = function () {
        timeout = null;
        if (!immediate) {
          result = func.apply(self, args);
        }
      };
      var callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) {
        result = func.apply(self, args);
      }
      return result;
    };
  }

  // Give the init function the Test prototype.
  Test.fn.init.prototype = Test.fn;

  return Test;
}());
module.exports = Test;
<commit_msg>Swapping jQuery.each for Array.forEach<commit_after>var Case = require('Case');

var Test = (function () {

  /**
   * A collection of Cases.
   */
  function Test (name, attributes) {
    return new Test.fn.init(name, attributes);
  }

  // Prototype object of the Test.
  Test.fn = Test.prototype = {
    constructor: Test,
    init: function (name, attributes) {
      this.listeners = {};
      this.length = 0;
      if (!name) {
        return this;
      }
      this.attributes = attributes || {};
      this.attributes.name = name;
      this.attributes.status = 'untested';
      this.attributes.complete = false;

      return this;
    },
    // Setting a length property makes it behave like an array.
    length: 0,
    // Details of the test.
    attributes: null,
    // Execute a callback for every element in the matched set.
    each: function (iterator) {
      var args = [].slice.call(arguments, 1);
      for (var i = 0, len = this.length; i < len; ++i) {
        args.unshift(this[i]);
        args.unshift(i);
        iterator.apply(this[i], args);
      }
      return this;
    },
    get: function (attr) {
      return this.attributes[attr];
    },
    set: function (attr, value) {
      var isStatusChanged = false;
      // Allow an object of attributes to be passed in.
      if (typeof attr === 'object') {
        for (var prop in attr) {
          if (attr.hasOwnProperty(prop)) {
            if (prop === 'status') {
              isStatusChanged = true;
            }
            this.attributes[prop] = attr[prop];
          }
        }
      }
      // Assign a single attribute value.
      else {
        if (attr === 'status') {
          isStatusChanged = true;
        }
        this.attributes[attr] = value;
      }

      if (isStatusChanged) {
        this.resolve();
      }
      return this;
    },
    add: function (_case) {
      this.listenTo(_case, 'resolve', this.caseResponded);
      this.listenTo(_case, 'timeout', this.caseResponded);
      // If the case is already resolved because it has a status, then trigger
      // its resolve event.
      if (_case.status) {
        _case.dispatch('resolve', _case);
      }
      this.push(_case);
      return _case;
    },
    invoke: function () {
      var name = this.get('name');
      // This test is already running.
      if (this.testComplete) {
        throw new Error('The test ' + name + ' is already running.');
      }
      // This test has already been run.
      if (this.attributes.complete) {
        throw new Error('The test ' + name + ' has already been run.');
      }

      var options = this.get('options');
      var callback = this.get('callback');
      var self = this;

      // Set the test complete method to the closure function that dispatches
      // the complete event. This method needs to be debounced so it is only
      // called after a pause of invocations.
      this.testComplete = debounce(testComplete.bind(this), 400);

      // Invoke the complete dispatcher to prevent the test from never
      // completing in the off chance that no Cases are created.
      this.testComplete(false);

      // Record the time the test started for performance monitoring.
      var start = new Date();
      this.set('startTime', start);

      if (callback && typeof callback.call === 'function') {
        debugger;
        try {
          callback.call(self, self, options);
        }
        catch (error) {
          if (window.console && window.console.error) {
            window.console.error(error.stack);
          }
        }
      }
      else {
        window.console.log('Skipping ' + name + ' because it does not have an associated method on Quail');
      }

      // Invoke the complete dispatcher to prevent the test from never
      // completing in the off chance that no Cases are created.
      this.testComplete();

      return this;
    },
    /**
     * Finds cases by their status.
     */
    findByStatus: function (statuses) {
      if (!statuses) {
        return;
      }
      var test = new Test();
      // A single status or an array of statuses is allowed. Always act on an
      // array.
      if (typeof statuses === 'string') {
        statuses = [statuses];
      }
      // Loop the through the statuses and find tests with them.
      for (var i = 0, il = statuses.length; i < il; ++i) {
        var status = statuses[i];
        // Loop through the cases.
        this.each(function (index, _case) {
          var caseStatus = _case.get('status');
          if (caseStatus === status) {
            test.add(_case);
          }
        });
      }
      return test;
    },
    /**
     * Returns a set of cases with corresponding to th supplied selector.
     */
    findCasesBySelector: function (selector) {
      var cases = this.groupCasesBySelector();
      if (cases.hasOwnProperty(selector)) {
        return cases[selector];
      }
      return new Test();
    },
    /**
     * Returns a single Case object the matches the supplied HTML.
     *
     * We make the assumption, rightly or wrongly, that if the HTML is the
     * same for a number of cases in a Test, then the outcome will also
     * be the same, so only use this method if you are probing the result
     * of the case, not other specifics of it.
     *
     * @param string html
     *   A string representing an HTML structure.
     *
     * @needstests
     */
    findCaseByHtml: function (html) {
      var _case;
      for (var i = 0, il = this.length; i < il; ++i) {
        _case = this[i];
        if (html === _case.get('html')) {
          return _case;
        }
      }
      // Always return a Case object.
      return Case();
    },
    /**
     * Groups the cases by element selector.
     *
     * @return object
     *  A hash of cases, keyed by the element selector.
     */
    groupCasesBySelector: function () {
      var casesBySelector = {};
      // Loop through the cases.
      this.each(function (index, _case) {
        var selector = _case.get('selector');
        if (!casesBySelector[selector]) {
          casesBySelector[selector] = new Test();
        }
        casesBySelector[selector].add(_case);
      });
      return casesBySelector;
    },
    /**
     * Groups the cases by serialized HTML string.
     *
     * @todo, the html string index needs to be hashed to a uniform length.
     *
     * @return object
     *  A hash of cases, keyed by the element selector.
     */
    groupCasesByHtml: function () {
      var casesByHtml = {};
      // Loop through the cases.
      this.each(function (index, _case) {
        var html = _case.get('html');
        if (!casesByHtml[html]) {
          casesByHtml[html] = new Test();
        }
        casesByHtml[html].add(_case);
      });
      return casesByHtml;
    },
    /**
     * @needsdoc
     */
    getGuidelineCoverage: function (name) {
      var config = this.get('guidelines');
      return config && config[name] || {};
    },
    /**
     * Adds the test that owns the Case to the set of arguments passed up to
     * listeners of this test's cases.
     */
    caseResponded: function (eventName, _case) {
      this.dispatch(eventName, this, _case);
      // Attempt to declare the Test complete.
      if (typeof this.testComplete === 'function') {
        this.testComplete();
      }
    },
    /**
     * Evaluates the test's cases and sets the test's status.
     */
    determineStatus: function () {
      // CantTell.
      if (this.findByStatus(['cantTell']).length === this.length) {
        this.set({
          status: 'cantTell'
        });
      }
      // inapplicable.
      else if (this.findByStatus(['inapplicable']).length === this.length) {
        this.set({
          status: 'inapplicable'
        });
      }
      // Failed.
      else if (this.findByStatus(['failed', 'untested']).length) {
        this.set({
          status: 'failed'
        });
      }
      else {
        this.set({
          status: 'passed'
        });
      }
    },
    resolve: function () {
      this.dispatch('complete', this);
    },
    /**
     * A stub method implementation.
     *
     * It is assigned a function value when the Test is invoked. See the
     * testComplete function in outer scope.
     */
    testComplete: null,
    // @todo, make this a set of methods that all classes extend.
    listenTo: function (dispatcher, eventName, handler) {
      handler = handler.bind(this);
      dispatcher.registerListener.call(dispatcher, eventName, handler);
    },
    registerListener: function (eventName, handler) {
      // nb: 'this' is the dispatcher object, not the one that invoked listenTo.
      if (!this.listeners[eventName]) {
        this.listeners[eventName] = [];
      }

      this.listeners[eventName].push(handler);
    },
    dispatch: function (eventName) {
      if (this.listeners[eventName] && this.listeners[eventName].length) {
        var eventArgs = [].slice.call(arguments);
        this.listeners[eventName].forEach(function (handler) {
          // Pass any additional arguments from the event dispatcher to the
          // handler function.
          handler.apply(null, eventArgs);
        });
      }
    },
    push: [].push,
    sort: [].sort,
    concat: [].concat,
    splice: [].splice
  };

  /**
   * Dispatches the complete event.
   *
   * This function is meant to be bound to a Test as a method through
   * a debounced proxy function.
   */
  function testComplete (complete) {
    complete = (typeof complete === 'undefined') ? true : complete;
    // @todo, this iteration would be faster with _.findWhere, that breaks on
    // the first match.
    this.each(function (index, _case) {
      if (!_case.get('status')) {
        complete = false;
      }
    });
    // If all the Cases have been evaluated, dispatch the event.
    if (complete) {
      // Set the end time for performance monitoring.
      var end = new Date();
      this.set('endTime', end);
      // Null out the testComplete callback on this test.
      this.testComplete = null;
      // @todo, this should be set with the set method and a silent flag.
      this.attributes.complete = true;
      this.determineStatus();
    }
    // Otherwise attempt to the complete the Test again after the debounce
    // period has expired.
    else {
      this.testComplete();
    }
  }

  /**
   * Limits the invocations of a function in a given time frame.
   *
   * Adapted from underscore.js. Replace with debounce from underscore once class
   * loading with modules is in place.
   *
   * @param {Function} callback
   *   The function to be invoked.
   *
   * @param {Number} wait
   *   The time period within which the callback function should only be
   *   invoked once. For example if the wait period is 250ms, then the callback
   *   will only be called at most 4 times per second.
   */
  function debounce (func, wait, immediate) {
    'use strict';

    var timeout, result;
    return function () {
      var self = this;
      var args = arguments;
      var later = function () {
        timeout = null;
        if (!immediate) {
          result = func.apply(self, args);
        }
      };
      var callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) {
        result = func.apply(self, args);
      }
      return result;
    };
  }

  // Give the init function the Test prototype.
  Test.fn.init.prototype = Test.fn;

  return Test;
}());
module.exports = Test;
<|endoftext|>