<commit_before>/**
 * imageSlider.jquery.js
 *
 * By Nachiketha S H Upadhya
 * Free to use under the MIT license.
 *
 * Aug 2015
 */

(function( $ ) {

    $.fn.imageSlider = function(data) {
        var elem = this;
        this.css({
            position: 'relative',
            overflow: 'hidden'
        });

        var leftArrow = $("<a/>", {class: "left-arrow"}).text("<<").css({'border-radius': '0 2px 2px 0'});
        var rightArrow = $("<a/>", {class: "right-arrow"}).text(">>").css({'border-radius': '2px 0 0 2px', right: '0'});

        this.append(leftArrow).append(rightArrow);
        this.find('a').css({userSelect: 'none'});

        $("a.left-arrow, a.right-arrow").css({
            position: 'absolute',
            top: '40%',
            'z-index': '999',
            display: 'block',
            padding: '4% 2%',
            width: 'auto',
            height: 'auto',
            background: '#2a2a2a',
            color: '#fff',
            'text-decoration': 'none',
            'font-weight': '600',
            'font-size': '18px',
            opacity: '0.6',
            cursor: 'pointer'
        });

        var ul = $("<ul/>").css({
            position: 'relative',
            margin: '0',
            padding: '0',
            'list-style': 'none'
        });

        for (var i = 0; i < data.length; i++) {
            ul.append($("<li/>", { value: i+1 }).css({
                'background-image': 'url(' + data[i].src + ')',
                'background-repeat': 'no-repeat',
                'background-size': 'contain',
                'background-position': 'center'
            }).append($('<footer/>').text(data[i].title).css({
                position: 'absolute',
                bottom: '0',
                'text-align': 'center',
                width: '100%',
                padding: '5px 0',
                'background-color': 'rgba(85, 86, 86, 0.4)',
                'font-weight': 'bold'
            })));
        }

        ul.appendTo(this);
        var ulElem = elem.find('ul');

        this.find("li").css({
            position: 'relative',
            display: 'block',
            'float': 'left',
            width: this.width(),
            height: this.height()
        });

        var slideCount = this.find('ul li').length;
        var slideWidth = this.width();
        var slideHeight = this.height();
        var sliderUlWidth = slideCount * slideWidth;

        this.css({ width: slideWidth, height: slideHeight });

        this.children("ul").css({ width: sliderUlWidth, marginLeft: -slideWidth});

        elem.find('ul li:last-child').prependTo($('ul'));

        this.moveLeft = function() {
            ulElem.animate({
                left: + slideWidth
            }, 200, function () {
                elem.find('ul li:last-child').prependTo(ulElem);
                ulElem.css('left', '');
                $(elem).trigger("slide", elem.find( "ul li:nth-child(2)" ).val());
            });
            return this;
        };

        this.moveRight = function() {
            ulElem.animate({
                left: - slideWidth
            }, 200, function () {
                elem.find('ul li:first-child').appendTo(ulElem);
                ulElem.css('left', '');
                $(elem).trigger("slide", elem.find( "ul li:nth-child(2)" ).val());
            });
            return this;
        };

        $('a.left-arrow').click(function () {
            elem.moveLeft();
        });

        $('a.right-arrow').click(function () {
            elem.moveRight();
        });

        return this;
    };

}( jQuery ));<commit_msg>Added src/github link in src<commit_after>/**
 * imageSlider.jquery.js
 *
 * By Nachiketha S H Upadhya
 * https://github.com/nachikethashu/Image-Slider-JQuery
 *
 * Free to use under the MIT license.
 *
 * Aug 2015
 */

(function( $ ) {

    $.fn.imageSlider = function(data) {
        var elem = this;
        this.css({
            position: 'relative',
            overflow: 'hidden'
        });

        var leftArrow = $("<a/>", {class: "left-arrow"}).text("<<").css({'border-radius': '0 2px 2px 0'});
        var rightArrow = $("<a/>", {class: "right-arrow"}).text(">>").css({'border-radius': '2px 0 0 2px', right: '0'});

        this.append(leftArrow).append(rightArrow);
        this.find('a').css({userSelect: 'none'});

        $("a.left-arrow, a.right-arrow").css({
            position: 'absolute',
            top: '40%',
            'z-index': '999',
            display: 'block',
            padding: '4% 2%',
            width: 'auto',
            height: 'auto',
            background: '#2a2a2a',
            color: '#fff',
            'text-decoration': 'none',
            'font-weight': '600',
            'font-size': '18px',
            opacity: '0.6',
            cursor: 'pointer'
        });

        var ul = $("<ul/>").css({
            position: 'relative',
            margin: '0',
            padding: '0',
            'list-style': 'none'
        });

        for (var i = 0; i < data.length; i++) {
            ul.append($("<li/>", { value: i+1 }).css({
                'background-image': 'url(' + data[i].src + ')',
                'background-repeat': 'no-repeat',
                'background-size': 'contain',
                'background-position': 'center'
            }).append($('<footer/>').text(data[i].title).css({
                position: 'absolute',
                bottom: '0',
                'text-align': 'center',
                width: '100%',
                padding: '5px 0',
                'background-color': 'rgba(85, 86, 86, 0.4)',
                'font-weight': 'bold'
            })));
        }

        ul.appendTo(this);
        var ulElem = elem.find('ul');

        this.find("li").css({
            position: 'relative',
            display: 'block',
            'float': 'left',
            width: this.width(),
            height: this.height()
        });

        var slideCount = this.find('ul li').length;
        var slideWidth = this.width();
        var slideHeight = this.height();
        var sliderUlWidth = slideCount * slideWidth;

        this.css({ width: slideWidth, height: slideHeight });

        this.children("ul").css({ width: sliderUlWidth, marginLeft: -slideWidth});

        elem.find('ul li:last-child').prependTo($('ul'));

        this.moveLeft = function() {
            ulElem.animate({
                left: + slideWidth
            }, 200, function () {
                elem.find('ul li:last-child').prependTo(ulElem);
                ulElem.css('left', '');
                $(elem).trigger("slide", elem.find( "ul li:nth-child(2)" ).val());
            });
            return this;
        };

        this.moveRight = function() {
            ulElem.animate({
                left: - slideWidth
            }, 200, function () {
                elem.find('ul li:first-child').appendTo(ulElem);
                ulElem.css('left', '');
                $(elem).trigger("slide", elem.find( "ul li:nth-child(2)" ).val());
            });
            return this;
        };

        $('a.left-arrow').click(function () {
            elem.moveLeft();
        });

        $('a.right-arrow').click(function () {
            elem.moveRight();
        });

        return this;
    };

}( jQuery ));<|endoftext|>


<commit_before>import { platform } from '../core/platform.js';
import { EventHandler } from '../core/event-handler.js';

import { TouchEvent } from './touch-event.js';

/**
 * Attach a TouchDevice to an element and it will receive and fire events when the element is
 * touched. See also {@link Touch} and {@link TouchEvent}.
 *
 * @augments EventHandler
 */
class TouchDevice extends EventHandler {
    /**
     * Create a new touch device and attach it to an element.
     *
     * @param {Element} element - The element to attach listen for events on.
     */
    constructor(element) {
        super();

        this._element = null;

        this._startHandler = this._handleTouchStart.bind(this);
        this._endHandler = this._handleTouchEnd.bind(this);
        this._moveHandler = this._handleTouchMove.bind(this);
        this._cancelHandler = this._handleTouchCancel.bind(this);

        this.attach(element);
    }

    /**
     * Attach a device to an element in the DOM. If the device is already attached to an element
     * this method will detach it first.
     *
     * @param {Element} element - The element to attach to.
     */
    attach(element) {
        if (this._element) {
            this.detach();
        }

        this._element = element;

        const opts = platform.passiveEvents ? { passive: true } : false;
        this._element.addEventListener('touchstart', this._startHandler, opts);
        this._element.addEventListener('touchend', this._endHandler, opts);
        this._element.addEventListener('touchmove', this._moveHandler, opts);
        this._element.addEventListener('touchcancel', this._cancelHandler, opts);
    }

    /**
     * Detach a device from the element it is attached to.
     */
    detach() {
        if (this._element) {
            this._element.removeEventListener('touchstart', this._startHandler, false);
            this._element.removeEventListener('touchend', this._endHandler, false);
            this._element.removeEventListener('touchmove', this._moveHandler, false);
            this._element.removeEventListener('touchcancel', this._cancelHandler, false);
        }
        this._element = null;
    }

    _handleTouchStart(e) {
        this.fire('touchstart', new TouchEvent(this, e));
    }

    _handleTouchEnd(e) {
        this.fire('touchend', new TouchEvent(this, e));
    }

    _handleTouchMove(e) {
        // call preventDefault to avoid issues in Chrome Android:
        // http://wilsonpage.co.uk/touch-events-in-chrome-android/
        e.preventDefault();
        this.fire('touchmove', new TouchEvent(this, e));
    }

    _handleTouchCancel(e) {
        this.fire('touchcancel', new TouchEvent(this, e));
    }
}

