aboutsummaryrefslogtreecommitdiff
path: root/pandoc
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2026-10-17 20:26:49 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2026-10-17 22:23:21 +0200
commit0ab311f66d6f3cb0b4d87c47e082d53fd67bb502 (patch)
tree092f3bb6726a234aa49b898bdf677f630f6dd81a /pandoc
parent5a865a67d70efd3d23448a7ac885305b0f58ff5d (diff)
downloadunterricht-0ab311f66d6f3cb0b4d87c47e082d53fd67bb502.tar.gz
unterricht-0ab311f66d6f3cb0b4d87c47e082d53fd67bb502.zip
support writing moodle tests in markdown
Diffstat (limited to 'pandoc')
-rw-r--r--pandoc/filters/moodle-quiz.lua140
-rw-r--r--pandoc/templates/moodle-quiz.xml15
2 files changed, 155 insertions, 0 deletions
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(' <answer fraction="100" format="html">\n')
+ a(' <text><![CDATA[');
+ a(pandoc.write(pandoc.Pandoc(answer), "html"))
+ a(']]></text>\n')
+
+ if answer.feedback then
+ a(' <feedback format="html"><text><![CDATA[')
+ a(pandoc.write(pandoc.Pandoc(answer.feedback), "html"))
+ a(']]></text></feedback>\n')
+ end
+ a(' </answer>\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('<question type="' .. question_type .. '">\n')
+
+ -- Question name
+ a(' <name><text>');
+ a(concat(question.name))
+ a('</text></name>\n')
+
+ -- Question text
+ a(' <questiontext format="html">\n <text><![CDATA[')
+ for _, part in ipairs(question.text) do
+ a(pandoc.write(pandoc.Pandoc(part), "html"))
+ end
+ a(']]></text>\n </questiontext>\n')
+ a(' <generalfeedback format="html"><text></text></generalfeedback>\n')
+ a(' <defaultgrade>1.0000000</defaultgrade>\n')
+ a(' <penalty>0.3333333</penalty>\n')
+ a(' <hidden>0</hidden>\n')
+ a(' <idnumber></idnumber>\n')
+ a(' <usecase>0</usecase>\n')
+
+ local answer_fn = write_answers_for[question_type]
+ a(answer_fn(question))
+ a('</question>\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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<quiz>
+<!-- question: 0 -->
+ <question type="category">
+ <category>
+ <text>$$module$$/top/Standard für ${title}</text>
+ </category>
+ <info format="moodle_auto_format">
+ <text>Standardkategorie für Fragen, die im Kontext '${title}' freigegeben sind.</text>
+ </info>
+ <idnumber></idnumber>
+ </question>
+
+${body}
+</quiz>