epub2md/pandoc/book.lua
dschlueter 86aa93c12c 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>
2026-10-06 06:48:18 +02:00

270 lines
10 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[
book.lua – Pandoc filter for book files from epub2md_merge.py (HTML output, e.g. PDF via weasyprint).
Pandoc's --toc always puts the table of contents (and its own title block) before the whole text,
i.e. before the book's cover, title page and imprint. epub2md_merge.py marks the right places:
<div id="epub2md-titlepage"></div> after the cover, only if the book has no title page of its own
<div id="epub2md-toc"></div> after the front matter (cover, title page, imprint …)
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 (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 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 \
--lua-filter /pfad/zu/epub2md/pandoc/callouts.lua --css /pfad/zu/epub2md/pandoc/print.css \
--pdf-engine=weasyprint -o buch.pdf
]]
local TOC_ID, TITLE_ID = "epub2md-toc", "epub2md-titlepage"
local TOC_TITLE = { de = "Inhaltsverzeichnis", en = "Contents" }
local function is_placeholder(block, id)
return block.t == "Div" and block.identifier == id and #block.content == 0
end
local function esc(s)
return (s:gsub("&", "&amp;"):gsub("<", "&lt;"):gsub(">", "&gt;"))
end
-- <header id="title-block-header"> as Pandoc's HTML template writes it.
local function title_block(meta)
local html = { '<header id="title-block-header">', '<h1 class="title">' .. esc(pandoc.utils.stringify(meta.title)) .. "</h1>" }
if meta.subtitle then
html[#html + 1] = '<p class="subtitle">' .. esc(pandoc.utils.stringify(meta.subtitle)) .. "</p>"
end
local authors = meta.author
if authors and pandoc.utils.type(authors) ~= "List" then
authors = { authors }
end
for _, a in ipairs(authors or {}) do
html[#html + 1] = '<p class="author">' .. esc(pandoc.utils.stringify(a)) .. "</p>"
end
if meta.date then
html[#html + 1] = '<p class="date">' .. esc(pandoc.utils.stringify(meta.date)) .. "</p>"
end
html[#html + 1] = "</header>"
return pandoc.RawBlock("html", table.concat(html, "\n"))
end
-- Paragraph that only holds chapter start anchors (<a id="001-cover"></a>) → id of the first one.
local function chapter_anchor(block)
if block.t ~= "Para" and block.t ~= "Plain" then
return nil
end
local id = nil
for _, inl in ipairs(block.content) do
if inl.t ~= "RawInline" then
return nil
end
local found = inl.text:match('^<a id="(%d%d%d%-[^"]*)">$') or inl.text:match('^<a id="(%d%d%d%-[^"]*)"></a>$')
if found then
id = id or found
elseif inl.text ~= "</a>" then
return nil
end
end
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
if is_placeholder(block, TOC_ID) then
toc_at = i
elseif is_placeholder(block, TITLE_ID) then
title_at = i
end
end
if not toc_at and not title_at then
return nil -- not a file from epub2md_merge.py
end
if not html then -- placeholders are top-level blocks: just drop them
local kept = pandoc.Blocks({})
for _, block in ipairs(doc.blocks) do
if not is_placeholder(block, TOC_ID) and not is_placeholder(block, TITLE_ID) then
kept:insert(block)
end
end
doc.blocks = kept
return 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
if PANDOC_WRITER_OPTIONS.table_of_contents then
-- --toc given as well: Pandoc's own table of contents stays at the top, no second one
else
local lang = doc.meta.lang and pandoc.utils.stringify(doc.meta.lang):sub(1, 2):lower() or "de"
local toc = pandoc.structure.table_of_contents(doc.blocks, PANDOC_WRITER_OPTIONS)
blocks:insert(pandoc.Div({
pandoc.Header(1, TOC_TITLE[lang] or TOC_TITLE.de, pandoc.Attr("toc-title", { "unnumbered", "unlisted" })),
toc,
}, pandoc.Attr("TOC", { "toc" })))
end
elseif i == title_at then
if doc.meta.title then
blocks:insert(title_block(doc.meta))
end
elseif anchor then
blocks:insert(pandoc.Div({}, pandoc.Attr(anchor, { "chapter-start" })))
-- 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 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
end
doc.blocks = blocks
-- The book's own title page (or the moved block) replaces Pandoc's title block at the very top.
if doc.meta.title then
doc.meta.pagetitle = doc.meta.pagetitle or pandoc.utils.stringify(doc.meta.title)
doc.meta.title = nil
end
return doc
end