1# vim:fileencoding=utf-8 2# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> 3from __python__ import hash_literals 4 5from complete import create_search_bar 6from dom import set_css, svgicon, ensure_id 7from elementmaker import E 8from gettext import gettext as _ 9from modals import error_dialog 10from widgets import create_tree, find_text_in_tree, scroll_tree_item_into_view 11from read_book.globals import toc_anchor_map, set_toc_anchor_map, current_spine_item, current_layout_mode, current_book 12from read_book.viewport import scroll_viewport 13 14 15def update_visible_toc_nodes(visible_anchors): 16 update_visible_toc_nodes.data = visible_anchors 17update_visible_toc_nodes.data = {} 18 19 20def iter_toc_descendants(node, callback): 21 for child in node.children: 22 if callback(child): 23 return 24 iter_toc_descendants(child, callback) 25 26 27def get_toc_nodes_bordering_spine_item(toc, csi): 28 # Find the ToC entries that point to the closest files on either side of the 29 # spine item 30 toc = toc or current_book().manifest.toc 31 csi = csi or current_spine_item() 32 spine = current_book().manifest.spine 33 spine_before, spine_after = {}, {} 34 which = spine_before 35 before = after = prev = None 36 for name in spine: 37 if name is csi: 38 which = spine_after 39 else: 40 which[name] = True 41 iter_toc_descendants(toc, def(node): 42 nonlocal prev, before, after 43 if node.dest: 44 if spine_before[node.dest]: 45 prev = node 46 elif spine_after[node.dest]: 47 if not before: 48 before = prev 49 if not after: 50 after = node 51 return True 52 ) 53 if not before and prev is not None: 54 before = prev 55 return before, after 56 57 58def get_border_nodes(toc, id_map): 59 data = update_visible_toc_nodes.data 60 before, after = data.before, data.after 61 if before: 62 before = id_map[before] 63 if after: 64 after = id_map[after] 65 if before and after: 66 # Both border nodes are in the current spine item 67 return before, after 68 sb, sa = get_toc_nodes_bordering_spine_item(toc) 69 before = before or sb 70 after = after or sa 71 return before, after 72 73 74def family_for_toc_node(toc_node_id, parent_map, id_map): 75 if not id_map or not parent_map: 76 toc = current_book().manifest.toc 77 parent_map, id_map = get_toc_maps(toc) 78 family = v'[]' 79 node = id_map[toc_node_id] 80 while node and node.title: 81 family.unshift(node) 82 parent = parent_map[node.id] 83 node = None 84 if parent: 85 node = id_map[parent.id] 86 return family 87 88 89def get_current_toc_nodes(): 90 toc = current_book().manifest.toc 91 parent_map, id_map = get_toc_maps(toc) 92 data = update_visible_toc_nodes.data 93 ans = {} 94 if data.has_visible: 95 ans = data.visible_anchors 96 else: 97 if data.before: 98 ans[data.before] = True 99 else: 100 before = get_border_nodes(toc, id_map)[0] 101 if before: 102 ans[before.id] = True 103 r = v'[]' 104 for x in Object.keys(ans): 105 fam = family_for_toc_node(x, parent_map, id_map) 106 if fam?.length: 107 r.push(fam) 108 return r 109 110 111def get_highlighted_toc_nodes(toc, parent_map, id_map): 112 data = update_visible_toc_nodes.data 113 ans = {} 114 if data.has_visible: 115 ans = data.visible_anchors 116 else: 117 if data.before: 118 ans[data.before] = True 119 else: 120 before = get_border_nodes(toc, id_map)[0] 121 if before: 122 ans[before.id] = True 123 for node_id in Object.keys(ans): 124 p = parent_map[node_id] 125 while p and p.title: 126 ans[p.id] = True 127 p = parent_map[p.id] 128 return ans 129 130def get_toc_maps(toc): 131 if not toc: 132 toc = current_book().manifest.toc 133 parent_map, id_map = {}, {} 134 135 def process_node(node, parent): 136 id_map[node.id] = node 137 parent_map[node.id] = parent 138 for c in node.children: 139 process_node(c, node) 140 141 process_node(toc) 142 return parent_map, id_map 143 144def create_toc_tree(toc, onclick): 145 parent_map, id_map = get_toc_maps(toc) 146 highlighted_toc_nodes = get_highlighted_toc_nodes(toc, parent_map, id_map) 147 148 def populate_data(node, li, a): 149 li.dataset.tocDest = node.dest or '' 150 li.dataset.tocFrag = node.frag or '' 151 title = node.title or '' 152 if highlighted_toc_nodes[node.id]: 153 a.appendChild(E.b(E.i(title))) 154 else: 155 a.textContent = title 156 157 return create_tree(toc, populate_data, onclick) 158 159def do_search(text): 160 container = document.getElementById(this) 161 a = find_text_in_tree(container, text) 162 if not text: 163 return 164 if not a: 165 return error_dialog(_('No matches found'), _( 166 'The text "{}" was not found in the Table of Contents').format(text)) 167 scroll_tree_item_into_view(a) 168 169def create_toc_panel(book, container, onclick): 170 def handle_click(event, li): 171 if event.button is 0: 172 onclick(li.dataset.tocDest, li.dataset.tocFrag) 173 toc_panel = create_toc_tree(book.manifest.toc, handle_click) 174 toc_panel_id = ensure_id(toc_panel) 175 set_css(container, display='flex', flex_direction='column', height='100%', min_height='100%', overflow='hidden', max_height='100vh', max_width='100vw') 176 set_css(toc_panel, flex_grow='10') 177 container.appendChild(toc_panel) 178 search_button = E.div(class_='simple-link', svgicon('search')) 179 t = _('Search Table of Contents') 180 search_bar = create_search_bar(do_search.bind(toc_panel_id), 'search-book-toc', button=search_button, placeholder=t) 181 set_css(search_bar, flex_grow='10', margin_right='1em') 182 container.appendChild(E.div(style='margin: 1ex 1em; display: flex; align-items: center', search_bar, search_button)) 183 for child in container.childNodes: 184 child.style.flexShrink = '0' 185 toc_panel.style.flexGrow = '100' 186 toc_panel.style.flexShrink = '1' 187 toc_panel.style.overflow = 'auto' 188 189 190def current_toc_anchor_map(tam, anchor_funcs): 191 current_map = toc_anchor_map() 192 if not (current_map and current_map.layout_mode is current_layout_mode() and current_map.width is scroll_viewport.width() and current_map.height is scroll_viewport.height()): 193 name = current_spine_item().name 194 am = {} 195 anchors = v'[]' 196 pos_map = {} 197 for i, anchor in enumerate(tam[name] or v'[]'): 198 val = anchor_funcs.pos_for_elem() 199 if anchor.frag: 200 elem = document.getElementById(anchor.frag) 201 if elem: 202 val = anchor_funcs.pos_for_elem(elem) 203 am[anchor.id] = val 204 anchors.push(anchor.id) 205 pos_map[anchor.id] = i 206 # stable sort by position in document 207 anchors.sort(def (a, b): return anchor_funcs.cmp(am[a], am[b]) or (pos_map[a] - pos_map[b]);) 208 209 current_map = {'layout_mode': current_layout_mode(), 'width': scroll_viewport.width(), 'height': scroll_viewport.height(), 'pos_map': am, 'sorted_anchors':anchors} 210 set_toc_anchor_map(current_map) 211 return current_map 212 213 214def update_visible_toc_anchors(toc_anchor_map, anchor_funcs): 215 tam = current_toc_anchor_map(toc_anchor_map, anchor_funcs) 216 before = after = None 217 visible_anchors = {} 218 has_visible = False 219 220 for anchor_id in tam.sorted_anchors: 221 pos = tam.pos_map[anchor_id] 222 visibility = anchor_funcs.visibility(pos) 223 if visibility < 0: 224 before = anchor_id 225 elif visibility is 0: 226 has_visible = True 227 visible_anchors[anchor_id] = True 228 elif visibility > 0: 229 after = anchor_id 230 break 231 232 return {'visible_anchors':visible_anchors, 'has_visible':has_visible, 'before':before, 'after':after, 'sorted_anchors':tam.sorted_anchors} 233 234 235def find_anchor_before_range(r, toc_anchor_map, anchor_funcs): 236 name = current_spine_item().name 237 prev_anchor = None 238 tam = current_toc_anchor_map(toc_anchor_map, anchor_funcs) 239 anchors = toc_anchor_map[name] 240 if anchors: 241 amap = {x.id: x for x in anchors} 242 for anchor_id in tam.sorted_anchors: 243 anchor = amap[anchor_id] 244 is_before = True 245 if anchor.frag: 246 elem = document.getElementById(anchor.frag) 247 if elem: 248 q = document.createRange() 249 q.selectNode(elem) 250 if q.compareBoundaryPoints(window.Range.START_TO_START, r) > 0: 251 is_before = False 252 if is_before: 253 prev_anchor = anchor 254 else: 255 break 256 return prev_anchor 257