--[[ 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:
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
coming back from HTML (md -> html -> pdf route) and styles the
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 = [[ ]] 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 --
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 }, }