Skip to content

Commit

Permalink
Merge pull request #3009 from udecode/feat/should-diff-descendants
Browse files Browse the repository at this point in the history
Add `shouldDiffDescendants`
  • Loading branch information
12joan authored Mar 1, 2024
2 parents a501e81 + eb0d661 commit 4936c99
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 55 deletions.
21 changes: 21 additions & 0 deletions .changeset/fluffy-donuts-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@udecode/plate-diff": minor
---

- Add `shouldDiffDescendants` option to `computeDiff` to control whether a pair of descendant lists should be diffed. If false, the parent node will be deleted and re-inserted. Defaults to `() => true`.
- Example use case: To prevent `computeDiff` from diffing the text of unrelated paragraphs, use a text similarity checking algorithm to determine whether the paragraphs are sufficiently similar, and return false if not.
- When multiple consecutive nodes have been deleted and inserted, `computeDiff` now groups all consecutive deletions together and does the same with all consecutive insertions.
- Example of a diff prior to this change:
```diff
- Old paragraph 1
+ New paragraph 1
- Old paragraph 2
+ New paragraph 2
```
- Example of a diff after this change:
```diff
- Old paragraph 1
- Old paragraph 2
+ New paragraph 1
+ New paragraph 2
```
109 changes: 103 additions & 6 deletions packages/diff/src/computeDiff.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
* contributors. See /packages/diff/LICENSE for more information.
*/

import { Value } from '@udecode/plate-common';
import { isText, Value } from '@udecode/plate-common';

import { computeDiff } from './computeDiff';
import { computeDiff, ComputeDiffOptions } from './computeDiff';

const ELEMENT_INLINE_VOID = 'inline-void';

interface ComputeDiffFixture {
interface ComputeDiffFixture
extends Pick<ComputeDiffOptions, 'lineBreakChar' | 'shouldDiffDescendants'> {
it?: typeof it;
lineBreakChar?: string;
input1: Value;
input2: Value;
expected: Value;
Expand Down Expand Up @@ -1448,16 +1448,113 @@ const fixtures: Record<string, ComputeDiffFixture> = {
},
],
},

shouldNotDiffDescendants: {
shouldDiffDescendants: ([firstNode]) =>
!firstNode ||
!isText(firstNode) ||
!firstNode.text.startsWith('NO_DIFF_INLINE'),
input1: [
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE FirstA' }],
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE SecondA' }],
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE ThirdA' }],
},
{
type: 'paragraph',
children: [{ text: 'Same' }],
},
],
input2: [
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE FirstB' }],
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE SecondB' }],
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE ThirdB' }],
},
{
type: 'paragraph',
children: [{ text: 'Same' }],
},
],
expected: [
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE FirstA' }],
diff: true,
diffOperation: {
type: 'delete',
},
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE SecondA' }],
diff: true,
diffOperation: {
type: 'delete',
},
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE ThirdA' }],
diff: true,
diffOperation: {
type: 'delete',
},
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE FirstB' }],
diff: true,
diffOperation: {
type: 'insert',
},
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE SecondB' }],
diff: true,
diffOperation: {
type: 'insert',
},
},
{
type: 'paragraph',
children: [{ text: 'NO_DIFF_INLINE ThirdB' }],
diff: true,
diffOperation: {
type: 'insert',
},
},
{
type: 'paragraph',
children: [{ text: 'Same' }],
},
],
},
};

