From c690588ed02cc66f85b1b86c7bee35b3c101f51e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 16 Oct 2017 15:06:29 +0530 Subject: [PATCH] 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. --- src/calibre/srv/TODO.rst | 4 +- src/pyj/read_book/footnotes.pyj | 106 +++++++++++++++++++++++++++++++- src/pyj/read_book/resources.pyj | 4 +- 3 files changed, 107 insertions(+), 7 deletions(-) diff --git a/src/calibre/srv/TODO.rst b/src/calibre/srv/TODO.rst index 1cbc551a81..87f778564e 100644 --- a/src/calibre/srv/TODO.rst +++ b/src/calibre/srv/TODO.rst @@ -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 diff --git a/src/pyj/read_book/footnotes.pyj b/src/pyj/read_book/footnotes.pyj index 1d43a0d6d6..3f050d5088 100644 --- a/src/pyj/read_book/footnotes.pyj +++ b/src/pyj/read_book/footnotes.pyj @@ -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) diff --git a/src/pyj/read_book/resources.pyj b/src/pyj/read_book/resources.pyj index 22cdfe17fe..f38fde2098 100644 --- a/src/pyj/read_book/resources.pyj +++ b/src/pyj/read_book/resources.pyj @@ -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)