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"
|
return "important"
|
||||||
if re.search(r"tipp|\btip|hint", h):
|
if re.search(r"tipp|\btip|hint", h):
|
||||||
return "tip"
|
return "tip"
|
||||||
|
if re.search(r"hinweis|\bnote|notiz|anmerkung|\binfo", h):
|
||||||
|
return "note"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -80,7 +81,8 @@ def book_uses_title_tables(docs: list[Doc]) -> bool:
|
||||||
return False
|
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
|
body = doc.body
|
||||||
for el in list(iter_elements(body)):
|
for el in list(iter_elements(body)):
|
||||||
if not _attached(el, 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"))
|
doc.toc_ids.update(e.get("id") for e in iter_elements(el) if e.get("id"))
|
||||||
remove_keep_tail(el)
|
remove_keep_tail(el)
|
||||||
diag.info("html-toc-removed", "HTML-Inhaltsverzeichnis entfernt (ersetzt durch README.md).", doc.path)
|
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:
|
if separated:
|
||||||
diag.info("inline-separator-added",
|
diag.info("inline-separator-added",
|
||||||
f"{separated} Leerzeichen zwischen aneinanderstoßenden Feld-Spans eingefügt.", doc.path)
|
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.
|
"""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
|
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
|
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
|
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
|
count = 0
|
||||||
for parent in iter_elements(body):
|
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)
|
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():
|
if len(lt.strip()) < 2 or len(rt.strip()) < 2 or lt[-1].isspace() or rt[0].isspace():
|
||||||
continue
|
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 = " "
|
left.tail = " "
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -16,7 +17,7 @@ from typing import Any
|
||||||
from . import TOOL_NAME, __version__
|
from . import TOOL_NAME, __version__
|
||||||
from .chapters import ChapterBuilder
|
from .chapters import ChapterBuilder
|
||||||
from .diagnostics import Diagnostics
|
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 .epub_reader import EpubReader, decode_markup, parse_xml
|
||||||
from .errors import ContainerError, InputError, StructureError
|
from .errors import ContainerError, InputError, StructureError
|
||||||
from .footnotes import collect_notes
|
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")
|
diag.step(f"EPUB {package.version}: {package.metadata.title!r}, {len(package.spine)} Spine-Dokumente")
|
||||||
docs = _load_docs(reader, package, diag)
|
docs = _load_docs(reader, package, diag)
|
||||||
stylesheet = _load_stylesheet(reader, package, docs, diag) # before normalization drops <style>
|
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)
|
notes, chapters, resolver = _build_structure(docs, package, opts, diag)
|
||||||
rendered = _render(reader, package, chapters, resolver, notes, opts, diag, stylesheet)
|
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,
|
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
|
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."""
|
"""Normalize publisher markup and drop HTML tables of contents that NAV/NCX already cover."""
|
||||||
title_tables = book_uses_title_tables(docs)
|
title_tables = book_uses_title_tables(docs)
|
||||||
for doc in 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}
|
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:
|
for lm in package.landmarks:
|
||||||
if lm.type == "toc" and lm.fragment:
|
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
|
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"
|
plain = convert(Book(docs=[Doc("c.xhtml", body)]), "--boxes", "plain").out / "README.md"
|
||||||
assert "--lua-filter" not in plain.read_text(encoding="utf-8")
|
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