describe('computeDiff', () => {
Object.entries(fixtures).forEach(
([name, { it: itFn = it, input1, input2, expected, lineBreakChar }]) => {
([name, { it: itFn = it, input1, input2, expected, ...options }]) => {
itFn(name, () => {
const output = computeDiff(input1, input2, {
isInline: (node) => node.type === ELEMENT_INLINE_VOID,
ignoreProps: ['id'],
lineBreakChar,
...options,
});

expect(output).toEqual(expected);
Expand Down
4 changes: 4 additions & 0 deletions packages/diff/src/computeDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface ComputeDiffOptions {
isInline: PlateEditor['isInline'];
ignoreProps?: string[];
lineBreakChar?: string;
shouldDiffDescendants?: (
nodes: TDescendant[],
nextNodes: TDescendant[]
) => boolean;
getInsertProps: (node: TDescendant) => any;
getDeleteProps: (node: TDescendant) => any;
getUpdateProps: (
Expand Down
68 changes: 45 additions & 23 deletions packages/diff/src/internal/transforms/transformDiffDescendants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,32 @@ export function transformDiffDescendants(
let i = 0;
const children: TDescendant[] = [];

const insertNodes = (...nodes: TDescendant[]) =>
children.push(
...nodes.map((node) => ({
...node,
...getInsertProps(node),
}))
);
let insertBuffer: TDescendant[] = [];
let deleteBuffer: TDescendant[] = [];

const flushBuffers = () => {
// Return all deletions followed by all insertions
children.push(...deleteBuffer, ...insertBuffer);
insertBuffer = [];
deleteBuffer = [];
};

const removeNodes = (...nodes: TDescendant[]) =>
children.push(
...nodes.map((node) => ({
...node,
...getDeleteProps(node),
}))
);
const insertNode = (node: TDescendant) =>
insertBuffer.push({
...node,
...getInsertProps(node),
});

const deleteNode = (node: TDescendant) =>
deleteBuffer.push({
...node,
...getDeleteProps(node),
});

const passThroughNodes = (...nodes: TDescendant[]) => {
flushBuffers();
children.push(...nodes);
};

const areNodeListsEquivalent = (
nodes0: TDescendant[],
Expand All @@ -72,7 +83,7 @@ export function transformDiffDescendants(

// If operation code is 0, it means the chunk is unchanged
if (op === 0) {
children.push(...nodes);
passThroughNodes(...nodes);
// Move to the next diff chunk
i += 1;
continue;
Expand All @@ -92,15 +103,15 @@ export function transformDiffDescendants(
* just return nextNodes.
*/
if (areNodeListsEquivalent(nodes, nextNodes)) {
children.push(...nextNodes);
passThroughNodes(...nextNodes);
// Consume two diff chunks (delete and insert)
i += 2;
continue;
}

// If both current and next chunks are text nodes, use transformTextNodes
if (isInlineList(nodes) && isInlineList(nextNodes)) {
children.push(...transformDiffTexts(nodes, nextNodes, options));
passThroughNodes(...transformDiffTexts(nodes, nextNodes, options));
// Consume two diff chunks (delete and insert)
i += 2;
continue;
Expand All @@ -110,23 +121,32 @@ export function transformDiffDescendants(
const diffResult = diffNodes(nodes, nextNodes);
diffResult.forEach((item: NodeRelatedItem) => {
if (item.delete) {
removeNodes(item.originNode);
deleteNode(item.originNode);
}
if (item.insert) {
insertNodes(item.originNode);
insertNode(item.originNode);
}
if (item.relatedNode) {
children.push(
...transformDiffNodes(item.originNode, item.relatedNode, options)
const diffNodesResult = transformDiffNodes(
item.originNode,
item.relatedNode,
options
);

if (diffNodesResult) {
passThroughNodes(...diffNodesResult);
} else {
deleteNode(item.originNode);
insertNode(item.relatedNode);
}
}
});
i += 2; // this consumed two entries from the diff array.
continue;
} else {
// Plain delete of some nodes (with no insert immediately after)
for (const node of nodes) {
removeNodes(node);
deleteNode(node);
}
i += 1; // consumes only one entry from diff array.
continue;
Expand All @@ -135,7 +155,7 @@ export function transformDiffDescendants(
if (op === 1) {
// insert new nodes.
for (const node of nodes) {
insertNodes(node);
insertNode(node);
}
i += 1;
continue;
Expand All @@ -145,5 +165,7 @@ export function transformDiffDescendants(
);
}

flushBuffers();

return children;
}
37 changes: 11 additions & 26 deletions packages/diff/src/internal/transforms/transformDiffNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ type Handler = (
* algorithm on the children.
*/
const childrenOnlyStrategy: Handler = (node, nextNode, options) => {
const { shouldDiffDescendants = () => true } = options;

if (
node['children'] != null &&
nextNode['children'] != null &&
isEqual(
copyWithout(node, ['children']),
copyWithout(nextNode, ['children'])
) &&
shouldDiffDescendants(
node['children'] as TDescendant[],
nextNode['children'] as TDescendant[]
)
) {
const children = computeDiff(
Expand Down Expand Up @@ -83,36 +89,14 @@ const propsOnlyStrategy: Handler = (node, nextNode, { getUpdateProps }) => {
];
};

// No other strategy applies, so remove and insert the node.
const fallbackStrategy: Handler = (
node,
nextNode,
{ getInsertProps, getDeleteProps }
) => {
return [
{
...node,
...getDeleteProps(node),
},
{
...nextNode,
...getInsertProps(nextNode),
},
];
};

const strategies: Handler[] = [
childrenOnlyStrategy,
propsOnlyStrategy,
fallbackStrategy,
];
const strategies: Handler[] = [childrenOnlyStrategy, propsOnlyStrategy];

// Replace node at path by nextNode using the first strategy that works.
export function transformDiffNodes(
node: TDescendant,
nextNode: TDescendant,
options: ComputeDiffOptions
): TDescendant[] {
): TDescendant[] | false {
// Try each strategy in turn
for (const strategy of strategies) {
// Attempt to generate operations with the current strategy and return the operations if the strategy succeeds
Expand All @@ -121,6 +105,7 @@ export function transformDiffNodes(
return ops;
}
}
// If no strategy succeeds, throw an error (should never happen because of the fallback strategy)
throw new Error('transformDiffNodes: No strategy succeeded');

// If no strategy succeeds, tell the caller that the nodes are not comparable
return false;
}

0 comments on commit 4936c99

Please sign in to comment.