export { TouchDevice };
<commit_msg>Revert "Make touch events passive like mouse events (#4279)" (#4495)<commit_after>import { EventHandler } from '../core/event-handler.js';

import { TouchEvent } from './touch-event.js';

/**
 * Attach a TouchDevice to an element and it will receive and fire events when the element is
 * touched. See also {@link Touch} and {@link TouchEvent}.
 *
 * @augments EventHandler
 */
class TouchDevice extends EventHandler {
    /**
     * Create a new touch device and attach it to an element.
     *
     * @param {Element} element - The element to attach listen for events on.
     */
    constructor(element) {
        super();

        this._element = null;

        this._startHandler = this._handleTouchStart.bind(this);
        this._endHandler = this._handleTouchEnd.bind(this);
        this._moveHandler = this._handleTouchMove.bind(this);
        this._cancelHandler = this._handleTouchCancel.bind(this);

        this.attach(element);
    }

    /**
     * Attach a device to an element in the DOM. If the device is already attached to an element
     * this method will detach it first.
     *
     * @param {Element} element - The element to attach to.
     */
    attach(element) {
        if (this._element) {
            this.detach();
        }

        this._element = element;

        this._element.addEventListener('touchstart', this._startHandler, false);
        this._element.addEventListener('touchend', this._endHandler, false);
        this._element.addEventListener('touchmove', this._moveHandler, false);
        this._element.addEventListener('touchcancel', this._cancelHandler, false);
    }

    /**
     * Detach a device from the element it is attached to.
     */
    detach() {
        if (this._element) {
            this._element.removeEventListener('touchstart', this._startHandler, false);
            this._element.removeEventListener('touchend', this._endHandler, false);
            this._element.removeEventListener('touchmove', this._moveHandler, false);
            this._element.removeEventListener('touchcancel', this._cancelHandler, false);
        }
        this._element = null;
    }

    _handleTouchStart(e) {
        this.fire('touchstart', new TouchEvent(this, e));
    }

    _handleTouchEnd(e) {
        this.fire('touchend', new TouchEvent(this, e));
    }

    _handleTouchMove(e) {
        // call preventDefault to avoid issues in Chrome Android:
        // http://wilsonpage.co.uk/touch-events-in-chrome-android/
        e.preventDefault();
        this.fire('touchmove', new TouchEvent(this, e));
    }

    _handleTouchCancel(e) {
        this.fire('touchcancel', new TouchEvent(this, e));
    }
}

export { TouchDevice };
<|endoftext|>


<commit_before>import * as d3 from "d3-selection";
import {zoom, zoomTransform, zoomIdentity} from "d3-zoom";
import {interpolate} from "d3-interpolate";

export default function(enable) {

    this._options.zoom = enable;

    if (this._options.zoom && !this._zoomBehavior) {
        createZoomBehavior.call(this);
    }

    return this;
}

export function createZoomBehavior() {

    function zoomed() {
        var g = d3.select(svg.node().querySelector("g"));
        g.attr('transform', d3.event.transform);
    }

    var root = this._selection;
    var svg = d3.select(root.node().querySelector("svg"));
    if (svg.size() == 0) {
        return this;
    }
    this._zoomSelection = svg;
    var zoomBehavior = zoom()
        .scaleExtent(this._options.zoomScaleExtent)
        .translateExtent(this._options.zoomTranslateExtent)
        .interpolate(interpolate)
        .on("zoom", zoomed);
    this._zoomBehavior = zoomBehavior;
    var g = d3.select(svg.node().querySelector("g"));
    svg.call(zoomBehavior);
    if (!this._active) {
        translateZoomBehaviorTransform.call(this, g);
    }
    this._originalTransform = zoomTransform(svg.node());

    return this;
};

export function getTranslatedZoomTransform(selection) {

    // Get the current zoom transform for the top level svg and
    // translate it uniformly with the given selection, using the
    // difference between the translation specified in the selection's
    // data and it's saved previous translation. The selection is
    // normally the top level g element of the graph.
    var oldTranslation = this._translation;
    var newTranslation = selection.datum().translation;
    var t = zoomTransform(this._zoomSelection.node());
    if (oldTranslation) {
        t = t.translate(-oldTranslation.x, -oldTranslation.y);
    }
    t = t.translate(newTranslation.x, newTranslation.y);
    return t;
}

export function translateZoomBehaviorTransform(selection) {

    // Translate the current zoom transform for the top level svg
    // uniformly with the given selection, using the difference
    // between the translation specified in the selection's data and
    // it's saved previous translation. The selection is normally the
    // top level g element of the graph.
    this._zoomBehavior.transform(this._zoomSelection, getTranslatedZoomTransform.call(this, selection));

    // Save the selections's new translation.
    this._translation = selection.datum().translation;

    // Set the original zoom transform to the translation specified in
    // the selection's data.
    this._originalTransform = zoomIdentity.translate(selection.datum().translation.x, selection.datum().translation.y);
}

export function resetZoom(transition) {

    // Reset the zoom transform to the original zoom transform.
    var selection = this._zoomSelection;
    if (transition) {
        selection = selection
            .transition(transition);
    }
    selection
        .call(this._zoomBehavior.transform, this._originalTransform);

    return this;
}

export function zoomScaleExtent(extent) {

    this._options.zoomScaleExtent = extent;

    return this;
}

export function zoomTranslateExtent(extent) {

    console.log('magjac 090:', extent);
    this._options.zoomTranslateExtent = extent;

    return this;
}
<commit_msg>Removed accidentally committed console.log<commit_after>import * as d3 from "d3-selection";
import {zoom, zoomTransform, zoomIdentity} from "d3-zoom";
import {interpolate} from "d3-interpolate";

export default function(enable) {

    this._options.zoom = enable;

    if (this._options.zoom && !this._zoomBehavior) {
        createZoomBehavior.call(this);
    }

    return this;
}

export function createZoomBehavior() {

    function zoomed() {
        var g = d3.select(svg.node().querySelector("g"));
        g.attr('transform', d3.event.transform);
    }

    var root = this._selection;
    var svg = d3.select(root.node().querySelector("svg"));
    if (svg.size() == 0) {
        return this;
    }
    this._zoomSelection = svg;
    var zoomBehavior = zoom()
        .scaleExtent(this._options.zoomScaleExtent)
        .translateExtent(this._options.zoomTranslateExtent)
        .interpolate(interpolate)
        .on("zoom", zoomed);
    this._zoomBehavior = zoomBehavior;
    var g = d3.select(svg.node().querySelector("g"));
    svg.call(zoomBehavior);
    if (!this._active) {
        translateZoomBehaviorTransform.call(this, g);
    }
    this._originalTransform = zoomTransform(svg.node());

    return this;
};

export function getTranslatedZoomTransform(selection) {

    // Get the current zoom transform for the top level svg and
    // translate it uniformly with the given selection, using the
    // difference between the translation specified in the selection's
    // data and it's saved previous translation. The selection is
    // normally the top level g element of the graph.
    var oldTranslation = this._translation;
    var newTranslation = selection.datum().translation;
    var t = zoomTransform(this._zoomSelection.node());
    if (oldTranslation) {
        t = t.translate(-oldTranslation.x, -oldTranslation.y);
    }
    t = t.translate(newTranslation.x, newTranslation.y);
    return t;
}

export function translateZoomBehaviorTransform(selection) {

    // Translate the current zoom transform for the top level svg
    // uniformly with the given selection, using the difference
    // between the translation specified in the selection's data and
    // it's saved previous translation. The selection is normally the
    // top level g element of the graph.
    this._zoomBehavior.transform(this._zoomSelection, getTranslatedZoomTransform.call(this, selection));

    // Save the selections's new translation.
    this._translation = selection.datum().translation;

    // Set the original zoom transform to the translation specified in
    // the selection's data.
    this._originalTransform = zoomIdentity.translate(selection.datum().translation.x, selection.datum().translation.y);
}

export function resetZoom(transition) {

    // Reset the zoom transform to the original zoom transform.
    var selection = this._zoomSelection;
    if (transition) {
        selection = selection
            .transition(transition);
    }
    selection
        .call(this._zoomBehavior.transform, this._originalTransform);

    return this;
}

export function zoomScaleExtent(extent) {

    this._options.zoomScaleExtent = extent;

    return this;
}

export function zoomTranslateExtent(extent) {

    this._options.zoomTranslateExtent = extent;

    return this;
}
<|endoftext|>


<commit_before>(function (root, factory) {
	if (typeof define === 'function' && define.amd) {
		/* AMD. Register as an anonymous module. */
		define(['codemirror', 'codemirror/addon/hint/show-hint'], factory);
	} else {
		/* Browser globals - dangerous */
		root.codeMirrorOmetaHinter = factory(root.CodeMirror);
	}
}(this, function (CodeMirror) {
	var forEach = function (o, f) {
		if($.isArray(o))
			for (var i = 0, e = o.length; i < e; ++i) f(o[i]);
		else
			for (var i in o) {
				if(o.hasOwnProperty(i)) {
					f(i);
				}
			}
	},
	arrayContains = function(arr, item) {
		if (!Array.prototype.indexOf) {
			var i = arr.length;
			while (i--) {
				if (arr[i] === item) {
					return true;
				}
			}
			return false;
		}
		return arr.indexOf(item) != -1;
	};

	return function(prependText) {
		var ometaHint = function(editor) {
			var modeName = editor.getOption('mode').name,
				mode = CodeMirror.getMode(null, modeName),
				grammar = mode.getGrammar(),
				cur = editor.getCursor(false),
				text = editor.getRange({ch:0, line:0}, cur),
				token = editor.getTokenAt(cur),
				tokenString = token.string.substr(0,cur.ch-token.start);
			if(prependText != null) {
				text = prependText() + text;
			}

			var found = [], start = tokenString.toLowerCase(), whitespace = "", whitespaceRegexp = /^[\s]+/;
			if(whitespaceRegexp.test(start)) {
				whitespace = whitespaceRegexp.exec(start)[0];
				start = start.replace(whitespace, '');
			}

			var maybeAdd = function(str) {
				if (str.toLowerCase().indexOf(start) == 0 && !arrayContains(found, str)) found.push(whitespace+str+" ");
			},
			addPossibilities = function(ruleMap, ruleArgs) {
				if($.isArray(ruleMap)) {
					forEach(ruleMap, maybeAdd);
				}
				else if($.isFunction(ruleMap)) {
					addPossibilities(ruleMap.apply(grammar, ruleArgs));
				}
				else {
					for(var prop in ruleMap) {
						if(ruleMap.hasOwnProperty(prop)) {
							// If ruleMap[prop] is an object then we should be looking at a dictionary lookup based on the first argument.
							if(typeof ruleMap[prop] == 'object') {
								// Do the dictionary lookup
								var lookupVal = '' + ruleArgs.shift();
								if(ruleMap.hasOwnProperty(lookupVal)) {
									addPossibilities(ruleMap[lookupVal], maybeAdd);
								}
								else {
									console.warn('Did not find a matching lookup property', lookupVal, ruleArgs, ruleMap);
								}
							}
							else {
								forEach(ruleMap, maybeAdd);
							}
							break;
						}
					}
				}
			};

			try {
				grammar._enableBranchTracking(grammar.branches);
				grammar._enableTokens();
				grammar.matchAll(text, 'Process');
			}
			catch(e) {}

			var branches = grammar._getBranches();
			var hintBranches = grammar.branches;

			var lookingForBranch = true;
			var tokenStart = text.length - (cur.ch - token.start);

			var checkBranch = function(currBranch) {
				if(currBranch != null) {
					for(var rule in currBranch) {
						if(currBranch.hasOwnProperty(rule)) {
							try {
								addPossibilities(hintBranches[rule], currBranch[rule]);
							} catch (e) {
								console.log(e);
							}
						}
					}
					lookingForBranch = false;
				}
			};
			// Iterate forwards from the start of the token, so completions are added in an order that feels right when using.
			for(var i = tokenStart; i < branches.length; i++) {
				checkBranch(branches[i]);
			}
			// If we didn't find any then look backwards for the first set of completions we find.
			for(var i = tokenStart - 1; lookingForBranch && i >= 0; i--) {
				checkBranch(branches[i]);
			}
			return {
				list: found,
				from: {line: cur.line, ch: token.start},
				to: {line: cur.line, ch: token.end}
			};
		};
		return function(instance, e) {
			// Hook into ctrl-space
			if (e.keyCode == 32 && (e.ctrlKey || e.metaKey) && !e.altKey) {
				e.preventDefault();
				return CodeMirror.showHint(instance, ometaHint);
			}
		};
	};
}));<commit_msg>Add support for the hinter being bound via codemirror keymaps.<commit_after>(function (root, factory) {
	if (typeof define === 'function' && define.amd) {
		/* AMD. Register as an anonymous module. */
		define(['codemirror', 'codemirror/addon/hint/show-hint'], factory);
	} else {
		/* Browser globals - dangerous */
		root.codeMirrorOmetaHinter = factory(root.CodeMirror);
	}
}(this, function (CodeMirror) {
	var forEach = function (o, f) {
		if($.isArray(o))
			for (var i = 0, e = o.length; i < e; ++i) f(o[i]);
		else
			for (var i in o) {
				if(o.hasOwnProperty(i)) {
					f(i);
				}
			}
	},
	arrayContains = function(arr, item) {
		if (!Array.prototype.indexOf) {
			var i = arr.length;
			while (i--) {
				if (arr[i] === item) {
					return true;
				}
			}
			return false;
		}
		return arr.indexOf(item) != -1;
	};

	return function(prependText) {
		var ometaHint = function(editor) {
			var modeName = editor.getOption('mode').name,
				mode = CodeMirror.getMode(null, modeName),
				grammar = mode.getGrammar(),
				cur = editor.getCursor(false),
				text = editor.getRange({ch:0, line:0}, cur),
				token = editor.getTokenAt(cur),
				tokenString = token.string.substr(0,cur.ch-token.start);
			if(prependText != null) {
				text = prependText() + text;
			}

			var found = [], start = tokenString.toLowerCase(), whitespace = "", whitespaceRegexp = /^[\s]+/;
			if(whitespaceRegexp.test(start)) {
				whitespace = whitespaceRegexp.exec(start)[0];
				start = start.replace(whitespace, '');
			}

			var maybeAdd = function(str) {
				if (str.toLowerCase().indexOf(start) == 0 && !arrayContains(found, str)) found.push(whitespace+str+" ");
			},
			addPossibilities = function(ruleMap, ruleArgs) {
				if($.isArray(ruleMap)) {
					forEach(ruleMap, maybeAdd);
				}
				else if($.isFunction(ruleMap)) {
					addPossibilities(ruleMap.apply(grammar, ruleArgs));
				}
				else {
					for(var prop in ruleMap) {
						if(ruleMap.hasOwnProperty(prop)) {
							// If ruleMap[prop] is an object then we should be looking at a dictionary lookup based on the first argument.
							if(typeof ruleMap[prop] == 'object') {
								// Do the dictionary lookup
								var lookupVal = '' + ruleArgs.shift();
								if(ruleMap.hasOwnProperty(lookupVal)) {
									addPossibilities(ruleMap[lookupVal], maybeAdd);
								}
								else {
									console.warn('Did not find a matching lookup property', lookupVal, ruleArgs, ruleMap);
								}
							}
							else {
								forEach(ruleMap, maybeAdd);
							}
							break;
						}
					}
				}
			};

			try {
				grammar._enableBranchTracking(grammar.branches);
				grammar._enableTokens();
				grammar.matchAll(text, 'Process');
			}
			catch(e) {}

			var branches = grammar._getBranches();
			var hintBranches = grammar.branches;

			var lookingForBranch = true;
			var tokenStart = text.length - (cur.ch - token.start);

			var checkBranch = function(currBranch) {
				if(currBranch != null) {
					for(var rule in currBranch) {
						if(currBranch.hasOwnProperty(rule)) {
							try {
								addPossibilities(hintBranches[rule], currBranch[rule]);
							} catch (e) {
								console.log(e);
							}
						}
					}
					lookingForBranch = false;
				}
			};
			// Iterate forwards from the start of the token, so completions are added in an order that feels right when using.
			for(var i = tokenStart; i < branches.length; i++) {
				checkBranch(branches[i]);
			}
			// If we didn't find any then look backwards for the first set of completions we find.
			for(var i = tokenStart - 1; lookingForBranch && i >= 0; i--) {
				checkBranch(branches[i]);
			}
			return {
				list: found,
				from: {line: cur.line, ch: token.start},
				to: {line: cur.line, ch: token.end}
			};
		};
		return function(instance, e) {
			if(e != null) {
				// If the event exists then make sure we're using ctrl-space and prevent the event.
				if (e.keyCode == 32 && (e.ctrlKey || e.metaKey) && !e.altKey) {
					e.preventDefault();
				}
				else {
					// Otherwise bail
					return;
				}
			}
			// If there is no event object then we're bound via a keymap, so we can just run the hints.
			return CodeMirror.showHint(instance, ometaHint);
		};
	};
}));<|endoftext|>


<commit_before>
// General
const SET = 'SET';
const REMOVE = 'REMOVE';

// Numeric
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// List
const LIST_APPEND = 'LIST_APPEND';
const LIST_REMOVE = 'LIST_REMOVE'; // TODO: Add Implementation
const MAP = 'MAP'; // TODO: Add Implementation
const REDUCE = 'REDUCE'; // TODO: Add Implementation

module.exports = {
  SET,
  REMOVE,
  INCREMENT,
  DECREMENT,
  LIST_APPEND
};<commit_msg>Changing the LIST_REMOVE to FLITER<commit_after>
// General
const SET = 'SET';
const REMOVE = 'REMOVE';

// Numeric
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// List
const LIST_APPEND = 'LIST_APPEND';
const FILTER = 'FILTER'; // TODO: Add Implementation
const MAP = 'MAP'; // TODO: Add Implementation
const REDUCE = 'REDUCE'; // TODO: Add Implementation

module.exports = {
  SET,
  REMOVE,
  INCREMENT,
  DECREMENT,
  LIST_APPEND
};<|endoftext|>


<commit_before>import React from 'react'
import Event from './event'
import Details from './details'
import { below } from 'react-waypoint'
import './index.scss'
import closestTo from 'date-fns/closest_to'
import isWithinRange from 'date-fns/is_within_range'
import format from 'date-fns/format'

function getWeekday(date) {
  return format(date, 'dddd')
}

function getCorrectDate(days, today = (new Date()).setHours(0, 0, 0, 0)) {
  const dates = days.map(({ date }) => date)
  let correctDay = dates[0]
  if (isWithinRange(today, dates[0], dates[days.length - 1])) {
    correctDay = closestTo(today, dates)
  }
  return getWeekday(correctDay)
}

class Events extends React.Component {
  static localStorageKey = 'jsconf-is-2018-schedule'

  static propTypes = {
    conference: React.PropTypes.arrayOf(React.PropTypes.object),
    so: React.PropTypes.arrayOf(React.PropTypes.object),
    footerPosition: React.PropTypes.string,
  }

  constructor (props) {
    super(props)
    this.state = {
      activeDetails: null,
      type: 'conference',
      activeDate: getCorrectDate(props.conference),
    }
  }

  componentDidMount () {
    window.addEventListener('keyup', this.onKeyUp)
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.footerPosition !== below && nextProps.footerPosition !== this.props.footerPosition) {
      this.setState({ activeDetails: null })
    }
  }

  componentWillUnmount () {
    window.removeEventListener('keyup', this.onKeyUp)
  }

  onKeyUp = ({ keyCode }) => {
    switch (keyCode) {
      case 37:
        this.onPrevious()
        break
      case 39:
        this.onNext()
        break
      default:
        break
    }
  }

  onChangeDay = day => () => {
    this.setState({
      activeDetails: null,
      activeDate: day,
    })
  }

  onOpenTrackDetails = details => () => {
    this.setState({ activeDetails: details })
  }

  onPrevious = () => {
    this.navigate(-1)
  }

  onNext = () => {
    this.navigate(1)
  }

  onSave = () => {
    const { activeDetails: { day, slot, track } } = this.state
    const { keys, days } = this.state
    const updatedDays = [...days]
    const record = updatedDays[day].slots[slot].tracks[track]

    record.saved = ! record.saved
    keys[record.slug] = record.saved

    this.setState({ keys, days: updatedDays })
    window.localStorage.setItem(Events.localStorageKey, JSON.stringify(keys))
  }

  onChangeTracks = () => {
    this.setState((state, props) => {
      const type = state.type === 'conference' ? 'so' : 'conference'
      return { type, activeDate: getCorrectDate(props[type]) }
    })
  }

  calculateNextTrack (direction, indicies) {
    const { activeDetails } = this.state
    const current = indicies || { ...activeDetails }

    const { days } = this.state

    const slots = days[current.day].slots
    const tracks = slots[current.slot].tracks

    current.track += direction

    if (current.track >= tracks.length) {
      current.slot += direction
      current.track = -1

      if (!slots[current.slot]) {
        return activeDetails
      }

      return this.calculateNextTrack(direction, current)
    }

    if (current.track < 0) {
      current.slot += direction

      if (!slots[current.slot]) {
        return activeDetails
      }

      current.track = slots[current.slot].tracks.length
      return this.calculateNextTrack(direction, current)
    }

    if (tracks[current.track].grayed) {
      return this.calculateNextTrack(direction, current)
    }

    return current
  }

  navigate (direction) {
    if (this.state.activeDetails) {
      this.setState({
        activeDetails: this.calculateNextTrack(direction),
      })
    }
  }

  render () {
    const { so, conference } = this.props
    const { type, activeDate, activeDetails } = this.state

    const days = type === 'conference'
      ? conference
      : so

    if (!days) {
      return null
    }

    const hasDetails = !! activeDetails
    let detailsProps = {}

    if (hasDetails) {
      const { day, slot, track } = activeDetails
      detailsProps = days[day].slots[slot].tracks[track]
      detailsProps.time = days[day].slots[slot].time
    }

    return (

        <nav className={`Events-tabs${!hasDetails ? ' is-centered' : ''}`}>

            Schedule: {type === 'conference' ? 'Conference' : 'Significant Other'}

            <button onClick={this.onChangeTracks}>
              View {type === 'conference' ? 'Significant Other' : 'Conference'} tracks
            </button>

          {days.map(({ date }) => {
            const weekday = getWeekday(date)
            const classes = `Events-tab${activeDate === weekday ? ' is-active' : ''}`
            return (
              <button key={date} onClick={this.onChangeDay(weekday)} className={classes}>
                {weekday}
              </button>
            )
          })}

        <div className={`Events-group${!hasDetails ? ' is-centered' : ''}`}>
          {days.map((day, index) =>
            <Event
              key={day.date}
              active={activeDetails}
              isActive={activeDate === getWeekday(day.date)}
              date={format(day.date, 'MMMM Do, YYYY')}
              slots={day.slots}
              index={index}
              onOpenTrackDetails={this.onOpenTrackDetails}
            />
          )}

    )
  }
}

