diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 6b942ec..63aa171 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -147,6 +147,12 @@ empfiehlt die passende Einstellung. Eine Datei pro **Kapitel** bekommst du so: python3 epub2md.py roman.epub output/roman --chapter-level 2 ``` +### Roman (Szenenwechsel, Gedichte) + +Szenenwechsel, die im Buch nur als Leerzeile zwischen zwei Absätzen erscheinen, werden als `* * *` +übernommen; sonst würden zwei Szenen nahtlos ineinanderlaufen. Gedichte und Liedtexte bleiben Zeile für +Zeile erhalten (als eingerückter Zitatblock, Strophen durch Leerzeile getrennt). + ### Programmierbuch ```bash diff --git a/README.md b/README.md index 92d572f..71be0ab 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,13 @@ weil CommonMark keine `[^…]`-Syntax kennt. CSS-Dateien des Buches aus: Klassen-, Element- und ID-Selektoren, Nachfahren- und Kind-Kombinatoren, Spezifität und `style`-Attribute. `@media`-Blöcke und Selektoren mit Pseudoklassen werden ignoriert. Direkt aneinanderstoßende gleiche Hervorhebungen werden zusammengefasst (`*x**y*` → `*xy*`). +- **Szenenwechsel und Verse:** + - Leere Absätze zwischen zwei Textabsätzen werden zu `* * *`; mehrere hintereinander ergeben einen + Wechsel. Ebenso werden Ornament-Absätze wie `***` oder `⁂` zu `* * *`. + - Leere `div`s und Absätze, die nur Anker enthalten, gelten als Layout und nicht als Wechsel. + - Gedichte (`poem`, `verse`, `gedicht` … bzw. `z3998:poem`) werden zum Zitatblock mit einer Zeile pro Vers + und einer Leerzeile zwischen Strophen. + - Anzahl im Bericht unter `layout`. - **Gescannte Texte:** nur Bilder, kein Text (keine OCR). - **DRM:** wird erkannt und abgelehnt. - **Register:** Buchstabengruppen werden zu `##`-Überschriften, Untereinträge erhalten `– `. Interne diff --git a/epub2md/html_converter.py b/epub2md/html_converter.py index a053fb8..d397e10 100644 --- a/epub2md/html_converter.py +++ b/epub2md/html_converter.py @@ -195,6 +195,8 @@ def _collapse_blank_lines(text: str) -> str: return "\n".join(out) +ORNAMENT = re.compile(r"(?:[*∗⁂❖✻✱✳◆◇•·~][ \t]*){1,9}") +POEM_CLASS = re.compile(r"(?:^|[-_])(poem|poetry|verse|vers|lyrics|stanza|gedicht|lied|song)(?:$|[-_\d])") ITALIC_TAGS = {"em", "i", "cite", "dfn", "var"} BOLD_TAGS = {"strong", "b"} CSS_SKIP_TAGS = {"br", "wbr", "img", "svg", "image", "math", "code", "samp", "tt", "kbd", "pre"} @@ -263,6 +265,8 @@ class Renderer: stylesheet: StyleSheet | None = None) -> None: self.entry_problem = entry_problem or (lambda path: None) self.css = stylesheet + self.scene_breaks = 0 + self.poems = 0 self.css_applied: Counter[str] = Counter() self.hidden_elements = 0 self.invisible_removed = 0 @@ -398,6 +402,13 @@ class Renderer: name = local(el.tag) if self._hidden(el): return [] + if name in ("p", "div") and not self.in_table: + if self._is_ornament(el) or self._is_scene_gap(el): + self.scene_breaks += 1 + anchor = self.anchors_of(el, subtree=True) + return ([anchor] if anchor else []) + ["* * *"] + if name in ("div", "section", "blockquote") and self._is_poem(el): + return self.poem(el) ch = self.ch if ch is not None and el is ch.title_el and not heading_level(el): return self.heading(el, forced_level=1) @@ -432,6 +443,85 @@ class Renderer: return [self.clean_html(el, keep_root=True)] return self._flow_with_anchor(el) + # ------------------------------------------------------------------ scene breaks and verse (P4) + + @staticmethod + def _is_empty(el: Any) -> bool: + return not text_content(el).replace("\xa0", " ").strip() and \ + not any(local(e.tag) in ("img", "svg", "math", "image", "hr") for e in iter_elements(el)) + + def _is_blank_p(self, el: Any) -> bool: + """
with nothing but (non-breaking) spaces or
. Empty
Erster Absatz.
' + '
Neue Szene.
Weiter.
***
' + 'Nach Ornament.
Text
⁂
Ende.
') + text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001") + body_md = text.split("# K", 1)[1] + assert "Erster Absatz.\n\n* * *\n\nNeue Szene." in body_md + assert body_md.count("* * *") == 3 # two empties collapse into one; ***, ⁂ are ornaments; none before the heading + assert "Weiter.\n\n* * *\n\nNach Ornament." in body_md + assert "\\*" not in body_md + assert body_md.strip().startswith("Erster Absatz.") # no break at chapter start + + +def test_p4_poems_keep_lines_and_stanzas(convert): + body = ('Davor.
»Erste Zeile
zweite Zeile
' + '
Zweite Strophe«
Danach.
') + text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001") + assert "> »Erste Zeile\\\n> zweite Zeile\n>\n> Zweite Strophe«" in text + assert "* * *" not in text