Überschriften, die genau einem Verzeichnisnamen entsprechen (Quellen, Literatur, Glossar, Register, Index …), beginnen auch unterhalb von --chapter-level eine eigene Datei mit passendem Typ. Die nächste Überschrift gleicher oder höherer Ebene beendet den Abschnitt; innerhalb eines Kapitels, das selbst ein Verzeichnis ist, wird nicht weiter geteilt. Im Roman-Beispiel wird 'Quellen' aus dem Anhang so zum Literaturverzeichnis; die anderen vier Beispielbücher bleiben unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
187 lines
9.7 KiB
Python
187 lines
9.7 KiB
Python
import yaml
|
|
|
|
from conftest import Book, Doc
|
|
|
|
|
|
def frontmatter(text: str) -> dict:
|
|
assert text.startswith("---\n")
|
|
return yaml.safe_load(text.split("---\n")[1])
|
|
|
|
|
|
def test_files_are_numbered_and_slugged(convert):
|
|
docs = [Doc("c1.xhtml", "<h1>Übersicht & Größen</h1><p>a</p>"), Doc("c2.xhtml", "<h1>Übersicht & Größen</h1><p>b</p>")]
|
|
run = convert(Book(docs=docs))
|
|
assert [p.name for p in run.chapters()] == ["001-uebersicht-groessen.md", "002-uebersicht-groessen.md"]
|
|
run_en = convert(Book(docs=docs), "--slug-language", "en")
|
|
assert run_en.chapters()[0].name == "001-ubersicht-grossen.md"
|
|
run_none = convert(Book(docs=docs), "--slug-language", "none")
|
|
assert run_none.chapters()[0].name == "001-übersicht-größen.md"
|
|
|
|
|
|
def test_h1_split_inside_one_file(convert):
|
|
body = "<h1>Eins</h1><p>a</p><h2>Eins.Eins</h2><p>b</p><h1>Zwei</h1><p>c</p>"
|
|
run = convert(Book(docs=[Doc("all.xhtml", body)]))
|
|
assert [p.name for p in run.chapters()] == ["001-eins.md", "002-zwei.md"]
|
|
assert "## Eins.Eins" in run.chapter("001")
|
|
|
|
|
|
def test_h2_split_with_chapter_level_2(convert):
|
|
body = "<h1>Teil I</h1><p>Intro</p><h2>Kapitel 1</h2><p>a</p><h2>Kapitel 2</h2><p>b</p>"
|
|
run = convert(Book(docs=[Doc("all.xhtml", body)]), "--chapter-level", "2")
|
|
names = [p.name for p in run.chapters()]
|
|
assert names == ["001-teil-i.md", "002-kapitel-1.md", "003-kapitel-2.md"]
|
|
assert run.chapter("002").count("\n# ") == 1 # exactly one H1 per file
|
|
|
|
|
|
def test_section_files_without_h1_continue_previous_chapter(convert):
|
|
docs = [
|
|
Doc("01.xhtml", "<h1>Kapitel 1</h1><p>a</p>"),
|
|
Doc("01b.xhtml", '<div class="wrap"><h2>1.1 Abschnitt</h2><p>b</p></div>'),
|
|
Doc("02.xhtml", "<h1>Kapitel 2</h1><p>c</p>"),
|
|
]
|
|
run = convert(Book(docs=docs))
|
|
assert len(run.chapters()) == 2
|
|
first = run.chapter("001")
|
|
assert "## 1.1 Abschnitt" in first
|
|
assert frontmatter(first)["source_hrefs"] == ["01.xhtml", "01b.xhtml"]
|
|
|
|
|
|
def test_headingless_documents_become_own_chapters_with_nav_title(convert):
|
|
docs = [Doc("titel.xhtml", "<p>Widmung für alle</p>"), Doc("c.xhtml", "<h1>Start</h1><p>x</p>")]
|
|
book = Book(docs=docs, nav=[("Widmung", "titel.xhtml"), ("Start", "c.xhtml")])
|
|
run = convert(book)
|
|
assert [p.name for p in run.chapters()] == ["001-widmung.md", "002-start.md"]
|
|
fm = frontmatter(run.chapter("001"))
|
|
assert fm["chapter_title"] == "Widmung"
|
|
assert "dedication" in fm["epub_type"]
|
|
|
|
|
|
def test_semantic_sections_are_split_and_classified(convert):
|
|
body = (
|
|
'<section epub:type="chapter"><h1>Kapitel</h1><p>Text</p></section>'
|
|
'<section epub:type="glossary"><h2>Glossar</h2><dl><dt>API</dt><dd>Schnittstelle.</dd></dl></section>'
|
|
'<section epub:type="bibliography"><h2>Literatur</h2><ul><li>Knuth, D.: TAOCP. '
|
|
'<a href="https://doi.org/10.1000/xyz">doi:10.1000/xyz</a>, ISBN 978-0-201-89683-1</li></ul></section>'
|
|
'<section epub:type="index"><h2>Sachregister</h2><p>A</p></section>'
|
|
'<section epub:type="index"><h2>Personenregister</h2><p>Knuth</p></section>'
|
|
)
|
|
run = convert(Book(docs=[Doc("b.xhtml", body)]))
|
|
names = [p.name for p in run.chapters()]
|
|
assert names == ["001-kapitel.md", "002-glossar.md", "003-literatur.md", "004-sachregister.md", "005-personenregister.md"]
|
|
assert frontmatter(run.chapter("002"))["epub_type"] == ["glossary"]
|
|
assert "**API:** Schnittstelle." in run.chapter("002")
|
|
lit = run.chapter("003")
|
|
assert "[doi:10.1000/xyz](https://doi.org/10.1000/xyz)" in lit and "ISBN 978-0-201-89683-1" in lit
|
|
assert "index-persons" in frontmatter(run.chapter("005"))["epub_type"]
|
|
readme = (run.out / "README.md").read_text(encoding="utf-8")
|
|
assert "Glossar: [Glossar](chapters/002-glossar.md)" in readme
|
|
assert "Personenregister: [Personenregister](chapters/005-personenregister.md)" in readme
|
|
types = {s["type"] for s in run.report["special_sections"]}
|
|
assert {"glossary", "bibliography", "index", "index-persons"} <= types
|
|
|
|
|
|
def test_wrapper_preamble_belongs_to_heading_chapter(convert):
|
|
body = '<div epub:type="chapter"><div class="ctx">Kontextinfo</div><h1>Kapitel</h1><p>Text</p></div>'
|
|
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
|
assert len(run.chapters()) == 1
|
|
text = run.chapter("001")
|
|
assert "Kontextinfo" in text and text.count("# Kapitel") == 1
|
|
|
|
|
|
def test_html_toc_document_is_not_duplicated(convert):
|
|
toc = '<h1>Inhaltsverzeichnis</h1><p><a href="a.xhtml">Kapitel A</a></p><p><a href="b.xhtml">Kapitel B</a></p><p><a href="b.xhtml#x">B.1</a></p>'
|
|
docs = [Doc("toc.xhtml", toc), Doc("a.xhtml", "<h1>A</h1><p>a</p>"), Doc("b.xhtml", '<h1>B</h1><p id="x">b</p>')]
|
|
run = convert(Book(docs=docs, nav=[("A", "a.xhtml"), ("B", "b.xhtml")]))
|
|
assert [p.name for p in run.chapters()] == ["001-a.md", "002-b.md"]
|
|
assert run.report["skipped_documents"] == [{"href": "OEBPS/toc.xhtml", "reason": "html-toc"}]
|
|
|
|
|
|
def test_short_title_page_is_kept(convert):
|
|
docs = [Doc("t.xhtml", "<p>Für M.</p>", "Widmung"), Doc("c.xhtml", "<h1>K</h1><p>x</p>")]
|
|
run = convert(Book(docs=docs, nav=[]))
|
|
assert len(run.chapters()) == 2
|
|
assert "Für M." in run.chapter("001")
|
|
|
|
|
|
def test_layout_table_headings_are_recognized(convert):
|
|
body = (
|
|
'<table class="Document_number"><tr><td>3</td><td class="Title_document">Grundlagen</td></tr></table>'
|
|
"<p>Text</p>"
|
|
'<table class="Head1"><tr><td>3.1</td><td>Einführung</td></tr></table><p>mehr</p>'
|
|
)
|
|
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
|
text = run.chapter("001")
|
|
assert "# 3 Grundlagen" in text
|
|
assert "## 3.1 Einführung" in text
|
|
assert "<table" not in text
|
|
|
|
|
|
def test_glossary_anchor_is_placed_before_bold_term(convert):
|
|
"""QA-F2: the anchor of a <dt id> must not end up inside the bold markers."""
|
|
body = ('<h1>K</h1><p><a href="#g">API</a></p><section epub:type="glossary"><h2>Glossar</h2>'
|
|
'<dl><dt id="g">API</dt><dd>Schnittstelle.</dd></dl></section>')
|
|
text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("002")
|
|
assert '<a id="g"></a>**API:** Schnittstelle.' in text
|
|
|
|
|
|
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)]))
|
|
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", "<h1>Teil</h1><h2>A</h2><p>x</p><h2>B</h2><p>y</p>")]))
|
|
assert not any(w["code"] == "chapter-large" for w in small.report["warnings"])
|
|
|
|
|
|
def test_titles_derived_from_content_when_no_other_source(convert):
|
|
docs = [
|
|
Doc("ccover.xhtml", '<div><img src="c.png" alt=""/></div>'),
|
|
Doc("cinner.xhtml", "<p>Erika Musterfrau</p><p>Testbuch</p><p>Roman</p>"),
|
|
Doc("cimp.xhtml", "<p>© 2024 Testverlag</p><p>Das Werk ist urheberrechtlich geschützt.</p>"),
|
|
Doc("cwid.xhtml", "<p>Für Anna, die immer an mich geglaubt hat, und für alle anderen.</p>"),
|
|
Doc("c1.xhtml", "<h1>Eins</h1><p>Text</p>"),
|
|
]
|
|
from conftest import PNG
|
|
run = convert(Book(docs=docs, nav=[("Eins", "c1.xhtml")], media={"c.png": PNG}))
|
|
names = [p.name for p in run.chapters()]
|
|
assert names == ["001-cover.md", "002-titelseite.md", "003-impressum.md",
|
|
"004-fuer-anna-die-immer-an-mich-geglaubt-hat.md", "005-eins.md"]
|
|
types = {c["file"]: c["types"] for c in run.report["chapters"]}
|
|
assert "cover" in types["chapters/001-cover.md"]
|
|
assert "titlepage" in types["chapters/002-titelseite.md"]
|
|
assert "colophon" in types["chapters/003-impressum.md"]
|
|
|
|
|
|
def test_adjacent_field_spans_get_a_separator(convert):
|
|
body = ('<h1>K</h1><div class="ContextInformation"><span class="Names">J. Liesen, V. Mehrmann</span>'
|
|
'<span class="BookTitles"><span class="BookTitle">Lineare Algebra</span></span>'
|
|
'<span class="Series">Springer Studium</span></div>'
|
|
'<p><span class="dropcap">D</span><span class="rest">as Wetter</span> war schlecht.</p>'
|
|
'<p><span class="a">Haupt</span><span class="b">bahnhof</span> und x<sub>i</sub>.</p>')
|
|
text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001")
|
|
assert "J. Liesen, V. Mehrmann Lineare Algebra Springer Studium" in text
|
|
assert "Das Wetter war schlecht." in text
|
|
assert "Hauptbahnhof und x<sub>i</sub>." in text
|
|
|
|
|
|
def test_special_sections_below_chapter_level_get_own_files(convert):
|
|
body = ("<h1>Anhang</h1><p>Vorbemerkung.</p>"
|
|
"<h2><em>Quellen</em></h2><ul><li>Archiv A</li><li>Buch B, ISBN 978-0-201-89683-1</li></ul>"
|
|
"<h2>Danksagung</h2><p>Danke allen.</p>"
|
|
"<h2>Literatur und Film</h2><p>Ein normaler Abschnitt.</p>")
|
|
run = convert(Book(docs=[Doc("a.xhtml", body)]))
|
|
assert [p.name for p in run.chapters()] == ["001-anhang.md", "002-quellen.md", "003-danksagung.md"]
|
|
assert "Literatur und Film" in run.chapter("003") # not a special heading: stays in the chapter
|
|
assert "Archiv A" in run.chapter("002") and "Danke" not in run.chapter("002")
|
|
assert frontmatter(run.chapter("002"))["epub_type"] == ["bibliography"]
|
|
readme = (run.out / "README.md").read_text(encoding="utf-8")
|
|
assert "Literatur / Quellen: [Quellen](chapters/002-quellen.md)" in readme
|
|
|
|
|
|
def test_special_chapter_is_not_split_further(convert):
|
|
body = ("<h1>Literatur</h1><h2>Quellen</h2><p>Q1</p><h2>Sekundärliteratur</h2><p>S1</p>"
|
|
"<h1>Register</h1><h2>A</h2><p>Apfel</p><h2>Index</h2><p>x</p>")
|
|
run = convert(Book(docs=[Doc("a.xhtml", body)]))
|
|
assert [p.name for p in run.chapters()] == ["001-literatur.md", "002-register.md"]
|