feat: --chapter-level auto teilt nur sehr große Kapitel (R2)

Neuer Standard auto: Ebene 1 wie bisher, aber Kapitel mit über 30 000
Wörtern und mindestens zwei Unterkapiteln der nächsten Ebene werden an
diesen geteilt (Teile eines Romans). Alle anderen Kapitel bleiben ganz;
Register, Literatur und Glossar werden nie zerlegt. Ein Probedurchlauf
des ChapterBuilder bestimmt die Kapitel, der zweite teilt sie (deep_starts).
Info chapter-split; eine feste Zahl verhält sich wie bisher.

Roman (Adler-Olsen): 78 statt 9 Dateien, wortgleich, 0 kaputte Links.
Klein, Krooß, Lerch, Liesen: Kapiteldateien byte-identisch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-06 21:23:47 +02:00
commit 6fe422919f

View file

@ -127,7 +127,7 @@ def test_glossary_anchor_is_placed_before_bold_term(convert):
def test_large_chapter_with_subheadings_suggests_deeper_chapter_level(convert):
words = " ".join(["Wort"] * 11000)
body = "<h1>Teil 1</h1>" + "".join(f"<h2>Kapitel {i}</h2><p>{words}</p>" for i in range(3))
run = convert(Book(docs=[Doc("t.xhtml", body)]))
run = convert(Book(docs=[Doc("t.xhtml", body)]), "--chapter-level", "1")
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"]
@ -135,6 +135,33 @@ def test_large_chapter_with_subheadings_suggests_deeper_chapter_level(convert):
assert not any(w["code"] == "chapter-large" for w in small.report["warnings"])
def test_auto_level_splits_only_oversized_chapters(convert):
"""Default --chapter-level auto: a part over 30000 words is split at its h2, other chapters stay whole (R2)."""
words = " ".join(["Wort"] * 11000)
docs = [
Doc("v.xhtml", "<h1>Vorwort</h1><p>kurz</p><h2>Dank</h2><p>danke</p><h2>Hinweise</h2><p>lesen</p>"),
Doc("t1.xhtml", "<h1>Teil 1</h1><p>Einleitung</p>"),
*[Doc(f"k{i}.xhtml", f"<h2>Kapitel {i}</h2><p>{words}</p><h3>Abschnitt</h3><p>x</p>") for i in range(1, 4)],
Doc("t2.xhtml", "<h1>Teil 2</h1><h2>Kapitel 4</h2><p>kurz</p><h2>Kapitel 5</h2><p>kurz</p>"),
]
run = convert(Book(docs=docs))
assert [p.name for p in run.chapters()] == [
"001-vorwort.md", "002-teil-1.md", "003-kapitel-1.md", "004-kapitel-2.md", "005-kapitel-3.md", "006-teil-2.md"]
assert "## Dank" in run.chapter("001") and "## Kapitel 5" in run.chapter("006")
assert "Einleitung" in run.chapter("002") and "## Abschnitt" in run.chapter("003")
assert run.chapter("003").count("\n# ") == 1
assert not any(w["code"] == "chapter-large" for w in run.report["warnings"])
assert any(i["code"] == "chapter-split" for i in run.report["infos"])
assert run.report["options"]["chapter_level"] == "auto"
explicit = convert(Book(docs=docs), "--chapter-level", "1")
assert len(explicit.chapters()) == 3
def test_chapter_level_rejects_invalid_values(convert):
for value in ("0", "7", "x"):
assert convert(Book(docs=[Doc("a.xhtml", "<h1>A</h1>")]), "--chapter-level", value).rc == 1
def test_titles_derived_from_content_when_no_other_source(convert):
docs = [
Doc("ccover.xhtml", '<div><img src="c.png" alt=""/></div>'),