novel-downloader 1.5.0__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +1 -3
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +26 -21
  5. novel_downloader/cli/download.py +77 -64
  6. novel_downloader/cli/export.py +16 -20
  7. novel_downloader/cli/main.py +1 -1
  8. novel_downloader/cli/search.py +62 -65
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +8 -5
  11. novel_downloader/config/adapter.py +65 -105
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +1 -0
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +3 -24
  21. novel_downloader/core/downloaders/base.py +49 -23
  22. novel_downloader/core/downloaders/common.py +191 -137
  23. novel_downloader/core/downloaders/qianbi.py +187 -146
  24. novel_downloader/core/downloaders/qidian.py +187 -141
  25. novel_downloader/core/downloaders/registry.py +4 -2
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +3 -20
  28. novel_downloader/core/exporters/base.py +33 -37
  29. novel_downloader/core/exporters/common/__init__.py +1 -2
  30. novel_downloader/core/exporters/common/epub.py +15 -10
  31. novel_downloader/core/exporters/common/main_exporter.py +19 -12
  32. novel_downloader/core/exporters/common/txt.py +14 -9
  33. novel_downloader/core/exporters/epub_util.py +59 -29
  34. novel_downloader/core/exporters/linovelib/__init__.py +1 -0
  35. novel_downloader/core/exporters/linovelib/epub.py +23 -25
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
  37. novel_downloader/core/exporters/linovelib/txt.py +17 -11
  38. novel_downloader/core/exporters/qidian.py +2 -8
  39. novel_downloader/core/exporters/registry.py +4 -2
  40. novel_downloader/core/exporters/txt_util.py +7 -7
  41. novel_downloader/core/fetchers/__init__.py +54 -48
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
  45. novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/lewenn.py +83 -0
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +46 -39
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +5 -16
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/shuhaige.py +84 -0
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/wanbengo.py +83 -0
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +1 -9
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +4 -17
  79. novel_downloader/core/interfaces/parser.py +5 -6
  80. novel_downloader/core/interfaces/searcher.py +9 -1
  81. novel_downloader/core/parsers/__init__.py +49 -12
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +63 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/esjzone.py +61 -66
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/linovelib.py +48 -64
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/qianbi.py +48 -50
  99. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  100. novel_downloader/core/parsers/qidian/chapter_encrypted.py +272 -330
  101. novel_downloader/core/parsers/qidian/chapter_normal.py +24 -55
  102. novel_downloader/core/parsers/qidian/main_parser.py +11 -38
  103. novel_downloader/core/parsers/qidian/utils/__init__.py +1 -0
  104. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  105. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  106. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  107. novel_downloader/core/parsers/quanben5.py +103 -0
  108. novel_downloader/core/parsers/registry.py +5 -16
  109. novel_downloader/core/parsers/sfacg.py +38 -45
  110. novel_downloader/core/parsers/shencou.py +215 -0
  111. novel_downloader/core/parsers/shuhaige.py +111 -0
  112. novel_downloader/core/parsers/tongrenquan.py +116 -0
  113. novel_downloader/core/parsers/ttkan.py +132 -0
  114. novel_downloader/core/parsers/wanbengo.py +191 -0
  115. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  116. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  117. novel_downloader/core/parsers/xs63b.py +161 -0
  118. novel_downloader/core/parsers/xshbook.py +134 -0
  119. novel_downloader/core/parsers/yamibo.py +87 -131
  120. novel_downloader/core/parsers/yibige.py +166 -0
  121. novel_downloader/core/searchers/__init__.py +34 -3
  122. novel_downloader/core/searchers/aaatxt.py +107 -0
  123. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  124. novel_downloader/core/searchers/base.py +112 -36
  125. novel_downloader/core/searchers/dxmwx.py +105 -0
  126. novel_downloader/core/searchers/eightnovel.py +84 -0
  127. novel_downloader/core/searchers/esjzone.py +43 -25
  128. novel_downloader/core/searchers/hetushu.py +92 -0
  129. novel_downloader/core/searchers/i25zw.py +93 -0
  130. novel_downloader/core/searchers/ixdzs8.py +107 -0
  131. novel_downloader/core/searchers/jpxs123.py +107 -0
  132. novel_downloader/core/searchers/piaotia.py +100 -0
  133. novel_downloader/core/searchers/qbtr.py +106 -0
  134. novel_downloader/core/searchers/qianbi.py +74 -40
  135. novel_downloader/core/searchers/quanben5.py +144 -0
  136. novel_downloader/core/searchers/registry.py +24 -8
  137. novel_downloader/core/searchers/shuhaige.py +124 -0
  138. novel_downloader/core/searchers/tongrenquan.py +110 -0
  139. novel_downloader/core/searchers/ttkan.py +92 -0
  140. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  141. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  142. novel_downloader/core/searchers/xs63b.py +104 -0
  143. novel_downloader/locales/en.json +31 -82
  144. novel_downloader/locales/zh.json +32 -83
  145. novel_downloader/models/__init__.py +21 -22
  146. novel_downloader/models/book.py +44 -0
  147. novel_downloader/models/config.py +4 -37
  148. novel_downloader/models/login.py +1 -1
  149. novel_downloader/models/search.py +5 -0
  150. novel_downloader/resources/config/settings.toml +8 -70
  151. novel_downloader/resources/json/xiguashuwu.json +718 -0
  152. novel_downloader/utils/__init__.py +13 -22
  153. novel_downloader/utils/chapter_storage.py +3 -2
  154. novel_downloader/utils/constants.py +4 -29
  155. novel_downloader/utils/cookies.py +6 -18
  156. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  157. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  158. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  159. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  160. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  161. novel_downloader/utils/epub/__init__.py +1 -1
  162. novel_downloader/utils/epub/constants.py +57 -16
  163. novel_downloader/utils/epub/documents.py +88 -194
  164. novel_downloader/utils/epub/models.py +0 -14
  165. novel_downloader/utils/epub/utils.py +63 -96
  166. novel_downloader/utils/file_utils/__init__.py +2 -23
  167. novel_downloader/utils/file_utils/io.py +3 -113
  168. novel_downloader/utils/file_utils/sanitize.py +0 -4
  169. novel_downloader/utils/fontocr.py +207 -0
  170. novel_downloader/utils/logger.py +8 -16
  171. novel_downloader/utils/network.py +2 -2
  172. novel_downloader/utils/state.py +4 -90
  173. novel_downloader/utils/text_utils/__init__.py +1 -7
  174. novel_downloader/utils/text_utils/diff_display.py +5 -7
  175. novel_downloader/utils/time_utils/__init__.py +5 -11
  176. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  177. novel_downloader/utils/time_utils/sleep_utils.py +4 -8
  178. novel_downloader/web/__init__.py +13 -0
  179. novel_downloader/web/components/__init__.py +11 -0
  180. novel_downloader/web/components/navigation.py +35 -0
  181. novel_downloader/web/main.py +66 -0
  182. novel_downloader/web/pages/__init__.py +17 -0
  183. novel_downloader/web/pages/download.py +78 -0
  184. novel_downloader/web/pages/progress.py +147 -0
  185. novel_downloader/web/pages/search.py +329 -0
  186. novel_downloader/web/services/__init__.py +17 -0
  187. novel_downloader/web/services/client_dialog.py +164 -0
  188. novel_downloader/web/services/cred_broker.py +113 -0
  189. novel_downloader/web/services/cred_models.py +35 -0
  190. novel_downloader/web/services/task_manager.py +264 -0
  191. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  192. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  193. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  194. novel_downloader/core/downloaders/biquge.py +0 -29
  195. novel_downloader/core/downloaders/esjzone.py +0 -29
  196. novel_downloader/core/downloaders/linovelib.py +0 -29
  197. novel_downloader/core/downloaders/sfacg.py +0 -29
  198. novel_downloader/core/downloaders/yamibo.py +0 -29
  199. novel_downloader/core/exporters/biquge.py +0 -22
  200. novel_downloader/core/exporters/esjzone.py +0 -22
  201. novel_downloader/core/exporters/qianbi.py +0 -22
  202. novel_downloader/core/exporters/sfacg.py +0 -22
  203. novel_downloader/core/exporters/yamibo.py +0 -22
  204. novel_downloader/core/fetchers/base/__init__.py +0 -14
  205. novel_downloader/core/fetchers/base/browser.py +0 -422
  206. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  207. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  208. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  209. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  210. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  211. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  212. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  213. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  214. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  215. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  216. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  217. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  218. novel_downloader/core/parsers/biquge.py +0 -139
  219. novel_downloader/models/chapter.py +0 -25
  220. novel_downloader/models/types.py +0 -13
  221. novel_downloader/tui/__init__.py +0 -7
  222. novel_downloader/tui/app.py +0 -32
  223. novel_downloader/tui/main.py +0 -17
  224. novel_downloader/tui/screens/__init__.py +0 -14
  225. novel_downloader/tui/screens/home.py +0 -198
  226. novel_downloader/tui/screens/login.py +0 -74
  227. novel_downloader/tui/styles/home_layout.tcss +0 -79
  228. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  229. novel_downloader/utils/cache.py +0 -24
  230. novel_downloader/utils/fontocr/__init__.py +0 -22
  231. novel_downloader/utils/fontocr/hash_store.py +0 -280
  232. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  233. novel_downloader/utils/fontocr/model_loader.py +0 -69
  234. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  235. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  236. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  237. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  238. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  239. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  240. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  241. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.utils.crypto_utils
