feat: Buchanfang Cover → Titel → Impressum → Inhaltsverzeichnis für alle Bücher
- Kapitelerkennung: erstes Kapitel nur aus einem Bild = Cover ("[Titelinfo]",
"Deckblatt"), Titel "Titel" = Titelseite
- epub2md_merge: Vorspann reicht bis vor den Hauptteil (Teil, Vorwort,
Einleitung, nummerierter Titel, > 1000 Wörter), kurze Zwischenseiten
(Auflage, Verlagsbrief, "Über dieses Buch") bleiben davor
- book.lua: Cover am Inhalt erkannt; LaTeX-Weg (Filter in beiden Schritten):
\tableofcontents hinter dem Vorspann, \clearpage je Kapitel, kein \maketitle
vor dem Cover, A4, seitengroßes Cover, DejaVu-Schriften und Mathe-Symbole
für fehlende Glyphen (Mathebuch: >10.000 fehlende Zeichen → 0)
- Buch-README: bei Formeln LaTeX-Weg empfohlen, sonst weasyprint
- Alle sechs Beispielbücher geprüft: Seite 1 Cover, Verzeichnis nach dem Vorspann
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
99c3c609ca
commit
86aa93c12c
10 changed files with 257 additions and 46 deletions
|
|
@ -323,7 +323,7 @@ P=~/Python_Programs/epub2md/pandoc # Pfad zu deinem epub2md-Ordner anpassen
|
|||
| E-Book (EPUB) | `pandoc meinbuch.md --toc --lua-filter $P/callouts.lua -o meinbuch.epub` |
|
||||
| Word | `pandoc meinbuch.md --toc --lua-filter $P/callouts.lua -o meinbuch.docx` |
|
||||
| LibreOffice | `pandoc meinbuch.md --toc --lua-filter $P/callouts.lua -o meinbuch.odt` |
|
||||
| PDF mit LaTeX | `pandoc meinbuch.md -s --lua-filter $P/callouts.lua -o meinbuch.html && pandoc meinbuch.html --toc --lua-filter $P/callouts.lua --pdf-engine=xelatex -o meinbuch.pdf` |
|
||||
| PDF mit LaTeX (Bücher mit Formeln) | `pandoc meinbuch.md -s --mathml --lua-filter $P/book.lua --lua-filter $P/callouts.lua -o meinbuch.html && pandoc meinbuch.html --lua-filter $P/book.lua --lua-filter $P/callouts.lua --pdf-engine=xelatex -o meinbuch.pdf` |
|
||||
| Kindle (AZW3) | erst EPUB wie oben, dann `ebook-convert meinbuch.epub meinbuch.azw3` |
|
||||
|
||||
Die drei Hilfsdateien:
|
||||
|
|
@ -331,9 +331,11 @@ Die drei Hilfsdateien:
|
|||
- **`book.lua`** ordnet den Buchanfang wie im gedruckten Buch: Cover, Titelseite, Impressum, dann das
|
||||
Inhaltsverzeichnis, jeweils auf einer neuen Seite. Ohne den Filter setzt Pandoc mit `--toc` das
|
||||
Inhaltsverzeichnis (und seinen eigenen Titelkopf) vor das Cover. Mit `book.lua` deshalb **kein** `--toc`
|
||||
angeben. Hat das Buch keine eigene Titelseite, setzt der Filter Pandocs Titelkopf hinter das Cover. Er
|
||||
wirkt nur bei HTML und beim PDF über weasyprint; EPUB, Word und LibreOffice bauen ihr Verzeichnis mit
|
||||
`--toc` selbst.
|
||||
angeben. Hat das Buch keine eigene Titelseite, setzt der Filter Pandocs Titelkopf hinter das Cover. Das
|
||||
gilt für HTML und für beide PDF-Wege (beim LaTeX-Weg den Filter in beiden Schritten angeben); EPUB, Word
|
||||
und LibreOffice bauen ihr Verzeichnis mit `--toc` selbst. Kurze Seiten zwischen Titelseite und Impressum
|
||||
(Auflagenvermerk, Verlagsbrief, „Über dieses Buch“) bleiben im Vorspann; das Inhaltsverzeichnis folgt vor
|
||||
dem ersten Teil, Vorwort oder nummerierten Kapitel.
|
||||
- **`callouts.lua`** zeichnet die Infokästen (`> [!TIP]`). Pandoc in der Ubuntu-Version kennt diese
|
||||
Schreibweise nicht und druckt „[!TIP]“ sonst einfach mit. In HTML, EPUB und im PDF über weasyprint werden
|
||||
daraus gerahmte, farbige Kästen mit Titel („Tipp: …“), im PDF über LaTeX und in Word/LibreOffice ein
|
||||
|
|
@ -346,12 +348,13 @@ Du musst die Befehle nicht abtippen: Die `README.md` im Ausgabeordner enthält s
|
|||
|
||||
**Zum PDF:**
|
||||
|
||||
- **Mit weasyprint** (empfohlen) genügt ein Schritt. Tabellen, Farben, Infokästen und die Gestaltung aus
|
||||
`--styles html` bleiben erhalten, der Buchanfang ist richtig geordnet. Für Bücher mit vielen Formeln ist
|
||||
der LaTeX-Weg sicherer.
|
||||
- **Mit weasyprint** (empfohlen für Bücher ohne Formeln) genügt ein Schritt. Tabellen, Farben,
|
||||
Infokästen und die Gestaltung aus `--styles html` bleiben erhalten.
|
||||
- **Mit LaTeX** immer den Zwischenschritt über HTML nehmen, wie in der Tabelle. Beim direkten Weg von
|
||||
Markdown nach LaTeX lässt Pandoc komplexe Tabellen weg, und Querverweise im PDF funktionieren nicht. Das
|
||||
Inhaltsverzeichnis steht hier am Anfang, vor dem Cover.
|
||||
Markdown nach LaTeX lässt Pandoc komplexe Tabellen weg, und Querverweise im PDF funktionieren nicht. Für
|
||||
Bücher mit Formeln ist das der richtige Weg, weil weasyprint keine Formeln setzen kann. Farben und
|
||||
Kastenrahmen gehen dabei verloren; Kästen erscheinen als eingerückter Block mit fettem Titel. `book.lua`
|
||||
stellt dabei A4 ein und wählt die Schrift DejaVu, die auch Zeichen wie ∈, λ oder ≠ im Fließtext enthält.
|
||||
|
||||
Bei Büchern mit Formeln für HTML zusätzlich `--mathml` angeben, sonst bleiben Matrizen als TeX-Text stehen.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ Versionsnummern nach [Semantic Versioning](https://semver.org/lang/de/).
|
|||
davor (Pandocs `--toc` setzt es immer an den Anfang). `epub2md_merge.py` markiert dafür das Ende des
|
||||
Vorspanns; ohne eigene Titelseite kommt Pandocs Titelkopf hinter das Cover. Jedes Kapitel, auch Cover,
|
||||
Titelseite und Impressum, beginnt mit `print.css` auf einer neuen Seite; das Inhaltsverzeichnis hat
|
||||
Seitenzahlen. Mit `epub2md_merge.py --toc` steht dessen Verzeichnis an derselben Stelle.
|
||||
Seitenzahlen. Mit `epub2md_merge.py --toc` steht dessen Verzeichnis an derselben Stelle. Gilt auch für den
|
||||
PDF-Weg über LaTeX (`\tableofcontents` hinter dem Vorspann); bei Büchern mit Formeln empfiehlt die
|
||||
Buch-README diesen Weg. Dort setzt der Filter außerdem A4, ein seitengroßes Cover und DejaVu-Schriften,
|
||||
sodass keine Zeichen mehr fehlen (Mathebuch: vorher über 10.000 fehlende Glyphen wie ∈, λ, ≠).
|
||||
- Kapitelerkennung: Ein erstes Kapitel nur aus einem Bild ist das Cover (auch „[Titelinfo]“, „Deckblatt“),
|
||||
ein Kapitel „Titel“ die Titelseite. Der Vorspann darf kurze Zwischenseiten enthalten.
|
||||
- `pandoc/print.css`: Druck-Layout für PDFs mit weasyprint (A4, Seitenzahlen, Kapitel auf neuer Seite,
|
||||
Silbentrennung außer in Code, umbrechende Codezeilen). Die Buch-README enthält den passenden Befehl.
|
||||
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -337,10 +337,10 @@ pandoc programmierbuch.md -s --lua-filter $P/book.lua --lua-filter $P/callouts.l
|
|||
pandoc programmierbuch.md -s --lua-filter $P/book.lua --lua-filter $P/callouts.lua -o programmierbuch.html
|
||||
pandoc programmierbuch.md --toc --lua-filter $P/callouts.lua -o programmierbuch.epub
|
||||
|
||||
cd output/mathematikbuch
|
||||
pandoc mathematikbuch.md --mathml -s -o mathematikbuch.html
|
||||
# PDF über den Zwischenschritt HTML (siehe unten)
|
||||
pandoc mathematikbuch.html --toc --pdf-engine=xelatex -o mathematikbuch.pdf
|
||||
cd output/mathematikbuch # Formeln: PDF über HTML und LaTeX (weasyprint setzt kein MathML)
|
||||
pandoc mathematikbuch.md -s --mathml --lua-filter $P/book.lua --lua-filter $P/callouts.lua -o mathematikbuch.html
|
||||
pandoc mathematikbuch.html --lua-filter $P/book.lua --lua-filter $P/callouts.lua --pdf-engine=xelatex \
|
||||
-o mathematikbuch.pdf
|
||||
```
|
||||
|
||||
Hilfsdateien in `pandoc/`:
|
||||
|
|
@ -351,8 +351,16 @@ Hilfsdateien in `pandoc/`:
|
|||
keine eigene Titelseite hat, die Stelle hinter dem Cover (`<div id="epub2md-titlepage"></div>`). Der
|
||||
Filter baut dort das Inhaltsverzeichnis (`--toc-depth` gilt), lässt Pandocs Titelkopf bei vorhandener
|
||||
Titelseite weg, zeigt das Cover ohne Bildunterschrift und macht aus den Kapitelankern
|
||||
`div.chapter-start` für Seitenumbrüche. Bei anderen Formaten entfernt er nur die Platzhalter. Wer selbst
|
||||
`--toc` angibt, bekommt Pandocs Verzeichnis wie gewohnt.
|
||||
`div.chapter-start` für Seitenumbrüche. Beim LaTeX-Weg (Filter in beiden Schritten) wird daraus
|
||||
`\tableofcontents` an derselben Stelle, `\clearpage` vor jedem Kapitel und kein `\maketitle` vor dem Cover.
|
||||
Außerdem A4 mit 2 cm Rand, das Cover in Seitengröße und – falls installiert und nicht selbst gewählt –
|
||||
DejaVu-Schriften (`mainfont`, `monofont`, `mathfont`): Latin Modern fehlen Zeichen wie ∈ λ ≠ ≥ □ und
|
||||
schmale Leerzeichen (im Mathebuch über 10.000 Stellen). ℓ, 〈 〉, ∖, ⋯ und „↩︎“ werden als Mathe-Symbole gesetzt.
|
||||
Bei anderen Formaten entfernt er nur die Platzhalter. Wer selbst `--toc` angibt, bekommt Pandocs
|
||||
Verzeichnis wie gewohnt.
|
||||
- Der Vorspann reicht bis vor das erste Kapitel des Hauptteils (Teil, Vorwort, Einleitung, Prolog,
|
||||
nummerierter Titel oder mehr als 1000 Wörter), sofern er Cover, Titelseite oder Impressum enthält. Ein
|
||||
erstes Kapitel nur aus einem Bild gilt als Cover, ein Kapitel „Titel“ als Titelseite.
|
||||
- **`print.css`** (weasyprint): A4, Seitenzahlen, jedes Kapitel (auch Cover, Titelseite, Impressum) und das
|
||||
Inhaltsverzeichnis auf neuer Seite, Inhaltsverzeichnis mit Punktlinie und Seitenzahl, Silbentrennung
|
||||
(nicht in Code), umbrechende Codezeilen. Mit `--css` lässt Pandoc sein eigenes Stylesheet weg, deshalb
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ KIND_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|||
("appendix", re.compile(r"^(anhang|appendix)", re.I)),
|
||||
("colophon", re.compile(r"^(impressum|colophon|kolophon)", re.I)),
|
||||
("dedication", re.compile(r"^(widmung|dedication)", re.I)),
|
||||
("cover", re.compile(r"^(cover|umschlag|titelbild)\b", re.I)),
|
||||
("titlepage", re.compile(r"^(titelei|titelseite|title ?page)", re.I)),
|
||||
("cover", re.compile(r"^(cover|umschlag|titelbild|deckblatt)\b", re.I)),
|
||||
("titlepage", re.compile(r"^(titelei|titelseite|title ?page|titel\s*$|title\s*$)", re.I)),
|
||||
("part", re.compile(r"^(teil|part)\s+([IVXLC]+|\d+|[a-zäöü]+)\b", re.I)),
|
||||
("epilogue", re.compile(r"^(epilog|nachwort|epilogue|afterword)", re.I)),
|
||||
("prologue", re.compile(r"^(prolog|prologue)", re.I)),
|
||||
|
|
@ -307,12 +307,21 @@ class ChapterBuilder:
|
|||
kind = classify_title(ch.title)
|
||||
if kind and kind not in ch.types:
|
||||
ch.types.append(kind)
|
||||
if ch.number == 1 and "cover" not in ch.types and _image_only(ch.items):
|
||||
ch.types.append("cover") # first chapter with nothing but a picture, whatever its label ('[Titelinfo]')
|
||||
if "index" in ch.types and classify_title(ch.title) == "index-persons":
|
||||
ch.types.remove("index")
|
||||
if not ch.types:
|
||||
ch.types.append("chapter")
|
||||
|
||||
|
||||
def _image_only(items: list[tuple[str, Any]]) -> bool:
|
||||
"""Content is one or more pictures without any text."""
|
||||
text = "".join(it if isinstance(it, str) else text_content(it) for _, it in items if isinstance(it, str) or is_element(it))
|
||||
return not text.strip() and any(is_element(it) and any(local(e.tag) in ("img", "svg", "image") for e in iter_elements(it))
|
||||
for _, it in items)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TypeCarrier:
|
||||
types: list[str]
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class ChapterFile:
|
|||
start_anchor: str = ""
|
||||
anchor_map: dict[str, str] = field(default_factory=dict)
|
||||
types: list[str] = field(default_factory=list)
|
||||
words: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -75,12 +76,13 @@ def load_book(out_dir: Path) -> tuple[dict, list[ChapterFile]]:
|
|||
report = json.loads(report_path.read_text(encoding="utf-8")) if report_path.is_file() else {}
|
||||
if manifest_path.is_file():
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
entries = [(c["number"], c["title"], c["file"], c.get("types") or []) for c in manifest.get("chapters", [])]
|
||||
entries = [(c["number"], c["title"], c["file"], c.get("types") or [], c.get("word_count", 0))
|
||||
for c in manifest.get("chapters", [])]
|
||||
else:
|
||||
files = sorted((out_dir / "chapters").glob("*.md"))
|
||||
entries = [(i + 1, p.stem, f"chapters/{p.name}", []) for i, p in enumerate(files)]
|
||||
chapters = [ChapterFile(n, t, rel, types=list(ty)) for n, t, rel, ty in entries if (out_dir / rel).is_file()]
|
||||
missing = [rel for _, _, rel, _ in entries if not (out_dir / rel).is_file()]
|
||||
entries = [(i + 1, p.stem, f"chapters/{p.name}", [], 0) for i, p in enumerate(files)]
|
||||
chapters = [ChapterFile(n, t, rel, types=list(ty), words=w) for n, t, rel, ty, w in entries if (out_dir / rel).is_file()]
|
||||
missing = [rel for _, _, rel, _, _ in entries if not (out_dir / rel).is_file()]
|
||||
if missing:
|
||||
raise MergeError(f"Kapiteldateien fehlen: {', '.join(missing[:5])}")
|
||||
if not chapters:
|
||||
|
|
@ -97,9 +99,25 @@ TOC_PLACEHOLDER = '<div id="epub2md-toc"></div>' # pandoc/book.lua puts the tab
|
|||
TITLE_PLACEHOLDER = '<div id="epub2md-titlepage"></div>' # … and Pandoc's title block here (no own title page)
|
||||
|
||||
|
||||
# Where the main matter starts: a part, preface, introduction …, a numbered title or a longer text.
|
||||
MAIN_TYPES = {"part", "preface", "foreword", "introduction", "prologue", "preamble", "appendix", "bodymatter", "toc"}
|
||||
NUMBERED_TITLE = re.compile(r"^\s*(\d+|[IVXLC]+\.\s|teil\b|kapitel\b|part\b|chapter\b|buch\b|book\b)", re.I)
|
||||
FRONT_MAX_WORDS = 1000
|
||||
FRONT_MAX_CHAPTERS = 12
|
||||
|
||||
|
||||
def front_matter_count(chapters: list[ChapterFile]) -> int:
|
||||
"""Number of leading chapters that are front matter (cover, title page, imprint …)."""
|
||||
n = 0
|
||||
"""Number of leading chapters before the table of contents: cover, title page, imprint and the short
|
||||
pages between them (series page, edition note, publisher's letter …) up to the start of the main matter."""
|
||||
k = 0
|
||||
while k < min(len(chapters), FRONT_MAX_CHAPTERS):
|
||||
ch = chapters[k]
|
||||
if MAIN_TYPES & set(ch.types) or NUMBERED_TITLE.match(ch.title) or ch.words > FRONT_MAX_WORDS:
|
||||
break
|
||||
k += 1
|
||||
if k < len(chapters) and any(FRONT_TYPES & set(c.types) for c in chapters[:k]):
|
||||
return k
|
||||
n = 0 # no clear start of the main matter: only the unbroken run of typed front matter
|
||||
while n < len(chapters) and FRONT_TYPES & set(chapters[n].types):
|
||||
n += 1
|
||||
return n
|
||||
|
|
@ -257,7 +275,7 @@ def merge(out_dir: Path, target: Path | None = None, *, frontmatter: bool = True
|
|||
if fm:
|
||||
parts.append("---\n" + to_yaml(fm) + "---")
|
||||
front = front_matter_count(chapters)
|
||||
has_title_page = any("titlepage" in ch.types for ch in chapters[:front])
|
||||
has_title_page = any({"titlepage", "halftitlepage"} & set(ch.types) for ch in chapters[:front])
|
||||
cover_first = bool(chapters) and "cover" in chapters[0].types
|
||||
if toc:
|
||||
lines = ["## Inhalt" if (report.get("metadata") or {}).get("language", "de").startswith("de") else "## Contents", ""]
|
||||
|
|
|
|||
|
|
@ -139,23 +139,26 @@ def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None,
|
|||
if callouts and CALLOUT_FILTER.is_file(): # '> [!TIP]' boxes: Pandoc needs the filter to draw real boxes
|
||||
lua = f" --lua-filter {shlex.quote(str(CALLOUT_FILTER))}"
|
||||
# book.lua: table of contents after cover/title page/imprint instead of Pandoc's --toc at the very top
|
||||
toc = f" --lua-filter {shlex.quote(str(BOOK_FILTER))}" if BOOK_FILTER.is_file() else " --toc"
|
||||
book = f" --lua-filter {shlex.quote(str(BOOK_FILTER))}" if BOOK_FILTER.is_file() else ""
|
||||
toc = book or " --toc"
|
||||
md_, html_, pdf_ = (shlex.quote(stem + ext) for ext in (".md", ".html", ".pdf"))
|
||||
latex = [
|
||||
"# PDF mit LaTeX über HTML: Pandoc übernimmt dann auch HTML-Tabellen und interne Sprungziele",
|
||||
f"pandoc {md_} -s{math}{book}{lua} -o {html_} && pandoc {html_}{toc}{lua} --pdf-engine=xelatex -o {pdf_}",
|
||||
]
|
||||
weasy = [
|
||||
"# PDF mit weasyprint: Farben, Kästen und HTML-Stile bleiben erhalten" + ("" if math else " (empfohlen)"),
|
||||
f"pandoc {md_} -s{math}{toc}{lua} --css {shlex.quote(str(PRINT_CSS))} --pdf-engine=weasyprint -o {pdf_}",
|
||||
] if PRINT_CSS.is_file() else []
|
||||
if math:
|
||||
latex[0] += " (empfohlen: weasyprint kann keine Formeln setzen)"
|
||||
commands = [
|
||||
"# Gesamtdatei erzeugen (im selben Arbeitsverzeichnis wie die Konvertierung)",
|
||||
f"python3 epub2md_merge.py {shlex.quote(output_dir)}",
|
||||
"# danach in diesem Ordner:",
|
||||
f"pandoc {shlex.quote(stem + '.md')} -s{math}{toc}{lua} -o {shlex.quote(stem + '.html')}",
|
||||
f"pandoc {shlex.quote(stem + '.md')} --toc{lua} -o {shlex.quote(stem + '.epub')}",
|
||||
"# PDF über HTML: Pandoc übernimmt dann auch HTML-Tabellen und interne Sprungziele",
|
||||
f"pandoc {shlex.quote(stem + '.md')} -s{math}{lua} -o {shlex.quote(stem + '.html')} && "
|
||||
f"pandoc {shlex.quote(stem + '.html')} --toc{lua} --pdf-engine=xelatex -o {shlex.quote(stem + '.pdf')}",
|
||||
]
|
||||
if PRINT_CSS.is_file():
|
||||
commands += [
|
||||
"# PDF ohne LaTeX mit weasyprint: Farben, Kästen und HTML-Stile bleiben erhalten",
|
||||
f"pandoc {shlex.quote(stem + '.md')} -s{math}{toc}{lua} --css {shlex.quote(str(PRINT_CSS))} "
|
||||
f"--pdf-engine=weasyprint -o {shlex.quote(stem + '.pdf')}",
|
||||
]
|
||||
f"pandoc {md_} -s{math}{toc}{lua} -o {html_}",
|
||||
f"pandoc {md_} --toc{lua} -o {shlex.quote(stem + '.epub')}",
|
||||
] + (latex + weasy if math else weasy + latex)
|
||||
out.append("## Weiterverarbeitung")
|
||||
out.append("```bash\n" + "\n".join(commands) + "\n```")
|
||||
if callouts:
|
||||
|
|
|
|||
144
pandoc/book.lua
144
pandoc/book.lua
|
|
@ -10,11 +10,13 @@ i.e. before the book's cover, title page and imprint. epub2md_merge.py marks the
|
|||
This filter, for HTML output:
|
||||
* builds the table of contents at the placeholder (use it INSTEAD of --toc; --toc-depth is honoured),
|
||||
* drops Pandoc's title block when the book has its own title page, or moves it to the placeholder,
|
||||
* shows the cover picture (chapter "NNN-cover") without the caption "Cover",
|
||||
* shows the cover (first chapter with nothing but one picture) without the caption "Cover",
|
||||
* turns the chapter anchors <a id="001-…"></a> into <div class="chapter-start"> so that print.css can
|
||||
start every chapter – including cover, title page and imprint – on a new page.
|
||||
|
||||
For other formats it only removes the placeholders (EPUB, DOCX, ODT: use --toc there).
|
||||
For LaTeX (PDF route md -> html -> pdf, filter in BOTH steps, no --toc) the table of contents becomes
|
||||
\tableofcontents at the same place, every chapter starts with \clearpage and there is no \maketitle
|
||||
before the cover. For other formats it only removes the placeholders (EPUB, DOCX, ODT: use --toc there).
|
||||
|
||||
Usage:
|
||||
pandoc buch.md -s --lua-filter /pfad/zu/epub2md/pandoc/book.lua \
|
||||
|
|
@ -73,7 +75,123 @@ local function chapter_anchor(block)
|
|||
return id
|
||||
end
|
||||
|
||||
-- Characters in running text that even DejaVu lacks: typeset as math symbols ("↩︎": footnote back-links).
|
||||
local LATEX_SYMBOLS = { ["↩"] = "$\\hookleftarrow$", ["ℓ"] = "$\\ell$", ["\u{2329}"] = "$\\langle$",
|
||||
["\u{232A}"] = "$\\rangle$", ["\u{3008}"] = "$\\langle$", ["\u{3009}"] = "$\\rangle$",
|
||||
["∖"] = "$\\setminus$", ["⋯"] = "$\\cdots$", ["\u{FE0E}"] = "" }
|
||||
local LATEX_FONTS = { mainfont = "DejaVu Serif", monofont = "DejaVu Sans Mono", mathfont = "DejaVu Math TeX Gyre" }
|
||||
|
||||
local function font_installed(name)
|
||||
local pipe = io.popen("fc-list -q " .. string.format("%q", name) .. " && echo yes")
|
||||
local out = pipe and pipe:read("a") or ""
|
||||
if pipe then
|
||||
pipe:close()
|
||||
end
|
||||
return out:match("yes") ~= nil
|
||||
end
|
||||
|
||||
local function latex_symbols(str)
|
||||
local found = false
|
||||
for _, c in utf8.codes(str.text) do
|
||||
if LATEX_SYMBOLS[utf8.char(c)] then
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
return nil
|
||||
end
|
||||
local out, buf = pandoc.Inlines({}), {}
|
||||
for _, c in utf8.codes(str.text) do
|
||||
local ch = utf8.char(c)
|
||||
local tex = LATEX_SYMBOLS[ch]
|
||||
if tex then
|
||||
if #buf > 0 then
|
||||
out:insert(pandoc.Str(table.concat(buf)))
|
||||
buf = {}
|
||||
end
|
||||
if tex ~= "" then
|
||||
out:insert(pandoc.RawInline("latex", tex))
|
||||
end
|
||||
else
|
||||
buf[#buf + 1] = ch
|
||||
end
|
||||
end
|
||||
if #buf > 0 then
|
||||
out:insert(pandoc.Str(table.concat(buf)))
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- LaTeX (2nd step of the PDF route md -> html -> pdf): the HTML from step 1 already holds the table of
|
||||
-- contents (div#TOC.toc), the chapter starts (div.chapter-start) and, if moved, Pandoc's title block.
|
||||
local function latex_book(doc)
|
||||
local found = false
|
||||
doc.blocks:walk({ Div = function(d)
|
||||
if d.identifier == "TOC" and d.classes:includes("toc") or d.classes:includes("chapter-start") then
|
||||
found = true
|
||||
end
|
||||
end })
|
||||
if not found then
|
||||
return nil
|
||||
end
|
||||
local blocks = pandoc.Blocks({})
|
||||
for _, block in ipairs(doc.blocks) do
|
||||
if block.t == "Div" and block.identifier == "TOC" and block.classes:includes("toc") then
|
||||
blocks:insert(pandoc.RawBlock("latex", "\\clearpage\n\\tableofcontents\n\\clearpage"))
|
||||
elseif block.t == "Div" and block.classes:includes("chapter-start") then
|
||||
blocks:insert(pandoc.RawBlock("latex", "\\clearpage"))
|
||||
blocks:insert(block)
|
||||
elseif block.t == "Div" and block.classes:includes("cover") then
|
||||
blocks:insert(block:walk({ Image = function(img) -- cover as large as the page allows
|
||||
img.attributes.width = nil
|
||||
img.attributes.height = "20cm"
|
||||
return img
|
||||
end }))
|
||||
elseif block.t == "Div" and block.identifier == "title-block-header" then
|
||||
blocks:insert(pandoc.RawBlock("latex", "\\clearpage\n\\begin{center}\n\\vspace*{3cm}"))
|
||||
for _, b in ipairs(block.content) do
|
||||
if b.t == "Header" then
|
||||
local title = pandoc.Inlines({ pandoc.RawInline("latex", "{\\Huge\\bfseries ") })
|
||||
title:extend(b.content)
|
||||
title:insert(pandoc.RawInline("latex", "}"))
|
||||
blocks:insert(pandoc.Para(title))
|
||||
else
|
||||
blocks:insert(b)
|
||||
end
|
||||
end
|
||||
blocks:insert(pandoc.RawBlock("latex", "\\end{center}"))
|
||||
else
|
||||
blocks:insert(block)
|
||||
end
|
||||
end
|
||||
doc.blocks = blocks:walk({ Str = latex_symbols })
|
||||
-- fonts with a wide Unicode coverage (∈ λ ≠ ≥ □, thin spaces …), unless the user chose fonts himself
|
||||
-- A4 with moderate margins, like print.css (Pandoc's LaTeX default: US letter, wide margins)
|
||||
doc.meta.papersize = doc.meta.papersize or "a4"
|
||||
doc.meta.geometry = doc.meta.geometry or "margin=2cm"
|
||||
for var, font in pairs(LATEX_FONTS) do
|
||||
if doc.meta[var] == nil and font_installed(font) then
|
||||
doc.meta[var] = font
|
||||
end
|
||||
end
|
||||
-- no \maketitle at the very top (before the cover); keep the PDF metadata
|
||||
if doc.meta.title then
|
||||
doc.meta["title-meta"] = pandoc.utils.stringify(doc.meta.title)
|
||||
doc.meta.title = nil
|
||||
end
|
||||
if doc.meta.author then
|
||||
doc.meta["author-meta"] = pandoc.utils.stringify(doc.meta.author)
|
||||
doc.meta.author = nil
|
||||
end
|
||||
doc.meta.date = nil
|
||||
return doc
|
||||
end
|
||||
|
||||
function Pandoc(doc)
|
||||
if FORMAT:match("latex") then
|
||||
return latex_book(doc)
|
||||
end
|
||||
local html = FORMAT:match("html") and not FORMAT:match("epub")
|
||||
local toc_at, title_at = nil, nil
|
||||
for i, block in ipairs(doc.blocks) do
|
||||
|
|
@ -98,6 +216,7 @@ function Pandoc(doc)
|
|||
end
|
||||
|
||||
local blocks = pandoc.Blocks({})
|
||||
local seen_chapter = false
|
||||
for i, block in ipairs(doc.blocks) do
|
||||
local anchor = chapter_anchor(block)
|
||||
if i == toc_at then
|
||||
|
|
@ -117,15 +236,26 @@ function Pandoc(doc)
|
|||
end
|
||||
elseif anchor then
|
||||
blocks:insert(pandoc.Div({}, pandoc.Attr(anchor, { "chapter-start" })))
|
||||
local nxt = doc.blocks[i + 1]
|
||||
if anchor:match("^%d%d%d%-cover") and nxt and nxt.t == "Figure" then
|
||||
-- cover chapter: the picture without the caption "Cover", centred on its own page
|
||||
local imgs = {}
|
||||
-- cover: the first chapter, if it holds nothing but one picture ('Cover', '[Titelinfo]', 'Deckblatt' …);
|
||||
-- shown without the caption "Cover", centred on its own page
|
||||
local nxt, after = doc.blocks[i + 1], doc.blocks[i + 2]
|
||||
local ends = after == nil or chapter_anchor(after) ~= nil or is_placeholder(after, TOC_ID)
|
||||
or is_placeholder(after, TITLE_ID)
|
||||
if not seen_chapter and nxt and (nxt.t == "Figure" or nxt.t == "Para") and ends then
|
||||
local imgs, text = {}, false
|
||||
nxt:walk({ Image = function(img) imgs[#imgs + 1] = img end })
|
||||
if #imgs == 1 then
|
||||
if nxt.t == "Para" then
|
||||
for _, inl in ipairs(nxt.content) do
|
||||
if inl.t ~= "Image" and inl.t ~= "Space" and inl.t ~= "SoftBreak" then
|
||||
text = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if #imgs == 1 and not text then
|
||||
doc.blocks[i + 1] = pandoc.Div({ pandoc.Plain({ imgs[1] }) }, pandoc.Attr("", { "cover" }))
|
||||
end
|
||||
end
|
||||
seen_chapter = true
|
||||
else
|
||||
blocks:insert(block)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -185,3 +185,18 @@ def test_special_chapter_is_not_split_further(convert):
|
|||
"<h1>Register</h1><h2>A</h2><p>Apfel</p><h2>Index</h2><p>x</p>")
|
||||
run = convert(Book(docs=[Doc("a.xhtml", body)]))
|
||||
assert [p.name for p in run.chapters()] == ["001-literatur.md", "002-register.md"]
|
||||
|
||||
|
||||
def test_first_image_only_chapter_is_cover_and_titel_is_title_page(convert):
|
||||
from conftest import PNG
|
||||
docs = [Doc("info.xhtml", '<div><img src="c.png" alt=""/></div>', "[Titelinfo]"),
|
||||
Doc("t.xhtml", "<p>Erika Musterfrau</p><p>Testbuch</p>", "Titel"),
|
||||
Doc("d.xhtml", '<div><img src="c.png" alt="Deckblatt"/></div>', "Deckblatt"),
|
||||
Doc("c1.xhtml", "<h1>Eins</h1><p>Text</p>")]
|
||||
run = convert(Book(docs=docs, nav=[("Eins", "c1.xhtml")], media={"c.png": PNG}))
|
||||
types = {c["file"]: c["types"] for c in run.report["chapters"]}
|
||||
first, second, third = sorted(types)[:3]
|
||||
assert "cover" in types[first]
|
||||
assert "titlepage" in types[second]
|
||||
assert "cover" in types[third] # 'Deckblatt' by title
|
||||
assert "cover" not in types[sorted(types)[3]]
|
||||
|
|
|
|||
|
|
@ -118,3 +118,21 @@ def test_merge_own_toc_replaces_placeholder(convert):
|
|||
assert merge_main([str(run.out), "--toc"]) == 0
|
||||
text = (run.out / f"{run.report['input']['name'][:-5]}.md").read_text(encoding="utf-8")
|
||||
assert "epub2md-toc" not in text and text.index("© 2024") < text.index("## Inhalt") < text.index("# Kapitel A")
|
||||
|
||||
|
||||
def test_merge_front_matter_region_with_gaps(convert):
|
||||
"""Krooss/Blake-like: short untyped pages between title page and imprint; main matter starts at '1 …'."""
|
||||
docs = [Doc("cover.xhtml", '<div><img src="img/a.png" alt="Cover"/></div>'),
|
||||
Doc("titel.xhtml", "<p>Erika Musterfrau</p><p>Vorspann</p><p>Roman</p>"),
|
||||
Doc("auflage.xhtml", "<h1>Vorspann</h1><p>4. Auflage 2025</p>"),
|
||||
Doc("impressum.xhtml", "<p>© 2024 Testverlag</p><p>Das Werk ist urheberrechtlich geschützt.</p>"),
|
||||
Doc("brief.xhtml", "<h1>Liebe Leserin, lieber Leser,</h1><p>Viel Spaß.</p>"),
|
||||
Doc("k1.xhtml", "<h1>1 Der Einstieg</h1><p>a</p>"), Doc("k2.xhtml", "<h1>Zwischenspiel</h1><p>b</p>")]
|
||||
book = Book(title="Vorspann", docs=docs, nav=[("1 Der Einstieg", "k1.xhtml"), ("Zwischenspiel", "k2.xhtml")],
|
||||
media={"img/a.png": PNG})
|
||||
run = convert(book)
|
||||
assert merge_main([str(run.out)]) == 0
|
||||
text = (run.out / f"{run.report['input']['name'][:-5]}.md").read_text(encoding="utf-8")
|
||||
toc = text.index('<div id="epub2md-toc"></div>')
|
||||
assert text.index("© 2024") < text.index("Viel Spaß.") < toc < text.index("# 1 Der Einstieg")
|
||||
assert text.count("epub2md-toc") == 1 and "epub2md-titlepage" not in text
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ def test_qa2_10_book_readme_recommends_pdf_via_html(convert):
|
|||
"""Pandoc drops raw HTML (tables, <a id>) when going Markdown -> LaTeX; the HTML route keeps both."""
|
||||
run = convert(Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>x</p>")]))
|
||||
readme = (run.out / "README.md").read_text(encoding="utf-8")
|
||||
assert re.search(r"pandoc \S+\.md -s -o \S+\.html && pandoc \S+\.html --toc --pdf-engine=xelatex -o \S+\.pdf", readme)
|
||||
book = r"--lua-filter \S+/pandoc/book\.lua" # table of contents after the front matter (both steps)
|
||||
assert re.search(rf"pandoc \S+\.md -s {book} -o \S+\.html && pandoc \S+\.html {book} --pdf-engine=xelatex -o \S+\.pdf",
|
||||
readme)
|
||||
|
||||
|
||||
def test_qa2_11_code_language_hint(convert):
|
||||
|
|
|
|||
Loading…
Reference in a new issue