"""Erzeugt die QA-Fixtures 01–14 (synthetisch, DRM-frei) unter .qa/generated-fixtures/.""" from __future__ import annotations import sys import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tests")) from conftest import PNG, SVG, Book, Doc, build_epub # noqa: E402 OUT = ROOT / ".qa" / "generated-fixtures" M = 'xmlns="http://www.w3.org/1998/Math/MathML"' def rewrite(path: Path, changes: dict[str, bytes | None], extra: dict[str, bytes] | None = None) -> None: """Replace/delete entries of an existing ZIP and add raw entries (names unchanged, even unsafe ones).""" with zipfile.ZipFile(path) as z: entries = [(i, z.read(i)) for i in z.infolist()] with zipfile.ZipFile(path, "w") as z: for info, data in entries: if info.filename in changes: if changes[info.filename] is None: continue data = changes[info.filename] z.writestr(info, data) for name, data in (extra or {}).items(): z.writestr(zipfile.ZipInfo(name), data) def f01() -> Book: return Book(version="2.0", ncx=True, title="Minimal EPUB 2", authors=["Ada Lovelace"], language="en", identifier="urn:uuid:11111111-2222-3333-4444-555555555555", docs=[Doc("chapter1.xhtml", "

Chapter One

Hello EPUB 2 world.

", "Chapter One")]) def f02() -> Book: return Book(title="Minimal EPUB 3", authors=["Grace Hopper"], docs=[Doc("c1.xhtml", "

Erstes Kapitel

Eins.

", "Erstes Kapitel"), Doc("c2.xhtml", "

Zweites Kapitel

Zwei.

", "Zweites Kapitel")]) def f03() -> Book: a = ('

Kapitel A

Zu Kapitel B, zum Anker unten, ' 'zum Ziel in B und ins Nichts, ' 'sowie fehlender Anker.

' '

A.1 Abschnitt

Text A.1

' '

Kapitel A2 in derselben Datei

Unten in A2.

' '

A2.1 Unterabschnitt

Mehr.

') b = ('

Kapitel B

B-Text.

B.1

Ziel in B.

' '

zurück zu A.1

') c = '

Kapitel C

Ende mit Link auf A.

' return Book(title="Kapitel und Links", docs=[Doc("a.xhtml", a, "A"), Doc("b.xhtml", b, "B"), Doc("c.xhtml", c, "C")]) PY = """class Stack: def __init__(self): self.items = [] def push(self, x): \tself.items.append(x) # Tab-Einrückung if len(self.items) > 10 and x < 0: raise ValueError("& zu voll") """ def f04() -> Book: body = ( "

Code und Tabellen

" "

