fix: Seitenmarken aus Kastentiteln vor den Kasten setzen; <mark> dokumentiert

- Marken im Kastentitel landen über marker_sink vor dem Kasten (wie bei
  Überschriften) statt im fetten Titel ("**\[S. 62\]Wozu …**")
- Doku: --no-css-emphasis lässt <mark> bestehen, das steuert --styles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-06 05:41:55 +02:00
commit 6e3d05fef1
5 changed files with 41 additions and 9 deletions

View file

@ -510,7 +510,7 @@ python3 epub2md_merge.py --help
| `--boxes callout/quote/html/plain` | Darstellung von Infokästen |
| `--styles semantic/html` | Farben, Schriften, Größen, Ausrichtung und Sprache als HTML übernehmen (`html`) |
| `--page-markers none/visible/comment` | Seitenzahlen der Druckausgabe als `[S. 123]` oder `<!-- S. 123 -->` |
| `--no-css-emphasis` | Hervorhebungen nur aus HTML-Tags übernehmen, nicht aus dem Stylesheet des Buches |
| `--no-css-emphasis` | Hervorhebungen nur aus HTML-Tags übernehmen, nicht aus dem Stylesheet des Buches (farbige Markierungen `<mark>` bleiben, sie gehören zu `--styles`) |
| `--math auto/tex/mathml/image` | Formeldarstellung |
| `--footnotes end-of-chapter/end-of-book/inline/keep-links` | Fußnoten-Platzierung |
| `--format gfm/commonmark` | Markdown-Variante; `commonmark` für sehr strikte Programme |

View file