export default Events
<commit_msg>Make schedule linkable<commit_after>import React from 'react'
import Event from './event'
import Details from './details'
import { below } from 'react-waypoint'
import './index.scss'
import closestTo from 'date-fns/closest_to'
import isWithinRange from 'date-fns/is_within_range'
import format from 'date-fns/format'

function getWeekday(date) {
  return format(date, 'dddd').toLowerCase()
}

function isValidDay(type) {
  return ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'].includes(type)
}

function isValidType(type) {
  return ['conference', 'so'].includes(type)
}

function getCorrectDate(days, today = (new Date()).setHours(0, 0, 0, 0)) {
  const dates = days.map(({ date }) => date)
  let correctDay = dates[0]
  if (isWithinRange(today, dates[0], dates[days.length - 1])) {
    correctDay = closestTo(today, dates)
  }
  return getWeekday(correctDay)
}

function parseHash(state, props) {
  const hash = window.location.hash.split('/')
  const type = hash.length > 1 && isValidType(hash[1]) ? hash[1] : state.type

  return {
    type,
    activeDate: hash.length > 2 && isValidDay(hash[2]) ? hash[2] : getCorrectDate(props[type]),
  }
}

function setHash(type, day) {
  window.location.hash = `#!/${type}/${day}`
}

class Events extends React.Component {
  static localStorageKey = 'jsconf-is-2018-schedule'

