From 0ab311f66d6f3cb0b4d87c47e082d53fd67bb502 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Sat, 17 Oct 2026 20:26:49 +0200 Subject: support writing moodle tests in markdown --- pandoc/filters/moodle-quiz.lua | 140 +++++++++++++++++++++++++++++++++++++++ pandoc/templates/moodle-quiz.xml | 15 +++++ 2 files changed, 155 insertions(+) create mode 100644 pandoc/filters/moodle-quiz.lua create mode 100644 pandoc/templates/moodle-quiz.xml (limited to 'pandoc') diff --git a/pandoc/filters/moodle-quiz.lua b/pandoc/filters/moodle-quiz.lua new file mode 100644 index 00000000..ab26f93c --- /dev/null +++ b/pandoc/filters/moodle-quiz.lua @@ -0,0 +1,140 @@ +--- moodle-test.lua – filter to convert markdown questions into Moodle XML +--- +--- Copyright: © 2026 Florian Fischer +--- License: GPL-3.0-or-later + +-- Collected questions +local questions = {} +local cur_question = nil + +local concat = function(inlines) + local s = pandoc.write(pandoc.Pandoc(pandoc.Plain(inlines)), 'plain', {wrap_text='wrap-none'}) + if s:sub(-1) == '\n' then + s = s:sub(1, -2) + end + return s +end + +local function detect_question_type(question) + local ret = "" + if question.header.classes:includes("shortanswer") then + ret = "shortanswer" + elseif question.header.classes:includes("multichoice") then + ret = "multichoice" + else + pandoc.warn("No question type found") + end + return ret +end + +local function write_shortanwser_answers(question) + local ret = "" + local a = function(s) ret = ret .. s end + + for _, answer in ipairs(question.answers) do + a(' \n') + a(' \n') + + if answer.feedback then + a(' \n') + end + a(' \n') + end + return ret +end + +local write_answers_for = { + shortanswer = write_shortanwser_answers, +} + +local function write_questions() + local res = "" + local a = function(s) res = res .. s end + + for _, question in ipairs(questions) do + if not question then + pandoc.log.warn("nil question") + end + local question_type = detect_question_type(question) + -- Question type + a('\n') + + -- Question name + a(' '); + a(concat(question.name)) + a('\n') + + -- Question text + a(' \n \n \n') + a(' \n') + a(' 1.0000000\n') + a(' 0.3333333\n') + a(' 0\n') + a(' \n') + a(' 0\n') + + local answer_fn = write_answers_for[question_type] + a(answer_fn(question)) + a('\n') + end + + return res +end + +local function Header(h) + cur_question = { header=h, text={}} + table.insert(questions, cur_question) + + cur_question.name = h.content +end + +local function Image(i) + local datauri = pandoc.mediabag.make_data_uri(pandoc.mediabag.fetch(i.src)) + return pandoc.Image(i.caption, datauri) +end + +local function Div(d) + -- append question content + table.insert(cur_question.text, pandoc.walk_block(d, {Image=Image})) -- embed all images +end + +local function BulletList(l) + cur_question.answers = l.content +end + +local filter = { + -- Traverse the AST depth-first to reach the content of a question after + -- reaching its header + traverse = 'topdown', + Header = Header, -- Detect questions + Div = Div, -- Collect question content + BulletList = BulletList, -- Collect question answers +} + +Writer = function(doc, opts) + local ctx = pandoc.template.meta_to_context(doc.meta, concat, concat) + + -- collect questions + doc:walk(filter) + local body = write_questions() + + ctx['body'] = body + for k, v in pairs(opts.variables) do + ctx[k] = v + end + + if not opts.template then + pandoc.log.warn("No quiz template specified") + os.exit(1) + end + local complete = pandoc.template.apply(opts.template, ctx) + return complete:render() +end diff --git a/pandoc/templates/moodle-quiz.xml b/pandoc/templates/moodle-quiz.xml new file mode 100644 index 00000000..761c8669 --- /dev/null +++ b/pandoc/templates/moodle-quiz.xml @@ -0,0 +1,15 @@ + + + + + + $$module$$/top/Standard für ${title} + + + Standardkategorie für Fragen, die im Kontext '${title}' freigegeben sind. + + + + +${body} + -- cgit v1.2.3