fix: Kastentyp aus Verlagsklasse, kein Feld-Trenner vor ausgeblendeten Spans
- _box_type erkennt hinweis/note/notiz/anmerkung/info als NOTE; damit
gewinnt die Klasse "box hinweis" gegen ein Typwort im Titel ("Wichtige …")
- _separate_field_spans setzt kein Leerzeichen, wenn einer der Spans per CSS
display:none ist ("<u>x</u> ." → "<u>x</u>.")
- gefunden mit der neuen QA-Fixture 14-typography (QA-Matrix jetzt 85 Fälle,
inkl. epub2md_merge.py und Pandoc-Filter)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f7cceeba25
commit
e8fd6c2fb9
4 changed files with 25 additions and 9 deletions
|
|
@ -221,6 +221,8 @@ def _box_type(hint: str) -> str:
|
|||
return "important"
|
||||
if re.search(r"tipp|\btip|hint", h):
|
||||
return "tip"
|
||||
if re.search(r"hinweis|\bnote|notiz|anmerkung|\binfo", h):
|
||||
return "note"
|
||||
return ""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -80,7 +81,8 @@ def book_uses_title_tables(docs: list[Doc]) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def normalize_doc(doc: Doc, diag: Diagnostics, title_tables: bool) -> None:
|
||||
def normalize_doc(doc: Doc, diag: Diagnostics, title_tables: bool,
|
||||
hidden: Callable[[Any], bool] | None = None) -> None:
|
||||
body = doc.body
|
||||
for el in list(iter_elements(body)):
|
||||
if not _attached(el, body):
|
||||
|
|
@ -107,19 +109,19 @@ 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)
|
||||
separated = _separate_field_spans(body, hidden or (lambda el: False))
|
||||
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:
|
||||
def _separate_field_spans(body: Any, hidden: Callable[[Any], bool]) -> 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.
|
||||
with an uppercase letter or digit. Spans hidden by the book's CSS (display:none) get no separator.
|
||||
"""
|
||||
count = 0
|
||||
for parent in iter_elements(body):
|
||||
|
|
@ -132,7 +134,7 @@ def _separate_field_spans(body: Any) -> int:
|
|||
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():
|
||||
if (rt[0].isupper() or rt[0].isdigit()) and not hidden(left) and not hidden(right):
|
||||
left.tail = " "
|
||||
count += 1
|
||||
return count
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import re
|
|||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
|
@ -16,7 +17,7 @@ from typing import Any
|
|||
from . import TOOL_NAME, __version__
|
||||
from .chapters import ChapterBuilder
|
||||
from .diagnostics import Diagnostics
|
||||
from .css import StyleSheet, base_style
|
||||
from .css import StyleSheet, base_style, emphasis
|
||||
from .epub_reader import EpubReader, decode_markup, parse_xml
|
||||
from .errors import ContainerError, InputError, StructureError
|
||||
from .footnotes import collect_notes
|
||||
|
|
@ -140,7 +141,8 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
|||
diag.step(f"EPUB {package.version}: {package.metadata.title!r}, {len(package.spine)} Spine-Dokumente")
|
||||
docs = _load_docs(reader, package, diag)
|
||||
stylesheet = _load_stylesheet(reader, package, docs, diag) # before normalization drops <style>
|
||||
_prepare_docs(docs, package, diag)
|
||||
hidden = (lambda el: emphasis(stylesheet.computed(el))["hidden"]) if opts.css_emphasis else None
|
||||
_prepare_docs(docs, package, diag, hidden)
|
||||
notes, chapters, resolver = _build_structure(docs, package, opts, diag)
|
||||
rendered = _render(reader, package, chapters, resolver, notes, opts, diag, stylesheet)
|
||||
files, all_chapters, report = _assemble(input_path, output_dir, opts, diag, generated_at, sha, package, docs,
|
||||
|
|
@ -184,11 +186,12 @@ class _Rendered:
|
|||
skipped_formula_images: int
|
||||
|
||||
|
||||
def _prepare_docs(docs: list[Doc], package: Any, diag: Diagnostics) -> None:
|
||||
def _prepare_docs(docs: list[Doc], package: Any, diag: Diagnostics,
|
||||
hidden: Callable[[Any], bool] | None = None) -> None:
|
||||
"""Normalize publisher markup and drop HTML tables of contents that NAV/NCX already cover."""
|
||||
title_tables = book_uses_title_tables(docs)
|
||||
for doc in docs:
|
||||
normalize_doc(doc, diag, title_tables)
|
||||
normalize_doc(doc, diag, title_tables, hidden)
|
||||
landmark_toc_docs = {lm.path for lm in package.landmarks if lm.type == "toc" and lm.path and not lm.fragment}
|
||||
for lm in package.landmarks:
|
||||
if lm.type == "toc" and lm.fragment:
|
||||
|
|
|
|||
|
|
@ -192,3 +192,12 @@ def test_book_readme_suggests_callout_filter(convert):
|
|||
assert "--lua-filter" in readme and "pandoc/callouts.lua" in readme and "1 Infokasten " in readme
|
||||
plain = convert(Book(docs=[Doc("c.xhtml", body)]), "--boxes", "plain").out / "README.md"
|
||||
assert "--lua-filter" not in plain.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_hidden_span_gets_no_field_separator_and_class_type_wins(convert):
|
||||
css = b".u { text-decoration: underline; } .weg { display: none; }"
|
||||
body = ('<h1>K</h1><p><span class="u">unterstrichen</span><span class="weg">VERSTECKT</span>.</p>'
|
||||
'<div class="box hinweis"><h3 class="boxheading">Wichtige Regel</h3><p>Immer testen.</p></div>')
|
||||
text = convert(Book(docs=[Doc("c.xhtml", body)], media={"s.css": css})).chapter("001")
|
||||
assert "<u>unterstrichen</u>." in text and "VERSTECKT" not in text
|
||||
assert "> [!NOTE]\n> **Wichtige Regel**" in text # class 'hinweis' beats the word in the title
|
||||
|
|
|
|||
Loading…
Reference in a new issue