@ -3,6 +3,17 @@
Alle nennenswerten Änderungen an epub2md. Format nach [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
Versionsnummern nach [Semantic Versioning](https://semver.org/lang/de/).
## [Unveröffentlicht]
### Geändert
- Seitenmarken (`--page-markers`) aus einem Kastentitel stehen jetzt vor dem Kasten statt im fetten Titel,
wie bei Überschriften.
### Dokumentation
- `--no-css-emphasis` lässt `<mark>`-Markierungen bestehen; diese steuert `--styles`.
## [0.2.0] – 2026-10-06
Schwerpunkt: Typografie und Gestaltung des Buches im Markdown abbilden.
@ -49,5 +60,6 @@ Erste Fassung.
- Alle Befunde der QA-Prüfungen (R1–R7, QA2-01 bis QA2-19) behoben oder abgemildert.
- README und Bedienungsanleitung.
[Unveröffentlicht]: https://kitux.de/forgejo/dschlueter/epub2md/compare/v0.2.0...main
[0.2.0]: https://kitux.de/forgejo/dschlueter/epub2md/compare/v0.1.0...v0.2.0
[0.1.0]: https://kitux.de/forgejo/dschlueter/epub2md/src/tag/v0.1.0

View file

@ -100,7 +100,7 @@ python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json
| `--boxes {callout,quote,html,plain}` | `callout` | Infokästen (Tipp, Hinweis, Warnung, Exkurs, Merke …): `callout` = `> [!TIP]` mit fettem Titel (GitHub-Alerts, Obsidian-Callouts; anderswo ein Zitatblock), `quote` = Zitatblock mit fettem Titel, `html` = `<div class="box box-tip">`, `plain` = normaler Text wie bisher |
| `--styles {semantic,html}` | `semantic` | `semantic`: nur bedeutungstragende Gestaltung (Hintergrundmarkierung → `<mark>`). `html`: zusätzlich Farbe, Hintergrund, Schriftgröße, Schriftart, Kapitälchen, Ausrichtung als `<span style>`/`<div style>` und fremdsprachige Passagen als `<span lang>`, jeweils nur, wo sie vom normalen Text des Buches abweichen. Obsidian, VS Code und Pandoc (HTML/PDF/EPUB) zeigen das an, GitHub nicht |
| `--page-markers {none,visible,comment}` | `none` | Seitenzahlen der Druckausgabe an der Stelle des Seitenwechsels: `visible` = `\[S. 123\]` im Text (zum Zitieren), `comment` = `<!-- S. 123 -->` (unsichtbar, aber durchsuchbar), `none` = weglassen |
| `--css-emphasis` / `--no-css-emphasis` | an | Fett, kursiv, unterstrichen, durchgestrichen, hoch-/tiefgestellt auch übernehmen, wenn das Buch sie nur per CSS-Klasse setzt (z. B. `span.italic`). Einzelwörter in Monospace-Schrift werden zu Inline-Code; per CSS ausgeblendete Elemente (`display:none`) entfallen. Statistik im Bericht unter `css` |
| `--css-emphasis` / `--no-css-emphasis` | an | Fett, kursiv, unterstrichen, durchgestrichen, hoch-/tiefgestellt auch übernehmen, wenn das Buch sie nur per CSS-Klasse setzt (z. B. `span.italic`). Einzelwörter in Monospace-Schrift werden zu Inline-Code; per CSS ausgeblendete Elemente (`display:none`) entfallen. Statistik im Bericht unter `css`. Hintergrundmarkierungen (`<mark>`) und Stile steuert unabhängig davon `--styles` |
| `--keep-invisible-chars` | aus | Nullbreiten-Zeichen und weiche Trennstriche im Fließtext behalten. Standard: entfernen, weil sie Suche und RAG stören (Anzahl im Bericht: `text_cleanup`). Code bleibt immer unverändert |
| `--keep-html` | aus | Unbekannte/nicht sicher konvertierbare Elemente als bereinigtes HTML erhalten (sonst: Text übernommen + Eintrag in `diagnostics/unsupported-elements.md`) |
| `--strict` | aus | Abbruch bei kritischen Fehlern (fehlende/unlesbare Spine-Dokumente, unsichere ZIP-Pfade, ungültige Spine-Verweise) |

View file

@ -303,6 +303,7 @@ class Renderer:
self.css = stylesheet
self.scene_breaks = 0
self.page_markers = 0
self.marker_sink: list[str] | None = None # collects page markers instead of emitting them inline
self.poems = 0
self.boxes = 0
self.in_box = False
@ -417,7 +418,11 @@ class Renderer:
return ""
self.page_markers += 1
page = html.escape(page, quote=False).replace("--", "")
return f"<!-- S. {page} -->" if self.opt.page_markers == "comment" else f"\\[S. {md_escape(page)}\\]"
marker = f"<!-- S. {page} -->" if self.opt.page_markers == "comment" else f"\\[S. {md_escape(page)}\\]"
if self.marker_sink is not None:
self.marker_sink.append(marker)
return ""
return marker
def anchors_of(self, el: Any, subtree: bool = False) -> str:
nodes = iter_elements(el) if subtree else [el]
@ -622,13 +627,18 @@ class Renderer:
break
title_el = self._box_title(members)
title = ""
markers: list[str] = [] # page breaks in the title go before the box, like for headings
if title_el is not None:
pieces: list[Piece] = [("t", self._text(title_el.text))] if title_el.text else []
for child in title_el:
if is_element(child) and not self._title_symbol(child):
pieces.extend(self.inline(child))
if child.tail:
pieces.append(("t", self._text(child.tail)))
self.marker_sink = markers
try:
for child in title_el:
if is_element(child) and not self._title_symbol(child):
pieces.extend(self.inline(child))
if child.tail:
pieces.append(("t", self._text(child.tail)))
finally:
self.marker_sink = None
title = join_inline(pieces, hard_break=" ").strip()
kind = kind or _box_type(norm_ws(text_content(title_el)))
saved = self.in_box
@ -653,7 +663,9 @@ class Renderer:
if title and norm_ws(re.sub(r"[*_\\:]", "", title)).lower() in TYPE_WORDS:
title = "" # 'Tipp' as title of a TIP box adds nothing
self.boxes += 1
out = [anchor] if anchor else []
out = ["".join(markers)] if markers else []
if anchor:
out.append(anchor)
head = [f"**{title}**"] if title else []
if self.opt.boxes == "html":
out.append(f'<div class="box box-{kind}">\n\n' + "\n\n".join(head + blocks) + "\n\n</div>")

View file

@ -201,3 +201,11 @@ def test_hidden_span_gets_no_field_separator_and_class_type_wins(convert):
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
def test_page_marker_in_box_title_goes_before_the_box(convert):
body = ('<h1>K</h1><div class="box"><h3 class="boxheading"><a id="page_62"/>Wozu Deklarationen?</h3>'
'<p>Weil es <a id="page_63"/>hilft.</p></div>')
text = convert(Book(docs=[Doc("c.xhtml", body)]), "--page-markers", "visible").chapter("001")
assert "\\[S. 62\\]\n\n> [!NOTE]\n> **Wozu Deklarationen?**" in text
assert "> Weil es \\[S. 63\\]hilft." in text # markers in the box body stay in place