# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2020, Kovid Goyal # 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