272 lines
11 KiB
Lua
272 lines
11 KiB
Lua
--[[
|
||
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="kap-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("&", "&"):gsub("<", "<"):gsub(">", ">"))
|
||
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="kap-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="(kap%-%d%d%d%-[^"]*)">$') or inl.text:match('^<a id="(kap%-%d%d%d%-[^"]*)"></a>$')
|
||
or inl.text:match('^<a id="(%d%d%d%-[^"]*)">$') or inl.text:match('^<a id="(%d%d%d%-[^"]*)"></a>$') -- before 0.3.0
|
||
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(pandoc.RawBlock("latex", "\\thispagestyle{empty}")) -- no page number on the cover
|
||
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
|