From 23bc88c681a53349b37224ca345ed1f1dcf99536 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 14 Dec 2011 16:38:10 +0530 Subject: [PATCH] Start work on implementing the EPUB 3 CFI standard --- .bzrignore | 1 + session.vim | 2 +- setup/resources.py | 34 ++++- src/calibre/ebooks/oeb/display/cfi.coffee | 136 ++++++++++++++++++ .../ebooks/oeb/display/test/cfi-test.coffee | 25 ++++ src/calibre/ebooks/oeb/display/test/test.html | 13 ++ src/calibre/ebooks/oeb/display/test/test.py | 26 ++++ src/calibre/utils/coffeescript.py | 98 +++++++++++++ 8 files changed, 329 insertions(+), 6 deletions(-) create mode 100644 src/calibre/ebooks/oeb/display/cfi.coffee create mode 100644 src/calibre/ebooks/oeb/display/test/cfi-test.coffee create mode 100644 src/calibre/ebooks/oeb/display/test/test.html create mode 100644 src/calibre/ebooks/oeb/display/test/test.py create mode 100644 src/calibre/utils/coffeescript.py diff --git a/.bzrignore b/.bzrignore index 181ecdc71a..5f0e9d30b6 100644 --- a/.bzrignore +++ b/.bzrignore @@ -2,6 +2,7 @@ .check-cache.pickle src/calibre/plugins resources/images.qrc +src/calibre/ebooks/oeb/display/test/*.js resources/display/*.js src/calibre/manual/.build/ src/calibre/manual/cli/ diff --git a/session.vim b/session.vim index b2617c6334..eb3f3935d9 100644 --- a/session.vim +++ b/session.vim @@ -1,5 +1,5 @@ " Project wide builtins -let g:pyflakes_builtins = ["_", "dynamic_property", "__", "P", "I", "lopen", "icu_lower", "icu_upper", "icu_title", "ngettext"] +let $PYFLAKES_BUILTINS = "_,dynamic_property,__,P,I,lopen,icu_lower,icu_upper,icu_title,ngettext" python << EOFPY import os, sys diff --git a/setup/resources.py b/setup/resources.py index 012c50a82a..723c37f7eb 100644 --- a/setup/resources.py +++ b/setup/resources.py @@ -31,18 +31,29 @@ class Coffee(Command): # {{{ def add_options(self, parser): parser.add_option('--watch', '-w', action='store_true', default=False, help='Autocompile when .coffee files are changed') + parser.add_option('--show-js', action='store_true', default=False, + help='Display the generated javascript') def run(self, opts): - self.do_coffee_compile() + self.do_coffee_compile(opts) if opts.watch: try: while True: - time.sleep(1) - self.do_coffee_compile(timestamp=True) + time.sleep(0.5) + self.do_coffee_compile(opts, timestamp=True, + ignore_errors=True) except KeyboardInterrupt: pass - def do_coffee_compile(self, timestamp=False): + def show_js(self, jsfile): + from pygments.lexers import JavascriptLexer + from pygments.formatters import TerminalFormatter + from pygments import highlight + with open(jsfile, 'rb') as f: + raw = f.read() + print highlight(raw, JavascriptLexer(), TerminalFormatter()) + + def do_coffee_compile(self, opts, timestamp=False, ignore_errors=False): for toplevel, dest in self.COFFEE_DIRS.iteritems(): dest = self.j(self.RESOURCES, dest) for x in glob.glob(self.j(self.SRC, __appname__, toplevel, '*.coffee')): @@ -50,7 +61,20 @@ class Coffee(Command): # {{{ if self.newer(js, x): print ('\t%sCompiling %s'%(time.strftime('[%H:%M:%S] ') if timestamp else '', os.path.basename(x))) - subprocess.check_call(['coffee', '-c', '-o', dest, x]) + try: + subprocess.check_call(['coffee', '-c', '-o', dest, x]) + except: + print ('\n\tCompilation of %s failed'%os.path.basename(x)) + if ignore_errors: + with open(js, 'wb') as f: + f.write('# Compilation from coffeescript failed') + else: + raise SystemExit(1) + else: + if opts.show_js: + self.show_js(js) + print ('#'*80) + print ('#'*80) def clean(self): for toplevel, dest in self.COFFEE_DIRS.iteritems(): diff --git a/src/calibre/ebooks/oeb/display/cfi.coffee b/src/calibre/ebooks/oeb/display/cfi.coffee new file mode 100644 index 0000000000..ecee9de211 --- /dev/null +++ b/src/calibre/ebooks/oeb/display/cfi.coffee @@ -0,0 +1,136 @@ +#!/usr/bin/env coffee +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +### + Copyright 2011, Kovid Goyal + Released under the GPLv3 License +### + +log = (error) -> + if error and window?.console?.log + window.console.log(error) + +fstr = (d) -> # {{{ + # Convert a timestamp floating point number to a string + ans = "" + if ( d < 0 ) + ans = "-" + d = -d + n = Math.floor(d) + ans += n + n = Math.round((d-n)*100) + if( n != 0 ) + ans += "." + ans += if (n % 10 == 0) then (n/10) else n + ans +# }}} + +class CanonicalFragmentIdentifier + + # This class is a namespace to expose CFI functions via the window.cfi + # object + + constructor: () -> + + encode: (doc, node, offset, tail) -> # {{{ + cfi = tail or "" + + # Handle the offset, if any + switch node.nodeType + when 1 # Element node + if typeoff(offset) == 'number' + node = node.childNodes.item(offset) + when 3, 4, 5, 6 # Text/entity/CDATA node + offset or= 0 + while true + p = node.previousSibling + if (p?.nodeType not in [3, 4, 5, 6]) + break + offset += p.nodeValue.length + node = p + cfi = ":" + offset + cfi + else # Not handled + log("Offsets for nodes of type #{ node.nodeType } are not handled") + + # Construct the path to node from root + until node == doc + p = node.parentNode + if not p + if node.nodeType == 9 # Document node (iframe) + win = node.defaultView + if win.frameElement + node = win.frameElement + cfi = "!" + cfi + continue + break + # Increase index by the length of all previous sibling text nodes + index = 0 + child = p.firstChild + while true + index |= 1 + if child.nodeType in [1, 7] + index++ + if child == node + break + child = child.nextSibling + + # Add id assertions for robustness where possible + id = node.getAttribute?('id') + idspec = if id then "[#{ id }]" else '' + cfi = '/' + index + idspec + cfi + node = p + + cfi + # }}} + + at: (x, y, doc=window.document) -> # {{{ + cdoc = doc + target = null + cwin = cdoc.defaultView + tail = '' + offset = null + name = null + + # Drill down into iframes, etc. + while true + target = cdoc.elementFromPoint x, y + if not target or target.localName == 'html' + log("No element at (#{ x }, #{ y })") + return null + + name = target.localName + if name not in ['iframe', 'embed', 'object'] + break + + cd = target.contentDocument + if not cd + break + + x = x + cwin.pageXOffset - target.offsetLeft + y = y + cwin.pageYOffset - target.offsetTop + cdoc = cd + cwin = cdoc.defaultView + + target.normalize() + + if name in ['audio', 'video'] + tail = "~" + fstr target.currentTime + + else if name in ['img'] + px = ((x + cwin.scrollX - target.offsetLeft)*100)/target.offsetWidth + py = ((y + cwin.scrollY - target.offsetTop)*100)/target.offsetHeight + tail = "#{ tail }@#{ fstr px },#{ fstr py }" + else + if cdoc.caretRangeFromPoint # WebKit + range = cdoc.caretRangeFromPoint(x, y) + if range + target = range.startContainer + offset = range.startOffset + else + # TODO: implement a span bisection algorithm for UAs + # without caretRangeFromPoint (Gecko, IE) + + this.encode(doc, target, offset, tail) + # }}} + +window.cfi = new CanonicalFragmentIdentifier() diff --git a/src/calibre/ebooks/oeb/display/test/cfi-test.coffee b/src/calibre/ebooks/oeb/display/test/cfi-test.coffee new file mode 100644 index 0000000000..5519b8625a --- /dev/null +++ b/src/calibre/ebooks/oeb/display/test/cfi-test.coffee @@ -0,0 +1,25 @@ +#!/usr/bin/env coffee +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +### + Copyright 2011, Kovid Goyal + Released under the GPLv3 License +### + +viewport_top = (node) -> + node.offsetTop - window.pageYOffset + +viewport_left = (node) -> + node.offsetLeft - window.pageXOffset + + +window.onload = -> + h1 = document.getElementsByTagName('h1')[0] + x = h1.scrollLeft + 150 + y = viewport_top(h1) + h1.offsetHeight/2 + e = document.elementFromPoint x, y + if e.getAttribute('id') != 'first-h1' + alert 'Failed to find top h1' + return + alert window.cfi.at x, y + diff --git a/src/calibre/ebooks/oeb/display/test/test.html b/src/calibre/ebooks/oeb/display/test/test.html new file mode 100644 index 0000000000..df93620c2c --- /dev/null +++ b/src/calibre/ebooks/oeb/display/test/test.html @@ -0,0 +1,13 @@ + + + + Testing CFI functionality + + + + +

Testing CFI functionality

+ + + + diff --git a/src/calibre/ebooks/oeb/display/test/test.py b/src/calibre/ebooks/oeb/display/test/test.py new file mode 100644 index 0000000000..568cffe5e6 --- /dev/null +++ b/src/calibre/ebooks/oeb/display/test/test.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os + +try: + from calibre.utils.coffeescript import serve +except ImportError: + import init_calibre + if False: init_calibre, serve + from calibre.utils.coffeescript import serve + + +def run_devel_server(): + os.chdir(os.path.dirname(__file__)) + serve(['../cfi.coffee', 'cfi-test.coffee']) + +if __name__ == '__main__': + run_devel_server() + diff --git a/src/calibre/utils/coffeescript.py b/src/calibre/utils/coffeescript.py new file mode 100644 index 0000000000..4d6d54e7f6 --- /dev/null +++ b/src/calibre/utils/coffeescript.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +''' +Utilities to help with developing coffeescript based apps +''' +import time, SimpleHTTPServer, SocketServer, threading, os, subprocess + +class Server(threading.Thread): + + def __init__(self, port=8000): + threading.Thread.__init__(self) + self.port = port + self.daemon = True + Handler = SimpleHTTPServer.SimpleHTTPRequestHandler + self.httpd = SocketServer.TCPServer(("localhost", port), Handler) + + def run(self): + print('serving at localhost:%d'%self.port) + self.httpd.serve_forever() + + def end(self): + self.httpd.shutdown() + self.join() + +class Compiler(threading.Thread): + + def __init__(self, coffee_files): + threading.Thread.__init__(self) + self.daemon = True + if not isinstance(coffee_files, dict): + coffee_files = {x:os.path.splitext(os.path.basename(x))[0]+'.js' + for x in coffee_files} + a = os.path.abspath + self.src_map = {a(x):a(y) for x, y in coffee_files.iteritems()} + self.keep_going = True + + def run(self): + while self.keep_going: + for src, dest in self.src_map.iteritems(): + if self.newer(src, dest): + self.compile(src, dest) + time.sleep(0.1) + + def newer(self, src, dest): + try: + sstat = os.stat(src) + except: + time.sleep(0.01) + sstat = os.stat(src) + return (not os.access(dest, os.R_OK) or sstat.st_mtime > + os.stat(dest).st_mtime) + + def compile(self, src, dest): + with open(dest, 'wb') as f: + try: + subprocess.check_call(['coffee', '-c', '-p', src], stdout=f) + except: + print('Compilation of %s failed'%src) + f.seek(0) + f.truncate() + f.write('// Compilation of cofeescript failed') + + def end(self): + self.keep_going = False + self.join() + for x in self.src_map.itervalues(): + try: + os.remove(x) + except: + pass + +def serve(coffee_files, port=8000): + ws = Server(port=port) + comp = Compiler(coffee_files) + comp.start() + ws.start() + + try: + while True: + time.sleep(1) + if not comp.is_alive() or not ws.is_alive(): + print ('Worker failed') + raise SystemExit(1) + except KeyboardInterrupt: + pass + finally: + try: + comp.end() + except: + pass + ws.end()