  static propTypes = {
    conference: React.PropTypes.arrayOf(React.PropTypes.object),
    so: React.PropTypes.arrayOf(React.PropTypes.object),
    footerPosition: React.PropTypes.string,
  }

  constructor (props) {
    super(props)
    this.state = {
      activeDetails: null,
      type: 'conference',
      activeDate: getCorrectDate(props.conference),
    }
  }

  componentDidMount () {
    window.addEventListener('keyup', this.onKeyUp)
    // eslint-disable-next-line react/no-did-mount-set-state
    this.setState(parseHash, () => {
      const { type, activeDate } = this.state
      setHash(type, activeDate)
    })
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.footerPosition !== below && nextProps.footerPosition !== this.props.footerPosition) {
      this.setState({ activeDetails: null })
    }
  }

  componentDidUpdate(prevProps, prevState) {
    const { type, activeDate } = this.state
    if (prevState.activeDate !== activeDate || prevState.type !== type) {
      setHash(type, activeDate)
    }
  }

  componentWillUnmount () {
    window.removeEventListener('keyup', this.onKeyUp)
  }

  onKeyUp = ({ keyCode }) => {
    switch (keyCode) {
      case 37:
        this.onPrevious()
        break
      case 39:
        this.onNext()
        break
      default:
        break
    }
  }

  onChangeDay = day => () => {
    this.setState({
      activeDetails: null,
      activeDate: day,
    })
  }

  onOpenTrackDetails = details => () => {
    this.setState({ activeDetails: details })
  }

  onPrevious = () => {
    this.navigate(-1)
  }

  onNext = () => {
    this.navigate(1)
  }

  onSave = () => {
    const { activeDetails: { day, slot, track } } = this.state
    const { keys, days } = this.state
    const updatedDays = [...days]
    const record = updatedDays[day].slots[slot].tracks[track]

    record.saved = ! record.saved
    keys[record.slug] = record.saved

    this.setState({ keys, days: updatedDays })
    window.localStorage.setItem(Events.localStorageKey, JSON.stringify(keys))
  }

  onChangeTracks = () => {
    this.setState((state, props) => {
      const type = state.type === 'conference' ? 'so' : 'conference'
      return { type, activeDate: getCorrectDate(props[type]) }
    })
  }

  calculateNextTrack (direction, indicies) {
    const { activeDetails } = this.state
    const current = indicies || { ...activeDetails }

    const { days } = this.state

    const slots = days[current.day].slots
    const tracks = slots[current.slot].tracks

    current.track += direction

    if (current.track >= tracks.length) {
      current.slot += direction
      current.track = -1

      if (!slots[current.slot]) {
        return activeDetails
      }

      return this.calculateNextTrack(direction, current)
    }

    if (current.track < 0) {
      current.slot += direction

      if (!slots[current.slot]) {
        return activeDetails
      }

      current.track = slots[current.slot].tracks.length
      return this.calculateNextTrack(direction, current)
    }

    if (tracks[current.track].grayed) {
      return this.calculateNextTrack(direction, current)
    }

    return current
  }

  navigate (direction) {
    if (this.state.activeDetails) {
      this.setState({
        activeDetails: this.calculateNextTrack(direction),
      })
    }
  }

  render () {
    const { so, conference } = this.props
    const { type, activeDate, activeDetails } = this.state

    const days = type === 'conference'
      ? conference
      : so

    if (!days) {
      return null
    }

    const hasDetails = !! activeDetails
    let detailsProps = {}

    if (hasDetails) {
      const { day, slot, track } = activeDetails
      detailsProps = days[day].slots[slot].tracks[track]
      detailsProps.time = days[day].slots[slot].time
    }

    return (

        <nav className={`Events-tabs${!hasDetails ? ' is-centered' : ''}`}>

            Schedule: {type === 'conference' ? 'Conference' : 'Significant Other'}

            <button onClick={this.onChangeTracks}>
              View {type === 'conference' ? 'Significant Other' : 'Conference'} tracks
            </button>

          {days.map(({ date }) => {
            const weekday = getWeekday(date)
            const classes = `Events-tab${activeDate === weekday ? ' is-active' : ''}`
            return (
              <button key={date} onClick={this.onChangeDay(weekday)} className={classes}>
                {weekday}
              </button>
            )
          })}

        <div className={`Events-group${!hasDetails ? ' is-centered' : ''}`}>
          {days.map((day, index) =>
            <Event
              key={day.date}
              active={activeDetails}
              isActive={activeDate === getWeekday(day.date)}
              date={format(day.date, 'MMMM Do, YYYY')}
              slots={day.slots}
              index={index}
              onOpenTrackDetails={this.onOpenTrackDetails}
            />
          )}

    )
  }
}

export default Events
<|endoftext|>


<commit_before>// Copyright 2019, University of Colorado Boulder

/**
 * This singleton is responsible for ensuring that the phet-io api is correct through the lifetime of the simulation.
 * The phet-io api is defined through multiple preloaded files. The "elements baseline" file holds an exact match of
 * what PhetioObject instances/metadata the sim should create on startup, where the "elements overrides" file is a
 * sparse list that can overwrite metadata without changing the code. See `grunt generate-phet-io-elements-files` for
 * more information. The complete list of checks was decided on in https://github.com/phetsims/phet-io/issues/1453 and
 * is as follows:
 *
 * 1.  A full schema is required - any phet-io brand sim without these will have a 404, but this rule isn't tested in this file.
 * 2.  Registered PhetioObject baseline must equal baseline schema to ensure that baseline changes are intentional.
 * 3.  Any registered PhetioObject must be included in the schema.
 * 4.  After startup, only dynamic instances can be registered.
 * 5.  When the sim is finished starting up, all schema entries must be registered.
 * 6.  Any static, registered PhetioObject can never be deregistered.
 * 7.  Any schema entries in the overrides file must exist in the baseline file
 * 8.  Any schema entries in the overrides file must be different from its baseline counterpart
 * TODO: Implement rules 9 and 10, see https://github.com/phetsims/phet-io/issues/1515
 * 9.  Any type described in the types file must exist on startup.
 * 10. For every type in the types file, every method must have the same name and signature.
 *
 * Terminology:
 * schema: specified through preloads. The full schema is the baseline plus the overrides, but those parts can be
 *         referred to separately.
 * registered: the process of instrumenting a PhetioObject and it "becoming" a PhET-iO Element on the wrapper side.
 * static PhetioObject: A registered PhetioObject that exists for the lifetime of the sim. It should not be removed
 *                      (even intermittently) and must be created during startup so that it is immediately interoperable.
 * dynamic PhetioObject: A registered PhetioObject that can be created and/or destroyed at any point. Only dynamic
 *                       PhetioObjects can be created after startup.
 *
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Chris Klusendorf (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const tandemNamespace = require( 'TANDEM/tandemNamespace' );

  class PhetioAPIValidation {

    constructor() {

      /**
       * {Object[]} - Each object holds a single "api mismatch" with the following keys:
       *                phetioID: {string}
       *                stack: {string} - for a stack trace
       *                ruleInViolation: {string} - one of the numbered list in the header doc.
       *                [message]: {string} - specific problem
       *
       * Feel free to add any other JSONifyable keys to this to make the error more clear! All mismatches are printed
       * at once for clarity, see PhetioEngine.
       * @private
       */
      this.apiMismatches = [];

      // @private - keep track of when the sim has started.
      this.simHasStarted = false;

      // @public (read-only) {boolean} - whether or not validation is enabled.
      this.enabled = !!( window.phet && window.phet.phetio && window.phet.phetio.queryParameters.phetioValidateAPI &&
                         window.phet.phetio.phetioElementsOverrides &&
                         window.phet.phetio.phetioElementsBaseline &&
                         !phet.phetio.queryParameters.phetioPrintPhetioFiles );

      this.validateOverridesFile(); // these preloads can be validated immediately
    }

    /**
     * This callback should be called before the overrides have been mixed into the PhetioObject metadata to ensure that
     * the comparison is baseline schema (from the file) to PhetioObject baseline (computed at runtime).
     *
     * @param {Tandem} tandem
     * @param {Object} phetioObjectBaselineMetadata
     * @public
     */
    onPhetioObjectPreOverrides( tandem, phetioObjectBaselineMetadata ) {
      if ( !this.enabled ) {
        return;
      }

      if ( !this.simHasStarted ) {

        const concretePhetioID = tandem.getConcretePhetioID();
        const baseline = window.phet.phetio.phetioElementsBaseline[ concretePhetioID ];

        if ( !baseline ) {
          this.addError( {
            phetioID: tandem.phetioID,
            ruleInViolation: '3. Any registered PhetioObject must be included in the schema',
            message: 'no baseline schema found for phetioID',
            concretePhetioID: concretePhetioID
          } );
          return;
        }

        // if simulation metadata is not equal to baseline before overrides applied
        if ( !_.isEqual( baseline, phetioObjectBaselineMetadata ) ) {
          this.addError( {
            phetioID: concretePhetioID,
            ruleInViolation: '2. Registered PhetioObject baseline must equal baseline schema to ensure that baseline changes are intentional.',
            message: 'baseline schema does not match PhetioObject computed baseline metadata',
            baselineSchema: baseline,
            phetioObjectBaselineMetadata: phetioObjectBaselineMetadata
          } );
        }
      }

      // Instances should generally be created on startup.  The only instances that it's OK to create after startup
      // are "dynamic instances" which have underscores (at the moment). Only assert if validating the phet-io API
      if ( this.simHasStarted && !phetio.PhetioIDUtils.isDynamicElement( tandem.phetioID ) ) {
        this.addError( {
          phetioID: tandem.phetioID,
          ruleInViolation: '4. After startup, only dynamic instances can be registered.'
        } );
      }
    }

    /**
     * Callback when the simulation is ready to go, and all static PhetioObjects have been created.
     * @param {Object.<string,PhetioObject>} phetioObjectMap
     * @param {Object.<string,Object>} phetioTypes
     * @public
     */
    onSimStarted( phetioObjectMap, phetioTypes ) {
      if ( !this.enabled ) {
        return;
      }

      // When the screens query parameter is specified, there will be many things in the baseline file but not in the sim.  Those will
      // not be validated.
      if ( phet.chipper.queryParameters.screens === null ) {

        // check to make sure all phet-io elements and type entries were used.  If an entry wasn't used, throw an
        // assertion error because the sim is missing something it is supposed to have.
        // Don't check for this when generating the API file from the code.
        for ( const phetioID in window.phet.phetio.phetioElementsBaseline ) {
          if ( window.phet.phetio.phetioElementsBaseline.hasOwnProperty( phetioID ) && !phetioObjectMap[ phetioID ] ) {
            this.addError( {
              phetioID: phetioID,
              ruleInViolation: '5. When the sim is finished starting up, all schema entries must be registered.',
              message: 'phetioID expected but does not exist'
            } );
          }
        }
        for ( const phetioType in window.phet.phetio.phetioTypes ) {
          if ( window.phet.phetio.phetioTypes.hasOwnProperty( phetioType ) && !phetioTypes[ phetioType ] ) {
            this.addError( {
              phetioType: phetioType,
              ruleInViolation: '9. Any type described in the types file must exist on startup.',
              message: 'phetioType expected but does not exist'
            } );
          }
        }

        this.assertOutIfErrorsPresent();
      }

      this.simHasStarted = true;
    }

    /**
     * Checks if a removed phetioObject is part of a Group
     * @param {PhetioObject} phetioObject
     * @public
     */
    onPhetioObjectRemoved( phetioObject ) {
      if ( !this.enabled ) {
        return;
      }

      const phetioID = phetioObject.tandem.phetioID;

      // if it isn't dynamic, then it shouldn't be removed during the lifetime of the sim.
      if ( !phetio.PhetioIDUtils.isDynamicElement( phetioObject.tandem.phetioID ) &&

           // TODO: Remove '~' check once TANDEM/Tandem.GroupTandem usages have been replaced, see https://github.com/phetsims/tandem/issues/87
           phetioID.indexOf( '~' ) === -1
      ) {
        this.addError( {
          phetioID: phetioID,
          ruleInViolation: '6. Any static, registered PhetioObject can never be deregistered.'
        } );
      }
    }

    /**
     * @private
     */
    validateOverridesFile() {
      if ( !this.enabled ) {
        return;
      }

      for ( const phetioID in window.phet.phetio.phetioElementsOverrides ) {
        if ( !window.phet.phetio.phetioElementsBaseline.hasOwnProperty( phetioID ) ) {
          this.addError( {
            phetioID: phetioID,
            ruleInViolation: '7. Any schema entries in the overrides file must exist in the baseline file.',
            message: 'phetioID expected in the baseline file but does not exist'
          } );
        }
        else {

          const override = window.phet.phetio.phetioElementsOverrides[ phetioID ];
          const baseline = window.phet.phetio.phetioElementsBaseline[ phetioID ];

          if ( Object.keys( override ).length === 0 ) {
            this.addError( {
              phetioID: phetioID,
              ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
              message: 'no metadata keys found for this override.'
            } );
          }

          for ( const metadataKey in override ) {
            if ( !baseline.hasOwnProperty( metadataKey ) ) {
              this.addError( {
                phetioID: phetioID,
                ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
                message: `phetioID metadata key not found in the baseline: ${metadataKey}`
              } );
            }
            if ( override[ metadataKey ] === baseline[ metadataKey ] ) {
              this.addError( {
                phetioID: phetioID,
                ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
                message: 'phetioID metadata override value is the same as the corresponding metadata value in the baseline.'
              } );
            }
          }
        }
      }

      this.assertOutIfErrorsPresent();
    }

    /**
     * If there are errors, then assert out and log them.
     * @private
     */
    assertOutIfErrorsPresent() {

      // if there are any api mismatches
      if ( assert && this.apiMismatches.length > 0 ) {
        console.log( 'mismatches:', this.apiMismatches );
        assert( false, 'api mismatches present:\n' + this.apiMismatches.map(
          mismatchData => `\n${mismatchData.phetioID}:  ${mismatchData.ruleInViolation}`
        ) );
      }
    }

    /**
     * Add an api error to the list to be flushed on sim completion, or immediately if the sim has already started.
     * @param {Object} apiErrorObject - see doc for this.apiMismatches
     * @private
     */
    addError( apiErrorObject ) {
      apiErrorObject.stack = new Error().stack;

      this.apiMismatches.push( apiErrorObject );

      // if the sim has already started, then immediately error out
      if ( this.simHasStarted ) {
        this.assertOutIfErrorsPresent();
      }
    }
  }

  return tandemNamespace.register( 'phetioAPIValidation', new PhetioAPIValidation() );
} );<commit_msg>Update TODO, see https://github.com/phetsims/phet-io/issues/1515<commit_after>// Copyright 2019, University of Colorado Boulder