4
- -----------------------------------
3
+ novel_downloader.utils.crypto_utils.rc4
4
+ ---------------------------------------
5
5
 
6
- Generic cryptographic utilities
6
+ RC4 stream cipher for simple text encryption and decryption.
7
7
  """
8
8
 
9
- from __future__ import annotations
10
-
11
9
  import base64
12
10
 
13
11
 
@@ -22,16 +20,11 @@ def rc4_crypt(
22
20
  Encrypt or decrypt data using RC4 and Base64.
23
21
 
24
22
  :param key: RC4 key (will be encoded using the specified encoding).
25
- :type key: str
26
23
  :param data: Plain-text (for 'encrypt') or Base64 cipher-text (for 'decrypt').
27
- :type data: str
28
24
  :param mode: Operation mode, either 'encrypt' or 'decrypt'. Defaults to 'encrypt'.
29
- :type mode: str, optional
30
25
  :param encoding: Character encoding for key and returned string. Defaults 'utf-8'.
31
- :type encoding: str, optional
32
26
 
33
27
  :return: Base64 cipher-text (for encryption) or decoded plain-text (for decryption).
34
- :rtype: str
35
28
 
36
29
  :raises ValueError: If mode is not 'encrypt' or 'decrypt'.
37
30
  """
@@ -14,7 +14,7 @@ Usage example:
14
14
 
