@amaui/diff
Version:
Diff algorithm
324 lines (265 loc) • 9.78 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
import is from '@amaui/utils/is';
import merge from '@amaui/utils/merge';
import stringify from '@amaui/utils/stringify';
const OPTIONS = {
default: {
init: {
method: value => String(value)
},
itemize: {
method: value => value.split('').filter(Boolean)
},
join: {
method: value => value.join('')
},
equal: {
method: (value, value1) => value === value1
}
},
word: {
init: {
method: value => String(value)
},
itemize: {
method: value => value.split(/\s|\b/).filter(Boolean)
},
join: {
method: value => value.join(' ')
}
},
line: {
init: {
method: value => String(value)
},
itemize: {
method: value => {
const values = value.split(/(\n|\r\n)/);
const newValues = [];
for (let i = 0; i < values.length; i++) {
if (i % 2) newValues[newValues.length - 1] += values[i];else newValues.push(values[i]);
}
return newValues;
}
},
join: {
method: value => value.join('')
}
},
sentence: {
init: {
method: value => String(value)
},
itemize: {
method: value => value.split(/(\S.*?[.!?])(?=\s +|$)/).filter(Boolean)
},
join: {
method: value => value.join('')
}
}
};
export const optionsDefault = _objectSpread(_objectSpread({}, OPTIONS.default), {}, {
output: {
compressed: true
}
});
class AmauiDiff {
static get amauidiff() {
return new AmauiDiff();
}
static get word() {
return new AmauiDiff(AmauiDiff.OPTIONS.word);
}
static get line() {
return new AmauiDiff(AmauiDiff.OPTIONS.line);
}
static get sentence() {
return new AmauiDiff(AmauiDiff.OPTIONS.sentence);
}
static get json() {
const options = AmauiDiff.OPTIONS.line;
options.init.method = value => stringify(value);
return new AmauiDiff(options);
}
static get OPTIONS() {
return OPTIONS;
}
static updateGroups(diff) {
const updateGroups = [];
let previousGroup = [];
let lastGroupIsRemove = false;
const items = [];
for (let i = 0; i < diff.items.length;) {
if (['r', 'remove'].indexOf(diff.items[i]) > -1) {
items.push(diff.items.slice(i, i + 2));
i += 2;
} else {
items.push(diff.items.slice(i, i + 3));
i += 3;
}
}
diff.items = items;
diff.items.forEach((item, index) => {
const itemFollowing = diff.items[index + 1];
const action = item[0];
const actionFollow = itemFollowing && itemFollowing[0];
if (['r', 'remove'].indexOf(action) > -1 && ['r', 'remove'].indexOf(actionFollow) > -1 && !lastGroupIsRemove) {
if (previousGroup.length) updateGroups.push(previousGroup);
lastGroupIsRemove = true;
previousGroup = [];
}
if (['a', 'add'].indexOf(action) > -1 && lastGroupIsRemove) {
if (previousGroup.length) updateGroups.push(previousGroup);
lastGroupIsRemove = false;
previousGroup = [];
}
previousGroup.push(item);
});
if (previousGroup.length) updateGroups.push(previousGroup);
return updateGroups;
}
constructor() {
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : optionsDefault;
_defineProperty(this, "options", void 0);
this.options = merge(options, optionsDefault);
}
diff(value_, value1_) {
let options_ = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
const options = merge(options_, this.options);
const value = options.init.method(value_);
const value1 = options.init.method(value1_);
if (is('string', value) && is('string', value1)) {
const valueArrayi = options.itemize.method(value1);
const valueArrayj = options.itemize.method(value);
const lengthi = valueArrayi.length;
const lengthj = valueArrayj.length;
const lengthMax = Math.max(lengthi, lengthj);
const matrix = [];
const lcs = [];
const findItem = item => {
if (item) {
let [i, j] = item.slice(1);
let left = matrix[i] && matrix[i][j - 1];
let top = matrix[i - 1] && matrix[i - 1][j];
let topLeft = matrix[i - 1] && matrix[i - 1][j - 1];
if (left && top && left[0] !== item[0] && top[0] !== item[0]) {
// Enter the new upper left matrix part
[i, j] = topLeft.slice(1);
}
let item_;
while (!item_) {
left = matrix[i] && matrix[i][j - 1];
top = matrix[i - 1] && matrix[i - 1][j];
topLeft = matrix[i - 1] && matrix[i - 1][j - 1];
if (left && left[0] === matrix[i][j][0]) {
[i, j] = left.slice(1);
} else if (top && top[0] === matrix[i][j][0]) {
[i, j] = top.slice(1);
} else {
item_ = matrix[i][j];
break;
}
}
return item_;
}
};
const addItem = (i, j) => {
const left = matrix[i] && matrix[i][j - 1];
const top = matrix[i - 1] && matrix[i - 1][j];
const topLeft = matrix[i - 1] && matrix[i - 1][j - 1];
const match = options.equal.method(valueArrayi[i], valueArrayj[j]);
let item = 0;
if (match) item = (topLeft ? topLeft[0] : 0) + 1;else item = Math.max(top ? top[0] : 0, left ? left[0] : 0);
const item_ = [item, i, j];
return item_;
}; // Make a matrix
for (let i = 0; i < lengthi; i++) {
matrix.push([]);
for (let j = 0; j < lengthj; j++) {
matrix[i][j] = addItem(i, j);
}
} // Make a lcs
const matrixItem = matrix[lengthi - 1][lengthj - 1];
if (matrixItem && matrixItem[0]) {
let item = matrixItem;
let i = lengthi - 1;
let j = lengthj - 1;
const left = matrix[i] && matrix[i][j - 1];
const top = matrix[i - 1] && matrix[i - 1][j];
const topLeft = matrix[i - 1] && matrix[i - 1][j - 1];
if (left && top && left[0] !== item[0] && top[0] !== item[0]) {
lcs.push(item); // Enter the new upper left matrix part
[i, j] = topLeft.slice(1);
}
while (item && item[1] !== 0 && item[2] !== 0) {
item = findItem(item);
lcs.push(item);
}
} // Sort based on value lcs
lcs.sort((a, b) => a[0] - b[0]); // Make a diff items
const diff = {
items: []
};
for (let i = 0; i < lengthMax; i++) {
const iItem = lcs.find(item => item[1] === i);
const jItem = lcs.find(item => item[2] === i);
if (!jItem && i < lengthj) diff.items.push(this.action('remove', i, undefined, options));
if (!iItem && i < lengthi) diff.items.push(this.action('add', i, valueArrayi[i], options));
}
function sortMethod(a, b) {
if (a[0] < b[0]) return 1;
if (a[0] > b[0]) return -1;
return 0;
}
diff.items.sort(sortMethod);
diff.items = diff.items.flat();
return diff;
}
}
update(value__, diff) {
let options_ = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
const options = merge(options_, this.options);
const value_ = options.init.method(value__);
if (is('string', value_) && diff !== undefined) {
var _diff$items;
const value = options.itemize.method(value_);
if ((_diff$items = diff.items) !== null && _diff$items !== void 0 && _diff$items.length) {
const updateGroups = AmauiDiff.updateGroups(diff);
updateGroups.forEach(group => {
const removeGroup = group.every(item => ['r', 'remove'].indexOf(item[0]) > -1);
if (removeGroup) {
group.sort((a, b) => b[1] - a[1]);
group.forEach(item => value.splice(item[1], 1));
} else {
group.forEach(item => {
const action = item[0];
const index = item[1];
const valueItem = item[2];
switch (action) {
case 'a':
case 'add':
value.splice(index, 0, valueItem);
break;
case 'r':
case 'remove':
value.splice(index, 1);
break;
default:
break;
}
});
}
});
}
return options.join.method(value);
}
}
action(action, index, value, options) {
var _options$output;
const compressed = (_options$output = options.output) === null || _options$output === void 0 ? void 0 : _options$output.compressed;
return [compressed ? action.charAt(0) : action, index, value].filter(item => item !== undefined);
}
}
export default AmauiDiff;