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:
Dieter Schlüter 2026-10-06 06:48:18 +02:00
commit 86aa93c12c

View file

@ -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