/**
 * This singleton is responsible for ensuring that the phet-io api is correct through the lifetime of the simulation.
 * The phet-io api is defined through multiple preloaded files. The "elements baseline" file holds an exact match of
 * what PhetioObject instances/metadata the sim should create on startup, where the "elements overrides" file is a
 * sparse list that can overwrite metadata without changing the code. See `grunt generate-phet-io-elements-files` for
 * more information. The complete list of checks was decided on in https://github.com/phetsims/phet-io/issues/1453 and
 * is as follows:
 *
 * 1.  A full schema is required - any phet-io brand sim without these will have a 404, but this rule isn't tested in this file.
 * 2.  Registered PhetioObject baseline must equal baseline schema to ensure that baseline changes are intentional.
 * 3.  Any registered PhetioObject must be included in the schema.
 * 4.  After startup, only dynamic instances can be registered.
 * 5.  When the sim is finished starting up, all schema entries must be registered.
 * 6.  Any static, registered PhetioObject can never be deregistered.
 * 7.  Any schema entries in the overrides file must exist in the baseline file
 * 8.  Any schema entries in the overrides file must be different from its baseline counterpart
 * TODO: Implement rule 10, see https://github.com/phetsims/phet-io/issues/1515
 * 9.  Any type described in the types file must exist on startup.
 * 10. For every type in the types file, every method must have the same name and signature.
 *
 * Terminology:
 * schema: specified through preloads. The full schema is the baseline plus the overrides, but those parts can be
 *         referred to separately.
 * registered: the process of instrumenting a PhetioObject and it "becoming" a PhET-iO Element on the wrapper side.
 * static PhetioObject: A registered PhetioObject that exists for the lifetime of the sim. It should not be removed
 *                      (even intermittently) and must be created during startup so that it is immediately interoperable.
 * dynamic PhetioObject: A registered PhetioObject that can be created and/or destroyed at any point. Only dynamic
 *                       PhetioObjects can be created after startup.
 *
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Chris Klusendorf (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const tandemNamespace = require( 'TANDEM/tandemNamespace' );

  class PhetioAPIValidation {

    constructor() {

      /**
       * {Object[]} - Each object holds a single "api mismatch" with the following keys:
       *                phetioID: {string}
       *                stack: {string} - for a stack trace
       *                ruleInViolation: {string} - one of the numbered list in the header doc.
       *                [message]: {string} - specific problem
       *
       * Feel free to add any other JSONifyable keys to this to make the error more clear! All mismatches are printed
       * at once for clarity, see PhetioEngine.
       * @private
       */
      this.apiMismatches = [];

      // @private - keep track of when the sim has started.
      this.simHasStarted = false;

      // @public (read-only) {boolean} - whether or not validation is enabled.
      this.enabled = !!( window.phet && window.phet.phetio && window.phet.phetio.queryParameters.phetioValidateAPI &&
                         window.phet.phetio.phetioElementsOverrides &&
                         window.phet.phetio.phetioElementsBaseline &&
                         !phet.phetio.queryParameters.phetioPrintPhetioFiles );

      this.validateOverridesFile(); // these preloads can be validated immediately
    }

    /**
     * This callback should be called before the overrides have been mixed into the PhetioObject metadata to ensure that
     * the comparison is baseline schema (from the file) to PhetioObject baseline (computed at runtime).
     *
     * @param {Tandem} tandem
     * @param {Object} phetioObjectBaselineMetadata
     * @public
     */
    onPhetioObjectPreOverrides( tandem, phetioObjectBaselineMetadata ) {
      if ( !this.enabled ) {
        return;
      }

      if ( !this.simHasStarted ) {

        const concretePhetioID = tandem.getConcretePhetioID();
        const baseline = window.phet.phetio.phetioElementsBaseline[ concretePhetioID ];

        if ( !baseline ) {
          this.addError( {
            phetioID: tandem.phetioID,
            ruleInViolation: '3. Any registered PhetioObject must be included in the schema',
            message: 'no baseline schema found for phetioID',
            concretePhetioID: concretePhetioID
          } );
          return;
        }

        // if simulation metadata is not equal to baseline before overrides applied
        if ( !_.isEqual( baseline, phetioObjectBaselineMetadata ) ) {
          this.addError( {
            phetioID: concretePhetioID,
            ruleInViolation: '2. Registered PhetioObject baseline must equal baseline schema to ensure that baseline changes are intentional.',
            message: 'baseline schema does not match PhetioObject computed baseline metadata',
            baselineSchema: baseline,
            phetioObjectBaselineMetadata: phetioObjectBaselineMetadata
          } );
        }
      }

      // Instances should generally be created on startup.  The only instances that it's OK to create after startup
      // are "dynamic instances" which have underscores (at the moment). Only assert if validating the phet-io API
      if ( this.simHasStarted && !phetio.PhetioIDUtils.isDynamicElement( tandem.phetioID ) ) {
        this.addError( {
          phetioID: tandem.phetioID,
          ruleInViolation: '4. After startup, only dynamic instances can be registered.'
        } );
      }
    }

    /**
     * Callback when the simulation is ready to go, and all static PhetioObjects have been created.
     * @param {Object.<string,PhetioObject>} phetioObjectMap
     * @param {Object.<string,Object>} phetioTypes
     * @public
     */
    onSimStarted( phetioObjectMap, phetioTypes ) {
      if ( !this.enabled ) {
        return;
      }

      // When the screens query parameter is specified, there will be many things in the baseline file but not in the sim.  Those will
      // not be validated.
      if ( phet.chipper.queryParameters.screens === null ) {

        // check to make sure all phet-io elements and type entries were used.  If an entry wasn't used, throw an
        // assertion error because the sim is missing something it is supposed to have.
        // Don't check for this when generating the API file from the code.
        for ( const phetioID in window.phet.phetio.phetioElementsBaseline ) {
          if ( window.phet.phetio.phetioElementsBaseline.hasOwnProperty( phetioID ) && !phetioObjectMap[ phetioID ] ) {
            this.addError( {
              phetioID: phetioID,
              ruleInViolation: '5. When the sim is finished starting up, all schema entries must be registered.',
              message: 'phetioID expected but does not exist'
            } );
          }
        }
        for ( const phetioType in window.phet.phetio.phetioTypes ) {
          if ( window.phet.phetio.phetioTypes.hasOwnProperty( phetioType ) && !phetioTypes[ phetioType ] ) {
            this.addError( {
              phetioType: phetioType,
              ruleInViolation: '9. Any type described in the types file must exist on startup.',
              message: 'phetioType expected but does not exist'
            } );
          }
        }

        this.assertOutIfErrorsPresent();
      }

      this.simHasStarted = true;
    }

    /**
     * Checks if a removed phetioObject is part of a Group
     * @param {PhetioObject} phetioObject
     * @public
     */
    onPhetioObjectRemoved( phetioObject ) {
      if ( !this.enabled ) {
        return;
      }

      const phetioID = phetioObject.tandem.phetioID;

      // if it isn't dynamic, then it shouldn't be removed during the lifetime of the sim.
      if ( !phetio.PhetioIDUtils.isDynamicElement( phetioObject.tandem.phetioID ) &&

           // TODO: Remove '~' check once TANDEM/Tandem.GroupTandem usages have been replaced, see https://github.com/phetsims/tandem/issues/87
           phetioID.indexOf( '~' ) === -1
      ) {
        this.addError( {
          phetioID: phetioID,
          ruleInViolation: '6. Any static, registered PhetioObject can never be deregistered.'
        } );
      }
    }

    /**
     * @private
     */
    validateOverridesFile() {
      if ( !this.enabled ) {
        return;
      }

      for ( const phetioID in window.phet.phetio.phetioElementsOverrides ) {
        if ( !window.phet.phetio.phetioElementsBaseline.hasOwnProperty( phetioID ) ) {
          this.addError( {
            phetioID: phetioID,
            ruleInViolation: '7. Any schema entries in the overrides file must exist in the baseline file.',
            message: 'phetioID expected in the baseline file but does not exist'
          } );
        }
        else {

          const override = window.phet.phetio.phetioElementsOverrides[ phetioID ];
          const baseline = window.phet.phetio.phetioElementsBaseline[ phetioID ];

          if ( Object.keys( override ).length === 0 ) {
            this.addError( {
              phetioID: phetioID,
              ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
              message: 'no metadata keys found for this override.'
            } );
          }

          for ( const metadataKey in override ) {
            if ( !baseline.hasOwnProperty( metadataKey ) ) {
              this.addError( {
                phetioID: phetioID,
                ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
                message: `phetioID metadata key not found in the baseline: ${metadataKey}`
              } );
            }
            if ( override[ metadataKey ] === baseline[ metadataKey ] ) {
              this.addError( {
                phetioID: phetioID,
                ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
                message: 'phetioID metadata override value is the same as the corresponding metadata value in the baseline.'
              } );
            }
          }
        }
      }

      this.assertOutIfErrorsPresent();
    }

    /**
     * If there are errors, then assert out and log them.
     * @private
     */
    assertOutIfErrorsPresent() {

      // if there are any api mismatches
      if ( assert && this.apiMismatches.length > 0 ) {
        console.log( 'mismatches:', this.apiMismatches );
        assert( false, 'api mismatches present:\n' + this.apiMismatches.map(
          mismatchData => `\n${mismatchData.phetioID}:  ${mismatchData.ruleInViolation}`
        ) );
      }
    }

    /**
     * Add an api error to the list to be flushed on sim completion, or immediately if the sim has already started.
     * @param {Object} apiErrorObject - see doc for this.apiMismatches
     * @private
     */
    addError( apiErrorObject ) {
      apiErrorObject.stack = new Error().stack;

      this.apiMismatches.push( apiErrorObject );

      // if the sim has already started, then immediately error out
      if ( this.simHasStarted ) {
        this.assertOutIfErrorsPresent();
      }
    }
  }

  return tandemNamespace.register( 'phetioAPIValidation', new PhetioAPIValidation() );
} );<|endoftext|>


