feat: Leerzeichen zwischen aneinanderstoßenden Feld-Spans (QA R4)

Zwei <span>-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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-05 20:47:40 +02:00
commit 5f58589c2e
3 changed files with 45 additions and 2 deletions

View file

@ -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.

View file

@ -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 <span> "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:

View file

@ -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 = ('<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