Change folder hierarchy
This commit is contained in:
@@ -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 },
|
||||
}
|
||||
Reference in New Issue
Block a user