feat: Verzeichnisse unterhalb von --chapter-level als eigene Datei
Ü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>
This commit is contained in:
parent
7835cde1ca
commit
2125555e2a
4 changed files with 77 additions and 5 deletions
|
|
@ -215,6 +215,18 @@ Standardmäßig stehen die Fußnoten am Ende des jeweiligen Kapitels. Alternativ
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Verzeichnisse: Quellen, Literatur, Register, Glossar
|
||||||
|
|
||||||
|
Quellen- und Literaturverzeichnisse, Sach- und Personenregister, Glossare und Anhänge aus dem E-Book werden
|
||||||
|
übernommen. Sie stehen jeweils in einer eigenen Datei und sind in der `README.md` unter „Besondere Abschnitte“
|
||||||
|
verlinkt. Das gilt auch, wenn ein solches Verzeichnis im Buch nur ein Unterabschnitt ist, etwa „Quellen“
|
||||||
|
im Anhang eines Romans. Neu erzeugt wird nichts: Hat ein Buch kein Register, entsteht auch keins.
|
||||||
|
|
||||||
|
- Register: Buchstabengruppen werden zu Überschriften, Untereinträge sind eingerückt, Verweise auf
|
||||||
|
Textstellen bleiben anklickbare Links.
|
||||||
|
- Literatur: Autoren, Titel, DOI, ISBN und Internetadressen bleiben vollständig erhalten; Adressen
|
||||||
|
bleiben Links.
|
||||||
|
|
||||||
## 7. Weiterverarbeiten
|
## 7. Weiterverarbeiten
|
||||||
|
|
||||||
### In Obsidian lesen
|
### In Obsidian lesen
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,13 @@ output/mein-buch/
|
||||||
- erst als letzte Notlösung der Dateiname
|
- erst als letzte Notlösung der Dateiname
|
||||||
5. Ein HTML-Inhaltsverzeichnis wird übersprungen, wenn NAV/NCX vorhanden ist (keine doppelte Ausgabe).
|
5. Ein HTML-Inhaltsverzeichnis wird übersprungen, wenn NAV/NCX vorhanden ist (keine doppelte Ausgabe).
|
||||||
Links darauf zeigen auf `README.md`.
|
Links darauf zeigen auf `README.md`.
|
||||||
6. Überschriftenebenen werden je Datei verschoben: Der Kapiteltitel ist `#`, alle weiteren beginnen bei `##`.
|
6. Verzeichnisse erhalten immer eine eigene Datei, auch unterhalb von `--chapter-level`, z. B. ein
|
||||||
|
`## Quellen` im Anhang. Das gilt nur für Überschriften, die genau einem Verzeichnisnamen entsprechen:
|
||||||
|
Literatur(verzeichnis), Quellen(verzeichnis), Bibliografie, References, Glossar, Stichwortverzeichnis,
|
||||||
|
Sach-/Personen-/Namensregister, Register, Index. Die nächste Überschrift gleicher oder höherer Ebene
|
||||||
|
beendet den Abschnitt. Innerhalb eines Kapitels, das selbst ein Verzeichnis ist, wird nicht weiter
|
||||||
|
geteilt.
|
||||||
|
7. Überschriftenebenen werden je Datei verschoben: Der Kapiteltitel ist `#`, alle weiteren beginnen bei `##`.
|
||||||
|
|
||||||
Verlagsspezifische Heuristiken: Layout-Tabellen mit Klassen wie `Head1`/`Title_document` werden zu
|
Verlagsspezifische Heuristiken: Layout-Tabellen mit Klassen wie `Head1`/`Title_document` werden zu
|
||||||
Überschriften. `<pre>` mit `<br/>`-Zeilen und `div.*programlisting* > span.pre` werden zu Codeblöcken.
|
Überschriften. `<pre>` mit `<br/>`-Zeilen und `div.*programlisting* > span.pre` werden zu Codeblöcken.
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,14 @@ KIND_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
||||||
("prologue", re.compile(r"^(prolog|prologue)", re.I)),
|
("prologue", re.compile(r"^(prolog|prologue)", re.I)),
|
||||||
]
|
]
|
||||||
TITLE_CLASS = re.compile(r"^(?!sub).*(heading|title)$")
|
TITLE_CLASS = re.compile(r"^(?!sub).*(heading|title)$")
|
||||||
|
# Headings below --chapter-level that still get their own file (exact names only, no prefixes)
|
||||||
|
SPECIAL_HEADING = re.compile(
|
||||||
|
r"^(literatur(verzeichnis|hinweise|angaben)?|weiterführende literatur|quellen(verzeichnis|angaben|nachweise?)?|"
|
||||||
|
r"bibliogra(fie|phie)|references|bibliography|glossar|glossary|stichwortverzeichnis|sachregister|"
|
||||||
|
r"sachverzeichnis|register|index|personenregister|namensregister|personenverzeichnis|namensverzeichnis)$",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
SPECIAL_KINDS = {"bibliography", "glossary", "index", "index-persons"}
|
||||||
NUMBER_PREFIX = re.compile(r"^\s*(?:(?:kapitel|chapter)\s+)?(?:\d+(?:\.\d+)*|[IVXLC]+)\.?\s+", re.I)
|
NUMBER_PREFIX = re.compile(r"^\s*(?:(?:kapitel|chapter)\s+)?(?:\d+(?:\.\d+)*|[IVXLC]+)\.?\s+", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,6 +98,8 @@ class ChapterBuilder:
|
||||||
book_title: str = "", authors: list[str] | None = None) -> None:
|
book_title: str = "", authors: list[str] | None = None) -> None:
|
||||||
self.level = chapter_level
|
self.level = chapter_level
|
||||||
self.diag = diag
|
self.diag = diag
|
||||||
|
self._special_level = 0 # level of a split-out special heading whose section is still open
|
||||||
|
self._in_special = False # current chapter itself is a bibliography/index/glossary
|
||||||
self.book_title = book_title
|
self.book_title = book_title
|
||||||
self.author_names = _surnames(authors or [])
|
self.author_names = _surnames(authors or [])
|
||||||
self.nav_titles: dict[str, str] = {}
|
self.nav_titles: dict[str, str] = {}
|
||||||
|
|
@ -103,19 +113,37 @@ class ChapterBuilder:
|
||||||
|
|
||||||
# ------------------------------------------------------------------ split detection
|
# ------------------------------------------------------------------ split detection
|
||||||
|
|
||||||
def _split_kind(self, el: Any) -> Marker | None:
|
def _split_kind(self, el: Any, update: bool = True) -> Marker | None:
|
||||||
|
"""Chapter start at ``el``? ``update=False`` only looks ahead without changing the section state."""
|
||||||
lvl = heading_level(el)
|
lvl = heading_level(el)
|
||||||
if lvl and lvl <= self.level and has_content(el):
|
if lvl and has_content(el):
|
||||||
|
special = _special_heading(el)
|
||||||
|
if lvl <= self.level:
|
||||||
|
if update:
|
||||||
|
self._special_level = 0
|
||||||
|
self._in_special = classify_title(_heading_text(el)) in SPECIAL_KINDS
|
||||||
|
return Marker(el, "heading", lvl)
|
||||||
|
if self._special_level and lvl <= self._special_level:
|
||||||
|
# a heading of the same or higher level ends the split-out special section
|
||||||
|
if update:
|
||||||
|
self._special_level = lvl if special else 0
|
||||||
|
return Marker(el, "heading", lvl)
|
||||||
|
if special and not self._in_special:
|
||||||
|
if update:
|
||||||
|
self._special_level = lvl
|
||||||
return Marker(el, "heading", lvl)
|
return Marker(el, "heading", lvl)
|
||||||
if local(el.tag) in CONTAINERS | {"body"}:
|
if local(el.tag) in CONTAINERS | {"body"}:
|
||||||
types = semantic_types(el)
|
types = semantic_types(el)
|
||||||
if types and local(el.tag) != "body":
|
if types and local(el.tag) != "body":
|
||||||
|
if update:
|
||||||
|
self._special_level = 0
|
||||||
|
self._in_special = bool(SPECIAL_KINDS & set(types))
|
||||||
return Marker(el, "semantic", 0, types)
|
return Marker(el, "semantic", 0, types)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _contains_split(self, el: Any) -> bool:
|
def _contains_split(self, el: Any) -> bool:
|
||||||
for e in iter_elements(el):
|
for e in iter_elements(el):
|
||||||
if e is not el and self._split_kind(e):
|
if e is not el and self._split_kind(e, update=False):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -279,6 +307,11 @@ class _TypeCarrier:
|
||||||
types: list[str]
|
types: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
def _special_heading(el: Any) -> bool:
|
||||||
|
title = NUMBER_PREFIX.sub("", norm_ws(_heading_text(el))).strip(" :.")
|
||||||
|
return bool(SPECIAL_HEADING.match(title))
|
||||||
|
|
||||||
|
|
||||||
def leading_heading_level(item: Any) -> int:
|
def leading_heading_level(item: Any) -> int:
|
||||||
"""Heading level of ``item`` or of the first meaningful descendant inside wrapper containers."""
|
"""Heading level of ``item`` or of the first meaningful descendant inside wrapper containers."""
|
||||||
node = item
|
node = item
|
||||||
|
|
|
||||||
|
|
@ -164,3 +164,24 @@ def test_adjacent_field_spans_get_a_separator(convert):
|
||||||
assert "J. Liesen, V. Mehrmann Lineare Algebra Springer Studium" in text
|
assert "J. Liesen, V. Mehrmann Lineare Algebra Springer Studium" in text
|
||||||
assert "Das Wetter war schlecht." in text
|
assert "Das Wetter war schlecht." in text
|
||||||
assert "Hauptbahnhof und x<sub>i</sub>." 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"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue