source: main/trunk/model-sites-dev/von-sparql/textedit-regex/js/predictionScript.js@ 29739

Last change on this file since 29739 was 29739, checked in by davidb, 9 years ago

Web application used to edit text using programming by demonstration (PBD)

File size: 37.0 KB
Line 
1/*jslint browser: true, regexp: true*/
2/*global $, jQuery, alert, devel, console, diff_match_patch, FileReader, Blob, saveAs */
3
4
5//Print debug output to console
6var DEBUG = 1;
7
8//Whether recording a new iteration of eventLoop
9var recording = 0;
10var recordingCircle;
11
12//Create new DIFF object
13//Used to detect changes made to text when demonstrating
14var Diff_Constructor = diff_match_patch;
15var diff = new Diff_Constructor();
16//Used as input to DIFF object
17var previousText;
18
19//History of previous program states
20//Used for UNDO functionality
21var stateHistory = [];
22
23//State object, contains the state of a program before any execution of a hypothesis
24//textboxContent: Contents of the editing textbox at this state
25//cursorLocation: Location of last action performed (so we know where to start from!)
26function State(textboxContent, cursorLocation) {
27 "use strict";
28 this.textboxContent = textboxContent;
29 this.cursorLocation = cursorLocation;
30}
31
32//Event trace currently being demonstrated
33var eventTrace = [];
34
35//History of demonstrated traces
36var demonstrationHistory = [];
37
38//The location where an insert/delete was last performed when executing a hypothesis.
39//This is used so we can tell from where to execute our hypothesis next
40var previousLocation = 0;
41
42//Contains contents of read file
43var fileContents = "";
44var fileName = "";
45
46// Enum for event types
47var EventNames = ["CLICK", "SELECT", "INSERT", "DELETE"];
48var EventType = {
49 CLICK: 0,
50 SELECT: 1,
51 INSERT: 2,
52 DELETE: 3
53};
54var Key = {
55 BACKSPACE: 8,
56 SPACE: 32,
57 RETURN: 13
58};
59var PositionNames = ["START", "MID", "END"];
60var Position = {
61 START: 0,
62 MID: 1,
63 END: 2
64};
65
66
67//Returns true if value is a number
68function isNumber(value) {
69 "use strict";
70 return typeof value === 'number' && isFinite(value);
71}
72
73//Types of character. Indexed so that they can be easily generalized by dividing by two.
74var CharType = {
75 //Heirachy shown below:
76 // UPPERCASE
77 // LETTER <
78 // LOWERCASE
79 //
80 // ALPHANUMERIC <
81 //
82 // -------
83 // NUMBER <
84 // -------
85 //
86 //CHARACTER <
87 //
88 // -------
89 // PUNCTUATION <
90 // -------
91 //
92 // NON-ALPHANUMERIC <
93 //
94 // NEWLINE
95 // WHITESPACE <
96 // SPACE
97
98 CHARACTER: 1,
99 ALPHANUMERIC: 2,
100 NON_ALPHANUMERIC: 3,
101 LETTER: 4,
102 NUMBER: 5,
103 PUNCTUATION: 6,
104 WHITESPACE: 7,
105 UPPERCASE: 8,
106 LOWERCASE: 9,
107 NEWLINE: 14,
108 SPACE: 15
109};
110
111//Mappings from CharType enum to string representation
112var CharNames = ["UNASSIGNED",
113 "CHARACTER",
114 "ALPHANUMERIC",
115 "NON-ALPHANUMERIC",
116 "LETTER",
117 "NUMBER",
118 "PUNCTUATION",
119 "WHITESPACE",
120 "UPPERCASE",
121 "LOWERCASE",
122 "UNASSIGNED",
123 "UNASSIGNED",
124 "UNASSIGNED",
125 "UNASSIGNED",
126 "NEWLINE",
127 "SPACE"];
128
129//Mappings from CharType enum to regexp tokens
130var CharTypeTokens = [
131 "", //UNDEFINED
132 ".", //CHARACTER
133 "[a-zA-Z\\d]", //ALPHANUMERIC
134 "[^a-zA-Z\\d]", //NON-ALPHANUMERIC
135 "[a-zA-Z]", //LETTER
136 "\\d", //NUMBER
137 "[^a-zA-Z\\d\\s]", //PUNCTUATION
138 "\\s", //WHITESPACE
139 "[A-Z]", //UPPERCASE
140 "[a-z]", //LOWERCASE
141 "", //UNDEFINED
142 "", //UNDEFINED
143 "", //UNDEFINED
144 "", //UNDEFINED
145 "\\n", //NEWLINE
146 "[^\\S\\n]" //SPACE (not non-whitespace, not newline)
147];
148
149// Defines an event that a user has demonstrated
150// CLICK eventType, wordPreceeding, wordFollowing, positionAttributes, distanceFromPrevious
151// SELECT eventType, wordPreceeding, wordFollowing, positionAttributes, distanceFromPrevious
152// INSERT eventType, data
153// DELETE eventType, data
154function Event(eventType, prefix, suffix, wordPos, linePos, data) {
155 "use strict";
156 this.eventType = eventType;
157 this.prefix = prefix;
158 this.suffix = suffix;
159 this.wordPos = wordPos;
160 this.linePos = linePos;
161 this.data = data;
162}
163
164//Contains data about a pattern.
165//symbols = Array containing symbols to be represented e.g. [CharType.CHARACTER, ",", " "]
166//quantifiers = Array containing quantifiers for the symbols e.g. ["*", ".", "."]
167function Pattern(symbols, quantifiers) {
168 "use strict";
169 this.symbols = symbols;
170 this.quantifiers = quantifiers;
171 this.condense = function () {
172 var i,
173 tmp;
174
175 //for every character except the last, try to merge with the character following
176 for (i = 0; i < this.symbols.length - 1; i += 1) {
177 //If next symbol is the same as this current one
178 if (this.symbols[i] === this.symbols[i + 1]) {
179 //If not already quantified, do so
180 if (this.quantifiers[i] !== "+") {
181 this.quantifiers[i] = "+";
182 }
183 //We can now delete the repeated symbol
184 this.symbols.splice(i + 1, 1);
185 this.quantifiers.splice(i + 1, 1);
186
187 //Symbol after [i] has been condensed. Retry condensing from same location now that new character is following.
188 i -= 1;
189 }
190 }
191 };
192
193 //Generalises the most specific symbol in the pattern.
194 //Loop through symbols, and find those which are the most specific. Then generalise them.
195 this.generalise = function () {
196 var mostSpecific = -1,
197 i;
198
199 //Find index of most specific symbol
200 for (i = 0; i < this.symbols.length; i += 1) {
201 if ((mostSpecific === -1 || this.symbols[mostSpecific] < this.symbols[i])
202 && isNumber(this.symbols[i])) {
203 mostSpecific = i;
204 }
205 }
206
207 //Generalise most specific symbol
208
209 //If symbol is as general as possible, generalise it's quantifier, else, divide by two to move up character heirachy
210 if (this.symbols[mostSpecific] === 1) {
211 console.log("Cannot make symbol any more general!");
212
213 //Attempt to generalise quantifiers
214 //Find the first "+" we come across, and make into a "*"
215 for (i = 0; i < this.symbols.length; i += 1) {
216 if (this.quantifiers[i] === "+") {
217 this.quantifiers[i] = "*";
218 return true;
219 }
220 }
221
222 //If we couldn't generalise a quantifier, return false
223 return false;
224 } else {
225 this.symbols[mostSpecific] = Math.floor(this.symbols[mostSpecific] / 2);
226 return true;
227 }
228 };
229
230 //Creates a equivalent regular expression from a Pattern object
231 this.getRegexp = function () {
232 var symbol,
233 i,
234 output = "";
235
236 for (i = 0; i < this.symbols.length; i += 1) {
237 //If is a number, append corresponding regexp token
238 if (isNumber(this.symbols[i])) {
239 output += CharTypeTokens[this.symbols[i]];
240 } else if (this.symbols[i] === ")" || this.symbols[i] === "(") {
241 output += "\\" + this.symbols[i];
242 } else {
243 output += this.symbols[i];
244 }
245
246 if (this.quantifiers[i] !== "1") {
247 if ($("#cbLazy").is(":checked")) {
248 output += this.quantifiers[i] + "?";
249 } else {
250 output += this.quantifiers[i];
251 }
252 }
253 }
254 return output;
255 };
256
257 //Prints a pattern's details to the console
258 this.printPattern = function () {
259 var i,
260 tmp = "";
261 for (i = 0; i < this.symbols.length; i += 1) {
262 if (!isNumber(this.symbols[i])) {
263 tmp += this.symbols[i] + ",";
264 } else {
265 tmp += CharNames[this.symbols[i]] + ",";
266 }
267 }
268 //remove trailing comma from output
269 tmp = tmp.substr(0, tmp.length - 1);
270
271 console.log("Pattern: [" + tmp + "]");
272 console.log("Quantifiers: [" + this.quantifiers + "]");
273 console.log("Regular Expression: " + this.getRegexp());
274 };
275}
276
277// Adds an event to current point in event trace
278function addToTrace(event) {
279 "use strict";
280 var tail,
281 replace;
282 //If no previous events, add to trace and return
283 if (eventTrace.length === 0) {
284 eventTrace.push(event);
285 return;
286 }
287
288 tail = eventTrace[eventTrace.length - 1];
289
290 //If there were previous events, we need to see if we are able to merge this new event with the one directly before it
291 //Click/select events can be replaced if performed concurrently
292 if ((tail.eventType === EventType.CLICK && event.eventType === EventType.SELECT)
293 || (tail.eventType === EventType.SELECT && event.eventType === EventType.CLICK)
294 || (tail.eventType === EventType.SELECT && event.eventType === EventType.SELECT)
295 || (tail.eventType === EventType.CLICK && event.eventType === EventType.CLICK)) {
296 replace = true;
297 }
298
299 //If events are the same type, and are INSERT/DELETE events, merge their contents and return
300 if ((tail.eventType === event.eventType)
301 && ((tail.eventType === (EventType.INSERT))
302 || tail.eventType === EventType.DELETE)) {
303 if (tail.eventType === EventType.INSERT) {
304 tail.data += event.data;
305 } else {
306 tail.data = event.data + tail.data;
307 }
308 return;
309 }
310
311 //Replace event, or add to end of trace
312 if (replace) {
313 eventTrace.pop();
314 eventTrace.push(event);
315 } else {
316 //Else, just add a new event to the trace
317 eventTrace.push(event);
318 }
319}
320
321function printEventTrace() {
322 "use strict";
323 var i = 0;
324 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");
325 console.log("////// EVENT TRACE //////");
326 console.log(eventTrace.length + " EVENTS IN TOTAL");
327 for (i = 0; i < eventTrace.length; i += 1) {
328 console.log("Trace[" + i + "] >> " + EventNames[eventTrace[i].eventType]);
329 console.log(eventTrace[i]);
330 }
331}
332
333// Returns index of the start of the line that the user has clicked
334function getStartOfLine(element) {
335 "use strict";
336 return element.val().substr(0, element[0].selectionStart).lastIndexOf("\n") + 1;
337}
338
339// Returns position of the caret in line
340// [start, end]
341function getCaretPos(element) {
342 "use strict";
343 var offset = getStartOfLine(element);
344 return [element[0].selectionStart - offset, element[0].selectionEnd - offset];
345}
346
347// Returns the word preceeding where the user has clicked
348function getWordPreceeding(element, caretPos) {
349 "use strict";
350 var line,
351 preceedingWord;
352
353 //Select from start of line to where caret is placed
354 line = element.val().substr(getStartOfLine(element), caretPos[0]);
355
356 //Match regex
357 preceedingWord = line.match(/(\S){0,12}\s*$/g);
358
359 //Return either empty string or preceeding word
360 return preceedingWord === null ? "" : preceedingWord;
361}
362
363// Returns the word following where the user has clicked
364function getWordFollowing(element, caretPos) {
365 "use strict";
366 var lineStartIndex = element.val().substr(0, element[0].selectionStart).lastIndexOf("\n") + 1,
367 line,
368 followingWord;
369
370 //Select from caret until end of line
371 //If caretPos[0] !== caterPos[1], user selected text. hence use word after end of selection.
372 if (caretPos[0] === caretPos[1]) {
373 line = element.val().substr(lineStartIndex + caretPos[0]);
374 } else {
375 line = element.val().substr(lineStartIndex + caretPos[1]);
376 }
377
378 //Match Regex (any amount of spaces, then any amount of non-whitespace characters
379 followingWord = line.match(/^[ \t\f]*(\S){0,12}/g);
380 return followingWord === null ? "" : followingWord;
381}
382
383// Gets position in word that user has clicked
384function getWordPosition(element, caretPos) {
385 "use strict";
386 var line,
387 character;
388
389 //if getWordPosition has been called when a word was selected (should return nothing)
390 if (caretPos[0] !== caretPos[1]) {
391 return null;
392 }
393
394 //Select entire line
395 line = element.val().substr(getStartOfLine(element));
396 line = line.match(/.*/g);
397
398 //Test if start of word
399 character = line[0].charCodeAt(caretPos[0] - 1);
400 //If Character before click is NaN (newline)
401 if (isNaN(character) || character === Key.SPACE) {
402 return Position.START;
403 }
404
405 //Test if end of word
406 character = line[0].charCodeAt(caretPos[0]);
407 if (isNaN(character) || character === Key.SPACE) {
408 return Position.END;
409 }
410
411 //If not start or end, must be middle
412 return Position.MID;
413}
414
415// Gets position in line that user has clicked
416function getLinePosition(element, caretPos) {
417 "use strict";
418 var line,
419 character;
420
421
422 //if getWordPosition has been called when a word was selected (should return nothing)
423 if (caretPos[0] !== caretPos[1]) {
424 return null;
425 }
426
427 //Select entire line
428 line = element.val().substr(getStartOfLine(element));
429 line = line.match(/.*/g);
430
431 //Test if start of word
432 character = line[0].charCodeAt(caretPos[0] - 1);
433 //If Character before click is NaN (newline)
434 if (isNaN(character)) {
435 return Position.START;
436 }
437
438 //Test if end of word
439 character = line[0].charCodeAt(caretPos[0]);
440 if (isNaN(character)) {
441 return Position.END;
442 }
443
444 //If not start or end, must be middle
445 return Position.MID;
446}
447
448//Returns Longest Common Subsequence of two strings.
449//Dynamic programming approach using code similar to Java implementation at following url
450//http://rosettacode.org/wiki/Longest_common_subsequence#Dynamic_Programming_2
451//Biased toward returning subsequence that contain punctuation.
452
453function lcsFAST(string1, string2) {
454 "use strict";
455 var lengths = new Array(string1.length + 1),
456 i,
457 j,
458 tmp,
459 strOut = "";
460
461 //Create array of correct size
462 for (i = 0; i < lengths.length; i += 1) {
463 lengths[i] = new Array(string2.length + 1);
464 }
465
466 //Initialise first column/row of array to zero
467 for (i = 0; i < string1.length + 1; i += 1) {
468 lengths[i][0] = 0;
469 }
470 for (i = 0; i < string2.length + 1; i += 1) {
471 lengths[0][i] = 0;
472 }
473
474 //Fill in array
475 for (i = 0; i < string1.length; i += 1) {
476 for (j = 0; j < string2.length; j += 1) {
477 if (string1.charAt(i) === string2.charAt(j)) {
478 tmp = string1.charCodeAt(i);
479 if ((tmp >= 33 && tmp <= 47)
480 || (tmp >= 58 && tmp <= 64)
481 || (tmp >= 91 && tmp <= 96)
482 || (tmp >= 123 && tmp <= 126)) {
483 lengths[i + 1][j + 1] = lengths[i][j] + 2;
484 } else {
485 lengths[i + 1][j + 1] = lengths[i][j] + 1;
486 }
487 } else {
488 lengths[i + 1][j + 1] = Math.max(lengths[i + 1][j], lengths[i][j + 1]);
489 }
490 }
491 }
492
493 //Read back substring from array
494 i = string1.length;
495 j = string2.length;
496 while (i !== 0 && j !== 0) {
497 if (lengths[i][j] === lengths[i - 1][j]) {
498 i -= 1;
499 } else if (lengths[i][j] === lengths[i][j - 1]) {
500 j -= 1;
501 } else {
502 strOut = string1.charAt(i - 1) + strOut;
503 i -= 1;
504 j -= 1;
505 }
506 }
507 return strOut;
508}
509
510//Flashes the circle div a specified color and time, then it returns to the color previously used.
511function flashCircle(color, time) {
512 "use strict";
513 var beforeColor = $("#divRecordingCircle").css("backgroundColor");
514
515 $("#divRecordingCircle").animate({backgroundColor: color}, time, function () {
516 $("#divRecordingCircle").animate({backgroundColor: beforeColor}, time);
517 });
518}
519
520//Returns Pattern representing a input string
521function generatePattern(str, lcs) {
522 "use strict";
523 var i = 0,
524 j = 0,
525 k = 0,
526 tmp,
527 mask = "",
528 output = [],
529 quantifiers = [];
530 //For each position in string, try to apply longest common subsequence
531 for (i = 0; i < str.length; i += 1) {
532 for (j = 0; j < str.length - i; j += 1) {
533 //if character in string matches lcs, move along LCS
534 if (str.charAt(i + j) === lcs.charAt(k)) {
535 mask += lcs.charAt(k);
536 k += 1;
537 } else {
538 mask += "*";
539 }
540 //if whole subsequence has matched
541 if (k === lcs.length) {
542 for (k = 0; k < str.length - i - j - 1; k += 1) {
543 mask += "*";
544 }
545 k = lcs.length;
546 break;
547 }
548 }
549 if (k === lcs.length) {
550 break;
551 }
552 mask = "";
553 }
554
555 console.log("'" + mask + "'");
556 // mask now contains text along the lines of:
557 // 2***22*** , for example, when str = 266622666, and LCS was 222.
558
559 //Now we want to generalise the characters that were not a part of the LCS.
560 //Loop through the characters in 'mask'
561 //Copy characters that are in LCS to output array directly
562 //When a '*' is come across, look at this position in original string,
563 //and find most specific class in character heirachy that matches
564
565 for (i = 0; i < str.length; i += 1) {
566 //literal from LCS, copy directly to output
567 if (mask.charAt(i) !== "*") {
568 output.push(mask.charAt(i));
569 } else {
570 //Come across a '*', get original character and find most general representation
571 tmp = str.charCodeAt(i);
572
573 //Check each leaf node on tree to see which applies to this character
574 if (tmp >= 65 && tmp <= 90) {
575 //If uppercase character
576 output.push(CharType.UPPERCASE);
577
578 } else if (tmp >= 97 && tmp <= 122) {
579 //If lowercase character
580 output.push(CharType.LOWERCASE);
581
582 } else if (tmp >= 48 && tmp <= 57) {
583 //If numeric
584 output.push(CharType.NUMBER);
585
586 } else if ((tmp >= 33 && tmp <= 47)
587 || (tmp >= 58 && tmp <= 64)
588 || (tmp >= 91 && tmp <= 96)
589 || (tmp >= 123 && tmp <= 126)) {
590 //If punctuation
591 output.push(CharType.PUNCTUATION);
592 } else if ((tmp === 32)
593 || (tmp === 9)) {
594 //If whitespace
595 output.push(CharType.SPACE);
596 } else {
597 //NEWLINE? NOT SURE, DON'T DO THIS LOL
598 output.push(CharType.NEWLINE);
599 }
600 }
601 }
602
603 //Construct a Pattern object to contain this pattern.
604 //Quantifiers default to "1" because the pattern has not been condensed yet.
605 for (i = 0; i < output.length; i += 1) {
606 quantifiers.push("1");
607 }
608 return new Pattern(output, quantifiers);
609}
610
611//Gets a regular expression satisfying all input strings
612function getGeneralRegExp(strings) {
613 "use strict";
614 var LCS = "",
615 i,
616 j,
617 match,
618 temp,
619 tempRegexp,
620 patterns = [];
621
622 if (strings.length === 1) {
623 return strings[0];
624 }
625 LCS = lcsFAST(strings[0], strings[1]);
626
627 //Find LCS of all strings
628 for (i = 2; i < strings.length; i += 1) {
629 LCS = lcsFAST(LCS, strings[i]);
630 }
631
632 //Create a Pattern object for each string
633 for (i = 0; i < strings.length; i += 1) {
634 patterns.push(generatePattern(strings[i], LCS));
635 patterns[i].condense();
636 patterns[i].printPattern();
637 }
638
639 //Try to generalize each pattern to match with the others, stop when a general pattern is found
640
641 for (i = 0; i < patterns.length; i += 1) {
642 for (j = 0; j < strings.length; j += 1) {
643 //Keep generalising pattern until it matches with this string
644 while ((match = strings[j].match(new RegExp("^" + patterns[i].getRegexp() + "$"))) === null) {
645 console.log("Generalising/condensing pattern " + i);
646 tempRegexp = patterns[i].getRegexp();
647 temp = patterns[i].generalise();
648
649 //If cannot generalise any more
650 if (temp !== true || tempRegexp === patterns[i].getRegexp()) {
651 console.log("Can't generalise pattern further to cover string");
652 break;
653 }
654
655 patterns[i].condense();
656 patterns[i].printPattern();
657 }
658
659 //If we were unable to generalise this pattern enough to match string, this pattern is useless, try the next one
660 if (match === null) {
661 console.log("Can't generalise string further to cover both strings");
662 break;
663 } else if (j === strings.length - i - 1) {
664 console.log("Found a pattern that covers all strings!");
665 patterns[i].printPattern();
666 return patterns[i].getRegexp();
667 }
668 }
669 }
670}
671
672//returns true if event trace is consistent will all past traces
673function validateEventTrace() {
674 "use strict";
675 var i,
676 j;
677
678 if (eventTrace.length === 0) {
679 return false;
680 }
681
682 if (demonstrationHistory.length === 0) {
683 return true;
684 }
685
686
687
688 for (i = 0; i < demonstrationHistory.length; i += 1) {
689 if (demonstrationHistory[i].length !== eventTrace.length) {
690 return false;
691 }
692 for (j = 0; j < demonstrationHistory[i].length; j += 1) {
693 if (demonstrationHistory[i][j].eventType !== eventTrace[j].eventType) {
694 return false;
695 }
696 }
697 }
698 return true;
699}
700
701function executeHypothesis(hypothesis, string, undoEnabled) {
702 "use strict";
703 var i,
704 regex,
705 regexString = "",
706 result,
707 location,
708 temp,
709 temp2;
710
711 location = 0;
712 previousLocation = 0;
713
714 if (hypothesis === undefined
715 || hypothesis.length === 0) {
716 alert("You haven't taught me anything yet...");
717 return -1;
718 }
719
720 //For each event in hypothesis
721 for (i = 0; i < hypothesis.length; i += 1) {
722 if (hypothesis[i].eventType === EventType.CLICK) {
723
724 if (hypothesis[i].prefix !== undefined) {
725 regexString += hypothesis[i].prefix;
726 }
727
728 if (hypothesis[i].suffix !== undefined) {
729 regexString += hypothesis[i].suffix;
730 }
731
732 //Find next occurence of regex
733 regex = new RegExp(regexString, "g");
734 regex.lastIndex = previousLocation;
735 //console.log("last regex match at index = " + regex.lastIndex);
736 location = (string.substr(previousLocation, string.length - previousLocation).search(regex));
737
738 //If no more matches occur
739 if (location === -1) {
740 return -1;
741 }
742
743 location += previousLocation;
744 //console.log("next occurence of regex = " + location);
745 result = regex.exec(string);
746
747 result = result[0];
748
749 if (undoEnabled === true) {
750 //Back up our current state, for UNDO functionality
751 stateHistory.push(new State($("#textMain").val(), location - 1));
752 }
753
754 //If a prefix has been identified,
755 //move cursor position to the middle of the matching parts
756 if (hypothesis[i].prefix !== null) {
757 //Find out length of prefix match, and move along that far so we are in-between prefix and suffix
758 regex = new RegExp(hypothesis[i].prefix);
759 temp2 = regex.exec(result);
760 location += temp2[0].length;
761
762 } else if (hypothesis[i].suffix === null) {
763 location = location + result.length;
764 }
765
766 } else if (hypothesis[i].eventType === EventType.DELETE) {
767 //If a CLICK action has been performed earlier we want to delete the words leading up to 'location'
768 if (location !== null) {
769 //Get previous characters leading up to location
770 temp = string.substr(0, location);
771 //console.log("Text before: " + temp);
772 temp2 = temp;
773 temp = temp.replace(new RegExp(hypothesis[i].data + "$"), "");
774 //console.log("Text after: " + temp);
775
776 //Update text box
777 string = (temp + string.substr(location, string.length));
778
779 //Because we just deleted text, move the match location back some characters to compensate
780 location -= temp2.length - temp.length;
781 }
782 } else if (hypothesis[i].eventType === EventType.INSERT) {
783 if (location !== null) {
784 //Insert text where next click would be
785 string = (string.substr(0, location) + hypothesis[i].data + string.substr(location, string.length - location));
786 //Because we just inserted text, move the match location along to compensate
787 location += hypothesis[i].data.length;
788 }
789 }
790
791 previousLocation = location;
792 }
793
794 return string;
795}
796
797function generaliseTrace() {
798 "use strict";
799 var hypothesisTrace = [],
800 prefixes = [],
801 suffixes = [],
802 datas = [],
803 wordPositions = [],
804 linePositions = [],
805 i,
806 j;
807 //Construct a new Event Trace.
808 //To start, trace must contain same amount of events, of the same type,
809 //and in the same order as those that the user has demonstrated.
810 if (demonstrationHistory.length === 0) {
811 return;
812 }
813
814 //Create empty events of same type that has been recorded
815 for (i = 0; i < demonstrationHistory[0].length; i += 1) {
816 hypothesisTrace.push(new Event(demonstrationHistory[0][i].eventType, null, null, null, null, null));
817 }
818
819 //For each hypothesis event, explore demonstrated events to generalise attributes
820 for (i = 0; i < hypothesisTrace.length; i += 1) {
821
822 //For each previously demonstrated instance of this event
823 for (j = 0; j < demonstrationHistory.length; j += 1) {
824 //If INSERT, collect these attributes
825 if (hypothesisTrace[i].eventType === EventType.INSERT || hypothesisTrace[i].eventType === EventType.DELETE) {
826 datas.push(demonstrationHistory[j][i].data);
827 } else if (hypothesisTrace[i].eventType === EventType.CLICK || hypothesisTrace[i].eventType === EventType.SELECT) {
828 //If CLICK, collect these attributes
829 prefixes.push(demonstrationHistory[j][i].prefix);
830 suffixes.push(demonstrationHistory[j][i].suffix);
831 wordPositions.push(demonstrationHistory[j][i].wordPos);
832 linePositions.push(demonstrationHistory[j][i].linePos);
833 }
834 }
835
836 //Once, for this particular action, all demonstration data has been collected, generalise!
837 //console.log(datas);
838 //console.log(prefixes);
839 //console.log(suffixes);
840
841 if (datas.length !== 0) {
842 hypothesisTrace[i].data = getGeneralRegExp(datas);
843 }
844
845 if (prefixes.length !== 0) {
846 hypothesisTrace[i].prefix = getGeneralRegExp(prefixes);
847 }
848
849 if (suffixes.length !== 0) {
850 hypothesisTrace[i].suffix = getGeneralRegExp(suffixes);
851 }
852
853 //Loop through word positions. If all are the same, use this in hypothesis.
854 for (j = 0; j < wordPositions.length; j += 1) {
855 //If different to the first wordpos, we don't use this property!
856 if (wordPositions[j] !== wordPositions[0]) {
857 hypothesisTrace[i].wordPos = null;
858 break;
859 } else {
860 //If the same, use it!
861 hypothesisTrace[i].wordPos = wordPositions[i];
862 }
863 }
864 //Loop through line positions. If all are the same, use this in hypothesis.
865 for (j = 0; j < linePositions.length; j += 1) {
866 //If different to the first line position, we don't use this property!
867 if (linePositions[j] !== linePositions[0]) {
868 hypothesisTrace[i].linePos = null;
869 break;
870 } else {
871 //If the same, use it!
872 hypothesisTrace[i].linePos = linePositions[i];
873 }
874 }
875
876
877 datas = [];
878 suffixes = [];
879 prefixes = [];
880 wordPositions = [];
881 linePositions = [];
882 }
883
884 console.log("");
885 console.log("Generalised trace:");
886 console.log(hypothesisTrace);
887 return hypothesisTrace;
888}
889//Prints out details about an event
890function debug(event) {
891 "use strict";
892 if (DEBUG === 1) {
893 console.log(event);
894 }
895}
896
897function handleFileSelect(event) {
898 "use strict";
899 var files = event.target.files,
900 reader = new FileReader();
901
902 reader.onload = function (event) {
903 // NOTE: event.target point to FileReader
904 if (files[0].size > 100000) {
905 alert("LARGE FILE WARNING\nMay take a very long time to process...\nSuggested maximum size: 100KB");
906 }
907 var contents = event.target.result;
908 fileName = files[0].name;
909 fileContents = contents;
910 //////
911 };
912
913 reader.readAsText(files[0]);
914}
915
916//Sets up onclick events, etc on page
917window.onload = function setupPage() {
918 "use strict";
919 var hypothesis,
920 i,
921 str = "";
922
923 document.getElementById("fileSelector").addEventListener('change', handleFileSelect, false);
924
925 $("#btnDemonstrate").click(function () {
926 if (!recording) {
927
928 recording = true;
929
930 //Set previousText to be the starting contents of the textbox
931 previousText = $("#textMain").val();
932
933 $("#divRecordingCircle").animate({backgroundColor: "#FF758F"}, 300);
934
935 $("#btnDemonstrate").text("End Demonstration");
936 } else {
937
938 recording = false;
939 clearInterval(recordingCircle);
940 $("#btnDemonstrate").text("Demonstrate");
941
942 $("#divRecordingCircle").css("backgroundColor", "#FFE7E3");
943
944 //Add demonstration to demonstration history
945 if (validateEventTrace()) {
946
947 flashCircle("green", 300);
948 demonstrationHistory.push(eventTrace);
949 previousLocation = 0;
950 } else {
951
952 flashCircle("0E0604", 300);
953 }
954
955 //clear event trace
956 eventTrace = [];
957
958 hypothesis = generaliseTrace();
959 str = "";
960 str += "Demonstration count: " + demonstrationHistory.length + "\n";
961 for (i = 0; i < hypothesis.length; i += 1) {
962 str += "\nEvent Type: " + EventNames[hypothesis[i].eventType] + "\n";
963 if (hypothesis[i].prefix !== null) {
964 str += "Prefix: " + hypothesis[i].prefix + "\n";
965 }
966 if (hypothesis[i].suffix !== null) {
967 str += "Suffix: " + hypothesis[i].suffix + "\n";
968 }
969 if (hypothesis[i].data !== null) {
970 str += "Data: " + hypothesis[i].data + "\n";
971 }
972 }
973
974 $("#divOutput").text(str).wrap('<pre />');
975 }
976 });
977
978 $("#cbLazy").click(function () {
979 var str = "";
980 hypothesis = generaliseTrace();
981
982 if (hypothesis === undefined) {
983 return;
984 }
985
986 str += "Demonstration count: " + demonstrationHistory.length + "\n";
987 for (i = 0; i < hypothesis.length; i += 1) {
988 str += "\nEvent Type: " + EventNames[hypothesis[i].eventType] + "\n";
989 if (hypothesis[i].prefix !== null) {
990 str += "Prefix: " + hypothesis[i].prefix + "\n";
991 }
992 if (hypothesis[i].suffix !== null) {
993 str += "Suffix: " + hypothesis[i].suffix + "\n";
994 }
995 if (hypothesis[i].data !== null) {
996 str += "Data: " + hypothesis[i].data + "\n";
997 }
998 }
999
1000 $("#divOutput").text(str).wrap('<pre />');
1001 });
1002 $("#btnDoSomething").click(function () {
1003 var hypothesisTrace,
1004 text,
1005 temp;
1006 console.log(demonstrationHistory);
1007
1008 hypothesisTrace = generaliseTrace();
1009
1010 //Look through hypothesised trace, and execute!
1011 temp = executeHypothesis(hypothesisTrace, $("#textMain").val(), true);
1012
1013 if (temp !== -1) {
1014 $("#textMain").val(temp);
1015 $("#btnUndo").removeAttr("disabled");
1016 }
1017
1018 });
1019
1020 $("#btnTest").click(function () {
1021
1022 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");
1023 var text1 = $("#textTest1").val(),
1024 text2 = $("#textTest2").val(),
1025 text3 = $("#textTest3").val(),
1026 temp;
1027
1028 temp = getGeneralRegExp([text1, text2, text3]);
1029 $("#divOutput").text(temp);
1030 });
1031
1032 $("#btnUndo").click(function () {
1033
1034 if (stateHistory.length === 0) {
1035 alert("Cannot go any further back than here");
1036 return;
1037 }
1038
1039 $("#textMain").val(stateHistory[stateHistory.length - 1].textboxContent);
1040 previousLocation = stateHistory[stateHistory.length - 1].cursorLocation;
1041 stateHistory.pop();
1042 if (stateHistory.length === 0) {
1043 $("#btnUndo").attr("disabled", "disabled");
1044 previousLocation = 0;
1045 }
1046 });
1047
1048 $("#btnClear").click(function () {
1049 demonstrationHistory = [];
1050 previousLocation = 0;
1051 stateHistory = [];
1052 $("#divOutput").text("");
1053 alert("Cleared demonstration history");
1054 });
1055
1056 $("#btnExecuteFile").click(function () {
1057 var hypothesisTrace,
1058 output,
1059 blob;
1060
1061 if (fileContents === "") {
1062 alert("Select a file first!");
1063 return;
1064 }
1065
1066 hypothesisTrace = generaliseTrace();
1067
1068 while ((output = executeHypothesis(hypothesisTrace, fileContents, false)) !== -1) {
1069 fileContents = output;
1070 }
1071
1072 blob = new Blob([fileContents], {type: "text/plain;charset=utf-8"});
1073 saveAs(blob, fileName + "-modified.txt");
1074 });
1075
1076 //Mouse clicked on text box
1077 $("#textMain").click(function () {
1078 var event,
1079 pos = getCaretPos($(this));
1080
1081 if (!recording) {
1082 return;
1083 }
1084
1085 //When clicked, it is assumed that the user has finished an edit event, so current state of text is saved
1086 previousText = $("#textMain").val();
1087
1088 console.log("New Event:");
1089 if (pos[0] === pos[1]) {
1090 console.log(" Type: CLICK");
1091 event = new Event(EventType.CLICK,
1092 getWordPreceeding($(this), pos)[0],
1093 getWordFollowing($(this), pos)[0],
1094 getWordPosition($(this), pos),
1095 getLinePosition($(this), pos),
1096 null
1097 );
1098
1099 } else {
1100 console.log(" Type: SELECT");
1101 event = new Event(EventType.SELECT,
1102 getWordPreceeding($(this), pos)[0],
1103 getWordFollowing($(this), pos)[0],
1104 getWordPosition($(this), pos),
1105 getLinePosition($(this), pos),
1106 null
1107 );
1108 }
1109
1110 addToTrace(event);
1111 debug(event);
1112 });
1113
1114 //Key typed into textbox
1115 $("#textMain").keyup(function (e) {
1116 var event,
1117 //lineNumber = $(this)[0].value.substr(0, $(this)[0].selectionStart).split("\n").length,
1118 newText = $("#textMain").val(),
1119 i = 0,
1120 changes,
1121 diffText,
1122 eventType;
1123
1124 if (!recording) {
1125 return;
1126 }
1127
1128 changes = diff.diff_main(previousText, newText);
1129
1130 for (i = 0; i < changes.length; i += 1) {
1131 if (changes[i][0] === 1) {
1132 diffText = changes[i][1];
1133 eventType = EventType.INSERT;
1134 break;
1135 }
1136 if (changes[i][0] === -1) {
1137 diffText = changes[i][1];
1138 eventType = EventType.DELETE;
1139 break;
1140 }
1141 }
1142
1143 if (eventType === undefined) {
1144 return;
1145 }
1146 event = new Event(eventType, null, null, null, null, diffText);
1147 addToTrace(event);
1148 debug(event);
1149 previousText = $("#textMain").val();
1150 });
1151};
Note: See TracBrowser for help on using the repository browser.