UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

295 lines 11.7 kB
import * as str from './str'; /** * Aggregate character-by-character patch into a line-by-line patch. * * @param patch Character-level patch * @returns Line-level patch */ export const agg = (patch) => { // console.log(patch); const lines = []; const length = patch.length; let line = []; const push = (type, str) => { if (!str.length) return; const length = line.length; if (length) { const lastOp = line[length - 1]; if (lastOp[0] === type) { lastOp[1] += str; return; } } line.push([type, str]); }; // console.log("PATCH", patch); LINES: for (let i = 0; i < length; i++) { const op = patch[i]; const type = op[0]; const str = op[1]; const index = str.indexOf('\n'); if (index < 0) { push(type, str); continue LINES; } else { push(type, str.slice(0, index + 1)); if (line.length) lines.push(line); line = []; } let prevIndex = index; const strLen = str.length; LINE: while (prevIndex < strLen) { const nextIndex = str.indexOf('\n', prevIndex + 1); if (nextIndex < 0) { push(type, str.slice(prevIndex + 1)); break LINE; } lines.push([[type, str.slice(prevIndex + 1, nextIndex + 1)]]); prevIndex = nextIndex; } } if (line.length) lines.push(line); // console.log("LINES", lines); { const length = lines.length; for (let i = 0; i < length; i++) { const line = (lines[i] = str.normalize(lines[i])); const lineLength = line.length; NORMALIZE_LINE_START: { if (lineLength < 2) break NORMALIZE_LINE_START; const firstOp = line[0]; const secondOp = line[1]; const secondOpType = secondOp[0]; if (firstOp[0] !== 0 /* str.PATCH_OP_TYPE.EQL */) break NORMALIZE_LINE_START; if (secondOpType !== -1 /* str.PATCH_OP_TYPE.DEL */ && secondOpType !== 1 /* str.PATCH_OP_TYPE.INS */) break NORMALIZE_LINE_START; for (let j = 2; j < lineLength; j++) if (line[j][0] !== secondOpType) break NORMALIZE_LINE_START; for (let j = i + 1; j < length; j++) { const targetLine = (lines[j] = str.normalize(lines[j])); const targetLineLength = targetLine.length; const pfx = firstOp[1]; let targetLineFirstOp; let targetLineSecondOp; if (targetLine.length > 1 && (targetLineFirstOp = targetLine[0])[0] === secondOpType && (targetLineSecondOp = targetLine[1])[0] === 0 /* str.PATCH_OP_TYPE.EQL */ && pfx === targetLineFirstOp[1]) { line.splice(0, 1); secondOp[1] = pfx + secondOp[1]; targetLineSecondOp[1] = pfx + targetLineSecondOp[1]; targetLine.splice(0, 1); break NORMALIZE_LINE_START; } else for (let k = 0; k < targetLineLength; k++) if (targetLine[k][0] !== secondOpType) break NORMALIZE_LINE_START; } } NORMALIZE_LINE_END: { /** * Brings forward EQL line ending if equivalent DEL line ending exists * in some following line and all inbetween operations are DEL. * * From: * * ``` * Line 1: [EQL, 'Hell'], [DEL, 'o\n'] * Line 2: [DEL, ' wor'], [DEL, 'ld\n'] * Line 3: [DEL, 'gog'], [EQL, 'o\n'] * ``` * * To: * * ``` * Line 1: [EQL, 'Hello\n'] * Line 2: [DEL, ' wor'], [DEL, 'ld\n'] * Line 3: [DEL, 'gogo\n'] * ``` */ if (line.length < 2) break NORMALIZE_LINE_END; const lastOp = line[line.length - 1]; const lastOpStr = lastOp[1]; if (lastOp[0] !== -1 /* str.PATCH_OP_TYPE.DEL */) break NORMALIZE_LINE_END; NEXT_LINE: for (let j = i + 1; j < length; j++) { const targetLine = (lines[j] = str.normalize(lines[j])); const targetLineLength = targetLine.length; let targetLineLastOp; if (targetLineLength === 0) continue NEXT_LINE; if (targetLineLength === 1) { targetLineLastOp = targetLine[0]; const targetLineLastOpType = targetLineLastOp[0]; if (targetLineLastOpType === -1 /* str.PATCH_OP_TYPE.DEL */) continue NEXT_LINE; if (targetLine[0][0] !== 0 /* str.PATCH_OP_TYPE.EQL */) break NORMALIZE_LINE_END; } else { targetLineLastOp = targetLine[1]; if (targetLineLength > 2) break NORMALIZE_LINE_END; const first = targetLine[0]; if (first[0] !== -1 /* str.PATCH_OP_TYPE.DEL */) break NORMALIZE_LINE_END; } const targetLineLastOpType = targetLineLastOp[0]; if (targetLineLastOpType === -1 /* str.PATCH_OP_TYPE.DEL */) continue NEXT_LINE; if (targetLineLastOpType !== 0 /* str.PATCH_OP_TYPE.EQL */) break NORMALIZE_LINE_END; const moveStr = targetLineLastOp[1]; if (moveStr.length > lastOpStr.length) break NORMALIZE_LINE_END; if (!lastOpStr.endsWith(moveStr)) break NORMALIZE_LINE_END; const index = lastOpStr.length - moveStr.length; lastOp[1] = lastOpStr.slice(0, index); line.push([0 /* str.PATCH_OP_TYPE.EQL */, moveStr]); targetLineLastOp[0] = -1 /* str.PATCH_OP_TYPE.DEL */; lines[i] = str.normalize(lines[i]); lines[j] = str.normalize(lines[j]); break NORMALIZE_LINE_END; } } } } // console.log("NORMALIZED LINES", lines); return lines; }; export const diff = (src, dst) => { if (!dst.length) return src.map((_, i) => [-1 /* LINE_PATCH_OP_TYPE.DEL */, i, -1]); if (!src.length) return dst.map((_, i) => [1 /* LINE_PATCH_OP_TYPE.INS */, -1, i]); const srcTxt = src.join('\n') + '\n'; const dstTxt = dst.join('\n') + '\n'; if (srcTxt === dstTxt) return []; const strPatch = str.diff(srcTxt, dstTxt); const lines = agg(strPatch); const length = lines.length; const patch = []; let srcIdx = -1; let dstIdx = -1; const srcLength = src.length; const dstLength = dst.length; for (let i = 0; i < length; i++) { const line = lines[i]; let lineLength = line.length; if (!lineLength) continue; const lastOp = line[lineLength - 1]; const lastOpType = lastOp[0]; const txt = lastOp[1]; if (txt === '\n') line.splice(lineLength - 1, 1); else { const strLength = txt.length; if (txt[strLength - 1] === '\n') { if (strLength === 1) line.splice(lineLength - 1, 1); else lastOp[1] = txt.slice(0, strLength - 1); } } let lineType = 0 /* LINE_PATCH_OP_TYPE.EQL */; lineLength = line.length; if (!lineLength) { if (lastOpType === 0 /* str.PATCH_OP_TYPE.EQL */) { lineType = 0 /* LINE_PATCH_OP_TYPE.EQL */; srcIdx++; dstIdx++; } else if (lastOpType === 1 /* str.PATCH_OP_TYPE.INS */) { lineType = 1 /* LINE_PATCH_OP_TYPE.INS */; dstIdx++; } else if (lastOpType === -1 /* str.PATCH_OP_TYPE.DEL */) { lineType = -1 /* LINE_PATCH_OP_TYPE.DEL */; srcIdx++; } } else { if (i + 1 === length) { if (srcIdx + 1 < srcLength) { if (dstIdx + 1 < dstLength) { lineType = lineLength === 1 && line[0][0] === 0 /* str.PATCH_OP_TYPE.EQL */ ? 0 /* LINE_PATCH_OP_TYPE.EQL */ : 2 /* LINE_PATCH_OP_TYPE.MIX */; srcIdx++; dstIdx++; } else { lineType = -1 /* LINE_PATCH_OP_TYPE.DEL */; srcIdx++; } } else { lineType = 1 /* LINE_PATCH_OP_TYPE.INS */; dstIdx++; } } else { const op = line[0]; const type = op[0]; if (lineLength === 1 && type === lastOpType && type === 0 /* str.PATCH_OP_TYPE.EQL */) { srcIdx++; dstIdx++; } else if (lastOpType === 0 /* str.PATCH_OP_TYPE.EQL */) { lineType = 2 /* LINE_PATCH_OP_TYPE.MIX */; srcIdx++; dstIdx++; } else if (lastOpType === 1 /* str.PATCH_OP_TYPE.INS */) { lineType = 1 /* LINE_PATCH_OP_TYPE.INS */; dstIdx++; } else if (lastOpType === -1 /* str.PATCH_OP_TYPE.DEL */) { lineType = -1 /* LINE_PATCH_OP_TYPE.DEL */; srcIdx++; } } } if (lineType === 0 /* LINE_PATCH_OP_TYPE.EQL */) { if (src[srcIdx] !== dst[dstIdx]) { lineType = 2 /* LINE_PATCH_OP_TYPE.MIX */; } } patch.push([lineType, srcIdx, dstIdx]); } // console.log("LINE PATCH", patch); return patch; }; export const apply = (patch, onDelete, onInsert, onMix) => { const length = patch.length; LOOP: for (let i = length - 1; i >= 0; i--) { const [type, posSrc, posDst] = patch[i]; switch (type) { case 0 /* LINE_PATCH_OP_TYPE.EQL */: continue LOOP; case -1 /* LINE_PATCH_OP_TYPE.DEL */: onDelete(posSrc); break; case 1 /* LINE_PATCH_OP_TYPE.INS */: onInsert(posSrc, posDst); break; case 2 /* LINE_PATCH_OP_TYPE.MIX */: onMix(posSrc, posDst); break; } } }; //# sourceMappingURL=line.js.map