<commit_before>// jspsych.js
// 
//	<NAME> 
//	Percepts and Concepts Lab, Indiana University
//
//
(function($) {
    jsPsych = (function() {

        //
        // public object
        //
        var core = {};

        //
        // private class variables
        //

        // options
        var opts = {};
        // exp structure
        var exp_blocks = [];
        // flow control
        var curr_block = 0;
        // everything loaded?
        var initialized = false;
        // target DOM element
        var DOM_target;
        // time that the experiment began
        var exp_start_time;
        // turk info
        var turk_info;

        //
        // public methods
        //

        // core.init creates the experiment and starts running it
        //		display_element is an HTML element (usually a <div>) that will display jsPsych content
        //		options is an object: {
        //			"experiment_structure": an array of blocks specifying the experiment
        //			"finish": function to execute when the experiment ends
        //		}
        //
        core.init = function(options) {

            // reset the key variables
            exp_blocks = [];
            opts = {};
            initialized = false;
            curr_block = 0;

            // check if there is a body element on the page
            var default_display_element = $('body');
            if (default_display_element.length === 0) {
                $(document.documentElement).append($('<body>'));
                default_display_element = $('body');
            }

            var defaults = {
                'display_element': default_display_element,
                'on_finish': function(data) {
                    return undefined;
                },
                'on_trial_start': function() {
                    return undefined;
                },
                'on_trial_finish': function() {
                    return undefined;
                },
                'on_data_update': function(data) {
                    return undefined;
                }
            };

            // import options
            opts = $.extend({}, defaults, options);

            // set target
            DOM_target = opts.display_element;

            // add CSS class to DOM_target
            DOM_target.addClass('jspsych-display-element');

            run();
        };

        // core.data returns all of the data objects for each block as an array
        //      where core.data[0] = data object from block 0, etc...
        // if flatten is true, then the hierarchical structure of the data
        // is removed and each array entry will be a single trial.

        core.data = function(flatten) {
            var all_data = [];
            for (var i = 0; i < exp_blocks.length; i++) {
                all_data[i] = exp_blocks[i].data;
            }

            if (flatten === true) {
                all_data = flattenData(all_data);
            }

            return all_data;
        };

        // core.dataAsCSV returns a CSV string that contains all of the data
        //      append_data is an option map object that will append values
        //      to every row. for example, if append_data = {"subject": 4},
        //      then a column called subject will be added to the data and
        //      it will always have the value 4.
        core.dataAsCSV = function(append_data) {
            var dataObj = core.data();
            return JSON2CSV(flattenData(dataObj, append_data));
        };

        core.saveCSVdata = function(filename, append_data) {
            var data_string = core.dataAsCSV(append_data);
            saveTextToFile(data_string, filename);
        };

        // core.progress returns an object with the following properties
        // 		total_blocks: the number of total blocks in the experiment
        //		total_trials: the number of total trials in the experiment
        //		current_trial_global: the current trial number in global terms
        // 					i.e. if each block has 20 trials and the experiment is
        //					currently in block 2 trial 10, this has a value of 30.
        //		current_trial_local: the current trial number within the block.
        //		current_block: the current block number.

        core.progress = function() {

            var total_trials = 0;
            for (var i = 0; i < exp_blocks.length; i++) {
                total_trials += exp_blocks[i].num_trials;
            }

            var current_trial_global = 0;
            for (var i = 0; i < curr_block; i++) {
                current_trial_global += exp_blocks[i].num_trials;
            }
            current_trial_global += exp_blocks[curr_block].trial_idx;

            var obj = {
                "total_blocks": exp_blocks.length,
                "total_trials": total_trials,
                "current_trial_global": current_trial_global,
                "current_trial_local": exp_blocks[curr_block].trial_idx,
                "current_block": curr_block
            };

            return obj;
        };

        // core.startTime() returns the Date object which represents the time that the experiment started.

        core.startTime = function() {
            return exp_start_time;
        };

        // core.totalTime() returns the length of time in ms since the experiment began

        core.totalTime = function() {
            return (new Date()).getTime() - exp_start_time.getTime();
        };

        // core.preloadImage will load images into the browser cache so that they appear quickly when
        // used during a trial. 
        //  images: array of paths to images
        //  callback_complete: a function with no arguments that calls when loading is complete
        //  callback_load: a function with a single argument that calls whenever an image is loaded
        //                  argument is the number of images currently loaded.

        core.preloadImages = function(images, callback_complete, callback_load) {

            // flatten the images array
            images = flatten(images);

            var n_loaded = 0;
            var loadfn = (typeof callback_load === 'undefined') ? function() {} : callback_load;
            var finishfn = (typeof callback_complete === 'undefined') ? function() {} : callback_complete;

            for (var i = 0; i < images.length; i++) {
                var img = new Image();

                img.onload = function() {
                    n_loaded++;
                    loadfn(n_loaded);
                    if (n_loaded == images.length) {
                        finishfn();
                    }
                };

                img.src = images[i];
            }
        };

        // core.turkInfo gets information relevant to mechanical turk experiments. returns an object
        // containing the workerID, assignmentID, and hitID, and whether or not the HIT is in
        // preview mode, meaning that they haven't accepted the HIT yet.
        core.turkInfo = function(force_refresh) {
            // default value is false
            force_refresh = (typeof force_refresh === 'undefined') ? false : force_refresh;
            // if we already have the turk_info and force_refresh is false
            // then just return the cached version.
            if (typeof turk_info !== 'undefined' && !force_refresh) {
                return turk_info;
            }
            else {

                var turk = {};

                var param = function(url, name) {
                    name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
                    var regexS = "[\\?&]" + name + "=([^&#]*)";
                    var regex = new RegExp(regexS);
                    var results = regex.exec(url);
                    return (results == null) ? "" : results[1];
                };

                var src = param(window.location.href, "assignmentId") ? window.location.href : document.referrer;

                var keys = ["assignmentId", "hitId", "workerId", "turkSubmitTo"];
                keys.map(

                function(key) {
                    turk[key] = unescape(param(src, key));
                });

                turk.previewMode = (turk.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE");

                turk.outsideTurk = (!turk.previewMode && turk.hitId === "" && turk.assignmentId == "" && turk.workerId == "")

                turk_info = turk;

                return turk;
            }

        };

        // core.submitToTurk will submit a MechanicalTurk ExternalHIT type

        core.submitToTurk = function(data) {

            var turkInfo = core.turkInfo();
            var assignmentId = turkInfo.assignmentId;
            var turkSubmitTo = turkInfo.turkSubmitTo;

            if (!assignmentId || !turkSubmitTo) return;

            var dataString = [];

            for (var key in data) {

                if (data.hasOwnProperty(key)) {
                    dataString.push(key + "=" + escape(data[key]));
                }
            }

            dataString.push("assignmentId=" + assignmentId);

            var url = turkSubmitTo + "/mturk/externalSubmit?" + dataString.join("&");

            window.location.href = url;
        }


        //
        // These are public functions, intended to be used for developing plugins.
        // They aren't considered part of the normal API for the core library.
        //

        core.normalizeTrialVariables = function(trial, protect) {

            protect = (typeof protect === 'undefined') ? [] : protect;

            var keys = getKeys(trial);

            var tmp = {};
            for (var i = 0; i < keys.length; i++) {

                var process = true;
                for (var j = 0; j < protect.length; j++) {
                    if (protect[j] == keys[i]) {
                        process = false;
                        break;
                    }
                }

                if (typeof trial[keys[i]] == "function" && process) {
                    tmp[keys[i]] = trial[keys[i]].call();
                }
                else {
                    tmp[keys[i]] = trial[keys[i]];
                }

            }

            return tmp;

        };

        // if possible_array is not an array, then return a one-element array
        // containing possible_array
        core.enforceArray = function(params, possible_arrays) {

            // function to check if something is an array, fallback
            // to string method if browser doesn't support Array.isArray
            var ckArray = Array.isArray || function(a) {
                    return toString.call(a) == '[object Array]';
                };

            for (var i = 0; i < possible_arrays.length; i++) {
                params[possible_arrays[i]] = ckArray(params[possible_arrays[i]]) ? params[possible_arrays[i]] : [params[possible_arrays[i]]];
            }

            return params;
        };

        core.getKeyboardResponse = function(callback_function, valid_responses, rt_method) {

            rt_method = (typeof rt_method === 'undefined') ? 'date' : rt_method;
            if (rt_method != 'date' && rt_method != 'performance') {
                console.log('Invalid RT method specified in getKeyboardResponse. Defaulting to "date" method.');
                rt_method = 'date';
            }

            var start_time;
            if (rt_method == 'date') {
                start_time = (new Date()).getTime();
            }
            if (rt_method == 'performance') {
                start_time = performance.now();
            }

            var listener_function = function(e) {

                var key_time;
                if (rt_method == 'date') {
                    key_time = (new Date()).getTime();
                }
                if (rt_method == 'performance') {
                    key_time = performance.now();
                }

                var valid_response = false;
                if (typeof valid_responses === 'undefined' || valid_responses.length === 0) {
                    valid_response = true;
                }
                for (var i = 0; i < valid_responses.length; i++) {
                    if (typeof valid_responses[i] == 'string') {
                        if(typeof keylookup[valid_responses[i]] !== 'undefined'){
                            if(e.which == keylookup[valid_responses[i]]) {
                                valid_response = true;
                            }
                        } else {
                            throw new Error('Invalid key string specified for getKeyboardResponse');
                        }
                    } else if (e.which == valid_responses[i]) {
                        valid_response = true;
                    }
                }

                if (valid_response) {
                    $(document).unbind('keydown', listener_function);
                    callback_function({
                        key: e.which,
                        rt: key_time - start_time
                    });
                }
            };

            $(document).keydown(listener_function);
        };

        //
        // private functions //
        //
        function run() {
            // take the experiment structure and dynamically create a set of blocks
            exp_blocks = new Array(opts.experiment_structure.length);

            // iterate through block list to create trials
            for (var i = 0; i < exp_blocks.length; i++) {

                // check to make sure plugin is loaded
                var plugin_name = opts.experiment_structure[i].type;
                if (typeof jsPsych[plugin_name] == 'undefined') {
                    throw new Error("Failed attempt to create trials using plugin type " + plugin_name + ". Is the plugin loaded?");
                }

                var trials = jsPsych[plugin_name]["create"].call(null, opts["experiment_structure"][i]);

                exp_blocks[i] = createBlock(trials);
            }

            // record the start time
            exp_start_time = new Date();

            // begin! - run the first block
            exp_blocks[0].next();
        }

        function nextBlock() {
            curr_block += 1;
            if (curr_block == exp_blocks.length) {
                finishExperiment();
            }
            else {
                exp_blocks[curr_block].next();
            }
        }

        function createBlock(trial_list) {
            var block = {
                trial_idx: -1,

                trials: trial_list,

                data: [],

                next: function() {

                    // call on_trial_finish() 
                    //     if not very first trial
                    //		and not the last call in this block (no trial due to advance in block)
                    if (typeof this.trials[this.trial_idx + 1] != "undefined" && (curr_block != 0 || this.trial_idx > -1)) {
                        opts.on_trial_finish();
                    };

                    this.trial_idx = this.trial_idx + 1;

                    var curr_trial = this.trials[this.trial_idx];

                    if (typeof curr_trial == "undefined") {
                        return this.done();
                    }

                    // call on_trial_start()
                    opts.on_trial_start();

                    do_trial(this, curr_trial);
                },

                writeData: function(data_object) {
                    this.data[this.trial_idx] = data_object;
                    opts.on_data_update(data_object);
                },


                done: nextBlock,

                num_trials: trial_list.length
            };

            return block;
        }

        function finishExperiment() {
            opts["on_finish"].apply((new Object()), [core.data()]);
        }

        function do_trial(block, trial) {
            // execute trial method
            jsPsych[trial.type]["trial"].call(this, DOM_target, block, trial, 1);
        }

        //
        // A few helper functions to handle data format conversion
        //
        function flattenData(data_object, append_data) {

            append_data = (typeof append_data === undefined) ? {} : append_data;

            var trials = [];

            // loop through data_object
            for (var i = 0; i < data_object.length; i++) {
                for (var j = 0; j < data_object[i].length; j++) {
                    var data = $.extend({}, data_object[i][j], append_data);
                    trials.push(data);
                }
            }

            return trials;
        }

        // this function based on code suggested by StackOverflow users:
        // http://stackoverflow.com/users/64741/zachary
        // http://stackoverflow.com/users/317/joseph-sturtevant
        function JSON2CSV(objArray) {
            var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
            var line = '';
            var result = '';
            var columns = [];

            var i = 0;
            for (var j = 0; j < array.length; j++) {
                for (var key in array[j]) {
                    var keyString = key + "";
                    keyString = '"' + keyString.replace(/"/g, '""') + '",';
                    if ($.inArray(key, columns) == -1) {
                        columns[i] = key;
                        line += keyString;
                        i++;
                    }
                }
            }

            line = line.slice(0, - 1);
            result += line + '\r\n';

            for (var i = 0; i < array.length; i++) {
                var line = '';
                for (var j = 0; j < columns.length; j++) {
                    var value = (typeof array[i][columns[j]] === 'undefined') ? '' : array[i][columns[j]];
                    var valueString = value + "";
                    line += '"' + valueString.replace(/"/g, '""') + '",';
                }

                line = line.slice(0, - 1);
                result += line + '\r\n';
            }

            return result;
        }

        function saveTextToFile(textstr, filename) {
            var blobToSave = new Blob([textstr], {
                type: 'text/plain'
            });
            var blobURL = "";
            if (typeof window.webkitURL !== 'undefined') {
                blobURL = window.webkitURL.createObjectURL(blobToSave);
            }
            else {
                blobURL = window.URL.createObjectURL(blobToSave);
            }
            DOM_target.append($('<a>', {
                id: 'jspsych-download-as-text-link',
                href: blobURL,
                css: {
                    display: 'none'
                },
                download: filename,
                html: 'download file'
            }));
            $('#jspsych-download-as-text-link')[0].click();
        }

        function getKeys(obj) {
            var r = [];
            for (var k in obj) {
                if (!obj.hasOwnProperty(k)) continue;
                r.push(k);
            }
            return r;
        }

        // private function to flatten nested arrays
        function flatten(arr, out) {
            out = (typeof out === 'undefined') ? [] : out;
            for (var i = 0; i < arr.length; i++) {
                if (Array.isArray(arr[i])) {
                    flatten(arr[i], out);
                }
                else {
                    out.push(arr[i]);
                }
            }
            return out;
        }
        
        // keycode lookup associative array
        var keylookup = {
            'backspace': 8,
            'tab': 9,
            'enter': 13,
            'shift': 16,
            'ctrl': 17,
            'alt': 18,
            'pause': 19,
            'capslock': 20,
            'esc': 27,
            'space':32,
            'spacebar':32,
            ' ':32,
            'pageup': 33,
            'pagedown': 34,
            'end': 35,
            'home': 36,
            'leftarrow': 37,
            'uparrow': 38,
            'rightarrow': 39,
            'downarrow': 40,
            'insert': 45,
            'delete': 46,
            '0': 48,
            '1': 49,
            '2': 50,
            '3': 51,
            '4': 52,
            '5': 53,
            '6': 54,
            '7': 55,
            '8': 56,
            '9': 57,
            'a': 65,
            'b': 66,
            'c': 67,
            'd': 68,
            'e': 69,
            'f': 70,
            'g': 71,
            'h': 72,
            'i': 73,
            'j': 74,
            'k': 75,
            'l': 76,
            'm': 77,
            'n': 78,
            'o': 79,
            'p': 80,
            'q': 81,
            'r': 82,
            's': 83,
            't': 84,
            'u': 85,
            'v': 86,
            'w': 87,
            'x': 88,
            'y': 89,
            'z': 90,
            'A': 65,
            'B': 66,
            'C': 67,
            'D': 68,
            'E': 69,
            'F': 70,
            'G': 71,
            'H': 72,
            'I': 73,
            'J': 74,
            'K': 75,
            'L': 76,
            'M': 77,
            'N': 78,
            'O': 79,
            'P': 80,
            'Q': 81,
            'R': 82,
            'S': 83,
            'T': 84,
            'U': 85,
            'V': 86,
            'W': 87,
            'X': 88,
            'Y': 89,
            'Z': 90,
            '0numpad': 96,
            '1numpad': 97,
            '2numpad': 98,
            '3numpad': 99,
            '4numpad': 100,
            '5numpad': 101,
            '6numpad': 102,
            '7numpad': 103,
            '8numpad': 104,
            '9numpad': 105,
            'multiply': 106,
            'plus': 107,
            'minus': 109,
            'decimal': 110,
            'divide': 111,
            'F1': 112,
            'F2': 113,
            'F3': 114,
            'F4': 115,
            'F5': 116,
            'F6': 117,
            'F7': 118,
            'F8': 119,
            'F9': 120,
            'F10': 121,
            'F11': 122,
            'F12': 123,
            '=': 187,
            ',': 188,
            '.': 190,
            '/': 191,
            '`': 192,
            '[': 219,
            '\\': 220,
            ']': 221
        };

        return core;
    })();
    
    jsPsych.randomization = (function() {
        
        module = {};
        
        module.simpleSample = function(array, repetitions, unpack) {
           
            var arr_isArray = Array.isArray(array);
            var rep_isArray = Array.isArray(repetitions);
            
            // if array is not an array, then we just repeat the item
            if(!arr_isArray){
                if(!rep_isArray) {
                    array = [array];
                    repetitions = [repetitions];
                } else {
                    repetitions = [repetitions[0]];
                    console.log('Unclear parameters given to randomizeSimpleSample. Multiple set sizes specified, but only one item exists to sample. Proceeding using the first set size.');
                }
            } else {
                if(!rep_isArray) {
                    var reps = [];
                    for(var i = 0; i < array.length; i++){
                        reps.push(repetitions);            
                    }
                    repetitions = reps;
                } else {
                    if(array.length != repetitions.length) {
                        // throw warning if repetitions is too short,
                        // throw warning if too long, and then use the first N
                    }
                }
            }
            
            // should be clear at this point to assume that array and repetitions are arrays with == length
            var allsamples = [];
            for(var i = 0; i < array.length; i++){
                for(var j = 0; j < repetitions[i]; j++){
                    allsamples.push(array[i]);
                }
            }
            
            var out = shuffle(allsamples);
            
            if(unpack) { out = unpackArray(out);  }
            
            return shuffle(out);   
        }
        
        function unpackArray(array) {
            
            var out = {};
        
            for(var i = 0; i < array.length; i++){
                var keys = Object.keys(array[i]);
                for(var k = 0; k < keys.length; k++){
                    if(typeof out[keys[k]] === 'undefined') {
                        out[keys[k]] = [];
                    }
                    out[keys[k]].push(array[i][keys[k]]);
                }
            }
            
            return out;
        }
        
        function shuffle(array) {
          var m = array.length, t, i;
        
          // While there remain elements to shuffle…
          while (m) {
        
            // Pick a remaining element…
            i = Math.floor(Math.random() * m--);
        
            // And swap it with the current element.<commit_msg>add factorial method, rename simpleSample to repeat<commit_after>// jspsych.js
// 
//	<NAME> 
//	Percepts and Concepts Lab, Indiana University
//
//
(function($) {
    jsPsych = (function() {

        //
        // public object
        //
        var core = {};

        //
        // private class variables
        //

        // options
        var opts = {};
        // exp structure
        var exp_blocks = [];
        // flow control
        var curr_block = 0;
        // everything loaded?
        var initialized = false;
        // target DOM element
        var DOM_target;
        // time that the experiment began
        var exp_start_time;
        // turk info
        var turk_info;

        //
        // public methods
        //

        // core.init creates the experiment and starts running it
        //		display_element is an HTML element (usually a <div>) that will display jsPsych content
        //		options is an object: {
        //			"experiment_structure": an array of blocks specifying the experiment
        //			"finish": function to execute when the experiment ends
        //		}
        //
        core.init = function(options) {

            // reset the key variables
            exp_blocks = [];
            opts = {};
            initialized = false;
            curr_block = 0;

            // check if there is a body element on the page
            var default_display_element = $('body');
            if (default_display_element.length === 0) {
                $(document.documentElement).append($('<body>'));
                default_display_element = $('body');
            }

            var defaults = {
                'display_element': default_display_element,
                'on_finish': function(data) {
                    return undefined;
                },
                'on_trial_start': function() {
                    return undefined;
                },
                'on_trial_finish': function() {
                    return undefined;
                },
                'on_data_update': function(data) {
                    return undefined;
                }
            };

            // import options
            opts = $.extend({}, defaults, options);

            // set target
            DOM_target = opts.display_element;

            // add CSS class to DOM_target
            DOM_target.addClass('jspsych-display-element');

            run();
        };

        // core.data returns all of the data objects for each block as an array
        //      where core.data[0] = data object from block 0, etc...
        // if flatten is true, then the hierarchical structure of the data
        // is removed and each array entry will be a single trial.

        core.data = function(flatten) {
            var all_data = [];
            for (var i = 0; i < exp_blocks.length; i++) {
                all_data[i] = exp_blocks[i].data;
            }

            if (flatten === true) {
                all_data = flattenData(all_data);
            }

            return all_data;
        };

        // core.dataAsCSV returns a CSV string that contains all of the data
        //      append_data is an option map object that will append values
        //      to every row. for example, if append_data = {"subject": 4},
        //      then a column called subject will be added to the data and
        //      it will always have the value 4.
        core.dataAsCSV = function(append_data) {
            var dataObj = core.data();
            return JSON2CSV(flattenData(dataObj, append_data));
        };

        core.saveCSVdata = function(filename, append_data) {
            var data_string = core.dataAsCSV(append_data);
            saveTextToFile(data_string, filename);
        };

        // core.progress returns an object with the following properties
        // 		total_blocks: the number of total blocks in the experiment
        //		total_trials: the number of total trials in the experiment
        //		current_trial_global: the current trial number in global terms
        // 					i.e. if each block has 20 trials and the experiment is
        //					currently in block 2 trial 10, this has a value of 30.
        //		current_trial_local: the current trial number within the block.
        //		current_block: the current block number.

        core.progress = function() {

            var total_trials = 0;
            for (var i = 0; i < exp_blocks.length; i++) {
                total_trials += exp_blocks[i].num_trials;
            }

            var current_trial_global = 0;
            for (var i = 0; i < curr_block; i++) {
                current_trial_global += exp_blocks[i].num_trials;
            }
            current_trial_global += exp_blocks[curr_block].trial_idx;

            var obj = {
                "total_blocks": exp_blocks.length,
                "total_trials": total_trials,
                "current_trial_global": current_trial_global,
                "current_trial_local": exp_blocks[curr_block].trial_idx,
                "current_block": curr_block
            };

            return obj;
        };

        // core.startTime() returns the Date object which represents the time that the experiment started.

        core.startTime = function() {
            return exp_start_time;
        };

        // core.totalTime() returns the length of time in ms since the experiment began

        core.totalTime = function() {
            return (new Date()).getTime() - exp_start_time.getTime();
        };

        // core.preloadImage will load images into the browser cache so that they appear quickly when
        // used during a trial. 
        //  images: array of paths to images
        //  callback_complete: a function with no arguments that calls when loading is complete
        //  callback_load: a function with a single argument that calls whenever an image is loaded
        //                  argument is the number of images currently loaded.

        core.preloadImages = function(images, callback_complete, callback_load) {

            // flatten the images array
            images = flatten(images);

            var n_loaded = 0;
            var loadfn = (typeof callback_load === 'undefined') ? function() {} : callback_load;
            var finishfn = (typeof callback_complete === 'undefined') ? function() {} : callback_complete;

            for (var i = 0; i < images.length; i++) {
                var img = new Image();

                img.onload = function() {
                    n_loaded++;
                    loadfn(n_loaded);
                    if (n_loaded == images.length) {
                        finishfn();
                    }
                };

                img.src = images[i];
            }
        };

        // core.turkInfo gets information relevant to mechanical turk experiments. returns an object
        // containing the workerID, assignmentID, and hitID, and whether or not the HIT is in
        // preview mode, meaning that they haven't accepted the HIT yet.
        core.turkInfo = function(force_refresh) {
            // default value is false
            force_refresh = (typeof force_refresh === 'undefined') ? false : force_refresh;
            // if we already have the turk_info and force_refresh is false
            // then just return the cached version.
            if (typeof turk_info !== 'undefined' && !force_refresh) {
                return turk_info;
            }
            else {

                var turk = {};

                var param = function(url, name) {
                    name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
                    var regexS = "[\\?&]" + name + "=([^&#]*)";
                    var regex = new RegExp(regexS);
                    var results = regex.exec(url);
                    return (results == null) ? "" : results[1];
                };

                var src = param(window.location.href, "assignmentId") ? window.location.href : document.referrer;

                var keys = ["assignmentId", "hitId", "workerId", "turkSubmitTo"];
                keys.map(

                function(key) {
                    turk[key] = unescape(param(src, key));
                });

                turk.previewMode = (turk.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE");

                turk.outsideTurk = (!turk.previewMode && turk.hitId === "" && turk.assignmentId == "" && turk.workerId == "")

                turk_info = turk;

                return turk;
            }

        };

        // core.submitToTurk will submit a MechanicalTurk ExternalHIT type

        core.submitToTurk = function(data) {

            var turkInfo = core.turkInfo();
            var assignmentId = turkInfo.assignmentId;
            var turkSubmitTo = turkInfo.turkSubmitTo;

            if (!assignmentId || !turkSubmitTo) return;

            var dataString = [];

            for (var key in data) {

                if (data.hasOwnProperty(key)) {
                    dataString.push(key + "=" + escape(data[key]));
                }
            }

            dataString.push("assignmentId=" + assignmentId);

            var url = turkSubmitTo + "/mturk/externalSubmit?" + dataString.join("&");

            window.location.href = url;
        }


        //
        // These are public functions, intended to be used for developing plugins.
        // They aren't considered part of the normal API for the core library.
        //

        core.normalizeTrialVariables = function(trial, protect) {

            protect = (typeof protect === 'undefined') ? [] : protect;

            var keys = getKeys(trial);

            var tmp = {};
            for (var i = 0; i < keys.length; i++) {

                var process = true;
                for (var j = 0; j < protect.length; j++) {
                    if (protect[j] == keys[i]) {
                        process = false;
                        break;
                    }
                }

                if (typeof trial[keys[i]] == "function" && process) {
                    tmp[keys[i]] = trial[keys[i]].call();
                }
                else {
                    tmp[keys[i]] = trial[keys[i]];
                }

            }

            return tmp;

        };

        // if possible_array is not an array, then return a one-element array
        // containing possible_array
        core.enforceArray = function(params, possible_arrays) {

            // function to check if something is an array, fallback
            // to string method if browser doesn't support Array.isArray
            var ckArray = Array.isArray || function(a) {
                    return toString.call(a) == '[object Array]';
                };

            for (var i = 0; i < possible_arrays.length; i++) {
                params[possible_arrays[i]] = ckArray(params[possible_arrays[i]]) ? params[possible_arrays[i]] : [params[possible_arrays[i]]];
            }

            return params;
        };

        core.getKeyboardResponse = function(callback_function, valid_responses, rt_method) {

            rt_method = (typeof rt_method === 'undefined') ? 'date' : rt_method;
            if (rt_method != 'date' && rt_method != 'performance') {
                console.log('Invalid RT method specified in getKeyboardResponse. Defaulting to "date" method.');
                rt_method = 'date';
            }

            var start_time;
            if (rt_method == 'date') {
                start_time = (new Date()).getTime();
            }
            if (rt_method == 'performance') {
                start_time = performance.now();
            }

            var listener_function = function(e) {

                var key_time;
                if (rt_method == 'date') {
                    key_time = (new Date()).getTime();
                }
                if (rt_method == 'performance') {
                    key_time = performance.now();
                }

                var valid_response = false;
                if (typeof valid_responses === 'undefined' || valid_responses.length === 0) {
                    valid_response = true;
                }
                for (var i = 0; i < valid_responses.length; i++) {
                    if (typeof valid_responses[i] == 'string') {
                        if(typeof keylookup[valid_responses[i]] !== 'undefined'){
                            if(e.which == keylookup[valid_responses[i]]) {
                                valid_response = true;
                            }
                        } else {
                            throw new Error('Invalid key string specified for getKeyboardResponse');
                        }
                    } else if (e.which == valid_responses[i]) {
                        valid_response = true;
                    }
                }

                if (valid_response) {
                    $(document).unbind('keydown', listener_function);
                    callback_function({
                        key: e.which,
                        rt: key_time - start_time
                    });
                }
            };

            $(document).keydown(listener_function);
        };

        //
        // private functions //
        //
        function run() {
            // take the experiment structure and dynamically create a set of blocks
            exp_blocks = new Array(opts.experiment_structure.length);

            // iterate through block list to create trials
            for (var i = 0; i < exp_blocks.length; i++) {

                // check to make sure plugin is loaded
                var plugin_name = opts.experiment_structure[i].type;
                if (typeof jsPsych[plugin_name] == 'undefined') {
                    throw new Error("Failed attempt to create trials using plugin type " + plugin_name + ". Is the plugin loaded?");
                }

                var trials = jsPsych[plugin_name]["create"].call(null, opts["experiment_structure"][i]);

                exp_blocks[i] = createBlock(trials);
            }

            // record the start time
            exp_start_time = new Date();

            // begin! - run the first block
            exp_blocks[0].next();
        }

        function nextBlock() {
            curr_block += 1;
            if (curr_block == exp_blocks.length) {
                finishExperiment();
            }
            else {
                exp_blocks[curr_block].next();
            }
        }

        function createBlock(trial_list) {
            var block = {
                trial_idx: -1,

                trials: trial_list,

                data: [],

                next: function() {

                    // call on_trial_finish() 
                    //     if not very first trial
                    //		and not the last call in this block (no trial due to advance in block)
                    if (typeof this.trials[this.trial_idx + 1] != "undefined" && (curr_block != 0 || this.trial_idx > -1)) {
                        opts.on_trial_finish();
                    };

                    this.trial_idx = this.trial_idx + 1;

                    var curr_trial = this.trials[this.trial_idx];

                    if (typeof curr_trial == "undefined") {
                        return this.done();
                    }

                    // call on_trial_start()
                    opts.on_trial_start();

                    do_trial(this, curr_trial);
                },

                writeData: function(data_object) {
                    this.data[this.trial_idx] = data_object;
                    opts.on_data_update(data_object);
                },


                done: nextBlock,

                num_trials: trial_list.length
            };

            return block;
        }

        function finishExperiment() {
            opts["on_finish"].apply((new Object()), [core.data()]);
        }

        function do_trial(block, trial) {
            // execute trial method
            jsPsych[trial.type]["trial"].call(this, DOM_target, block, trial, 1);
        }

        //
        // A few helper functions to handle data format conversion
        //
        function flattenData(data_object, append_data) {

            append_data = (typeof append_data === undefined) ? {} : append_data;

            var trials = [];

            // loop through data_object
            for (var i = 0; i < data_object.length; i++) {
                for (var j = 0; j < data_object[i].length; j++) {
                    var data = $.extend({}, data_object[i][j], append_data);
                    trials.push(data);
                }
            }

            return trials;
        }

        // this function based on code suggested by StackOverflow users:
        // http://stackoverflow.com/users/64741/zachary
        // http://stackoverflow.com/users/317/joseph-sturtevant
        function JSON2CSV(objArray) {
            var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
            var line = '';
            var result = '';
            var columns = [];

            var i = 0;
            for (var j = 0; j < array.length; j++) {
                for (var key in array[j]) {
                    var keyString = key + "";
                    keyString = '"' + keyString.replace(/"/g, '""') + '",';
                    if ($.inArray(key, columns) == -1) {
                        columns[i] = key;
                        line += keyString;
                        i++;
                    }
                }
            }

            line = line.slice(0, - 1);
            result += line + '\r\n';

            for (var i = 0; i < array.length; i++) {
                var line = '';
                for (var j = 0; j < columns.length; j++) {
                    var value = (typeof array[i][columns[j]] === 'undefined') ? '' : array[i][columns[j]];
                    var valueString = value + "";
                    line += '"' + valueString.replace(/"/g, '""') + '",';
                }

                line = line.slice(0, - 1);
                result += line + '\r\n';
            }

            return result;
        }

        function saveTextToFile(textstr, filename) {
            var blobToSave = new Blob([textstr], {
                type: 'text/plain'
            });
            var blobURL = "";
            if (typeof window.webkitURL !== 'undefined') {
                blobURL = window.webkitURL.createObjectURL(blobToSave);
            }
            else {
                blobURL = window.URL.createObjectURL(blobToSave);
            }
            DOM_target.append($('<a>', {
                id: 'jspsych-download-as-text-link',
                href: blobURL,
                css: {
                    display: 'none'
                },
                download: filename,
                html: 'download file'
            }));
            $('#jspsych-download-as-text-link')[0].click();
        }

        function getKeys(obj) {
            var r = [];
            for (var k in obj) {
                if (!obj.hasOwnProperty(k)) continue;
                r.push(k);
            }
            return r;
        }

        // private function to flatten nested arrays
        function flatten(arr, out) {
            out = (typeof out === 'undefined') ? [] : out;
            for (var i = 0; i < arr.length; i++) {
                if (Array.isArray(arr[i])) {
                    flatten(arr[i], out);
                }
                else {
                    out.push(arr[i]);
                }
            }
            return out;
        }
        
        // keycode lookup associative array
        var keylookup = {
            'backspace': 8,
            'tab': 9,
            'enter': 13,
            'shift': 16,
            'ctrl': 17,
            'alt': 18,
            'pause': 19,
            'capslock': 20,
            'esc': 27,
            'space':32,
            'spacebar':32,
            ' ':32,
            'pageup': 33,
            'pagedown': 34,
            'end': 35,
            'home': 36,
            'leftarrow': 37,
            'uparrow': 38,
            'rightarrow': 39,
            'downarrow': 40,
            'insert': 45,
            'delete': 46,
            '0': 48,
            '1': 49,
            '2': 50,
            '3': 51,
            '4': 52,
            '5': 53,
            '6': 54,
            '7': 55,
            '8': 56,
            '9': 57,
            'a': 65,
            'b': 66,
            'c': 67,
            'd': 68,
            'e': 69,
            'f': 70,
            'g': 71,
            'h': 72,
            'i': 73,
            'j': 74,
            'k': 75,
            'l': 76,
            'm': 77,
            'n': 78,
            'o': 79,
            'p': 80,
            'q': 81,
            'r': 82,
            's': 83,
            't': 84,
            'u': 85,
            'v': 86,
            'w': 87,
            'x': 88,
            'y': 89,
            'z': 90,
            'A': 65,
            'B': 66,
            'C': 67,
            'D': 68,
            'E': 69,
            'F': 70,
            'G': 71,
            'H': 72,
            'I': 73,
            'J': 74,
            'K': 75,
            'L': 76,
            'M': 77,
            'N': 78,
            'O': 79,
            'P': 80,
            'Q': 81,
            'R': 82,
            'S': 83,
            'T': 84,
            'U': 85,
            'V': 86,
            'W': 87,
            'X': 88,
            'Y': 89,
            'Z': 90,
            '0numpad': 96,
            '1numpad': 97,
            '2numpad': 98,
            '3numpad': 99,
            '4numpad': 100,
            '5numpad': 101,
            '6numpad': 102,
            '7numpad': 103,
            '8numpad': 104,
            '9numpad': 105,
            'multiply': 106,
            'plus': 107,
            'minus': 109,
            'decimal': 110,
            'divide': 111,
            'F1': 112,
            'F2': 113,
            'F3': 114,
            'F4': 115,
            'F5': 116,
            'F6': 117,
            'F7': 118,
            'F8': 119,
            'F9': 120,
            'F10': 121,
            'F11': 122,
            'F12': 123,
            '=': 187,
            ',': 188,
            '.': 190,
            '/': 191,
            '`': 192,
            '[': 219,
            '\\': 220,
            ']': 221
        };

        return core;
    })();
    
    jsPsych.randomization = (function() {
        
        module = {};
        
        module.repeat = function(array, repetitions, unpack) {
           
            var arr_isArray = Array.isArray(array);
            var rep_isArray = Array.isArray(repetitions);
            
            // if array is not an array, then we just repeat the item
            if(!arr_isArray){
                if(!rep_isArray) {
                    array = [array];
                    repetitions = [repetitions];
                } else {
                    repetitions = [repetitions[0]];
                    console.log('Unclear parameters given to randomizeSimpleSample. Multiple set sizes specified, but only one item exists to sample. Proceeding using the first set size.');
                }
            } else {
                if(!rep_isArray) {
                    var reps = [];
                    for(var i = 0; i < array.length; i++){
                        reps.push(repetitions);            
                    }
                    repetitions = reps;
                } else {
                    if(array.length != repetitions.length) {
                        // throw warning if repetitions is too short,
                        // throw warning if too long, and then use the first N
                    }
                }
            }
            
            // should be clear at this point to assume that array and repetitions are arrays with == length
            var allsamples = [];
            for(var i = 0; i < array.length; i++){
                for(var j = 0; j < repetitions[i]; j++){
                    allsamples.push(array[i]);
                }
            }
            
            var out = shuffle(allsamples);
            
            if(unpack) { out = unpackArray(out);  }
            
            return shuffle(out);   
        }
        
        module.factorial = function(factors, repetitions, unpack){
    
            var factorNames = Object.keys(factors);
            
            var factor_combinations = [];
            
            for(var i = 0; i < factors[factorNames[0]].length; i++){
                factor_combinations.push({});
                factor_combinations[i][factorNames[0]] = factors[factorNames[0]][i];
            }
            
            for(var i = 1; i< factorNames.length; i++){
                var toAdd = factors[factorNames[i]];
                var n = factor_combinations.length;
                for(var j = 0; j < n; j++){
                    var base = factor_combinations[j];
                    for(var k = 0; k < toAdd.length; k++){
                        var newpiece = {};
                        newpiece[factorNames[i]] = toAdd[k];
                        factor_combinations.push($.extend({}, base, newpiece));
                    }
                }
                factor_combinations.splice(0,n);
            }
            
            repetitions = (typeof repetitions === 'undefined') ? 1 : repetitions;
            var with_repetitions = module.repeat(factor_combinations, repetitions, unpack);
            
            return with_repetitions;
        }
        
        function unpackArray(array) {
            
            var out = {};
        
            for(var i = 0; i < array.length; i++){
                var keys = Object.keys(array[i]);
                for(var k = 0; k < keys.length; k++){
                    if(typeof out[keys[k]] === 'undefined') {
                        out[keys[k]] = [];
                    }
                    out[keys[k]].push(array[i][keys[k]]);
                }
            }
            
            return out;
        }
        
        function shuffle(array) {
          var m = array.length, t, i;
        
          // While there remain elements to shuffle…
          while (m) {
        
            // Pick a remaining element…
            i = Math.floor(Math.random() * m--);
        
            // And swap it with the current element.
{'fraction_non_alphanumeric': 0.11404917113178714, 'fraction_numerical': 0.014372207284755401, 'mean_word_length': 1.1807061205551104, 'pattern_counts': {'":': 16, '<': 55, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '851516', 'n_tokens_mistral': 14568, 'n_tokens_neox': 13068, 'n_words': 3589}