// special values that mean this program has graphical/other output var GRAPHICAL_VALUE = "_graphical"; var OTHER_VALUE = "_other"; var codebase = ""; var timer = null; document.observe("dom:loaded", function() { $("homework").onchange = homeworkChange; $("add").onclick = addClick; $("remove").onclick = removeClick; $("compare").onclick = compareClick; $("expectedtext").spellcheck = false; $("expectedtext").onmouseup = textMouseUp; $("expectedtext").onclick = textMouseUp; $("expectedtext").onkeydown = textMouseUp; $("expectedtext").onkeyup = textMouseUp; $("actualtext").spellcheck = false; $("actualtext").onmouseup = textMouseUp; $("actualtext").onclick = textMouseUp; $("actualtext").onkeydown = textMouseUp; $("actualtext").onkeyup = textMouseUp; // query parameter to fetch output for a given assignment var hw = getQueryParameter("assignment"); if (hw && $(hw)) { var option = $(hw).select("option")[0]; if (option) { var index = $A($("homework").options).indexOf(option); if (index >= 0) { $("homework").selectedIndex = index; homeworkChange(); } } } }); /** Returns the current page's HTML query value for the given parameter, else null. */ function getQueryParameter(name) { var url = window.location.search.substring(1); var chunks = url.split(/&/); for (var i = 0; i < chunks.length; i++) { var keyValue = chunks[i].split(/=/); if (keyValue[0] == name) { return keyValue[1]; } } return null; }; function textMouseUp(event) { var selectionLength = 0; if (this.selectionEnd !== undefined && this.selectionStart !== undefined) { selectionLength = Math.abs(this.selectionEnd - this.selectionStart); } else { selectionLength = getSelectedText().length; } if (selectionLength !== undefined && !isNaN(selectionLength)) { $(this.className + "chars").innerHTML = "(" + selectionLength + " characters selected)"; } } function numDigits(array) { // capped at 9999 lines if (array.length >= 1000) { return 4; } else if (array.length >= 100) { return 3; } else if (array.length >= 10) { return 2; } return 1; } function addLineNumbers(text) { var lines = text.split(/\n/); var digits = numDigits(lines); // build new string of lines with prepended numbers var newText = ""; for (var i = 0; i < lines.length; i++) { if (newText.length > 0) { newText += "\n"; } // don't include a number for a final blank line if (i < lines.length - 1 || lines[i].length > 0) { newText += padL("" + (i + 1), digits) + " " + lines[i]; } } return newText; } function hasLineNumbers(text) { var lines = text.split(/\n/); var alreadyNumberedCount = 0; for (var i = 0; i < lines.length; i++) { if (lines[i].indexOf("" + (i + 1)) >= 0) { alreadyNumberedCount++; } } return alreadyNumberedCount >= lines.length - 1; } // pre: all lines have numbers function removeLineNumbers(text) { var lines = text.split(/\n/); var digits = numDigits(lines); // build new string of lines with prepended numbers var newText = ""; for (var i = 0; i < lines.length; i++) { if (newText.length > 0) { newText += "\n"; } newText += lines[i].substring(digits + 2); } return newText; } function addLineNumbersExpected() { if (!hasLineNumbers($("expectedtext").value)) { $("expectedtext").value = addLineNumbers(rtrim($("expectedtext").value)); } } function addLineNumbersActual() { if (!hasLineNumbers($("actualtext").value)) { $("actualtext").value = addLineNumbers(rtrim($("actualtext").value)); } } function addClick() { addLineNumbersExpected(); addLineNumbersActual(); compareClick(); } function removeLineNumbersExpected() { if (hasLineNumbers($("expectedtext").value)) { $("expectedtext").value = removeLineNumbers($("expectedtext").value); } } function removeLineNumbersActual() { if (hasLineNumbers($("actualtext").value)) { $("actualtext").value = removeLineNumbers($("actualtext").value); } } function removeClick() { removeLineNumbersExpected(); removeLineNumbersActual(); compareClick(); } function splitIntoLines(text, ignoreLeadingSpaces) { // split into lines (trims trailing whitespace, except on POS IE6) var lines = text.split(/[ ]*\n/); // trim trailing whitespace for (var i = 0; i < lines.length; i++) { lines[i] = rtrim(lines[i]); // possibly also trim leading whitespace if (ignoreLeadingSpaces) { lines[i] = ltrim(lines[i]); } } return lines; } function compareClick() { if ($("fetchiframe")) { new Effect.BlindUp($("fetchiframe")); } var expectedText = rtrim($("expectedtext").value); var actualText = rtrim($("actualtext").value); var resultText = ""; var SEP = "
"; var diffCount = 0; var ignoreLeadingSpaces = $("stripwhitespace").checked; var expectedLines = splitIntoLines(expectedText, ignoreLeadingSpaces); var actualLines = splitIntoLines(actualText, ignoreLeadingSpaces); var hasNumbers = hasLineNumbers(expectedText) && hasLineNumbers(actualText); var out = betterDiff(expectedLines, actualLines, hasNumbers); for (var i = 0; i < out.length; i++) { /* // I was trying to make the tool understand how to eliminate // leading/trailing diffs for creative output, // but this JavaScript diff tool doesn't give me back line numbers // so it's pretty much impossible to tell "where" a diff is. // Screw it. if (i == 0 && $("creativestart").checked) { // file2 is allowed to have stuff not present in file1 alert("creative start!"); } else if (i == out.length - 1 && $("creativeend").checked) { // file2 is allowed to have stuff not present in file1 alert("creative start!"); } */ var empty = true; if (out[i]["file1"]) { for (var j = 0; j < out[i]["file1"].length; j++) { empty = false; var encoded = htmlEncode(out[i]["file1"][j]); resultText += "" + "< " + encoded + "" + SEP; diffCount++; } } if (out[i]["file2"]) { for (var j = 0; j < out[i]["file2"].length; j++) { empty = false; var encoded = htmlEncode(out[i]["file2"][j]); resultText += "" + "> " + encoded + "" + SEP; diffCount++; } } if (!empty) { resultText += SEP; } } if (diffCount > 0) { $("differencesheader").innerHTML = "Differences:"; } else { $("differencesheader").innerHTML = "No differences found."; } $("result").innerHTML = resultText; try { if ($("diffarea").style.display == "none") { new Effect.Appear($("diffarea")); } else { new Effect.Highlight($("diffarea")); } } catch (e) { appearHelper($("diffarea")); } } function appearHelper(element, displayType) { element.style.display = displayType ? displayType : "block"; element.style.visibility = "visible"; element.style.opacity = "1.0"; } function hideHelper(element) { element.style.display = "none"; } function homeworkChange() { var select = $("homework"); if (!select.value) { return; } else if (select.value == GRAPHICAL_VALUE) { alert("This assignment uses graphical output.\n" + "To compare the output, go to your DrawingPanel window\n" + "and press \"File\" => \"Compare to Web File...\"."); return; } else if (select.value == OTHER_VALUE) { alert("This assignment doesn't produce output that can be compared by this tool."); return; } /* var optgroup = $(select.options[select.selectedIndex]).up("optgroup"); $("creativestart").checked = optgroup && optgroup.hasClassName("creativestart"); $("creativeend").checked = optgroup && optgroup.hasClassName("creativeend"); */ if (select.className == "useiframe" || (select.value && select.value.startsWith("http") && select.value.indexOf("://") >= 0)) { // special case: a full URL path name. // Can't fetch with Ajax because of security restrictions. // The closest we can come is to inject it into an iframe and let the user // copy/paste it above. $("expectedtext").value = "Because of security restrictions imposed\n" + "by the web browser, you have to copy the text\n" + "from the white box below into the green box above.\n" + "Then you can run the Compare command as usual.\n" + "Sorry for the inconvenience."; var iframe = document.createElement("iframe"); iframe.id = "fetchiframe"; iframe.className = "fetchiframe"; iframe.src = select.value; iframe.style.display = "none"; $("expectedtext").parentNode.insertBefore(iframe, $("expectedtext").nextSibling ? $("expectedtext").nextSibling : $("expectedtext")); new Effect.BlindDown(iframe); } else { // standard case; just fetch the file from the web site using Ajax if ($("fetchiframe")) { new Effect.BlindUp($("fetchiframe")); } //try { // $("loading").appear(); //} catch (e) { appearHelper($("loading"), "inline"); //} $("expectedtext").value = "Downloading..." new Ajax.Request(codebase + select.value, { method: "get", onSuccess: function(ajax) { // got the data correctly; put it onto the page $("expectedtext").value = ajax.responseText; try { $("loading").fade(); } catch (e) { hideHelper($("loading")); } }, parameters: {'nocache' : parseInt(Math.random() * 999999999)}, onFailure: ajaxError, onException: ajaxError }); } } // Fetches data using Ajax and calls the given function when it arrives. function ajaxError(ajax, exception) { hideHelper($("loading")); var errorText = "Error making web request.\n\nServer status:\n" + ajax.status + " " + ajax.statusText + "\n\n" + "Server response text:\n" + ajax.responseText; if (exception) { errorText += "\nException: " + exception.message; } alert(errorText); return errorText; } // A cross-browser way to get the text that is currently selected. function getSelectedText() { var w=window,d=document,gS='getSelection'; return (''+(w[gS]?w[gS]():d[gS]?d[gS]():d.selection.createRange().text)); } function htmlEncode(text) { text = text.replace(//g, ">"); text = text.replace(/ /g, " "); return text; } function ltrim(str) { for (var k = 0; k < str.length && str.charAt(k) <= " "; k++); return str.substring(k, str.length); } function rtrim(str) { for (var j = str.length - 1; j >= 0 && str.charAt(j) <= " "; j--); return str.substring(0, j+1); } function trim(str) { return ltrim(rtrim(str)); } function padL(text, length) { while (text.length < length) { text = " " + text; } return text; } /* Copyright (c) 2006 Tony Garnock-Jones * Copyright (c) 2006 LShift Ltd. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. As an example, the output from diff("the quick brown fox jumped over".split(/\s+/), "the quick fox jumps over".split(/\s+/)) is [{common:["the","quick"]}, {file1:["brown"], file2:[]}, {common:["fox"]}, {file1:["jumped"], file2:["jumps"]}, {common:["over"]} ] */ function betterDiff(file1, file2, hasNumbers) { /* Text diff algorithm following Hunt and McIlroy 1976. * J. W. Hunt and M. D. McIlroy, An algorithm for differential file * comparison, Bell Telephone Laboratories CSTR #41 (1976) * http://www.cs.dartmouth.edu/~doug/ * * Expects two arrays of strings. */ // used to delete line numbers from the starts of lines var start1 = 0; var start2 = 0; if (hasNumbers) { start1 = numDigits(file1) + 2; start2 = numDigits(file2) + 2; } var equivalenceClasses = {}; for (var j = 0; j < file2.length; j++) { var line = file2[j]; if (equivalenceClasses[line.substring(start2)]) { equivalenceClasses[line.substring(start2)].push(j); } else { equivalenceClasses[line.substring(start2)] = [j]; } } var candidates = [{file1index: -1, file2index: -1, chain: null}]; for (var i = 0; i < file1.length; i++) { var line = file1[i]; var file2indices = equivalenceClasses[line.substring(start1)] || []; var r = 0; var c = candidates[0]; for (var jX = 0; jX < file2indices.length; jX++) { var j = file2indices[jX]; for (var s = 0; s < candidates.length; s++) { if ((candidates[s].file2index < j) && ((s == candidates.length - 1) || (candidates[s + 1].file2index > j))) break; } if (s < candidates.length) { var newCandidate = {file1index: i, file2index: j, chain: candidates[s]}; if (r == candidates.length) { candidates.push(c); } else { candidates[r] = c; } r = s + 1; c = newCandidate; if (r == candidates.length) { break; // no point in examining further (j)s } } } candidates[r] = c; } // At this point, we know the LCS: it's in the reverse of the // linked-list through .chain of // candidates[candidates.length - 1]. // We now apply the LCS to build a "comm"-style picture of the // differences between file1 and file2. var result = []; var tail1 = file1.length; var tail2 = file2.length; var common = {common: []}; function processCommon() { if (common.common.length) { common.common.reverse(); result.push(common); common = {common: []}; } } for (var candidate = candidates[candidates.length - 1]; candidate != null; candidate = candidate.chain) { var different = {file1: [], file2: []}; while (--tail1 > candidate.file1index) { different.file1.push(file1[tail1]); } while (--tail2 > candidate.file2index) { different.file2.push(file2[tail2]); } if (different.file1.length || different.file2.length) { processCommon(); different.file1.reverse(); different.file2.reverse(); result.push(different); } if (tail1 >= 0) { common.common.push(file1[tail1].substring(start1)); } } processCommon(); result.reverse(); return result; }