Make callout headings explicit
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-26 01:44:41 +05:30
parent 0025e453e8
commit b03d27cc3a
20 changed files with 1052 additions and 330 deletions

View file

@ -279,7 +279,8 @@ authorName: "Neeldhara"
- Standard Markdown headings, lists, links, tables, code fences, footnotes, and raw HTML are preserved.
- Use colocated relative assets: \`![Caption](image.png)\`.
- Quarto image attributes such as \`{width=70%}\` are removed during import. Use normal Markdown now; layout should be handled by Astro CSS.
- Quarto callouts are converted to simple blockquotes:
- Quarto callouts are converted to simple blockquotes. The callout type controls
styling only; it does not create a visible heading by default:
\`\`\`markdown
> **Note**
@ -287,14 +288,22 @@ authorName: "Neeldhara"
> Text of the callout.
\`\`\`
- For new notes, use the same blockquote convention for portable callouts:
- For new notes, the bracket form is also supported:
\`\`\`markdown
> **Warning**
> [!warning]
>
> This point needs attention.
\`\`\`
- To show a heading, put it explicitly inside the bracket after \`:\` or \`|\`:
\`\`\`markdown
> [!tip: Useful observation]
>
> Text of the callout.
\`\`\`
- Quarto margin notes such as \`[text]{.aside}\` are converted to:
\`\`\`markdown

View file

@ -101,6 +101,13 @@ local function first_inline_text(block)
return inline_text(block.content[1])
end
local function paragraph_text(block)
if not block or block.t ~= "Para" then
return ""
end
return pandoc.utils.stringify(block)
end
local function latex_escape(value)
value = tostring(value or "")
value = value:gsub("\\", "\\textbackslash{}")
@ -356,6 +363,70 @@ local function strip_text_prefix(block, pattern)
end
end
local function cleanup_inline_start(block)
if not block or block.t ~= "Para" or not block.content then
return
end
while #block.content > 0 do
local first = block.content[1]
if first.t == "Space" or first.t == "SoftBreak" or first.t == "LineBreak" then
table.remove(block.content, 1)
elseif first.t == "Str" then
first.text = first.text:gsub("^%s+", "")
if first.text == "" then
table.remove(block.content, 1)
else
return
end
else
return
end
end
end
local function strip_bracket_callout(block)
if not block or block.t ~= "Para" or not block.content then
return
end
while #block.content > 0 do
local first = block.content[1]
local text = inline_text(first)
local close_index = text:find("%]")
if close_index then
local remaining = text:sub(close_index + 1):gsub("^%s+", "")
if first.t == "Str" and remaining ~= "" then
first.text = remaining
elseif remaining ~= "" then
block.content[1] = pandoc.Str(remaining)
else
table.remove(block.content, 1)
end
break
end
table.remove(block.content, 1)
end
cleanup_inline_start(block)
end
local function parse_bracket_callout(text)
local key, title = text:match("^%s*%[!([%a-]+)%s*[:|]%s*([^%]]+)%]")
if key then
return normalize_label(key), title:gsub("^%s+", ""):gsub("%s+$", "")
end
key = text:match("^%s*%[!([%a-]+)%]")
if key then
return normalize_label(key), ""
end
return nil, nil
end
local function detect_callout(block)
local text = first_inline_text(block)
if text == "" then
@ -365,21 +436,18 @@ local function detect_callout(block)
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" }
return { key = key, title = "", 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
local key, title = parse_bracket_callout(paragraph_text(block))
if key and callout_labels[key] then
return { key = key, title = title or "", remover = "bracket" }
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 }
return { key = item.key, title = "", remover = "marker", marker = item.marker }
end
end
@ -400,7 +468,7 @@ local function callout_to_latex(el)
if callout.remover == "strong" then
remove_first_inline(blocks[1])
elseif callout.remover == "bracket" then
strip_text_prefix(blocks[1], "^%[![%a-]+%]%s*")
strip_bracket_callout(blocks[1])
elseif callout.remover == "marker" then
strip_text_prefix(blocks[1], "^" .. callout.marker .. "%s*")
end
@ -412,7 +480,7 @@ local function callout_to_latex(el)
local env = callout_envs[callout.key] or "blognotebox"
return pandoc.RawBlock(
"latex",
"\\begin{" .. env .. "}{}\n"
"\\begin{" .. env .. "}{" .. latex_escape(callout.title or "") .. "}\n"
.. blocks_to_latex(blocks)
.. "\n\\end{" .. env .. "}"
)

View file

@ -46,49 +46,119 @@
}
}
\newtcolorbox{blognotebox}[2][]{
blog callout,
title={NOTE\if\relax\detokenize{#2}\relax\else: #2\fi},
colback=BlogCalloutNoteBack,
colframe=BlogCalloutNoteFrame,
boxed title style={colback=BlogCalloutNoteBack},
#1
\NewDocumentEnvironment{blognotebox}{O{} m}{%
\if\relax\detokenize{#2}\relax
\begin{tcolorbox}[
blog callout,
colback=BlogCalloutNoteBack,
colframe=BlogCalloutNoteFrame,
boxed title style={colback=BlogCalloutNoteBack},
#1
]%
\else
\begin{tcolorbox}[
blog callout,
title={#2},
colback=BlogCalloutNoteBack,
colframe=BlogCalloutNoteFrame,
boxed title style={colback=BlogCalloutNoteBack},
#1
]%
\fi
}{%
\end{tcolorbox}%
}
\newtcolorbox{blogtipbox}[2][]{
blog callout,
title={TIP\if\relax\detokenize{#2}\relax\else: #2\fi},
colback=BlogCalloutTipBack,
colframe=BlogCalloutTipFrame,
boxed title style={colback=BlogCalloutTipBack},
#1
\NewDocumentEnvironment{blogtipbox}{O{} m}{%
\if\relax\detokenize{#2}\relax
\begin{tcolorbox}[
blog callout,
colback=BlogCalloutTipBack,
colframe=BlogCalloutTipFrame,
boxed title style={colback=BlogCalloutTipBack},
#1
]%
\else
\begin{tcolorbox}[
blog callout,
title={#2},
colback=BlogCalloutTipBack,
colframe=BlogCalloutTipFrame,
boxed title style={colback=BlogCalloutTipBack},
#1
]%
\fi
}{%
\end{tcolorbox}%
}
\newtcolorbox{blogwarningbox}[2][]{
blog callout,
title={WARNING\if\relax\detokenize{#2}\relax\else: #2\fi},
colback=BlogCalloutWarningBack,
colframe=BlogCalloutWarningFrame,
boxed title style={colback=BlogCalloutWarningBack},
#1
\NewDocumentEnvironment{blogwarningbox}{O{} m}{%
\if\relax\detokenize{#2}\relax
\begin{tcolorbox}[
blog callout,
colback=BlogCalloutWarningBack,
colframe=BlogCalloutWarningFrame,
boxed title style={colback=BlogCalloutWarningBack},
#1
]%
\else
\begin{tcolorbox}[
blog callout,
title={#2},
colback=BlogCalloutWarningBack,
colframe=BlogCalloutWarningFrame,
boxed title style={colback=BlogCalloutWarningBack},
#1
]%
\fi
}{%
\end{tcolorbox}%
}
\newtcolorbox{blogcautionbox}[2][]{
blog callout,
title={CAUTION\if\relax\detokenize{#2}\relax\else: #2\fi},
colback=BlogCalloutCautionBack,
colframe=BlogCalloutCautionFrame,
boxed title style={colback=BlogCalloutCautionBack},
#1
\NewDocumentEnvironment{blogcautionbox}{O{} m}{%
\if\relax\detokenize{#2}\relax
\begin{tcolorbox}[
blog callout,
colback=BlogCalloutCautionBack,
colframe=BlogCalloutCautionFrame,
boxed title style={colback=BlogCalloutCautionBack},
#1
]%
\else
\begin{tcolorbox}[
blog callout,
title={#2},
colback=BlogCalloutCautionBack,
colframe=BlogCalloutCautionFrame,
boxed title style={colback=BlogCalloutCautionBack},
#1
]%
\fi
}{%
\end{tcolorbox}%
}
\newtcolorbox{blogasidebox}[2][]{
blog callout,
title={ASIDE\if\relax\detokenize{#2}\relax\else: #2\fi},
colback=BlogCalloutAsideBack,
colframe=BlogCalloutAsideFrame,
boxed title style={colback=BlogCalloutAsideBack},
#1
\NewDocumentEnvironment{blogasidebox}{O{} m}{%
\if\relax\detokenize{#2}\relax
\begin{tcolorbox}[
blog callout,
colback=BlogCalloutAsideBack,
colframe=BlogCalloutAsideFrame,
boxed title style={colback=BlogCalloutAsideBack},
#1
]%
\else
\begin{tcolorbox}[
blog callout,
title={#2},
colback=BlogCalloutAsideBack,
colframe=BlogCalloutAsideFrame,
boxed title style={colback=BlogCalloutAsideBack},
#1
]%
\fi
}{%
\end{tcolorbox}%
}
\ExplSyntaxOn