From 631a907773d31e58c09594b3db965f2ba4370ceb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 30 Jun 2020 13:55:56 +0530 Subject: [PATCH] Implement merging of annotations in rapydscript --- src/pyj/read_book/annotations.pyj | 93 +++++++++++++++++++++++++++++++ src/pyj/test.pyj | 1 + src/pyj/test_annotations.pyj | 35 ++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 src/pyj/read_book/annotations.pyj create mode 100644 src/pyj/test_annotations.pyj diff --git a/src/pyj/read_book/annotations.pyj b/src/pyj/read_book/annotations.pyj new file mode 100644 index 0000000000..de12408f7f --- /dev/null +++ b/src/pyj/read_book/annotations.pyj @@ -0,0 +1,93 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2020, Kovid Goyal +from __python__ import bound_methods, hash_literals + +from read_book.cfi import create_cfi_cmp, cfi_sort_key + + +no_cfi = '/99999999' + + +def bookmark_get_cfi(b): + if b.pos_type is 'epubcfi': + return b.pos[8:-1] + + +def highlight_get_cfi(hl): + cfi = hl.start_cfi + if cfi: + return cfi[8:-1] + + +def sort_annot_list(annots, get_cfi_func): + key_map = {no_cfi: cfi_sort_key(no_cfi)} + for annot in annots: + cfi = get_cfi_func(annot) + if cfi and not key_map[cfi]: + key_map[cfi] = cfi_sort_key(cfi) + cfi_cmp = create_cfi_cmp(key_map) + annots.sort(def (a, b): + acfi = get_cfi_func(a) or no_cfi + bcfi = get_cfi_func(b) or no_cfi + return cfi_cmp(acfi, bcfi) + ) + + +def annots_descending_cmp(a, b): + return -1 if a.timestamp > b.timestamp else (1 if a.timestamp < b.timestamp else 0) + + +def merge_annots_with_identical_field(annots_a, annots_b, field, cfi_getter_func): + title_groups = {} + changed = False + all_annots = annots_a.concat(annots_b) + for a in all_annots: + q = title_groups[a[field]] + if not q: + q = title_groups[a[field]] = v'[]' + q.push(a) + for tg in Object.values(title_groups): + tg.sort(annots_descending_cmp) + seen = {} + ans = v'[]' + for a in all_annots: + title = a[field] + if not seen[title]: + seen[title] = True + candidates = title_groups[title] + if not changed and candidates.length > 1 and candidates[0].timestamp is not candidates[1].timestamp: + changed = True + ans.push(title_groups[title][0]) + if ans.length is not annots_a.length or ans.length is not annots_b.length: + changed = True + if changed: + sort_annot_list(ans, cfi_getter_func) + return changed, ans + + +field_map = {'bookmark': 'title', 'highlight': 'uuid'} +getter_map = {'bookmark': bookmark_get_cfi, 'highlight': highlight_get_cfi} + + +def merge_annot_lists(a, b, field): + key_field = field_map[field] + return merge_annots_with_identical_field(a, b, key_field, getter_map[field]) + + +def merge_annotation_maps(a, b): + updated = False + ans = {} + for field in field_map: + a_items = a[field] or v'[]' + b_items = b[field] or v'[]' + if not a_items.length: + ans[field] = b_items + continue + if not b_items.length: + ans[field] = a_items + continue + changed, ans[field] = merge_annot_lists(a_items, b_items, field) + if changed: + updated = True + + return updated, ans diff --git a/src/pyj/test.pyj b/src/pyj/test.pyj index d3751b00a6..95373dde7a 100644 --- a/src/pyj/test.pyj +++ b/src/pyj/test.pyj @@ -8,6 +8,7 @@ import initialize # noqa: unused-import import test_date # noqa: unused-import import test_utils # noqa: unused-import import read_book.test_cfi # noqa: unused-import +import test_annotations # noqa: unused-import from testing import registered_tests, reset_dom diff --git a/src/pyj/test_annotations.pyj b/src/pyj/test_annotations.pyj new file mode 100644 index 0000000000..651678445c --- /dev/null +++ b/src/pyj/test_annotations.pyj @@ -0,0 +1,35 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2020, Kovid Goyal +from __python__ import bound_methods, hash_literals + +from read_book.annotations import merge_annot_lists +from testing import assert_equal, test, assert_true + + +def bm(title, bmid, year=20, first_cfi_number=1): + return { + 'title': title, 'id': bmid, 'timestamp': str.format('20{}-06-29T03:21:48.895323+00:00', year), + 'pos_type': 'epubcfi', 'pos': str.format('epubcfi(/{}/4/8)', first_cfi_number) + } + + +def hl(uuid, hlid, year=20, first_cfi_number=1): + return { + 'uuid': uuid, 'id': hlid, 'timestamp': str.format('20{}-06-29T03:21:48.895323+00:00', year), + 'start_cfi': str.format('epubcfi(/{}/4/8)', first_cfi_number) + } + + +@test +def merging_annotations(): + for atype in 'bookmark highlight'.split(' '): + f = bm if atype == 'bookmark' else hl + a = [f('one', 1, 20, 2), f('two', 2, 20, 4), f('a', 3, 20, 16),] + b = [f('one', 10, 30, 2), f('two', 20, 10, 4), f('b', 30, 20, 8),] + changed, c = merge_annot_lists(a, b, atype) + assert_true(changed) + + def get_id(x): + return x.id + + assert_equal(list(map(get_id, c)), [10, 2, 30, 3])