mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-02-16 00:09:36 -05:00
303 lines
11 KiB
Plaintext
303 lines
11 KiB
Plaintext
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
|
from __python__ import bound_methods, hash_literals
|
|
|
|
from dom import clear
|
|
from iframe_comm import IframeClient
|
|
from read_book.globals import runtime, current_spine_item, set_current_spine_item
|
|
from read_book.resources import finalize_resources, unserialize_html
|
|
from read_book.settings import (
|
|
apply_settings, set_color_scheme_class, update_settings
|
|
)
|
|
|
|
block_names = dict.fromkeys(v"['p', 'div', 'li', 'td', 'h1', 'h2', 'h2', 'h3', 'h4', 'h5', 'h6', 'body']", True).as_object()
|
|
block_display_styles = dict.fromkeys(v"['block', 'list-item', 'table-cell', 'table']", True).as_object()
|
|
|
|
def elem_roles(elem):
|
|
return {k.toLowerCase(): True for k in (elem.getAttribute('role') or '').split(' ')}
|
|
|
|
|
|
def epub_type(elem):
|
|
for a in elem.attributes:
|
|
if a.nodeName.toLowerCase() == 'epub:type' and a.nodeValue:
|
|
return a.nodeValue
|
|
|
|
|
|
def get_containing_block(node):
|
|
while node and node.tagName and block_names[node.tagName.toLowerCase()] is not True:
|
|
node = node.parentNode
|
|
return node
|
|
|
|
|
|
def is_footnote_link(a, dest_name, dest_frag, src_name, link_to_map):
|
|
roles = elem_roles(a)
|
|
if roles['doc-noteref'] or roles['doc-biblioref'] or roles['doc-glossref']:
|
|
return True
|
|
if roles['doc-link']:
|
|
return False
|
|
if epub_type(a) is 'noteref':
|
|
return True
|
|
|
|
# Check if node or any of its first few parents have vertical-align set
|
|
x, num = a, 3
|
|
while x and num > 0:
|
|
style = window.getComputedStyle(x)
|
|
if not is_footnote_link.inline_displays[style.display]:
|
|
break
|
|
if is_footnote_link.vert_aligns[style.verticalAlign]:
|
|
return True
|
|
x = x.parentNode
|
|
num -= 1
|
|
|
|
# Check if node has a single child with the appropriate css
|
|
children = [x for x in a.childNodes if x.nodeType is Node.ELEMENT_NODE]
|
|
if children.length == 1:
|
|
style = window.getComputedStyle(children[0])
|
|
if is_footnote_link.inline_displays[style.display] and is_footnote_link.vert_aligns[style.verticalAlign]:
|
|
text_children = [x for x in a.childNodes if x.nodeType is Node.TEXT_NODE and x.nodeValue and /\S+/.test(x.nodeValue)]
|
|
if not text_children.length:
|
|
return True
|
|
|
|
eid = a.getAttribute('id') or a.getAttribute('name')
|
|
files_linking_to_self = link_to_map[src_name]
|
|
if eid and files_linking_to_self:
|
|
files_linking_to_anchor = files_linking_to_self[eid] or v'[]'
|
|
if files_linking_to_anchor.length > 1 or (files_linking_to_anchor.length == 1 and files_linking_to_anchor[0] is not src_name):
|
|
# An <a href="..." id="..."> link that is linked back from some other
|
|
# file in the spine, most likely an endnote. We exclude links that are
|
|
# the only content of their parent block tag, as these are not likely
|
|
# to be endnotes.
|
|
cb = get_containing_block(a)
|
|
if not cb or cb.tagName.toLowerCase() == 'body':
|
|
return False
|
|
ltext = a.textContent
|
|
if not ltext:
|
|
return False
|
|
ctext = cb.textContent
|
|
if not ctext:
|
|
return False
|
|
if ctext.strip() is ltext.strip():
|
|
return False
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
is_footnote_link.inline_displays = {'inline': True, 'inline-block': True}
|
|
is_footnote_link.vert_aligns = {'sub': True, 'super': True, 'top': True, 'bottom': True}
|
|
|
|
|
|
def is_epub_footnote(node):
|
|
et = epub_type(node)
|
|
if et:
|
|
et = et.toLowerCase()
|
|
if et is 'note' or et is 'footnote' or et is 'rearnote':
|
|
return True
|
|
roles = elem_roles(node)
|
|
if roles['doc-note'] or roles['doc-footnote'] or roles['doc-rearnote']:
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_note_container(node):
|
|
while node and block_names[node.tagName.toLowerCase()] is not True and block_display_styles[window.getComputedStyle(node).display] is not True:
|
|
node = node.parentNode
|
|
return node
|
|
|
|
|
|
def get_parents_and_self(node):
|
|
ans = v'[]'
|
|
while node and node is not document.body:
|
|
ans.push(node)
|
|
node = node.parentNode
|
|
return ans
|
|
|
|
|
|
def get_page_break(node):
|
|
style = window.getComputedStyle(node)
|
|
return {
|
|
'before': get_page_break.on[style.getPropertyValue('page-break-before')] is True,
|
|
'after': get_page_break.on[style.getPropertyValue('page-break-after')] is True,
|
|
}
|
|
get_page_break.on = {'always': True, 'left': True, 'right': True}
|
|
|
|
|
|
def hide_children(node):
|
|
for child in node.childNodes:
|
|
if child.nodeType is Node.ELEMENT_NODE:
|
|
if child.do_not_hide:
|
|
hide_children(child)
|
|
v'delete child.do_not_hide'
|
|
else:
|
|
child.style.display = 'none'
|
|
|
|
|
|
def unhide_tree(elem):
|
|
elem.do_not_hide = True
|
|
for c in elem.getElementsByTagName('*'):
|
|
c.do_not_hide = True
|
|
|
|
|
|
ok_list_types = {'disc': True, 'circle': True, 'square': True}
|
|
|
|
|
|
def is_new_footnote_start(elem, start_elem):
|
|
c = get_note_container(elem)
|
|
return c is not start_elem and c is not start_elem.firstElementChild
|
|
|
|
|
|
def show_footnote(target, known_anchors):
|
|
if not target:
|
|
return
|
|
start_elem = document.getElementById(target)
|
|
if not start_elem:
|
|
return
|
|
start_elem = get_note_container(start_elem)
|
|
for elem in get_parents_and_self(start_elem):
|
|
elem.do_not_hide = True
|
|
style = window.getComputedStyle(elem)
|
|
if style.display is 'list-item' and ok_list_types[style.listStyleType] is not True:
|
|
# We cannot display list numbers since they will be
|
|
# incorrect as we are removing siblings of this element.
|
|
elem.style.listStyleType = 'none'
|
|
if is_epub_footnote(start_elem):
|
|
unhide_tree(start_elem)
|
|
else:
|
|
# Try to detect natural boundaries based on markup for this note
|
|
found_note_start = False
|
|
for elem in document.documentElement.getElementsByTagName('*'):
|
|
if found_note_start:
|
|
eid = elem.getAttribute('id')
|
|
if eid is not target and known_anchors[eid] and is_new_footnote_start(elem, start_elem):
|
|
# console.log('Breaking footnote on anchor: ' + elem.getAttribute('id'))
|
|
v'delete get_note_container(elem).do_not_hide'
|
|
break
|
|
pb = get_page_break(elem)
|
|
if pb.before:
|
|
# console.log('Breaking footnote on page break before')
|
|
break
|
|
if pb.after:
|
|
unhide_tree(elem)
|
|
# console.log('Breaking footnote on page break after')
|
|
break
|
|
elem.do_not_hide = True
|
|
else if elem is start_elem:
|
|
found_note_start = True
|
|
|
|
hide_children(document.body)
|
|
window.location.hash = '#' + target
|
|
|
|
|
|
class PopupIframeBoss:
|
|
|
|
def __init__(self):
|
|
handlers = {
|
|
'initialize': self.initialize,
|
|
'clear': self.on_clear,
|
|
'display': self.display,
|
|
}
|
|
self.comm = IframeClient(handlers, 'popup-iframe')
|
|
self.blob_url_map = {}
|
|
self.name = None
|
|
self.frag = None
|
|
self.link_attr = None
|
|
|
|
def initialize(self, data):
|
|
window.addEventListener('error', self.on_error)
|
|
|
|
def on_error(self, evt):
|
|
msg = evt.message
|
|
script_url = evt.filename
|
|
line_number = evt.lineno
|
|
column_number = evt.colno
|
|
error_object = evt.error
|
|
evt.stopPropagation(), evt.preventDefault()
|
|
if error_object is None:
|
|
# This happens for cross-domain errors (probably javascript injected
|
|
# into the browser via extensions/ userscripts and the like). It also
|
|
# happens all the time when using Chrome on Safari
|
|
console.log(f'Unhandled error from external javascript, ignoring: {msg} {script_url} {line_number}:{column_number}')
|
|
else:
|
|
console.log(error_object)
|
|
|
|
def display(self, data):
|
|
self.book = data.book
|
|
self.name = data.name
|
|
self.frag = data.frag
|
|
self.link_attr = 'data-' + self.book.manifest.link_uid
|
|
spine = self.book.manifest.spine
|
|
index = spine.indexOf(data.name)
|
|
set_current_spine_item({
|
|
'name':data.name,
|
|
'is_first':index is 0,
|
|
'is_last':index is spine.length - 1,
|
|
'index': index,
|
|
'initial_position':data.initial_position
|
|
})
|
|
update_settings(data.settings)
|
|
for name in self.blob_url_map:
|
|
window.URL.revokeObjectURL(self.blob_url_map[name])
|
|
document.body.style.removeProperty('font-family')
|
|
root_data, self.mathjax, self.blob_url_map = finalize_resources(self.book, data.name, data.resource_data)
|
|
self.resource_urls = unserialize_html(root_data, self.content_loaded, self.show_only_footnote, data.name)
|
|
|
|
def connect_links(self):
|
|
for a in document.body.querySelectorAll(f'a[{self.link_attr}]'):
|
|
a.addEventListener('click', self.link_activated)
|
|
if runtime.is_standalone_viewer:
|
|
# links with a target get turned into requests to open a new window by Qt
|
|
for a in document.body.querySelectorAll('a[target]'):
|
|
a.removeAttribute('target')
|
|
|
|
def link_activated(self, evt):
|
|
try:
|
|
data = JSON.parse(evt.currentTarget.getAttribute(self.link_attr))
|
|
except:
|
|
print('WARNING: Failed to parse link data {}, ignoring'.format(evt.currentTarget?.getAttribute?(self.link_attr)))
|
|
return
|
|
self.activate_link(data.name, data.frag, evt.currentTarget)
|
|
|
|
def activate_link(self, name, frag, target_elem):
|
|
if not name:
|
|
name = current_spine_item().name
|
|
try:
|
|
is_popup = is_footnote_link(target_elem, name, frag, current_spine_item().name, self.book.manifest.link_to_map or {})
|
|
title = target_elem.textContent
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
is_popup = False
|
|
title = ''
|
|
self.comm.send_message('link_activated', is_popup=is_popup, name=name, frag=frag, title=title)
|
|
|
|
def on_clear(self, data):
|
|
clear(document.head)
|
|
clear(document.body)
|
|
document.body.textContent = data.text
|
|
self.name = None
|
|
self.frag = None
|
|
|
|
def show_only_footnote(self):
|
|
known_anchors = {}
|
|
ltm = self.book.manifest.link_to_map?[self.name]
|
|
if ltm:
|
|
known_anchors = {k:True for k in Object.keys(ltm)}
|
|
show_footnote(self.frag, known_anchors)
|
|
|
|
def content_loaded(self):
|
|
if not self.comm.encrypted_communications:
|
|
window.setTimeout(self.content_loaded, 2)
|
|
return
|
|
self.connect_links()
|
|
# this is the loading styles used to suppress scrollbars during load
|
|
# added in unserialize_html
|
|
document.head.removeChild(document.head.firstChild)
|
|
document.body.classList.add('calibre-footnote-container')
|
|
set_color_scheme_class()
|
|
apply_settings(True)
|
|
self.comm.send_message('content_loaded', height=document.documentElement.scrollHeight + 25)
|
|
|
|
|
|
def main():
|
|
main.boss = PopupIframeBoss()
|