<commit_before>(function($){
	rootNS = this;

	FloatingDiv = function(){
		var self = this;
		this._color = "#FDFF00";
		this._selections = [];
	};
	FloatingDiv.constructor = FloatingDiv;
	FloatingDiv.prototype = {};

	$.extend(FloatingDiv.prototype, {

		_showSelectionObject: function(){
			var self = this;
			return this._selections;
		},

		_checkForID: function(objID){
			for (var i=0;i<this._selections.length;i++){
				if (this._selections[i].id==objID){
					return false;
				}
			}
			return true;
		},

		_getUniqueId:  function(o) {
				// http://www.sitepoint.com/forums/showthread.php?t=318819
				// changed a bit to receive prefix and length
				o.prefix = (typeof o.prefix == 'undefined') ? "anote_" : o.prefix;
				o.length = (typeof o.length == 'undefined') ? 8 : o.length;

				function getRandomNumber(range)	{
					return Math.floor(Math.random() * range);
				}

				function getRandomChar() {
					var chars = "0123456789abcdefghijklmnopqurstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ";
					return chars.substr(getRandomNumber(62), 1);
				}

				function randomID(length)	{
					var str = "";
					for(var i = 0; i < length; i++) {
						str += getRandomChar();
					}
					return str;
				}

				return o.prefix + randomID(o.length); /// returns length of 8

		},

		_getIndexOfId: function(selId){

			for (var i=0;i<this._selections.length;i++){
				if (this._selections[i].id == selId){
					return i;
				}
			}
			return null;
		},

		_getPathToNode: function(node){
			path = "";

			if (node.id){
				return "#"+node.id;
			}
			else{
				var childNo=this._getChildNumber(node,node.nodeName);

				path=">*:eq("+childNo+")";
				while(node.parentNode){
					node = node.parentNode;
					if (node.id){
						return "#"+node.id+path;
					}
					else{
						var childNo=this._getChildNumber(node,node.nodeName);

						if (childNo) {
							path = ">*:eq(" + childNo + ")" + path;
						}
						else{
							return node.nodeName+path;
						}
					}
				}
				return node.nodeName+":"+path;
			}

		},

		_getChildNumber: function(obj){
			if (obj.parentNode) {
				var children = obj.parentNode.childNodes;
				var num;
				for (var i = 0; i < children.length; i++) {
					if (children[i].isSameNode(obj)) {

						return i;
					}
				}
			}

			return null;
		},

		_rgb2hex: function(rgb) {

			// from http://stackoverflow.com/questions/638948/background-color-hex-to-javascript-variable-jquery
			rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
				function hex(x) {
					return ("0" + parseInt(x,10).toString(16)).slice(-2);
				}
			return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
		},

		_addColorSelector: function(selId) {
			// add the color selector to each highlight
			// add onclick behavior to show
			var self = this;
			var selectorHTML = '

';
			// can't use .last() or .first()
			if($('span.'+selId).length>1){
				// attach to first
				$('span.'+selId+"eq:("+($('span.'+selId).length-1)+")").append(selectorHTML);
			} else {
				$('span.'+selId).append(selectorHTML);
			}

			var currColor = $('span.'+selId).css('background-color');
			currColor = this._rgb2hex(currColor);

			$('span.'+selId).ColorPicker({
					color: currColor,
					onShow: function (colpkr) {
						$(colpkr).fadeIn(500);
						return false;
					},
					onHide: function (colpkr) {
						$(colpkr).fadeOut(500);
						return false;
					},
					onChange: function (hsb, hex, rgb) {
						$('span.'+selId).css('background-color', '#'+hex);
						self.changeSelectionColor(selId, hex);

					}
				});

		},

		init: function(myID) {
			// remove any dups
			$('#'.myID).empty().remove();

			var htmlString;

			htmlString = '<fieldset>' +
			'
' +
			'
' +
				'Label:' +
				'<input id="formLabel1" name="Label1" type="text" class="tagComplete" />' +
				'   ' +
				'' +
				'  (Add/Delete Labels)' +
			'
' +
			'
' +
			'</fieldset>' +
			'
' +
			'<input name="highLightID" type="hidden" id="highlightID" value="" />'+
			'<input name="Color" type="hidden" id="CINATnoteColor" value="" />' +

			$('<form></form>')
				.attr({
					'id':myID,
					'name':'TILE Label',
					'class':'addLabelForm'
				})
				.html(htmlString)
				.appendTo('body')
				.hide();
		},

		createDialog:function(myID) {
			var self = this;

			// get id from object
			var elem = '#'.myID;

			//create dialog from passed element with passed title
			$(elem).dialog({
				autoOpen: false,
				bgiframe: true,
				resizable: false,
				title: name,
				position: 'top',
				persist: false,
				width: 350,
				closeOnEscape: true,
				beforeclose: function(event, ui) {
					// listener?
				},
				close: function(event, ui) {
					$(elem).dialog('destroy');
					$(elem).empty().remove();
					return null;
				}
			})
		},

		addAutoComplete: function(elem, labels) {
			var self = this;

			$CINATjq(elem).autocomplete({

				source: function(req, add){

						//pass request to server
						$CINATjq.getJSON(myUrl + "?callback=?", req, function(data) {

							//create array for response objects
							var tags = [];

							//process response
							$CINATjq.each(data, function(i, val){
								tags.push(val.title);
							});

						//pass array to callback
						add(tags);
					});
				}
			});

			return false;
		},

		changeSelectionColor:function(selId,hexcolor){
			// get selection object from this._selections
			// by selId.
			var self = this;
			self._selections[self._getIndexOfId(selId)].color = '#'+hexcolor;

		},

		changePenColor:function(color){
			// From now on, highlight in Color
			this._color = color;
			return true;
		},

	});

	rootNS.FloatingDiv = FloatingDiv;
})(jQuery);

// Returns selection object
// of the following type:
// {
//  "id": unique
//	"StartParent": "Id of anchor parent",
//   "StartOffset": "anchor offset"
//   "StartChild": "child in anchorNode",
//
//	 "EndParent": "Id of focus parent",
//   "EndOffset": "focus offset",
//   "EndChild": "child in focus Node",
// 	 "color": "hex color"
// }

