--[[ 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:
after the cover, only if the book has no title page of its own
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 into
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("&", "&"):gsub("<", "<"):gsub(">", ">")) end --
as Pandoc's HTML template writes it. local function title_block(meta) local html = { '
', '

' .. esc(pandoc.utils.stringify(meta.title)) .. "

" } if meta.subtitle then html[#html + 1] = '

' .. esc(pandoc.utils.stringify(meta.subtitle)) .. "

" 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] = '

' .. esc(pandoc.utils.stringify(a)) .. "

" end if meta.date then html[#html + 1] = '

' .. esc(pandoc.utils.stringify(meta.date)) .. "

" end html[#html + 1] = "
" return pandoc.RawBlock("html", table.concat(html, "\n")) end -- Paragraph that only holds chapter start anchors () → 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('^$') or inl.text:match('^$') if found then id = id or found elseif inl.text ~= "" 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