Inline: len(x) < 3 && y, mit Backtick a`b und *nicht kursiv*.

" f'
{PY}
' "
$ find . -name '*.py' | xargs grep -n \"TODO\" \\\n    | sort -u > todo.txt\n$ echo $HOME
" "
Markdown mit ```fences``` und `ticks`\n    eingerückt
" '
{\n  "a": [1, 2],\n  "b": "<tag>"\n}
' "
#include <stdio.h>\nint main(void) { return 0; }
" "" "
  1. drei
  2. vier
" '

Unbekanntes Element: Widget-Inhalt.

' "" "" "
BefehlBedeutung
ls | wc -lDateien zählen
a && bund
" ) return Book(title="Code und Tabellen", docs=[Doc("code.xhtml", body, "Code")]) def f05() -> Book: body = ( "

Bilder

" '
Ein Foto' "
Abbildung 1.1: Ein Foto mit Unterschrift.
" '

Nochmals dasselbe Foto: Wiederholung

' '
Abbildung 1.2: Ein SVG-Diagramm.
' '

Inline-SVG: Kreis' '

' '

Siehe Abbildung 1.1 und SVG direkt.

' '

Fehlendes Bild: weg

' ) return Book(title="Bilder und SVG", docs=[Doc("bilder.xhtml", body, "Bilder")], media={"images/foto 1 ä.png": PNG, "images/diagramm.svg": SVG, "images/unbenutzt.png": PNG}) def f06() -> Book: body = ( "

Formeln

" f"

Bruch inline: ab.

" f"

Potenz: x2, Index: ai, " f"beides: xi2.

" f'
eiπ' '+1=0(2.1)
' '

Nach Gleichung (2.1) folgt …

' f'

Nicht unterstützt: z.

' f'

Mit TeX-Annotation: y\\vec{{y}}' ".

" f'

alttext: 2.

' '

SVG-Formel: Formel E=mc²

' '

Bildformel: Summe über i

' '
$$\\int_0^1 x\\,dx = \\frac{1}{2}$$ (2.2)
' ) return Book(title="MathML und Formeln", docs=[Doc("math.xhtml", body, "Formeln")], media={"f/formel.svg": SVG, "f/formel.png": PNG, "f/tex.png": PNG}) def f07() -> Book: ch1 = ( "

Kapitel mit Fußnoten

" '

Aussage A1 und B' '2, nochmal A1.

' '

Endnote3 und kaputt9.

' '' f'" ) notes = ( '

Anmerkungen

' '
' ) return Book(title="Fußnoten und Endnoten", docs=[Doc("ch1.xhtml", ch1, "Kapitel"), Doc("notes.xhtml", notes, "Anmerkungen")]) def f08() -> Book: ch = ('

Hauptteil

Der Begriff API und ' 'Rekursion nach Knuth [Knu97].

') glossary = ('

Glossar

' '
API
Application Programming Interface, eine Schnittstelle.
' "
Rekursion

Selbstaufruf einer Funktion.

Siehe auch Iteration.

") lit = ('

Literatur

    ' '
  1. Knuth, D. E.: The Art of Computer Programming, Bd. 1, 3. Aufl., Addison-Wesley, 1997, ' "ISBN 978-0-201-89683-1, S. 1–650.
  2. " '
  3. Doe, J.: Ein Artikel. Journal 12 (2020), DOI 10.1000/xyz123.
  4. ' '
  5. Webquelle: https://example.org/seite?a=1&b=2 ' "(Zugriff: 01.02.2024).
") sach = ('

Sachregister

' '

A

API Glossar

' '

R

Rekursion 17, 42

' '

endständige, 45

') pers = ('

Personenregister

K

' '

Knuth, Donald E. 17

') return Book(title="Glossar, Literatur, Register", docs=[ Doc("main.xhtml", ch, "Hauptteil"), Doc("glossar.xhtml", glossary, "Glossar"), Doc("lit.xhtml", lit, "Literatur"), Doc("sach.xhtml", sach, "Sachregister"), Doc("pers.xhtml", pers, "Personenregister")]) def f09() -> Book: body = ( "

Komplexe Tabelle

" "" '' "" '
Tabelle 9.1: Messwerte
ProbeMessung
vorhernachher
A

1,5

korrigiert

2,0
Bausgefallen
siehe Text
' ) return Book(title="Komplexe Tabelle", docs=[Doc("t.xhtml", body, "Tabelle")]) def f10() -> Book: body = ('

Böse Pfade

raus ' 'escape abs

') return Book(title="Malicious", docs=[Doc("ok.xhtml", body, "OK")], extra_entries={"../../outside.txt": b"evil", "../escape.xhtml": b"", "/absolute/path.txt": b"evil", "OEBPS/../../../tmp/qa-epub2md-escape.txt": b"evil"}) def f12() -> Book: text = ("Ärger über Öl und Straße – „deutsche Anführungszeichen“, ‚einfach‘ und «guillemets» — " "café, crème brûlée, naïve, Noël; Ελληνικά: αβγ ΔΣΩ; Mathe: ∀x∈ℝ: x² ≥ 0, ∑ᵢ aᵢ ≈ π, ∞ ≠ ∅ … ") body = f'

Größe & Übermaß

{text}

Rücksprung

' return Book(title="Kodierung – Ümläute", authors=["Zoë Ørsted"], docs=[Doc("kapitel-ü.xhtml", body, "Größe"), Doc("café.xhtml", "

Café crème

Ça va.

", "Café")]) TYPO_CSS = b""" p { font-family: Georgia, serif; } .kursiv { font-style: italic; } .fett { font-weight: bold; } span.u { text-decoration: underline; } .weg { display: none; } .markiert { background-color: #ffff00; } .rot { color: #cc0000; } .gross { font-size: 1.6em; } .kap { font-variant: small-caps; } p.mitte { text-align: center; } @media print { .kursiv { font-weight: bold; } } """ def f14() -> Book: """Typografie: CSS-Hervorhebungen, unsichtbare Zeichen, Szenenwechsel, Gedicht, Kästen, Stile, Seitenmarken.""" c1 = ('

Gestaltung

' '

Ein kursives und fettes Wort, ' 'unterstrichenVERSTECKT.

' '

Null\u200bbreite und wei\u00adches Trenn\u00adzeichen.

' '

Szene eins endet hier.

Szene zwei beginnt hier.

' '

Zeile eins

Zeile zwei

' '

Ein markierter, roter und ' 'großer Text in Kapitälchen, ' 'an English phrase.

' '

Zentrierte Widmung

' '

Seite elf endet und Seite zwölf ' 'geht weiter bis dreizehn.

' '
x = 1\u200b  # bleibt
') c2 = ('

Kästen

' '

[»]Wichtige Regel

' '

Immer testen.

' '

Nachher.

') return Book(title="Typografie", authors=["Tina Typo"], media={"style.css": TYPO_CSS}, docs=[Doc("c1.xhtml", c1, "Gestaltung"), Doc("c2.xhtml", c2, "Kästen")]) def main() -> None: OUT.mkdir(parents=True, exist_ok=True) for name, book in [ ("01-minimal-epub2.epub", f01()), ("02-minimal-epub3-nav.epub", f02()), ("03-chapters-and-links.epub", f03()), ("04-code-and-tables.epub", f04()), ("05-images-and-svg.epub", f05()), ("06-mathml-and-equations.epub", f06()), ("07-footnotes-and-endnotes.epub", f07()), ("08-glossary-bibliography-index.epub", f08()), ("09-complex-table.epub", f09()), ("10-malicious-paths.epub", f10()), ("12-encoding-edge-cases.epub", f12()), ("14-typography.epub", f14()), ]: build_epub(OUT / name, book) # 10: additionally a manifest item that points outside the EPUB root p10 = OUT / "10-malicious-paths.epub" with zipfile.ZipFile(p10) as z: opf = z.read("OEBPS/content.opf").decode() opf = opf.replace("", '') rewrite(p10, {"OEBPS/content.opf": opf.encode()}) # 11: broken container variants base = Book(title="Defekt", docs=[Doc("a.xhtml", "

A

x

", "A")]) p11 = build_epub(OUT / "11-broken-container.epub", base) rewrite(p11, {"META-INF/container.xml": ( b'' b'' b"")}) p11b = build_epub(OUT / "11b-broken-spine.epub", Book(title="Spine", docs=[Doc("a.xhtml", "

A

x

", "A")], spine_extra='')) assert p11b.exists() p11c = build_epub(OUT / "11c-broken-opf-xml.epub", base) rewrite(p11c, {"OEBPS/content.opf": b"kaputt"}) p11d = build_epub(OUT / "11d-missing-spine-file.epub", base) rewrite(p11d, {"OEBPS/a.xhtml": None}) (OUT / "11e-not-a-zip.epub").write_bytes(b"PK\x03\x04 kein echtes zip") p11f = build_epub(OUT / "11f-missing-container.epub", base) rewrite(p11f, {"META-INF/container.xml": None}) p11g = build_epub(OUT / "11g-broken-xhtml.epub", base) rewrite(p11g, {"OEBPS/a.xhtml": b"

Kaputt

nicht geschlossen   &unbekannt; fett"}) print("\n".join(sorted(p.name for p in OUT.glob("*.epub")))) if __name__ == "__main__": main()