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