init commit (base extensions: notes)

This commit is contained in:
2026-09-14 07:59:32 +02:00
parent b0b859d20b
commit 7178558330
16 changed files with 2130 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
-- exo.lua — numbered exercise environments for Quarto (HTML + PDF)
--
-- :::: {.exo title="Divergence practice"}
-- Statement, figures, anything...
--
-- ::: {.hint}
-- First hint.
-- :::
--
-- ::: {.hint}
-- Second hint.
-- :::
--
-- ::: {.solution}
-- Full solution.
-- :::
-- ::::
--
-- Numbering is chapter.n where "chapter" counts level-1 headers (#) and n
-- resets per chapter (1.1, 1.2, 2.1, ...). Without any level-1 header the
-- number is just n.
--
-- HTML: hints are numbered folds that unlock one by one; the solution is a
-- fold, closed by default.
-- PDF: the statement stays in place, hints become small highlight boxes,
-- solutions are collected into an unnumbered "Solutions" section at
-- the end of each chapter, cross-linked both ways with page refs.
--
-- Labels are configurable via metadata:
-- exo:
-- label: "Exercice"
-- hint: "Indice"
-- solution: "Solution"
-- solutions: "Solutions"
local labels = {
label = "Exercise",
hint = "Hint",
solution = "Solution",
solutions = "Solutions",
}
local function fmt_is(name)
if quarto and quarto.doc and quarto.doc.is_format then
return quarto.doc.is_format(name)
end
return FORMAT ~= nil and FORMAT:match(name) ~= nil
end
------------------------------------------------------------------ meta
local function read_meta(meta)
if meta.exo ~= nil then
for k in pairs(labels) do
if meta.exo[k] ~= nil then
labels[k] = pandoc.utils.stringify(meta.exo[k])
end
end
end
if quarto ~= nil and quarto.doc ~= nil then
if fmt_is("html") then
quarto.doc.add_html_dependency({
name = "calc-exo",
version = "1.0.0",
stylesheets = { "exo.css" },
scripts = { "exo.js" },
})
elseif fmt_is("latex") then
quarto.doc.use_latex_package("tcolorbox")
end
end
return meta
end
------------------------------------------------------------------ pieces
-- Quarto pre-processes ::: {.solution} (and .proof/.remark) into an
-- internal custom node before user filters run. Inside an .exo we take
-- such a node as the solution and unwrap its scaffold to get the body.
local function custom_proof_body(div)
for _, sc in ipairs(div.content) do
if sc.t == "Div" and sc.attributes.__quarto_custom_scaffold ~= nil
and #sc.content > 0 then
local inner = sc.content
if #inner == 1 and inner[1].t == "Div" and #inner[1].classes == 0 then
return inner[1].content
end
return inner
end
end
return div.content
end
local function split_exo(el)
local stmt, hints, sol = pandoc.Blocks({}), {}, nil
for _, b in ipairs(el.content) do
if b.t == "Div" and b.classes:includes("hint") then
hints[#hints + 1] = b.content
elseif b.t == "Div" and (b.classes:includes("solution")
or b.classes:includes("sol")) then
sol = b.content
elseif b.t == "Div" and b.attributes.__quarto_custom_type == "Proof" then
sol = custom_proof_body(b)
else
stmt:insert(b)
end
end
return stmt, hints, sol
end
local function head_inlines(num, title)
local ins = pandoc.Inlines({ pandoc.Str(labels.label .. " " .. num) })
if title ~= nil then
ins:insert(pandoc.Str("\u{2002}\u{2002}"))
ins:insert(pandoc.Emph(pandoc.Inlines({ pandoc.Str(title) })))
end
return ins
end
------------------------------------------------------------------ html
local function html_exo(num, title, stmt, hints, sol)
local blocks = pandoc.Blocks({})
blocks:insert(pandoc.Div(
pandoc.Blocks({ pandoc.Para(pandoc.Inlines({
pandoc.Strong(head_inlines(num, title)) })) }),
pandoc.Attr("", { "exo-head" })))
blocks:extend(stmt)
for i, h in ipairs(hints) do
local locked = (i > 1) and " exo-locked" or ""
blocks:insert(pandoc.RawBlock("html",
'<details class="exo-hint' .. locked .. '"><summary>'
.. labels.hint .. " " .. i .. '</summary><div class="exo-fold-body">'))
blocks:extend(h)
blocks:insert(pandoc.RawBlock("html", "</div></details>"))
end
if sol ~= nil then
blocks:insert(pandoc.RawBlock("html",
'<details class="exo-sol"><summary>' .. labels.solution
.. '</summary><div class="exo-fold-body">'))
blocks:extend(sol)
blocks:insert(pandoc.RawBlock("html", "</div></details>"))
end
return pandoc.Div(blocks, pandoc.Attr("exo-" .. num, { "exo" }))
end
------------------------------------------------------------------ latex
local function latex_exo(num, title, stmt, hints, sol)
local out = pandoc.Blocks({})
local head = pandoc.Inlines({
pandoc.RawInline("latex", "\\hypertarget{exo-" .. num .. "}{}"),
pandoc.Strong(head_inlines(num, title)),
})
out:insert(pandoc.Para(head))
out:extend(stmt)
for i, h in ipairs(hints) do
out:insert(pandoc.RawBlock("latex",
"\\begin{tcolorbox}[colback=notecolor!5!white,colframe=notecolor!40!white,colbacktitle=notecolor!15!white,"
.. "coltitle=black,fonttitle=\\small\\bfseries,title={" .. labels.hint
.. " " .. i .. "},left=1.5mm,right=1.5mm,top=1mm,bottom=1mm]"))
out:extend(h)
out:insert(pandoc.RawBlock("latex", "\\end{tcolorbox}"))
end
local solblocks = nil
if sol ~= nil then
out:insert(pandoc.Para(pandoc.Inlines({
pandoc.RawInline("latex",
"{\\small\\itshape\\hyperlink{sol-" .. num .. "}{" .. labels.solution
.. " " .. num .. " $\\to$ p.\\,\\pageref{sol:" .. num .. "}}}"),
})))
solblocks = pandoc.Blocks({ pandoc.Para(pandoc.Inlines({
pandoc.RawInline("latex",
"\\hypertarget{sol-" .. num .. "}{}\\label{sol:" .. num .. "}"),
pandoc.Strong(pandoc.Inlines({ pandoc.Str(labels.solution .. " " .. num) })),
pandoc.Str("\u{2002}"),
pandoc.RawInline("latex",
"{\\small(\\hyperlink{exo-" .. num .. "}{$\\leftarrow$ "
.. labels.label .. " " .. num .. "})}"),
})) })
solblocks:extend(sol)
end
return out, solblocks
end
------------------------------------------------------------------ walk
local function walk(doc)
local is_latex = fmt_is("latex")
local is_html = fmt_is("html")
local chapter, cnt = 0, 0
local pending = {}
local out = pandoc.Blocks({})
local function flush()
if #pending == 0 then return end
out:insert(pandoc.Header(2,
pandoc.Inlines({ pandoc.Str(labels.solutions) }),
pandoc.Attr("", { "unnumbered" })))
for _, s in ipairs(pending) do out:extend(s) end
pending = {}
end
for _, b in ipairs(doc.blocks) do
if b.t == "Header" and b.level == 1 then
if is_latex then flush() end
if not b.classes:includes("unnumbered") then -- prefaces etc. don't count
chapter = chapter + 1
cnt = 0
end
out:insert(b)
elseif b.t == "Div" and b.classes:includes("exo") then
cnt = cnt + 1
local num = chapter > 0 and (chapter .. "." .. cnt) or tostring(cnt)
local stmt, hints, sol = split_exo(b)
if is_latex then
local inplace, solb = latex_exo(num, b.attributes.title, stmt, hints, sol)
out:extend(inplace)
if solb ~= nil then pending[#pending + 1] = solb end
elseif is_html then
out:insert(html_exo(num, b.attributes.title, stmt, hints, sol))
else
out:insert(b)
end
else
out:insert(b)
end
end
if is_latex then flush() end
doc.blocks = out
return doc
end
return {
{ Meta = read_meta },
{ Pandoc = walk },
}