<commit_before>// Copyright 2019-2020, University of Colorado Boulder

/**
 * This singleton is responsible for ensuring that the phet-io api is correct through the lifetime of the simulation.
 * The phet-io api is defined through multiple preloaded files. The "elements baseline" file holds an exact match of
 * what PhetioObject instances/metadata the sim should create on startup, where the "elements overrides" file is a
 * sparse list that can overwrite metadata without changing the code. See `grunt generate-phet-io-elements-files` for
 * more information. The complete list of checks was decided on in https://github.com/phetsims/phet-io/issues/1453 and
 * is as follows:
 *
 * 1. A full schema is required - any phet-io brand sim without these will have a 404, but this rule isn't tested in this file.
 * 2. Registered PhetioObject baseline must equal baseline schema to ensure that baseline changes are intentional.
 * 3. ~~ is no more
 * 4. After startup, only dynamic instances prescribed by the baseline file can be registered.
 * 5. When the sim is finished starting up, all non-dynamic schema entries must be registered.
 * 6. Any static, registered PhetioObject can never be deregistered.
 * 7. Any schema entries in the overrides file must exist in the baseline file
 * 8. Any schema entries in the overrides file must be different from its baseline counterpart
 * 9. Types in the sim must exactly match types in the types file to ensure that type changes are intentional.
 *
 * Terminology:
 * schema: specified through preloads. The full schema is the baseline plus the overrides, but those parts can be
 *         referred to separately.
 * registered: the process of instrumenting a PhetioObject and it "becoming" a PhET-iO Element on the wrapper side.
 * static PhetioObject: A registered PhetioObject that exists for the lifetime of the sim. It should not be removed
 *                      (even intermittently) and must be created during startup so that it is immediately interoperable.
 * dynamic PhetioObject: A registered PhetioObject that can be created and/or destroyed at any point. Only dynamic
 *                       PhetioObjects can be created after startup.
 *
 * See https://github.com/phetsims/phet-io/issues/1443#issuecomment-484306552 for an explanation of how to maintain the
 * PhET-iO API for a simulation.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Chris Klusendorf (PhET Interactive Simulations)
 */

// import phetioEngine from '../../phet-io/js/phetioEngine.js';
import Tandem from './Tandem.js';
import tandemNamespace from './tandemNamespace.js';

class PhetioAPIValidation {

  constructor() {

    /**
     * {Object[]} - Each object holds a single "api mismatch" with the following keys:
     *                phetioID: {string}
     *                stack: {string} - for a stack trace
     *                ruleInViolation: {string} - one of the numbered list in the header doc.
     *                [message]: {string} - specific problem
     *
     * Feel free to add any other JSONifyable keys to this to make the error more clear! All mismatches are printed
     * at once for clarity, see PhetioEngine.
     * @private
     */
    this.apiMismatches = [];

    // @private - keep track of when the sim has started.
    this.simHasStarted = false;

    // @public (read-only) {boolean} - whether or not validation is enabled.
    this.enabled = !!( assert && window.phet && Tandem.PHET_IO_ENABLED &&
                       window.phet.preloads.phetio.queryParameters.phetioValidateAPI &&
                       window.phet.preloads.phetio.phetioElementsOverrides &&
                       window.phet.preloads.phetio.phetioElementsBaseline &&
                       window.phet.preloads.phetio.phetioTypes &&
                       !phet.preloads.phetio.queryParameters.phetioPrintAPI );

    // @public (read-only) {boolean} - whether or not validation is enabled. We check the overrides more eagerly to make
    // sure they don't become stale.
    this.isValidateOverrides = assert && Tandem.PHET_IO_ENABLED;

    // @private {Object.<typeName:string, function(new:ObjectIO)>} - this must be all phet-io types so that the
    // following would fail:  add a phetioType, then remove it, then add a different one under the same typeName.
    // A Note about memory: Every TypeIO that is loaded through requirejs is already loaded on the namespace. Therefore
    // this map doesn't add any memory by storing these. The exception to this is parametric TypeIOs. It should be
    // double checked that anything being passed into a parametric type is memory safe. As of this writing, only TypeIOs
    // are passed to parametric TypeIOs, so this pattern remains memory leak free. Furthermore, this list is only
    // populated when `this.enabled`.
    this.everyPhetioType = {};
  }

  /**
   * Callback when the simulation is ready to go, and all static PhetioObjects have been created.
   * @param {PhetioEngine} phetioEngine
   * @public
   */
  onSimStarted( phetioEngine ) {

    this.isValidateOverrides && this.validateOverridesFile();

    if ( !this.enabled ) {
      return;
    }

    const phetioElementsBaseline = phetioEngine.getPhetioElementsBaseline();
    const phetioTypes = phetioEngine.getPhetioTypes();

    // When screen-related query parameters are specified, there will be many things in the baseline file but not
    // in the sim. Those will not be validated.
    if ( phet.chipper.queryParameters.screens === null && phet.chipper.queryParameters.homeScreen ) {

      // check to make sure all phet-io elements and type entries were used.  If an entry wasn't used, throw an
      // assertion error because the sim is missing something it is supposed to have.
      // Don't check for this when generating the API file from the code.
      for ( const phetioID in window.phet.preloads.phetio.phetioElementsBaseline ) {
        if (
          window.phet.preloads.phetio.phetioElementsBaseline.hasOwnProperty( phetioID ) &&
          !phetioEngine.phetioObjectMap[ phetioID ]
          && !window.phet.preloads.phetio.phetioElementsBaseline[ phetioID ].phetioDynamicElement
        ) {
          this.assertAPIError( {
            phetioID: phetioID,
            ruleInViolation: '5. When the sim is finished starting up, all non-dynamic schema entries must be registered.',
            message: 'phetioID expected but does not exist'
          } );
        }
      }

      if ( !_.isEqual( phetioElementsBaseline, window.phet.preloads.phetio.phetioElementsBaseline ) ) {
        this.assertAPIError( {
          // Note: this breaks rule 2 which may in some cases be rule 3
          ruleInViolation: '2. Registered PhetioObject baseline must equal baseline schema to ensure that baseline changes are intentional.',
          message: 'baseline schema does not match PhetioObject computed baseline metadata',
          phetioElementsBaseline: phetioElementsBaseline,
          stringifiedBaseline: JSON.stringify( window.phet.preloads.phetCore.copyWithSortedKeys( phetioElementsBaseline ), null, 2 ),
          phetioElementsBaselineFromFile: window.phet.preloads.phetio.phetioElementsBaseline
        } );
      }

      if ( !_.isEqual( phetioTypes, window.phet.preloads.phetio.phetioTypes ) ) {
        const phetioTypesKeys = Object.keys( phetioTypes );
        const windowPhetioTypesKeys = Object.keys( window.phet.preloads.phetio.phetioTypes );

        this.assertAPIError( {
          ruleInViolation: '9. Types in the sim must exactly match types in the types file to ensure that type changes are intentional.',
          message: 'phetioTypes are not equivalent',
          areKeysEquivalent: _.isEqual( phetioTypesKeys.sort(), windowPhetioTypesKeys.sort() ),
          typesNotInSim: windowPhetioTypesKeys.filter( x => !phetioTypesKeys.includes( x ) ),
          typesNotInFile: phetioTypesKeys.filter( x => !windowPhetioTypesKeys.includes( x ) )
        } );
      }
    }

    this.simHasStarted = true;
  }

  /**
   * Checks if a removed phetioObject is part of a Group
   * @param {PhetioObject} phetioObject
   * @public
   */
  onPhetioObjectRemoved( phetioObject ) {
    if ( !this.enabled ) {
      return;
    }

    const phetioID = phetioObject.tandem.phetioID;

    // if it isn't dynamic, then it shouldn't be removed during the lifetime of the sim.
    if ( !phetioObject.phetioDynamicElement &&

         // TODO: Remove '~' check once TANDEM/Tandem.GroupTandem usages have been replaced, see https://github.com/phetsims/tandem/issues/87
         phetioID.indexOf( '~' ) === -1
    ) {
      this.assertAPIError( {
        phetioID: phetioID,
        ruleInViolation: '6. Any static, registered PhetioObject can never be deregistered.'
      } );
    }
  }

