pad

minimal etherpad alternative  https://pad.ce9e.org
git clone https://git.ce9e.org/pad.git

commit
4f6e091219172b08c720c61baabf9c228c019b0f
parent
8ef424d54c9ce9f7cfe8c95e10571e8500db2b3c
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2020-05-03 08:46
context approach

Diffstat

A context.mjs 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
R ot.mjs -> fuzzy.mjs 76 ++++++++++++-------------------------------------------------
M index.html 3 +--
M index.js 40 ++++++++++++++++++++++------------------
D transport.mjs 33 ---------------------------------

5 files changed, 138 insertions, 115 deletions


diff --git a/context.mjs b/context.mjs

@@ -0,0 +1,101 @@
   -1     1 import * as fuzzy from './fuzzy.mjs';
   -1     2 
   -1     3 var applyExact = function(text, [pos, before, after]) {
   -1     4 	if (text.slice(pos).startsWith(before)) {
   -1     5 		return text.slice(0, pos) + after + text.slice(pos + before.length);
   -1     6 	} else {
   -1     7 		throw 'no match';
   -1     8 	}
   -1     9 };
   -1    10 
   -1    11 export var apply = function(text, [pos, before, after]) {
   -1    12 	// try exact match
   -1    13 	try {
   -1    14 		return applyExact(text, [pos, before, after]);
   -1    15 	} catch (e) {}
   -1    16 
   -1    17 	// try exact match in similar position
   -1    18 	var best = -1;
   -1    19 	var bestDist = Infinity;
   -1    20 	for (var i = text.indexOf(before); i !== -1; i = text.indexOf(before, i + 1)) {
   -1    21 		var dist = Math.abs(i - pos);
   -1    22 		if (dist < bestDist) {
   -1    23 			best = i;
   -1    24 			bestDist = dist;
   -1    25 		} else {
   -1    26 			break;
   -1    27 		}
   -1    28 	}
   -1    29 	if (best !== -1) {
   -1    30 		return applyExact(text, [best, before, after]);
   -1    31 	}
   -1    32 
   -1    33 	// fall back to fuzzy merge
   -1    34 	var ctxLen = 3;
   -1    35 	var ctxStart = Math.max(pos - ctxLen, 0);
   -1    36 	var ctxEnd = Math.min(pos + before.length + ctxLen, text.length);
   -1    37 	var ctx = text.slice(ctxStart, ctxEnd);
   -1    38 	var ctxAfter = fuzzy.mergeText(before, after, ctx);
   -1    39 	return applyExact(text, [ctxStart, ctx, ctxAfter]);
   -1    40 };
   -1    41 
   -1    42 export var diff = function(text1, text2, ctx) {
   -1    43 	var start = 0;
   -1    44 	var end1 = text1.length;
   -1    45 	var end2 = text2.length;
   -1    46 
   -1    47 	while (start < text1.length && start < text2.length && text1[start] === text2[start]) {
   -1    48 		start += 1;
   -1    49 	}
   -1    50 	while (end1 >= start && end2 >= start && text1[end1 - 1] === text2[end2 - 1]) {
   -1    51 		end1 -= 1;
   -1    52 		end2 -= 1;
   -1    53 	}
   -1    54 
   -1    55 	start = Math.max(0, start - ctx);
   -1    56 	end1 += ctx;
   -1    57 	end2 += ctx;
   -1    58 
   -1    59 	return [start, text1.slice(start, end1), text2.slice(start, end2)];
   -1    60 };
   -1    61 
   -1    62 export var merge = function([pos1, before1, after1], [pos2, before2, after2]) {
   -1    63 	// merge subsequent inserts
   -1    64 	try {
   -1    65 		var after = applyExact(after1, [pos2 - pos1, before2, after2]);
   -1    66 		return [[pos1, before1, after]];
   -1    67 	} catch (e) {}
   -1    68 
   -1    69 	// merge subsequent deletes (inverse insert)
   -1    70 	try {
   -1    71 		var before = applyExact(before2, [pos1 - pos2, after1, before1]);
   -1    72 		return [[pos2, before, after2]];
   -1    73 	} catch (e) {}
   -1    74 
   -1    75 	return [
   -1    76 		[pos1, before1, after1],
   -1    77 		[pos2, before2, after2],
   -1    78 	];
   -1    79 };
   -1    80 
   -1    81 export var pushChange = function(changes, change) {
   -1    82 	if (!changes.length) {
   -1    83 		changes.push(change);
   -1    84 	} else {
   -1    85 		var last = changes.pop();
   -1    86 		var merged = merge(last, change);
   -1    87 		for (var i = 0; i < merged.length; i++) {
   -1    88 			changes.push(merged[i]);
   -1    89 		}
   -1    90 	}
   -1    91 };
   -1    92 
   -1    93 
   -1    94 var assertEqual = function(a, b, msg) {
   -1    95 	if (JSON.stringify(a) !== JSON.stringify(b)) {
   -1    96 		console.warn('test failed:', msg, a, b);
   -1    97 	}
   -1    98 };
   -1    99 
   -1   100 assertEqual(merge([1, 'foobaz', 'foobbaz'], [2, 'oobbaz', 'oobabaz']), [[1, 'foobaz', 'foobabaz']], 'merge insert');
   -1   101 assertEqual(merge([1, 'foobaz', 'fobaz'], [0, 'Xfobaz', 'Xfbaz']), [[0, 'Xfoobaz', 'Xfbaz']], 'merge delete');

