E-book viewer: Handle links to missing internal files with an error popup rather than becoming non-functional

This commit is contained in:
Kovid Goyal 2025-05-05 12:41:55 +05:30
parent c0f15325ed
commit 9d81359d8a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 30 additions and 17 deletions

View File

@ -13,7 +13,7 @@ from itertools import count
from lxml.etree import Comment from lxml.etree import Comment
from calibre import detect_ncpus, force_unicode, prepare_string_for_xml from calibre import detect_ncpus, prepare_string_for_xml
from calibre.customize.ui import plugin_for_input_format from calibre.customize.ui import plugin_for_input_format
from calibre.ebooks.oeb.base import EPUB, OEB_DOCS, OEB_STYLES, OPF, SMIL, XHTML, XHTML_NS, XLINK, rewrite_links, urlunquote from calibre.ebooks.oeb.base import EPUB, OEB_DOCS, OEB_STYLES, OPF, SMIL, XHTML, XHTML_NS, XLINK, rewrite_links, urlunquote
from calibre.ebooks.oeb.base import XPath as _XPath from calibre.ebooks.oeb.base import XPath as _XPath
@ -33,7 +33,7 @@ from polyglot.binary import as_base64_unicode as encode_component
from polyglot.binary import from_base64_bytes from polyglot.binary import from_base64_bytes
from polyglot.binary import from_base64_unicode as decode_component from polyglot.binary import from_base64_unicode as decode_component
from polyglot.builtins import as_bytes from polyglot.builtins import as_bytes
from polyglot.urllib import quote, urlparse from polyglot.urllib import urlparse
try: try:
from calibre_extensions.speedup import get_num_of_significant_chars from calibre_extensions.speedup import get_num_of_significant_chars
@ -98,9 +98,7 @@ def create_link_replacer(container, link_uid, changed):
frag = urlunquote(frag) frag = urlunquote(frag)
url = resource_template.format(encode_url(name, frag)) url = resource_template.format(encode_url(name, frag))
else: else:
if isinstance(name, str): url = 'missing:' + name
name = name.encode('utf-8')
url = 'missing:' + force_unicode(quote(name), 'utf-8')
changed.add(base) changed.add(base)
return url return url
@ -475,14 +473,18 @@ def transform_html(container, name, virtualize_resources, link_uid, link_to_map,
href = link_replacer(name, href) href = link_replacer(name, href)
elif attr in a.attrib: elif attr in a.attrib:
a.set(attr, 'javascript:void(0)') a.set(attr, 'javascript:void(0)')
if href and href.startswith(link_uid): if href:
a.set(attr, 'javascript:void(0)') if href.startswith(link_uid):
parts = href.split('|') a.set(attr, 'javascript:void(0)')
if len(parts) > 1: parts = href.split('|')
parts = decode_url(parts[1]) if len(parts) > 1:
lname, lfrag = parts[0], parts[1] parts = decode_url(parts[1])
link_to_map.setdefault(lname, {}).setdefault(lfrag or '', set()).add(name) lname, lfrag = parts[0], parts[1]
a.set('data-' + link_uid, json.dumps({'name':lname, 'frag':lfrag}, ensure_ascii=False)) link_to_map.setdefault(lname, {}).setdefault(lfrag or '', set()).add(name)
a.set('data-' + link_uid, json.dumps({'name':lname, 'frag':lfrag}, ensure_ascii=False))
elif href.startswith('missing:'):
a.set(attr, 'javascript:void(0)')
a.set('data-' + link_uid, json.dumps({'name':href[len('missing:'):], 'frag':'', 'missing': True}, ensure_ascii=False))
for a in link_xpath(root): for a in link_xpath(root):
handle_link(a) handle_link(a)
@ -519,8 +521,12 @@ def virtualize_html(container, name, link_uid, link_to_map, virtualized_names):
link_to_map.setdefault(lname, {}).setdefault(lfrag or '', set()).add(name) link_to_map.setdefault(lname, {}).setdefault(lfrag or '', set()).add(name)
a.set('data-' + link_uid, json.dumps({'name':lname, 'frag':lfrag}, ensure_ascii=False)) a.set('data-' + link_uid, json.dumps({'name':lname, 'frag':lfrag}, ensure_ascii=False))
elif href: elif href:
a.set('target', '_blank') if href.startswith('missing:'):
a.set('rel', 'noopener noreferrer') a.set(attr, 'javascript:void(0)')
a.set('data-' + link_uid, json.dumps({'name':href[len('missing:'):], 'frag':'', 'missing': True}, ensure_ascii=False))
else:
a.set('target', '_blank')
a.set('rel', 'noopener noreferrer')
elif attr in a.attrib: elif attr in a.attrib:
a.set(attr, 'javascript:void(0)') a.set(attr, 'javascript:void(0)')
@ -978,7 +984,7 @@ def develop(max_workers=1, wait_for_input=True):
with TemporaryDirectory() as tdir: with TemporaryDirectory() as tdir:
render( render(
path, tdir, serialize_metadata=True, path, tdir, serialize_metadata=True,
extract_annotations=True, virtualize_resources=True, max_workers=max_workers extract_annotations=True, virtualize_resources=False, max_workers=max_workers
) )
print('Extracted to:', tdir) print('Extracted to:', tdir)
if wait_for_input: if wait_for_input:

View File

@ -745,7 +745,10 @@ class IframeBoss:
except: except:
print('WARNING: Failed to parse link data {}, ignoring'.format(evt.currentTarget?.getAttribute?(self.link_attr))) print('WARNING: Failed to parse link data {}, ignoring'.format(evt.currentTarget?.getAttribute?(self.link_attr)))
return return
self.activate_link(data.name, data.frag, evt.currentTarget) if data.missing:
self.send_message('link_to_missing_activated', name=data.name)
else:
self.activate_link(data.name, data.frag, evt.currentTarget)
def activate_link(self, name, frag, target_elem): def activate_link(self, name, frag, target_elem):
if not name: if not name:

View File

@ -254,6 +254,7 @@ class View:
'report_cfi': self.on_report_cfi, 'report_cfi': self.on_report_cfi,
'request_size': self.on_request_size, 'request_size': self.on_request_size,
'scroll_to_anchor': self.on_scroll_to_anchor, 'scroll_to_anchor': self.on_scroll_to_anchor,
'link_to_missing_activated': self.on_link_to_missing_activated,
'selectionchange': self.on_selection_change, 'selectionchange': self.on_selection_change,
'update_selection_position': self.update_selection_position, 'update_selection_position': self.update_selection_position,
'columns_per_screen_changed': self.on_columns_per_screen_changed, 'columns_per_screen_changed': self.on_columns_per_screen_changed,
@ -1122,6 +1123,9 @@ class View:
frag = item.frag or '' frag = item.frag or ''
self.show_name(name, initial_position={'type':'pagelist_ref', 'anchor':frag, 'replace_history':False}) self.show_name(name, initial_position={'type':'pagelist_ref', 'anchor':frag, 'replace_history':False})
def on_link_to_missing_activated(self, data):
ui_operations.show_error(_('Invalid link'), _('This link points to the file {} which does not exist in the book').format(data.name))
def on_scroll_to_anchor(self, data): def on_scroll_to_anchor(self, data):
self.show_name(data.name, initial_position={'type':'anchor', 'anchor':data.frag, 'replace_history':False}) self.show_name(data.name, initial_position={'type':'anchor', 'anchor':data.frag, 'replace_history':False})