Skip to content

Instantly share code, notes, and snippets.

@jsomers
Created June 12, 2012 03:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jsomers/2914774 to your computer and use it in GitHub Desktop.
Save jsomers/2914774 to your computer and use it in GitHub Desktop.
A minimal etherpad-like diff patch recorder (with playback)
<!-- TODOS
* More feedback about when the last save was, and what type it was.
* Word count responding to highlights initiated via the keyboard.
-->
<link type="text/css" href="./pad.css" rel="stylesheet" />
<script src="./diff_match_patch.js" type="text/javascript"></script>
<script src="./jquery-1.3.2.min.js" type="text/javascript"></script>
<script src="./field-selection.js" type="text/javascript"></script>
<script src="./file_system.js" type="text/javascript"></script>
<script src="./word_counts.js" type="text/javascript"></script>
<script type="text/javascript">
var dmp = new diff_match_patch(),
original = "",
patchfile,
snapfile,
gist_id;
function diffLoop() {
var a = original, b = $("textarea#pad").val();
if (a != b) {
var patch = dmp.diff_toDelta( dmp.diff_main(a, b) );
var calculatedNextOriginal = dmp.diff_text2( dmp.diff_fromDelta(a, patch) );
fileSystem.overwriteFile(snapfile, calculatedNextOriginal, function() {
fileSystem.appendToFile(patchfile, patch);
original = calculatedNextOriginal;
});
};
setTimeout("diffLoop()", 2000);
};
$(document).ready(function() {
fileSystem.open(function(filesystem) {
fs = filesystem;
patchfile = location.search.split("?p=")[1];
snapfile = patchfile + "-snap";
fileSystem.readFile(patchfile, null, function(e) {
if (e.code == FileError.NOT_FOUND_ERR) {
fileSystem.createFile(patchfile, "");
} else { fileSystem.errorHandler(e) };
});
var initializePad = function(snapfileContents) {
original = snapfileContents;
$("textarea#pad").val(snapfileContents);
};
fileSystem.readFile(snapfile, initializePad, function(e) {
if (e.code == FileError.NOT_FOUND_ERR) {
fileSystem.createFile(snapfile, "");
} else { fileSystem.errorHandler(e) };
});
diffLoop();
});
$("textarea#pad").focus();
$("textarea#pad").keyup(function() {
count_words($("textarea#pad").val().trim());
});
$("textarea#pad").mouseup(function() {
var range = $(this).getSelection();
if (range.text != "") {
count_words(range.text);
} else {
count_words($("textarea#pad").val().trim());
};
});
$("#save_gist").click(function () {
var filename = pad + ".markdown";
var data = {
"public": false,
"files": {
filename: {
"content": $("textarea#pad").val()
}
}
};
var url = (gist_id == null) ? "https://api.github.com/gists" : "https://api.github.com/gists/" + gist_id;
var callback = function(gist) {
gist_id = gist.id;
$("#gist span").html($("<a>" + gist_id + "</a>").attr("href", "https://gist.github.com/" + gist_id).attr("target", "_blank"));
alert("Saved");
};
$.ajax({
type: 'POST',
url: url,
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/x-www-form-urlencoded',
success: callback,
beforeSend : function(xhr) {
xhr.setRequestHeader("Authorization",
"Basic " + btoa('[username]:[password]'));
}
});
return false;
});
});
</script>
<textarea id="pad"></textarea>
<div id="word_count">(<span>0</span> words)</div>
<div id="gist"><span></span> [<a href="#" id="save_gist">Gist</a>]</div>
var fs;
function toArray(list) {
return Array.prototype.slice.call(list || [], 0);
};
fileSystem = {
errorHandler: function errorHandler(e) {
var msg = '';
switch (e.code) {
case FileError.QUOTA_EXCEEDED_ERR:
msg = 'QUOTA_EXCEEDED_ERR';
break;
case FileError.NOT_FOUND_ERR:
msg = 'NOT_FOUND_ERR';
break;
case FileError.SECURITY_ERR:
msg = 'SECURITY_ERR';
break;
case FileError.INVALID_MODIFICATION_ERR:
msg = 'INVALID_MODIFICATION_ERR';
break;
case FileError.INVALID_STATE_ERR:
msg = 'INVALID_STATE_ERR';
break;
default:
msg = 'Unknown Error';
break;
};
console.log('Error: ' + msg);
},
storageLeft: function storageLeft() {
window.webkitStorageInfo.queryUsageAndQuota(webkitStorageInfo.PERSISTENT,
function(used, remaining) {
console.log("Used quota: " + used + ", remaining quota: " + remaining);
}, function(e) {
console.log('Error', e);
} );
},
requestBytes: function requestBytes(b) {
window.webkitStorageInfo.requestQuota(PERSISTENT, b, function(grantedBytes) {
window.requestFileSystem(PERSISTENT, grantedBytes, fileSystem.onInitFs, fileSystem.errorHandler);
}, function(e) {
console.log('Error', e);
});
},
onInitFs: function onInitFs(filesystem) {
fs = filesystem;
},
open: function open(fn) {
window.webkitRequestFileSystem(window.PERSISTENT, 2*1024*1024*1024 /* 2GB */, fn || fileSystem.onInitFs, fileSystem.errorHandler);
},
mkdir: function mkdir(dir) {
fs.root.getDirectory(dir, {create: true}, function(dirEntry) {
console.log(dirEntry);
}, fileSystem.errorHandler)
},
listResults: function listResults(entries) {
entries.forEach(function(entry, i) {
if (!entry.isDirectory) {
var a = $("<a>").attr("href", "/ether.html?p=" + entry.name).html(entry.name);
$("ul#files").append($("<li>").html(a));
};
});
},
ls: function ls(dir) {
var dirReader = fs.root.createReader();
var entries = [];
// Call the reader.readEntries() until no more results are returned.
var readEntries = function() {
dirReader.readEntries (function(results) {
if (!results.length) {
fileSystem.listResults(entries.sort());
} else {
entries = entries.concat(toArray(results));
readEntries();
}
}, fileSystem.errorHandler);
};
readEntries(); // Start reading dirs.
},
createFile: function createFile(filename, contents, callback) {
fs.root.getFile(filename, {create: true, exclusive: true}, function(fileEntry) {
fileEntry.createWriter(function(fileWriter) {
fileWriter.onwriteend = function(txt) {
console.log(filename + " successfully written.");
if (callback)
callback();
};
fileWriter.onerror = function(e) {
console.log('Write failed: ' + e.toString());
};
// Create a new Blob and write it to the file.
var bb = new window.WebKitBlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
bb.append(contents);
fileWriter.write(bb.getBlob('text/plain'));
}, fileSystem.errorHandler);
}, fileSystem.errorHandler);
},
appendToFile: function appendToFile(filename, contents) {
console.log("Appending to " + filename);
fs.root.getFile(filename, {create: false}, function(fileEntry) {
fileEntry.createWriter(function(fileWriter) {
fileWriter.seek(fileWriter.length); // Start write position at EOF.
var bb = new window.WebKitBlobBuilder();
bb.append("\n" + contents);
fileWriter.write(bb.getBlob('text/plain'));
}, fileSystem.errorHandler);
}, fileSystem.errorHandler);
},
readFile: function readFile(filename, successCallback, errorCallback) {
console.log(successCallback);
fs.root.getFile(filename, {}, function(fileEntry) {
fileEntry.file(function(file) {
var reader = new FileReader();
reader.onloadend = function(e) {
if (successCallback) {
successCallback(this.result);
} else {
console.log(this.result);
}
};
reader.readAsText(file);
}, errorCallback);
}, errorCallback);
},
overwriteFile: function overwriteFile(filename, contents, callback) {
fs.root.getFile(filename, {create: false}, function(fileEntry) {
fileEntry.remove(function() {
console.log(filename + " deleted.")
fileSystem.createFile(filename, contents, callback);
}, fileSystem.errorHandler);
}, fileSystem.errorHandler);
}
};
body {
background: black;
}
textarea {
border: none;
font-family: Monaco, courier;
display: block;
margin:0 auto;
margin-top: 120px;
margin-bottom: 20px;
font-size: 12.5px;
line-height: 1.5em;
width: 730px;
height: 100%;
outline: none;
color: #66ff44;
background: black;
}
#word_count {
position: absolute;
font-family: Arial;
bottom: 10px;
left: 10px;
font-size: 11px;
color: #66ff44;
}
#gist {
position: absolute;
font-family: Arial;
bottom: 0px;
right: 0px;
font-size: 11px;
background: black;
padding-right: 10px;
padding-bottom: 10px;
padding-top: 10px;
}
#gist, #gist a {
color: #66ff44;
}
#pad::-webkit-scrollbar {
height: 16px;
overflow: visible;
width: 16px;
}
#pad::-webkit-scrollbar-thumb {
background-color: #444;
background-clip: padding-box;
border: solid transparent;
min-height: 28px;
padding: 100px 0 0;
box-shadow: inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);
border-width: 1px 1px 1px 6px;
}
#pad::-webkit-scrollbar-button {
height: 0;
width: 0;
}
<!--
* There should be a page where I can:
(a) Create a new pad
(b) Load an existing pad from the browser FileSystem
(c) Load an existing pad from a Gist
(d) Play back an existing pad from the browser FileSystem (only available for those for which we've got a patch file)
(e) Play back an uploaded patch file
* Make playback more robust, so that if we get these diffing errors we're not fucked. Or try to make it so that we can never get them in the first place.
-->
<link type="text/css" href="./jquery-ui-1.7.3.custom.css" rel="stylesheet" />
<script src="./diff_match_patch.js" type="text/javascript"></script>
<script src="./showdown.js" type="text/javascript"></script>
<script src="./jquery-1.3.2.min.js" type="text/javascript"></script>
<script src="./jquery-ui-1.7.3.custom.min.js" type="text/javascript"></script>
<script src="./file_system.js" type="text/javascript"></script>
<script src="./field-selection.js" type="text/javascript"></script>
<script src="./word_counts.js" type="text/javascript"></script>
<script type="text/javascript">
var dmp = new diff_match_patch(),
converter = new Showdown.converter(),
diffs = [];
$(document).ready(function() {
fileSystem.open(function(filesystem) {
fs = filesystem;
var pad = location.search.split("?p=")[1];
fileSystem.readFile(pad, function(serializedPatches) {
explodePatches( serializedPatches.split("\n") );
$("#seeker").slider({
min: 0,
max: diffs.length - 1,
slide: function(event, ui) { seekPatch(ui.value) }
});
$("#seeker").slider("value", diffs.length - 1);
seekPatch(diffs.length - 1);
});
});
$("#output").mouseup(function() {
var sel = window.getSelection();
if ("" + sel != "") {
count_words("" + sel);
} else {
count_words($("#output").text().trim());
};
});
});
var explodePatches = function explodePatches(patches) {
var s = "";
for (var i in patches) {
var patch = patches[i];
console.log(patch);
d = dmp.diff_fromDelta(s, patch);
diffs.push(d);
s = dmp.diff_text2(d);
};
};
var displayText = function displayText(txt) {
var html = converter.makeHtml(txt);
$("#output").html(html);
};
var seekPatch = function seekPatch(i) {
var diff = diffs[i];
var txt = dmp.diff_text2(diff);
count_words(txt);
displayText(txt);
};
</script>
<style type="text/css">
body {
font-family: Arial;
font-size: 14px;
line-height: 1.3em;
}
#seeker_container {
background: white;
padding: 20px 0px 10px 0px;
position: fixed;
top: 0px;
left: 50%;
margin-left: -376px;
border-bottom: 1px solid #eee;
}
#seeker {
font-size: 11px;
width: 100%;
width: 750px;
}
#output_container {
margin:0 auto;
margin-top: 40px;
margin-bottom: 20px;
width: 730px;
min-height: 500px;
border: 1px solid #eee;
border-top: none;
padding: 10px 10px 10px 10px;
}
#output ul li, #output ol li {
margin-top: 5px;
}
#output blockquote {
border-left: 3px solid #eee;
padding-left: 10px;
color: #666;
}
#word_count {
position: fixed;
font-family: Arial;
bottom: 10px;
left: 10px;
font-size: 11px;
}
</style>
<div id="word_count">(<span>0</span> words)</div>
<div id="seeker_container">
<div id="seeker"></div>
</div>
<div id="output_container">
<div id="output">
</div>
if (typeof(String.prototype.trim) === "undefined") {
String.prototype.trim = function() {
return String(this).replace(/^\s+|\s+$/g, '');
};
};
function number_with_commas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
function count_words(words) {
var ct = (words == "") ? 0 : words.split(/[\s\.\?]+/).length;
$("#word_count span").html(number_with_commas(ct));
};
@jtarchie
Copy link

5.Use gist to store the diffs, have text history of changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment