epub2md/pandoc/callouts.lua
dschlueter f7cceeba25 feat: Pandoc-Filter callouts.lua für Infokästen
Pandoc 3.1.3 kennt '> [!TIP]' nicht und druckt die Marke als Text.
pandoc/callouts.lua macht daraus in HTML/EPUB gerahmte, farbige Kästen
(<div class="callout callout-tip"> + CSS) und in LaTeX/DOCX/ODT einen
Zitatblock mit fettem Titel ("Tipp: …", englisch bei lang: en); auch für
den PDF-Weg über HTML.

- Buch-README: Pandoc-Befehle enthalten --lua-filter, wenn Callouts da sind
- Kastentitel ohne dekorative Symbole (<span class="box_icon">[»]</span>)
- README: css.py/pandoc in der Architektur, veralteter CSS-Hinweis korrigiert

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

132 lines
4.1 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 and a small stylesheet
* 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" },
}
local CSS = [[
<style>
.callout, .box { border: 1px solid #c8ccd2; border-left: 0.35em solid #4a7bd0; border-radius: 0.3em;
background: #f5f8fc; padding: 0.4em 1em; margin: 1em 0; }
.callout > p:first-child { margin-top: 0.4em; }
.callout-tip { border-left-color: #2e9a4f; background: #f3faf5; }
.callout-important { border-left-color: #8250df; background: #f8f5fe; }
.callout-warning { border-left-color: #c58a00; background: #fffaf0; }
.callout-caution { border-left-color: #cf222e; background: #fff5f5; }
</style>
]]
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 is_html() then
local includes = meta["header-includes"]
if includes == nil then
includes = pandoc.MetaList({})
elseif includes.t ~= "MetaList" then
includes = pandoc.MetaList({ includes })
end
includes:insert(pandoc.MetaBlocks({ pandoc.RawBlock("html", CSS) }))
meta["header-includes"] = includes
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
return {
{ Meta = Meta },
{ BlockQuote = BlockQuote, Div = Div },
}