calibre/src/pyj/read_book/footnotes.pyj

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()