Coverage for src/epublib/nav/__init__.py: 82%
158 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 16:07 -0300
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 16:07 -0300
1from __future__ import annotations
3from abc import ABC
4from collections.abc import Sequence
5from operator import attrgetter
6from typing import Literal, override
8import bs4
10from epublib.nav.util import LandmarkEntryData, PageBreakData, TOCEntryData, epub_type
11from epublib.reference import NavigationReference, NavigationRoot
12from epublib.util import (
13 get_actual_tag_position,
14 get_relative_href,
15)
18class NavItem(NavigationReference["NavItem"]):
19 """
20 A navigation item in the navigation document. The text tag and the
21 href tag are the same.
23 Example of tag:
25 .. code-block:: html
27 <li>
28 <a href="chapter1.xhtml">Chapter 1</a>
29 <ol>[...children]</ol>
30 </li>
31 """
33 tag_name: str = "li"
34 text_selector: str = "& > span, a"
35 text_tag_name: str = "span"
36 href_selector: str = "& > a"
37 href_tag_name: str = "a"
38 href_attr: str = "href"
40 @property
41 @override
42 def items(self) -> Sequence[NavItem]:
43 return tuple(self._items)
45 @override
46 def _get_children_tags(self) -> list[bs4.Tag]:
47 parent_tag = self.tag.ol
48 if not parent_tag:
49 return []
51 return parent_tag.select("li")
53 @override
54 def _insert_tag(self, position: int, tag: bs4.Tag):
55 ol = self.tag.ol
57 if not ol:
58 ol = self.soup.new_tag("ol")
59 __ = self.tag.append(ol)
61 __ = ol.insert(get_actual_tag_position(ol, position), tag)
63 @override
64 def _create_href(self, value: str) -> bs4.Tag | None:
65 text_tag = self._get_text_tag()
67 if text_tag is None:
68 text_tag = self._create_text("")
70 text_tag.name = "a"
71 text_tag[self.href_attr] = value
73 return text_tag
75 @override
76 def _get_href_tag(self) -> bs4.Tag | None:
77 return super()._get_text_tag() # href tag is the same as text tag
79 @override
80 def _set_href_tag(self, value: str) -> None:
81 super()._set_href_tag(value)
82 text_tag = self._get_text_tag()
83 if text_tag:
84 text_tag.name = "a"
87class NavRoot[D]( # type: ignore[reportUnsafeMultipleInheritance]
88 NavigationRoot[NavItem, D],
89 NavItem,
90 ABC,
91):
92 text_selector: str = "& > h1, & > h2, & > h3, & > h4, & > h5, & > h6"
93 new_attrs: dict[str, str] = {}
94 child_class: type[NavItem] = NavItem # type: ignore[reportIncompatibleVariableOverride]
96 @override
97 def _create_own_tag(self):
98 tag = self.soup.new_tag("nav", attrs=self.new_attrs.copy())
99 __ = tag.append(self.soup.new_tag("ol"))
101 return tag
103 @override
104 def _insert_self_in_soup(self):
105 if self.soup.body:
106 __ = self.soup.body.insert(0, self.tag)
108 __ = self.soup.insert(0, self.tag)
110 def _find_heading_level(self) -> Literal["h1", "h2", "h3", "h4", "h5", "h6"]:
111 if self.soup.find("h1"):
112 return "h2"
113 return "h1"
115 @override
116 def _create_text(self, value: str) -> bs4.Tag:
117 htag = self.soup.new_tag(self._find_heading_level())
118 htag.string = value
119 __ = self.tag.insert(0, htag)
121 return htag
124class TocRoot(NavRoot[TOCEntryData]):
125 new_attrs: dict[str, str] = {
126 epub_type: "toc",
127 "role": "doc-toc",
128 "id": "toc",
129 }
131 @override
132 def _insert_self_in_soup(self):
133 assert not self.soup.select_one('nav[epub|type="toc"]'), "toc already existent!"
135 landmarks = self.soup.select_one('nav[epub|type="landmarks"]')
136 if landmarks:
137 __ = landmarks.insert_before(self.tag)
138 return
140 page_list = self.soup.select_one('nav[epub|type="page-list"]')
141 if page_list:
142 __ = page_list.insert_before(self.tag)
143 return
145 super()._insert_self_in_soup()
147 @override
148 def reset(self, entries: Sequence[TOCEntryData]):
149 new_tag = self._create_own_tag()
150 __ = self.tag.replace_with(new_tag)
151 self.tag: bs4.Tag = new_tag
152 self._items: list[NavItem] = []
154 for entry in entries:
155 href = f"{get_relative_href(self.base_filename, entry.filename)}"
156 if entry.id is not None:
157 href += f"#{entry.id}"
158 __ = self.add_item(text=entry.label, href=href)
160 @override
161 def __repr__(self) -> str:
162 return f"{self.__class__.__name__}({len(self.items)} items)"
165class PageListRoot(NavRoot[PageBreakData]):
166 new_attrs: dict[str, str] = {
167 epub_type: "page-list",
168 "id": "page-list",
169 "hidden": "",
170 }
172 @override
173 def _insert_self_in_soup(self):
174 assert not self.soup.select_one('nav[epub|type="page-list"]'), (
175 "page list already existent!"
176 )
178 toc = self.soup.select_one('nav[epub|type="toc"]')
179 if toc:
180 __ = toc.insert_after(self.tag)
181 return
183 landmarks = self.soup.select_one('nav[epub|type="toc"]')
184 if landmarks:
185 __ = landmarks.insert_before(self.tag)
186 return
188 super()._insert_self_in_soup()
190 @override
191 def reset(self, entries: Sequence[PageBreakData]):
192 new_tag = self._create_own_tag()
193 __ = self.tag.replace_with(new_tag)
194 self.tag: bs4.Tag = new_tag
195 self._items: list[NavItem] = []
197 for pagebreak in sorted(entries, key=attrgetter("page")):
198 href = f"{get_relative_href(self.base_filename, pagebreak.filename)}#{pagebreak.id}"
199 __ = self.add_item(text=pagebreak.label, href=href)
201 @override
202 def __repr__(self) -> str:
203 return f"{self.__class__.__name__}({len(self.items)} items)"
206class LandmarksRoot(NavRoot[LandmarkEntryData]):
207 new_attrs: dict[str, str] = {
208 epub_type: "landmarks",
209 "id": "landmarks",
210 "hidden": "",
211 }
213 @override
214 def _insert_self_in_soup(self):
215 assert not self.soup.select_one('nav[epub|type="landmarks"]'), (
216 "landmarks already existent!"
217 )
219 page_list = self.soup.select_one('nav[epub|type="page-list"]')
220 if page_list:
221 __ = page_list.insert_after(self.tag)
222 return
224 toc = self.soup.select_one('nav[epub|type="toc"]')
225 if toc:
226 __ = toc.insert_after(self.tag)
227 return
229 super()._insert_self_in_soup()
231 @override
232 def reset(self, entries: Sequence[LandmarkEntryData]):
233 new_tag = self._create_own_tag()
234 __ = self.tag.replace_with(new_tag)
235 self.tag: bs4.Tag = new_tag
236 self._items: list[NavItem] = []
238 for entry in entries:
239 href = f"{get_relative_href(self.base_filename, entry.filename)}"
241 if entry.id is not None:
242 href += f"#{entry.id}"
244 item = self.add_item(text=entry.label, href=href)
245 if entry.epub_type:
246 item.tag.attrs[epub_type] = entry.epub_type
248 @override
249 def __repr__(self) -> str:
250 return f"{self.__class__.__name__}({len(self.items)} items)"