Popup footnotes now work

This commit is contained in:
Kovid Goyal 2014-11-05 18:45:00 +05:30
parent d559fad378
commit f8e54c26e3
3 changed files with 135 additions and 18 deletions

View File

@ -29,17 +29,21 @@ inline_styles = (node) ->
return cnode return cnode
is_footnote_link = (node, url) -> get_epub_type = (node, possible_values) ->
if not url or url.substr(0, 'file://'.length).toLowerCase() != 'file://' # Try to get the value of the epub:type attribute. Complex as we dont
return false # Ignore non-local links # operate in XML mode
# Check for epub:type="noteref", a little complex as we dont operate in XML
# mode
epub_type = node.getAttributeNS("http://www.idpf.org/2007/ops", 'type') or node.getAttribute('epub:type') epub_type = node.getAttributeNS("http://www.idpf.org/2007/ops", 'type') or node.getAttribute('epub:type')
if not epub_type if not epub_type
for x in node.attributes # consider any xxx:type="noteref" attribute as marking a note 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' if x.nodeName and x.nodeValue in possible_values and x.nodeName.slice(-':type'.length) == ':type'
epub_type = 'noteref' epub_type = x.nodeValue
break 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' if epub_type and epub_type.toLowerCase() == 'noteref'
return true return true
@ -61,6 +65,35 @@ is_footnote_link = (node, url) ->
return false 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 class CalibreExtract
# This class is a namespace to expose functions via the # This class is a namespace to expose functions via the
# window.calibre_extract object. # window.calibre_extract object.
@ -88,6 +121,39 @@ class CalibreExtract
ans[url] = 1 ans[url] = 1
return JSON.stringify(ans) 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? if window?
window.calibre_extract = new CalibreExtract() window.calibre_extract = new CalibreExtract()

View File

@ -49,7 +49,8 @@ def apply_basic_settings(settings):
settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.PluginsEnabled, False)
settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False) settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False)
settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, 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.setAttribute(QWebSettings.NotificationsEnabled, False)
settings.setThirdPartyCookiePolicy(QWebSettings.AlwaysBlockThirdPartyCookies) settings.setThirdPartyCookiePolicy(QWebSettings.AlwaysBlockThirdPartyCookies)
@ -1319,6 +1320,7 @@ class DocumentView(QWebView): # {{{
if url.isValid() and self.manager is not None: if url.isValid() and self.manager is not None:
fd = self.footnotes.get_footnote_data(url) fd = self.footnotes.get_footnote_data(url)
if fd: if fd:
self.footnotes.show_footnote(fd)
self.manager.show_footnote_view() self.manager.show_footnote_view()
ev.accept() ev.accept()
return return

View File

@ -9,20 +9,55 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import json import json
from collections import defaultdict 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.QtWebKitWidgets import QWebView, QWebPage
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from calibre import prints from calibre import prints
from calibre.constants import DEBUG
from calibre.ebooks.oeb.display.webview import load_html
class FootnotesPage(QWebPage): class FootnotesPage(QWebPage):
def __init__(self, parent): def __init__(self, parent):
QWebPage.__init__(self, parent) QWebPage.__init__(self, parent)
self.js_loader = None
self._footnote_data = ''
from calibre.gui2.viewer.documentview import apply_basic_settings from calibre.gui2.viewer.documentview import apply_basic_settings
settings = self.settings() settings = self.settings()
apply_basic_settings(settings) apply_basic_settings(settings)
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, False) 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): class FootnotesView(QWidget):
@ -31,10 +66,12 @@ class FootnotesView(QWidget):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.l = l = QHBoxLayout(self) self.l = l = QHBoxLayout(self)
self.view = v = QWebView(self) self.view = v = QWebView(self)
self._page = FootnotesPage(v)
v.setPage(self._page)
l.addWidget(v) l.addWidget(v)
def page(self): def page(self):
return self.view.page() return self._page
def sizeHint(self): def sizeHint(self):
return QSize(400, 200) return QSize(400, 200)
@ -49,6 +86,8 @@ class Footnotes(object):
def set_footnotes_view(self, fv): def set_footnotes_view(self, fv):
self.footnotes_view = fv self.footnotes_view = fv
self.clone_settings() self.clone_settings()
fv.page().linkClicked.connect(self.view.link_clicked)
fv.page().js_loader = self.view.document.js_loader
def clone_settings(self): def clone_settings(self):
source = self.view.document.settings() source = self.view.document.settings()
@ -61,26 +100,28 @@ class Footnotes(object):
def clear(self): def clear(self):
self.footnote_data_cache = {} self.footnote_data_cache = {}
self.known_footnote_targets = defaultdict(set) self.known_footnote_targets = defaultdict(set)
self.showing_url = None
def spine_index(self, path): def spine_path(self, path):
try: 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): except (AttributeError, ValueError):
return -1 pass
def load_footnote_data(self, current_url): def load_footnote_data(self, current_url):
fd = self.footnote_data_cache[current_url] = {} fd = self.footnote_data_cache[current_url] = {}
try: try:
raw = self.view.document.javascript('window.calibre_extract.get_footnote_data()', typ='string') 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: if x not in fd:
qu = QUrl(x) qu = QUrl(x)
path = qu.toLocalFile() path = qu.toLocalFile()
si = self.spine_index(path) spath = self.spine_path(path)
if si > -1: if spath is not None:
target = qu.fragment(QUrl.FullyDecoded) target = qu.fragment(QUrl.FullyDecoded)
fd[qu.toString()] = (path, target) fd[qu.toString()] = (spath, target, qu)
self.known_footnote_targets[path].add(target) self.known_footnote_targets[spath].add(target)
except Exception: except Exception:
prints('Failed to get footnote data, with error:') prints('Failed to get footnote data, with error:')
import traceback import traceback
@ -95,3 +136,11 @@ class Footnotes(object):
if fd is None: if fd is None:
fd = self.load_footnote_data(current_url) fd = self.load_footnote_data(current_url)
return fd.get(qurl.toString()) 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]})