diff --git a/src/calibre/ebooks/oeb/display/extract.coffee b/src/calibre/ebooks/oeb/display/extract.coffee index a8499dd00e..6dd9c9b232 100644 --- a/src/calibre/ebooks/oeb/display/extract.coffee +++ b/src/calibre/ebooks/oeb/display/extract.coffee @@ -29,17 +29,21 @@ inline_styles = (node) -> return cnode -is_footnote_link = (node, url) -> - if not url or url.substr(0, 'file://'.length).toLowerCase() != 'file://' - return false # Ignore non-local links - # Check for epub:type="noteref", a little complex as we dont operate in XML - # mode +get_epub_type = (node, possible_values) -> + # Try to get the value of the epub:type attribute. Complex as we dont + # operate in XML mode epub_type = node.getAttributeNS("http://www.idpf.org/2007/ops", 'type') or node.getAttribute('epub:type') if not epub_type for x in node.attributes # consider any xxx:type="noteref" attribute as marking a note - if x.nodeName and x.nodeValue == 'noteref' and x.nodeName.slice(-':type'.length) == ':type' - epub_type = 'noteref' + if x.nodeName and x.nodeValue in possible_values and x.nodeName.slice(-':type'.length) == ':type' + epub_type = x.nodeValue break + return epub_type + +is_footnote_link = (node, url) -> + if not url or url.substr(0, 'file://'.length).toLowerCase() != 'file://' + return false # Ignore non-local links + epub_type = get_epub_type(node, ['noteref']) if epub_type and epub_type.toLowerCase() == 'noteref' return true @@ -61,6 +65,35 @@ is_footnote_link = (node, url) -> return false +is_epub_footnote = (node) -> + pv = ['note', 'footnote', 'rearnote'] + epub_type = get_epub_type(node, pv) + if epub_type and epub_type.toLowerCase() in pv + return true + return false + +get_parents_and_self = (node) -> + ans = [] + while node and node isnt document.body + ans.push(node) + node = node.parentNode + return ans + +get_page_break = (node) -> + style = getComputedStyle(node) + ans = {} + for x in ['before', 'after'] + ans[x] = style.getPropertyValue('page-break-'.concat(x)) in ['always', 'left', 'right'] + return ans + +hide_children = (node) -> + for child in node.childNodes + if child.nodeType == Node.ELEMENT_NODE + if child.do_not_hide + hide_children(child) + else + child.style.display = 'none' + class CalibreExtract # This class is a namespace to expose functions via the # window.calibre_extract object. @@ -88,6 +121,39 @@ class CalibreExtract ans[url] = 1 return JSON.stringify(ans) + show_footnote: (target, known_targets) -> + if not target + return + start_elem = document.getElementById(target) + if not start_elem + return + in_note = false + is_footnote_container = is_epub_footnote(start_elem) + for elem in get_parents_and_self(start_elem) + elem.do_not_hide = true + for elem in document.body.getElementsByTagName('*') + if in_note + if known_targets.hasOwnProperty(elem.getAttribute('id')) + in_note = false + continue + pb = get_page_break(elem) + if pb['before'] + in_note = false + else if pb['after'] + in_note = false + for child in elem.getElementsByTagName('*') + child.do_not_hide = true + else + elem.do_not_hide = true + else + if elem is start_elem + in_note = not is_footnote_container and not get_page_break(elem)['after'] + if not in_note + for child in elem.getElementsByTagName('*') + child.do_not_hide = true + hide_children(document.body) + location.hash = '#' + target + if window? window.calibre_extract = new CalibreExtract() diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 36bf1111f8..0facce5a8d 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -49,7 +49,8 @@ def apply_basic_settings(settings): settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False) settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, False) - settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) + # PrivateBrowsing disables console messages + # settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) settings.setAttribute(QWebSettings.NotificationsEnabled, False) settings.setThirdPartyCookiePolicy(QWebSettings.AlwaysBlockThirdPartyCookies) @@ -1319,6 +1320,7 @@ class DocumentView(QWebView): # {{{ if url.isValid() and self.manager is not None: fd = self.footnotes.get_footnote_data(url) if fd: + self.footnotes.show_footnote(fd) self.manager.show_footnote_view() ev.accept() return diff --git a/src/calibre/gui2/viewer/footnote.py b/src/calibre/gui2/viewer/footnote.py index 2c84759e9a..c42f0a92f1 100644 --- a/src/calibre/gui2/viewer/footnote.py +++ b/src/calibre/gui2/viewer/footnote.py @@ -9,20 +9,55 @@ __copyright__ = '2014, Kovid Goyal ' import json from collections import defaultdict -from PyQt5.Qt import QUrl, QWidget, QHBoxLayout, QSize +from PyQt5.Qt import QUrl, QWidget, QHBoxLayout, QSize, pyqtSlot from PyQt5.QtWebKitWidgets import QWebView, QWebPage from PyQt5.QtWebKit import QWebSettings from calibre import prints +from calibre.constants import DEBUG +from calibre.ebooks.oeb.display.webview import load_html class FootnotesPage(QWebPage): def __init__(self, parent): QWebPage.__init__(self, parent) + self.js_loader = None + self._footnote_data = '' from calibre.gui2.viewer.documentview import apply_basic_settings settings = self.settings() apply_basic_settings(settings) settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, False) + self.setLinkDelegationPolicy(self.DelegateAllLinks) + self.mainFrame().javaScriptWindowObjectCleared.connect(self.add_window_objects) + self.add_window_objects() + + def add_window_objects(self, add_ready_listener=True): + self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) + evaljs = self.mainFrame().evaluateJavaScript + if self.js_loader is not None: + for x in 'utils extract'.split(): + evaljs(self.js_loader.get(x)) + + @pyqtSlot(str) + def debug(self, msg): + prints(msg) + + @pyqtSlot(result=str) + def footnote_data(self): + return self._footnote_data + + def set_footnote_data(self, target, known_targets): + self._footnote_data = json.dumps({'target':target, 'known_targets':known_targets}) + if self._footnote_data: + self.mainFrame().evaluateJavaScript( + 'data = JSON.parse(py_bridge.footnote_data()); calibre_extract.show_footnote(data["target"], data["known_targets"])') + + def javaScriptAlert(self, frame, msg): + prints('FootnoteView:alert::', msg) + + def javaScriptConsoleMessage(self, msg, lineno, source_id): + if DEBUG: + prints('FootnoteView:%s:%s:'%(unicode(source_id), lineno), unicode(msg)) class FootnotesView(QWidget): @@ -31,10 +66,12 @@ class FootnotesView(QWidget): QWidget.__init__(self, parent) self.l = l = QHBoxLayout(self) self.view = v = QWebView(self) + self._page = FootnotesPage(v) + v.setPage(self._page) l.addWidget(v) def page(self): - return self.view.page() + return self._page def sizeHint(self): return QSize(400, 200) @@ -49,6 +86,8 @@ class Footnotes(object): def set_footnotes_view(self, fv): self.footnotes_view = fv self.clone_settings() + fv.page().linkClicked.connect(self.view.link_clicked) + fv.page().js_loader = self.view.document.js_loader def clone_settings(self): source = self.view.document.settings() @@ -61,26 +100,28 @@ class Footnotes(object): def clear(self): self.footnote_data_cache = {} self.known_footnote_targets = defaultdict(set) + self.showing_url = None - def spine_index(self, path): + def spine_path(self, path): try: - return self.view.manager.iterator.spine.index(path) + si = self.view.manager.iterator.spine.index(path) + return self.view.manager.iterator.spine[si] except (AttributeError, ValueError): - return -1 + pass def load_footnote_data(self, current_url): fd = self.footnote_data_cache[current_url] = {} try: raw = self.view.document.javascript('window.calibre_extract.get_footnote_data()', typ='string') - for x in json.loads(raw): + for x in json.loads(raw or '{}'): if x not in fd: qu = QUrl(x) path = qu.toLocalFile() - si = self.spine_index(path) - if si > -1: + spath = self.spine_path(path) + if spath is not None: target = qu.fragment(QUrl.FullyDecoded) - fd[qu.toString()] = (path, target) - self.known_footnote_targets[path].add(target) + fd[qu.toString()] = (spath, target, qu) + self.known_footnote_targets[spath].add(target) except Exception: prints('Failed to get footnote data, with error:') import traceback @@ -95,3 +136,11 @@ class Footnotes(object): if fd is None: fd = self.load_footnote_data(current_url) return fd.get(qurl.toString()) + + def show_footnote(self, fd): + path, target, self.showing_url = fd + + if hasattr(self, 'footnotes_view'): + if load_html(path, self.footnotes_view.view, codec=getattr(path, 'encoding', 'utf-8'), + mime_type=getattr(path, 'mime_type', 'text/html')): + self.footnotes_view.page().set_footnote_data(target, {k:True for k in self.known_footnote_targets[path]})