diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2026-10-17 20:26:49 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2026-10-17 22:23:21 +0200 |
| commit | 0ab311f66d6f3cb0b4d87c47e082d53fd67bb502 (patch) | |
| tree | 092f3bb6726a234aa49b898bdf677f630f6dd81a | |
| parent | 5a865a67d70efd3d23448a7ac885305b0f58ff5d (diff) | |
| download | unterricht-0ab311f66d6f3cb0b4d87c47e082d53fd67bb502.tar.gz unterricht-0ab311f66d6f3cb0b4d87c47e082d53fd67bb502.zip | |
support writing moodle tests in markdown
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | common.mk | 4 | ||||
| -rw-r--r-- | pandoc/filters/moodle-quiz.lua | 140 | ||||
| -rw-r--r-- | pandoc/templates/moodle-quiz.xml | 15 | ||||
| -rwxr-xr-x | scripts/lesson-make | 4 |
5 files changed, 164 insertions, 1 deletions
@@ -15,6 +15,8 @@ test_loesung.pdf sequence.pdf *print.pdf *_printout.pdf +moodle-quiz.xml +!pandoc/templates/moodle-quiz.xml .build/ .owncloudsync.log* .sync_journal.db* @@ -70,6 +70,10 @@ schedule.pdf: schedule.md $(MAKEFILE_LIST) @echo "building $@" $(VERBOSE)$(PANDOC) --pdf-engine-opt=--fmt=$(LATEX_FMT_BASE)/schedule.fmt -f markdown $< -o $@ +moodle-quiz.xml: moodle-quiz.md $(wildcard img/*) $(MAKEFILE_LIST) + @echo "building $@" + $(VERBOSE)$(PANDOC) --template=$(PANDOC_TEMPLATE_DIR)/moodle-quiz.xml -t $(PANDOC_FILTER_DIR)/moodle-quiz.lua --embed-resources $< -o $@ + %.pdf: %.md $(MAKEFILE_LIST) @echo "building $@" $(VERBOSE)$(PANDOC) -f markdown $< -o $@ 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> diff --git a/scripts/lesson-make b/scripts/lesson-make index d5639b7a..3c6a4890 100755 --- a/scripts/lesson-make +++ b/scripts/lesson-make @@ -22,6 +22,9 @@ if [[ -z "${targets[*]}" ]]; then slides*.md) new_targets+=("${source_name/.md/.pdf}" "${source_name/.md/.pptx}") ;; + moodle-quiz.md) + new_targets+=("${source_name/.md/.xml}") + ;; *) mapfile -t new_targets < <(echo "${source}" | sed -e 's/\.tex$/.pdf/g' -e 's/\.md$/.pdf/g') ;; @@ -43,4 +46,3 @@ if [[ -z "${targets[*]}" ]]; then fi exec make -f "${MAKEFILE}" "${targets[@]}" - |
