266 lines
16 KiB
Python
266 lines
16 KiB
Python
|
|
"""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", "<h1>Chapter One</h1><p>Hello <em>EPUB 2</em> world.</p>", "Chapter One")])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def f02() -> Book:
|
|||
|
|
return Book(title="Minimal EPUB 3", authors=["Grace Hopper"],
|
|||
|
|
docs=[Doc("c1.xhtml", "<h1>Erstes Kapitel</h1><p>Eins.</p>", "Erstes Kapitel"),
|
|||
|
|
Doc("c2.xhtml", "<h1>Zweites Kapitel</h1><p>Zwei.</p>", "Zweites Kapitel")])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def f03() -> Book:
|
|||
|
|
a = ('<h1 id="a-start">Kapitel A</h1><p>Zu <a href="b.xhtml">Kapitel B</a>, zum <a href="#a-unten">Anker unten</a>, '
|
|||
|
|
'zum <a href="b.xhtml#b-ziel">Ziel in B</a> und ins <a href="nirgendwo.xhtml#weg">Nichts</a>, '
|
|||
|
|
'sowie <a href="b.xhtml#gibt-es-nicht">fehlender Anker</a>.</p>'
|
|||
|
|
'<h2 id="a-sec">A.1 Abschnitt</h2><p>Text A.1</p>'
|
|||
|
|
'<h1 id="a2">Kapitel A2 in derselben Datei</h1><p id="a-unten">Unten in A2.</p>'
|
|||
|
|
'<h2>A2.1 Unterabschnitt</h2><p>Mehr.</p>')
|
|||
|
|
b = ('<h1>Kapitel B</h1><p>B-Text.</p><h2 id="b-sec">B.1</h2><p id="b-ziel">Ziel in B.</p>'
|
|||
|
|
'<p><a href="a.xhtml#a-sec">zurück zu A.1</a></p>')
|
|||
|
|
c = '<h1>Kapitel C</h1><p>Ende mit <a href="a.xhtml">Link auf A</a>.</p>'
|
|||
|
|
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 = (
|
|||
|
|
"<h1>Code und Tabellen</h1>"
|
|||
|
|
"<p>Inline: <code>len(x) < 3 && y</code>, mit Backtick <code>a`b</code> und <code>*nicht kursiv*</code>.</p>"
|
|||
|
|
f'<pre><code class="language-python">{PY}</code></pre>'
|
|||
|
|
"<pre class=\"shell\">$ find . -name '*.py' | xargs grep -n \"TODO\" \\\n | sort -u > todo.txt\n$ echo $HOME</pre>"
|
|||
|
|
"<pre><code>Markdown mit ```fences``` und `ticks`\n eingerückt</code></pre>"
|
|||
|
|
'<pre data-language="json">{\n "a": [1, 2],\n "b": "<tag>"\n}</pre>'
|
|||
|
|
"<pre>#include <stdio.h>\nint main(void) { return 0; }</pre>"
|
|||
|
|
"<ul><li>Punkt 1<ul><li>Punkt 1.1<ol><li>Schritt a</li><li>Schritt b</li></ol></li></ul></li>"
|
|||
|
|
"<li>Punkt 2</li></ul>"
|
|||
|
|
"<ol start=\"3\"><li>drei</li><li>vier<ul><li>vier.a</li></ul></li></ol>"
|
|||
|
|
'<p>Unbekanntes Element: <x-widget data-k="v" onclick="evil()">Widget-Inhalt</x-widget>.</p>'
|
|||
|
|
"<table><thead><tr><th>Befehl</th><th>Bedeutung</th></tr></thead>"
|
|||
|
|
"<tbody><tr><td><code>ls | wc -l</code></td><td>Dateien <strong>zählen</strong></td></tr>"
|
|||
|
|
"<tr><td><code>a && b</code></td><td>und</td></tr></tbody></table>"
|
|||
|
|
)
|
|||
|
|
return Book(title="Code und Tabellen", docs=[Doc("code.xhtml", body, "Code")])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def f05() -> Book:
|
|||
|
|
body = (
|
|||
|
|
"<h1>Bilder</h1>"
|
|||
|
|
'<figure id="fig1"><img src="images/foto%201%20%C3%A4.png" alt="Ein Foto" title="Fototitel"/>'
|
|||
|
|
"<figcaption>Abbildung 1.1: Ein Foto mit Unterschrift.</figcaption></figure>"
|
|||
|
|
'<p>Nochmals dasselbe Foto: <img src="images/foto%201%20%C3%A4.png" alt="Wiederholung"/></p>'
|
|||
|
|
'<figure><img src="images/diagramm.svg" alt=""/><figcaption>Abbildung 1.2: Ein SVG-Diagramm.</figcaption></figure>'
|
|||
|
|
'<p>Inline-SVG: <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><title>Kreis</title>'
|
|||
|
|
'<circle cx="10" cy="10" r="8"/></svg></p>'
|
|||
|
|
'<p>Siehe <a href="#fig1">Abbildung 1.1</a> und <a href="images/diagramm.svg">SVG direkt</a>.</p>'
|
|||
|
|
'<p>Fehlendes Bild: <img src="images/fehlt.png" alt="weg"/></p>'
|
|||
|
|
)
|
|||
|
|
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 = (
|
|||
|
|
"<h1>Formeln</h1>"
|
|||
|
|
f"<p>Bruch inline: <math {M}><mfrac><mi>a</mi><mi>b</mi></mfrac></math>.</p>"
|
|||
|
|
f"<p>Potenz: <math {M}><msup><mi>x</mi><mn>2</mn></msup></math>, Index: <math {M}><msub><mi>a</mi><mi>i</mi></msub></math>, "
|
|||
|
|
f"beides: <math {M}><msubsup><mi>x</mi><mi>i</mi><mn>2</mn></msubsup></math>.</p>"
|
|||
|
|
f'<div class="equation" id="eq1"><math {M} display="block"><mrow><msup><mi>e</mi><mrow><mi>i</mi><mi>π</mi></mrow></msup>'
|
|||
|
|
'<mo>+</mo><mn>1</mn><mo>=</mo><mn>0</mn></mrow></math><span class="eqno">(2.1)</span></div>'
|
|||
|
|
'<p>Nach Gleichung <a href="#eq1">(2.1)</a> folgt …</p>'
|
|||
|
|
f'<p>Nicht unterstützt: <math {M}><menclose notation="box"><mi>z</mi></menclose></math>.</p>'
|
|||
|
|
f'<p>Mit TeX-Annotation: <math {M}><semantics><mi>y</mi><annotation encoding="application/x-tex">\\vec{{y}}</annotation>'
|
|||
|
|
"</semantics></math>.</p>"
|
|||
|
|
f'<p>alttext: <math {M} alttext="\\sqrt{{2}}"><msqrt><mn>2</mn></msqrt></math>.</p>'
|
|||
|
|
'<p>SVG-Formel: <span class="math"><img src="f/formel.svg" alt="Formel E=mc²"/></span></p>'
|
|||
|
|
'<p>Bildformel: <span class="equation"><img src="f/formel.png" alt="Summe über i"/></span></p>'
|
|||
|
|
'<div class="Equation"><img src="f/tex.png" alt="$$\\int_0^1 x\\,dx = \\frac{1}{2}$$"/> <span>(2.2)</span></div>'
|
|||
|
|
)
|
|||
|
|
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 = (
|
|||
|
|
"<h1>Kapitel mit Fußnoten</h1>"
|
|||
|
|
'<p>Aussage A<sup><a epub:type="noteref" href="#fn1" id="ref1">1</a></sup> und B'
|
|||
|
|
'<sup><a role="doc-noteref" href="#fn2" id="ref2">2</a></sup>, nochmal A<sup><a epub:type="noteref" href="#fn1" id="ref1b">1</a></sup>.</p>'
|
|||
|
|
'<p>Endnote<sup><a epub:type="noteref" href="notes.xhtml#en1" id="eref1">3</a></sup> und kaputt<a epub:type="noteref" href="#fehlt">9</a>.</p>'
|
|||
|
|
'<aside epub:type="footnote" id="fn1"><p><a href="#ref1">1</a> Erster Absatz mit <em>Hervorhebung</em> und '
|
|||
|
|
'<a href="https://example.org/quelle">Link</a>.</p><p>Zweiter Absatz.</p></aside>'
|
|||
|
|
f'<aside epub:type="footnote" id="fn2"><p><a href="#ref2">2</a> Mit Formel <math {M}><msup><mi>x</mi><mn>3</mn></msup></math> '
|
|||
|
|
"und Code <code>f(x)</code>.</p></aside>"
|
|||
|
|
)
|
|||
|
|
notes = (
|
|||
|
|
'<section epub:type="endnotes"><h1>Anmerkungen</h1>'
|
|||
|
|
'<aside epub:type="endnote" id="en1"><p><a href="ch1.xhtml#eref1">3</a> Eine Endnote mit Rücklink.</p></aside></section>'
|
|||
|
|
)
|
|||
|
|
return Book(title="Fußnoten und Endnoten", docs=[Doc("ch1.xhtml", ch1, "Kapitel"), Doc("notes.xhtml", notes, "Anmerkungen")])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def f08() -> Book:
|
|||
|
|
ch = ('<h1>Hauptteil</h1><p>Der Begriff <a href="glossar.xhtml#g-api">API</a> und '
|
|||
|
|
'<span id="idx-rekursion">Rekursion</span> nach <span id="p-knuth">Knuth</span> [<a href="lit.xhtml#knuth">Knu97</a>].</p>')
|
|||
|
|
glossary = ('<section epub:type="glossary"><h1>Glossar</h1><dl>'
|
|||
|
|
'<dt id="g-api">API</dt><dd>Application Programming Interface, eine Schnittstelle.</dd>'
|
|||
|
|
"<dt>Rekursion</dt><dd><p>Selbstaufruf einer Funktion.</p><p>Siehe auch Iteration.</p></dd></dl></section>")
|
|||
|
|
lit = ('<section epub:type="bibliography"><h1>Literatur</h1><ol>'
|
|||
|
|
'<li id="knuth">Knuth, D. E.: <em>The Art of Computer Programming</em>, Bd. 1, 3. Aufl., Addison-Wesley, 1997, '
|
|||
|
|
"ISBN 978-0-201-89683-1, S. 1–650.</li>"
|
|||
|
|
'<li>Doe, J.: Ein Artikel. <em>Journal</em> 12 (2020), DOI <a href="https://doi.org/10.1000/xyz123">10.1000/xyz123</a>.</li>'
|
|||
|
|
'<li>Webquelle: <a href="https://example.org/seite?a=1&b=2">https://example.org/seite?a=1&b=2</a> '
|
|||
|
|
"(Zugriff: 01.02.2024).</li></ol></section>")
|
|||
|
|
sach = ('<section epub:type="index"><h1>Sachregister</h1>'
|
|||
|
|
'<div class="group"><h2>A</h2><p class="index1">API <a href="glossar.xhtml#g-api">Glossar</a></p></div>'
|
|||
|
|
'<div class="group"><h2>R</h2><p class="index1">Rekursion <a href="main.xhtml#idx-rekursion">17</a>, 42</p>'
|
|||
|
|
'<p class="index2">endständige, 45</p></div></section>')
|
|||
|
|
pers = ('<section epub:type="index"><h1>Personenregister</h1><h2>K</h2>'
|
|||
|
|
'<p>Knuth, Donald E. <a href="main.xhtml#p-knuth">17</a></p></section>')
|
|||
|
|
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 = (
|
|||
|
|
"<h1>Komplexe Tabelle</h1>"
|
|||
|
|
"<table><caption>Tabelle 9.1: Messwerte</caption>"
|
|||
|
|
'<thead><tr><th rowspan="2">Probe</th><th colspan="2">Messung</th></tr><tr><th>vorher</th><th>nachher</th></tr></thead>'
|
|||
|
|
"<tbody><tr><td>A</td><td><p>1,5</p><p><em>korrigiert</em></p></td><td><strong>2,0</strong></td></tr>"
|
|||
|
|
'<tr><td>B</td><td colspan="2">ausgefallen<br/>siehe Text</td></tr></tbody></table>'
|
|||
|
|
)
|
|||
|
|
return Book(title="Komplexe Tabelle", docs=[Doc("t.xhtml", body, "Tabelle")])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def f10() -> Book:
|
|||
|
|
body = ('<h1>Böse Pfade</h1><p><img src="../../outside.png" alt="raus"/> '
|
|||
|
|
'<a href="../escape.xhtml">escape</a> <img src="/absolute/path.png" alt="abs"/></p>')
|
|||
|
|
return Book(title="Malicious", docs=[Doc("ok.xhtml", body, "OK")],
|
|||
|
|
extra_entries={"../../outside.txt": b"evil", "../escape.xhtml": b"<html/>", "/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'<h1 id="größe">Größe & Übermaß</h1><p>{text}</p><p><a href="#größe">Rücksprung</a></p>'
|
|||
|
|
return Book(title="Kodierung – Ümläute", authors=["Zoë Ørsted"],
|
|||
|
|
docs=[Doc("kapitel-ü.xhtml", body, "Größe"), Doc("café.xhtml", "<h1>Café crème</h1><p>Ça va.</p>", "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 = ('<h1>Gestaltung</h1>'
|
|||
|
|
'<p>Ein <span class="kursiv">kursives</span> und <span class="fett">fettes</span> Wort, '
|
|||
|
|
'<span class="u">unterstrichen</span><span class="weg">VERSTECKT</span>.</p>'
|
|||
|
|
'<p>Null\u200bbreite und wei\u00adches Trenn\u00adzeichen.</p>'
|
|||
|
|
'<p>Szene eins endet hier.</p><p class="leer"> </p><p>Szene zwei beginnt hier.</p>'
|
|||
|
|
'<div class="poem"><p>Zeile eins</p><p>Zeile zwei</p></div>'
|
|||
|
|
'<p>Ein <span class="markiert">markierter</span>, <span class="rot">roter</span> und '
|
|||
|
|
'<span class="gross">großer</span> Text in <span class="kap">Kapitälchen</span>, '
|
|||
|
|
'<span lang="en">an English phrase</span>.</p>'
|
|||
|
|
'<p class="mitte">Zentrierte Widmung</p>'
|
|||
|
|
'<p>Seite elf endet<span epub:type="pagebreak" id="page12" title="12"/> und Seite zwölf '
|
|||
|
|
'geht weiter<a id="page_13"/> bis dreizehn.</p>'
|
|||
|
|
'<pre><code>x = 1\u200b # bleibt</code></pre>')
|
|||
|
|
c2 = ('<h1>Kästen</h1>'
|
|||
|
|
'<div class="box hinweis"><h3 class="boxheading"><span class="box_icon">[»]</span>Wichtige Regel</h3>'
|
|||
|
|
'<p>Immer testen.</p></div>'
|
|||
|
|
'<aside epub:type="tip"><p>Zwischendurch speichern.</p></aside><p>Nachher.</p>')
|
|||
|
|
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("</manifest>", '<item id="evil" href="../../escape.xhtml" media-type="application/xhtml+xml"/></manifest>')
|
|||
|
|
rewrite(p10, {"OEBPS/content.opf": opf.encode()})
|
|||
|
|
|
|||
|
|
# 11: broken container variants
|
|||
|
|
base = Book(title="Defekt", docs=[Doc("a.xhtml", "<h1>A</h1><p>x</p>", "A")])
|
|||
|
|
p11 = build_epub(OUT / "11-broken-container.epub", base)
|
|||
|
|
rewrite(p11, {"META-INF/container.xml": (
|
|||
|
|
b'<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">'
|
|||
|
|
b'<rootfiles><rootfile full-path="OEBPS/gibt-es-nicht.opf" media-type="application/oebps-package+xml"/>'
|
|||
|
|
b"</rootfiles></container>")})
|
|||
|
|
p11b = build_epub(OUT / "11b-broken-spine.epub", Book(title="Spine", docs=[Doc("a.xhtml", "<h1>A</h1><p>x</p>", "A")],
|
|||
|
|
spine_extra='<itemref idref="fehlt"/>'))
|
|||
|
|
assert p11b.exists()
|
|||
|
|
p11c = build_epub(OUT / "11c-broken-opf-xml.epub", base)
|
|||
|
|
rewrite(p11c, {"OEBPS/content.opf": b"<package><metadata><dc:title>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"<html><body><h1>Kaputt<p>nicht geschlossen &unbekannt; <b>fett</body>"})
|
|||
|
|
print("\n".join(sorted(p.name for p in OUT.glob("*.epub"))))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|