  /**
   * Should be called from phetioEngine when a PhetioObject is added to the PhET-iO
   * @param {PhetioObject} phetioObject
   * @public
   */
  onPhetioObjectAdded( phetioObject ) {
    if ( !this.enabled ) {
      return;
    }

    const newPhetioType = phetioObject.phetioType;
    const oldPhetioType = this.everyPhetioType[ newPhetioType.typeName ];

    if ( !oldPhetioType ) { // This may not be necessary, but may be helpful so that we don't overwrite if rule 10 is in violation
      this.everyPhetioType[ newPhetioType.typeName ] = newPhetioType;
    }

    if ( this.simHasStarted &&

         // TODO: Remove '~' check once TANDEM/Tandem.GroupTandem usages have been replaced, see https://github.com/phetsims/tandem/issues/87
         phetioObject.tandem.phetioID.indexOf( '~' ) === -1 ) {

      const concretePhetioID = phetioObject.tandem.getConcretePhetioID();
      const baselineFromFile = window.phet.preloads.phetio.phetioElementsBaseline[ concretePhetioID ];

      if ( !baselineFromFile ) {
        this.assertAPIError( {
          phetioID: phetioObject.tandem.phetioID,
          ruleInViolation: '4. After startup, only dynamic instances prescribed by the baseline file can be registered.',
          message: 'element\'s concrete phetioID was not in the baseline file after being created after startup.'
        } );
      }

      // Here we need to kick this validation to the next frame to support construction in any order. Parent first, or
      // child first. Use namespace to avoid because timer is a PhetioObject.
      phet.axon.timer.setTimeout( () => {

        // Everything in the baseline was created on startup, but the archetypes mark dynamic elements' non-dynamic
        // counterparts. The only instances that it's OK to create after startup are "dynamic instances" which are
        // marked as such.
        if ( !( baselineFromFile.phetioIsArchetype && phetioObject.phetioDynamicElement ) ) {
          this.assertAPIError( {
            phetioID: phetioObject.tandem.phetioID,
            ruleInViolation: '4. After startup, only dynamic instances prescribed by the baseline file can be registered.'
          } );
        }
      }, 0 );
    }
  }

  /**
   * @private
   */
  validateOverridesFile() {

    // import phetioEngine causes a cycle and cannot be used, hence we must use the namespace
    const baseline = phet.phetio.phetioEngine.getPhetioElementsMetadata();

    for ( const phetioID in window.phet.preloads.phetio.phetioElementsOverrides ) {
      if ( !baseline.hasOwnProperty( phetioID ) ) {
        this.assertAPIError( {
          phetioID: phetioID,
          ruleInViolation: '7. Any schema entries in the overrides file must exist in the baseline file.',
          message: 'phetioID expected in the baseline file but does not exist'
        } );
      }
      else {

        const override = window.phet.preloads.phetio.phetioElementsOverrides[ phetioID ];
        const baseline = baseline[ phetioID ];

        if ( Object.keys( override ).length === 0 ) {
          this.assertAPIError( {
            phetioID: phetioID,
            ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
            message: 'no metadata keys found for this override.'
          } );
        }

        for ( const metadataKey in override ) {
          if ( !baseline.hasOwnProperty( metadataKey ) ) {
            this.assertAPIError( {
              phetioID: phetioID,
              ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
              message: `phetioID metadata key not found in the baseline: ${metadataKey}`
            } );
          }
        }
      }
    }
  }

  /**
   * Assert out the failed api validation rule.
   * @param {Object} apiErrorObject - see doc for this.apiMismatches
   * @private
   */
  assertAPIError( apiErrorObject ) {

    const mismatchMessage = apiErrorObject.phetioID ? `${apiErrorObject.phetioID}:  ${apiErrorObject.ruleInViolation}` :
                            `${apiErrorObject.ruleInViolation}`;

    console.log( 'error data:', apiErrorObject );
    assert && assert( false, 'PhET-iO API error:\n' + mismatchMessage );
  }
}

export default tandemNamespace.register( 'phetioAPIValidation', new PhetioAPIValidation() );<commit_msg>Fix baseline variable name, see https://github.com/phetsims/phet-io/issues/1648<commit_after>// Copyright 2019-2020, University of Colorado Boulder

/**
 * This singleton is responsible for ensuring that the phet-io api is correct through the lifetime of the simulation.
 * The phet-io api is defined through multiple preloaded files. The "elements baseline" file holds an exact match of
 * what PhetioObject instances/metadata the sim should create on startup, where the "elements overrides" file is a
 * sparse list that can overwrite metadata without changing the code. See `grunt generate-phet-io-elements-files` for
 * more information. The complete list of checks was decided on in https://github.com/phetsims/phet-io/issues/1453 and
 * is as follows:
 *
 * 1. A full schema is required - any phet-io brand sim without these will have a 404, but this rule isn't tested in this file.
 * 2. Registered PhetioObject baseline must equal baseline schema to ensure that baseline changes are intentional.
 * 3. ~~ is no more
 * 4. After startup, only dynamic instances prescribed by the baseline file can be registered.
 * 5. When the sim is finished starting up, all non-dynamic schema entries must be registered.
 * 6. Any static, registered PhetioObject can never be deregistered.
 * 7. Any schema entries in the overrides file must exist in the baseline file
 * 8. Any schema entries in the overrides file must be different from its baseline counterpart
 * 9. Types in the sim must exactly match types in the types file to ensure that type changes are intentional.
 *
 * Terminology:
 * schema: specified through preloads. The full schema is the baseline plus the overrides, but those parts can be
 *         referred to separately.
 * registered: the process of instrumenting a PhetioObject and it "becoming" a PhET-iO Element on the wrapper side.
 * static PhetioObject: A registered PhetioObject that exists for the lifetime of the sim. It should not be removed
 *                      (even intermittently) and must be created during startup so that it is immediately interoperable.
 * dynamic PhetioObject: A registered PhetioObject that can be created and/or destroyed at any point. Only dynamic
 *                       PhetioObjects can be created after startup.
 *
 * See https://github.com/phetsims/phet-io/issues/1443#issuecomment-484306552 for an explanation of how to maintain the
 * PhET-iO API for a simulation.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Chris Klusendorf (PhET Interactive Simulations)
 */

// import phetioEngine from '../../phet-io/js/phetioEngine.js';
import Tandem from './Tandem.js';
import tandemNamespace from './tandemNamespace.js';

class PhetioAPIValidation {

  constructor() {

    /**
     * {Object[]} - Each object holds a single "api mismatch" with the following keys:
     *                phetioID: {string}
     *                stack: {string} - for a stack trace
     *                ruleInViolation: {string} - one of the numbered list in the header doc.
     *                [message]: {string} - specific problem
     *
     * Feel free to add any other JSONifyable keys to this to make the error more clear! All mismatches are printed
     * at once for clarity, see PhetioEngine.
     * @private
     */
    this.apiMismatches = [];

    // @private - keep track of when the sim has started.
    this.simHasStarted = false;

    // @public (read-only) {boolean} - whether or not validation is enabled.
    this.enabled = !!( assert && window.phet && Tandem.PHET_IO_ENABLED &&
                       window.phet.preloads.phetio.queryParameters.phetioValidateAPI &&
                       window.phet.preloads.phetio.phetioElementsOverrides &&
                       window.phet.preloads.phetio.phetioElementsBaseline &&
                       window.phet.preloads.phetio.phetioTypes &&
                       !phet.preloads.phetio.queryParameters.phetioPrintAPI );

    // @public (read-only) {boolean} - whether or not validation is enabled. We check the overrides more eagerly to make
    // sure they don't become stale.
    this.isValidateOverrides = assert && Tandem.PHET_IO_ENABLED;

    // @private {Object.<typeName:string, function(new:ObjectIO)>} - this must be all phet-io types so that the
    // following would fail:  add a phetioType, then remove it, then add a different one under the same typeName.
    // A Note about memory: Every TypeIO that is loaded through requirejs is already loaded on the namespace. Therefore
    // this map doesn't add any memory by storing these. The exception to this is parametric TypeIOs. It should be
    // double checked that anything being passed into a parametric type is memory safe. As of this writing, only TypeIOs
    // are passed to parametric TypeIOs, so this pattern remains memory leak free. Furthermore, this list is only
    // populated when `this.enabled`.
    this.everyPhetioType = {};
  }

  /**
   * Callback when the simulation is ready to go, and all static PhetioObjects have been created.
   * @param {PhetioEngine} phetioEngine
   * @public
   */
  onSimStarted( phetioEngine ) {

    this.isValidateOverrides && this.validateOverridesFile();

    if ( !this.enabled ) {
      return;
    }

    const phetioElementsBaseline = phetioEngine.getPhetioElementsBaseline();
    const phetioTypes = phetioEngine.getPhetioTypes();

    // When screen-related query parameters are specified, there will be many things in the baseline file but not
    // in the sim. Those will not be validated.
    if ( phet.chipper.queryParameters.screens === null && phet.chipper.queryParameters.homeScreen ) {

      // check to make sure all phet-io elements and type entries were used.  If an entry wasn't used, throw an
      // assertion error because the sim is missing something it is supposed to have.
      // Don't check for this when generating the API file from the code.
      for ( const phetioID in window.phet.preloads.phetio.phetioElementsBaseline ) {
        if (
          window.phet.preloads.phetio.phetioElementsBaseline.hasOwnProperty( phetioID ) &&
          !phetioEngine.phetioObjectMap[ phetioID ]
          && !window.phet.preloads.phetio.phetioElementsBaseline[ phetioID ].phetioDynamicElement
        ) {
          this.assertAPIError( {
            phetioID: phetioID,
            ruleInViolation: '5. When the sim is finished starting up, all non-dynamic schema entries must be registered.',
            message: 'phetioID expected but does not exist'
          } );
        }
      }

      if ( !_.isEqual( phetioElementsBaseline, window.phet.preloads.phetio.phetioElementsBaseline ) ) {
        this.assertAPIError( {
          // Note: this breaks rule 2 which may in some cases be rule 3
          ruleInViolation: '2. Registered PhetioObject baseline must equal baseline schema to ensure that baseline changes are intentional.',
          message: 'baseline schema does not match PhetioObject computed baseline metadata',
          phetioElementsBaseline: phetioElementsBaseline,
          stringifiedBaseline: JSON.stringify( window.phet.preloads.phetCore.copyWithSortedKeys( phetioElementsBaseline ), null, 2 ),
          phetioElementsBaselineFromFile: window.phet.preloads.phetio.phetioElementsBaseline
        } );
      }

      if ( !_.isEqual( phetioTypes, window.phet.preloads.phetio.phetioTypes ) ) {
        const phetioTypesKeys = Object.keys( phetioTypes );
        const windowPhetioTypesKeys = Object.keys( window.phet.preloads.phetio.phetioTypes );

        this.assertAPIError( {
          ruleInViolation: '9. Types in the sim must exactly match types in the types file to ensure that type changes are intentional.',
          message: 'phetioTypes are not equivalent',
          areKeysEquivalent: _.isEqual( phetioTypesKeys.sort(), windowPhetioTypesKeys.sort() ),
          typesNotInSim: windowPhetioTypesKeys.filter( x => !phetioTypesKeys.includes( x ) ),
          typesNotInFile: phetioTypesKeys.filter( x => !windowPhetioTypesKeys.includes( x ) )
        } );
      }
    }

    this.simHasStarted = true;
  }

