From 087012501909c81634e1dab074655c8f66957c9b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 14 Jan 2019 18:34:13 +0530 Subject: [PATCH] Edit book: Add a convenience function to jump to the line corresponding to a cfi To use, boss.show_partial_cfi_in_editor(filename, cfi) --- src/calibre/ebooks/epub/cfi/parse.py | 37 ++++++++++++++++++++++++++++ src/calibre/ebooks/epub/cfi/tests.py | 36 ++++++++++++++++++++++++++- src/calibre/gui2/tweak_book/boss.py | 18 ++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/epub/cfi/parse.py b/src/calibre/ebooks/epub/cfi/parse.py index 74c6a98599..25c4f688af 100644 --- a/src/calibre/ebooks/epub/cfi/parse.py +++ b/src/calibre/ebooks/epub/cfi/parse.py @@ -204,3 +204,40 @@ def cfi_sort_key(cfi, only_path=True): step = steps[-1] if steps else {} offsets = (step.get('temporal_offset', 0), tuple(reversed(step.get('spatial_offset', (0, 0)))), step.get('text_offset', 0), ) return (step_nums, offsets) + + +def decode_cfi(root, cfi): + from lxml.etree import XPathEvalError + p = parser() + try: + pcfi = p.parse_path(cfi)[0] + except Exception: + import traceback + traceback.print_exc() + return + if not pcfi: + import sys + print ('Failed to parse CFI: %r' % pcfi, file=sys.stderr) + return + steps = get_steps(pcfi) + ans = root + for step in steps: + num = step.get('num', 0) + node_id = step.get('id') + try: + match = ans.xpath('descendant::*[@id="%s"]' % node_id) + except XPathEvalError: + match = () + if match: + ans = match[0] + continue + index = 0 + for child in ans.iterchildren('*'): + index |= 1 # increment index by 1 if it is even + index += 1 + if index == num: + ans = child + break + else: + return + return ans diff --git a/src/calibre/ebooks/epub/cfi/tests.py b/src/calibre/ebooks/epub/cfi/tests.py index 88baac1f2e..969e69536d 100644 --- a/src/calibre/ebooks/epub/cfi/tests.py +++ b/src/calibre/ebooks/epub/cfi/tests.py @@ -9,7 +9,7 @@ __copyright__ = '2014, Kovid Goyal ' import unittest from polyglot.builtins import map -from calibre.ebooks.epub.cfi.parse import parser, cfi_sort_key +from calibre.ebooks.epub.cfi.parse import parser, cfi_sort_key, decode_cfi class Tests(unittest.TestCase): @@ -96,6 +96,40 @@ class Tests(unittest.TestCase): ]: self.assertEqual(p.parse_path(raw), (path, leftover)) + def test_cfi_decode(self): + from calibre.ebooks.oeb.polish.parsing import parse + root = parse(''' + + + +

+

+

+

+

xxxyyy0123456789

+

+

+ … +

+

hellogoodbyetext hereadieutext there

+ + +''', line_numbers=True, linenumber_attribute='data-lnum') + body = root[-1] + + def test(cfi, expected): + self.assertIs(decode_cfi(root, cfi), expected) + + for cfi in '/4 /4[body01] /900[body01] /2[body01]'.split(): + test(cfi, body) + + for i in range(len(body)): + test('/4/{}'.format((i + 1)*2), body[i]) + + p = body[4] + test('/4/999[para05]', p) + test('/4/999[para05]/2', p[0]) + def find_tests(): return unittest.TestLoader().loadTestsFromTestCase(Tests) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 40ab7a165d..f1f4e27054 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -1404,6 +1404,24 @@ class Boss(QObject): if name is not None and getattr(ed, 'syntax', None) == 'html': self.gui.preview.sync_to_editor(name, ed.current_tag()) + def show_partial_cfi_in_editor(self, name, cfi): + editor = self.edit_file(name, 'html') + if not editor or not editor.has_line_numbers: + return False + from calibre.ebooks.oeb.polish.parsing import parse + from calibre.ebooks.epub.cfi.parse import decode_cfi + root = parse( + editor.get_raw_data(), decoder=lambda x: x.decode('utf-8'), + line_numbers=True, linenumber_attribute='data-lnum') + node = decode_cfi(root, cfi) + if node is not None: + lnum = node.get('data-lnum') + if lnum: + lnum = int(lnum) + editor.current_line = lnum + return True + return False + def goto_style_declaration(self, data): name = data['name'] editor = self.edit_file(name, syntax=data['syntax'])