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
1 changed files with 257 additions and 46 deletions
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue