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.
- {txt2ebook-0.1.156/src/txt2ebook.egg-info → txt2ebook-0.1.158}/PKG-INFO +17 -1
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/pyproject.toml +15 -15
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/base.py +93 -0
- txt2ebook-0.1.158/src/txt2ebook/formats/gmi.py +164 -0
- txt2ebook-0.1.158/src/txt2ebook/formats/md.py +157 -0
- txt2ebook-0.1.158/src/txt2ebook/formats/txt.py +169 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/typ.py +40 -3
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/massage.py +30 -9
- {txt2ebook-0.1.156 → txt2ebook-0.1.158/src/txt2ebook.egg-info}/PKG-INFO +17 -1
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/requires.txt +19 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_parser.py +6 -3
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_sort_volume_and_chapter_flag.py +24 -20
- txt2ebook-0.1.156/src/txt2ebook/formats/gmi.py +0 -177
- txt2ebook-0.1.156/src/txt2ebook/formats/md.py +0 -169
- txt2ebook-0.1.156/src/txt2ebook/formats/txt.py +0 -199
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/LICENSE.md +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/README.md +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/setup.cfg +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/__init__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/__main__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/cli.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/exceptions.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/__init__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/epub.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/pdf.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/templates/__init__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/templates/epub/__init__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/formats/tex.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/helpers/__init__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/languages/__init__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/languages/en.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/languages/zh_cn.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/languages/zh_tw.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/models/__init__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/models/book.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/models/chapter.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/models/volume.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/parser.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/__init__.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/env.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/epub.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/gmi.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/md.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/parse.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/pdf.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/tex.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/subcommands/typ.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/tokenizer.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook/zh_utils.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/SOURCES.txt +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/dependency_links.txt +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/entry_points.txt +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/src/txt2ebook.egg-info/top_level.txt +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_header_number_flag.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_input_file_arg.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_language_option.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_output_file_arg.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_overwrite_flag.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_purge_flag.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_quiet_flag.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_split_volume_and_chapter_flag.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_test_parsing_flag.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_tokenizer.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_txt2ebook.py +0 -0
- {txt2ebook-0.1.156 → txt2ebook-0.1.158}/tests/test_verbose_flag.py +0 -0
- {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.
|
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.
|
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
|
-
[
|
64
|
-
|
65
|
-
"
|
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
|
-
|
82
|
-
|
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
|
+
)
|