feat: Inhaltsverzeichnis nach Cover, Titelseite und Impressum (pandoc/book.lua)

Pandocs --toc setzt Inhaltsverzeichnis und Titelkopf immer vor den
Buchtext, also vor Cover, Titelseite und Impressum.

- epub2md_merge.py: Kapiteltypen aus manifest.json; Platzhalter
  <div id="epub2md-toc"> hinter dem Vorspann und, ohne eigene Titelseite,
  <div id="epub2md-titlepage"> hinter dem Cover; --toc-Liste an derselben Stelle
- pandoc/book.lua (HTML/weasyprint, statt --toc): baut dort das
  Inhaltsverzeichnis, lässt Pandocs Titelkopf bei eigener Titelseite weg,
  Cover ohne Bildunterschrift, Kapitelanker → div.chapter-start;
  andere Formate: nur Platzhalter entfernen
- print.css: jedes Kapitel (auch Cover/Titelseite/Impressum) und das
  Verzeichnis auf neuer Seite, Verzeichnis mit Punktlinie und Seitenzahl
- Buch-README, Bedienungsanleitung, README: Befehle mit book.lua

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-06 06:27:27 +02:00
commit 99c3c609ca
2 changed files with 299 additions and 65 deletions

140
pandoc/book.lua Normal file
View file

@ -0,0 +1,140 @@
--[[
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 picture (chapter "NNN-cover") 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).
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
function Pandoc(doc)
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({})
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" })))
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 = {}
nxt:walk({ Image = function(img) imgs[#imgs + 1] = img end })
if #imgs == 1 then
doc.blocks[i + 1] = pandoc.Div({ pandoc.Plain({ imgs[1] }) }, pandoc.Attr("", { "cover" }))
end
end
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

View file

@ -1,5 +1,7 @@
/* Print layout for PDFs from epub2md output via Pandoc + weasyprint:
pandoc buch.md -s --toc --lua-filter pandoc/callouts.lua --css pandoc/print.css --pdf-engine=weasyprint -o buch.pdf */
pandoc buch.md -s --lua-filter pandoc/book.lua --lua-filter pandoc/callouts.lua --css pandoc/print.css \
--pdf-engine=weasyprint -o buch.pdf
book.lua places the table of contents after cover, title page and imprint and marks chapter starts. */
@page { size: A4; margin: 2cm 1.8cm; @bottom-center { content: counter(page); font-size: 9pt; } }
body { max-width: none; font-size: 10.5pt; line-height: 1.4; hyphens: auto; text-align: justify;
font-family: "DejaVu Serif", "Liberation Serif", Georgia, serif; }
@ -7,13 +9,23 @@ body { max-width: none; font-size: 10.5pt; line-height: 1.4; hyphens: auto; text
h1, h2, h3, h4 { text-align: left; hyphens: manual; page-break-after: avoid; }
pre, code { font-family: "DejaVu Sans Mono", "Liberation Mono", monospace; hyphens: none; }
pre { background: #f6f8fa; padding: 0.5em 0.8em; border-radius: 0.3em; page-break-inside: auto; }
blockquote { margin: 1em 0 1em 1.5em; padding-left: 0.8em; border-left: 2px solid #ccc; color: #444; }
nav#TOC ul { list-style: none; padding-left: 1.2em; }
a { color: inherit; text-decoration: none; }
pre, pre code { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 8.5pt; } /* long code lines wrap */
pre { overflow: visible; }
img { max-width: 100%; height: auto; }
h1 { page-break-before: always; } /* every chapter on a new page */
#title-block-header h1, #TOC h1 { page-break-before: auto; }
nav#TOC { page-break-after: always; }
blockquote { margin: 1em 0 1em 1.5em; padding-left: 0.8em; border-left: 2px solid #ccc; color: #444; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; max-height: 24cm; height: auto; }
table { font-size: 9pt; }
.cover { text-align: center; }
.cover img { max-height: 25cm; }
/* New pages: every chapter (book.lua: div.chapter-start, also cover, title page, imprint), the title
block, the table of contents; without book.lua every <h1>. No second break for the chapter's own <h1>. */
h1, .chapter-start, #title-block-header, #TOC { page-break-before: always; }
.chapter-start + h1, #title-block-header h1, #TOC h1 { page-break-before: auto; }
#TOC { page-break-after: always; }
/* Table of contents with dotted leaders and page numbers. */
#TOC ul { list-style: none; padding-left: 1.2em; margin: 0; }
#TOC > ul { padding-left: 0; }
#TOC li { text-align: left; }
#TOC a::after { content: leader(".") target-counter(attr(href), page); }