Last active
June 8, 2024 23:38
-
-
Save jourdanrodrigues/574b7874ba1c6c3c11358c2fd44769d3 to your computer and use it in GitHub Desktop.
Diff algorithm for comparison between 2 SlateJS content objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from typing import List, Iterable, Mapping | |
from diff_match_patch import DiffMatchPatch # https://github.com/google/diff-match-patch/blob/master/python3/diff_match_patch.py | |
def _index_or_none(items: list, index: int): | |
try: | |
return items[index] | |
except IndexError: | |
return None | |
def _is_iterable(value) -> bool: | |
return isinstance(value, Iterable) and not isinstance(value, (str, bytes)) | |
class Block(dict): | |
__nodes = None | |
__hash = None | |
# Using tuples for it to be immutable | |
__object_nodes_map = ( | |
('block', 'nodes'), | |
('document', 'nodes'), | |
('text', 'leaves'), | |
('inline', 'nodes'), | |
) | |
def __setitem__(self, key, value): | |
raise NotImplementedError | |
def update(self, __m: Mapping, **kwargs) -> None: | |
raise NotImplementedError | |
def __hash__(self): | |
if not self.__hash: | |
items = ((key, tuple(value) if _is_iterable(value) else value) for key, value in self.items()) | |
self.__hash = hash(tuple(sorted(items))) | |
return self.__hash | |
@classmethod | |
def from_dict(cls, dict_: Mapping, **kwargs): | |
result = kwargs.copy() | |
for key, value in dict_.items(): | |
extra = {} if key in ['data', 'marks'] else kwargs | |
if isinstance(value, Mapping): | |
result[key] = cls.from_dict(value, **extra) | |
elif _is_iterable(value): | |
result[key] = type(value)(cls.from_dict(item, **extra) for item in value) | |
else: | |
result[key] = value | |
return cls(**result) | |
def have_same_attributes(self, block: 'Block') -> bool: | |
return ( | |
self['object'] == block['object'] and | |
self.get('type') == block.get('type') and | |
self.get('data') == block.get('data') and | |
self.get('marks') == block.get('marks') and | |
self.get('text') == block.get('text') | |
) | |
def has_different_text(self, block: 'Block') -> bool: | |
return self.get('text') != block.get('text') and self.get('marks') == block.get('marks') | |
def get_nodes(self) -> List[dict] or None: | |
if self.__nodes is None: | |
self.__nodes = self.get(self.get_nodes_key()) | |
return self.__nodes | |
def get_nodes_key(self) -> List[dict] or None: | |
return dict(self.__object_nodes_map).get(self['object']) | |
class ContentDiffer(dict): | |
def __init__(self, old: dict, new: dict): | |
old_ = Block.from_dict(old) | |
new_ = Block.from_dict(new) | |
document = self._diff_document_root(old_['document'], new_['document']) | |
super().__init__(new_, document=document) | |
@classmethod | |
def _diff_document_root(cls, old: Block, new: Block) -> Block: | |
return new if old == new else Block(new, nodes=cls._get_nodes_diff(old.get_nodes(), new.get_nodes())) | |
@classmethod | |
def _get_nodes_diff(cls, old_nodes: List[Block], new_nodes: List[Block]) -> List[Block]: | |
if old_nodes == new_nodes: # Shortcut | |
return new_nodes | |
old_i = 0 | |
new_i = 0 | |
result_nodes = [] | |
while old_i < len(old_nodes) or new_i < len(new_nodes): | |
# Without type notation, Block's methods aren't discovered | |
old_node: Block = _index_or_none(old_nodes, old_i) | |
new_node: Block = _index_or_none(new_nodes, new_i) | |
if None not in [old_node, new_node] and old_node == new_node: | |
result_nodes.append(new_node) | |
elif old_node is None: | |
result_nodes.append(Block.from_dict(new_node, isAdded=True)) | |
elif new_node is None: | |
result_nodes.append(Block.from_dict(old_node, isDeleted=True)) | |
else: | |
if old_node in new_nodes or new_node in old_nodes: | |
if new_node in old_nodes[old_i:]: | |
if cls._has_item_in_between(new_nodes, new_i, old_nodes, old_i): | |
result_nodes.append(Block.from_dict(new_node, isAdded=True)) | |
new_i += 1 | |
else: | |
result_nodes.append(Block.from_dict(old_node, isDeleted=True)) | |
old_i += 1 | |
continue | |
elif old_node in new_nodes[new_i:]: | |
if cls._has_item_in_between(old_nodes, old_i, new_nodes, new_i): | |
result_nodes.append(Block.from_dict(old_node, isDeleted=True)) | |
old_i += 1 | |
else: | |
result_nodes.append(Block.from_dict(new_node, isAdded=True)) | |
new_i += 1 | |
continue | |
if not old_node.have_same_attributes(new_node): | |
if old_node.has_different_text(new_node): | |
result_nodes += cls._get_text_diff_nodes(old_node, new_node) | |
else: | |
result_nodes += [ | |
Block.from_dict(new_node, isAdded=True), | |
Block.from_dict(old_node, isDeleted=True), | |
] | |
elif old_node.get_nodes() != new_node.get_nodes(): | |
new_inner_nodes = cls._get_nodes_diff(old_node.get_nodes(), new_node.get_nodes()) | |
nodes_key = new_node.get_nodes_key() | |
result_nodes.append(Block(new_node, **{nodes_key: new_inner_nodes})) | |
old_i += 1 | |
new_i += 1 | |
return result_nodes | |
@staticmethod | |
def _has_item_in_between(source_nodes, source_i, target_nodes, target_i) -> bool: | |
target_limit = target_nodes.index(source_nodes[source_i]) | |
# i + 1 to ignore current nodes | |
return bool(set(source_nodes[source_i + 1:]).intersection(target_nodes[target_i + 1:target_limit])) | |
@classmethod | |
def _get_text_diff_nodes(cls, old_node: Block, new_node: Block) -> List[Block]: | |
dmp = DiffMatchPatch() | |
dmp.Diff_EditCost = 7 | |
# "new_node" comes first for "added" to come first | |
diff = dmp.diff_main(new_node['text'], old_node['text']) | |
dmp.diff_cleanupEfficiency(diff) | |
return [ | |
Block(new_node, text=text, **cls._get_flag_from_diff_index(diff_index)) | |
for diff_index, text in diff | |
] | |
@staticmethod | |
def _get_flag_from_diff_index(index: int) -> dict: | |
# Switched for "added" to come first (see comment in ContentDiffer._get_text_diff_nodes) | |
if index == DiffMatchPatch.DIFF_EQUAL: | |
return {} | |
elif index == DiffMatchPatch.DIFF_INSERT: | |
return {'isDeleted': True} | |
elif index == DiffMatchPatch.DIFF_DELETE: | |
return {'isAdded': True} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from typing import List | |
from unittest import TestCase | |
from content_differ import ContentDiffer | |
class TestInit(TestCase): | |
maxDiff = None | |
def test_when_text_changed_completely_then_returns_added_and_deleted(self): | |
def generate_document(*, paragraph_text: str) -> dict: | |
return { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': paragraph_text, 'marks': []}] | |
}] | |
}] | |
} | |
} | |
old_text = 'some normal text' | |
new_text = 'completely different string' | |
old = generate_document(paragraph_text=old_text) | |
new = generate_document(paragraph_text=new_text) | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [ | |
{'object': 'leaf', 'text': new_text, 'marks': [], 'isAdded': True}, | |
{'object': 'leaf', 'text': old_text, 'marks': [], 'isDeleted': True}, | |
], | |
}], | |
}], | |
}, | |
}) | |
def test_when_text_changed_partially_then_returns_correct_output(self): | |
def generate_document(*, paragraph_text: str) -> dict: | |
return { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [{ | |
'type': 'paragraph', | |
'object': 'block', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': paragraph_text, 'marks': []}] | |
}] | |
}] | |
} | |
} | |
old = generate_document(paragraph_text='some normal text') | |
new = generate_document(paragraph_text='some updated text') | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [ | |
{'object': 'leaf', 'text': 'some ', 'marks': []}, | |
{'object': 'leaf', 'text': 'updated', 'marks': [], 'isAdded': True}, | |
{'object': 'leaf', 'text': 'normal', 'marks': [], 'isDeleted': True}, | |
{'object': 'leaf', 'text': ' text', 'marks': []}, | |
], | |
}], | |
}], | |
}, | |
}) | |
def test_when_marks_are_changed_then_label_the_block_as_changed(self): | |
text = 'some text' | |
def generate_document(*, marks: List[dict] = None) -> dict: | |
return { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': text, 'marks': marks or []}] | |
}] | |
}] | |
} | |
} | |
old_marks = [{'object': 'mark', 'type': 'strong', 'data': {}}] | |
old = generate_document(marks=old_marks) | |
new = generate_document() | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [ | |
{'object': 'leaf', 'text': text, 'marks': [], 'isAdded': True}, | |
{'object': 'leaf', 'text': text, 'marks': old_marks, 'isDeleted': True}, | |
], | |
}], | |
}], | |
}, | |
}) | |
def test_when_block_is_added_then_returns_correct_output(self): | |
def generate_document(*, paragraphs: int): | |
return { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': f'Paragraph {i + 1}', 'marks': []}], | |
}], | |
} for i in range(paragraphs)], | |
}, | |
} | |
old = generate_document(paragraphs=1) | |
new = generate_document(paragraphs=2) | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Paragraph 1', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'isAdded': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'isAdded': True, | |
'leaves': [{'object': 'leaf', 'isAdded': True, 'text': 'Paragraph 2', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
}) | |
def test_when_block_is_removed_then_returns_correct_output(self): | |
def generate_document(*, paragraphs: int): | |
return { | |
'object': 'value', | |
'document': { | |
'data': {}, | |
'object': 'document', | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': f'Paragraph {i + 1}', 'marks': []}], | |
}], | |
} for i in range(paragraphs)], | |
}, | |
} | |
old = generate_document(paragraphs=2) | |
new = generate_document(paragraphs=1) | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Paragraph 1', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'isDeleted': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'isDeleted': True, | |
'leaves': [{'object': 'leaf', 'isDeleted': True, 'text': 'Paragraph 2', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
}) | |
def test_when_nested_block_is_removed_then_returns_correct_output(self): | |
old = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'My first paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'bulleted-list', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'list-item', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'my item 1', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'bulleted-list', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'list-item', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'my item 1.1', 'marks': []}], | |
}], | |
}], | |
}], | |
}, | |
], | |
}], | |
}, | |
], | |
}, | |
} | |
new = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'My first paragraph', 'marks': []}], | |
}], | |
}], | |
}, | |
} | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'My first paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'bulleted-list', | |
'isVoid': False, | |
'isDeleted': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'list-item', | |
'isVoid': False, | |
'isDeleted': True, | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'isDeleted': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'isDeleted': True, | |
'leaves': [{ | |
'object': 'leaf', | |
'text': 'my item 1', | |
'marks': [], | |
'isDeleted': True, | |
}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'bulleted-list', | |
'isVoid': False, | |
'isDeleted': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'list-item', | |
'isVoid': False, | |
'isDeleted': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'isDeleted': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'isDeleted': True, | |
'leaves': [{ | |
'object': 'leaf', | |
'text': 'my item 1.1', | |
'marks': [], | |
'isDeleted': True, | |
}], | |
}], | |
}], | |
}], | |
}, | |
], | |
}], | |
}, | |
], | |
}, | |
}) | |
def test_when_middle_block_is_removed_then_returns_correct_output(self): | |
old = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
new = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'isDeleted': True, | |
'nodes': [{ | |
'object': 'text', | |
'isDeleted': True, | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': [], 'isDeleted': True}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
}) | |
def test_when_multiple_middle_blocks_are_removed_then_returns_correct_output(self): | |
old = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Fourth paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Fifth paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
new = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Fifth paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'isDeleted': True, | |
'nodes': [{ | |
'object': 'text', | |
'isDeleted': True, | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': [], 'isDeleted': True}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'isDeleted': True, | |
'nodes': [{ | |
'object': 'text', | |
'isDeleted': True, | |
'leaves': [{'object': 'leaf', 'text': 'Fourth paragraph', 'marks': [], 'isDeleted': True}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Fifth paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
}) | |
def test_when_middle_block_is_added_then_returns_correct_output(self): | |
old = { | |
'object': 'value', | |
'document': { | |
'data': {}, | |
'object': 'document', | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'data': {}, | |
'isVoid': False, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
new = { | |
'object': 'value', | |
'document': { | |
'data': {}, | |
'object': 'document', | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'data': {}, | |
'isVoid': False, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'isAdded': True, | |
'nodes': [{ | |
'object': 'text', | |
'isAdded': True, | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': [], 'isAdded': True}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
}) | |
def test_when_multiple_middle_blocks_are_added_then_returns_correct_output(self): | |
old = { | |
'object': 'value', | |
'document': { | |
'data': {}, | |
'object': 'document', | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'data': {}, | |
'isVoid': False, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Fifth paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
new = { | |
'object': 'value', | |
'document': { | |
'data': {}, | |
'object': 'document', | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'data': {}, | |
'isVoid': False, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Fourth paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Fifth paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'isAdded': True, | |
'nodes': [{ | |
'object': 'text', | |
'isAdded': True, | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': [], 'isAdded': True}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'isAdded': True, | |
'nodes': [{ | |
'object': 'text', | |
'isAdded': True, | |
'leaves': [{'object': 'leaf', 'text': 'Fourth paragraph', 'marks': [], 'isAdded': True}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Fifth paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
}) | |
def test_when_block_is_moved_backwards_then_returns_correct_output(self): | |
old = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'isVoid': False, | |
'type': 'paragraph', | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
new = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}] | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}] | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'isAdded': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'isAdded': True, | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': [], 'isAdded': True}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'isDeleted': True, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'isDeleted': True, | |
'leaves': [ | |
{'object': 'leaf', 'text': 'Third paragraph', 'marks': [], 'isDeleted': True}, | |
], | |
}], | |
}, | |
], | |
}, | |
}) | |
def test_when_block_is_moved_forwards_then_returns_correct_output(self): | |
old = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'isVoid': False, | |
'object': 'block', | |
'type': 'paragraph', | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}], | |
}], | |
}, | |
], | |
}, | |
} | |
new = { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'type': 'paragraph', | |
'object': 'block', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}] | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': []}] | |
}], | |
}, | |
], | |
}, | |
} | |
output = ContentDiffer(old, new) | |
self.assertDictEqual(output, { | |
'object': 'value', | |
'document': { | |
'object': 'document', | |
'data': {}, | |
'nodes': [ | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isDeleted': True, | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'isDeleted': True, | |
'leaves': [{'object': 'leaf', 'text': 'First paragraph', 'marks': [], 'isDeleted': True}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [{'object': 'leaf', 'text': 'Second paragraph', 'marks': []}], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'leaves': [ | |
{'object': 'leaf', 'text': 'Third paragraph', 'marks': []}, | |
], | |
}], | |
}, | |
{ | |
'object': 'block', | |
'type': 'paragraph', | |
'isAdded': True, | |
'isVoid': False, | |
'data': {}, | |
'nodes': [{ | |
'object': 'text', | |
'isAdded': True, | |
'leaves': [ | |
{'object': 'leaf', 'text': 'First paragraph', 'marks': [], 'isAdded': True}, | |
], | |
}], | |
}, | |
], | |
}, | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment