@amaui/diff
Version:
Diff algorithm
594 lines (521 loc) • 22 kB
JavaScript
/** @license AmauiDiff v1.0.11141
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.AmauiDiff = factory());
})(this, (function () { 'use strict';
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var global$1 = (typeof global !== "undefined" ? global :
typeof self !== "undefined" ? self :
typeof window !== "undefined" ? window : {});
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var is$1 = {exports: {}};
(function (module, exports) {
Object.defineProperty(exports, "__esModule", { value: true });
const optionsDefault = {};
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
const isNodejs = !!(typeof global$1 !== 'undefined' && 'object' !== 'undefined' && module.exports);
// Multiple is methods instead of one,
// so it's lighter for tree shaking usability reasons
function is(type, value, options_ = {}) {
var _a;
const options = Object.assign(Object.assign({}, optionsDefault), options_);
const { variant } = options;
const prototype = value && typeof value === 'object' && Object.getPrototypeOf(value);
switch (type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !Number.isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'object':
const isObject = typeof value === 'object' && !!value && value.constructor === Object;
return isObject;
// Map, null, WeakMap, Date, etc.
case 'object-like':
return typeof value === 'object' && (value === null || value.constructor !== Object);
case 'class':
return ((typeof value === 'object' || typeof value === 'function') &&
(/class/gi.test(String(value)) || /class/gi.test(String(value === null || value === void 0 ? void 0 : value.constructor))));
case 'function':
return !!(value && value instanceof Function);
case 'async':
// If it's browser avoid calling the method
// to see if it's async func or not,
// where as in nodejs we have no other choice
// that i know of when using transpilation
// And also it might not be always correct, as
// a method that returns a promise is also async
// but we can't know that until the method is called and
// we inspect the method's return value
return !!(is('function', value) && (isBrowser ? value.constructor.name === 'AsyncFunction' : value() instanceof Promise));
case 'map':
return !!(prototype === Map.prototype);
case 'weakmap':
return !!(prototype === WeakMap.prototype);
case 'set':
return !!(prototype === Set.prototype);
case 'weakset':
return !!(prototype === WeakSet.prototype);
case 'promise':
return !!(prototype === Promise.prototype);
case 'int8array':
return !!(prototype === Int8Array.prototype);
case 'uint8array':
return !!(prototype === Uint8Array.prototype);
case 'uint8clampedarray':
return !!(prototype === Uint8ClampedArray.prototype);
case 'int16array':
return !!(prototype === Int16Array.prototype);
case 'uint16array':
return !!(prototype === Uint16Array.prototype);
case 'int32array':
return !!(prototype === Int32Array.prototype);
case 'uint32array':
return !!(prototype === Uint32Array.prototype);
case 'float32array':
return !!(prototype === Float32Array.prototype);
case 'float64array':
return !!(prototype === Float64Array.prototype);
case 'bigint64array':
return !!(prototype === BigInt64Array.prototype);
case 'biguint64array':
return !!(prototype === BigUint64Array.prototype);
case 'typedarray':
return is('int8array', value) || is('uint8array', value) || is('uint8clampedarray', value) || is('int16array', value) || is('uint16array', value) || is('int32array', value) || is('uint32array', value) || is('float32array', value) || is('float64array', value) || is('bigint64array', value) || is('biguint64array', value);
case 'dataview':
return !!(prototype === DataView.prototype);
case 'arraybuffer':
return !!(prototype === ArrayBuffer.prototype);
case 'sharedarraybuffer':
return typeof SharedArrayBuffer !== 'undefined' && !!(prototype === SharedArrayBuffer.prototype);
case 'symbol':
return !!(typeof value === 'symbol');
case 'error':
return !!(value && value instanceof Error);
case 'date':
return !!(value && value instanceof Date);
case 'regexp':
return !!(value && value instanceof RegExp);
case 'arguments':
return !!(value && value.toString() === '[object Arguments]');
case 'null':
return value === null;
case 'undefined':
return value === undefined;
case 'blob':
return isBrowser && value instanceof Blob;
case 'buffer':
return !!(isNodejs && typeof ((_a = value === null || value === void 0 ? void 0 : value.constructor) === null || _a === void 0 ? void 0 : _a.isBuffer) === 'function' && value.constructor.isBuffer(value));
case 'element':
if (value) {
switch (variant) {
case undefined:
case 'html':
case 'element':
return isBrowser && (typeof HTMLElement === 'object' ?
value instanceof HTMLElement :
value && typeof value === 'object' && value !== null && value.nodeType === 1 && typeof value.nodeName === 'string');
case 'node':
return isBrowser && (typeof Node === 'object' ?
value instanceof Node :
value && typeof value === 'object' && value !== null && typeof value.nodeType === 'number' && typeof value.nodeName === 'string');
case 'react':
return value.elementType || value.hasOwnProperty('$$typeof');
default:
return false;
}
}
return false;
case 'simple':
return (is('string', value, options) ||
is('number', value, options) ||
is('boolean', value, options) ||
is('undefined', value, options) ||
is('null', value, options));
case 'not-array-object':
return !is('array', value, options) && !is('object', value, options);
default:
return false;
}
}
exports.default = is;
}(is$1, is$1.exports));
var is = /*@__PURE__*/getDefaultExportFromCjs(is$1.exports);
var merge$1 = {};
var copy$1 = {};
Object.defineProperty(copy$1, "__esModule", { value: true });
const isArray = value => Array.isArray(value);
const isObject = value => typeof value === 'object' && !!value && value.constructor === Object;
// It keeps the references of the methods and classes,
// unlike JSON.stringify usually used for deep simple copy
const copy = (value) => {
if (isArray(value))
return value.map(item => copy(item));
if (isObject(value)) {
const newValue = {};
Object.keys(value).forEach(key => newValue[key] = copy(value[key]));
return newValue;
}
return value;
};
copy$1.default = copy;
var __importDefault = (undefined && undefined.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(merge$1, "__esModule", { value: true });
const is_1 = __importDefault(is$1.exports);
const copy_1 = __importDefault(copy$1);
const optionsDefault$1 = {
copy: false,
merge: {
array: false,
},
};
const merge = (target, source, options_ = {}) => {
const options = Object.assign(Object.assign({}, optionsDefault$1), options_);
if (options.merge.array &&
(0, is_1.default)('array', target) && (0, is_1.default)('array', source)) {
const length = Math.max(target.length, source.length);
for (let i = 0; i < length; i++) {
if (target[i] === undefined)
target[i] = source[i];
if (((0, is_1.default)('object', target[i]) && (0, is_1.default)('object', source[i])) ||
(0, is_1.default)('array', target[i]) && (0, is_1.default)('array', source[i]))
target[i] = merge(target[i], source[i], options);
}
}
if ((0, is_1.default)('object', target) && (0, is_1.default)('object', source)) {
Object.keys(source).forEach(key => {
// We only care about direct target object properties
// not about inherited properties from a prototype chain
if (target.hasOwnProperty(key)) {
if ((0, is_1.default)('object', target[key]) && (0, is_1.default)('object', source[key]))
target[key] = merge(target[key], source[key], options);
}
else
target[key] = options.copy ? (0, copy_1.default)(source[key]) : source[key];
});
}
return target;
};
var _default$1 = merge$1.default = merge;
var stringify$1 = {};
Object.defineProperty(stringify$1, "__esModule", { value: true });
// Fix for undefined
// + ref circular values
const method = () => {
const values = new WeakSet();
return (property, value) => {
// Ref circular values
const getValue = (property_, value_) => {
if (typeof value_ === 'object' && value_ !== null) {
if (values.has(value_))
return;
values.add(value_);
}
return value_;
};
if (getValue(property, value) === undefined ||
value === undefined)
return undefined;
return value;
};
};
const stringify = (value_, spaces = 2, replacer = method()) => {
try {
let value = JSON.stringify(value_, replacer, spaces);
// Array circular ref value update
value = value
// first item
.replace(/(?!\[)\n* *null,/g, '')
// index 1+
.replace(/,\n* *null/g, '');
return value;
}
catch (error) { }
return String(value_);
};
var _default = stringify$1.default = stringify;
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; }
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('')
}
}
};
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 => _default(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 = _default$1(options, optionsDefault);
}
diff(value_, value1_) {
let options_ = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
const options = _default$1(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 = _default$1(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;
}
});
}
});
}
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);
}
}
var AmauiDiff$1 = AmauiDiff;
return AmauiDiff$1;
}));