calibre/src/pyj/range_utils.pyj

147 lines
4.2 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
# globals: NodeFilter, Range
from __python__ import bound_methods, hash_literals
def is_non_empty_text_node(node):
return (node.nodeType is Node.TEXT_NODE or node.nodeType is Node.CDATA_SECTION_NODE) and node.nodeValue.length > 0
def text_nodes_in_range(r):
parent = r.commonAncestorContainer
doc = parent.ownerDocument or document
iterator = doc.createNodeIterator(parent)
in_range = False
ans = v'[]'
while True:
node = iterator.nextNode()
if not node:
break
if not in_range and node.isSameNode(r.startContainer):
in_range = True
if in_range:
if is_non_empty_text_node(node):
ans.push(node)
if node.isSameNode(r.endContainer):
break
return ans
def remove(node):
if node.parentNode:
node.parentNode.removeChild(node)
def replace_node(replacement, node):
remove(replace_node)
node.parentNode.insertBefore(replacement, node)
remove(node)
def unwrap(node):
r = (node.ownerDocument or document).createRange()
r.selectNodeContents(node)
replace_node(r.extractContents(), node)
p = node.parentNode
if p:
p.normalize()
def unwrap_crw(crw):
for node in document.querySelectorAll(f'span[data-calibre-range-wrapper="{crw}"]'):
unwrap(node)
def unwrap_all_crw():
for node in document.querySelectorAll('span[data-calibre-range-wrapper]'):
unwrap(node)
def select_crw(crw):
nodes = document.querySelectorAll(f'span[data-calibre-range-wrapper="{crw}"]')
r = document.createRange()
r.setStart(nodes[0].firstChild, 0)
r.setEnd(nodes[-1].lastChild, nodes[-1].lastChild.nodeValue.length)
sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(r)
def create_wrapper_function(wrapper_elem, r, intersecting_wrappers, process_wrapper):
start_node = r.startContainer
end_node = r.endContainer
start_offset = r.startOffset
end_offset = r.endOffset
def wrap_node(node):
nonlocal start_node, end_node, start_offset, end_offset
current_range = (node.ownerDocument or document).createRange()
current_wrapper = wrapper_elem.cloneNode()
current_range.selectNodeContents(node)
if node.isSameNode(start_node):
current_range.setStart(node, start_offset)
start_node = current_wrapper
start_offset = 0
if node.isSameNode(end_node):
current_range.setEnd(node, end_offset)
end_node = current_wrapper
end_offset = 1
crw = node.parentNode.dataset.calibreRangeWrapper
if crw:
intersecting_wrappers[crw] = True
current_range.surroundContents(current_wrapper)
if process_wrapper:
process_wrapper(current_wrapper)
return current_wrapper
return wrap_node
wrapper_counter = 0
def wrap_text_in_range(style, r, process_wrapper):
if not r:
sel = window.getSelection()
if not sel or not sel.rangeCount:
return None, v'[]'
r = sel.getRangeAt(0)
if r.isCollapsed:
return None, v'[]'
wrapper_elem = document.createElement('span')
wrapper_elem.dataset.calibreRangeWrapper = v'++wrapper_counter' + ''
if style:
if !style.endsWith(';'):
style += ';'
style = str.replace(style, ';', ' !important;')
wrapper_elem.setAttribute('style', style)
intersecting_wrappers = {}
wrap_node = create_wrapper_function(wrapper_elem, r, intersecting_wrappers, process_wrapper)
text_nodes_in_range(r).map(wrap_node)
crw = wrapper_elem.dataset.calibreRangeWrapper
v'delete intersecting_wrappers[crw]'
return crw, Object.keys(intersecting_wrappers)
def reset_highlight_counter():
nonlocal wrapper_counter
wrapper_counter = 0
def set_selection_to_highlight():
sel = window.getSelection()
if not sel or not sel.rangeCount:
return
r = sel.getRangeAt(0)
crw = None
for node in text_nodes_in_range(r):
crw = node.parentNode.dataset.calibreRangeWrapper
if crw:
break
if crw:
select_crw(crw)
return crw or None