<commit_msg>FloatingDiv code<commit_after>(function($){
	rootNS = this;

	FloatingDiv = function(){
		var self = this;
		this._color = "#FDFF00";
		this._labels = [];
	};
	FloatingDiv.constructor = FloatingDiv;
	FloatingDiv.prototype = {};

	$.extend(FloatingDiv.prototype, {

		_rgb2hex: function(rgb) {

			// from http://stackoverflow.com/questions/638948/background-color-hex-to-javascript-variable-jquery
			rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
				function hex(x) {
					return ("0" + parseInt(x,10).toString(16)).slice(-2);
				}
			return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
		},

		init: function(myID, labels) {
			// remove any dups
			$('#'+myID).empty().remove();

			var htmlString;

			htmlString = '<fieldset class="label_formFieldSet">' +
			'
' +
			'
' +
				'Label:' +
				'<input id="formLabel1" name="Label1" type="text" class="tagComplete" />' +
				'   ' +
				'' +
				'  (Add/Delete Labels)
' +
			'
' +
			'
' +
			'<input type="submit" class="submit" value="Apply" id="submitFloatingDiv">' +
			'<input name="hlID" type="hidden" id="TILEid" value="" />'+
			'<input name="hlHEX" type="hidden" id="TILEcolor" value="" />' +
			'</fieldset>' +
			'
';

			$('<form></form>')
				.attr({
					'id':myID+'_floatingDiv',
					'name':'TILE Label',
					'class':'addLabelForm',
					'method':'post'
				})
				.html(htmlString)
				.appendTo('body')
				.hide();

			this.addAutoComplete('li#formField1 input.tagComplete', labels);

		},

		createDialog:function(myID) {

			// get id from object
			var elem = '#'+myID+'_floatingDiv';

			//create dialog from passed element with passed title
			$(elem).dialog({
				autoOpen: true,
				bgiframe: true,
				resizable: false,
				title: 'TILE Dialog',
				position: 'top',
				persist: false,
				width: 350,
				closeOnEscape: true,
				close: function(event, ui) {
					$(elem).dialog('destroy');
					$(elem).empty().remove();
					return null;
				}
			});
		},

		addAutoComplete: function(elem, labels) {

			$(elem).autocomplete({
				 source: labels
			});

			return false;
		},

		addColorSelector: function(myID, o) {
			var self = this;
			var htmlString;

			htmlString = 'Highlight Color:

';

			$('

')
				.attr({
					'id':myID+'_colorSelect',
					'name':'TILE Color Selector',
					'class':'addColor'
				})
				.html(htmlString)
				.appendTo('#'+myID+'_floatingDiv'+' fieldset');

			var currColor = $('span.'+o.id).css('background-color');
			currColor = this._rgb2hex(currColor);

			$('#floatingPenColor').ColorPicker({
					color: currColor,
					onShow: function (colpkr) {
						$(colpkr).fadeIn(500);
						return false;
					},
					onHide: function (colpkr) {
						$(colpkr).fadeOut(500);
						return false;
					},
					onChange: function (hsb, hex, rgb) {
						$('span.'+o.id).css('background-color', '#'+hex);
						$('#floatingPenColor div').css('backgroundColor', '#' + hex);
						self.addColorToForm(hex);
					}
				});

		},

		addColorToForm:function(hexcolor){
			// add the new selected color to hidden form field
			$('input#TILEcolor').val('#'+hexcolor);
			return true;

		},

		addIDToForm:function(o){
			// From now on, highlight in Color
			$('input#TILEid').val(o.id);
			return true;
		},

		addLabelsToObject:function(o) {
			var self = this;
			// loop through form field vals and assign to _labels obj

			$('li.cloneMe').each(function(i) {
				var n = i+1;
				alert(n + ': ' + $('input#formLabel'+n).val());
				self._labels[i] = $('input#formLabel'+n).val();
			});

			o.labels = self._labels;

			return o;
		}

	});

	rootNS.FloatingDiv = FloatingDiv;
})(jQuery);

// Floating Div Plugin Code
// Functions:
// start()
// restart()
// bundleData()
// close()
// Variables:
// _close
// json

/*
var FD={
	id:"FD1000",
	outputCall:"fdOutputCall101",
	activeCall:"sprTSActivate**",
	start:function(e,json,layout){
		var self=this;
		self.textsel=new FloatingDiv();
		self.manifest=[];
		self.activeSel="";
		self.curLink=null;
		if(json){
			for(j in json){
				self.manifest[j]=[];
				if(json[j].selections) self.manifest[j]=json[j].selections;
			}
		}

		// set up the tool buttons and insert into
		// TILE interface
		// append html
		$("

<input type=\"button\" value=\"Highlight\" id=\"getHLite\" class=\"button\"/>

").appendTo($("#transcript_toolbar"));
		// getText button
		self.getHLite=$("#getHLite").bind('click',function(e){
			e.stopPropagation();
			// make active and stop all listeners of other objects
			$("body:first").trigger(self.activeCall,[self.id]);
			$(document).mouseup(function(e){
				$(document).unbind("mouseup");
				var url=$("#srcImageForCanvas").attr('src');
				// de-select all transcript lines
				$(".line_selected").removeClass("line_selected");
				// clear all other highlights - will come back if they are saved
				// into the manifest
				$("span[class^=anno]").each(function(){
					$(this).children(".button").remove();
					$(this).children("div").remove();
				});

				self.textsel.clearSelections();

				var sel=self.textsel.getSelectedText();
				if(sel==null) return;
				// make sure it's not highlighting the whole page
				if(!(/div\#TILE\_Transcript\_/.test(sel.StartParent))){
					return;
				}

				// show on screen
				self.textsel.addSelection(sel);
				if(!self.manifest[url]) self.manifest[url]=[];
				self.manifest[url].push(sel);
				// find span tag and attach buttons
				self.attachButtons(sel.id);

				// make active
					self.activeSel=sel.id;
				// attach any current divs for references
				self._attachLinkDiv(self.curLink);
				$("body:first").trigger(self.outputCall,[{id:sel.id,type:"selections",attachHandle:"."+sel.id+":eq("+($("."+sel.id).length-1)+")",tool:self.id}]);

				$("body > .colorpicker").css({"z-index":"9999"});

			});

		});

		// bind addLink
		// $("body").bind("addLink",{obj:self},self.addLinkHandle);
		// global bind for when a user clicks on object that loads items
		$("body").bind("loadItems",{obj:self},self._loadItemsHandle);
	},
	attachButtons:function(id){
		var self=this;

		// $("span[class^=anno]").each(function(i,o){
		// 			var id=$(o).attr('class');
			// find span tag and attach buttons
		if($("span."+id).length>1){
			$("SelectDelete").appendTo($("span."+id+":eq("+($("span."+id).length-1)+")"));
		} else {
			$("SelectDelete").appendTo($("span."+id));
		}

		// selector
		$("#select_"+id).click(function(e){
			e.stopImmediatePropagation();
			$(".line_selected").removeClass("line_selected");
			$("span[title='highlight']").removeClass("active");
			$("span."+id).addClass("active");
			self.activeSel=id;
		});
		$("#deleteHLite"+id).click(function(e){
			$(this).parent().removeClass("active");
			var id=$(this).parent().attr('class');
			var url=$("#srcImageForCanvas").attr('src');
			for(sh in self.manifest[url]){
				if(self.manifest[url][sh].id==id){
					self.manifest[url][sh];
					self.manifest[url][sh]=null;
					if(self.manifest[url].length>1){
						var ac=self.manifest[url].slice(0,sh);
						var bc=self.manifest[url].slice((sh+1));
						self.manifest[url]=ac.concat(bc);
					} else {
						self.manifest[url].length=0;
					}
					break;
				}
			}
			$("span."+id+" > div").remove();
			$("span."+id+" > .button").remove();
			self.textsel.removeHighlightMarkers();
			$("body:first").trigger(self.deleteCall,[{id:id,type:"selections",tool:self.id}]);
			// self.textsel.importSelections(self.manifest[url]);

		});

		// attach any current divs for references
		self._attachLinkDiv(self.curLink);
		$("body > .colorpicker").css({"z-index":"9999"});
	},
	// e : {Event}
	// ref : {Object} reference object that has two properties:
	//      id: {String} id of  reference
	//      type: {String} type of reference
	addLinkHandle:function(e,ref){
		if((ref.type=='lines')||(ref.type=='selections')) return;
		var self=e.data.obj;
		var url=$("#srcImageForCanvas").attr('src');
		if(!self.manifest[url]) self.manifest[url]=[];
		// put in manifest
		for(m in self.manifest[url]){
			if(self.manifest[url][m].id==self.activeSel){
				if(self.manifest[url][m][ref.type]){
					if($.inArray(ref.id,self.manifest[url][m][ref.type])<0){
						self.curLink=ref;
						// add to array
						self.manifest[url][m][ref.type].push(ref.id);
						self._attachLinkDiv(ref);
						$("body:first").trigger("addLink",[{id:self.manifest[url][m].id,type:'selections'}]);

					}
				} else {
					self.curLink=ref;
					self.manifest[url][m][ref.type]=[ref.id];
					self._attachLinkDiv(ref);
					$("body:first").trigger("addLink",[{id:self.manifest[url][m].id,type:'selections'}]);
				}

				break;
			}
		}
	},
	// attaches a span tag that displays reference
	_attachLinkDiv:function(ref){
		if((!ref)||(ref=="")) return;
		var self=this;
		var t=ref.type;
		// generate DOM buttons to add onto the highlight
		$(""+ref.type+": "+ref.id+"X").appendTo($("span."+self.activeSel));
		$("#del"+self.activeSel+ref.id).click(function(e){
			e.stopImmediatePropagation();
			var url=$("#srcImageForCanvas").attr('src');
			var n=$.inArray(ref.id,self.manifest[url][t]);
			if(n<0) return;
			if(self.manifest[url][t].length>1){
				var ac=self.manifest[url][t].slice(0,n);
				var bc=self.manifest[url][t].slice((n+1));
				self.manifest[url][t]=ac.concat(bc);
			} else {
				self.manifest[url][t]=[];
			}
			if(self.curLink.id==ref.id) self.curLink=null;
			$(this).parent().remove();
			$(".line_selected").removeClass("line_selected");
			return;
		});
	},
	// e : {event}
	// data : {Object}
	_loadItemsHandle:function(e,data){
		var self=e.data.obj;
		$("span[class^='anno']").each(function(e){
			$(this).children(".button").remove();
			$(this).children("div").remove();
		});
		self.textsel.removeHighlightMarkers();

		if(!data||(data.length==0)) return;
		var url=$("#srcImageForCanvas").attr('src');
		var vd=[];
		for(x in self.manifest[url]){
			// if(__v) console.log("self.manifest[url]["+x+"]="+self.manifest[url][x].id);
			if($.inArray(self.manifest[url][x].id,data)>=0){
				vd.push(self.manifest[url][x]);
			}
		}
		if(vd.length==0) return;
		// if(__v) console.log("loading highlights from: "+vd);
		self.textsel.importSelections(vd);
		// attach buttons
		for(var v in vd){
			self.attachButtons(vd[v].id);
		}

	},
	unActive:function(){
		var self=this;
		$("span[class^='anno']").each(function(i,o){
			$(o).children("div").remove();
			$(o).children("span").remove();
			$(o).remove();
		});
	},
	restart:function(){

	},
	close:function(){
		var self=this;
		$("body:first").trigger(self._close);
	},
	// Insert the highlight data into the global json object
	// json: {Object} Global JSON object that has page URL's as key
	bundleData:function(json){
		var self=this;
		for(url in self.manifest){
			for(j in json){
				if(j==url){
					json[j].selections=self.manifest[url];
				}
			}
		}
		return json;
	},
	_close:"CloseSelector",
	json:null
};
*/
<|endoftext|>


<commit_before>cg.Renderer = (function () {

    /**
     * Contain all the default value of the renderer config, these values will be used when they aren't specified
     * into the config given in parameter.
     * @type {Object}
     */
    var DEFAULT_CONFIG = {
        "zoom": {
            "maxZoom": 5,
            "minZoom": 0.25
        },
        "group": {
            "defaultWidth": 300,
            "defaultHeight": 200,
            "heading": 20,
            "padding": 20,
            "borderRadius": 0
        },
        "block": {
            "borderRadius": 0,
            "padding": 20,
            "heading": 40,
            "centerSpacing": 20
        },
        "getter": {
            "height": 40
        },
        "point": {
            "height": 20,
            "padding": 5,
            "circle-size": 3
        },
        "background-grid": {
            "pattern-size": 10,
            "step-size": 5,
            "line": {
                "stroke": "rgba(0, 0, 0, 0.1)",
                "stroke-width": 2
            },
            "big-line": {
                "stroke": "rgba(0, 0, 0, 0.4)",
                "stroke-width": 3
            }
        }
    };

    /**
     *
     * @extends pandora.EventEmitter
     * @constructor
     */
    var Renderer = pandora.class_("Renderer", pandora.EventEmitter, function (graph, svg, config) {
        pandora.EventEmitter.call(this);

        /**
         * Renderer configuration.
         * @type {Object}
         */
        this._config = pandora.mergeObjects(config || {}, DEFAULT_CONFIG, true, true);

        /**
         * Graph reference.
         * @type {cg.Graph}
         * @private
         */
        this._graph = graph;

        /**
         * The root SVG node this renderer is attached to.
         * @type {d3.selection}
         * @private
         */
        this._svg = d3.select(svg);

        /**
         * The root group created.
         * @type {d3.selection}
         * @private
         */
        this._rootGroup = null;

        /**
         * The group layer.
         * @type {d3.selection}
         * @private
         */
        this._groupLayer = null;

        /**
         * The connection layer.
         * @type {d3.selection}
         * @private
         */
        this._connectionLayer = null;

        /**
         * The layer for temporary connections that are not attached yet to a block but to the cursor instead.
         * @type {null}
         * @private
         */
        this._cursorConnectionLayer = null;

        /**
         * The block layer.
         * @type {d3.selection}
         * @private
         */
        this._blockLayer = null;

        /**
         * If this graph is already rendered.
         * @type {boolean}
         * @private
         */
        this._rendered = false;

        /**
         * Point used for matrix transformations.
         * @type {SVGPoint}
         * @private
         */
        this._svgPoint = null;

        /**
         *
         * @type {d3.selection}
         * @private
         */
        this._selectionRectangle = null;

        /**
         *
         * @type {pandora.Box2}
         * @private
         */
        this._selectionBox = new pandora.Box2();
    });

    /**
     *
     * @private
     */
    Renderer.prototype._initialize = function () {
        this._rootGroup = this._svg.append("svg:g");
        this._groupLayer = this._rootGroup.append("svg:g").attr("id", "group-layer");
        this._connectionLayer = this._rootGroup.append("svg:g").attr("id", "connection-layer");
        this._cursorConnectionLayer = this._rootGroup.append("svg:g").attr("id", "cursor-connection-layer");
        this._blockLayer = this._rootGroup.append("svg:g").attr("id", "block-layer");
        this._svgPoint = this._svg.node().createSVGPoint();
    };

    /**
     * Render the graph.
     */
    Renderer.prototype.render = function () {
        if (this._rendered) {
            this.emit("error", new cg.RendererError("Cannot re-render the graph"));
            return;
        }
        this._rendered = true;
        this._initialize();
        this._render();
        this._renderSelection();
        this._renderZoom();
        this._updateRendererCollisions();
        setTimeout(this._render.bind(this), 100); // TODO: First render, all elements have bad size.
        this._graph.on("entity.add", function () {
            this._render();
        }.bind(this));
        this._graph.on("entity.remove", function () {
            this._render();
        }.bind(this));
        this._graph.on("connection.add", function () {
            this._renderConnections(this.getConnections());
        }.bind(this));
        this._graph.on("connection.remove", function () {
            this._renderConnections(this.getConnections());
        }.bind(this));
        this._updateRendererCollisions();
    };

    /**
     * Return d3 selection for all entities.
     * @return {d3.selection}
     */
    Renderer.prototype.getEntities = function () {
        return this._rootGroup
            .selectAll("#group-layer > .group, #block-layer > .block");
    };

    /**
     * Return d3 selection for all groups.
     * @return {d3.selection}
     */
    Renderer.prototype.getGroups = function () {
        return this._groupLayer
            .selectAll(".group")
            .data(this._graph.groups(), function (group) {
                return group._id;
            });
    };

    /**
     * Return d3 selection for all blocks.
     * @return {d3.selection}
     */
    Renderer.prototype.getBlocks = function () {
        return this._blockLayer
            .selectAll(".block")
            .data(this._graph.blocks(), function (block) {
                return block._id;
            });
    };

    /**
     * Return d3 selection for all points.
     * @return {d3.selection}
     */
    Renderer.prototype.getPoints = function () {
        return this._blockLayer
            .selectAll(".points > .point");
    };
    /**
     * Return d3 selection for all connections.
     * @return {d3.selection}
     */
    Renderer.prototype.getConnections = function () {
        var pointId = function (point) {
            var block = point.block;
            return block._id + "@" + (point.index + (point.isInput ? 0 : block.inputs.length));
        };
        return this._connectionLayer
            .selectAll(".connection")
            .data(this._graph.connections, function (connection) {
                return pointId(connection._inputPoint) + "," + pointId(connection._outputPoint);
            });
    };

    /**
     * Return if there are selected entities.
     * @param rootOnly {Boolean?} return only selected entities with the graph as parent.
     * @return {Boolean}
     */
    Renderer.prototype.hasSelectedEntities = function (rootOnly) {
        return this.getSelectedEntities(rootOnly)[0].length > 0;
    };

    /**
     * Return all selected entities.
     * @param rootOnly {Boolean?} return only selected entities with the graph as parent.
     * @return {d3.selection}
     */
    Renderer.prototype.getSelectedEntities = function (rootOnly) {
        return this._rootGroup
            .selectAll(".group.selected, .block.selected")
            .filter(function (entity) {
                return !rootOnly || entity.parent === this._graph;
            }.bind(this));
    };

    /**
     * Return all selected connections.
     * @returns {*}
     */
    Renderer.prototype.getSelectedConnections = function () {
        return this._rootGroup.selectAll(".connection.selected");
    };

    /**
     * Remove select connections and entities.
     */
    Renderer.prototype.removeSelection = function () {
        var renderer = this;
        var selectedConnections = this.getSelectedConnections();
        var selectedEntities = this.getSelectedEntities();
        selectedConnections.each(function (selectedConnection) {
            renderer._graph.removeConnection(selectedConnection);
        });
        selectedEntities.each(function (selectedEntity) {
            renderer._graph.removeEntity(selectedEntity);
        });
    };

    /**
     * Create a group from selection, putting all selected root entities in the children of the newly created group.
     * @param id
     * @param name
     */
    Renderer.prototype.createGroupFromSelection = function (id, name) {
        var renderer = this;
        var selectedEntities = renderer.getSelectedEntities(true);
        var selectionBbox = renderer._getSelectionZoomedSVGBox(selectedEntities);
        var group = new cg.Group(id, name,
            new pandora.Vec2(
                selectionBbox.x - this._config.group.padding,
                selectionBbox.y - this._config.group.padding - this._config.group.heading
            ),
            new pandora.Vec2(
                selectionBbox.width + this._config.group.padding * 2,
                selectionBbox.height + this._config.group.padding * 2 + this._config.group.heading
            )
        );
        renderer._graph.addEntity(group, renderer._graph);
        selectedEntities.each(function (selectedEntity) {
            renderer._graph.moveEntity(selectedEntity, group);
        });
    };

    /**
     * Create an empty group in the center of the viewport.
     * @param id
     * @param name
     */
    Renderer.prototype.createEmptyGroup = function (id, name) {
        var group = new cg.Group(id, name,
            new pandora.Vec2(0, 0),
            new pandora.Vec2(
                this._config.group.defaultWidth,
                this._config.group.defaultHeight
            )
        );
        this._graph.addEntity(group, this._graph);
    };

    /**
     * Create a group from selection if there is a selection, or create an empty group.
     * @param id
     * @param name
     */
    Renderer.prototype.createSmartGroup = function (id, name) {
        if (this.hasSelectedEntities()) {
            return this.createGroupFromSelection(id, name);
        }
        return this.createEmptyGroup(id, name);
    };

    return Renderer;

})();<commit_msg>Remove one useless updateRendererCollisions<commit_after>cg.Renderer = (function () {

    /**
     * Contain all the default value of the renderer config, these values will be used when they aren't specified
     * into the config given in parameter.
     * @type {Object}
     */
    var DEFAULT_CONFIG = {
        "zoom": {
            "maxZoom": 5,
            "minZoom": 0.25
        },
        "group": {
            "defaultWidth": 300,
            "defaultHeight": 200,
            "heading": 20,
            "padding": 20,
            "borderRadius": 0
        },
        "block": {
            "borderRadius": 0,
            "padding": 20,
            "heading": 40,
            "centerSpacing": 20
        },
        "getter": {
            "height": 40
        },
        "point": {
            "height": 20,
            "padding": 5,
            "circle-size": 3
        },
        "background-grid": {
            "pattern-size": 10,
            "step-size": 5,
            "line": {
                "stroke": "rgba(0, 0, 0, 0.1)",
                "stroke-width": 2
            },
            "big-line": {
                "stroke": "rgba(0, 0, 0, 0.4)",
                "stroke-width": 3
            }
        }
    };

    /**
     *
     * @extends pandora.EventEmitter
     * @constructor
     */
    var Renderer = pandora.class_("Renderer", pandora.EventEmitter, function (graph, svg, config) {
        pandora.EventEmitter.call(this);

        /**
         * Renderer configuration.
         * @type {Object}
         */
        this._config = pandora.mergeObjects(config || {}, DEFAULT_CONFIG, true, true);

        /**
         * Graph reference.
         * @type {cg.Graph}
         * @private
         */
        this._graph = graph;

        /**
         * The root SVG node this renderer is attached to.
         * @type {d3.selection}
         * @private
         */
        this._svg = d3.select(svg);

        /**
         * The root group created.
         * @type {d3.selection}
         * @private
         */
        this._rootGroup = null;

        /**
         * The group layer.
         * @type {d3.selection}
         * @private
         */
        this._groupLayer = null;

        /**
         * The connection layer.
         * @type {d3.selection}
         * @private
         */
        this._connectionLayer = null;

        /**
         * The layer for temporary connections that are not attached yet to a block but to the cursor instead.
         * @type {null}
         * @private
         */
        this._cursorConnectionLayer = null;

        /**
         * The block layer.
         * @type {d3.selection}
         * @private
         */
        this._blockLayer = null;

        /**
         * If this graph is already rendered.
         * @type {boolean}
         * @private
         */
        this._rendered = false;

        /**
         * Point used for matrix transformations.
         * @type {SVGPoint}
         * @private
         */
        this._svgPoint = null;

        /**
         *
         * @type {d3.selection}
         * @private
         */
        this._selectionRectangle = null;

        /**
         *
         * @type {pandora.Box2}
         * @private
         */
        this._selectionBox = new pandora.Box2();
    });

    /**
     *
     * @private
     */
    Renderer.prototype._initialize = function () {
        this._rootGroup = this._svg.append("svg:g");
        this._groupLayer = this._rootGroup.append("svg:g").attr("id", "group-layer");
        this._connectionLayer = this._rootGroup.append("svg:g").attr("id", "connection-layer");
        this._cursorConnectionLayer = this._rootGroup.append("svg:g").attr("id", "cursor-connection-layer");
        this._blockLayer = this._rootGroup.append("svg:g").attr("id", "block-layer");
        this._svgPoint = this._svg.node().createSVGPoint();
    };

    /**
     * Render the graph.
     */
    Renderer.prototype.render = function () {
        if (this._rendered) {
            this.emit("error", new cg.RendererError("Cannot re-render the graph"));
            return;
        }
        this._rendered = true;
        this._initialize();
        this._render();
        this._updateRendererCollisions();
        this._renderSelection();
        this._renderZoom();
        setTimeout(this._render.bind(this), 100); // TODO: First render, all elements have bad size.
        this._graph.on("entity.add", function () {
            this._render();
        }.bind(this));
        this._graph.on("entity.remove", function () {
            this._render();
        }.bind(this));
        this._graph.on("connection.add", function () {
            this._renderConnections(this.getConnections());
        }.bind(this));
        this._graph.on("connection.remove", function () {
            this._renderConnections(this.getConnections());
        }.bind(this));
    };

    /**
     * Return d3 selection for all entities.
     * @return {d3.selection}
     */
    Renderer.prototype.getEntities = function () {
        return this._rootGroup
            .selectAll("#group-layer > .group, #block-layer > .block");
    };

    /**
     * Return d3 selection for all groups.
     * @return {d3.selection}
     */
    Renderer.prototype.getGroups = function () {
        return this._groupLayer
            .selectAll(".group")
            .data(this._graph.groups(), function (group) {
                return group._id;
            });
    };

    /**
     * Return d3 selection for all blocks.
     * @return {d3.selection}
     */
    Renderer.prototype.getBlocks = function () {
        return this._blockLayer
            .selectAll(".block")
            .data(this._graph.blocks(), function (block) {
                return block._id;
            });
    };

    /**
     * Return d3 selection for all points.
     * @return {d3.selection}
     */
    Renderer.prototype.getPoints = function () {
        return this._blockLayer
            .selectAll(".points > .point");
    };
    /**
     * Return d3 selection for all connections.
     * @return {d3.selection}
     */
    Renderer.prototype.getConnections = function () {
        var pointId = function (point) {
            var block = point.block;
            return block._id + "@" + (point.index + (point.isInput ? 0 : block.inputs.length));
        };
        return this._connectionLayer
            .selectAll(".connection")
            .data(this._graph.connections, function (connection) {
                return pointId(connection._inputPoint) + "," + pointId(connection._outputPoint);
            });
    };

    /**
     * Return if there are selected entities.
     * @param rootOnly {Boolean?} return only selected entities with the graph as parent.
     * @return {Boolean}
     */
    Renderer.prototype.hasSelectedEntities = function (rootOnly) {
        return this.getSelectedEntities(rootOnly)[0].length > 0;
    };

    /**
     * Return all selected entities.
     * @param rootOnly {Boolean?} return only selected entities with the graph as parent.
     * @return {d3.selection}
     */
    Renderer.prototype.getSelectedEntities = function (rootOnly) {
        return this._rootGroup
            .selectAll(".group.selected, .block.selected")
            .filter(function (entity) {
                return !rootOnly || entity.parent === this._graph;
            }.bind(this));
    };

    /**
     * Return all selected connections.
     * @returns {*}
     */
    Renderer.prototype.getSelectedConnections = function () {
        return this._rootGroup.selectAll(".connection.selected");
    };

    /**
     * Remove select connections and entities.
     */
    Renderer.prototype.removeSelection = function () {
        var renderer = this;
        var selectedConnections = this.getSelectedConnections();
        var selectedEntities = this.getSelectedEntities();
        selectedConnections.each(function (selectedConnection) {
            renderer._graph.removeConnection(selectedConnection);
        });
        selectedEntities.each(function (selectedEntity) {
            renderer._graph.removeEntity(selectedEntity);
        });
    };

    /**
     * Create a group from selection, putting all selected root entities in the children of the newly created group.
     * @param id
     * @param name
     */
    Renderer.prototype.createGroupFromSelection = function (id, name) {
        var renderer = this;
        var selectedEntities = renderer.getSelectedEntities(true);
        var selectionBbox = renderer._getSelectionZoomedSVGBox(selectedEntities);
        var group = new cg.Group(id, name,
            new pandora.Vec2(
                selectionBbox.x - this._config.group.padding,
                selectionBbox.y - this._config.group.padding - this._config.group.heading
            ),
            new pandora.Vec2(
                selectionBbox.width + this._config.group.padding * 2,
                selectionBbox.height + this._config.group.padding * 2 + this._config.group.heading
            )
        );
        renderer._graph.addEntity(group, renderer._graph);
        selectedEntities.each(function (selectedEntity) {
            renderer._graph.moveEntity(selectedEntity, group);
        });
    };

    /**
     * Create an empty group in the center of the viewport.
     * @param id
     * @param name
     */
    Renderer.prototype.createEmptyGroup = function (id, name) {
        var group = new cg.Group(id, name,
            new pandora.Vec2(0, 0),
            new pandora.Vec2(
                this._config.group.defaultWidth,
                this._config.group.defaultHeight
            )
        );
        this._graph.addEntity(group, this._graph);
    };

    /**
     * Create a group from selection if there is a selection, or create an empty group.
     * @param id
     * @param name
     */
    Renderer.prototype.createSmartGroup = function (id, name) {
        if (this.hasSelectedEntities()) {
            return this.createGroupFromSelection(id, name);
        }
        return this.createEmptyGroup(id, name);
    };

    return Renderer;

})();<|endoftext|>


<commit_before>'use strict';

// Load modules

const Diff = require('diff');
const StringifySafe = require('json-stringify-safe');
const StableStringify = require('json-stable-stringify');

// Declare internals

const internals = {
    width: 50
};

internals.stringify = (obj, serializer, indent) => {

    return StableStringify(obj,
        { space: indent, replacer: StringifySafe.getSerialize(serializer) });
};

exports = module.exports = internals.Reporter = function (options) {

    this.settings = options;
    this.count = 0;
    this.last = [];

    this.colors = internals.colors(options.colors);
};

internals.Reporter.prototype.start = function (notebook) {

};

internals.Reporter.prototype.test = function (test) {

    if (this.settings.progress === 0) {
        return;
    }

    if (this.settings['silent-skips'] && (test.skipped || test.todo)) {
        return;
    }

    if (this.settings.progress === 1) {

        // ..x....x.-..

        if (!this.count) {
            this.report('\n  ');
        }

        this.count++;

        if ((this.count - 1) % internals.width === 0) {
            this.report('\n  ');
        }

        this.report(test.err ? this.colors.red('x') : (test.skipped || test.todo ? this.colors.magenta('-') : '.'));
    }
    else {

        // Can't be fully covereed but it's ok
        // $lab:coverage:off$
        const check = process.platform === 'win32' ? '\u221A' : '\u2714';
        const asterisk = process.platform === 'win32' ? '\u00D7' : '\u2716';
        // $lab:coverage:on$

        // Verbose (Spec reporter)

        for (let i = 0; i < test.path.length; ++i) {
            if (test.path[i] !== this.last[i] || (i > 0 && test.path[i - 1] !== this.last[i - 1])) {
                this.report(internals.spacer(i * 2) + test.path[i] + '\n');
            }
        }

        this.last = test.path;

        const spacer = internals.spacer(test.path.length * 2);
        if (test.err) {
            this.report(spacer + this.colors.red(asterisk + ' ' + test.id + ') ' + test.relativeTitle) + '\n');
        }
        else {
            const symbol = test.skipped || test.todo ? this.colors.magenta('-') : this.colors.green(check);
            const assertion = test.assertions === undefined ? '' : ' and ' + test.assertions + ' assertions';
            this.report(spacer + symbol + ' ' + this.colors.gray(test.id + ') ' + test.relativeTitle +
                ' (' + test.duration + ' ms' + assertion + ')') + '\n');
        }
    }
};

internals.spacer = function (length) {

    return new Array(length + 1).join(' ');
};

internals.stringifyReplacer = function (key, value) {

    // Show usually invisible values from JSON.stringify in a different way,
    // follow the bracket format of json-stringify-safe.

    if (value === undefined) {
        return '[undefined]';
    }

    if (typeof value === 'function' || value === Infinity || value === -Infinity) {
        return '[' + value.toString() + ']';
    }

    /* $lab:coverage:off$ */ // There is no way to cover that in node 0.10
    if (typeof value === 'symbol') {
        return '[' + value.toString() + ']';
    }
    /* $lab:coverage:on$ */

    return value;
};

internals.Reporter.prototype.end = function (notebook) {

    if (this.settings.progress) {
        this.report('\n\n');
    }

    // Colors

    const red = this.colors.red;
    const green = this.colors.green;
    const gray = this.colors.gray;
    const yellow = this.colors.yellow;
    const whiteRedBg = this.colors.whiteRedBg;
    const blackGreenBg = this.colors.blackGreenBg;
    const magenta = this.colors.magenta;

    // Tests

    const notes = notebook.tests.filter(internals.filterNotes);

    const failures = notebook.tests.filter(internals.filterFailures);

    const skipped = notebook.tests.filter(internals.filterSkipped);

    let output = '';
    const totalTests = notebook.tests.length - skipped.length;

    const errors = notebook.errors || [];
    if (errors.length) {
        output += 'Test script errors:\n\n';
        errors.forEach((err = {}) => {

            output += red(err.message) + '\n';
            if (err.stack) {
                const stack = err.stack.slice(err.stack.indexOf('\n') + 1)
                    .replace(/^/gm, '  ')
                    .split('\n')
                    .filter(internals.filterNodeModules)               // Remove node_modules files
                    .slice(0, 5)                                       // Show only first 5 stack lines
                    .join('\n');

                output += gray(stack) + '\n';
            }

            output += '\n';
        });
        output += red('There were ' + errors.length + ' test script error(s).') + '\n\n';
    }

    if (failures.length) {
        output += 'Failed tests:\n\n';

        for (let i = 0; i < failures.length; ++i) {
            const test = failures[i];
            const message = test.err.message || '';

            output += '  ' + test.id + ') ' + test.title + ':\n\n';

            // Actual vs Expected

            if (test.err.actual !== undefined &&
                test.err.expected !== undefined) {

                const actual = internals.stringify(test.err.actual, internals.stringifyReplacer, 2);
                const expected = internals.stringify(test.err.expected, internals.stringifyReplacer, 2);

                output += '      ' + whiteRedBg('actual') + ' ' + blackGreenBg('expected') + '\n\n      ';

                const comparison = Diff.diffWords(actual, expected);
                for (let j = 0; j < comparison.length; ++j) {
                    const item = comparison[j];
                    const value = item.value;
                    const lines = value.split('\n');
                    for (let k = 0; k < lines.length; ++k) {
                        if (k) {
                            output += '\n      ';
                        }

                        if (item.added || item.removed) {
                            output += item.added ? blackGreenBg(lines[k]) : whiteRedBg(lines[k]);
                        }
                        else {
                            output += lines[k];
                        }
                    }
                }

                output += '\n\n      ' + yellow(message);
                output += '\n\n';
            }
            else {
                output += '      ' + red(message) + '\n\n';
            }

            if (test.err.at) {
                output += gray('      at ' + test.err.at.filename + ':' + test.err.at.line + ':' + test.err.at.column) + '\n';
            }
            else if (!test.timeout &&
                test.err.stack) {
                let stack = test.err.stack;
                if (stack.indexOf(message) !== -1) {
                    stack = stack.slice(stack.indexOf(message) + message.length + 1);
                }

                output += gray(stack.replace(/^/gm, '  ')) + '\n';
            }

            if (test.err.data) {
                const isObject = typeof test.err.data === 'object' && !Array.isArray(test.err.data);
                let errorData = internals.stringify(test.err.data, null, isObject ? 4 : null);
                if (isObject) {
                    errorData = errorData.replace(/(\n\s*)"(.*)"\:/g, '$1$2:').split('\n').slice(1, -1).join('\n');
                }

                output += gray('\n      Additional error data:\n' + errorData.replace(/^/gm, '      ')) + '\n';
            }

            output += '\n';
        }

        output += '\n' + red(failures.length + ' of ' + totalTests + ' tests failed');
    }
    else {
        output += green(totalTests + ' tests complete');
    }

    if (skipped.length) {
        output += magenta(` (${skipped.length} skipped)`);
    }

    output += '\n';
    output += 'Test duration: ' + notebook.ms + ' ms\n';

    // Assertions

    if (notebook.assertions !== undefined) {
        output += 'Assertions count: ' + notebook.assertions + ' (verbosity: ' + (notebook.assertions / totalTests).toFixed(2) + ')\n';
    }

    // Leaks

    if (notebook.leaks) {
        if (notebook.leaks.length) {
            output += red('The following leaks were detected:' + notebook.leaks.join(', ')) + '\n';
        }
        else {
            output += green('No global variable leaks detected') + '\n';
        }
    }

    if (notebook.shuffle) {
        output += 'Randomized with seed: ' + notebook.seed + '. Use --shuffle --seed ' + notebook.seed + ' to run tests in same order again.\n';
    }

    // Coverage

    const coverage = notebook.coverage;
    if (coverage) {
        const status = 'Coverage: ' + coverage.percent.toFixed(2) + '%';

        output += coverage.percent === 100 ? green(status) : red(status + ' (' + (coverage.sloc - coverage.hits) + '/' + coverage.sloc + ')');
        if (coverage.percent < 100) {
            coverage.files.forEach((file) => {

                let missingLines;
                if (file.sourcemaps) {
                    const missingLinesByFile = {};
                    Object.keys(file.source).forEach((lineNumber) => {

                        const line = file.source[lineNumber];
                        if (line.miss) {
                            missingLines = missingLinesByFile[line.originalFilename] = missingLinesByFile[line.originalFilename] || [];
                            missingLines.push(line.originalLine);
                        }
                    });

                    const files = Object.keys(missingLinesByFile);
                    if (files.length) {
                        output += red('\n' + file.filename + ' missing coverage from file(s):');
                        files.forEach((filename) => {

                            output += red('\n\t' + filename + ' on line(s): ' + missingLinesByFile[filename].join(', '));
                        });
                    }
                }
                else {
                    missingLines = [];
                    Object.keys(file.source).forEach((lineNumber) => {

                        const line = file.source[lineNumber];
                        if (line.miss) {
                            missingLines.push(parseInt(lineNumber, 10));
                        }
                    });

                    if (missingLines.length) {
                        const lineReportLimit = 8;
                        const span = {
                            start: missingLines[0],
                            end: missingLines[0]
                        };
                        const missingLinesReport = [];
                        for (let i = 1; i <= missingLines.length; ++i) {
                            const line = missingLines[i];
                            if (line === span.end + 1) {
                                // Extend the current span.
                                span.end = line;
                            }
                            else {
                                // Flush current span to output.
                                if (span.start === span.end) {
                                    missingLinesReport.push(span.start.toString());
                                }
                                else if (span.start + 1 === span.end) {
                                    missingLinesReport.push(span.start.toString());
                                    missingLinesReport.push(span.end.toString());
                                }
                                else {
                                    missingLinesReport.push(span.start.toString() + '-' + span.end.toString());
                                }

                                // Start a new span.
                                span.start = line;
                                span.end = line;
                            }

                            // If the report gets too long, truncate it.
                            if (missingLinesReport.length >= lineReportLimit) {
                                const remainingLines = missingLines.length - i;
                                missingLinesReport.push('and ' + remainingLines + ' more');
                                break;
                            }
                        }

                        output += red('\n' + file.filename + ' missing coverage on line(s): ' + missingLinesReport.join(', '));
                    }
                }
            });

            if (coverage.percent < this.settings.threshold || isNaN(coverage.percent)) {
                output += red('\nCode coverage below threshold: ' + coverage.percent.toFixed(2) + ' < ' + this.settings.threshold);
            }
        }

        output += '\n';
    }

    const lint = notebook.lint;
    if (lint) {
        output += 'Linting results:';

        let hasErrors = false;
        lint.lint.forEach((entry) => {

            // Don't show anything if there aren't issues
            if (!entry.errors || !entry.errors.length) {
                return;
            }

            hasErrors = true;
            output += gray('\n\t' + entry.filename + ':');
            entry.errors.forEach((err) => {

                output += (err.severity === 'ERROR' ? red : yellow)('\n\t\tLine ' + err.line + ': ' + err.message);
            });
        });

        if (!hasErrors) {
            output += green(' No issues\n');
        }
    }

    if (notes.length) {
        output += '\n\nTest notes:\n';
        notes.forEach((test) => {

            output += gray(test.relativeTitle) + '\n';
            test.notes.forEach((note) => {

                output += yellow(`\t* ${note}`);
            });
        });
    }

    output += '\n';
    this.report(output);
};

internals.color = function (name, code, enabled) {

    if (enabled && Array.isArray(code)) {
        const color = '\u001b[' + code[0] + ';' + code[1] + 'm';
        return function (text) {

            return color + text + '\u001b[0m';
        };
    }
    else if (enabled) {
        const color = '\u001b[' + code + 'm';
        return function (text) {

            return color + text + '\u001b[0m';
        };
    }

    return function (text) {

        return text;
    };
};

internals.colors = function (enabled) {

    if (enabled === null) {
        enabled = require('supports-color');
    }

    const codes = {
        'black': 0,
        'gray': 92,
        'red': 31,
        'green': 32,
        'yellow': 33,
        'magenta': 35,
        'redBg': 41,
        'greenBg': 42,
        'whiteRedBg': [37, 41],
        'blackGreenBg': [30, 42]
    };

    const colors = {};
    const names = Object.keys(codes);
    for (let i = 0; i < names.length; ++i) {
        const name = names[i];
        colors[name] = internals.color(name, codes[name], enabled);
    }

    return colors;
};

internals.filterNotes = function (test) {

    return test.notes && test.notes.length;
};

internals.filterFailures = function (test) {

    return !!test.err;
};

internals.filterNodeModules = function (line) {

    return !(/\/node_modules\//.test(line));
};

internals.filterSkipped = function (test) {

    return test.skipped || test.todo;
};
<commit_msg>Recolor console coverage report details to improve readability<commit_after>'use strict';

// Load modules

const Diff = require('diff');
const StringifySafe = require('json-stringify-safe');
const StableStringify = require('json-stable-stringify');

// Declare internals

const internals = {
    width: 50
};

internals.stringify = (obj, serializer, indent) => {

    return StableStringify(obj,
        { space: indent, replacer: StringifySafe.getSerialize(serializer) });
};

exports = module.exports = internals.Reporter = function (options) {

    this.settings = options;
    this.count = 0;
    this.last = [];

    this.colors = internals.colors(options.colors);
};

internals.Reporter.prototype.start = function (notebook) {

};

internals.Reporter.prototype.test = function (test) {

    if (this.settings.progress === 0) {
        return;
    }

    if (this.settings['silent-skips'] && (test.skipped || test.todo)) {
        return;
    }

    if (this.settings.progress === 1) {

        // ..x....x.-..

        if (!this.count) {
            this.report('\n  ');
        }

        this.count++;

        if ((this.count - 1) % internals.width === 0) {
            this.report('\n  ');
        }

        this.report(test.err ? this.colors.red('x') : (test.skipped || test.todo ? this.colors.magenta('-') : '.'));
    }
    else {

        // Can't be fully covereed but it's ok
        // $lab:coverage:off$
        const check = process.platform === 'win32' ? '\u221A' : '\u2714';
        const asterisk = process.platform === 'win32' ? '\u00D7' : '\u2716';
        // $lab:coverage:on$

        // Verbose (Spec reporter)

        for (let i = 0; i < test.path.length; ++i) {
            if (test.path[i] !== this.last[i] || (i > 0 && test.path[i - 1] !== this.last[i - 1])) {
                this.report(internals.spacer(i * 2) + test.path[i] + '\n');
            }
        }

        this.last = test.path;

        const spacer = internals.spacer(test.path.length * 2);
        if (test.err) {
            this.report(spacer + this.colors.red(asterisk + ' ' + test.id + ') ' + test.relativeTitle) + '\n');
        }
        else {
            const symbol = test.skipped || test.todo ? this.colors.magenta('-') : this.colors.green(check);
            const assertion = test.assertions === undefined ? '' : ' and ' + test.assertions + ' assertions';
            this.report(spacer + symbol + ' ' + this.colors.gray(test.id + ') ' + test.relativeTitle +
                ' (' + test.duration + ' ms' + assertion + ')') + '\n');
        }
    }
};

internals.spacer = function (length) {

    return new Array(length + 1).join(' ');
};

internals.stringifyReplacer = function (key, value) {

    // Show usually invisible values from JSON.stringify in a different way,
    // follow the bracket format of json-stringify-safe.

    if (value === undefined) {
        return '[undefined]';
    }

    if (typeof value === 'function' || value === Infinity || value === -Infinity) {
        return '[' + value.toString() + ']';
    }

    /* $lab:coverage:off$ */ // There is no way to cover that in node 0.10
    if (typeof value === 'symbol') {
        return '[' + value.toString() + ']';
    }
    /* $lab:coverage:on$ */

    return value;
};

internals.Reporter.prototype.end = function (notebook) {

    if (this.settings.progress) {
        this.report('\n\n');
    }

    // Colors

    const red = this.colors.red;
    const green = this.colors.green;
    const gray = this.colors.gray;
    const yellow = this.colors.yellow;
    const whiteRedBg = this.colors.whiteRedBg;
    const blackGreenBg = this.colors.blackGreenBg;
    const magenta = this.colors.magenta;

    // Tests

    const notes = notebook.tests.filter(internals.filterNotes);

    const failures = notebook.tests.filter(internals.filterFailures);

    const skipped = notebook.tests.filter(internals.filterSkipped);

    let output = '';
    const totalTests = notebook.tests.length - skipped.length;

    const errors = notebook.errors || [];
    if (errors.length) {
        output += 'Test script errors:\n\n';
        errors.forEach((err = {}) => {

            output += red(err.message) + '\n';
            if (err.stack) {
                const stack = err.stack.slice(err.stack.indexOf('\n') + 1)
                    .replace(/^/gm, '  ')
                    .split('\n')
                    .filter(internals.filterNodeModules)               // Remove node_modules files
                    .slice(0, 5)                                       // Show only first 5 stack lines
                    .join('\n');

                output += gray(stack) + '\n';
            }

            output += '\n';
        });
        output += red('There were ' + errors.length + ' test script error(s).') + '\n\n';
    }

    if (failures.length) {
        output += 'Failed tests:\n\n';

        for (let i = 0; i < failures.length; ++i) {
            const test = failures[i];
            const message = test.err.message || '';

            output += '  ' + test.id + ') ' + test.title + ':\n\n';

            // Actual vs Expected

            if (test.err.actual !== undefined &&
                test.err.expected !== undefined) {

                const actual = internals.stringify(test.err.actual, internals.stringifyReplacer, 2);
                const expected = internals.stringify(test.err.expected, internals.stringifyReplacer, 2);

                output += '      ' + whiteRedBg('actual') + ' ' + blackGreenBg('expected') + '\n\n      ';

                const comparison = Diff.diffWords(actual, expected);
                for (let j = 0; j < comparison.length; ++j) {
                    const item = comparison[j];
                    const value = item.value;
                    const lines = value.split('\n');
                    for (let k = 0; k < lines.length; ++k) {
                        if (k) {
                            output += '\n      ';
                        }

                        if (item.added || item.removed) {
                            output += item.added ? blackGreenBg(lines[k]) : whiteRedBg(lines[k]);
                        }
                        else {
                            output += lines[k];
                        }
                    }
                }

                output += '\n\n      ' + yellow(message);
                output += '\n\n';
            }
            else {
                output += '      ' + red(message) + '\n\n';
            }

            if (test.err.at) {
                output += gray('      at ' + test.err.at.filename + ':' + test.err.at.line + ':' + test.err.at.column) + '\n';
            }
            else if (!test.timeout &&
                test.err.stack) {
                let stack = test.err.stack;
                if (stack.indexOf(message) !== -1) {
                    stack = stack.slice(stack.indexOf(message) + message.length + 1);
                }

                output += gray(stack.replace(/^/gm, '  ')) + '\n';
            }

            if (test.err.data) {
                const isObject = typeof test.err.data === 'object' && !Array.isArray(test.err.data);
                let errorData = internals.stringify(test.err.data, null, isObject ? 4 : null);
                if (isObject) {
                    errorData = errorData.replace(/(\n\s*)"(.*)"\:/g, '$1$2:').split('\n').slice(1, -1).join('\n');
                }

                output += gray('\n      Additional error data:\n' + errorData.replace(/^/gm, '      ')) + '\n';
            }

            output += '\n';
        }

        output += '\n' + red(failures.length + ' of ' + totalTests + ' tests failed');
    }
    else {
        output += green(totalTests + ' tests complete');
    }

    if (skipped.length) {
        output += magenta(` (${skipped.length} skipped)`);
    }

    output += '\n';
    output += 'Test duration: ' + notebook.ms + ' ms\n';

    // Assertions

    if (notebook.assertions !== undefined) {
        output += 'Assertions count: ' + notebook.assertions + ' (verbosity: ' + (notebook.assertions / totalTests).toFixed(2) + ')\n';
    }

    // Leaks

    if (notebook.leaks) {
        if (notebook.leaks.length) {
            output += red('The following leaks were detected:' + notebook.leaks.join(', ')) + '\n';
        }
        else {
            output += green('No global variable leaks detected') + '\n';
        }
    }

    if (notebook.shuffle) {
        output += 'Randomized with seed: ' + notebook.seed + '. Use --shuffle --seed ' + notebook.seed + ' to run tests in same order again.\n';
    }

    // Coverage

    const coverage = notebook.coverage;
    if (coverage) {
        const status = 'Coverage: ' + coverage.percent.toFixed(2) + '%';

        output += coverage.percent === 100 ? green(status) : red(status + ' (' + (coverage.sloc - coverage.hits) + '/' + coverage.sloc + ')');
        if (coverage.percent < 100) {
            coverage.files.forEach((file) => {

                let missingLines;
                if (file.sourcemaps) {
                    const missingLinesByFile = {};
                    Object.keys(file.source).forEach((lineNumber) => {

                        const line = file.source[lineNumber];
                        if (line.miss) {
                            missingLines = missingLinesByFile[line.originalFilename] = missingLinesByFile[line.originalFilename] || [];
                            missingLines.push(line.originalLine);
                        }
                    });

                    const files = Object.keys(missingLinesByFile);
                    if (files.length) {
                        output += red('\n' + file.filename + ' missing coverage from file(s):');
                        files.forEach((filename) => {

                            output += red('\n\t' + filename + ' on line(s): ' + missingLinesByFile[filename].join(', '));
                        });
                    }
                }
                else {
                    missingLines = [];
                    Object.keys(file.source).forEach((lineNumber) => {

                        const line = file.source[lineNumber];
                        if (line.miss) {
                            missingLines.push(parseInt(lineNumber, 10));
                        }
                    });

                    if (missingLines.length) {
                        const lineReportLimit = 8;
                        const span = {
                            start: missingLines[0],
                            end: missingLines[0]
                        };
                        const missingLinesReport = [];
                        for (let i = 1; i <= missingLines.length; ++i) {
                            const line = missingLines[i];
                            if (line === span.end + 1) {
                                // Extend the current span.
                                span.end = line;
                            }
                            else {
                                // Flush current span to output.
                                if (span.start === span.end) {
                                    missingLinesReport.push(span.start.toString());
                                }
                                else if (span.start + 1 === span.end) {
                                    missingLinesReport.push(span.start.toString());
                                    missingLinesReport.push(span.end.toString());
                                }
                                else {
                                    missingLinesReport.push(span.start.toString() + '-' + span.end.toString());
                                }

                                // Start a new span.
                                span.start = line;
                                span.end = line;
                            }

                            // If the report gets too long, truncate it.
                            if (missingLinesReport.length >= lineReportLimit) {
                                const remainingLines = missingLines.length - i;
                                missingLinesReport.push('and ' + remainingLines + ' more');
                                break;
                            }
                        }

                        output += yellow('\n' + file.filename + ' missing coverage on line(s): ' + missingLinesReport.join(', '));
                    }
                }
            });

            if (coverage.percent < this.settings.threshold || isNaN(coverage.percent)) {
                output += red('\nCode coverage below threshold: ' + coverage.percent.toFixed(2) + ' < ' + this.settings.threshold);
            }
        }

        output += '\n';
    }

    const lint = notebook.lint;
    if (lint) {
        output += 'Linting results:';

        let hasErrors = false;
        lint.lint.forEach((entry) => {

            // Don't show anything if there aren't issues
            if (!entry.errors || !entry.errors.length) {
                return;
            }

            hasErrors = true;
            output += gray('\n\t' + entry.filename + ':');
            entry.errors.forEach((err) => {

                output += (err.severity === 'ERROR' ? red : yellow)('\n\t\tLine ' + err.line + ': ' + err.message);
            });
        });

        if (!hasErrors) {
            output += green(' No issues\n');
        }
    }

    if (notes.length) {
        output += '\n\nTest notes:\n';
        notes.forEach((test) => {

            output += gray(test.relativeTitle) + '\n';
            test.notes.forEach((note) => {

                output += yellow(`\t* ${note}`);
            });
        });
    }

    output += '\n';
    this.report(output);
};

internals.color = function (name, code, enabled) {

    if (enabled && Array.isArray(code)) {
        const color = '\u001b[' + code[0] + ';' + code[1] + 'm';
        return function (text) {

            return color + text + '\u001b[0m';
        };
    }
    else if (enabled) {
        const color = '\u001b[' + code + 'm';
        return function (text) {

            return color + text + '\u001b[0m';
        };
    }

    return function (text) {

        return text;
    };
};

internals.colors = function (enabled) {

    if (enabled === null) {
        enabled = require('supports-color');
    }

    const codes = {
        'black': 0,
        'gray': 92,
        'red': 31,
        'green': 32,
        'yellow': 33,
        'magenta': 35,
        'redBg': 41,
        'greenBg': 42,
        'whiteRedBg': [37, 41],
        'blackGreenBg': [30, 42]
    };

    const colors = {};
    const names = Object.keys(codes);
    for (let i = 0; i < names.length; ++i) {
        const name = names[i];
        colors[name] = internals.color(name, codes[name], enabled);
    }

    return colors;
};

internals.filterNotes = function (test) {

    return test.notes && test.notes.length;
};

internals.filterFailures = function (test) {

    return !!test.err;
};

internals.filterNodeModules = function (line) {

    return !(/\/node_modules\//.test(line));
};

internals.filterSkipped = function (test) {

    return test.skipped || test.todo;
};
<|endoftext|>