  /**
   * Checks if a removed phetioObject is part of a Group
   * @param {PhetioObject} phetioObject
   * @public
   */
  onPhetioObjectRemoved( phetioObject ) {
    if ( !this.enabled ) {
      return;
    }

    const phetioID = phetioObject.tandem.phetioID;

    // if it isn't dynamic, then it shouldn't be removed during the lifetime of the sim.
    if ( !phetioObject.phetioDynamicElement &&

         // TODO: Remove '~' check once TANDEM/Tandem.GroupTandem usages have been replaced, see https://github.com/phetsims/tandem/issues/87
         phetioID.indexOf( '~' ) === -1
    ) {
      this.assertAPIError( {
        phetioID: phetioID,
        ruleInViolation: '6. Any static, registered PhetioObject can never be deregistered.'
      } );
    }
  }

  /**
   * Should be called from phetioEngine when a PhetioObject is added to the PhET-iO
   * @param {PhetioObject} phetioObject
   * @public
   */
  onPhetioObjectAdded( phetioObject ) {
    if ( !this.enabled ) {
      return;
    }

    const newPhetioType = phetioObject.phetioType;
    const oldPhetioType = this.everyPhetioType[ newPhetioType.typeName ];

    if ( !oldPhetioType ) { // This may not be necessary, but may be helpful so that we don't overwrite if rule 10 is in violation
      this.everyPhetioType[ newPhetioType.typeName ] = newPhetioType;
    }

    if ( this.simHasStarted &&

         // TODO: Remove '~' check once TANDEM/Tandem.GroupTandem usages have been replaced, see https://github.com/phetsims/tandem/issues/87
         phetioObject.tandem.phetioID.indexOf( '~' ) === -1 ) {

      const concretePhetioID = phetioObject.tandem.getConcretePhetioID();
      const baselineFromFile = window.phet.preloads.phetio.phetioElementsBaseline[ concretePhetioID ];

      if ( !baselineFromFile ) {
        this.assertAPIError( {
          phetioID: phetioObject.tandem.phetioID,
          ruleInViolation: '4. After startup, only dynamic instances prescribed by the baseline file can be registered.',
          message: 'element\'s concrete phetioID was not in the baseline file after being created after startup.'
        } );
      }

      // Here we need to kick this validation to the next frame to support construction in any order. Parent first, or
      // child first. Use namespace to avoid because timer is a PhetioObject.
      phet.axon.timer.setTimeout( () => {

        // Everything in the baseline was created on startup, but the archetypes mark dynamic elements' non-dynamic
        // counterparts. The only instances that it's OK to create after startup are "dynamic instances" which are
        // marked as such.
        if ( !( baselineFromFile.phetioIsArchetype && phetioObject.phetioDynamicElement ) ) {
          this.assertAPIError( {
            phetioID: phetioObject.tandem.phetioID,
            ruleInViolation: '4. After startup, only dynamic instances prescribed by the baseline file can be registered.'
          } );
        }
      }, 0 );
    }
  }

  /**
   * @private
   */
  validateOverridesFile() {

    // import phetioEngine causes a cycle and cannot be used, hence we must use the namespace
    const entireBaseline = phet.phetio.phetioEngine.getPhetioElementsMetadata();

    for ( const phetioID in window.phet.preloads.phetio.phetioElementsOverrides ) {
      if ( !entireBaseline.hasOwnProperty( phetioID ) ) {
        this.assertAPIError( {
          phetioID: phetioID,
          ruleInViolation: '7. Any schema entries in the overrides file must exist in the baseline file.',
          message: 'phetioID expected in the baseline file but does not exist'
        } );
      }
      else {

        const override = window.phet.preloads.phetio.phetioElementsOverrides[ phetioID ];
        const baseline = entireBaseline[ phetioID ];

        if ( Object.keys( override ).length === 0 ) {
          this.assertAPIError( {
            phetioID: phetioID,
            ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
            message: 'no metadata keys found for this override.'
          } );
        }

        for ( const metadataKey in override ) {
          if ( !baseline.hasOwnProperty( metadataKey ) ) {
            this.assertAPIError( {
              phetioID: phetioID,
              ruleInViolation: '8. Any schema entries in the overrides file must be different from its baseline counterpart.',
              message: `phetioID metadata key not found in the baseline: ${metadataKey}`
            } );
          }
        }
      }
    }
  }

  /**
   * Assert out the failed api validation rule.
   * @param {Object} apiErrorObject - see doc for this.apiMismatches
   * @private
   */
  assertAPIError( apiErrorObject ) {

    const mismatchMessage = apiErrorObject.phetioID ? `${apiErrorObject.phetioID}:  ${apiErrorObject.ruleInViolation}` :
                            `${apiErrorObject.ruleInViolation}`;

    console.log( 'error data:', apiErrorObject );
    assert && assert( false, 'PhET-iO API error:\n' + mismatchMessage );
  }
}

export default tandemNamespace.register( 'phetioAPIValidation', new PhetioAPIValidation() );<|endoftext|>


<commit_before>'use strict';

let Promise = require('bluebird'),
  urlParser = require('url'),
  log = require('intel'),
  BmpRunner = require('./bmp_runner'),
  harBuilder = require('../support/har_builder'),
  trafficShapeParser = require('../support/traffic_shape_parser');

class ProxyRunDelegate {
  constructor(options) {
    this.trafficShapeConfig = trafficShapeParser.parseTrafficShapeConfig(options);
    this.createHar = !options.skipHar;

    if (this.trafficShapeConfig || this.createHar === true) {
      this.bmpRunner = new BmpRunner();
    }
  }

  __runIfProxy(block) {
    if (this.bmpRunner) {
      return block();
    } else {
      return Promise.resolve()
    }
  }

  onStart() {
    return this.__runIfProxy(() => this.bmpRunner.start());
  }

  onStartRun(options) {
    return this.__runIfProxy(() => {
      this.isFirstPage = true;
      let promise = this.bmpRunner.startProxy()
        .tap(() => {
          let proxyConfigTasks = [];
          if (this.trafficShapeConfig) {
            proxyConfigTasks.push(this.bmpRunner.setLimit(trafficShapeConfig));
          }
          const auth = options.basicAuth;
          if (auth) {
            if (!auth.domain) {
              auth.domain = urlParser.parse(url).host;
              log.verbose('Extracting domain %s for basic authentication from url', auth.domain);
            }
            proxyConfigTasks.push(this.bmpRunner.addBasicAuth(auth.domain, auth.username, auth.password));
          }
          return Promise.all(proxyConfigTasks);
        })
        .tap(function(proxyPort) {
          options.proxyPort = proxyPort;
        });

      if (this.createHar) {
        promise = promise.then(() => this.bmpRunner.createHAR());
      }

      return promise;

    });
  }

  onStartPage() {
    return this.__runIfProxy(() => {
      let promise = Promise.resolve();

      if (this.createHar && !this.isFirstPage) {
        promise = this.bmpRunner.startNewPage();
      }

      this.isFirstPage = false;

      return promise;
    });
  }

  onStopRun(result) {
    return this.__runIfProxy(() => {
      let promise = Promise.resolve();

      if (this.createHar) {
        promise = promise.then(() => {
          this.bmpRunner.getHAR()
            .then(JSON.parse)
            .tap(function(har) {
              harBuilder.addCreator(har);
              result.har = har;
            });
        });
      }

      return promise.tap(() => this.bmpRunner.stopProxy());
    });
  }

  onStop() {
    return this.__runIfProxy(() => {
      log.debug('Stopping proxy process');
      return this.bmpRunner.stop()
        .tap(function() {
          log.debug('Stopped proxy process');
        });
    });
  }

}

module.exports = ProxyRunDelegate;<commit_msg>ES6 syntax + tweak return value of onStopRun.<commit_after>'use strict';

let Promise = require('bluebird'),
  urlParser = require('url'),
  log = require('intel'),
  BmpRunner = require('./bmp_runner'),
  harBuilder = require('../support/har_builder'),
  trafficShapeParser = require('../support/traffic_shape_parser');

class ProxyRunDelegate {
  constructor(options) {
    this.trafficShapeConfig = trafficShapeParser.parseTrafficShapeConfig(options);
    this.createHar = !options.skipHar;

    if (this.trafficShapeConfig || this.createHar === true) {
      this.bmpRunner = new BmpRunner();
    }
  }

  __runIfProxy(block) {
    if (this.bmpRunner) {
      return block();
    } else {
      return Promise.resolve()
    }
  }

  onStart() {
    return this.__runIfProxy(() => this.bmpRunner.start());
  }

  onStartRun(options) {
    return this.__runIfProxy(() => {
      this.isFirstPage = true;
      let promise = this.bmpRunner.startProxy()
        .tap(() => {
          let proxyConfigTasks = [];
          if (this.trafficShapeConfig) {
            proxyConfigTasks.push(this.bmpRunner.setLimit(trafficShapeConfig));
          }
          const auth = options.basicAuth;
          if (auth) {
            if (!auth.domain) {
              auth.domain = urlParser.parse(url).host;
              log.verbose('Extracting domain %s for basic authentication from url', auth.domain);
            }
            proxyConfigTasks.push(this.bmpRunner.addBasicAuth(auth.domain, auth.username, auth.password));
          }
          return Promise.all(proxyConfigTasks);
        })
        .tap((proxyPort) => {
          options.proxyPort = proxyPort;
        });

      if (this.createHar) {
        promise = promise.then(() => this.bmpRunner.createHAR());
      }

      return promise;

    });
  }

  onStartPage() {
    return this.__runIfProxy(() => {
      let promise = Promise.resolve();

      if (this.createHar && !this.isFirstPage) {
        promise = this.bmpRunner.startNewPage();
      }

      this.isFirstPage = false;

      return promise;
    });
  }

  onStopRun(result) {
    return this.__runIfProxy(() => {
      let promise = Promise.resolve();

      if (this.createHar) {
        promise = promise.then(() => {
          this.bmpRunner.getHAR()
            .then(JSON.parse)
            .then((har) => {
              harBuilder.addCreator(har);
              result.har = har;
              return result;
            });
        });
      }

      return promise.tap(() => this.bmpRunner.stopProxy());
    });
  }

  onStop() {
    return this.__runIfProxy(() => {
      log.debug('Stopping proxy process');
      return this.bmpRunner.stop()
        .tap(() => {
          log.debug('Stopped proxy process');
        });
    });
  }

}

module.exports = ProxyRunDelegate;<|endoftext|>