mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start work on implementing the EPUB 3 CFI standard
This commit is contained in:
parent
dc594ba4c8
commit
23bc88c681
@ -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/
|
||||
|
@ -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
|
||||
|
@ -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)))
|
||||
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():
|
||||
|
136
src/calibre/ebooks/oeb/display/cfi.coffee
Normal file
136
src/calibre/ebooks/oeb/display/cfi.coffee
Normal file
@ -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 <kovid@kovidgoyal.net>
|
||||
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()
|
25
src/calibre/ebooks/oeb/display/test/cfi-test.coffee
Normal file
25
src/calibre/ebooks/oeb/display/test/cfi-test.coffee
Normal file
@ -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 <kovid@kovidgoyal.net>
|
||||
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
|
||||
|
13
src/calibre/ebooks/oeb/display/test/test.html
Normal file
13
src/calibre/ebooks/oeb/display/test/test.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing CFI functionality</title>
|
||||
<script type="text/javascript" src="cfi.js"></script>
|
||||
<script type="text/javascript" src="cfi-test.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="first-h1" style="border: solid 1px red">Testing CFI functionality</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
26
src/calibre/ebooks/oeb/display/test/test.py
Normal file
26
src/calibre/ebooks/oeb/display/test/test.py
Normal file
@ -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 <kovid@kovidgoyal.net>'
|
||||
__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()
|
||||
|
98
src/calibre/utils/coffeescript.py
Normal file
98
src/calibre/utils/coffeescript.py
Normal file
@ -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 <kovid@kovidgoyal.net>'
|
||||
__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()
|
Loading…
x
Reference in New Issue
Block a user