/*jslint browser: true, regexp: true*/ /*global $, jQuery, alert, devel, console, diff_match_patch, FileReader, Blob, saveAs */ //Print debug output to console var DEBUG = 1; //Whether recording a new iteration of eventLoop var recording = 0; var recordingCircle; //Create new DIFF object //Used to detect changes made to text when demonstrating var Diff_Constructor = diff_match_patch; var diff = new Diff_Constructor(); //Used as input to DIFF object var previousText; //History of previous program states //Used for UNDO functionality var stateHistory = []; //State object, contains the state of a program before any execution of a hypothesis //textboxContent: Contents of the editing textbox at this state //cursorLocation: Location of last action performed (so we know where to start from!) function State(textboxContent, cursorLocation) { "use strict"; this.textboxContent = textboxContent; this.cursorLocation = cursorLocation; } //Event trace currently being demonstrated var eventTrace = []; //History of demonstrated traces var demonstrationHistory = []; //The location where an insert/delete was last performed when executing a hypothesis. //This is used so we can tell from where to execute our hypothesis next var previousLocation = 0; //Contains contents of read file var fileContents = ""; var fileName = ""; // Enum for event types var EventNames = ["CLICK", "SELECT", "INSERT", "DELETE"]; var EventType = { CLICK: 0, SELECT: 1, INSERT: 2, DELETE: 3 }; var Key = { BACKSPACE: 8, SPACE: 32, RETURN: 13 }; var PositionNames = ["START", "MID", "END"]; var Position = { START: 0, MID: 1, END: 2 }; //Returns true if value is a number function isNumber(value) { "use strict"; return typeof value === 'number' && isFinite(value); } //Types of character. Indexed so that they can be easily generalized by dividing by two. var CharType = { //Heirachy shown below: // UPPERCASE // LETTER < // LOWERCASE // // ALPHANUMERIC < // // ------- // NUMBER < // ------- // //CHARACTER < // // ------- // PUNCTUATION < // ------- // // NON-ALPHANUMERIC < // // NEWLINE // WHITESPACE < // SPACE CHARACTER: 1, ALPHANUMERIC: 2, NON_ALPHANUMERIC: 3, LETTER: 4, NUMBER: 5, PUNCTUATION: 6, WHITESPACE: 7, UPPERCASE: 8, LOWERCASE: 9, NEWLINE: 14, SPACE: 15 }; //Mappings from CharType enum to string representation var CharNames = ["UNASSIGNED", "CHARACTER", "ALPHANUMERIC", "NON-ALPHANUMERIC", "LETTER", "NUMBER", "PUNCTUATION", "WHITESPACE", "UPPERCASE", "LOWERCASE", "UNASSIGNED", "UNASSIGNED", "UNASSIGNED", "UNASSIGNED", "NEWLINE", "SPACE"]; //Mappings from CharType enum to regexp tokens var CharTypeTokens = [ "", //UNDEFINED ".", //CHARACTER "[a-zA-Z\\d]", //ALPHANUMERIC "[^a-zA-Z\\d]", //NON-ALPHANUMERIC "[a-zA-Z]", //LETTER "\\d", //NUMBER "[^a-zA-Z\\d\\s]", //PUNCTUATION "\\s", //WHITESPACE "[A-Z]", //UPPERCASE "[a-z]", //LOWERCASE "", //UNDEFINED "", //UNDEFINED "", //UNDEFINED "", //UNDEFINED "\\n", //NEWLINE "[^\\S\\n]" //SPACE (not non-whitespace, not newline) ]; // Defines an event that a user has demonstrated // CLICK eventType, wordPreceeding, wordFollowing, positionAttributes, distanceFromPrevious // SELECT eventType, wordPreceeding, wordFollowing, positionAttributes, distanceFromPrevious // INSERT eventType, data // DELETE eventType, data function Event(eventType, prefix, suffix, wordPos, linePos, data) { "use strict"; this.eventType = eventType; this.prefix = prefix; this.suffix = suffix; this.wordPos = wordPos; this.linePos = linePos; this.data = data; } //Contains data about a pattern. //symbols = Array containing symbols to be represented e.g. [CharType.CHARACTER, ",", " "] //quantifiers = Array containing quantifiers for the symbols e.g. ["*", ".", "."] function Pattern(symbols, quantifiers) { "use strict"; this.symbols = symbols; this.quantifiers = quantifiers; this.condense = function () { var i, tmp; //for every character except the last, try to merge with the character following for (i = 0; i < this.symbols.length - 1; i += 1) { //If next symbol is the same as this current one if (this.symbols[i] === this.symbols[i + 1]) { //If not already quantified, do so if (this.quantifiers[i] !== "+") { this.quantifiers[i] = "+"; } //We can now delete the repeated symbol this.symbols.splice(i + 1, 1); this.quantifiers.splice(i + 1, 1); //Symbol after [i] has been condensed. Retry condensing from same location now that new character is following. i -= 1; } } }; //Generalises the most specific symbol in the pattern. //Loop through symbols, and find those which are the most specific. Then generalise them. this.generalise = function () { var mostSpecific = -1, i; //Find index of most specific symbol for (i = 0; i < this.symbols.length; i += 1) { if ((mostSpecific === -1 || this.symbols[mostSpecific] < this.symbols[i]) && isNumber(this.symbols[i])) { mostSpecific = i; } } //Generalise most specific symbol //If symbol is as general as possible, generalise it's quantifier, else, divide by two to move up character heirachy if (this.symbols[mostSpecific] === 1) { console.log("Cannot make symbol any more general!"); //Attempt to generalise quantifiers //Find the first "+" we come across, and make into a "*" for (i = 0; i < this.symbols.length; i += 1) { if (this.quantifiers[i] === "+") { this.quantifiers[i] = "*"; return true; } } //If we couldn't generalise a quantifier, return false return false; } else { this.symbols[mostSpecific] = Math.floor(this.symbols[mostSpecific] / 2); return true; } }; //Creates a equivalent regular expression from a Pattern object this.getRegexp = function () { var symbol, i, output = ""; for (i = 0; i < this.symbols.length; i += 1) { //If is a number, append corresponding regexp token if (isNumber(this.symbols[i])) { output += CharTypeTokens[this.symbols[i]]; } else if (this.symbols[i] === ")" || this.symbols[i] === "(") { output += "\\" + this.symbols[i]; } else { output += this.symbols[i]; } if (this.quantifiers[i] !== "1") { if ($("#cbLazy").is(":checked")) { output += this.quantifiers[i] + "?"; } else { output += this.quantifiers[i]; } } } return output; }; //Prints a pattern's details to the console this.printPattern = function () { var i, tmp = ""; for (i = 0; i < this.symbols.length; i += 1) { if (!isNumber(this.symbols[i])) { tmp += this.symbols[i] + ","; } else { tmp += CharNames[this.symbols[i]] + ","; } } //remove trailing comma from output tmp = tmp.substr(0, tmp.length - 1); console.log("Pattern: [" + tmp + "]"); console.log("Quantifiers: [" + this.quantifiers + "]"); console.log("Regular Expression: " + this.getRegexp()); }; } // Adds an event to current point in event trace function addToTrace(event) { "use strict"; var tail, replace; //If no previous events, add to trace and return if (eventTrace.length === 0) { eventTrace.push(event); return; } tail = eventTrace[eventTrace.length - 1]; //If there were previous events, we need to see if we are able to merge this new event with the one directly before it //Click/select events can be replaced if performed concurrently if ((tail.eventType === EventType.CLICK && event.eventType === EventType.SELECT) || (tail.eventType === EventType.SELECT && event.eventType === EventType.CLICK) || (tail.eventType === EventType.SELECT && event.eventType === EventType.SELECT) || (tail.eventType === EventType.CLICK && event.eventType === EventType.CLICK)) { replace = true; } //If events are the same type, and are INSERT/DELETE events, merge their contents and return if ((tail.eventType === event.eventType) && ((tail.eventType === (EventType.INSERT)) || tail.eventType === EventType.DELETE)) { if (tail.eventType === EventType.INSERT) { tail.data += event.data; } else { tail.data = event.data + tail.data; } return; } //Replace event, or add to end of trace if (replace) { eventTrace.pop(); eventTrace.push(event); } else { //Else, just add a new event to the trace eventTrace.push(event); } } function printEventTrace() { "use strict"; var i = 0; console.log("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); console.log("////// EVENT TRACE //////"); console.log(eventTrace.length + " EVENTS IN TOTAL"); for (i = 0; i < eventTrace.length; i += 1) { console.log("Trace[" + i + "] >> " + EventNames[eventTrace[i].eventType]); console.log(eventTrace[i]); } } // Returns index of the start of the line that the user has clicked function getStartOfLine(element) { "use strict"; return element.val().substr(0, element[0].selectionStart).lastIndexOf("\n") + 1; } // Returns position of the caret in line // [start, end] function getCaretPos(element) { "use strict"; var offset = getStartOfLine(element); return [element[0].selectionStart - offset, element[0].selectionEnd - offset]; } // Returns the word preceeding where the user has clicked function getWordPreceeding(element, caretPos) { "use strict"; var line, preceedingWord; //Select from start of line to where caret is placed line = element.val().substr(getStartOfLine(element), caretPos[0]); //Match regex preceedingWord = line.match(/(\S){0,12}\s*$/g); //Return either empty string or preceeding word return preceedingWord === null ? "" : preceedingWord; } // Returns the word following where the user has clicked function getWordFollowing(element, caretPos) { "use strict"; var lineStartIndex = element.val().substr(0, element[0].selectionStart).lastIndexOf("\n") + 1, line, followingWord; //Select from caret until end of line //If caretPos[0] !== caterPos[1], user selected text. hence use word after end of selection. if (caretPos[0] === caretPos[1]) { line = element.val().substr(lineStartIndex + caretPos[0]); } else { line = element.val().substr(lineStartIndex + caretPos[1]); } //Match Regex (any amount of spaces, then any amount of non-whitespace characters followingWord = line.match(/^[ \t\f]*(\S){0,12}/g); return followingWord === null ? "" : followingWord; } // Gets position in word that user has clicked function getWordPosition(element, caretPos) { "use strict"; var line, character; //if getWordPosition has been called when a word was selected (should return nothing) if (caretPos[0] !== caretPos[1]) { return null; } //Select entire line line = element.val().substr(getStartOfLine(element)); line = line.match(/.*/g); //Test if start of word character = line[0].charCodeAt(caretPos[0] - 1); //If Character before click is NaN (newline) if (isNaN(character) || character === Key.SPACE) { return Position.START; } //Test if end of word character = line[0].charCodeAt(caretPos[0]); if (isNaN(character) || character === Key.SPACE) { return Position.END; } //If not start or end, must be middle return Position.MID; } // Gets position in line that user has clicked function getLinePosition(element, caretPos) { "use strict"; var line, character; //if getWordPosition has been called when a word was selected (should return nothing) if (caretPos[0] !== caretPos[1]) { return null; } //Select entire line line = element.val().substr(getStartOfLine(element)); line = line.match(/.*/g); //Test if start of word character = line[0].charCodeAt(caretPos[0] - 1); //If Character before click is NaN (newline) if (isNaN(character)) { return Position.START; } //Test if end of word character = line[0].charCodeAt(caretPos[0]); if (isNaN(character)) { return Position.END; } //If not start or end, must be middle return Position.MID; } //Returns Longest Common Subsequence of two strings. //Dynamic programming approach using code similar to Java implementation at following url //http://rosettacode.org/wiki/Longest_common_subsequence#Dynamic_Programming_2 //Biased toward returning subsequence that contain punctuation. function lcsFAST(string1, string2) { "use strict"; var lengths = new Array(string1.length + 1), i, j, tmp, strOut = ""; //Create array of correct size for (i = 0; i < lengths.length; i += 1) { lengths[i] = new Array(string2.length + 1); } //Initialise first column/row of array to zero for (i = 0; i < string1.length + 1; i += 1) { lengths[i][0] = 0; } for (i = 0; i < string2.length + 1; i += 1) { lengths[0][i] = 0; } //Fill in array for (i = 0; i < string1.length; i += 1) { for (j = 0; j < string2.length; j += 1) { if (string1.charAt(i) === string2.charAt(j)) { tmp = string1.charCodeAt(i); if ((tmp >= 33 && tmp <= 47) || (tmp >= 58 && tmp <= 64) || (tmp >= 91 && tmp <= 96) || (tmp >= 123 && tmp <= 126)) { lengths[i + 1][j + 1] = lengths[i][j] + 2; } else { lengths[i + 1][j + 1] = lengths[i][j] + 1; } } else { lengths[i + 1][j + 1] = Math.max(lengths[i + 1][j], lengths[i][j + 1]); } } } //Read back substring from array i = string1.length; j = string2.length; while (i !== 0 && j !== 0) { if (lengths[i][j] === lengths[i - 1][j]) { i -= 1; } else if (lengths[i][j] === lengths[i][j - 1]) { j -= 1; } else { strOut = string1.charAt(i - 1) + strOut; i -= 1; j -= 1; } } return strOut; } //Flashes the circle div a specified color and time, then it returns to the color previously used. function flashCircle(color, time) { "use strict"; var beforeColor = $("#divRecordingCircle").css("backgroundColor"); $("#divRecordingCircle").animate({backgroundColor: color}, time, function () { $("#divRecordingCircle").animate({backgroundColor: beforeColor}, time); }); } //Returns Pattern representing a input string function generatePattern(str, lcs) { "use strict"; var i = 0, j = 0, k = 0, tmp, mask = "", output = [], quantifiers = []; //For each position in string, try to apply longest common subsequence for (i = 0; i < str.length; i += 1) { for (j = 0; j < str.length - i; j += 1) { //if character in string matches lcs, move along LCS if (str.charAt(i + j) === lcs.charAt(k)) { mask += lcs.charAt(k); k += 1; } else { mask += "*"; } //if whole subsequence has matched if (k === lcs.length) { for (k = 0; k < str.length - i - j - 1; k += 1) { mask += "*"; } k = lcs.length; break; } } if (k === lcs.length) { break; } mask = ""; } console.log("'" + mask + "'"); // mask now contains text along the lines of: // 2***22*** , for example, when str = 266622666, and LCS was 222. //Now we want to generalise the characters that were not a part of the LCS. //Loop through the characters in 'mask' //Copy characters that are in LCS to output array directly //When a '*' is come across, look at this position in original string, //and find most specific class in character heirachy that matches for (i = 0; i < str.length; i += 1) { //literal from LCS, copy directly to output if (mask.charAt(i) !== "*") { output.push(mask.charAt(i)); } else { //Come across a '*', get original character and find most general representation tmp = str.charCodeAt(i); //Check each leaf node on tree to see which applies to this character if (tmp >= 65 && tmp <= 90) { //If uppercase character output.push(CharType.UPPERCASE); } else if (tmp >= 97 && tmp <= 122) { //If lowercase character output.push(CharType.LOWERCASE); } else if (tmp >= 48 && tmp <= 57) { //If numeric output.push(CharType.NUMBER); } else if ((tmp >= 33 && tmp <= 47) || (tmp >= 58 && tmp <= 64) || (tmp >= 91 && tmp <= 96) || (tmp >= 123 && tmp <= 126)) { //If punctuation output.push(CharType.PUNCTUATION); } else if ((tmp === 32) || (tmp === 9)) { //If whitespace output.push(CharType.SPACE); } else { //NEWLINE? NOT SURE, DON'T DO THIS LOL output.push(CharType.NEWLINE); } } } //Construct a Pattern object to contain this pattern. //Quantifiers default to "1" because the pattern has not been condensed yet. for (i = 0; i < output.length; i += 1) { quantifiers.push("1"); } return new Pattern(output, quantifiers); } //Gets a regular expression satisfying all input strings function getGeneralRegExp(strings) { "use strict"; var LCS = "", i, j, match, temp, tempRegexp, patterns = []; if (strings.length === 1) { return strings[0]; } LCS = lcsFAST(strings[0], strings[1]); //Find LCS of all strings for (i = 2; i < strings.length; i += 1) { LCS = lcsFAST(LCS, strings[i]); } //Create a Pattern object for each string for (i = 0; i < strings.length; i += 1) { patterns.push(generatePattern(strings[i], LCS)); patterns[i].condense(); patterns[i].printPattern(); } //Try to generalize each pattern to match with the others, stop when a general pattern is found for (i = 0; i < patterns.length; i += 1) { for (j = 0; j < strings.length; j += 1) { //Keep generalising pattern until it matches with this string while ((match = strings[j].match(new RegExp("^" + patterns[i].getRegexp() + "$"))) === null) { console.log("Generalising/condensing pattern " + i); tempRegexp = patterns[i].getRegexp(); temp = patterns[i].generalise(); //If cannot generalise any more if (temp !== true || tempRegexp === patterns[i].getRegexp()) { console.log("Can't generalise pattern further to cover string"); break; } patterns[i].condense(); patterns[i].printPattern(); } //If we were unable to generalise this pattern enough to match string, this pattern is useless, try the next one if (match === null) { console.log("Can't generalise string further to cover both strings"); break; } else if (j === strings.length - i - 1) { console.log("Found a pattern that covers all strings!"); patterns[i].printPattern(); return patterns[i].getRegexp(); } } } } //returns true if event trace is consistent will all past traces function validateEventTrace() { "use strict"; var i, j; if (eventTrace.length === 0) { return false; } if (demonstrationHistory.length === 0) { return true; } for (i = 0; i < demonstrationHistory.length; i += 1) { if (demonstrationHistory[i].length !== eventTrace.length) { return false; } for (j = 0; j < demonstrationHistory[i].length; j += 1) { if (demonstrationHistory[i][j].eventType !== eventTrace[j].eventType) { return false; } } } return true; } function executeHypothesis(hypothesis, string, undoEnabled) { "use strict"; var i, regex, regexString = "", result, location, temp, temp2; location = 0; previousLocation = 0; if (hypothesis === undefined || hypothesis.length === 0) { alert("You haven't taught me anything yet..."); return -1; } //For each event in hypothesis for (i = 0; i < hypothesis.length; i += 1) { if (hypothesis[i].eventType === EventType.CLICK) { if (hypothesis[i].prefix !== undefined) { regexString += hypothesis[i].prefix; } if (hypothesis[i].suffix !== undefined) { regexString += hypothesis[i].suffix; } //Find next occurence of regex regex = new RegExp(regexString, "g"); regex.lastIndex = previousLocation; //console.log("last regex match at index = " + regex.lastIndex); location = (string.substr(previousLocation, string.length - previousLocation).search(regex)); //If no more matches occur if (location === -1) { return -1; } location += previousLocation; //console.log("next occurence of regex = " + location); result = regex.exec(string); result = result[0]; if (undoEnabled === true) { //Back up our current state, for UNDO functionality stateHistory.push(new State($("#textMain").val(), location - 1)); } //If a prefix has been identified, //move cursor position to the middle of the matching parts if (hypothesis[i].prefix !== null) { //Find out length of prefix match, and move along that far so we are in-between prefix and suffix regex = new RegExp(hypothesis[i].prefix); temp2 = regex.exec(result); location += temp2[0].length; } else if (hypothesis[i].suffix === null) { location = location + result.length; } } else if (hypothesis[i].eventType === EventType.DELETE) { //If a CLICK action has been performed earlier we want to delete the words leading up to 'location' if (location !== null) { //Get previous characters leading up to location temp = string.substr(0, location); //console.log("Text before: " + temp); temp2 = temp; temp = temp.replace(new RegExp(hypothesis[i].data + "$"), ""); //console.log("Text after: " + temp); //Update text box string = (temp + string.substr(location, string.length)); //Because we just deleted text, move the match location back some characters to compensate location -= temp2.length - temp.length; } } else if (hypothesis[i].eventType === EventType.INSERT) { if (location !== null) { //Insert text where next click would be string = (string.substr(0, location) + hypothesis[i].data + string.substr(location, string.length - location)); //Because we just inserted text, move the match location along to compensate location += hypothesis[i].data.length; } } previousLocation = location; } return string; } function generaliseTrace() { "use strict"; var hypothesisTrace = [], prefixes = [], suffixes = [], datas = [], wordPositions = [], linePositions = [], i, j; //Construct a new Event Trace. //To start, trace must contain same amount of events, of the same type, //and in the same order as those that the user has demonstrated. if (demonstrationHistory.length === 0) { return; } //Create empty events of same type that has been recorded for (i = 0; i < demonstrationHistory[0].length; i += 1) { hypothesisTrace.push(new Event(demonstrationHistory[0][i].eventType, null, null, null, null, null)); } //For each hypothesis event, explore demonstrated events to generalise attributes for (i = 0; i < hypothesisTrace.length; i += 1) { //For each previously demonstrated instance of this event for (j = 0; j < demonstrationHistory.length; j += 1) { //If INSERT, collect these attributes if (hypothesisTrace[i].eventType === EventType.INSERT || hypothesisTrace[i].eventType === EventType.DELETE) { datas.push(demonstrationHistory[j][i].data); } else if (hypothesisTrace[i].eventType === EventType.CLICK || hypothesisTrace[i].eventType === EventType.SELECT) { //If CLICK, collect these attributes prefixes.push(demonstrationHistory[j][i].prefix); suffixes.push(demonstrationHistory[j][i].suffix); wordPositions.push(demonstrationHistory[j][i].wordPos); linePositions.push(demonstrationHistory[j][i].linePos); } } //Once, for this particular action, all demonstration data has been collected, generalise! //console.log(datas); //console.log(prefixes); //console.log(suffixes); if (datas.length !== 0) { hypothesisTrace[i].data = getGeneralRegExp(datas); } if (prefixes.length !== 0) { hypothesisTrace[i].prefix = getGeneralRegExp(prefixes); } if (suffixes.length !== 0) { hypothesisTrace[i].suffix = getGeneralRegExp(suffixes); } //Loop through word positions. If all are the same, use this in hypothesis. for (j = 0; j < wordPositions.length; j += 1) { //If different to the first wordpos, we don't use this property! if (wordPositions[j] !== wordPositions[0]) { hypothesisTrace[i].wordPos = null; break; } else { //If the same, use it! hypothesisTrace[i].wordPos = wordPositions[i]; } } //Loop through line positions. If all are the same, use this in hypothesis. for (j = 0; j < linePositions.length; j += 1) { //If different to the first line position, we don't use this property! if (linePositions[j] !== linePositions[0]) { hypothesisTrace[i].linePos = null; break; } else { //If the same, use it! hypothesisTrace[i].linePos = linePositions[i]; } } datas = []; suffixes = []; prefixes = []; wordPositions = []; linePositions = []; } console.log(""); console.log("Generalised trace:"); console.log(hypothesisTrace); return hypothesisTrace; } //Prints out details about an event function debug(event) { "use strict"; if (DEBUG === 1) { console.log(event); } } function handleFileSelect(event) { "use strict"; var files = event.target.files, reader = new FileReader(); reader.onload = function (event) { // NOTE: event.target point to FileReader if (files[0].size > 100000) { alert("LARGE FILE WARNING\nMay take a very long time to process...\nSuggested maximum size: 100KB"); } var contents = event.target.result; fileName = files[0].name; fileContents = contents; ////// }; reader.readAsText(files[0]); } //Sets up onclick events, etc on page window.onload = function setupPage() { "use strict"; var hypothesis, i, str = ""; document.getElementById("fileSelector").addEventListener('change', handleFileSelect, false); $("#btnDemonstrate").click(function () { if (!recording) { recording = true; //Set previousText to be the starting contents of the textbox previousText = $("#textMain").val(); $("#divRecordingCircle").animate({backgroundColor: "#FF758F"}, 300); $("#btnDemonstrate").text("End Demonstration"); } else { recording = false; clearInterval(recordingCircle); $("#btnDemonstrate").text("Demonstrate"); $("#divRecordingCircle").css("backgroundColor", "#FFE7E3"); //Add demonstration to demonstration history if (validateEventTrace()) { flashCircle("green", 300); demonstrationHistory.push(eventTrace); previousLocation = 0; } else { flashCircle("0E0604", 300); } //clear event trace eventTrace = []; hypothesis = generaliseTrace(); str = ""; str += "Demonstration count: " + demonstrationHistory.length + "\n"; for (i = 0; i < hypothesis.length; i += 1) { str += "\nEvent Type: " + EventNames[hypothesis[i].eventType] + "\n"; if (hypothesis[i].prefix !== null) { str += "Prefix: " + hypothesis[i].prefix + "\n"; } if (hypothesis[i].suffix !== null) { str += "Suffix: " + hypothesis[i].suffix + "\n"; } if (hypothesis[i].data !== null) { str += "Data: " + hypothesis[i].data + "\n"; } } $("#divOutput").text(str).wrap('
');
        }
    });

    $("#cbLazy").click(function () {
        var str = "";
        hypothesis = generaliseTrace();

        if (hypothesis === undefined) {
            return;
        }

        str += "Demonstration count: " + demonstrationHistory.length + "\n";
        for (i = 0; i < hypothesis.length; i += 1) {
            str += "\nEvent Type: " + EventNames[hypothesis[i].eventType] + "\n";
            if (hypothesis[i].prefix !== null) {
                str += "Prefix: " + hypothesis[i].prefix + "\n";
            }
            if (hypothesis[i].suffix !== null) {
                str += "Suffix: " + hypothesis[i].suffix + "\n";
            }
            if (hypothesis[i].data !== null) {
                str += "Data: " + hypothesis[i].data + "\n";
            }
        }

        $("#divOutput").text(str).wrap('
');
    });
    $("#btnDoSomething").click(function () {
        var hypothesisTrace,
            text,
            temp;
        console.log(demonstrationHistory);

        hypothesisTrace = generaliseTrace();

        //Look through hypothesised trace, and execute!
        temp = executeHypothesis(hypothesisTrace, $("#textMain").val(), true);

        if (temp !== -1) {
            $("#textMain").val(temp);
            $("#btnUndo").removeAttr("disabled");
        }

    });

    $("#btnTest").click(function () {

        console.log("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
        var text1 = $("#textTest1").val(),
            text2 = $("#textTest2").val(),
            text3 = $("#textTest3").val(),
            temp;

        temp = getGeneralRegExp([text1, text2, text3]);
        $("#divOutput").text(temp);
    });

    $("#btnUndo").click(function () {

        if (stateHistory.length === 0) {
            alert("Cannot go any further back than here");
            return;
        }

        $("#textMain").val(stateHistory[stateHistory.length - 1].textboxContent);
        previousLocation = stateHistory[stateHistory.length - 1].cursorLocation;
        stateHistory.pop();
        if (stateHistory.length === 0) {
            $("#btnUndo").attr("disabled", "disabled");
            previousLocation = 0;
        }
    });

    $("#btnClear").click(function () {
        demonstrationHistory = [];
        previousLocation = 0;
        stateHistory = [];
        $("#divOutput").text("");
        alert("Cleared demonstration history");
    });

    $("#btnExecuteFile").click(function () {
        var hypothesisTrace,
            output,
            blob;

        if (fileContents === "") {
            alert("Select a file first!");
            return;
        }

        hypothesisTrace = generaliseTrace();

        while ((output = executeHypothesis(hypothesisTrace, fileContents, false)) !== -1) {
            fileContents = output;
        }

        blob = new Blob([fileContents], {type: "text/plain;charset=utf-8"});
        saveAs(blob, fileName + "-modified.txt");
    });

    //Mouse clicked on text box
    $("#textMain").click(function () {
        var event,
            pos = getCaretPos($(this));

        if (!recording) {
            return;
        }

        //When clicked, it is assumed that the user has finished an edit event, so current state of text is saved
        previousText = $("#textMain").val();

        console.log("New Event:");
        if (pos[0] === pos[1]) {
            console.log("   Type: CLICK");
            event = new Event(EventType.CLICK,
                              getWordPreceeding($(this), pos)[0],
                              getWordFollowing($(this), pos)[0],
                              getWordPosition($(this), pos),
                              getLinePosition($(this), pos),
                              null
                             );

        } else {
            console.log("   Type: SELECT");
            event = new Event(EventType.SELECT,
                              getWordPreceeding($(this), pos)[0],
                              getWordFollowing($(this), pos)[0],
                              getWordPosition($(this), pos),
                              getLinePosition($(this), pos),
                              null
                             );
        }

        addToTrace(event);
        debug(event);
    });

    //Key typed into textbox
    $("#textMain").keyup(function (e) {
        var event,
            //lineNumber = $(this)[0].value.substr(0, $(this)[0].selectionStart).split("\n").length,
            newText = $("#textMain").val(),
            i = 0,
            changes,
            diffText,
            eventType;

        if (!recording) {
            return;
        }

        changes = diff.diff_main(previousText, newText);

        for (i = 0; i < changes.length; i += 1) {
            if (changes[i][0] === 1) {
                diffText = changes[i][1];
                eventType = EventType.INSERT;
                break;
            }
            if (changes[i][0] === -1) {
                diffText = changes[i][1];
                eventType = EventType.DELETE;
                break;
            }
        }

        if (eventType === undefined) {
            return;
        }
        event = new Event(eventType, null, null, null, null, diffText);
        addToTrace(event);
        debug(event);
        previousText = $("#textMain").val();
    });
};