Browser viewer: Show footnotes in a popup window

Works similar to the desktop calibre viewer, except that the popup
window is over the text instead of at the bottom.
This commit is contained in:
Kovid Goyal 2017-10-16 15:06:29 +05:30
parent 9ed99e752f
commit c690588ed0
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 107 additions and 7 deletions

View File

@ -5,8 +5,6 @@ particular order.
New features for the in-browser viewer
----------------------------------------
- Footnote popups
- Bookmarks and more generally, annotations such as highlighting text and
adding comments
@ -17,7 +15,7 @@ New features for the in-browser viewer
New features for the server generally
---------------------------------------
- Create a UI for making changes to the library such as editing metadta,
- Create a UI for making changes to the library such as editing metadata,
adding/deleting books, converting, sending by email, etc.
- Add a way to search the set of locally available books stored in offline

View File

@ -7,15 +7,17 @@ from read_book.comm import IframeClient
from read_book.resources import finalize_resources, unserialize_html
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 get_containing_block(node):
while node and node.tagName and get_containing_block.block_names[node.tagName.toLowerCase()] is not True:
while node and node.tagName and block_names[node.tagName.toLowerCase()] is not True:
node = node.parentNode
return node
get_containing_block.block_names = dict.fromkeys(v"['p', 'div', 'li', 'td', 'h1', 'h2', 'h2', 'h3', 'h4', 'h5', 'h6', 'body']", True).as_object()
def is_footnote_link(a, dest_name, dest_frag, src_name, link_to_map):
@ -74,6 +76,97 @@ 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):
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 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.body.getElementsByTagName('*'):
if found_note_start:
eid = elem.getAttribute('id')
if eid is not target and known_anchors[eid] and get_note_container(elem) is not 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):
@ -106,7 +199,7 @@ class PopupIframeBoss:
for name in self.blob_url_map:
window.URL.revokeObjectURL(self.blob_url_map[name])
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.resource_urls = unserialize_html(root_data, self.content_loaded, self.show_only_footnote)
def on_clear(self, data):
clear(document.head)
@ -115,5 +208,12 @@ class PopupIframeBoss:
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):
self.comm.send_message('content_loaded', height=document.documentElement.scrollHeight + 25)

View File

@ -220,7 +220,7 @@ def process_stack(stack, tag_map, ns_map, load_required, onload, resource_urls):
for v'var i = node.length - 1; i >= 1; i--': # noqa: unused-local
stack.push(v'[node[i], elem]')
def unserialize_html(serialized_data, proceed):
def unserialize_html(serialized_data, proceed, postprocess_dom):
tag_map = serialized_data.tag_map
tree = serialized_data.tree
ns_map = serialized_data.ns_map
@ -258,6 +258,8 @@ def unserialize_html(serialized_data, proceed):
for v'var i = body.length - 1; i >= 1; i--': # noqa: unused-local
stack.push(v'[body[i], document.body]')
process_stack(stack, tag_map, ns_map, load_required, onload, resource_urls)
if postprocess_dom:
postprocess_dom()
ev = document.createEvent('Event')
ev.initEvent('DOMContentLoaded', True, True)
document.dispatchEvent(ev)