mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Port cfi.coffee to cfi.pyj
This commit is contained in:
parent
11a52bb0d7
commit
cc0e4f9dc7
73
src/calibre/ebooks/oeb/display/test-cfi/cfi-test.pyj
Normal file
73
src/calibre/ebooks/oeb/display/test-cfi/cfi-test.pyj
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
from cfi import scroll_to, at_current, at
|
||||||
|
|
||||||
|
def show_cfi():
|
||||||
|
if window.current_cfi:
|
||||||
|
scroll_to(window.current_cfi, def(x, y):
|
||||||
|
ms = document.getElementById("marker").style
|
||||||
|
ms.display = 'block'
|
||||||
|
ms.top = y - 30 + 'px'
|
||||||
|
ms.left = x - 1 + 'px'
|
||||||
|
)
|
||||||
|
|
||||||
|
def mark_and_reload(evt):
|
||||||
|
x = evt.clientX
|
||||||
|
y = evt.clientY
|
||||||
|
if evt.button is 2:
|
||||||
|
return # Right mouse click, generated only in firefox
|
||||||
|
|
||||||
|
elem = document.elementFromPoint(x, y)
|
||||||
|
if elem and elem.getAttribute('id') in ['reset', 'viewport_mode']:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Remove image in case the click was on the image itself, we want the cfi to
|
||||||
|
# be on the underlying element
|
||||||
|
ms = document.getElementById("marker")
|
||||||
|
ms.style.display = 'none'
|
||||||
|
|
||||||
|
if document.getElementById('viewport_mode').checked:
|
||||||
|
cfi = at_current()
|
||||||
|
scroll_to(cfi)
|
||||||
|
return
|
||||||
|
|
||||||
|
def fn():
|
||||||
|
try:
|
||||||
|
window.current_cfi = at(x, y)
|
||||||
|
except Exception as err:
|
||||||
|
alert(str.format("Failed to calculate cfi: {}", err.message))
|
||||||
|
return
|
||||||
|
if window.current_cfi:
|
||||||
|
epubcfi = 'epubcfi(' + window.current_cfi + ')'
|
||||||
|
ypos = window.pageYOffset
|
||||||
|
newloc = window.location.href.replace(/#.*$/, '') + "#" + ypos + epubcfi
|
||||||
|
window.location.replace(newloc)
|
||||||
|
window.location.reload()
|
||||||
|
|
||||||
|
setTimeout(fn, 1)
|
||||||
|
|
||||||
|
def frame_clicked(evt):
|
||||||
|
iframe = evt.target.ownerDocument.defaultView.frameElement
|
||||||
|
# We know that the offset parent of the iframe is body
|
||||||
|
# So we can easily calculate the event co-ords w.r.t. the browser window
|
||||||
|
rect = iframe.getBoundingClientRect()
|
||||||
|
x = evt.clientX + rect.left
|
||||||
|
y = evt.clientY + rect.top
|
||||||
|
mark_and_reload({'clientX':x, 'clientY':y, 'button':evt.button})
|
||||||
|
|
||||||
|
window.onload = def():
|
||||||
|
document.onclick = mark_and_reload
|
||||||
|
for iframe in document.getElementsByTagName("iframe"):
|
||||||
|
iframe.contentWindow.document.onclick = frame_clicked
|
||||||
|
|
||||||
|
r = window.location.hash.match(/#(\d*)epubcfi\((.+)\)$/)
|
||||||
|
if r:
|
||||||
|
window.current_cfi = r[2]
|
||||||
|
ypos = 0+r[1] if r[1] else 0
|
||||||
|
base = document.getElementById('first-h1').innerHTML
|
||||||
|
document.title = base + ": " + window.current_cfi
|
||||||
|
setTimeout(def():
|
||||||
|
show_cfi()
|
||||||
|
window.scrollTo(0, ypos)
|
||||||
|
, 100)
|
@ -1,6 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
<title>Testing EPUB CFI</title>
|
<title>Testing EPUB CFI</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Testing cfi.coffee</title>
|
<title>Testing cfi.coffee</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<script type="text/javascript" src="cfi-test.js"></script>
|
||||||
<script type="text/javascript" src="cfi.coffee"></script>
|
<script type="text/javascript" src="cfi.coffee"></script>
|
||||||
<script type="text/javascript" src="cfi-test.coffee"></script>
|
<script type="text/javascript" src="cfi-test.coffee"></script>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
39
src/calibre/ebooks/oeb/display/test-cfi/run_rapydscript.py
Normal file
39
src/calibre/ebooks/oeb/display/test-cfi/run_rapydscript.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
# 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, shutil, tempfile
|
||||||
|
import SimpleHTTPServer
|
||||||
|
import SocketServer
|
||||||
|
|
||||||
|
def run_devel_server():
|
||||||
|
base = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
tdir = tempfile.gettempdir()
|
||||||
|
dest = os.path.join(tdir, os.path.basename(base))
|
||||||
|
if os.path.exists(dest):
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
shutil.copytree(base, dest)
|
||||||
|
for i in range(5):
|
||||||
|
base = os.path.dirname(base)
|
||||||
|
shutil.copy(os.path.join(base, 'pyj', 'read_book', 'cfi.pyj'), dest)
|
||||||
|
os.chdir(dest)
|
||||||
|
from calibre.utils.rapydscript import compile_pyj
|
||||||
|
with lopen('cfi-test.pyj', 'rb') as f, lopen('cfi-test.js', 'wb') as js:
|
||||||
|
js.write(compile_pyj(f.read()).encode('utf-8'))
|
||||||
|
PORT = 8000
|
||||||
|
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
|
||||||
|
httpd = SocketServer.TCPServer(("", PORT), Handler)
|
||||||
|
print('Serving CFI test at http://localhost:%d' % PORT)
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_devel_server()
|
||||||
|
|
590
src/pyj/read_book/cfi.pyj
Normal file
590
src/pyj/read_book/cfi.pyj
Normal file
@ -0,0 +1,590 @@
|
|||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
# Based on code originally written by Peter Sorotkin
|
||||||
|
# http://code.google.com/p/epub-revision/source/browse/trunk/src/samples/cfi/epubcfi.js
|
||||||
|
#
|
||||||
|
# Improvements with respect to that code:
|
||||||
|
# 1. Works on all browsers (WebKit, Firefox and IE >= 9)
|
||||||
|
# 2. Works for content in elements that are scrollable (i.e. have their own scrollbars)
|
||||||
|
# 3. Much more comprehensive testing/error handling
|
||||||
|
# 4. Properly encodes/decodes assertions
|
||||||
|
# 5. Handles points in the padding of elements consistently
|
||||||
|
# 6. Has a utility method to calculate the CFI for the current viewport position robustly
|
||||||
|
#
|
||||||
|
# Tested on: Firefox 9, IE 9, Chromium 16, Qt WebKit 2.1
|
||||||
|
# The main entry points are:
|
||||||
|
# at(x, y): Maps a point to a CFI, if possible
|
||||||
|
# at_current(): Returns the CFI corresponding to the current viewport scroll location
|
||||||
|
#
|
||||||
|
# scroll_to(cfi): which scrolls the browser to a point corresponding to the
|
||||||
|
# given cfi, and returns the x and y co-ordinates of the point.
|
||||||
|
|
||||||
|
|
||||||
|
# CFI escaping {{{
|
||||||
|
escape_pat = /[\[\],^();~@!-]/g
|
||||||
|
unescape_pat = /[\^](.)/g
|
||||||
|
|
||||||
|
def escape_for_cfi(raw):
|
||||||
|
return (raw or '').replace(escape_pat, '^$&')
|
||||||
|
|
||||||
|
def unescape_from_cfi(raw):
|
||||||
|
return (raw or '').replace(unescape_pat, '$1')
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def 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 is not 0:
|
||||||
|
ans += "." + (n/10 if (n % 10 is 0) else n)
|
||||||
|
return ans
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def get_current_time(target): # {{{
|
||||||
|
return fstr(target.currentTime or 0)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def window_scroll_pos(w): # {{{
|
||||||
|
w = w or window
|
||||||
|
return w.pageXOffset, w.pageYOffset
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def viewport_to_document(x, y, doc): # {{{
|
||||||
|
doc = doc or window.document
|
||||||
|
while doc is not window.document:
|
||||||
|
# we are in a frame
|
||||||
|
frame = doc.defaultView.frameElement
|
||||||
|
rect = frame.getBoundingClientRect()
|
||||||
|
x += rect.left
|
||||||
|
y += rect.top
|
||||||
|
doc = frame.ownerDocument
|
||||||
|
wx, wy = window_scroll_pos(doc.defaultView)
|
||||||
|
x += wx
|
||||||
|
y += wy
|
||||||
|
return x, y
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Convert point to character offset {{{
|
||||||
|
def range_has_point(range_, x, y):
|
||||||
|
rects = range_.getClientRects()
|
||||||
|
for v'var i = 0; i < rects.length; i++':
|
||||||
|
rect = rects[i]
|
||||||
|
if (rect.left <= x <= rect.right) and (rect.top <= y <= rect.bottom):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def offset_in_text_node(node, range_, x, y):
|
||||||
|
limits = v'[0, node.nodeValue.length]'
|
||||||
|
while limits[0] is not limits[1]:
|
||||||
|
pivot = Math.floor( (limits[0] + limits[1]) / 2 )
|
||||||
|
lr = v'[limits[0], pivot]'
|
||||||
|
rr = v'[pivot+1, limits[1]]'
|
||||||
|
range_.setStart(node, pivot)
|
||||||
|
range_.setEnd(node, pivot+1)
|
||||||
|
if range_has_point(range_, x, y):
|
||||||
|
return pivot
|
||||||
|
range_.setStart(node, rr[0])
|
||||||
|
range_.setEnd(node, rr[1])
|
||||||
|
if range_has_point(range_, x, y):
|
||||||
|
limits = rr
|
||||||
|
continue
|
||||||
|
range_.setStart(node, lr[0])
|
||||||
|
range_.setEnd(node, lr[1])
|
||||||
|
if range_has_point(range_, x, y):
|
||||||
|
limits = lr
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
return limits[0]
|
||||||
|
|
||||||
|
def find_offset_for_point(x, y, node, cdoc):
|
||||||
|
range_ = cdoc.createRange()
|
||||||
|
child = node.firstChild
|
||||||
|
while child:
|
||||||
|
if Node.TEXT_NODE <= child.nodeType <= Node.ENTITY_NODE and child.nodeValue and child.nodeValue.length:
|
||||||
|
range_.setStart(child, 0)
|
||||||
|
range_.setEnd(child, child.nodeValue.length)
|
||||||
|
if range_has_point(range_, x, y):
|
||||||
|
return v'[child, offset_in_text_node(child, range_, x, y)]'
|
||||||
|
child = child.nextSibling
|
||||||
|
|
||||||
|
# The point must be after the last bit of text/in the padding/border, we dont know
|
||||||
|
# how to get a good point in this case
|
||||||
|
raise ValueError(str.format("Point ({}, {}) is in the padding/border of the node, so cannot calculate offset", x, y))
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def set_current_time(target, val): # {{{
|
||||||
|
if target.currentTime is undefined:
|
||||||
|
return
|
||||||
|
if target.readyState is 4 or target.readyState is "complete":
|
||||||
|
target.currentTime = val + 0
|
||||||
|
else:
|
||||||
|
target.addEventListener("canplay", def(): target.currentTime = val;, False)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def encode(doc, node, offset, tail): # {{{
|
||||||
|
cfi = tail or ""
|
||||||
|
|
||||||
|
# Handle the offset, if any
|
||||||
|
if node.nodeType is Node.ELEMENT_NODE:
|
||||||
|
if type(offset) is 'number':
|
||||||
|
node = node.childNodes.item(offset)
|
||||||
|
elif Node.TEXT_NODE <= node.nodeType <= Node.ENTITY_NODE:
|
||||||
|
offset = offset or 0
|
||||||
|
while True:
|
||||||
|
p = node.previousSibling
|
||||||
|
if not p or p.nodeType > Node.COMMENT_NODE:
|
||||||
|
break
|
||||||
|
# log("previous sibling:"+ p + " " + p?.nodeType + " length: " + p?.nodeValue?.length)
|
||||||
|
if p.nodeType not in v'[Node.ATTRIBUTE_NODE, Node.COMMENT_NODE]' and p.nodeValue and p.nodeValue.length:
|
||||||
|
offset += p.nodeValue.length
|
||||||
|
node = p
|
||||||
|
cfi = ":" + offset + cfi
|
||||||
|
else: # Not handled
|
||||||
|
print(str.format("Offsets for nodes of type {} are not handled", node.nodeType))
|
||||||
|
|
||||||
|
# Construct the path to node from root
|
||||||
|
while node is not doc:
|
||||||
|
p = node.parentNode
|
||||||
|
if not p:
|
||||||
|
if node.nodeType == Node.DOCUMENT_NODE: # Document node (iframe)
|
||||||
|
win = node.defaultView
|
||||||
|
if win.frameElement:
|
||||||
|
node = win.frameElement
|
||||||
|
cfi = "!" + cfi
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
# Find position of node in parent
|
||||||
|
index = 0
|
||||||
|
child = p.firstChild
|
||||||
|
while True:
|
||||||
|
index |= 1 # Increment index by 1 if it is even
|
||||||
|
if child.nodeType is Node.ELEMENT_NODE:
|
||||||
|
index += 1
|
||||||
|
if child is node:
|
||||||
|
break
|
||||||
|
child = child.nextSibling
|
||||||
|
|
||||||
|
# Add id assertions for robustness where possible
|
||||||
|
id = node.getAttribute('id') if node.getAttribute else None
|
||||||
|
idspec = ('[' + escape_for_cfi(id) + ']') if id else ''
|
||||||
|
cfi = '/' + index + idspec + cfi
|
||||||
|
node = p
|
||||||
|
|
||||||
|
return cfi
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def decode(cfi, doc): # {{{
|
||||||
|
doc = doc or window.document
|
||||||
|
simple_node_regex = ///
|
||||||
|
^/(\d+) # The node count
|
||||||
|
(\[[^\]]*\])? # The optional id assertion
|
||||||
|
///
|
||||||
|
error = None
|
||||||
|
node = doc
|
||||||
|
|
||||||
|
while cfi.length > 0 and not error:
|
||||||
|
r = cfi.match(simple_node_regex)
|
||||||
|
if r: # Path step
|
||||||
|
target = parseInt(r[1])
|
||||||
|
assertion = r[2]
|
||||||
|
if assertion:
|
||||||
|
assertion = unescape_from_cfi(assertion.slice(1, assertion.length-1))
|
||||||
|
index = 0
|
||||||
|
child = node.firstChild
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if not child:
|
||||||
|
if assertion: # Try to use the assertion to find the node
|
||||||
|
child = doc.getElementById(assertion)
|
||||||
|
if child:
|
||||||
|
node = child
|
||||||
|
if not child:
|
||||||
|
error = "No matching child found for CFI: " + cfi
|
||||||
|
cfi = cfi.substr(r[0].length)
|
||||||
|
break
|
||||||
|
index |= 1 # Increment index by 1 if it is even
|
||||||
|
if child.nodeType is 1:
|
||||||
|
index += 1
|
||||||
|
if index is target:
|
||||||
|
cfi = cfi.substr(r[0].length)
|
||||||
|
node = child
|
||||||
|
if assertion and node.id is not assertion:
|
||||||
|
# The found child does not match the id assertion,
|
||||||
|
# trust the id assertion if an element with that id
|
||||||
|
# exists
|
||||||
|
child = doc.getElementById(assertion)
|
||||||
|
if child:
|
||||||
|
node = child
|
||||||
|
break
|
||||||
|
child = child.nextSibling
|
||||||
|
|
||||||
|
else if cfi[0] is '!': # Indirection
|
||||||
|
if node.contentDocument:
|
||||||
|
node = node.contentDocument
|
||||||
|
cfi = cfi.substr(1)
|
||||||
|
else:
|
||||||
|
error = "Cannot reference " + node.nodeName + "'s content: " + cfi
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if error:
|
||||||
|
print(error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
point = {}
|
||||||
|
error = None
|
||||||
|
offset = None
|
||||||
|
|
||||||
|
r = cfi.match(/^:(\d+)/)
|
||||||
|
if r:
|
||||||
|
# Character offset
|
||||||
|
offset = parseInt(r[1])
|
||||||
|
cfi = cfi.substr(r[0].length)
|
||||||
|
|
||||||
|
r = cfi.match(/^~(-?\d+(\.\d+)?)/)
|
||||||
|
if r:
|
||||||
|
# Temporal offset
|
||||||
|
point.time = r[1] - 0 # Coerce to number
|
||||||
|
cfi = cfi.substr(r[0].length)
|
||||||
|
|
||||||
|
r = cfi.match(/^@(-?\d+(\.\d+)?):(-?\d+(\.\d+)?)/)
|
||||||
|
if r:
|
||||||
|
# Spatial offset
|
||||||
|
point.x = r[1] - 0 # Coerce to number
|
||||||
|
point.y = r[3] - 0 # Coerce to number
|
||||||
|
cfi = cfi.substr(r[0].length)
|
||||||
|
|
||||||
|
r = cfi.match(/^\[([^\]]+)\]/)
|
||||||
|
if r:
|
||||||
|
assertion = r[1]
|
||||||
|
cfi = cfi.substr(r[0].length)
|
||||||
|
r = assertion.match(/;s=([ab])$/)
|
||||||
|
if r:
|
||||||
|
if r.index > 0 and assertion[r.index - 1] is not '^':
|
||||||
|
assertion = assertion.substr(0, r.index)
|
||||||
|
point.forward = (r[1] is 'a')
|
||||||
|
assertion = unescape_from_cfi(assertion)
|
||||||
|
# TODO: Handle text assertion
|
||||||
|
|
||||||
|
# Find the text node that contains the offset
|
||||||
|
if node and node.parentNode:
|
||||||
|
node.parentNode.normalize()
|
||||||
|
if offset is not None:
|
||||||
|
while True:
|
||||||
|
l = node.nodeValue.length
|
||||||
|
if offset < l or (not point.forward and offset is l):
|
||||||
|
break
|
||||||
|
next = False
|
||||||
|
while True:
|
||||||
|
nn = node.nextSibling
|
||||||
|
if not nn:
|
||||||
|
break
|
||||||
|
if Node.TEXT_NODE <= nn.nodeType <= Node.ENTITY_NODE and nn.nodeValue and nn.nodeValue.length:
|
||||||
|
next = nn
|
||||||
|
break
|
||||||
|
node = nn
|
||||||
|
if not next:
|
||||||
|
if offset > l:
|
||||||
|
error = "Offset out of range: " + offset
|
||||||
|
offset = l
|
||||||
|
break
|
||||||
|
node = next
|
||||||
|
offset -= l
|
||||||
|
point.offset = offset
|
||||||
|
|
||||||
|
point.node = node
|
||||||
|
if error:
|
||||||
|
point.error = error
|
||||||
|
else if cfi.length > 0:
|
||||||
|
point.error = "Undecoded CFI: " + cfi
|
||||||
|
|
||||||
|
print(point.error)
|
||||||
|
|
||||||
|
return point
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def at(x, y, doc): # {{{
|
||||||
|
# x, y are in viewport co-ordinates
|
||||||
|
doc = doc or window.document
|
||||||
|
cdoc = doc
|
||||||
|
target = None
|
||||||
|
tail = ''
|
||||||
|
offset = None
|
||||||
|
name = None
|
||||||
|
|
||||||
|
# Drill down into iframes, etc.
|
||||||
|
while True:
|
||||||
|
target = cdoc.elementFromPoint(x, y)
|
||||||
|
if not target or target is cdoc.documentElement or target is cdoc.body:
|
||||||
|
# We ignore both html and body even though body could
|
||||||
|
# have text nodes under it as performance is very poor if body
|
||||||
|
# has large margins/padding (for e.g. in fullscreen mode)
|
||||||
|
# A possible solution for this is to wrap all text node
|
||||||
|
# children of body in <span> but that is seriously ugly and
|
||||||
|
# might have side effects. Lets do this only if there are lots of
|
||||||
|
# books in the wild that actually have text children of body,
|
||||||
|
# and even in this case it might be better to change the input
|
||||||
|
# plugin to prevent this from happening.
|
||||||
|
# log("No element at (#{ x }, #{ y })")
|
||||||
|
return None
|
||||||
|
|
||||||
|
name = target.localName
|
||||||
|
if name not in {'iframe', 'embed', 'object'}:
|
||||||
|
break
|
||||||
|
|
||||||
|
cd = target.contentDocument
|
||||||
|
if not cd:
|
||||||
|
break
|
||||||
|
|
||||||
|
# We have an embedded document, transforms x, y into the co-prd
|
||||||
|
# system of the embedded document's viewport
|
||||||
|
rect = target.getBoundingClientRect()
|
||||||
|
x -= rect.left
|
||||||
|
y -= rect.top
|
||||||
|
cdoc = cd
|
||||||
|
|
||||||
|
(target.parentNode or target).normalize()
|
||||||
|
|
||||||
|
if name in {'audio', 'video'}:
|
||||||
|
tail = "~" + get_current_time(target)
|
||||||
|
|
||||||
|
if name in {'img', 'video'}:
|
||||||
|
rect = target.getBoundingClientRect()
|
||||||
|
px = ((x - rect.left)*100)/target.offsetWidth
|
||||||
|
py = ((y - rect.top)*100)/target.offsetHeight
|
||||||
|
tail = str.format('{}@{}:{}', tail, fstr(px), fstr(py))
|
||||||
|
else if name is not 'audio':
|
||||||
|
# Get the text offset
|
||||||
|
# We use a custom function instead of caretRangeFromPoint as
|
||||||
|
# caretRangeFromPoint does weird things when the point falls in the
|
||||||
|
# padding of the element
|
||||||
|
target, offset = find_offset_for_point(x, y, target, cdoc)
|
||||||
|
|
||||||
|
return encode(doc, target, offset, tail)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def point(cfi, doc): # {{{
|
||||||
|
doc = doc or window.document
|
||||||
|
r = decode(cfi, doc)
|
||||||
|
if not r:
|
||||||
|
return None
|
||||||
|
node = r.node
|
||||||
|
ndoc = node.ownerDocument
|
||||||
|
if not ndoc:
|
||||||
|
print(str.format("CFI node has no owner document: {} {}", cfi, node))
|
||||||
|
return None
|
||||||
|
|
||||||
|
x = None
|
||||||
|
y = None
|
||||||
|
range_ = None
|
||||||
|
|
||||||
|
if type(r.offset) is "number":
|
||||||
|
# Character offset
|
||||||
|
range_ = ndoc.createRange()
|
||||||
|
if r.forward:
|
||||||
|
try_list = [{'start':0, 'end':0, 'a':0.5}, {'start':0, 'end':1, 'a':1}, {'start':-1, 'end':0, 'a':0}]
|
||||||
|
else:
|
||||||
|
try_list = [{'start':0, 'end':0, 'a':0.5}, {'start':-1, 'end':0, 'a':0}, {'start':0, 'end':1, 'a':1}]
|
||||||
|
a = None
|
||||||
|
rects = None
|
||||||
|
node_len = node.nodeValue.length
|
||||||
|
offset = r.offset
|
||||||
|
for v'var i = 0; i < 2; i++':
|
||||||
|
# Try reducing the offset by 1 if we get no match as if it refers to the position after the
|
||||||
|
# last character we wont get a match with getClientRects
|
||||||
|
offset = r.offset - i
|
||||||
|
if offset < 0:
|
||||||
|
offset = 0
|
||||||
|
k = 0
|
||||||
|
while (not rects or not rects.length) and k < try_list.length:
|
||||||
|
t = try_list[k]
|
||||||
|
k += 1
|
||||||
|
start_offset = offset + t.start
|
||||||
|
end_offset = offset + t.end
|
||||||
|
a = t.a
|
||||||
|
if start_offset < 0 or end_offset >= node_len:
|
||||||
|
continue
|
||||||
|
range_.setStart(node, start_offset)
|
||||||
|
range_.setEnd(node, end_offset)
|
||||||
|
rects = range_.getClientRects()
|
||||||
|
|
||||||
|
if not rects or not rects.length:
|
||||||
|
print(str.format("Could not find caret position: rects: {} offset: {}", rects, r.offset))
|
||||||
|
return None
|
||||||
|
|
||||||
|
else:
|
||||||
|
x, y = r.x, r.y
|
||||||
|
|
||||||
|
return {'x':x, 'y':y, 'node':r.node, 'time':r.time, 'range':range_, 'a':a}
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def scroll_to(cfi, callback, doc): # {{{
|
||||||
|
doc = doc or window.doc
|
||||||
|
# TODO: Port MathJax support
|
||||||
|
if window.mathjax and window.mathjax.math_present and not window.mathjax.math_loaded:
|
||||||
|
window.mathjax.pending_cfi = v'[cfi, callback]'
|
||||||
|
return
|
||||||
|
point_ = point(cfi, doc)
|
||||||
|
if not point_:
|
||||||
|
print("No point found for cfi: " + cfi)
|
||||||
|
return
|
||||||
|
if type(point_.time) is 'number':
|
||||||
|
set_current_time(point_.node, point_.time)
|
||||||
|
|
||||||
|
if point_.range is not None:
|
||||||
|
# Character offset
|
||||||
|
r = point_.range
|
||||||
|
so, eo, sc, ec = r.startOffset, r.endOffset, r.startContainer, r.endContainer
|
||||||
|
node = r.startContainer
|
||||||
|
ndoc = node.ownerDocument
|
||||||
|
span = ndoc.createElement('span')
|
||||||
|
span.setAttribute('style', 'border-width: 0; padding: 0; margin: 0')
|
||||||
|
r.surroundContents(span)
|
||||||
|
span.scrollIntoView()
|
||||||
|
fn = def():
|
||||||
|
# Remove the span and get the new position now that scrolling
|
||||||
|
# has (hopefully) completed
|
||||||
|
#
|
||||||
|
# In WebKit, the boundingrect of the span is wrong in some
|
||||||
|
# situations, whereas in IE resetting the range causes it to
|
||||||
|
# loose bounding info. So we use the range's rects unless they
|
||||||
|
# are absent, in which case we use the span's rect
|
||||||
|
#
|
||||||
|
rect = span.getBoundingClientRect()
|
||||||
|
|
||||||
|
# Remove the span we inserted
|
||||||
|
p = span.parentNode
|
||||||
|
for node in span.childNodes:
|
||||||
|
span.removeChild(node)
|
||||||
|
p.insertBefore(node, span)
|
||||||
|
p.removeChild(span)
|
||||||
|
p.normalize()
|
||||||
|
|
||||||
|
# Reset the range to what it was before the span was added
|
||||||
|
r.setStart(sc, so)
|
||||||
|
r.setEnd(ec, eo)
|
||||||
|
rects = r.getClientRects()
|
||||||
|
if rects.length > 0:
|
||||||
|
rect = rects[0]
|
||||||
|
|
||||||
|
x = (point_.a*rect.left + (1-point_.a)*rect.right)
|
||||||
|
y = (rect.top + rect.bottom)/2
|
||||||
|
x, y = viewport_to_document(x, y, ndoc)
|
||||||
|
if callback:
|
||||||
|
callback(x, y)
|
||||||
|
else:
|
||||||
|
node = point_.node
|
||||||
|
node.scrollIntoView()
|
||||||
|
|
||||||
|
fn = def():
|
||||||
|
r = node.getBoundingClientRect()
|
||||||
|
x, y = viewport_to_document(r.left, r.top, node.ownerDocument)
|
||||||
|
if type(point_.x) is 'number' and node.offsetWidth:
|
||||||
|
x += (point_.x*node.offsetWidth)/100
|
||||||
|
if type(point_.y) is 'number' and node.offsetHeight:
|
||||||
|
y += (point_.y*node.offsetHeight)/100
|
||||||
|
window.scrollTo(x, y)
|
||||||
|
if callback:
|
||||||
|
callback(x, y)
|
||||||
|
|
||||||
|
setTimeout(fn, 10)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def at_point(ox, oy): # {{{
|
||||||
|
# The CFI at the specified point. Different to at() in that this method
|
||||||
|
# returns null if there is an error, and also calculates a point from
|
||||||
|
# the CFI and returns null if the calculated point is far from the
|
||||||
|
# original point.
|
||||||
|
|
||||||
|
def dist(p1, p2):
|
||||||
|
Math.sqrt(Math.pow(p1[0]-p2[0], 2), Math.pow(p1[1]-p2[1], 2))
|
||||||
|
|
||||||
|
try:
|
||||||
|
cfi = window.cfi.at(ox, oy)
|
||||||
|
point = window.cfi.point(cfi)
|
||||||
|
except Exception:
|
||||||
|
cfi = None
|
||||||
|
|
||||||
|
|
||||||
|
if cfi:
|
||||||
|
if point.range is not None:
|
||||||
|
r = point.range
|
||||||
|
rect = r.getClientRects()[0]
|
||||||
|
|
||||||
|
x = (point.a*rect.left + (1-point.a)*rect.right)
|
||||||
|
y = (rect.top + rect.bottom)/2
|
||||||
|
x, y = viewport_to_document(x, y, r.startContainer.ownerDocument)
|
||||||
|
else:
|
||||||
|
node = point.node
|
||||||
|
r = node.getBoundingClientRect()
|
||||||
|
x, y = viewport_to_document(r.left, r.top, node.ownerDocument)
|
||||||
|
if type(point.x) is 'number' and node.offsetWidth:
|
||||||
|
x += (point.x*node.offsetWidth)/100
|
||||||
|
if type(point.y) is 'number' and node.offsetHeight:
|
||||||
|
y += (point.y*node.offsetHeight)/100
|
||||||
|
|
||||||
|
if dist(viewport_to_document(ox, oy), v'[x, y]') > 50:
|
||||||
|
cfi = None
|
||||||
|
|
||||||
|
return cfi
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def at_current(): # {{{
|
||||||
|
winx, winy = window_scroll_pos()
|
||||||
|
winw, winh = window.innerWidth, window.innerHeight
|
||||||
|
max = Math.max
|
||||||
|
winw = max(winw, 400)
|
||||||
|
winh = max(winh, 600)
|
||||||
|
deltay = Math.floor(winh/50)
|
||||||
|
deltax = Math.floor(winw/25)
|
||||||
|
miny = max(-winy, -winh)
|
||||||
|
maxy = winh
|
||||||
|
minx = max(-winx, -winw)
|
||||||
|
maxx = winw
|
||||||
|
|
||||||
|
def x_loop(cury):
|
||||||
|
for direction in v'[-1, 1]':
|
||||||
|
delta = deltax * direction
|
||||||
|
curx = 0
|
||||||
|
while not ((direction < 0 and curx < minx) or (direction > 0 and curx > maxx)):
|
||||||
|
cfi = at_point(curx, cury)
|
||||||
|
if cfi:
|
||||||
|
return cfi
|
||||||
|
curx += delta
|
||||||
|
|
||||||
|
for direction in v'[-1, 1]':
|
||||||
|
delta = deltay * direction
|
||||||
|
cury = 0
|
||||||
|
while not( (direction < 0 and cury < miny) or (direction > 0 and cury > maxy) ):
|
||||||
|
cfi = x_loop(cury, -1)
|
||||||
|
if cfi:
|
||||||
|
return cfi
|
||||||
|
cury += delta
|
||||||
|
|
||||||
|
# Use a spatial offset on the html element, since we could not find a
|
||||||
|
# normal CFI
|
||||||
|
x, y = window_scroll_pos()
|
||||||
|
de = document.documentElement
|
||||||
|
rect = de.getBoundingClientRect()
|
||||||
|
px = (x*100)/rect.width
|
||||||
|
py = (y*100)/rect.height
|
||||||
|
cfi = str.format("/2@{}:{}", fstr(px), fstr(py))
|
||||||
|
|
||||||
|
return cfi
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
if __name__ is '__main__':
|
||||||
|
t = 'a^!,1'
|
||||||
|
if unescape_from_cfi(escape_for_cfi(t)) is not t:
|
||||||
|
raise Exception('Failed to properly roundtrip cfi')
|
Loading…
x
Reference in New Issue
Block a user