15
15
  ```python
16
16
  builder = EpubBuilder(title="My Novel", author="Author Name", uid="uuid-1234")
17
- builder.add_chapter(Chapter(id="ch1", title="Chapter 1", content="<p>xxx</p>"))
17
+ builder.chapters.append(Chapter(id="ch1", title="Chapter 1", content="<p>xxx</p>"))
18
18
  builder.export("output/my_novel.epub")
19
19
  ```
20
20
  """
@@ -11,7 +11,6 @@ EPUB-specific constants used by the builder, including:
11
11
  - Template strings for container.xml and cover image HTML
12
12
  """
13
13
 
14
- PRETTY_PRINT_FLAG = True
15
14
  ROOT_PATH = "OEBPS"
16
15
  IMAGE_FOLDER = "Images"
17
16
  TEXT_FOLDER = "Text"
@@ -24,18 +23,6 @@ NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
24
23
  OPF_NS = "http://www.idpf.org/2007/opf"
25
24
  DC_NS = "http://purl.org/dc/elements/1.1/"
26
25
 
27
- OPF_PKG_ATTRIB = {
28
- "version": "3.0",
29
- "unique-identifier": "id",
30
- "prefix": "rendition: http://www.idpf.org/vocab/rendition/#",
31
- }
32
- CHAP_DOC_TYPE = (
33
- '<?xml version="1.0" encoding="utf-8"?>\n'
34
- "<!DOCTYPE html PUBLIC "
35
- '"-//W3C//DTD XHTML 1.1//EN" '
36
- '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
37
- )
38
-
39
26
  IMAGE_MEDIA_TYPES: dict[str, str] = {
40
27
  "png": "image/png",
41
28
  "jpg": "image/jpeg",
@@ -45,13 +32,15 @@ IMAGE_MEDIA_TYPES: dict[str, str] = {
45
32
  "webp": "image/webp",
46
33
  }
47
34
 
48
- CONTAINER_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
35
+ CONTAINER_TEMPLATE = """\
36
+ <?xml version="1.0" encoding="UTF-8"?>
49
37
  <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
50
38
  <rootfiles>
51
39
  <rootfile full-path="{root_path}/content.opf"
52
40
  media-type="application/oebps-package+xml"/>
53
41
  </rootfiles>
54
- </container>"""
42
+ </container>
43
+ """
55
44
 
56
45
  COVER_IMAGE_TEMPLATE = (
57
46
  f'<div style="text-align: center; margin: 0; padding: 0;">'
@@ -66,7 +55,8 @@ CSS_TMPLATE = (
66
55
  )
67
56
 
68
57
  CHAP_TMPLATE = f"""\
69
- {CHAP_DOC_TYPE}
58
+ <?xml version="1.0" encoding="utf-8"?>
59
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
70
60
  <html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
71
61
  <head>
72
62
  <title>{{title}}</title>
@@ -75,3 +65,54 @@ CHAP_TMPLATE = f"""\
75
65
  <body>{{content}}</body>
76
66
  </html>
77
67
  """
68
+
69
+ NAV_TEMPLATE = f"""\
70
+ <?xml version='1.0' encoding='utf-8'?>
71
+ <!DOCTYPE html>
72
+ <html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
73
+ <head>
74
+ <title>{{title}}</title>
75
+ </head>
76
+ <body>
77
+ <nav epub:type="toc" id="{{id}}" role="doc-toc">
78
+ <h2>{{title}}</h2>
79
+ <ol>
80
+ {{items}}
81
+ </ol>
82
+ </nav>
83
+ </body>
84
+ </html>
85
+ """
86
+
87
+ NCX_TEMPLATE = f"""\
88
+ <?xml version='1.0' encoding='utf-8'?>
89
+ <ncx xmlns="{NCX_NS}" version="2005-1">
90
+ <head>
91
+ <meta name="dtb:uid" content="{{uid}}"/>
92
+ <meta name="dtb:depth" content="{{depth}}"/>
93
+ <meta name="dtb:totalPageCount" content="0"/>
94
+ <meta name="dtb:maxPageNumber" content="0"/>
95
+ </head>
96
+ <docTitle>
97
+ <text>{{title}}</text>
98
+ </docTitle>
99
+ <navMap>
100
+ {{navpoints}}
101
+ </navMap>
102
+ </ncx>
103
+ """
104
+
105
+ OPF_TEMPLATE = f"""\
106
+ <?xml version='1.0' encoding='utf-8'?>
107
+ <package xmlns="{OPF_NS}" xmlns:dc="{DC_NS}" xmlns:opf="{OPF_NS}" version="3.0" unique-identifier="id" prefix="rendition: http://www.idpf.org/vocab/rendition/#">
108
+ <metadata>
109
+ {{metadata}}
110
+ </metadata>
111
+ <manifest>
112
+ {{manifest_items}}
113
+ </manifest>
114
+ <spine{{spine_toc}}>
115
+ {{spine_items}}
116
+ </spine>
117
+ {{guide_section}}</package>
118
+ """ # noqa: E501
@@ -13,18 +13,10 @@ from collections.abc import Sequence
13
13
  from dataclasses import dataclass, field
14
14
  from datetime import UTC, datetime
15
15
 
16
- from lxml import etree
17
- from lxml.builder import ElementMaker
18
-
19
16
  from .constants import (
20
- DC_NS,
21
- EPUB_NS,
22
- NCX_NS,
23
- OPF_NS,
24
- OPF_PKG_ATTRIB,
25
- PRETTY_PRINT_FLAG,
26
- XHTML_NS,
27
- XML_NS,
17
+ NAV_TEMPLATE,
18
+ NCX_TEMPLATE,
19
+ OPF_TEMPLATE,
28
20
  )
29
21
  from .models import (
30
22
  ChapterEntry,
@@ -35,21 +27,6 @@ from .models import (
35
27
  VolumeEntry,
36
28
  )
37
29
 
38
- NAV = ElementMaker(
39
- namespace=XHTML_NS,
40
- nsmap={None: XHTML_NS, "epub": EPUB_NS},
41
- )
42
- NCX = ElementMaker(namespace=NCX_NS, nsmap={None: NCX_NS})
43
- PKG = ElementMaker(
44
- namespace=OPF_NS,
45
- nsmap={
46
- None: OPF_NS,
47
- "dc": DC_NS,
48
- "opf": OPF_NS,
49
- },
50
- )
51
- DC = ElementMaker(namespace=DC_NS)
52
-
53
30
 
54
31
  @dataclass
55
32
  class NavDocument(EpubResource):
@@ -60,12 +37,7 @@ class NavDocument(EpubResource):
60
37
  media_type: str = field(init=False, default="application/xhtml+xml")
61
38
  content_items: list[ChapterEntry | VolumeEntry] = field(default_factory=list)
62
39
 
63
- def add_chapter(
64
- self,
65
- id: str,
66
- label: str,
67
- src: str,
68
- ) -> None:
40
+ def add_chapter(self, id: str, label: str, src: str) -> None:
69
41
  """
70
42
  Add a top-level chapter entry to the navigation.
71
43
 
@@ -100,55 +72,29 @@ class NavDocument(EpubResource):
100
72
 
101
73
  :return: A string containing the full XHTML for nav.xhtml.
102
74
  """
103
- # build the root <html> with both lang attributes
104
- html_el = NAV.html(
105
- # head/title
106
- NAV.head(NAV.title(self.title)),
107
- # body/nav/ol subtree
108
- NAV.body(
109
- NAV.nav(
110
- NAV.h2(self.title),
111
- NAV.ol(*self._render_items(self.content_items)),
112
- # namespaced + regular attributes
113
- **{
114
- f"{{{EPUB_NS}}}type": "toc",
115
- "id": self.id,
116
- "role": "doc-toc",
117
- },
118
- )
119
- ),
120
- # html attributes
75
+ items_str = self._render_items_str(self.content_items)
76
+ raw = NAV_TEMPLATE.format(
121
77
  lang=self.language,
122
- **{f"{{{XML_NS}}}lang": self.language},
78
+ id=self.id,
79
+ title=self.title,
80
+ items=items_str,
123
81
  )
124
-
125
- xml_bytes = etree.tostring(
126
- html_el,
127
- xml_declaration=True,
128
- encoding="utf-8",
129
- pretty_print=PRETTY_PRINT_FLAG,
130
- doctype="<!DOCTYPE html>",
131
- )
132
- xml_string: str = xml_bytes.decode("utf-8")
133
- return xml_string
82
+ return raw
134
83
 
135
84
  @classmethod
136
- def _render_items(
137
- cls,
138
- items: Sequence[ChapterEntry | VolumeEntry],
139
- ) -> list[etree._Element]:
140
- """
141
- Recursively build <li> elements (and nested <ol>) for each TOC entry.
142
- """
143
- elements: list[etree._Element] = []
85
+ def _render_items_str(cls, items: Sequence[ChapterEntry | VolumeEntry]) -> str:
86
+ lines: list[str] = []
144
87
  for item in items:
145
88
  if isinstance(item, VolumeEntry) and item.chapters:
146
- li = NAV.li(NAV.a(item.label, href=item.src))
147
- li.append(NAV.ol(*cls._render_items(item.chapters)))
89
+ lines.append(f'<li><a href="{item.src}">{item.label}</a>')
90
+ lines.append(" <ol>")
91
+ child = cls._render_items_str(item.chapters)
92
+ lines.extend(child.splitlines())
93
+ lines.append(" </ol>")
94
+ lines.append("</li>")
148
95
  else:
149
- li = NAV.li(NAV.a(item.label, href=item.src))
150
- elements.append(li)
151
- return elements
96
+ lines.append(f'<li><a href="{item.src}">{item.label}</a></li>')
97
+ return "\n".join(lines)
152
98
 
153
99
 
154
100
  @dataclass
@@ -190,29 +136,19 @@ class NCXDocument(EpubResource):
190
136
 
191
137
  :return: A string containing the full NCX XML document.
192
138
  """
193
- root = NCX.ncx(version="2005-1")
194
- head = NCX.head(
195
- NCX.meta(name="dtb:uid", content=self.uid),
196
- NCX.meta(name="dtb:depth", content=str(self._depth(self.nav_points))),
197
- NCX.meta(name="dtb:totalPageCount", content="0"),
198
- NCX.meta(name="dtb:maxPageNumber", content="0"),
139
+ order = 1
140
+ lines: list[str] = []
141
+ for pt in self.nav_points:
142
+ order, block = self._render_navpoint_str(pt, order)
143
+ lines.extend(block)
144
+ navpoints = "\n".join(lines)
145
+ raw = NCX_TEMPLATE.format(
146
+ uid=self.uid,
147
+ depth=self._depth(self.nav_points),
148
+ title=self.title,
149
+ navpoints=navpoints,
199
150
  )
200
- root.append(head)
201
- root.append(NCX.docTitle(NCX.text(self.title)))
202
-
203
- navMap = NCX.navMap()
204
- root.append(navMap)
205
-
206
- self._render_navpoints(navMap, self.nav_points, start=1)
207
-
208
- xml_bytes = etree.tostring(
209
- root,
210
- xml_declaration=True,
211
- encoding="utf-8",
212
- pretty_print=PRETTY_PRINT_FLAG,
213
- )
214
- xml_string: str = xml_bytes.decode("utf-8")
215
- return xml_string
151
+ return raw
216
152
 
217
153
  @classmethod
218
154
  def _depth(cls, points: list[NavPoint]) -> int:
@@ -221,32 +157,21 @@ class NCXDocument(EpubResource):
221
157
  return 1 + max(cls._depth(child.children) for child in points)
222
158
 
223
159
  @classmethod
224
- def _render_navpoints(
225
- cls,
226
- parent: etree._Element,
227
- points: list[NavPoint],
228
- start: int,
229
- ) -> int:
230
- """
231
- Recursively append <navPoint> elements under `parent`,
232
- assigning playOrder starting from `start`.
233
- Returns the next unused playOrder.
234
- """
235
- play = start
236
- for pt in points:
237
- np = etree.SubElement(
238
- parent,
239
- "navPoint",
240
- id=pt.id,
241
- playOrder=str(play),
242
- )
243
- play += 1
244
- navLabel = etree.SubElement(np, "navLabel")
245
- lbl_text = etree.SubElement(navLabel, "text")
246
- lbl_text.text = pt.label
247
- etree.SubElement(np, "content", src=pt.src)
248
- play = cls._render_navpoints(np, pt.children, play)
249
- return play
160
+ def _render_navpoint_str(cls, pt: NavPoint, order: int) -> tuple[int, list[str]]:
161
+ lines: list[str] = []
162
+ # open navPoint
163
+ lines.append(f'<navPoint id="{pt.id}" playOrder="{order}">')
164
+ order += 1
165
+ # label and content
166
+ lines.append(f"<navLabel><text>{pt.label}</text></navLabel>")
167
+ lines.append(f'<content src="{pt.src}"/>')
168
+ # children
169
+ for child in pt.children:
170
+ order, child_lines = cls._render_navpoint_str(child, order)
171
+ lines.extend(child_lines)
172
+ # close
173
+ lines.append("</navPoint>")
174
+ return order, lines
250
175
 
251
176
 
252
177
  @dataclass
@@ -318,86 +243,55 @@ class OpfDocument(EpubResource):
318
243
  """
319
244
  now_iso = datetime.now(UTC).replace(microsecond=0).isoformat()
320
245
 
321
- # <package> root
322
- package = PKG.package(**OPF_PKG_ATTRIB)
323
-
324
- # <metadata>
325
- metadata = PKG.metadata()
326
- package.append(metadata)
327
-
328
- # modified timestamp
329
- modified = PKG.meta(property="dcterms:modified")
330
- modified.text = now_iso
331
- metadata.append(modified)
332
-
333
- # mandatory DC elements
334
- id_el = DC.identifier(id="id")
335
- id_el.text = self.uid
336
- title_el = DC.title()
337
- title_el.text = self.title
338
- lang_el = DC.language()
339
- lang_el.text = self.language
340
- metadata.extend([id_el, title_el, lang_el])
341
-
342
- # optional DC elements
246
+ # metadata block
247
+ meta_lines: list[str] = []
248
+ meta_lines.append(f'<meta property="dcterms:modified">{now_iso}</meta>')
249
+ meta_lines.append(f'<dc:identifier id="id">{self.uid}</dc:identifier>')
250
+ meta_lines.append(f"<dc:title>{self.title}</dc:title>")
251
+ meta_lines.append(f"<dc:language>{self.language}</dc:language>")
343
252
  if self.author:
344
- creator = DC.creator(id="creator")
345
- creator.text = self.author
346
- metadata.append(creator)
253
+ meta_lines.append(f'<dc:creator id="creator">{self.author}</dc:creator>')
347
254
  if self.description:
348
- desc = DC.description()
349
- desc.text = self.description
350
- metadata.append(desc)
255
+ meta_lines.append(f"<dc:description>{self.description}</dc:description>")
351
256
  if self.subject:
352
- subj = DC.subject()
353
- subj.text = ",".join(self.subject)
354
- metadata.append(subj)
257
+ joined = ",".join(self.subject)
258
+ meta_lines.append(f"<dc:subject>{joined}</dc:subject>")
355
259
  if self.include_cover and self._cover_item:
356
- cover_meta = PKG.meta(name="cover", content=self._cover_item.id)
357
- metadata.append(cover_meta)
260
+ meta_lines.append(f'<meta name="cover" content="{self._cover_item.id}"/>')
261
+ metadata = "\n".join(meta_lines)
358
262
 
359
- # <manifest>
360
- manifest_el = PKG.manifest()
263
+ # manifest block
264
+ man_lines: list[str] = []
361
265
  for item in self.manifest:
362
- attrs = {
363
- "id": item.id,
364
- "href": item.href,
365
- "media-type": item.media_type,
366
- }
367
- if item.properties:
368
- attrs["properties"] = item.properties
369
- manifest_el.append(PKG.item(**attrs))
370
- package.append(manifest_el)
371
-
372
- # <spine>
373
- spine_attrs = {}
374
- if self._toc_item:
375
- spine_attrs["toc"] = self._toc_item.id
376
- spine_el = PKG.spine(**spine_attrs)
266
+ props = f' properties="{item.properties}"' if item.properties else ""
267
+ man_lines.append(
268
+ f'<item id="{item.id}" href="{item.href}" media-type="{item.media_type}"{props}/>' # noqa: E501
269
+ )
270
+ manifest_items = "\n".join(man_lines)
271
+
272
+ # spine block
273
+ toc_attr = f' toc="{self._toc_item.id}"' if self._toc_item else ""
274
+ spine_lines: list[str] = []
377
275
  for ref in self.spine:
378
- attrs = {"idref": ref.idref}
379
- if ref.properties:
380
- attrs["properties"] = ref.properties
381
- spine_el.append(PKG.itemref(**attrs))
382
- package.append(spine_el)
276
+ props = f' properties="{ref.properties}"' if ref.properties else ""
277
+ spine_lines.append(f' <itemref idref="{ref.idref}"{props}/>')
278
+ spine_items = "\n".join(spine_lines)
383
279
 
384
- # optional <guide> for cover
280
+ # guide block
385
281
  if self.include_cover and self._cover_doc:
386
- guide_el = PKG.guide()
387
- guide_el.append(
388
- PKG.reference(
389
- type="cover",
390
- title="Cover",
391
- href=self._cover_doc.href,
392
- )
282
+ guide_section = (
283
+ " <guide>\n"
284
+ f' <reference type="cover" title="Cover" href="{self._cover_doc.href}"/>\n' # noqa: E501
285
+ " </guide>\n"
393
286
  )
394
- package.append(guide_el)
395
-
396
- xml_bytes = etree.tostring(
397
- package,
398
- xml_declaration=True,
399
- encoding="utf-8",
400
- pretty_print=PRETTY_PRINT_FLAG,
287
+ else:
288
+ guide_section = ""
289
+
290
+ raw = OPF_TEMPLATE.format(
291
+ metadata=metadata,
292
+ manifest_items=manifest_items,
293
+ spine_toc=toc_attr,
294
+ spine_items=spine_items,
295
+ guide_section=guide_section,
401
296
  )
402
- xml_string: str = xml_bytes.decode("utf-8")
403
- return xml_string
297
+ return raw
@@ -65,12 +65,6 @@ class NavPoint:
65
65
  src: str
66
66
  children: list[NavPoint] = field(default_factory=list)
67
67
 
68
- def add_child(self, point: NavPoint) -> None:
69
- """
70
- Append a child nav point under this one.
71
- """
72
- self.children.append(point)
73
-
74
68
 
75
69
  @dataclass
76
70
  class EpubResource:
@@ -101,10 +95,6 @@ class Chapter(EpubResource):
101
95
  css: list[StyleSheet] = field(default_factory=list)
102
96
  media_type: str = field(init=False, default="application/xhtml+xml")
103
97
 
104
- def __post_init__(self) -> None:
105
- if not self.filename:
106
- object.__setattr__(self, "filename", f"{self.id}.xhtml")
107
-
108
98
  def to_xhtml(self, lang: str = "zh-CN") -> str:
109
99
  """
110
100
  Generate the XHTML for a chapter.
@@ -128,7 +118,3 @@ class Volume:
128
118
  intro: str = ""
129
119
  cover: Path | None = None
130
120
  chapters: list[Chapter] = field(default_factory=list)
131
-
132
- def add_chapter(self, chapter: Chapter) -> None:
133
- """Append a chapter to this volume."""
134
- self.chapters.append(chapter)