txt2ebook 0.1.156__tar.gz → 0.1.158__tar.gz

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 (66) hide show
  1. {txt2ebook-0.1.156/src/txt2ebook.egg-info → txt2ebook-0.1.158}/PKG-INFO +17 -1
  2. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/pyproject.toml +15 -15
  3. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/base.py +93 -0
  4. txt2ebook-0.1.158/src/txt2ebook/formats/gmi.py +164 -0
  5. txt2ebook-0.1.158/src/txt2ebook/formats/md.py +157 -0
  6. txt2ebook-0.1.158/src/txt2ebook/formats/txt.py +169 -0
  7. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/typ.py +40 -3
  8. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/massage.py +30 -9
  9. {txt2ebook-0.1.156 → txt2ebook-0.1.158/src/txt2ebook.egg-info}/PKG-INFO +17 -1
  10. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/requires.txt +19 -0
  11. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_parser.py +6 -3
  12. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_sort_volume_and_chapter_flag.py +24 -20
  13. txt2ebook-0.1.156/src/txt2ebook/formats/gmi.py +0 -177
  14. txt2ebook-0.1.156/src/txt2ebook/formats/md.py +0 -169
  15. txt2ebook-0.1.156/src/txt2ebook/formats/txt.py +0 -199
  16. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/LICENSE.md +0 -0
  17. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/README.md +0 -0
  18. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/setup.cfg +0 -0
  19. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/__init__.py +0 -0
  20. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/__main__.py +0 -0
  21. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/cli.py +0 -0
  22. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/exceptions.py +0 -0
  23. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/__init__.py +0 -0
  24. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/epub.py +0 -0
  25. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/pdf.py +0 -0
  26. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/templates/__init__.py +0 -0
  27. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/templates/epub/__init__.py +0 -0
  28. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/tex.py +0 -0
  29. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/helpers/__init__.py +0 -0
  30. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/languages/__init__.py +0 -0
  31. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/languages/en.py +0 -0
  32. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/languages/zh_cn.py +0 -0
  33. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/languages/zh_tw.py +0 -0
  34. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/models/__init__.py +0 -0
  35. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/models/book.py +0 -0
  36. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/models/chapter.py +0 -0
  37. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/models/volume.py +0 -0
  38. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/parser.py +0 -0
  39. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/__init__.py +0 -0
  40. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/env.py +0 -0
  41. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/epub.py +0 -0
  42. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/gmi.py +0 -0
  43. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/md.py +0 -0
  44. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/parse.py +0 -0
  45. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/pdf.py +0 -0
  46. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/tex.py +0 -0
  47. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/typ.py +0 -0
  48. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/tokenizer.py +0 -0
  49. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/zh_utils.py +0 -0
  50. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/SOURCES.txt +0 -0
  51. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/dependency_links.txt +0 -0
  52. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/entry_points.txt +0 -0
  53. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/top_level.txt +0 -0
  54. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_header_number_flag.py +0 -0
  55. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_input_file_arg.py +0 -0
  56. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_language_option.py +0 -0
  57. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_output_file_arg.py +0 -0
  58. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_overwrite_flag.py +0 -0
  59. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_purge_flag.py +0 -0
  60. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_quiet_flag.py +0 -0
  61. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_split_volume_and_chapter_flag.py +0 -0
  62. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_test_parsing_flag.py +0 -0
  63. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_tokenizer.py +0 -0
  64. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_txt2ebook.py +0 -0
  65. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_verbose_flag.py +0 -0
  66. {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_volume_page_flag.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: txt2ebook
3
- Version: 0.1.156
3
+ Version: 0.1.158
4
4
  Summary: CLI tool to convert txt file to ebook format
5
5
  Author-email: Kian-Meng Ang <kianmeng@cpan.org>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -39,6 +39,22 @@ Requires-Dist: regex<2022,>=2021.11.10
39
39
  Requires-Dist: reportlab<5,>=4.0.0
40
40
  Requires-Dist: typing-extensions<5,>=4.5.0
41
41
  Requires-Dist: typst>=0.13.0
42
+ Provides-Extra: test
43
+ Requires-Dist: pytest; extra == "test"
44
+ Requires-Dist: pytest-cov; extra == "test"
45
+ Requires-Dist: pytest-randomly; extra == "test"
46
+ Requires-Dist: pytest-xdist; extra == "test"
47
+ Requires-Dist: scripttest; extra == "test"
48
+ Provides-Extra: doc
49
+ Requires-Dist: myst-parser; extra == "doc"
50
+ Requires-Dist: sphinx; extra == "doc"
51
+ Requires-Dist: sphinx-autobuild; extra == "doc"
52
+ Requires-Dist: sphinx-autodoc-typehints; extra == "doc"
53
+ Requires-Dist: sphinx-copybutton; extra == "doc"
54
+ Provides-Extra: lint
55
+ Requires-Dist: pre-commit; extra == "lint"
56
+ Requires-Dist: ruff; extra == "lint"
57
+ Requires-Dist: mypy; extra == "lint"
42
58
  Dynamic: license-file
43
59
 
44
60
  # txt2ebook
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "txt2ebook"
3
- version = "0.1.156"
3
+ version = "0.1.158"
4
4
  description = "CLI tool to convert txt file to ebook format"
5
5
  authors = [{ name = "Kian-Meng Ang", email = "kianmeng@cpan.org" }]
6
6
  requires-python = "~=3.9"
@@ -60,26 +60,26 @@ Repository = "https://github.com/kianmeng/txt2ebook"
60
60
  txt2ebook = "txt2ebook.cli:main"
61
61
  tte = "txt2ebook.cli:main"
62
62
 
63
- [dependency-groups]
64
- dev = [
65
- "babel",
66
- "bandit",
67
- "flake8-simplify",
68
- "mypy",
69
- "myst-parser",
70
- "nox",
71
- "pep8-naming",
72
- "pre-commit",
63
+ [project.optional-dependencies]
64
+ test = [
65
+ "pytest",
73
66
  "pytest-cov",
74
67
  "pytest-randomly",
75
68
  "pytest-xdist",
76
- "pytest",
77
- "ruff",
78
69
  "scripttest",
70
+ ]
71
+ doc = [
72
+ "myst-parser",
73
+ "sphinx",
74
+ "sphinx-autobuild",
79
75
  "sphinx-autodoc-typehints",
80
76
  "sphinx-copybutton",
81
- "sphinx",
82
- "vulture",
77
+ ]
78
+
79
+ lint = [
80
+ "pre-commit",
81
+ "ruff",
82
+ "mypy",
83
83
  ]
84
84
 
85
85
  [build-system]
@@ -24,6 +24,7 @@ import shutil
24
24
  import subprocess
25
25
  import sys
26
26
  from abc import ABC, abstractmethod
27
+ from datetime import datetime as dt
27
28
  from importlib import import_module
28
29
  from pathlib import Path
29
30
 
@@ -134,6 +135,98 @@ class BaseWriter(ABC):
134
135
  file.parent, self.config.output_folder, lower_underscore(file.stem)
135
136
  ).with_suffix(extension)
136
137
 
138
+ def _get_toc_content_for_split(self) -> str:
139
+ raise NotImplementedError
140
+
141
+ def _get_volume_chapter_content_for_split(
142
+ self, volume: Volume, chapter: Chapter
143
+ ) -> str:
144
+ raise NotImplementedError
145
+
146
+ def _get_chapter_content_for_split(self, chapter: Chapter) -> str:
147
+ raise NotImplementedError
148
+
149
+ def _get_file_extension_for_split(self) -> str:
150
+ raise NotImplementedError
151
+
152
+ def _export_multiple_files(self) -> None:
153
+ logger.info("Split multiple files")
154
+
155
+ extension = self._get_file_extension_for_split()
156
+ txt_filename = Path(self.config.input_file.name)
157
+
158
+ export_filename = self._get_metadata_filename_for_split(txt_filename, extension)
159
+ export_filename.parent.mkdir(parents=True, exist_ok=True)
160
+ logger.info("Creating %s", export_filename)
161
+ with open(export_filename, "w", encoding="utf8") as file:
162
+ file.write(self._to_metadata_txt())
163
+
164
+ sc_seq = 1
165
+ if self.config.with_toc:
166
+ export_filename = self._get_toc_filename_for_split(txt_filename, extension)
167
+ export_filename.parent.mkdir(parents=True, exist_ok=True)
168
+ logger.info("Creating %s", export_filename)
169
+ with open(export_filename, "w", encoding="utf8") as file:
170
+ file.write(self._get_toc_content_for_split())
171
+
172
+ sc_seq = 2
173
+
174
+ for section in self.book.toc:
175
+ section_seq = str(sc_seq).rjust(2, "0")
176
+
177
+ ct_seq = 0
178
+ if isinstance(section, Volume):
179
+ for chapter in section.chapters:
180
+ chapter_seq = str(ct_seq).rjust(2, "0")
181
+ export_filename = self._get_volume_chapter_filename_for_split(
182
+ txt_filename, section_seq, chapter_seq, section, chapter, extension
183
+ )
184
+ export_filename.parent.mkdir(parents=True, exist_ok=True)
185
+ logger.info("Creating %s", export_filename)
186
+ with open(export_filename, "w", encoding="utf8") as file:
187
+ file.write(
188
+ self._get_volume_chapter_content_for_split(
189
+ section, chapter
190
+ )
191
+ )
192
+ ct_seq = ct_seq + 1
193
+ if isinstance(section, Chapter):
194
+ export_filename = self._get_chapter_filename_for_split(
195
+ txt_filename, section_seq, section, extension
196
+ )
197
+ export_filename.parent.mkdir(parents=True, exist_ok=True)
198
+ logger.info("Creating %s", export_filename)
199
+ with open(export_filename, "w", encoding="utf8") as file:
200
+ file.write(self._get_chapter_content_for_split(section))
201
+
202
+ sc_seq = sc_seq + 1
203
+
204
+ @abstractmethod
205
+ def _get_metadata_filename_for_split(self, txt_filename: Path, extension: str) -> Path:
206
+ raise NotImplementedError
207
+
208
+ @abstractmethod
209
+ def _get_toc_filename_for_split(self, txt_filename: Path, extension: str) -> Path:
210
+ raise NotImplementedError
211
+
212
+ @abstractmethod
213
+ def _get_volume_chapter_filename_for_split(
214
+ self,
215
+ txt_filename: Path,
216
+ section_seq: str,
217
+ chapter_seq: str,
218
+ volume: Volume,
219
+ chapter: Chapter,
220
+ extension: str,
221
+ ) -> Path:
222
+ raise NotImplementedError
223
+
224
+ @abstractmethod
225
+ def _get_chapter_filename_for_split(
226
+ self, txt_filename: Path, section_seq: str, chapter: Chapter, extension: str
227
+ ) -> Path:
228
+ raise NotImplementedError
229
+
137
230
  def _to_metadata_txt(self) -> str:
138
231
  metadata = [
139
232
  self._("title:") + self.book.title,
@@ -0,0 +1,164 @@
1
+ # Copyright (c) 2021,2022,2023,2024,2025 Kian-Meng Ang
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU Affero General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU Affero General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Affero General Public License
14
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ """Convert and back source text file into text as well."""
17
+
18
+ import logging
19
+ from pathlib import Path
20
+ from typing import List
21
+
22
+ from txt2ebook.formats.base import BaseWriter
23
+ from txt2ebook.helpers import lower_underscore
24
+ from txt2ebook.models import Chapter, Volume
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class GmiWriter(BaseWriter):
30
+ """Module for writing ebook in GemText (gmi) format."""
31
+
32
+ def write(self) -> None:
33
+ """Generate GemText files."""
34
+ if self.config.split_volume_and_chapter:
35
+ self._export_multiple_files()
36
+ else:
37
+ output_filename = self._output_filename(".gmi")
38
+ output_filename.parent.mkdir(parents=True, exist_ok=True)
39
+
40
+ with open(output_filename, "w", encoding="utf8") as file:
41
+ logger.info("Generate Gemini file: %s", output_filename.resolve())
42
+ file.write(self._to_gmi())
43
+
44
+ if self.config.open:
45
+ self._open_file(output_filename)
46
+
47
+ def _get_toc_content_for_split(self) -> str:
48
+ return self._to_toc("*", "# ")
49
+
50
+ def _get_volume_chapter_content_for_split(
51
+ self, volume: Volume, chapter: Chapter
52
+ ) -> str:
53
+ return self._to_volume_chapter_txt(volume, chapter)
54
+
55
+ def _get_chapter_content_for_split(self, chapter: Chapter) -> str:
56
+ return self._to_chapter_txt(chapter)
57
+
58
+ def _get_file_extension_for_split(self) -> str:
59
+ return ".gmi"
60
+
61
+ def _get_metadata_filename_for_split(self, txt_filename: Path, extension: str) -> Path:
62
+ return Path(
63
+ txt_filename.resolve().parent.joinpath(
64
+ self.config.output_folder,
65
+ lower_underscore(
66
+ f"00_{txt_filename.stem}_" + self._("metadata") + extension
67
+ ),
68
+ )
69
+ )
70
+
71
+ def _get_toc_filename_for_split(self, txt_filename: Path, extension: str) -> Path:
72
+ return Path(
73
+ txt_filename.resolve().parent.joinpath(
74
+ self.config.output_folder,
75
+ lower_underscore(
76
+ f"01_{txt_filename.stem}_" + self._("toc") + extension
77
+ ),
78
+ )
79
+ )
80
+
81
+ def _get_volume_chapter_filename_for_split(
82
+ self,
83
+ txt_filename: Path,
84
+ section_seq: str,
85
+ chapter_seq: str,
86
+ volume: Volume,
87
+ chapter: Chapter,
88
+ extension: str,
89
+ ) -> Path:
90
+ return Path(
91
+ txt_filename.resolve().parent.joinpath(
92
+ self.config.output_folder,
93
+ lower_underscore(
94
+ (
95
+ f"{section_seq}"
96
+ f"_{chapter_seq}"
97
+ f"_{txt_filename.stem}"
98
+ f"_{volume.title}"
99
+ f"_{chapter.title}"
100
+ f"{extension}"
101
+ )
102
+ ),
103
+ )
104
+ )
105
+
106
+ def _get_chapter_filename_for_split(
107
+ self, txt_filename: Path, section_seq: str, chapter: Chapter, extension: str
108
+ ) -> Path:
109
+ return Path(
110
+ txt_filename.resolve().parent.joinpath(
111
+ self.config.output_folder,
112
+ lower_underscore(
113
+ (f"{section_seq}_{txt_filename.stem}_{chapter.title}{extension}")
114
+ ),
115
+ )
116
+ )
117
+
118
+ def _to_gmi(self) -> str:
119
+ toc = self._to_toc("*", "# ") if self.config.with_toc else ""
120
+ return self._to_metadata_txt() + toc + self._to_body_txt()
121
+
122
+ def _to_body_txt(self) -> str:
123
+ content = []
124
+ for section in self.book.toc:
125
+ if isinstance(section, Volume):
126
+ content.append(self._to_volume_txt(section))
127
+ if isinstance(section, Chapter):
128
+ content.append(self._to_chapter_txt(section))
129
+
130
+ return f"{self.config.paragraph_separator}".join(content)
131
+
132
+ def _to_volume_txt(self, volume) -> str:
133
+ return (
134
+ f"# {volume.title}"
135
+ + self.config.paragraph_separator
136
+ + self.config.paragraph_separator.join(
137
+ [
138
+ self._to_chapter_txt(chapter, True)
139
+ for chapter in volume.chapters
140
+ ]
141
+ )
142
+ )
143
+
144
+ def _to_chapter_txt(self, chapter, part_of_volume=False) -> str:
145
+ header = "##" if part_of_volume else "#"
146
+ return (
147
+ f"{header} {chapter.title}"
148
+ + self.config.paragraph_separator
149
+ + self.config.paragraph_separator.join(
150
+ self._remove_newline(chapter.paragraphs)
151
+ )
152
+ )
153
+
154
+ def _to_volume_chapter_txt(self, volume, chapter) -> str:
155
+ return (
156
+ f"# {volume.title} {chapter.title}"
157
+ + self.config.paragraph_separator
158
+ + self.config.paragraph_separator.join(
159
+ self._remove_newline(chapter.paragraphs)
160
+ )
161
+ )
162
+
163
+ def _remove_newline(self, paragraphs) -> List:
164
+ return list(map(lambda p: p.replace("\n", ""), paragraphs))
@@ -0,0 +1,157 @@
1
+ # Copyright (c) 2021,2022,2023,2024,2025 Kian-Meng Ang
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU Affero General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU Affero General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Affero General Public License
14
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ """Convert and back source text file into text as well."""
17
+
18
+ import logging
19
+ from pathlib import Path
20
+ from pathlib import Path
21
+
22
+ from txt2ebook.formats.base import BaseWriter
23
+ from txt2ebook.helpers import lower_underscore
24
+ from txt2ebook.models import Chapter, Volume
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class MdWriter(BaseWriter):
30
+ """Module for writing ebook in Markdown (md) format."""
31
+
32
+ def write(self) -> None:
33
+ """Generate Markdown files."""
34
+ if self.config.split_volume_and_chapter:
35
+ self._export_multiple_files()
36
+ else:
37
+ output_filename = self._output_filename(".md")
38
+ output_filename.parent.mkdir(parents=True, exist_ok=True)
39
+
40
+ with open(output_filename, "w", encoding="utf8") as file:
41
+ logger.info("Generate Markdown file: %s", output_filename.resolve())
42
+ file.write(self._to_md())
43
+
44
+ if self.config.open:
45
+ self._open_file(output_filename)
46
+
47
+ def _get_toc_content_for_split(self) -> str:
48
+ return self._to_toc("-", "# ")
49
+
50
+ def _get_volume_chapter_content_for_split(
51
+ self, volume: Volume, chapter: Chapter
52
+ ) -> str:
53
+ return self._to_volume_chapter_txt(volume, chapter)
54
+
55
+ def _get_chapter_content_for_split(self, chapter: Chapter) -> str:
56
+ return self._to_chapter_txt(chapter)
57
+
58
+ def _get_file_extension_for_split(self) -> str:
59
+ return ".md"
60
+
61
+ def _get_metadata_filename_for_split(self, txt_filename: Path, extension: str) -> Path:
62
+ return Path(
63
+ txt_filename.resolve().parent.joinpath(
64
+ self.config.output_folder,
65
+ lower_underscore(
66
+ f"00_{txt_filename.stem}_" + self._("metadata") + extension
67
+ ),
68
+ )
69
+ )
70
+
71
+ def _get_toc_filename_for_split(self, txt_filename: Path, extension: str) -> Path:
72
+ return Path(
73
+ txt_filename.resolve().parent.joinpath(
74
+ self.config.output_folder,
75
+ lower_underscore(
76
+ f"01_{txt_filename.stem}_" + self._("toc") + extension
77
+ ),
78
+ )
79
+ )
80
+
81
+ def _get_volume_chapter_filename_for_split(
82
+ self,
83
+ txt_filename: Path,
84
+ section_seq: str,
85
+ chapter_seq: str,
86
+ volume: Volume,
87
+ chapter: Chapter,
88
+ extension: str,
89
+ ) -> Path:
90
+ return Path(
91
+ txt_filename.resolve().parent.joinpath(
92
+ self.config.output_folder,
93
+ lower_underscore(
94
+ (
95
+ f"{section_seq}"
96
+ f"_{chapter_seq}"
97
+ f"_{txt_filename.stem}"
98
+ f"_{volume.title}"
99
+ f"_{chapter.title}"
100
+ f"{extension}"
101
+ )
102
+ ),
103
+ )
104
+ )
105
+
106
+ def _get_chapter_filename_for_split(
107
+ self, txt_filename: Path, section_seq: str, chapter: Chapter, extension: str
108
+ ) -> Path:
109
+ return Path(
110
+ txt_filename.resolve().parent.joinpath(
111
+ self.config.output_folder,
112
+ lower_underscore(
113
+ (f"{section_seq}_{txt_filename.stem}_{chapter.title}{extension}")
114
+ ),
115
+ )
116
+ )
117
+
118
+ def _to_md(self) -> str:
119
+ toc = self._to_toc("-", "# ") if self.config.with_toc else ""
120
+ return self._to_metadata_txt() + toc + self._to_body_txt()
121
+
122
+ def _to_body_txt(self) -> str:
123
+ content = []
124
+ for section in self.book.toc:
125
+ if isinstance(section, Volume):
126
+ content.append(self._to_volume_txt(section))
127
+ if isinstance(section, Chapter):
128
+ content.append(self._to_chapter_txt(section))
129
+
130
+ return f"{self.config.paragraph_separator}".join(content)
131
+
132
+ def _to_volume_txt(self, volume) -> str:
133
+ return (
134
+ f"# {volume.title}"
135
+ + self.config.paragraph_separator
136
+ + self.config.paragraph_separator.join(
137
+ [
138
+ self._to_chapter_txt(chapter, True)
139
+ for chapter in volume.chapters
140
+ ]
141
+ )
142
+ )
143
+
144
+ def _to_chapter_txt(self, chapter, part_of_volume=False) -> str:
145
+ header = "##" if part_of_volume else "#"
146
+ return (
147
+ f"{header} {chapter.title}"
148
+ + self.config.paragraph_separator
149
+ + self.config.paragraph_separator.join(chapter.paragraphs)
150
+ )
151
+
152
+ def _to_volume_chapter_txt(self, volume, chapter) -> str:
153
+ return (
154
+ f"# {volume.title} {chapter.title}"
155
+ + self.config.paragraph_separator
156
+ + self.config.paragraph_separator.join(chapter.paragraphs)
157
+ )
@@ -0,0 +1,169 @@
1
+ # Copyright (c) 2021,2022,2023,2024,2025 Kian-Meng Ang
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU Affero General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU Affero General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Affero General Public License
14
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ """Convert and backup source text file into text as well."""
17
+
18
+ import logging
19
+ import shutil
20
+ from datetime import datetime as dt
21
+ from pathlib import Path
22
+
23
+ from txt2ebook.formats.base import BaseWriter
24
+ from txt2ebook.helpers import lower_underscore
25
+ from txt2ebook.models import Chapter, Volume
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class TxtWriter(BaseWriter):
31
+ """Module for writing ebook in txt format."""
32
+
33
+ def write(self) -> None:
34
+ """Optionally backup and overwrite the txt file.
35
+
36
+ If the input content came from stdin, we'll skip backup and overwrite
37
+ source text file.
38
+ """
39
+ if self.config.input_file.name == "<stdin>":
40
+ logger.info("Skip backup source text file as content from stdin")
41
+ elif self.config.split_volume_and_chapter:
42
+ self._export_multiple_files()
43
+ else:
44
+ output_filename = self._output_filename(".txt")
45
+ output_filename.parent.mkdir(parents=True, exist_ok=True)
46
+
47
+ if self.config.overwrite and output_filename == Path(
48
+ self.config.input_file.name
49
+ ):
50
+ ymd_hms = dt.now().strftime("%Y%m%d_%H%M%S")
51
+ backup_filename = Path(
52
+ Path(self.config.input_file.name)
53
+ .resolve()
54
+ .parent.joinpath(
55
+ lower_underscore(
56
+ Path(self.config.input_file.name).stem
57
+ + "_" + ymd_hms + ".txt"
58
+ )
59
+ )
60
+ )
61
+ logger.info("Backup source text file: %s", backup_filename.resolve())
62
+ shutil.copyfile(output_filename, backup_filename)
63
+
64
+ with open(output_filename, "w", encoding="utf8") as file:
65
+ logger.info("Generate TXT file: %s", output_filename.resolve())
66
+ file.write(self._to_txt())
67
+
68
+ if self.config.open:
69
+ self._open_file(output_filename)
70
+
71
+
72
+
73
+
74
+
75
+ def _get_metadata_filename_for_split(self, txt_filename: Path, extension: str) -> Path:
76
+ return Path(
77
+ txt_filename.resolve().parent.joinpath(
78
+ self.config.output_folder,
79
+ lower_underscore(
80
+ f"00_{txt_filename.stem}_" + self._("metadata") + extension
81
+ ),
82
+ )
83
+ )
84
+
85
+ def _get_toc_filename_for_split(self, txt_filename: Path, extension: str) -> Path:
86
+ return Path(
87
+ txt_filename.resolve().parent.joinpath(
88
+ self.config.output_folder,
89
+ lower_underscore(
90
+ f"01_{txt_filename.stem}_" + self._("toc") + extension
91
+ ),
92
+ )
93
+ )
94
+
95
+ def _get_volume_chapter_filename_for_split(
96
+ self,
97
+ txt_filename: Path,
98
+ section_seq: str,
99
+ chapter_seq: str,
100
+ volume: Volume,
101
+ chapter: Chapter,
102
+ extension: str,
103
+ ) -> Path:
104
+ return Path(
105
+ txt_filename.resolve().parent.joinpath(
106
+ self.config.output_folder,
107
+ lower_underscore(
108
+ (
109
+ f"{section_seq}"
110
+ f"_{chapter_seq}"
111
+ f"_{txt_filename.stem}"
112
+ f"_{volume.title}"
113
+ f"_{chapter.title}"
114
+ f"{extension}"
115
+ )
116
+ ),
117
+ )
118
+ )
119
+
120
+ def _get_chapter_filename_for_split(
121
+ self, txt_filename: Path, section_seq: str, chapter: Chapter, extension: str
122
+ ) -> Path:
123
+ return Path(
124
+ txt_filename.resolve().parent.joinpath(
125
+ self.config.output_folder,
126
+ lower_underscore(
127
+ (f"{section_seq}_{txt_filename.stem}_{chapter.title}{extension}")
128
+ ),
129
+ )
130
+ )
131
+
132
+ def _to_txt(self) -> str:
133
+ toc = self._to_toc("-") if self.config.with_toc else ""
134
+ return self._to_metadata_txt() + toc + self._to_body_txt()
135
+
136
+ def _to_body_txt(self) -> str:
137
+ content = []
138
+ for section in self.book.toc:
139
+ if isinstance(section, Volume):
140
+ content.append(self._to_volume_txt(section))
141
+ if isinstance(section, Chapter):
142
+ content.append(self._to_chapter_txt(section))
143
+
144
+ return f"{self.config.paragraph_separator}".join(content)
145
+
146
+ def _to_volume_txt(self, volume) -> str:
147
+ return (
148
+ volume.title
149
+ + self.config.paragraph_separator
150
+ + self.config.paragraph_separator.join(
151
+ [self._to_chapter_txt(chapter) for chapter in volume.chapters]
152
+ )
153
+ )
154
+
155
+ def _to_chapter_txt(self, chapter) -> str:
156
+ return (
157
+ chapter.title
158
+ + self.config.paragraph_separator
159
+ + self.config.paragraph_separator.join(chapter.paragraphs)
160
+ )
161
+
162
+ def _to_volume_chapter_txt(self, volume, chapter) -> str:
163
+ return (
164
+ volume.title
165
+ + " "
166
+ + chapter.title
167
+ + self.config.paragraph_separator
168
+ + self.config.paragraph_separator.join(chapter.paragraphs)
169
+ )