`);
}
- rootEnd(context) {
+ rootEnd(context: HtmlFormatterContext) {
context.out(
`
${
context.hasArrows
@@ -55,7 +76,13 @@ class HtmlFormatter extends BaseFormatter {
);
}
- nodeBegin(context, key, leftKey, type, nodeType) {
+ nodeBegin(
+ context: HtmlFormatterContext,
+ key: string,
+ leftKey: string | number,
+ type: DeltaType,
+ nodeType: NodeType,
+ ) {
const nodeClass = `jsondiffpatch-${type}${
nodeType ? ` jsondiffpatch-child-node-type-${nodeType}` : ''
}`;
@@ -65,14 +92,18 @@ class HtmlFormatter extends BaseFormatter {
);
}
- nodeEnd(context) {
+ nodeEnd(context: HtmlFormatterContext) {
context.out('');
}
/* jshint camelcase: false */
/* eslint-disable camelcase */
- format_unchanged(context, delta, left) {
+ format_unchanged(
+ context: HtmlFormatterContext,
+ delta: undefined,
+ left: unknown,
+ ) {
if (typeof left === 'undefined') {
return;
}
@@ -81,7 +112,11 @@ class HtmlFormatter extends BaseFormatter {
context.out('');
}
- format_movedestination(context, delta, left) {
+ format_movedestination(
+ context: HtmlFormatterContext,
+ delta: undefined,
+ left: unknown,
+ ) {
if (typeof left === 'undefined') {
return;
}
@@ -90,7 +125,11 @@ class HtmlFormatter extends BaseFormatter {
context.out('');
}
- format_node(context, delta, left) {
+ format_node(
+ context: HtmlFormatterContext,
+ delta: ObjectDelta | ArrayDelta,
+ left: unknown,
+ ) {
// recurse
const nodeType = delta._t === 'a' ? 'array' : 'object';
context.out(
@@ -100,13 +139,13 @@ class HtmlFormatter extends BaseFormatter {
context.out('');
}
- format_added(context, delta) {
+ format_added(context: HtmlFormatterContext, delta: AddedDelta) {
context.out('');
this.formatValue(context, delta[0]);
context.out(
@@ -116,13 +155,13 @@ class HtmlFormatter extends BaseFormatter {
context.out('
');
}
- format_deleted(context, delta) {
+ format_deleted(context: HtmlFormatterContext, delta: DeletedDelta) {
context.out('');
this.formatValue(context, delta[0]);
context.out(
@@ -153,14 +192,14 @@ class HtmlFormatter extends BaseFormatter {
context.hasArrows = true;
}
- format_textdiff(context, delta) {
+ format_textdiff(context: HtmlFormatterContext, delta: TextDiffDelta) {
context.out('
');
this.formatTextDiffString(context, delta[0]);
context.out('
');
}
}
-function htmlEscape(text) {
+function htmlEscape(text: string) {
let html = text;
const replacements = [
[/&/g, '&'],
@@ -168,24 +207,33 @@ function htmlEscape(text) {
[/>/g, '>'],
[/'/g, '''],
[/"/g, '"'],
- ];
+ ] as const;
for (let i = 0; i < replacements.length; i++) {
html = html.replace(replacements[i][0], replacements[i][1]);
}
return html;
}
-const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(nodeArg) {
+const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(
+ nodeArg?: Element,
+) {
const node = nodeArg || document;
- const getElementText = ({ textContent, innerText }) =>
+ const getElementText = ({ textContent, innerText }: HTMLDivElement) =>
textContent || innerText;
- const eachByQuery = (el, query, fn) => {
+ const eachByQuery = (
+ el: Element | Document,
+ query: string,
+ fn: (element: HTMLElement) => void,
+ ) => {
const elems = el.querySelectorAll(query);
for (let i = 0, l = elems.length; i < l; i++) {
- fn(elems[i]);
+ fn(elems[i] as HTMLDivElement);
}
};
- const eachChildren = ({ children }, fn) => {
+ const eachChildren = (
+ { children }: ParentNode,
+ fn: (child: Element, index: number) => void,
+ ) => {
for (let i = 0, l = children.length; i < l; i++) {
fn(children[i], i);
}
@@ -194,18 +242,18 @@ const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(nodeArg) {
node,
'.jsondiffpatch-arrow',
({ parentNode, children, style }) => {
- const arrowParent = parentNode;
- const svg = children[0];
- const path = svg.children[1];
+ const arrowParent = parentNode as HTMLElement;
+ const svg = children[0] as SVGSVGElement;
+ const path = svg.children[1] as SVGPathElement;
svg.style.display = 'none';
const destination = getElementText(
- arrowParent.querySelector('.jsondiffpatch-moved-destination'),
+ arrowParent.querySelector('.jsondiffpatch-moved-destination')!,
);
- const container = arrowParent.parentNode;
- let destinationElem;
+ const container = arrowParent.parentNode!;
+ let destinationElem: HTMLElement | undefined;
eachChildren(container, (child) => {
if (child.getAttribute('data-key') === destination) {
- destinationElem = child;
+ destinationElem = child as HTMLElement;
}
});
if (!destinationElem) {
@@ -213,7 +261,7 @@ const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(nodeArg) {
}
try {
const distance = destinationElem.offsetTop - arrowParent.offsetTop;
- svg.setAttribute('height', Math.abs(distance) + 6);
+ svg.setAttribute('height', `${Math.abs(distance) + 6}`);
style.top = `${-8 + (distance > 0 ? 0 : distance)}px`;
const curve =
distance > 0
@@ -229,7 +277,11 @@ const adjustArrows = function jsondiffpatchHtmlFormatterAdjustArrows(nodeArg) {
/* jshint camelcase: true */
/* eslint-enable camelcase */
-export const showUnchanged = (show, node, delay) => {
+export const showUnchanged = (
+ show?: boolean,
+ node?: Element,
+ delay?: number,
+) => {
const el = node || document.body;
const prefix = 'jsondiffpatch-unchanged-';
const classes = {
@@ -283,13 +335,14 @@ export const showUnchanged = (show, node, delay) => {
}, delay);
};
-export const hideUnchanged = (node, delay) => showUnchanged(false, node, delay);
+export const hideUnchanged = (node?: Element, delay?: number) =>
+ showUnchanged(false, node, delay);
export default HtmlFormatter;
-let defaultInstance;
+let defaultInstance: HtmlFormatter | undefined;
-export function format(delta, left) {
+export function format(delta: Delta, left: unknown) {
if (!defaultInstance) {
defaultInstance = new HtmlFormatter();
}
diff --git a/src/formatters/index.js b/src/formatters/index.ts
similarity index 100%
rename from src/formatters/index.js
rename to src/formatters/index.ts
diff --git a/src/formatters/jsonpatch.js b/src/formatters/jsonpatch.js
deleted file mode 100644
index ebc28664..00000000
--- a/src/formatters/jsonpatch.js
+++ /dev/null
@@ -1,180 +0,0 @@
-import BaseFormatter from './base';
-
-const OPERATIONS = {
- add: 'add',
- remove: 'remove',
- replace: 'replace',
- move: 'move',
-};
-
-class JSONFormatter extends BaseFormatter {
- constructor() {
- super();
- this.includeMoveDestinations = true;
- }
-
- prepareContext(context) {
- super.prepareContext(context);
- context.result = [];
- context.path = [];
- context.pushCurrentOp = function (obj) {
- const { op, value } = obj;
- const val = {
- op,
- path: this.currentPath(),
- };
- if (typeof value !== 'undefined') {
- val.value = value;
- }
- this.result.push(val);
- };
-
- context.pushMoveOp = function (to) {
- const from = this.currentPath();
- this.result.push({
- op: OPERATIONS.move,
- from,
- path: this.toPath(to),
- });
- };
-
- context.currentPath = function () {
- return `/${this.path.join('/')}`;
- };
-
- context.toPath = function (toPath) {
- const to = this.path.slice();
- to[to.length - 1] = toPath;
- return `/${to.join('/')}`;
- };
- }
-
- typeFormattterErrorFormatter(context, err) {
- context.out(`[ERROR] ${err}`);
- }
-
- rootBegin() {}
- rootEnd() {}
-
- nodeBegin({ path }, key, leftKey) {
- path.push(leftKey);
- }
-
- nodeEnd({ path }) {
- path.pop();
- }
-
- /* jshint camelcase: false */
- /* eslint-disable camelcase */
-
- format_unchanged() {}
-
- format_movedestination() {}
-
- format_node(context, delta, left) {
- this.formatDeltaChildren(context, delta, left);
- }
-
- format_added(context, delta) {
- context.pushCurrentOp({ op: OPERATIONS.add, value: delta[0] });
- }
-
- format_modified(context, delta) {
- context.pushCurrentOp({ op: OPERATIONS.replace, value: delta[1] });
- }
-
- format_deleted(context) {
- context.pushCurrentOp({ op: OPERATIONS.remove });
- }
-
- format_moved(context, delta) {
- const to = delta[1];
- context.pushMoveOp(to);
- }
-
- format_textdiff() {
- throw new Error('Not implemented');
- }
-
- format(delta, left) {
- const context = {};
- this.prepareContext(context);
- this.recurse(context, delta, left);
- return context.result;
- }
-}
-
-/* jshint camelcase: true */
-/* eslint-enable camelcase */
-
-export default JSONFormatter;
-
-const last = (arr) => arr[arr.length - 1];
-
-const sortBy = (arr, pred) => {
- arr.sort(pred);
- return arr;
-};
-
-const compareByIndexDesc = (indexA, indexB) => {
- const lastA = parseInt(indexA, 10);
- const lastB = parseInt(indexB, 10);
- if (!(isNaN(lastA) || isNaN(lastB))) {
- return lastB - lastA;
- } else {
- return 0;
- }
-};
-
-const opsByDescendingOrder = (removeOps) =>
- sortBy(removeOps, (a, b) => {
- const splitA = a.path.split('/');
- const splitB = b.path.split('/');
- if (splitA.length !== splitB.length) {
- return splitA.length - splitB.length;
- } else {
- return compareByIndexDesc(last(splitA), last(splitB));
- }
- });
-
-export const partitionOps = (arr, fns) => {
- const initArr = Array(fns.length + 1)
- .fill()
- .map(() => []);
- return arr
- .map((item) => {
- let position = fns.map((fn) => fn(item)).indexOf(true);
- if (position < 0) {
- position = fns.length;
- }
- return { item, position };
- })
- .reduce((acc, item) => {
- acc[item.position].push(item.item);
- return acc;
- }, initArr);
-};
-const isMoveOp = ({ op }) => op === 'move';
-const isRemoveOp = ({ op }) => op === 'remove';
-
-const reorderOps = (diff) => {
- const [moveOps, removedOps, restOps] = partitionOps(diff, [
- isMoveOp,
- isRemoveOp,
- ]);
- const removeOpsReverse = opsByDescendingOrder(removedOps);
- return [...removeOpsReverse, ...moveOps, ...restOps];
-};
-
-let defaultInstance;
-
-export const format = (delta, left) => {
- if (!defaultInstance) {
- defaultInstance = new JSONFormatter();
- }
- return reorderOps(defaultInstance.format(delta, left));
-};
-
-export const log = (delta, left) => {
- console.log(format(delta, left));
-};
diff --git a/src/formatters/jsonpatch.ts b/src/formatters/jsonpatch.ts
new file mode 100644
index 00000000..375c4e62
--- /dev/null
+++ b/src/formatters/jsonpatch.ts
@@ -0,0 +1,237 @@
+import BaseFormatter, { BaseFormatterContext } from './base';
+import {
+ AddedDelta,
+ ArrayDelta,
+ Delta,
+ ModifiedDelta,
+ MovedDelta,
+ ObjectDelta,
+} from '../contexts/diff';
+
+const OPERATIONS = {
+ add: 'add',
+ remove: 'remove',
+ replace: 'replace',
+ move: 'move',
+} as const;
+
+interface AddOp {
+ op: 'add';
+ path: string;
+ value: unknown;
+}
+
+interface RemoveOp {
+ op: 'remove';
+ path: string;
+}
+
+interface ReplaceOp {
+ op: 'replace';
+ path: string;
+ value: unknown;
+}
+
+interface MoveOp {
+ op: 'move';
+ from: string;
+ path: string;
+}
+
+type Op = AddOp | RemoveOp | ReplaceOp | MoveOp;
+
+interface JSONFormatterContext extends BaseFormatterContext {
+ result: Op[];
+ path: Array
;
+ pushCurrentOp: (
+ obj:
+ | { op: 'add'; value: unknown }
+ | { op: 'replace'; value: unknown }
+ | { op: 'remove' },
+ ) => void;
+ pushMoveOp: (to: number) => void;
+ currentPath: () => string;
+ toPath: (toPath: number) => string;
+}
+
+class JSONFormatter extends BaseFormatter {
+ constructor() {
+ super();
+ this.includeMoveDestinations = true;
+ }
+
+ prepareContext(context: Partial) {
+ super.prepareContext(context);
+ context.result = [];
+ context.path = [];
+ context.pushCurrentOp = function (obj) {
+ if (obj.op === 'add' || obj.op === 'replace') {
+ this.result!.push({
+ op: obj.op,
+ path: this.currentPath!(),
+ value: obj.value,
+ });
+ } else if (obj.op === 'remove') {
+ this.result!.push({ op: obj.op, path: this.currentPath!() });
+ } else {
+ obj satisfies never;
+ }
+ };
+
+ context.pushMoveOp = function (to) {
+ const from = this.currentPath!();
+ this.result!.push({
+ op: OPERATIONS.move,
+ from,
+ path: this.toPath!(to),
+ });
+ };
+
+ context.currentPath = function () {
+ return `/${this.path!.join('/')}`;
+ };
+
+ context.toPath = function (toPath) {
+ const to = this.path!.slice();
+ to[to.length - 1] = toPath;
+ return `/${to.join('/')}`;
+ };
+ }
+
+ typeFormattterErrorFormatter(context: JSONFormatterContext, err: unknown) {
+ context.out(`[ERROR] ${err}`);
+ }
+
+ rootBegin() {}
+ rootEnd() {}
+
+ nodeBegin(
+ { path }: JSONFormatterContext,
+ key: string,
+ leftKey: string | number,
+ ) {
+ path.push(leftKey);
+ }
+
+ nodeEnd({ path }: JSONFormatterContext) {
+ path.pop();
+ }
+
+ /* jshint camelcase: false */
+ /* eslint-disable camelcase */
+
+ format_unchanged() {}
+
+ format_movedestination() {}
+
+ format_node(
+ context: JSONFormatterContext,
+ delta: ObjectDelta | ArrayDelta,
+ left: unknown,
+ ) {
+ this.formatDeltaChildren(context, delta, left);
+ }
+
+ format_added(context: JSONFormatterContext, delta: AddedDelta) {
+ context.pushCurrentOp({ op: OPERATIONS.add, value: delta[0] });
+ }
+
+ format_modified(context: JSONFormatterContext, delta: ModifiedDelta) {
+ context.pushCurrentOp({ op: OPERATIONS.replace, value: delta[1] });
+ }
+
+ format_deleted(context: JSONFormatterContext) {
+ context.pushCurrentOp({ op: OPERATIONS.remove });
+ }
+
+ format_moved(context: JSONFormatterContext, delta: MovedDelta) {
+ const to = delta[1];
+ context.pushMoveOp(to);
+ }
+
+ format_textdiff() {
+ throw new Error('Not implemented');
+ }
+
+ format(delta: Delta, left: unknown) {
+ const context: Partial = {};
+ this.prepareContext(context);
+ this.recurse(context as JSONFormatterContext, delta, left);
+ return (context as JSONFormatterContext).result;
+ }
+}
+
+/* jshint camelcase: true */
+/* eslint-enable camelcase */
+
+export default JSONFormatter;
+
+const last = (arr: T[]): T => arr[arr.length - 1];
+
+const sortBy = (arr: T[], pred: (a: T, b: T) => number) => {
+ arr.sort(pred);
+ return arr;
+};
+
+const compareByIndexDesc = (indexA: string, indexB: string) => {
+ const lastA = parseInt(indexA, 10);
+ const lastB = parseInt(indexB, 10);
+ if (!(isNaN(lastA) || isNaN(lastB))) {
+ return lastB - lastA;
+ } else {
+ return 0;
+ }
+};
+
+const opsByDescendingOrder = (removeOps: Op[]) =>
+ sortBy(removeOps, (a, b) => {
+ const splitA = a.path.split('/');
+ const splitB = b.path.split('/');
+ if (splitA.length !== splitB.length) {
+ return splitA.length - splitB.length;
+ } else {
+ return compareByIndexDesc(last(splitA), last(splitB));
+ }
+ });
+
+export const partitionOps = (arr: Op[], fns: Array<(op: Op) => boolean>) => {
+ const initArr: Op[][] = Array(fns.length + 1)
+ .fill(undefined)
+ .map(() => []);
+ return arr
+ .map((item) => {
+ let position = fns.map((fn) => fn(item)).indexOf(true);
+ if (position < 0) {
+ position = fns.length;
+ }
+ return { item, position };
+ })
+ .reduce((acc, item) => {
+ acc[item.position].push(item.item);
+ return acc;
+ }, initArr);
+};
+const isMoveOp = ({ op }: Op) => op === 'move';
+const isRemoveOp = ({ op }: Op) => op === 'remove';
+
+const reorderOps = (diff: Op[]) => {
+ const [moveOps, removedOps, restOps] = partitionOps(diff, [
+ isMoveOp,
+ isRemoveOp,
+ ]);
+ const removeOpsReverse = opsByDescendingOrder(removedOps);
+ return [...removeOpsReverse, ...moveOps, ...restOps];
+};
+
+let defaultInstance: JSONFormatter | undefined;
+
+export const format = (delta: Delta, left: unknown) => {
+ if (!defaultInstance) {
+ defaultInstance = new JSONFormatter();
+ }
+ return reorderOps(defaultInstance.format(delta, left));
+};
+
+export const log = (delta: Delta, left: unknown) => {
+ console.log(format(delta, left));
+};
diff --git a/src/index.d.ts b/src/index.d.ts
deleted file mode 100644
index 715ce30d..00000000
--- a/src/index.d.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-export interface Formatter {
- format(delta: Delta, original: any): string;
-}
-
-export interface HtmlFormatter extends Formatter {
- /**
- * Set whether to show or hide unchanged parts of a diff.
- * @param show Whether to show unchanged parts
- * @param node The root element the diff is contained within. (Default: body)
- * @param delay Transition time in ms. (Default: no transition)
- */
- showUnchanged(show: boolean, node?: Element | null, delay?: number): void;
-
- /**
- * An alias for showUnchanged(false, ...)
- * @param node The root element the diff is contained within (Default: body)
- * @param delay Transition time in ms. (Default: no transition)
- */
- hideUnchanged(node?: Element | null, delay?: number): void;
-}
-
-export interface Delta {
- [key: string]: any;
- [key: number]: any;
-}
-
-export class Context {
- nested: boolean;
- exiting?: boolean;
- options: Config;
- parent?: PatchContext;
- childName?: string;
- children?: PatchContext[];
- root?: PatchContext;
- next?: PatchContext;
- nextAfterChildren?: PatchContext;
- hasResult: boolean;
- setResult(result: any): Context;
- exit(): Context;
-}
-
-export class PatchContext extends Context {
- pipe: 'patch';
- left: any;
- delta: Delta;
-}
-
-export class DiffContext extends Context {
- pipe: 'diff';
- left: any;
- right: any;
-}
-
-export class ReverseContext extends Context {
- pipe: 'reverse';
- delta: Delta;
-}
-
-type FilterContext = PatchContext | DiffContext | ReverseContext;
-
-/**
- * A plugin which can modify the diff(), patch() or reverse() operations
- */
-export interface Filter {
- /**
- * A function which is called at each stage of the operation and can update the context to modify the result
- * @param context The current state of the operation
- */
- (context: TContext): void;
-
- /**
- * A unique name which can be used to insert other filters before/after, or remove/replace this filter
- */
- filterName: string;
-}
-
-/**
- * A collection of Filters run on each diff(), patch() or reverse() operation
- */
-export class Pipe {
- /**
- * Append one or more filters to the existing list
- */
- append(...filters: Filter[]): void;
-
- /**
- * Prepend one or more filters to the existing list
- */
- prepend(...filters: Filter[]): void;
-
- /**
- * Add one ore more filters after the specified filter
- * @param filterName The name of the filter to insert before
- * @param filters Filters to be inserted
- */
- after(filterName: string, ...filters: Filter[]): void;
-
- /**
- * Add one ore more filters before the specified filter
- * @param filterName The name of the filter to insert before
- * @param filters Filters to be inserted
- */
- before(filterName: string, ...filters: Filter[]): void;
-
- /**
- * Replace the specified filter with one ore more filters
- * @param filterName The name of the filter to replace
- * @param filters Filters to be inserted
- */
- replace(filterName: string, ...filters: Filter[]): void;
-
- /**
- * Remove the filter with the specified name
- * @param filterName The name of the filter to remove
- */
- remove(filterName: string): void;
-
- /**
- * Remove all filters from this pipe
- */
- clear(): void;
-
- /**
- * Return array of ordered filter names for this pipe
- */
- list(): void;
-}
-
-export class Processor {
- constructor(options?: Config);
-
- pipes: {
- patch: Pipe;
- diff: Pipe;
- reverse: Pipe;
- };
-}
-
-export interface Config {
- // used to match objects when diffing arrays, by default only === operator is used
- objectHash?: (item: any, index: number) => string;
-
- arrays?: {
- // default true, detect items moved inside the array (otherwise they will be registered as remove+add)
- detectMove: boolean;
- // default false, the value of items moved is not included in deltas
- includeValueOnMove: boolean;
- };
-
- textDiff?: {
- // default 60, minimum string length (left and right sides) to use text diff algorythm: google-diff-match-patch
- minLength: number;
- };
-
- /**
- * this optional function can be specified to ignore object properties (eg. volatile data)
- * @param name property name, present in either context.left or context.right objects
- * @param context the diff context (has context.left and context.right objects)
- */
- /**
- *
- */
- propertyFilter?: (name: string, context: DiffContext) => boolean;
-
- /**
- * default false. if true, values in the obtained delta will be cloned (using jsondiffpatch.clone by default),
- * to ensure delta keeps no references to left or right objects. this becomes useful if you're diffing and patching
- * the same objects multiple times without serializing deltas.
- *
- * instead of true, a function can be specified here to provide a custom clone(value)
- */
- cloneDiffValues?: boolean | ((value: any) => any);
-}
-
-export class DiffPatcher {
- constructor(options?: Config);
-
- processor: Processor;
-
- clone: (value: any) => any;
- diff: (left: any, right: any) => Delta | undefined;
- patch: (left: any, delta: Delta) => any;
- reverse: (delta: Delta) => Delta | undefined;
- unpatch: (right: any, delta: Delta) => any;
-}
-
-export const create: (options?: any) => DiffPatcher;
-
-export const formatters: {
- annotated: Formatter;
- console: Formatter;
- html: HtmlFormatter;
- jsonpatch: Formatter;
-};
-
-export const console: Formatter;
-
-export const dateReviver: (key: string, value: any) => any;
-
-export const diff: (left: any, right: any) => Delta | undefined;
-export const patch: (left: any, delta: Delta) => any;
-export const reverse: (delta: Delta) => Delta | undefined;
-export const unpatch: (right: any, delta: Delta) => any;
diff --git a/src/main.js b/src/index.ts
similarity index 50%
rename from src/main.js
rename to src/index.ts
index 6ae18423..b3a35017 100644
--- a/src/main.js
+++ b/src/index.ts
@@ -1,5 +1,7 @@
import DiffPatcher from './diffpatcher';
import dateReviver from './date-reviver';
+import { Options } from './processor';
+import { Delta } from './contexts/diff';
export { DiffPatcher, dateReviver };
@@ -7,43 +9,43 @@ export * as formatters from './formatters/index';
export * as console from './formatters/console';
-export function create(options) {
+export function create(options?: Options) {
return new DiffPatcher(options);
}
-let defaultInstance;
+let defaultInstance: DiffPatcher;
-export function diff() {
+export function diff(left: unknown, right: unknown) {
if (!defaultInstance) {
defaultInstance = new DiffPatcher();
}
- return defaultInstance.diff.apply(defaultInstance, arguments);
+ return defaultInstance.diff(left, right);
}
-export function patch() {
+export function patch(left: unknown, delta: Delta) {
if (!defaultInstance) {
defaultInstance = new DiffPatcher();
}
- return defaultInstance.patch.apply(defaultInstance, arguments);
+ return defaultInstance.patch(left, delta);
}
-export function unpatch() {
+export function unpatch(right: unknown, delta: Delta) {
if (!defaultInstance) {
defaultInstance = new DiffPatcher();
}
- return defaultInstance.unpatch.apply(defaultInstance, arguments);
+ return defaultInstance.unpatch(right, delta);
}
-export function reverse() {
+export function reverse(delta: Delta) {
if (!defaultInstance) {
defaultInstance = new DiffPatcher();
}
- return defaultInstance.reverse.apply(defaultInstance, arguments);
+ return defaultInstance.reverse(delta);
}
-export function clone() {
+export function clone(value: unknown) {
if (!defaultInstance) {
defaultInstance = new DiffPatcher();
}
- return defaultInstance.clone.apply(defaultInstance, arguments);
+ return defaultInstance.clone(value);
}
diff --git a/src/pipe.js b/src/pipe.ts
similarity index 64%
rename from src/pipe.js
rename to src/pipe.ts
index 460634c4..119bf5bd 100644
--- a/src/pipe.js
+++ b/src/pipe.ts
@@ -1,10 +1,24 @@
-class Pipe {
- constructor(name) {
+import Context from './contexts/context';
+import Processor from './processor';
+
+export interface Filter {
+ (context: TContext): void;
+ filterName: string;
+}
+
+class Pipe> {
+ name?: string;
+ filters: Filter[];
+ processor?: Processor;
+ debug?: boolean;
+ resultCheck?: ((context: TContext) => void) | null;
+
+ constructor(name: string) {
this.name = name;
this.filters = [];
}
- process(input) {
+ process(input: TContext) {
if (!this.processor) {
throw new Error('add this pipe to a processor before using it');
}
@@ -27,21 +41,21 @@ class Pipe {
}
}
- log(msg) {
+ log(msg: string) {
console.log(`[jsondiffpatch] ${this.name} pipe, ${msg}`);
}
- append(...args) {
+ append(...args: Filter[]) {
this.filters.push(...args);
return this;
}
- prepend(...args) {
+ prepend(...args: Filter[]) {
this.filters.unshift(...args);
return this;
}
- indexOf(filterName) {
+ indexOf(filterName: string) {
if (!filterName) {
throw new Error('a filter name is required');
}
@@ -58,40 +72,25 @@ class Pipe {
return this.filters.map((f) => f.filterName);
}
- after(filterName) {
+ after(filterName: string, ...params: Filter[]) {
const index = this.indexOf(filterName);
- const params = Array.prototype.slice.call(arguments, 1);
- if (!params.length) {
- throw new Error('a filter is required');
- }
- params.unshift(index + 1, 0);
- Array.prototype.splice.apply(this.filters, params);
+ this.filters.splice(index + 1, 0, ...params);
return this;
}
- before(filterName) {
+ before(filterName: string, ...params: Filter[]) {
const index = this.indexOf(filterName);
- const params = Array.prototype.slice.call(arguments, 1);
- if (!params.length) {
- throw new Error('a filter is required');
- }
- params.unshift(index, 0);
- Array.prototype.splice.apply(this.filters, params);
+ this.filters.splice(index, 0, ...params);
return this;
}
- replace(filterName) {
+ replace(filterName: string, ...params: Filter[]) {
const index = this.indexOf(filterName);
- const params = Array.prototype.slice.call(arguments, 1);
- if (!params.length) {
- throw new Error('a filter is required');
- }
- params.unshift(index, 1);
- Array.prototype.splice.apply(this.filters, params);
+ this.filters.splice(index, 1, ...params);
return this;
}
- remove(filterName) {
+ remove(filterName: string) {
const index = this.indexOf(filterName);
this.filters.splice(index, 1);
return this;
@@ -102,7 +101,7 @@ class Pipe {
return this;
}
- shouldHaveResult(should) {
+ shouldHaveResult(should?: boolean) {
if (should === false) {
this.resultCheck = null;
return;
@@ -114,7 +113,9 @@ class Pipe {
this.resultCheck = (context) => {
if (!context.hasResult) {
console.log(context);
- const error = new Error(`${pipe.name} failed`);
+ const error: Error & { noResult?: boolean } = new Error(
+ `${pipe.name} failed`,
+ );
error.noResult = true;
throw error;
}
diff --git a/src/processor.js b/src/processor.js
deleted file mode 100644
index ccc0ae6b..00000000
--- a/src/processor.js
+++ /dev/null
@@ -1,65 +0,0 @@
-class Processor {
- constructor(options) {
- this.selfOptions = options || {};
- this.pipes = {};
- }
-
- options(options) {
- if (options) {
- this.selfOptions = options;
- }
- return this.selfOptions;
- }
-
- pipe(name, pipeArg) {
- let pipe = pipeArg;
- if (typeof name === 'string') {
- if (typeof pipe === 'undefined') {
- return this.pipes[name];
- } else {
- this.pipes[name] = pipe;
- }
- }
- if (name && name.name) {
- pipe = name;
- if (pipe.processor === this) {
- return pipe;
- }
- this.pipes[pipe.name] = pipe;
- }
- pipe.processor = this;
- return pipe;
- }
-
- process(input, pipe) {
- let context = input;
- context.options = this.options();
- let nextPipe = pipe || input.pipe || 'default';
- let lastPipe;
- let lastContext;
- while (nextPipe) {
- if (typeof context.nextAfterChildren !== 'undefined') {
- // children processed and coming back to parent
- context.next = context.nextAfterChildren;
- context.nextAfterChildren = null;
- }
-
- if (typeof nextPipe === 'string') {
- nextPipe = this.pipe(nextPipe);
- }
- nextPipe.process(context);
- lastContext = context;
- lastPipe = nextPipe;
- nextPipe = null;
- if (context) {
- if (context.next) {
- context = context.next;
- nextPipe = lastContext.nextPipe || context.pipe || lastPipe;
- }
- }
- }
- return context.hasResult ? context.result : undefined;
- }
-}
-
-export default Processor;
diff --git a/src/processor.ts b/src/processor.ts
new file mode 100644
index 00000000..77eacc1c
--- /dev/null
+++ b/src/processor.ts
@@ -0,0 +1,93 @@
+import Pipe from './pipe';
+import Context from './contexts/context';
+import DiffContext from './contexts/diff';
+
+export interface Options {
+ objectHash?: (item: object, index?: number) => string;
+ matchByPosition?: boolean;
+ arrays?: {
+ detectMove?: boolean;
+ includeValueOnMove?: boolean;
+ };
+ textDiff?: {
+ minLength?: number;
+ };
+ propertyFilter?: (name: string, context: DiffContext) => boolean;
+ cloneDiffValues?: boolean | ((value: unknown) => unknown);
+}
+
+class Processor {
+ selfOptions: Options;
+ pipes: { [pipeName: string]: Pipe> | undefined };
+
+ constructor(options?: Options) {
+ this.selfOptions = options || {};
+ this.pipes = {};
+ }
+
+ options(options?: Options) {
+ if (options) {
+ this.selfOptions = options;
+ }
+ return this.selfOptions;
+ }
+
+ pipe>(
+ name: string | Pipe,
+ pipeArg?: Pipe,
+ ) {
+ let pipe = pipeArg;
+ if (typeof name === 'string') {
+ if (typeof pipe === 'undefined') {
+ return this.pipes[name]!;
+ } else {
+ this.pipes[name] = pipe as Pipe>;
+ }
+ }
+ if (name && (name as Pipe).name) {
+ pipe = name as Pipe;
+ if (pipe.processor === this) {
+ return pipe;
+ }
+ this.pipes[pipe.name!] = pipe as Pipe>;
+ }
+ pipe!.processor = this;
+ return pipe!;
+ }
+
+ process>(
+ input: TContext,
+ pipe?: Pipe,
+ ): TContext['result'] | undefined {
+ let context = input;
+ context.options = this.options();
+ let nextPipe: Pipe | string | null =
+ pipe || input.pipe || 'default';
+ let lastPipe;
+ let lastContext;
+ while (nextPipe) {
+ if (typeof context.nextAfterChildren !== 'undefined') {
+ // children processed and coming back to parent
+ context.next = context.nextAfterChildren;
+ context.nextAfterChildren = null;
+ }
+
+ if (typeof nextPipe === 'string') {
+ nextPipe = this.pipe(nextPipe);
+ }
+ (nextPipe as Pipe).process(context);
+ lastContext = context;
+ lastPipe = nextPipe;
+ nextPipe = null;
+ if (context) {
+ if (context.next) {
+ context = context.next;
+ nextPipe = lastContext.nextPipe || context.pipe || lastPipe;
+ }
+ }
+ }
+ return context.hasResult ? context.result : undefined;
+ }
+}
+
+export default Processor;
diff --git a/test/index.spec.js b/test/index.spec.js
index 306bdfc8..4bf7c683 100644
--- a/test/index.spec.js
+++ b/test/index.spec.js
@@ -1,4 +1,4 @@
-import * as jsondiffpatch from '../src/main';
+import * as jsondiffpatch from '../src';
import lcs from '../src/filters/lcs';
import examples from './examples/diffpatch';
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..7daae30d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "target": "ES2016",
+ "module": "Node16",
+ "noEmit": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "skipLibCheck": false
+ }
+}