Improve callouts and symbol publishing
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-25 15:48:33 +05:30
parent 6439559017
commit 7aa1fd2372
31 changed files with 1939 additions and 12 deletions

View file

@ -9,6 +9,58 @@ local env_types = {
proof = true,
}
traverse = "topdown"
local callout_labels = {
aside = "Aside",
caution = "Caution",
note = "Note",
tip = "Tip",
warning = "Warning",
}
local callout_envs = {
aside = "blogasidebox",
caution = "blogcautionbox",
note = "blognotebox",
tip = "blogtipbox",
warning = "blogwarningbox",
}
local callout_markers = {
{ marker = "📝", key = "note" },
{ marker = "💡", key = "tip" },
{ marker = "", key = "tip" },
{ marker = "⚠️", key = "warning" },
{ marker = "", key = "warning" },
}
local chess_macros = {
[""] = "\\BlogChessWhite{\\symking}",
[""] = "\\BlogChessWhite{\\symqueen}",
[""] = "\\BlogChessWhite{\\symrook}",
[""] = "\\BlogChessWhite{\\symbishop}",
[""] = "\\BlogChessWhite{\\symknight}",
[""] = "\\BlogChessWhite{\\sympawn}",
[""] = "\\BlogChessBlack{\\symking}",
[""] = "\\BlogChessBlack{\\symqueen}",
[""] = "\\BlogChessBlack{\\symrook}",
[""] = "\\BlogChessBlack{\\symbishop}",
[""] = "\\BlogChessBlack{\\symknight}",
[""] = "\\BlogChessBlack{\\sympawn}",
}
local card_macros = {
[""] = "\\BlogCardBlack{\\ensuremath{\\spadesuit}}",
[""] = "\\BlogCardBlack{\\ensuremath{\\spadesuit}}",
[""] = "\\BlogCardBlack{\\ensuremath{\\clubsuit}}",
[""] = "\\BlogCardBlack{\\ensuremath{\\clubsuit}}",
[""] = "\\BlogCardRed{\\ensuremath{\\heartsuit}}",
[""] = "\\BlogCardRed{\\ensuremath{\\heartsuit}}",
[""] = "\\BlogCardRed{\\ensuremath{\\diamondsuit}}",
[""] = "\\BlogCardRed{\\ensuremath{\\diamondsuit}}",
}
local function has_class(el, class_name)
for _, value in ipairs(el.classes) do
if value == class_name then
@ -27,6 +79,28 @@ local function first_env_class(el)
return nil
end
local function normalize_label(value)
value = tostring(value or ""):lower()
value = value:gsub("^%s+", ""):gsub("%s+$", "")
value = value:gsub("^%[!", ""):gsub("%]$", "")
value = value:gsub(":$", "")
return value
end
local function inline_text(inline)
if not inline then
return ""
end
return pandoc.utils.stringify(inline)
end
local function first_inline_text(block)
if not block or block.t ~= "Para" or not block.content or #block.content == 0 then
return ""
end
return inline_text(block.content[1])
end
local function latex_escape(value)
value = tostring(value or "")
value = value:gsub("\\", "\\textbackslash{}")
@ -36,6 +110,154 @@ local function latex_escape(value)
return value
end
local function codepoint_at(value, index)
return utf8.codepoint(value, index, index)
end
local function codepoint_len(value, index)
local byte = string.byte(value, index)
if not byte then
return 0
elseif byte < 0x80 then
return 1
elseif byte < 0xE0 then
return 2
elseif byte < 0xF0 then
return 3
end
return 4
end
local function char_at(value, index)
local len = codepoint_len(value, index)
return value:sub(index, index + len - 1), index + len
end
local function is_variation_selector(cp)
return cp == 0xFE0E or cp == 0xFE0F
end
local function is_skin_tone(cp)
return cp >= 0x1F3FB and cp <= 0x1F3FF
end
local function is_emoji_base(cp)
return (cp >= 0x1F000 and cp <= 0x1FAFF)
or (cp >= 0x2600 and cp <= 0x27BF)
or cp == 0x00A9
or cp == 0x00AE
or cp == 0x203C
or cp == 0x2049
or cp == 0x2122
or cp == 0x2139
or cp == 0x3030
or cp == 0x303D
or cp == 0x3297
or cp == 0x3299
end
local function emoji_cluster(value, index)
local start = index
local current, next_index = char_at(value, index)
if current == "" then
return "", index
end
index = next_index
while index <= #value do
local cp = codepoint_at(value, index)
if is_variation_selector(cp) or is_skin_tone(cp) then
local _, ni = char_at(value, index)
index = ni
elseif cp == 0x200D then
local _, ni = char_at(value, index)
index = ni
if index <= #value then
local _, after_joined = char_at(value, index)
index = after_joined
end
else
break
end
end
return value:sub(start, index - 1), index
end
local function symbol_macro_for_cluster(cluster)
local cp = utf8.codepoint(cluster, 1, 1)
if not cp then
return nil
end
if chess_macros[cluster] then
return chess_macros[cluster]
end
if card_macros[cluster] then
return card_macros[cluster]
end
if cp >= 0x1F0A0 and cp <= 0x1F0FF then
return "\\BlogEmojiText{" .. cluster .. "}"
end
if is_emoji_base(cp) then
return "\\BlogEmojiText{" .. cluster .. "}"
end
return nil
end
local function symbol_str_to_latex(el)
if not FORMAT:match("latex") then
return nil
end
local text = el.text
local pieces = pandoc.List()
local plain = {}
local changed = false
local index = 1
local function flush_plain()
if #plain > 0 then
pieces:insert(pandoc.Str(table.concat(plain)))
plain = {}
end
end
while index <= #text do
local cp = codepoint_at(text, index)
if cp and (chess_macros[utf8.char(cp)] or card_macros[utf8.char(cp)] or is_emoji_base(cp)) then
local cluster, next_index = emoji_cluster(text, index)
local macro = symbol_macro_for_cluster(cluster)
if macro then
flush_plain()
pieces:insert(pandoc.RawInline("latex", macro))
changed = true
index = next_index
else
local char, ni = char_at(text, index)
plain[#plain + 1] = char
index = ni
end
else
local char, ni = char_at(text, index)
plain[#plain + 1] = char
index = ni
end
end
flush_plain()
if changed then
return pieces
end
return nil
end
local function internal_link_to_cref(el)
if FORMAT:match("latex") and el.target:sub(1, 1) == "#" then
local label = el.target:sub(2)
@ -46,12 +268,24 @@ local function internal_link_to_cref(el)
return nil
end
local function latexize_doc(doc)
return doc:walk({
Link = internal_link_to_cref,
Str = symbol_str_to_latex,
})
end
local function markdown_to_latex(markdown)
local doc = pandoc.read(
markdown or "",
"markdown+fenced_code_attributes+tex_math_dollars+tex_math_single_backslash+footnotes+raw_html"
)
doc = doc:walk({ Link = internal_link_to_cref })
doc = latexize_doc(doc)
return pandoc.write(doc, "latex")
end
local function blocks_to_latex(blocks)
local doc = latexize_doc(pandoc.Pandoc(blocks))
return pandoc.write(doc, "latex")
end
@ -88,6 +322,102 @@ local function interactive_to_latex(el)
)
end
local function remove_first_inline(block)
if not block or block.t ~= "Para" or not block.content or #block.content == 0 then
return
end
table.remove(block.content, 1)
while #block.content > 0 and block.content[1].t == "Space" do
table.remove(block.content, 1)
end
if #block.content > 0 and block.content[1].t == "Str" then
block.content[1].text = block.content[1].text:gsub("^:%s*", "")
if block.content[1].text == "" then
table.remove(block.content, 1)
end
end
end
local function strip_text_prefix(block, pattern)
if not block or block.t ~= "Para" or not block.content or #block.content == 0 then
return
end
local first = block.content[1]
if first.t ~= "Str" then
return
end
first.text = first.text:gsub(pattern, "")
if first.text == "" then
remove_first_inline(block)
end
end
local function detect_callout(block)
local text = first_inline_text(block)
if text == "" then
return nil
end
if block.content[1].t == "Strong" then
local key = normalize_label(text)
if callout_labels[key] then
return { key = key, label = callout_labels[key], remover = "strong" }
end
end
local bracket = text:match("^%[!([%a-]+)%]")
if bracket then
local key = normalize_label(bracket)
if callout_labels[key] then
return { key = key, label = callout_labels[key], remover = "bracket" }
end
end
for _, item in ipairs(callout_markers) do
if text:sub(1, #item.marker) == item.marker then
return { key = item.key, label = callout_labels[item.key], remover = "marker", marker = item.marker }
end
end
return nil
end
local function callout_to_latex(el)
if not FORMAT:match("latex") or not el.content or #el.content == 0 then
return nil
end
local blocks = pandoc.List(el.content)
local callout = detect_callout(blocks[1])
if not callout then
return nil
end
if callout.remover == "strong" then
remove_first_inline(blocks[1])
elseif callout.remover == "bracket" then
strip_text_prefix(blocks[1], "^%[![%a-]+%]%s*")
elseif callout.remover == "marker" then
strip_text_prefix(blocks[1], "^" .. callout.marker .. "%s*")
end
if blocks[1] and blocks[1].t == "Para" and #blocks[1].content == 0 then
table.remove(blocks, 1)
end
local env = callout_envs[callout.key] or "blognotebox"
return pandoc.RawBlock(
"latex",
"\\begin{" .. env .. "}{}\n"
.. blocks_to_latex(blocks)
.. "\n\\end{" .. env .. "}"
)
end
local function env_to_latex(el)
local env_type = el.attributes.type or first_env_class(el)
if not env_type or not env_types[env_type] then
@ -126,6 +456,14 @@ function CodeBlock(el)
return nil
end
function BlockQuote(el)
return callout_to_latex(el)
end
function Link(el)
return internal_link_to_cref(el)
end
function Str(el)
return symbol_str_to_latex(el)
end