From 8afa1cc6d4a4fafc8fb85e78af478b02c5b9f9a3 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 5 Oct 2026 20:00:02 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Warnung=20chapter-large=20mit=20--chapt?= =?UTF-8?q?er-level-Empfehlung=20f=C3=BCr=20sehr=20gro=C3=9Fe=20Kapitel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kapiteldateien über 30 000 Wörtern mit mindestens zwei ##-Unterabschnitten erzeugen eine Warnung mit der passenden --chapter-level-Empfehlung (QA-Befund R2, z. B. Roman mit Teilen als h1). Co-Authored-By: Claude Opus 5.5 --- README.md | 2 ++ epub2md/pipeline.py | 20 ++++++++++++++++++-- tests/test_chapters.py | 11 +++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60e0ece..109032d 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,8 @@ Literaturlisten mit eigenen Labels (`[BryL06]`) werden nicht doppelt nummeriert. **Tipp:** Hat ein Buch Teile als `h1` und Kapitel als `h2` (typisch bei Romanen), erzeugt `--chapter-level 2` eine Datei pro Kapitel. Mit dem Standard `1` entsteht eine Datei pro Teil. +Ist eine Kapiteldatei größer als 30 000 Wörter und hat sie mindestens zwei `##`-Unterabschnitte, meldet +epub2md eine Warnung `chapter-large` mit der passenden `--chapter-level`-Empfehlung. ## Math-Modi diff --git a/epub2md/pipeline.py b/epub2md/pipeline.py index 0ec0ea7..9c1fc73 100644 --- a/epub2md/pipeline.py +++ b/epub2md/pipeline.py @@ -4,6 +4,7 @@ from __future__ import annotations import os import posixpath +import re import shutil import sys import tempfile @@ -28,6 +29,7 @@ from .utils import is_inside, iter_elements, sha256_file, slugify from . import writer MARKER_FILES = ("conversion-report.json", "manifest.json") +LARGE_CHAPTER_WORDS = 30000 # above this a deeper --chapter-level is suggested (RAG/readability) @dataclass @@ -189,6 +191,7 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio diag.step(f"Rendere {ch.filename}") ch.markdown = renderer.render_chapter(ch) ch.stats.word_count = _word_count(ch.markdown) + _check_chapter_size(ch, opts.chapter_level, diag) endnotes: Chapter | None = None endnotes_md = "" if opts.footnotes == "end-of-book" and renderer.book_notes: @@ -288,9 +291,22 @@ def _assign_filenames(chapters: list[Chapter], slug_language: str) -> None: ch.filename = f"chapters/{candidate}.md" -def _word_count(markdown: str) -> int: - import re +def _check_chapter_size(ch: Chapter, chapter_level: int, diag: Diagnostics) -> None: + """Suggest a deeper --chapter-level for very large chapters with enough sub-headings.""" + if ch.stats.word_count <= LARGE_CHAPTER_WORDS or chapter_level >= 6: + return + sections = len(re.findall(r"^## ", ch.markdown, re.M)) + if sections >= 2: + diag.warn( + "chapter-large", + f"Kapitel „{ch.title}“ hat {ch.stats.word_count} Wörter und {sections} Unterabschnitte; " + f"--chapter-level {chapter_level + 1} würde sie als eigene Dateien ausgeben.", + ch.docs[0] if ch.docs else "", + ch.filename, + ) + +def _word_count(markdown: str) -> int: text = re.sub(r"^(`{3,}).*?^\1\s*$", " ", markdown, flags=re.M | re.S) text = re.sub(r"<[^>]+>|\]\([^)]*\)", " ", text) return len(re.findall(r"\w+", text)) diff --git a/tests/test_chapters.py b/tests/test_chapters.py index 23adb2e..2e12493 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -122,3 +122,14 @@ def test_glossary_anchor_is_placed_before_bold_term(convert): '
API
Schnittstelle.
') text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("002") assert '**API:** Schnittstelle.' in text + + +def test_large_chapter_with_subheadings_suggests_deeper_chapter_level(convert): + words = " ".join(["Wort"] * 11000) + body = "

Teil 1

" + "".join(f"

Kapitel {i}

{words}

" for i in range(3)) + run = convert(Book(docs=[Doc("t.xhtml", body)])) + hints = [w for w in run.report["warnings"] if w["code"] == "chapter-large"] + assert len(hints) == 1 + assert "--chapter-level 2" in hints[0]["message"] and "3 Unterabschnitte" in hints[0]["message"] + small = convert(Book(docs=[Doc("t.xhtml", "

Teil

A

x

B

y

")])) + assert not any(w["code"] == "chapter-large" for w in small.report["warnings"])