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

1from __future__ import annotations 

2 

3from abc import ABC 

4from collections.abc import Sequence 

5from operator import attrgetter 

6from typing import Literal, override 

7 

8import bs4 

9 

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) 

16 

17 

18class NavItem(NavigationReference["NavItem"]): 

19 """ 

20 A navigation item in the navigation document. The text tag and the 

21 href tag are the same. 

22 

23 Example of tag: 

24 

25 .. code-block:: html 

26 

27 <li> 

28 <a href="chapter1.xhtml">Chapter 1</a> 

29 <ol>[...children]</ol> 

30 </li> 

31 """ 

32 

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" 

39 

40 @property 

41 @override 

42 def items(self) -> Sequence[NavItem]: 

43 return tuple(self._items) 

44 

45 @override 

46 def _get_children_tags(self) -> list[bs4.Tag]: 

47 parent_tag = self.tag.ol 

48 if not parent_tag: 

49 return [] 

50 

51 return parent_tag.select("li") 

52 

53 @override 

54 def _insert_tag(self, position: int, tag: bs4.Tag): 

55 ol = self.tag.ol 

56 

57 if not ol: 

58 ol = self.soup.new_tag("ol") 

59 __ = self.tag.append(ol) 

60 

61 __ = ol.insert(get_actual_tag_position(ol, position), tag) 

62 

63 @override 

64 def _create_href(self, value: str) -> bs4.Tag | None: 

65 text_tag = self._get_text_tag() 

66 

67 if text_tag is None: 

68 text_tag = self._create_text("") 

69 

70 text_tag.name = "a" 

71 text_tag[self.href_attr] = value 

72 

73 return text_tag 

74 

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 

78 

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" 

85 

86 

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] 

95 

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")) 

100 

101 return tag 

102 

103 @override 

104 def _insert_self_in_soup(self): 

105 if self.soup.body: 

106 __ = self.soup.body.insert(0, self.tag) 

107 

108 __ = self.soup.insert(0, self.tag) 

109 

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" 

114 

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) 

120 

121 return htag 

122 

123 

124class TocRoot(NavRoot[TOCEntryData]): 

125 new_attrs: dict[str, str] = { 

126 epub_type: "toc", 

127 "role": "doc-toc", 

128 "id": "toc", 

129 } 

130 

131 @override 

132 def _insert_self_in_soup(self): 

133 assert not self.soup.select_one('nav[epub|type="toc"]'), "toc already existent!" 

134 

135 landmarks = self.soup.select_one('nav[epub|type="landmarks"]') 

136 if landmarks: 

137 __ = landmarks.insert_before(self.tag) 

138 return 

139 

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 

144 

145 super()._insert_self_in_soup() 

146 

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] = [] 

153 

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) 

159 

160 @override 

161 def __repr__(self) -> str: 

162 return f"{self.__class__.__name__}({len(self.items)} items)" 

163 

164 

165class PageListRoot(NavRoot[PageBreakData]): 

166 new_attrs: dict[str, str] = { 

167 epub_type: "page-list", 

168 "id": "page-list", 

169 "hidden": "", 

170 } 

171 

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 ) 

177 

178 toc = self.soup.select_one('nav[epub|type="toc"]') 

179 if toc: 

180 __ = toc.insert_after(self.tag) 

181 return 

182 

183 landmarks = self.soup.select_one('nav[epub|type="toc"]') 

184 if landmarks: 

185 __ = landmarks.insert_before(self.tag) 

186 return 

187 

188 super()._insert_self_in_soup() 

189 

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] = [] 

196 

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) 

200 

201 @override 

202 def __repr__(self) -> str: 

203 return f"{self.__class__.__name__}({len(self.items)} items)" 

204 

205 

206class LandmarksRoot(NavRoot[LandmarkEntryData]): 

207 new_attrs: dict[str, str] = { 

208 epub_type: "landmarks", 

209 "id": "landmarks", 

210 "hidden": "", 

211 } 

212 

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 ) 

218 

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 

223 

224 toc = self.soup.select_one('nav[epub|type="toc"]') 

225 if toc: 

226 __ = toc.insert_after(self.tag) 

227 return 

228 

229 super()._insert_self_in_soup() 

230 

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] = [] 

237 

238 for entry in entries: 

239 href = f"{get_relative_href(self.base_filename, entry.filename)}" 

240 

241 if entry.id is not None: 

242 href += f"#{entry.id}" 

243 

244 item = self.add_item(text=entry.label, href=href) 

245 if entry.epub_type: 

246 item.tag.attrs[epub_type] = entry.epub_type 

247 

248 @override 

249 def __repr__(self) -> str: 

250 return f"{self.__class__.__name__}({len(self.items)} items)"