-- 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", '
' .. labels.hint .. " " .. i .. '
')) blocks:extend(h) blocks:insert(pandoc.RawBlock("html", "
")) end if sol ~= nil then blocks:insert(pandoc.RawBlock("html", '
' .. labels.solution .. '
')) blocks:extend(sol) blocks:insert(pandoc.RawBlock("html", "
")) 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 }, }