feat: Seitenzahlen der Druckausgabe per --page-markers (P5)

- Erkennung: epub:type/role pagebreak (aria-label, title, Text) sowie
  Seitenanker-IDs page_47, page37, seite-12 und p17 (nur leere a/span)
- Ausgabe: visible = \[S. 123\], comment = <!-- S. 123 -->, none (Standard)
- keine Marker in Code; Marker in Überschriften als Absatz davor;
  Layout-Tabellen-Überschriften behalten ihre Seitenmarke
- Bericht: layout.page_markers; interne data-e2m-*-Attribute nie im HTML

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-06 05:10:38 +02:00
commit 16e2d186a4
7 changed files with 110 additions and 9 deletions

View file

@ -243,6 +243,20 @@ Schriftart, Kapitälchen und zentrierte oder rechtsbündige Absätze als HTML er
sie vom normalen Text abweichen. Fremdsprachige Passagen werden als solche gekennzeichnet. GitHub zeigt sie vom normalen Text abweichen. Fremdsprachige Passagen werden als solche gekennzeichnet. GitHub zeigt
diese Angaben nicht an. diese Angaben nicht an.
### Seitenzahlen der Druckausgabe (zum Zitieren)
Viele E-Books enthalten unsichtbare Marken, wo im gedruckten Buch eine neue Seite beginnt. Standardmäßig
lässt das Programm sie weg. Mit `--page-markers visible` erscheinen sie im Text als `[S. 123]`, mit
`--page-markers comment` als unsichtbarer Kommentar `<!-- S. 123 -->`. Den findest du über die Suche, er
stört aber beim Lesen nicht.
```bash
python3 epub2md.py roman.epub roman-md --page-markers visible
```
In Codebeispielen stehen keine Seitenmarken, damit der Code unverändert bleibt. Enthält das E-Book keine
Seitenmarken, bleibt die Option ohne Wirkung; im Bericht steht dann `"page_markers": 0`.
### Buch mit vielen Fußnoten ### Buch mit vielen Fußnoten
Standardmäßig stehen die Fußnoten am Ende des jeweiligen Kapitels. Alternativen: Standardmäßig stehen die Fußnoten am Ende des jeweiligen Kapitels. Alternativen:
@ -477,6 +491,7 @@ python3 epub2md_merge.py --help
| `--keep-invisible-chars` | unsichtbare Zeichen (Nullbreiten-Leerzeichen, weiche Trennstriche) behalten | | `--keep-invisible-chars` | unsichtbare Zeichen (Nullbreiten-Leerzeichen, weiche Trennstriche) behalten |
| `--boxes callout/quote/html/plain` | Darstellung von Infokästen | | `--boxes callout/quote/html/plain` | Darstellung von Infokästen |
| `--styles semantic/html` | Farben, Schriften, Größen, Ausrichtung und Sprache als HTML übernehmen (`html`) | | `--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 |
| `--math auto/tex/mathml/image` | Formeldarstellung | | `--math auto/tex/mathml/image` | Formeldarstellung |
| `--footnotes end-of-chapter/end-of-book/inline/keep-links` | Fußnoten-Platzierung | | `--footnotes end-of-chapter/end-of-book/inline/keep-links` | Fußnoten-Platzierung |

View file

@ -99,6 +99,7 @@ python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json
| `--keep-wrap-markers` | aus | Druck-Umbruchzeichen `↪`/`➥` in Codeblöcken stehen lassen. Standard: Fortsetzungszeilen werden wieder zur ursprünglichen Codezeile zusammengefügt (Anzahl im Bericht: `wrap_lines_joined`) | | `--keep-wrap-markers` | aus | Druck-Umbruchzeichen `↪`/`➥` in Codeblöcken stehen lassen. Standard: Fortsetzungszeilen werden wieder zur ursprünglichen Codezeile zusammengefügt (Anzahl im Bericht: `wrap_lines_joined`) |
| `--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 | | `--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 | | `--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` |
| `--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-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`) | | `--keep-html` | aus | Unbekannte/nicht sicher konvertierbare Elemente als bereinigtes HTML erhalten (sonst: Text übernommen + Eintrag in `diagnostics/unsupported-elements.md`) |
@ -249,6 +250,11 @@ weil CommonMark keine `[^…]`-Syntax kennt.
bleiben sie als HTML-Stil erhalten. Übernommen wird nur, was vom Grundstil abweicht (häufigste Werte aller bleiben sie als HTML-Stil erhalten. Übernommen wird nur, was vom Grundstil abweicht (häufigste Werte aller
Absätze; Schriftgröße erst ab ±15 %). Abgesetzte Formeln und Bildabsätze bekommen keine Ausrichtung. Werte Absätze; Schriftgröße erst ab ±15 %). Abgesetzte Formeln und Bildabsätze bekommen keine Ausrichtung. Werte
mit `url(...)`, `expression` usw. werden verworfen. mit `url(...)`, `expression` usw. werden verworfen.
- **Seitenzahlen (`--page-markers`):** Erkannt werden `epub:type="pagebreak"`/`role="doc-pagebreak"`
(Nummer aus `aria-label`, `title` oder Text) und Seitenanker mit IDs wie `page_47`, `page37`, `seite-12`
oder – nur bei leeren `<a>`/`<span>` – `p17`. In Codeblöcken und Inline-Code stehen keine Marker; ein
Seitenwechsel in einer Überschrift steht als eigener Absatz davor. Die Anzahl steht im Bericht unter
`layout.page_markers`.
- **Infokästen:** - **Infokästen:**
- **Erkennung:** `aside`/`epub:type` (sidebar, tip, warning …), Klassennamen (box, kasten, tipp, hinweis, - **Erkennung:** `aside`/`epub:type` (sidebar, tip, warning …), Klassennamen (box, kasten, tipp, hinweis,
warnung, achtung, wichtig, merke, exkurs …) oder ein CSS-Rahmen bzw. -Hintergrund um kurzen Fließtext. warnung, achtung, wichtig, merke, exkurs …) oder ein CSS-Rahmen bzw. -Hintergrund um kurzen Fließtext.

View file

@ -93,6 +93,9 @@ def build_parser() -> argparse.ArgumentParser:
help="semantic: nur Bedeutung übernehmen (Hintergrundmarkierung → <mark>); html: zusätzlich Farbe, " help="semantic: nur Bedeutung übernehmen (Hintergrundmarkierung → <mark>); html: zusätzlich Farbe, "
"Hintergrund, Schriftgröße/-art, Kapitälchen, Ausrichtung und Fremdsprache als <span style>/" "Hintergrund, Schriftgröße/-art, Kapitälchen, Ausrichtung und Fremdsprache als <span style>/"
"<span lang> (wo sie vom normalen Text abweichen; GitHub zeigt das nicht an) (Standard: semantic)") "<span lang> (wo sie vom normalen Text abweichen; GitHub zeigt das nicht an) (Standard: semantic)")
p.add_argument("--page-markers", choices=["none", "visible", "comment"], default="none",
help="Seitenzahlen der Druckausgabe: visible = '[S. 123]' im Text, comment = '<!-- S. 123 -->' "
"(unsichtbar, durchsuchbar), none = weglassen (Standard: none)")
p.add_argument("--css-emphasis", action=argparse.BooleanOptionalAction, default=True, p.add_argument("--css-emphasis", action=argparse.BooleanOptionalAction, default=True,
help="Fett/kursiv/unterstrichen/hoch-/tiefgestellt auch übernehmen, wenn das Buch sie nur per CSS " help="Fett/kursiv/unterstrichen/hoch-/tiefgestellt auch übernehmen, wenn das Buch sie nur per CSS "
"setzt; per CSS ausgeblendete Elemente weglassen (Standard: an)") "setzt; per CSS ausgeblendete Elemente weglassen (Standard: an)")
@ -137,6 +140,7 @@ def main(argv: list[str] | None = None) -> int:
css_emphasis=args.css_emphasis, css_emphasis=args.css_emphasis,
boxes=args.boxes, boxes=args.boxes,
styles=args.styles, styles=args.styles,
page_markers=args.page_markers,
strict=args.strict, strict=args.strict,
verbose=args.verbose, verbose=args.verbose,
dry_run=args.dry_run, dry_run=args.dry_run,

View file

@ -35,6 +35,7 @@ from .footnotes import NoteIndex
from .mathml import Unsupported, is_display_tex, looks_like_tex, mathml_to_tex, strip_tex_delimiters from .mathml import Unsupported, is_display_tex, looks_like_tex, mathml_to_tex, strip_tex_delimiters
from .media import MediaRegistry from .media import MediaRegistry
from .models import Chapter, Note from .models import Chapter, Note
from .normalize import PAGE_ATTR
from .references import LinkResolver from .references import LinkResolver
from .utils import ( from .utils import (
MATHML_NS, MATHML_NS,
@ -100,6 +101,7 @@ class RenderOptions:
keep_invisible_chars: bool = False # --keep-invisible-chars: keep zero-width chars and soft hyphens keep_invisible_chars: bool = False # --keep-invisible-chars: keep zero-width chars and soft hyphens
boxes: str = "callout" # --boxes {callout,quote,html,plain} boxes: str = "callout" # --boxes {callout,quote,html,plain}
styles: str = "semantic" # --styles {semantic,html} styles: str = "semantic" # --styles {semantic,html}
page_markers: str = "none" # --page-markers {none,visible,comment}: print page numbers
css_emphasis: bool = True # --no-css-emphasis: ignore bold/italic/… that only the stylesheet expresses css_emphasis: bool = True # --no-css-emphasis: ignore bold/italic/… that only the stylesheet expresses
@ -298,6 +300,7 @@ class Renderer:
self.entry_problem = entry_problem or (lambda path: None) self.entry_problem = entry_problem or (lambda path: None)
self.css = stylesheet self.css = stylesheet
self.scene_breaks = 0 self.scene_breaks = 0
self.page_markers = 0
self.poems = 0 self.poems = 0
self.boxes = 0 self.boxes = 0
self.in_box = False self.in_box = False
@ -405,6 +408,15 @@ class Renderer:
def _anchor(self, name: str) -> str: def _anchor(self, name: str) -> str:
return f'<a id="{name}"></a>' return f'<a id="{name}"></a>'
def page_marker(self, el: Any) -> str:
"""Visible '[S. 12]' or '<!-- S. 12 -->' for a print page-break element (--page-markers)."""
page = el.get(PAGE_ATTR)
if not page or self.opt.page_markers == "none":
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)}\\]"
def anchors_of(self, el: Any, subtree: bool = False) -> str: def anchors_of(self, el: Any, subtree: bool = False) -> str:
nodes = iter_elements(el) if subtree else [el] nodes = iter_elements(el) if subtree else [el]
names = [] names = []
@ -729,13 +741,14 @@ class Renderer:
level = heading_level(el) - (ch.shift if ch else 0) level = heading_level(el) - (ch.shift if ch else 0)
level = max(self.min_level, min(6, level)) level = max(self.min_level, min(6, level))
anchors = self.anchors_of(el, subtree=True) anchors = self.anchors_of(el, subtree=True)
markers = "".join(self.page_marker(e) for e in iter_elements(el))
self.in_heading = True self.in_heading = True
try: try:
text = join_inline(self.inline_children(el), hard_break=" ") text = join_inline(self.inline_children(el), hard_break=" ")
finally: finally:
self.in_heading = False self.in_heading = False
text = re.sub(r"<a id=\"[^\"]*\"></a>", "", text).strip() text = re.sub(r"<a id=\"[^\"]*\"></a>", "", text).strip()
out = [] out = [markers] if markers else []
if anchors: if anchors:
out.append(anchors) out.append(anchors)
if not text: if not text:
@ -1154,12 +1167,15 @@ class Renderer:
name = local(el.tag) name = local(el.tag)
anchor = self.anchors_of(el) if name not in ("a",) else "" anchor = self.anchors_of(el) if name not in ("a",) else ""
pre: list[Piece] = [("r", anchor)] if anchor and not self.in_heading else [] pre: list[Piece] = [("r", anchor)] if anchor and not self.in_heading else []
marker = self.page_marker(el) if not self.in_heading else ""
if marker:
pre.insert(0, ("r", marker))
if name == "br": if name == "br":
return pre + ([("t", " ")] if self.in_heading else [("br", "")]) return pre + ([("t", " ")] if self.in_heading else [("br", "")])
if name == "wbr": if name == "wbr":
return pre return pre
if name == "a": if name == "a":
return self.link(el) return pre[:1] + self.link(el) if marker else self.link(el)
if name == "img": if name == "img":
return pre + self.image(el) return pre + self.image(el)
if name == "image": if name == "image":
@ -1585,6 +1601,12 @@ class Renderer:
return out.strip() return out.strip()
def _html(self, el: Any, in_pre: bool, keep: bool = False) -> str: def _html(self, el: Any, in_pre: bool, keep: bool = False) -> str:
marker = self.page_marker(el) if not in_pre else ""
if marker.startswith("\\["):
marker = marker.replace("\\", "") # inside HTML no Markdown escapes
return marker + self._html_core(el, in_pre, keep)
def _html_core(self, el: Any, in_pre: bool, keep: bool = False) -> str:
name = local(el.tag) name = local(el.tag)
if name == "math": if name == "math":
# Markdown math ($…$, \(…\)) is not rendered inside an HTML block; MathML is. # Markdown math ($…$, \(…\)) is not rendered inside an HTML block; MathML is.
@ -1615,7 +1637,7 @@ class Renderer:
if keep and re.fullmatch(r"[a-z][a-z0-9-]*", name): if keep and re.fullmatch(r"[a-z][a-z0-9-]*", name):
safe = [f'{k.rsplit("}", 1)[-1]}="{html.escape(v)}"' for k, v in el.attrib.items() safe = [f'{k.rsplit("}", 1)[-1]}="{html.escape(v)}"' for k, v in el.attrib.items()
if re.fullmatch(r"[A-Za-z_:][-A-Za-z0-9_:.]*", k.rsplit("}", 1)[-1]) if re.fullmatch(r"[A-Za-z_:][-A-Za-z0-9_:.]*", k.rsplit("}", 1)[-1])
and not k.rsplit("}", 1)[-1].lower().startswith("on") and k.rsplit("}", 1)[-1] != "style" and not k.rsplit("}", 1)[-1].lower().startswith(("on", "data-e2m-")) and k.rsplit("}", 1)[-1] != "style"
and not v.strip().lower().startswith("javascript:")] and not v.strip().lower().startswith("javascript:")]
return f"<{name}{(' ' + ' '.join(safe)) if safe else ''}>{content}</{name}>" return f"<{name}{(' ' + ' '.join(safe)) if safe else ''}>{content}</{name}>"
if name not in HTML_KEEP_TAGS or name == "span": if name not in HTML_KEEP_TAGS or name == "span":

View file

@ -15,6 +15,9 @@ HEAD_CLASS_RE = re.compile(r"^(?:head|heading|h|ueberschrift|uberschrift)[_-]?(\
TITLE_CLASS_RE = re.compile(r"^(?:title[_-]?document|chapter[_-]?title|document[_-]?number|chaptertitle)$") TITLE_CLASS_RE = re.compile(r"^(?:title[_-]?document|chapter[_-]?title|document[_-]?number|chaptertitle)$")
TOC_TITLE_RE = re.compile(r"^\s*(inhalt|inhaltsverzeichnis|inhaltsübersicht|contents|table of contents|toc)\s*$", re.I) TOC_TITLE_RE = re.compile(r"^\s*(inhalt|inhaltsverzeichnis|inhaltsübersicht|contents|table of contents|toc)\s*$", re.I)
MEDIA_TAGS = {"img", "svg", "math", "audio", "video", "object", "embed", "iframe", "image"} MEDIA_TAGS = {"img", "svg", "math", "audio", "video", "object", "embed", "iframe", "image"}
PAGE_ATTR = "data-e2m-page" # print page number of a page-break marker (rendered with --page-markers)
PAGE_ID_RE = re.compile(r"^(?:page|pg|seite)[_-]?(\d+|[ivxlcdm]+)$", re.I) # id="page_47", <span id="page37">
PAGE_ID_SHORT_RE = re.compile(r"^p[_-]?(\d+)$", re.I) # id="p17": only on empty <a>/<span>
@dataclass @dataclass
@ -88,9 +91,11 @@ def normalize_doc(doc: Doc, diag: Diagnostics, title_tables: bool) -> None:
continue continue
types = epub_types(el) types = epub_types(el)
if "pagebreak" in types or "doc-pagebreak" in roles(el): if "pagebreak" in types or "doc-pagebreak" in roles(el):
_replace_with_anchor(el) _replace_with_anchor(el, _pagebreak_label(el))
doc.pagebreaks += 1 doc.pagebreaks += 1
continue continue
if name in ("a", "span") and not el.get("href") and (page := _page_from_id(el)):
el.set(PAGE_ATTR, page)
if name == "table": if name == "table":
_maybe_heading_table(el, title_tables) _maybe_heading_table(el, title_tables)
elif name in ("p", "div") and len(classes(el)) == 1 and (m := HEAD_CLASS_RE.match(classes(el)[0])): elif name in ("p", "div") and len(classes(el)) == 1 and (m := HEAD_CLASS_RE.match(classes(el)[0])):
@ -142,13 +147,35 @@ def _attached(el: Any, body: Any) -> bool:
return False return False
def _replace_with_anchor(el: Any) -> None: def _pagebreak_label(el: Any) -> str:
"""Page number of an epub:type="pagebreak" element (aria-label, title, text or id)."""
for value in (el.get("aria-label"), el.get("title"), text_content(el)):
value = norm_ws(value or "")
if value and len(value) <= 12:
return re.sub(r"^(?:seite|page|s\.|p\.)\s*", "", value, flags=re.I)
m = PAGE_ID_RE.match(el.get("id") or "") or PAGE_ID_SHORT_RE.match(el.get("id") or "")
return m.group(1) if m else ""
def _page_from_id(el: Any) -> str:
el_id = el.get("id") or ""
if m := PAGE_ID_RE.match(el_id):
return m.group(1)
if (m := PAGE_ID_SHORT_RE.match(el_id)) and not norm_ws(text_content(el)) and len(el) == 0:
return m.group(1)
return ""
def _replace_with_anchor(el: Any, page: str = "") -> None:
ids = [e.get("id") for e in iter_elements(el) if e.get("id")] ids = [e.get("id") for e in iter_elements(el) if e.get("id")]
if not ids: if not ids and not page:
remove_keep_tail(el) remove_keep_tail(el)
return return
anchor = _make(el, "a") anchor = _make(el, "a")
anchor.set("id", ids[0]) if ids:
anchor.set("id", ids[0])
if page:
anchor.set(PAGE_ATTR, page)
anchor.tail = el.tail anchor.tail = el.tail
el.getparent().replace(el, anchor) el.getparent().replace(el, anchor)
@ -177,6 +204,7 @@ def _maybe_heading_table(table: Any, title_tables: bool) -> None:
level = max(1, min(6, level)) level = max(1, min(6, level))
texts = [] texts = []
ids = [e.get("id") for e in iter_elements(table) if e.get("id")] ids = [e.get("id") for e in iter_elements(table) if e.get("id")]
pages = {e.get("id"): page for e in iter_elements(table) if local(e.tag) in ("a", "span") and (page := _page_from_id(e))}
for cell in cells: for cell in cells:
parts = [] parts = []
for node in cell.iter(): for node in cell.iter():
@ -193,6 +221,8 @@ def _maybe_heading_table(table: Any, title_tables: bool) -> None:
for anchor_id in ids: for anchor_id in ids:
a = _make(table, "a") a = _make(table, "a")
a.set("id", anchor_id) a.set("id", anchor_id)
if anchor_id in pages:
a.set(PAGE_ATTR, pages[anchor_id])
heading.append(a) heading.append(a)
if len(heading): if len(heading):
heading[-1].tail = " ".join(texts) heading[-1].tail = " ".join(texts)

View file

@ -52,6 +52,7 @@ class Options:
css_emphasis: bool = True css_emphasis: bool = True
boxes: str = "callout" boxes: str = "callout"
styles: str = "semantic" styles: str = "semantic"
page_markers: str = "none"
strict: bool = False strict: bool = False
verbose: bool = False verbose: bool = False
dry_run: bool = False dry_run: bool = False
@ -257,7 +258,7 @@ def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver:
extract_media=opts.extract_media, keep_html=opts.keep_html, language=language, extract_media=opts.extract_media, keep_html=opts.keep_html, language=language,
code_language=opts.code_language, keep_wrap_markers=opts.keep_wrap_markers, code_language=opts.code_language, keep_wrap_markers=opts.keep_wrap_markers,
keep_invisible_chars=opts.keep_invisible_chars, css_emphasis=opts.css_emphasis, keep_invisible_chars=opts.keep_invisible_chars, css_emphasis=opts.css_emphasis,
boxes=opts.boxes, styles=opts.styles), boxes=opts.boxes, styles=opts.styles, page_markers=opts.page_markers),
resolver, media, notes, diag, set(reader.names), reader.entry_problem, stylesheet, resolver, media, notes, diag, set(reader.names), reader.entry_problem, stylesheet,
base_style(stylesheet, [p for c in chapters for _, it in c.items if is_element(it) base_style(stylesheet, [p for c in chapters for _, it in c.items if is_element(it)
for p in iter_elements(it) if local(p.tag) == "p"][:20000]) if stylesheet else None, for p in iter_elements(it) if local(p.tag) == "p"][:20000]) if stylesheet else None,
@ -522,7 +523,8 @@ def _report(input_path: Path, sha: str, generated_at: str, package: Any, opts: O
"text_cleanup": {"invisible_chars_removed": renderer.invisible_removed}, "text_cleanup": {"invisible_chars_removed": renderer.invisible_removed},
"css": {"rules": len(renderer.css.rules) if renderer.css else 0, "applied": dict(renderer.css_applied), "css": {"rules": len(renderer.css.rules) if renderer.css else 0, "applied": dict(renderer.css_applied),
"hidden_elements": renderer.hidden_elements}, "hidden_elements": renderer.hidden_elements},
"layout": {"scene_breaks": renderer.scene_breaks, "poems": renderer.poems, "boxes": renderer.boxes}, "layout": {"scene_breaks": renderer.scene_breaks, "poems": renderer.poems, "boxes": renderer.boxes,
"page_markers": renderer.page_markers},
"warnings": warnings, "warnings": warnings,
"errors": errors, "errors": errors,
"infos": [d.as_dict() for d in diag.items if d.level == "info"], "infos": [d.as_dict() for d in diag.items if d.level == "info"],

View file

@ -162,3 +162,25 @@ def test_p3_p7_html_styles(convert):
assert '<div style="text-align: center">\n\nWidmung\n\n</div>' in text assert '<div style="text-align: center">\n\nWidmung\n\n</div>' in text
assert "Zweiter Absatz." in text and "Aldus" not in text # base paragraph style is not repeated assert "Zweiter Absatz." in text and "Aldus" not in text # base paragraph style is not repeated
assert run.report["css"]["applied"]["style"] >= 5 assert run.report["css"]["applied"]["style"] >= 5
PAGES = ('<h1>K</h1><p>Ende Seite 89.</p><div aria-label="90" epub:type="pagebreak" id="page90" role="doc-pagebreak"/>'
'<p>Text<a id="page_12"/> weiter, <span id="page_13">Wort</span> und<a id="p14"/> fort.</p>'
'<p id="p15">Absatz mit Absatz-ID.</p><h2><a id="page_16"/>Kapitelanfang</h2><p>x</p><pre><a id="page_17"/>code</pre>'
'<table class="Head2"><tr><td class="sec_num"><a id="page_18"/>1.2</td><td>Tabellentitel</td></tr></table>')
def test_p5_page_markers(convert):
none = convert(Book(docs=[Doc("c.xhtml", PAGES)])).chapter("001")
assert "S. 90" not in none and "S. 12" not in none
vis = convert(Book(docs=[Doc("c.xhtml", PAGES)]), "--page-markers", "visible")
text = vis.chapter("001")
assert "Ende Seite 89.\n\n\\[S. 90\\]" in text
assert "Text\\[S. 12\\] weiter, \\[S. 13\\]Wort und\\[S. 14\\] fort." in text
assert "S. 15" not in text # paragraph id, not a page
assert "\\[S. 16\\]\n\n## Kapitelanfang" in text # marker before the heading, not inside it
assert "```\ncode\n```" in text # nothing inside code
assert "\\[S. 18\\]\n\n## 1.2 Tabellentitel" in text # layout-table heading keeps the page
assert vis.report["layout"]["page_markers"] == 6
com = convert(Book(docs=[Doc("c.xhtml", PAGES)]), "--page-markers", "comment").chapter("001")
assert "<!-- S. 90 -->" in com and "Text<!-- S. 12 --> weiter" in com