epub2md/pandoc/callouts.lua
dschlueter c761a53728 fix: Kasten-CSS des Pandoc-Filters wirkt auch mit -H und in EPUB-Kapiteln
- CSS in pandoc/callouts.css (eine Quelle), vom Filter neben sich gelesen
- HTML: <style> am Dokumentanfang statt header-includes, das ein -H auf der
  Kommandozeile ersetzt hat (PDF über weasyprint mit Druck-CSS: Kästen ohne Rahmen)
- EPUB: Kasten-CSS als Stylesheet aller Kapitel (zusammen mit Pandocs
  Standard-epub.css oder zu eigenen css:-Angaben); vorher nur auf der Titelseite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:55:25 +02:00

174 lines
5.2 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.

--[[
callouts.lua – Pandoc filter for the info boxes written by epub2md (--boxes callout).
epub2md writes boxes as GitHub alerts / Obsidian callouts:
> [!TIP]
> **Title**
>
> Text …
Pandoc < 3.1.10 does not know this syntax and prints "[!TIP]" literally. This filter turns such
block quotes into real boxes:
* HTML, EPUB: <div class="callout callout-tip"> with a bold label, styled by callouts.css
(next to this filter; HTML: <style> at the start of the body, so -H cannot drop it;
EPUB: added to the book's stylesheets together with Pandoc's default epub.css)
* other formats (LaTeX/PDF, DOCX, ODT …): block quote with a bold label "Tipp: Title"
It also handles <div class="callout …"> coming back from HTML (md -> html -> pdf route) and styles
the <div class="box …"> blocks of --boxes html.
Usage:
pandoc buch.md -s --lua-filter /pfad/zu/epub2md/pandoc/callouts.lua -o buch.html
]]
local LABELS = {
de = { NOTE = "Hinweis", TIP = "Tipp", IMPORTANT = "Wichtig", WARNING = "Warnung", CAUTION = "Vorsicht" },
en = { NOTE = "Note", TIP = "Tip", IMPORTANT = "Important", WARNING = "Warning", CAUTION = "Caution" },
}
-- callouts.css next to this filter is the single source of the box styles.
local function read_file(path)
local fh = io.open(path, "r")
if not fh then
return nil
end
local text = fh:read("a")
fh:close()
return text
end
local CSS_PATH = (PANDOC_SCRIPT_FILE and PANDOC_SCRIPT_FILE:match("^(.*[/\\])") or "") .. "callouts.css"
local CSS = read_file(CSS_PATH)
if not CSS then
io.stderr:write("callouts.lua: " .. CSS_PATH .. " nicht gefunden – Kästen ohne Rahmen\n")
end
-- EPUB: a stylesheet in metadata `css` replaces Pandoc's default epub.css, so combine both in a temp file.
local function epub_stylesheet()
local default = ""
local pipe = io.popen("pandoc --print-default-data-file epub.css 2>/dev/null")
if pipe then
default = pipe:read("a") or ""
pipe:close()
end
local path = os.tmpname() -- small temp file; Pandoc reads it after the filter has finished
local fh = io.open(path, "w")
if not fh then
return nil
end
fh:write(default, "\n", CSS)
fh:close()
return path
end
local lang = "de"
local function is_html()
return FORMAT:match("html") ~= nil or FORMAT:match("epub") ~= nil
end
local function label_for(kind)
local labels = LABELS[lang] or LABELS.de
return labels[kind] or labels.NOTE
end
local function drop_leading_breaks(inlines)
while #inlines > 0 and (inlines[1].t == "SoftBreak" or inlines[1].t == "LineBreak" or inlines[1].t == "Space") do
inlines:remove(1)
end
return inlines
end
-- Build the box from its kind ("TIP" …), optional title inlines and body blocks.
local function make_box(kind, title, body)
local head = pandoc.Inlines({ pandoc.Str(label_for(kind)) })
if title and #title > 0 then
head:insert(pandoc.Str(":"))
head:insert(pandoc.Space())
head:extend(title)
end
local blocks = pandoc.Blocks({ pandoc.Para({ pandoc.Strong(head) }) })
blocks:extend(body)
if is_html() then
return pandoc.Div(blocks, pandoc.Attr("", { "callout", "callout-" .. kind:lower() }))
end
return pandoc.BlockQuote(blocks)
end
local function Meta(meta)
if meta.lang then
local l = pandoc.utils.stringify(meta.lang):lower():sub(1, 2)
if LABELS[l] then
lang = l
end
end
if CSS and FORMAT:match("epub") then
local css = meta.css
local path = nil
if css == nil then -- no own stylesheet: default epub.css + ours; otherwise just add ours
path = epub_stylesheet()
end
if css == nil then
css = pandoc.List({})
elseif pandoc.utils.type(css) ~= "List" then
css = pandoc.List({ css })
end
if path then
css:insert(pandoc.MetaString(path))
else
css:insert(pandoc.MetaString(CSS_PATH))
end
meta.css = css
end
return meta
end
local function BlockQuote(quote)
local first = quote.content[1]
if not first or first.t ~= "Para" or #first.content == 0 or first.content[1].t ~= "Str" then
return nil
end
local kind = first.content[1].text:match("^%[!(%a+)%]$")
if not kind then
return nil
end
kind = kind:upper()
local rest = drop_leading_breaks(first.content:clone())
rest:remove(1)
rest = drop_leading_breaks(rest)
local body = pandoc.Blocks({})
for i = 2, #quote.content do
body:insert(quote.content[i])
end
local title = nil
if #rest == 1 and rest[1].t == "Strong" then
title = rest[1].content -- "> **Title**" line
elseif #rest > 0 then
body:insert(1, pandoc.Para(rest)) -- text directly after the marker
end
return make_box(kind, title, body)
end
-- <div class="callout callout-tip"> read back from HTML: keep it as a box in non-HTML formats.
local function Div(div)
if not div.classes:includes("callout") or is_html() then
return nil
end
return pandoc.BlockQuote(div.content)
end
-- HTML: the stylesheet goes into the body, because a command-line -H replaces header-includes from a filter.
local function Pandoc(doc)
if CSS and FORMAT:match("html") and not FORMAT:match("epub") then
doc.blocks:insert(1, pandoc.RawBlock("html", "<style>\n" .. CSS .. "</style>"))
end
return doc
end
return {
{ Meta = Meta },
{ BlockQuote = BlockQuote, Div = Div },
{ Pandoc = Pandoc },
}