EPUB как второй вход в конвейер; листинги кода не переводятся и не ломают приёмку

- epub2html.py: EPUB -> тот же XHTML, что pdf2html.py, только стандартная
  библиотека. Уровень глав определяется по книге, а не берётся из <h1>:
  в EPUB там обычно название и части.
- pdf2html.py: листинг узнаётся по флагу моноширинного шрифта PyMuPDF,
  переносы и отступы внутри <pre> сохраняются, де-дефисация к коду не
  применяется.
- translate.py: <pre> исключён из проверок языка (английский код утягивал
  долю кириллицы ниже порога приёмки), рабочий каталог привязан к книге,
  устаревшие главы прошлого прогона чистятся, инлайновый <code> переживает
  переводчика.
- bookhtml.py: общий каркас документа и CSS для обоих входов.
- Тесты: test_epub2html.py на собранном в памяти EPUB, четыре новых случая
  в test_translate.py.
This commit is contained in:
chesirecatt
2026-08-20 17:58:09 +03:00
parent 6c81673b4f
commit db9fe5e0fa
7 changed files with 614 additions and 43 deletions
+67 -4
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Самопроверка моста без сети: python3 test_translate.py"""
import json
import re
from pathlib import Path
import translate as t
@@ -17,6 +18,14 @@ def test_marks_roundtrip():
assert "&amp;" in body, "спецсимволы должны экранироваться обратно"
def test_inline_code_survives():
"""Имена функций в прозе не должны терять разметку по дороге к модели."""
marked = t.to_marks("call <code>fetch()</code> twice")
assert marked == "call ⟦code⟧fetch()⟦/code⟧ twice"
body, ok = t.from_marks(marked)
assert ok and "<code>fetch()</code>" in body
def test_broken_marks_drop_tags():
body, ok = t.from_marks("текст ⟦i⟧без закрытия")
assert not ok and "⟦" not in body and "<i>" not in body
@@ -71,6 +80,58 @@ def test_split_and_rebuild(tmp: Path):
assert result.count("<h1>") == 2
def test_code_listings_are_left_alone(tmp: Path):
"""Листинги не переводятся и не участвуют в приёмке по языку: иначе книга,
где кода много, честно переведётся и завалит проверку."""
listing = "\n".join([
"<pre>def compute_total(items, discount_rate, shipping_cost):",
" subtotal = sum(item.price * item.quantity for item in items)",
" return subtotal * (1 - discount_rate) + shipping_cost # not translated",
"</pre>",
])
prose = ("<p>Текст главы про обработку данных, списки значений и способы "
"хранения промежуточных результатов между запусками.</p>")
src = tmp / "code.html"
src.write_text("<h1>Глава</h1>\n" + (listing + "\n" + prose + "\n") * 3,
encoding="utf-8")
blocks = t.parse_blocks(src)
assert [b[1] for b in blocks] == ["h1", "p", "p", "p"], \
"листинг не должен попасть в перевод"
# латиницы в файле больше, чем кириллицы, — но вся она в <pre>
raw = re.sub("<[^>]+>", "", src.read_text(encoding="utf-8"))
assert t.detect_language(raw)[0] == "en", "образец должен быть латинским целиком"
assert t.verify_output(src, "ru"), "код не должен заваливать приёмку"
def test_workdir_belongs_to_one_book(tmp: Path):
"""Имена chapter_NNN.json у всех книг одинаковы: чужой workdir склеит
перевод одной книги с текстом другой."""
wd = tmp / "wd"
wd.mkdir()
a = [(0, "p", "", "First book text.")]
b = [(0, "p", "", "Совершенно другая книга.")]
t.claim_workdir(wd, Path("a.html"), a)
t.claim_workdir(wd, Path("a.html"), a) # повторный запуск той же книги — ок
try:
t.claim_workdir(wd, Path("b.html"), b)
except SystemExit as e:
assert "занят другой книгой" in str(e)
else:
raise AssertionError("чужая книга в занятом workdir должна отвергаться")
def test_stale_chapters_removed(tmp: Path):
"""Прошлый прогон был длиннее — его главы иначе уедут в перевод."""
extracted = tmp / "extracted"
extracted.mkdir()
(extracted / "chapter_009.json").write_text("{}", encoding="utf-8")
t.write_input([[(0, "h1", "", "Глава")]], extracted)
assert not (extracted / "chapter_009.json").exists()
assert (extracted / "chapter_000.json").exists()
def test_verify_output_catches_untranslated(tmp: Path):
"""Регресс: первый боевой прогон отрапортовал успех на непереведённом файле."""
bad = tmp / "bad.html"
@@ -90,10 +151,12 @@ if __name__ == "__main__":
import tempfile
test_marks_roundtrip()
test_inline_code_survives()
test_broken_marks_drop_tags()
test_language_detection()
with tempfile.TemporaryDirectory() as d:
test_split_and_rebuild(Path(d))
with tempfile.TemporaryDirectory() as d:
test_verify_output_catches_untranslated(Path(d))
for case in (test_split_and_rebuild, test_code_listings_are_left_alone,
test_workdir_belongs_to_one_book, test_stale_chapters_removed,
test_verify_output_catches_untranslated):
with tempfile.TemporaryDirectory() as d:
case(Path(d))
print("OK")