diff --git a/sourcing/edit.py b/sourcing/edit.py new file mode 100644 index 0000000..653c458 --- /dev/null +++ b/sourcing/edit.py @@ -0,0 +1,89 @@ +import attr + +class EditOutOfRange(Exception): + pass + +def apply_delete(current_spans, edit): + assert edit + if not current_spans: + raise ValueError('edit is out of bounds') + + spans = [] + pos = 0 + edit_end = edit['start'] + len(edit['old']) + + cur_span = current_spans.pop(0) + while pos + cur_span.length < edit['start']: + spans.append(cur_span) + pos += cur_span.length + cur_span = current_spans.pop(0) + + if edit['start'] > pos: + new_span = attr.evolve(cur_span, length=edit['start'] - pos) + spans.append(new_span) + + while pos + cur_span.length < edit_end: + pos += cur_span.length + cur_span = current_spans.pop(0) + + if pos + cur_span.length != edit_end: + offset = cur_span.start - pos + new_start = offset + (edit_end - pos) + diff = new_start - cur_span.start + new_span = attr.evolve(cur_span, + length=cur_span.length - diff, + start=new_start) + spans.append(new_span) + + spans += current_spans + return spans + +def apply_insert(current_spans, edit): + if not current_spans and edit['0'] == 0: + return edit['span'] + + pos = 0 + spans = [] + cur_span = current_spans.pop(0) + while pos + cur_span.length < edit['start']: + spans.append(cur_span) + pos += cur_span.length + cur_span = current_spans.pop(0) + + if edit['start'] >= pos: + length_a = edit['start'] - pos + length_b = cur_span.length - length_a + + if length_a: + span_a = attr.evolve(cur_span, length=length_a) + pos += length_a + spans.append(span_a) + + spans.append(edit['span']) + pos += edit['span'].length + + if length_b: + span_b = attr.evolve(cur_span, + start=cur_span.start + length_a, + length=length_b) + spans.append(span_b) + + pos += length_b + else: + spans.append(edit['span']) + + spans += current_spans + return spans + +def apply_edits(spans, edits): + for edit in edits: + if edit['op'] == 'delete': + spans = apply_delete(spans, edit) + continue + if edit['op'] == 'insert': + spans = apply_insert(spans, edit) + continue + + return spans + + diff --git a/sourcing/span.py b/sourcing/span.py new file mode 100644 index 0000000..08f9cd8 --- /dev/null +++ b/sourcing/span.py @@ -0,0 +1,18 @@ +import attr + +def greater_than_zero(instance, attribute, value): + if value <= 0: + raise ValueError('must be greater than 0') + +def is_positive(instance, attribute, value): + if value < 0: + raise ValueError('must be positive') + +@attr.s +class Span: + url: int = attr.ib() + start: int = attr.ib(validator=is_positive) + length: int = attr.ib(validator=greater_than_zero) + + def end(self) -> int: + return self.start + self.length diff --git a/tests/test_edit.py b/tests/test_edit.py new file mode 100644 index 0000000..93eb59b --- /dev/null +++ b/tests/test_edit.py @@ -0,0 +1,54 @@ +from sourcing.span import Span +from sourcing.edit import apply_edits +import pytest + +def test_xanadoc_apply_delete_start(): + spans = [Span('http://test/test', 0, 11)] + + edits = [{'op': 'delete', 'start': 0, 'old': 'aaa'}] + spans = apply_edits(spans, edits) + assert spans == [Span('http://test/test', 3, 8)] + +def test_xanadoc_apply_delete_start_offset(): + offset = 14 + spans = [Span('http://test/test', offset, 11)] + + edits = [{'op': 'delete', 'start': 0, 'old': 'aaa'}] + spans = apply_edits(spans, edits) + assert spans == [Span('http://test/test', offset + 3, 8)] + +def test_xanadoc_apply_delete_middle(): + spans = [Span('http://test/test', 0, 11)] + + edits = [{'op': 'delete', 'start': 4, 'old': 'bbb'}] + spans = apply_edits(spans, edits) + assert spans == [Span('http://test/test', 0, 4), + Span('http://test/test', 7, 4)] + +def test_xanadoc_apply_delete_end(): + spans = [Span('http://test/test', 0, 11)] + + edits = [{'op': 'delete', 'start': 8, 'old': 'ccc'}] + spans = apply_edits(spans, edits) + assert spans == [Span('http://test/test', 0, 8)] + +def test_xanadoc_apply_delete_all(): + spans = [Span('http://test/test', 0, 3)] + + edits = [{'op': 'delete', 'start': 0, 'old': 'aaa'}] + spans = apply_edits(spans, edits) + assert spans == [] + +def test_xanadoc_apply_insert_start(): + existing_span = Span('http://test/test', 0, 8) + new_span = Span('http://test/new_span', 10, 4) + edits = [{'op': 'insert', 'start': 0, 'span': new_span}] + spans = apply_edits([existing_span], edits) + assert spans == [new_span, existing_span] + +def test_xanadoc_apply_insert_end(): + existing_span = Span('http://test/test', 0, 8) + new_span = Span('http://test/new_span', 10, 4) + edits = [{'op': 'insert', 'start': 8, 'span': new_span}] + spans = apply_edits([existing_span], edits) + assert spans == [existing_span, new_span]