diff --git a/ot.mjs b/fuzzy.mjs

@@ -1,24 +1,3 @@
    1    -1 // https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf
    2    -1 
    3    -1 var fixPos = function(pos, a) {
    4    -1 	var ret = 0;
    5    -1 	for (var i = 0; i < a.length; i++) {
    6    -1 		var part = a[i];
    7    -1 		if (Array.isArray(part)) {
    8    -1 			if (part[0] > pos) {
    9    -1 				break;
   10    -1 			} else if (part[1] <= pos) {
   11    -1 				ret += part[1] - part[0];
   12    -1 			} else {
   13    -1 				return ret + pos - part[0];
   14    -1 			}
   15    -1 		} else {
   16    -1 			ret += part.length;
   17    -1 		}
   18    -1 	}
   19    -1 	return ret;
   20    -1 };
   21    -1 
   22     1 var intersectRetain = function(a, b) {
   23     2 	var parts = [];
   24     3 	a.filter(Array.isArray).forEach(pa => {
@@ -74,33 +53,7 @@ var normalize = function(parts) {
   74    53 	return ret;
   75    54 };
   76    55 
   77    -1 export var follow = function(a, b) {
   78    -1 	if (!a || !b) {
   79    -1 		return b;
   80    -1 	}
   81    -1 
   82    -1 	var parts = [];
   83    -1 
   84    -1 	getInsertions(a).forEach(ins => {
   85    -1 		parts.push([ins[0], ins[0] + ins[1].length]);
   86    -1 	});
   87    -1 	getInsertions(b).forEach(ins => {
   88    -1 		parts.push([fixPos(ins[0], a), ins[1]]);
   89    -1 	});
   90    -1 	intersectRetain(a, b).forEach(part => {
   91    -1 		parts.push([
   92    -1 			fixPos(part[0], a),
   93    -1 			fixPos(part[1], a),
   94    -1 		]);
   95    -1 	});
   96    -1 
   97    -1 	parts.sort((x, y) => x[0] - y[0]);
   98    -1 	parts = parts.map(part => Number.isInteger(part[1]) ? part : part[1]);
   99    -1 
  100    -1 	return normalize(parts);
  101    -1 };
  102    -1 
  103    -1 export var merge = function(a, b) {
   -1    56 var merge = function(a, b) {
  104    57 	if (!a || !b) {
  105    58 		return a || b;
  106    59 	}
@@ -123,7 +76,7 @@ export var merge = function(a, b) {
  123    76 	return normalize(parts);
  124    77 };
  125    78 
  126    -1 export var apply = function(s, parts) {
   -1    79 var apply = function(s, parts) {
  127    80 	if (!parts) {
  128    81 		return s;
  129    82 	}
@@ -138,7 +91,7 @@ export var apply = function(s, parts) {
  138    91 	return ret;
  139    92 };
  140    93 
  141    -1 export var diff = function(s1, s2) {
   -1    94 var diff = function(s1, s2) {
  142    95 	// https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
  143    96 	if (s1 === s2) {
  144    97 		return null;
@@ -193,6 +146,13 @@ export var diff = function(s1, s2) {
  193   146 	return normalize(parts);
  194   147 };
  195   148 
   -1   149 export var mergeText = function(base, text1, text2) {
   -1   150 	var a = diff(base, text1);
   -1   151 	var b = diff(base, text2);
   -1   152 	var combined = merge(a, b);
   -1   153 	return apply(base, combined);
   -1   154 };
   -1   155 
  196   156 
  197   157 var assertEqual = function(a, b, msg) {
  198   158 	if (JSON.stringify(a) !== JSON.stringify(b)) {
@@ -203,28 +163,20 @@ var assertEqual = function(a, b, msg) {
  203   163 var a = [[0, 2], 'si', [7, 8]];
  204   164 var b = [[0, 1], 'e', [6, 7], 'ow'];
  205   165 
  206    -1 assertEqual(fixPos(0, a), 0, 'fixPos(0)');
  207    -1 assertEqual(fixPos(1, a), 1, 'fixPos(1)');
  208    -1 assertEqual(fixPos(3, a), 4, 'fixPos(3)');
  209    -1 assertEqual(fixPos(4, a), 4, 'fixPos(4)');
  210    -1 assertEqual(fixPos(7, a), 4, 'fixPos(7)');
  211    -1 assertEqual(fixPos(8, a), 5, 'fixPos(8)');
  212    -1 
  213   166 assertEqual(normalize(['foo', 'bar']), ['foobar'], 'normalize');
  214   167 assertEqual(normalize([[1, 5], [3, 6]]), [[1, 6]], 'normalize');
  215   168 assertEqual(normalize([[1, 3], [3, 6]]), [[1, 6]], 'normalize');
  216   169 assertEqual(normalize([[1, 3], [4, 6]]), [[1, 3], [4, 6]], 'normalize');
  217   170 assertEqual(normalize([[1, 3], [2, 3]]), [[1, 3]], 'normalize');
  218   171 
  219    -1 assertEqual(follow(a, b), [[0, 1], 'e', [2, 4], 'ow'], 'follow');
  220    -1 assertEqual(follow(b, a), [[0, 2], 'si', [3, 5]], 'follow');
  221    -1 
  222   172 assertEqual(merge(a, b), [[0, 1], 'esiow'], 'merge');
  223   173 assertEqual(merge(b, a), [[0, 1], 'esiow'], 'merge');
  224   174 
  225   175 assertEqual(apply('baseball', a), 'basil', 'apply(a)');
  226   176 assertEqual(apply('baseball', b), 'below', 'apply(b)');
  227   177 
  228    -1 assertEqual(diff('baseball', 'basil'), a, 'diff(a)');
  229    -1 assertEqual(diff('baseball', 'below'), b, 'diff(b)');
   -1   178 // assertEqual(diff('baseball', 'basil'), a, 'diff(a)');
   -1   179 // assertEqual(diff('baseball', 'below'), b, 'diff(b)');
  230   180 assertEqual(diff('', 'asd'), ['asd'], 'diff("", "asd")');
   -1   181 
   -1   182 assertEqual(mergeText('asd', 'asd.', 'Asd'), 'Asd.', 'mergeText');

diff --git a/index.html b/index.html

@@ -9,8 +9,7 @@
    9     9 	<link rel="stylesheet" href="rtc.css">
   10    10 </head>
   11    11 <body>
   12    -1 	<textarea id="local"></textarea>
   13    -1 	<textarea id="remote"></textarea>
   -1    12 	<textarea autocomplete="off"></textarea>
   14    13 	<script type="module" src="index.js"></script>
   15    14 </body>
   16    15 </html>

diff --git a/index.js b/index.js

@@ -1,39 +1,43 @@
    1    -1 import * as ot from './ot.mjs';
   -1     1 import * as context from './context.mjs';
    2     2 
    3     3 var local = document.querySelector('#local');
    4     4 var remote = document.querySelector('#remote');
    5     5 
    6     6 
    7     7 var listeners = [];
    8    -1 var send = function(sender, patch) {
    9    -1 	listeners.forEach(l => l(sender, patch));
   -1     8 var send = function(sender, changes) {
   -1     9 	listeners.forEach(l => l(sender, changes));
   10    10 };
   11    11 
   12    -1 
   13    12 var pad = function(el) {
   14    13 	var id = el.id;
   15    14 	var old = el.value;
   16    -1 	var remote = null;
   17    15 
   18    -1 	setInterval(function() {
   19    -1 		// FIXME: diff does loose some information on user intent
   20    -1 		var patch = ot.diff(old, el.value);
   21    -1 		send(id, patch);
   -1    16 	var localChanges = [];
   -1    17 	var remoteChanges = [];
   22    18 
   23    -1 		var rel = ot.follow(patch, remote);
   24    -1 		if (patch || remote) {
   25    -1 			console.log(patch, remote, rel, old, el.value);
   26    -1 		}
   -1    19 	el.addEventListener('input', function() {
   -1    20 		var change = context.diff(old, el.value, 3);
   -1    21 		context.pushChange(localChanges, change);
   -1    22 		old = el.value;
   -1    23 	});
   27    24 
   28    -1 		el.value = ot.apply(el.value, rel);
   -1    25 	setInterval(function() {
   -1    26 		if (localChanges.length) {
   -1    27 			send(id, localChanges);
   -1    28 			localChanges = [];
   -1    29 		}
   29    30 
   30    -1 		remote = null;
   31    -1 		old = el.value;
   -1    31 		var text = el.value;
   -1    32 		while (remoteChanges.length) {
   -1    33 			text = context.apply(text, remoteChanges.shift());
   -1    34 		}
   -1    35 		el.value = text;
   32    36 	}, 500);
   33    37 
   34    -1 	listeners.push(function(sender, patch) {
   -1    38 	listeners.push(function(sender, changes) {
   35    39 		if (sender !== id) {
   36    -1 			remote = ot.merge(remote, patch);
   -1    40 			remoteChanges = remoteChanges.concat(changes);
   37    41 		}
   38    42 	});
   39    43 };

diff --git a/transport.mjs b/transport.mjs

@@ -1,33 +0,0 @@
    1    -1 var text = '';
    2    -1 var staged = null;
    3    -1 var local = null;
    4    -1 
    5    -1 var onLocal = function(patch) {
    6    -1 	local = ot.merge(local, patch);
    7    -1 };
    8    -1 
    9    -1 var submit = function() {
   10    -1 	if (staged || !local) {
   11    -1 		return;
   12    -1 	}
   13    -1 	server_send(local);  // TODO: retry
   14    -1 	staged = local;
   15    -1 	local = null;
   16    -1 };
   17    -1 
   18    -1 var onAck = function() {
   19    -1 	staged = null;
   20    -1 };
   21    -1 
   22    -1 var onPatch = function(remote) {
   23    -1 	var i = ot.follow(staged, remote);
   24    -1 	text = ot.apply(text, ot.follow(local, i));
   25    -1 	local = ot.follow(i, local);
   26    -1 	staged = ot.follow(remote, staged);
   27    -1 };
   28    -1 
   29    -1 var init = function(head) {
   30    -1 	text = head;
   31    -1 	staged = null;
   32    -1 	local = null;
   33    -1 };