mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
147 lines
4.2 KiB
Plaintext
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
|