From 5f58589c2e14b0289096cecf2e1b884c37643982 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 5 Oct 2026 20:47:40 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Leerzeichen=20zwischen=20aneinandersto?= =?UTF-8?q?=C3=9Fenden=20Feld-Spans=20(QA=20R4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei -Geschwister mit unterschiedlichen Klassen, ohne Text dazwischen und mit Großbuchstabe oder Ziffer rechts bekommen ein trennendes Leerzeichen. Initialen (D|as) und Wortteile (Haupt|bahnhof) bleiben unberührt. Im Mathebuch werden so 49 Stellen korrigiert, die anderen drei Beispielbücher bleiben unverändert. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++++-- epub2md/normalize.py | 29 +++++++++++++++++++++++++++++ tests/test_chapters.py | 12 ++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fa95893..0e8650e 100644 --- a/README.md +++ b/README.md @@ -343,8 +343,10 @@ Verlustfreiheit behauptet (`lossless: false`). - Titel überschriftsloser Seiten werden aus dem Inhalt abgeleitet (Cover, Titelseite, Impressum, erste Wörter). Das ist eine Heuristik und trifft nicht jede Seite. -- Aneinanderstoßende Inline-Elemente, zwischen denen auch in der Quelle kein Leerraum steht, laufen - zusammen (z. B. Autor- und Titelzeilen in Springer-Kapitelköpfen). +- Aneinanderstoßende Feld-Spans ohne Leerraum (z. B. Autor | Titel | Reihe in Springer-Kapitelköpfen) + erhalten ein trennendes Leerzeichen. Das gilt nur bei unterschiedlichen Klassen und wenn rechts ein + Großbuchstabe oder eine Ziffer folgt. Andere direkt aneinanderstoßende Inline-Elemente bleiben + unverändert, damit Initialen und Wortteile nicht zerrissen werden. - Fehlerhafte Einrückungen der Vorlage werden zeichengetreu übernommen. Druck-Fortsetzungszeichen (`↪`/`➥`) am Zeilenanfang werden dagegen standardmäßig aufgelöst (siehe `--keep-wrap-markers`). - Formelbilder ohne TeX-Alternativtext bleiben Bilder. diff --git a/epub2md/normalize.py b/epub2md/normalize.py index 886c9b0..6152766 100644 --- a/epub2md/normalize.py +++ b/epub2md/normalize.py @@ -102,6 +102,35 @@ def normalize_doc(doc: Doc, diag: Diagnostics, title_tables: bool) -> None: doc.toc_ids.update(e.get("id") for e in iter_elements(el) if e.get("id")) remove_keep_tail(el) diag.info("html-toc-removed", "HTML-Inhaltsverzeichnis entfernt (ersetzt durch README.md).", doc.path) + separated = _separate_field_spans(body) + if separated: + diag.info("inline-separator-added", + f"{separated} Leerzeichen zwischen aneinanderstoßenden Feld-Spans eingefügt.", doc.path) + + +def _separate_field_spans(body: Any) -> int: + """Insert a space between adjacent class-styled "fields" that touch without whitespace. + + Publishers sometimes lay out metadata as sibling spans (author | title | series) separated only by + CSS. Narrow rule to keep drop caps ("D|as") and split words ("Haupt|bahnhof") intact: both spans + carry different classes, no text between them, both have >= 2 characters and the right one starts + with an uppercase letter or digit. + """ + count = 0 + for parent in iter_elements(body): + children = [c for c in parent if is_element(c)] + for left, right in zip(children, children[1:]): + if local(left.tag) != "span" or local(right.tag) != "span" or left.getnext() is not right: + continue + if left.tail or not classes(left) or not classes(right) or classes(left) == classes(right): + continue + lt, rt = text_content(left), text_content(right) + if len(lt.strip()) < 2 or len(rt.strip()) < 2 or lt[-1].isspace() or rt[0].isspace(): + continue + if rt[0].isupper() or rt[0].isdigit(): + left.tail = " " + count += 1 + return count def _attached(el: Any, body: Any) -> bool: diff --git a/tests/test_chapters.py b/tests/test_chapters.py index f123df0..7c1712b 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -152,3 +152,15 @@ def test_titles_derived_from_content_when_no_other_source(convert): 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 = ('

K

J. Liesen, V. Mehrmann' + 'Lineare Algebra' + 'Springer Studium
' + '

Das Wetter war schlecht.

' + '

Hauptbahnhof und xi.

') + 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 xi." in text