9

I am familiar with the package moodle. I however have a question in a reverse direction.

Is there a method or tool to convert xml moodle question banks to LaTeX?

Edit: A sample of an xml file is like below

<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0  -->
  <question type="category">
    <category>
      <text>$course$/top/Default for my course</text>
    </category>
    <info format="moodle_auto_format">
      <text>The default category for questions shared in context 'my course'.</text>
    </info>
    <idnumber></idnumber>
  </question>
<!-- question: 137829  -->
  <question type="essay">
    <name>
      <text>Vector space problem</text>
    </name>
    <questiontext format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">Prove that a vector space cannot be written as a union of two proper subspaces.<br></p>]]></text>
    </questiontext>
    <generalfeedback format="html">
      <text><![CDATA[If we replace 'two' by 'three" the assertion may not be true.<br>]]></text>
    </generalfeedback>
    <defaultgrade>7.0000000</defaultgrade>
    <penalty>0.0000000</penalty>
    <hidden>0</hidden>
    <idnumber></idnumber>
    <responseformat>editorfilepicker</responseformat>
    <responserequired>0</responserequired>
    <responsefieldlines>15</responsefieldlines>
    <attachments>-1</attachments>
    <attachmentsrequired>0</attachmentsrequired>
    <maxbytes>0</maxbytes>
    <filetypeslist>.odt,.pdf</filetypeslist>
    <graderinfo format="html">
      <text><![CDATA[The assertion is true for groups as well.<br>]]></text>
    </graderinfo>
    <responsetemplate format="html">
      <text></text>
    </responsetemplate>
  </question>

<!-- question: 128217  -->
  <question type="multichoice">
    <name>
      <text>Querstion 2</text>
    </name>
    <questiontext format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">The number of the subgroups of the alternating group \( A_4 \) is:<br></p>]]></text>
    </questiontext>
    <generalfeedback format="html">
      <text><![CDATA[\( A_4 \) is the subgroup of the symmetric group \( S_4 \) consisting of even permutations.<br>]]></text>
    </generalfeedback>
    <defaultgrade>10.0000000</defaultgrade>
    <penalty>0.3333333</penalty>
    <hidden>0</hidden>
    <idnumber></idnumber>
    <single>true</single>
    <shuffleanswers>true</shuffleanswers>
    <answernumbering>abc</answernumbering>
    <showstandardinstruction>0</showstandardinstruction>
    <correctfeedback format="html">
      <text>Your answer is correct.</text>
    </correctfeedback>
    <partiallycorrectfeedback format="html">
      <text>Your answer is partially correct.</text>
    </partiallycorrectfeedback>
    <incorrectfeedback format="html">
      <text>Your answer is incorrect.</text>
    </incorrectfeedback>
    <shownumcorrect/>
    <answer fraction="0" format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">8<br></p>]]></text>
      <feedback format="html">
        <text><![CDATA[<p dir="ltr" style="text-align: left;">oops!<br></p>]]></text>
      </feedback>
    </answer>
    <answer fraction="0" format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">9<br></p>]]></text>
      <feedback format="html">
        <text><![CDATA[<p dir="ltr" style="text-align: left;">oops!<br></p>]]></text>
      </feedback>
    </answer>
    <answer fraction="100" format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">10<br></p>]]></text>
      <feedback format="html">
        <text><![CDATA[<p dir="ltr" style="text-align: left;">Yes!<br></p>]]></text>
      </feedback>
    </answer>
    <answer fraction="0" format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">11<br></p>]]></text>
      <feedback format="html">
        <text><![CDATA[<p dir="ltr" style="text-align: left;">oops!<br></p>]]></text>
      </feedback>
    </answer>
  </question>

<!-- question: 128220  -->
  <question type="multichoice">
    <name>
      <text>Question 6</text>
    </name>
    <questiontext format="html">
      <text><![CDATA[What is \( \lim_{n\to\infty}(1+\frac{1}{n})^n \)?<br>]]></text>
    </questiontext>
    <generalfeedback format="html">
      <text></text>
    </generalfeedback>
    <defaultgrade>3.0000000</defaultgrade>
    <penalty>0.3333333</penalty>
    <hidden>0</hidden>
    <idnumber></idnumber>
    <single>true</single>
    <shuffleanswers>true</shuffleanswers>
    <answernumbering>abc</answernumbering>
    <showstandardinstruction>0</showstandardinstruction>
    <correctfeedback format="html">
      <text>Your answer is correct.</text>
    </correctfeedback>
    <partiallycorrectfeedback format="html">
      <text>Your answer is partially correct.</text>
    </partiallycorrectfeedback>
    <incorrectfeedback format="html">
      <text>Your answer is incorrect.</text>
    </incorrectfeedback>
    <shownumcorrect/>
    <answer fraction="100" format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">e<br></p>]]></text>
      <feedback format="html">
        <text><![CDATA[<p dir="ltr" style="text-align: left;">Yes.<br></p>]]></text>
      </feedback>
    </answer>
    <answer fraction="100" format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">exp(1)<br></p>]]></text>
      <feedback format="html">
        <text></text>
      </feedback>
    </answer>
    <answer fraction="80" format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">2.7<br></p>]]></text>
      <feedback format="html">
        <text><![CDATA[<p dir="ltr" style="text-align: left;">Good.<br></p>]]></text>
      </feedback>
    </answer>
    <answer fraction="50" format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">2.9<br></p>]]></text>
      <feedback format="html">
        <text>near!</text>
      </feedback>
    </answer>
  </question>

<!-- question: 128219  -->
  <question type="numerical">
    <name>
      <text>Question 5</text>
    </name>
    <questiontext format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">What is the approximate value of \( \int_1^2\frac{1}{x}dx? \)<br></p>]]></text>
    </questiontext>
    <generalfeedback format="html">
      <text></text>
    </generalfeedback>
    <defaultgrade>7.0000000</defaultgrade>
    <penalty>0.3333333</penalty>
    <hidden>0</hidden>
    <idnumber></idnumber>
    <answer fraction="100" format="moodle_auto_format">
      <text>0.693147</text>
      <feedback format="html">
        <text></text>
      </feedback>
      <tolerance>0.001</tolerance>
    </answer>
    <answer fraction="50" format="moodle_auto_format">
      <text>0.693147</text>
      <feedback format="html">
        <text></text>
      </feedback>
      <tolerance>0.01</tolerance>
    </answer>
    <unitgradingtype>0</unitgradingtype>
    <unitpenalty>0.1000000</unitpenalty>
    <showunits>3</showunits>
    <unitsleft>0</unitsleft>
  </question>

<!-- question: 128218  -->
  <question type="shortanswer">
    <name>
      <text>Question 3</text>
    </name>
    <questiontext format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">What is the number of the subgroups of the alternating group \( A_4 \)?<br></p>]]></text>
    </questiontext>
    <generalfeedback format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">You should specify a number.<br></p>]]></text>
    </generalfeedback>
    <defaultgrade>2.0000000</defaultgrade>
    <penalty>0.3333333</penalty>
    <hidden>0</hidden>
    <idnumber></idnumber>
    <usecase>0</usecase>
    <answer fraction="100" format="moodle_auto_format">
      <text>10</text>
      <feedback format="html">
        <text></text>
      </feedback>
    </answer>
    <answer fraction="100" format="moodle_auto_format">
      <text>ten</text>
      <feedback format="html">
        <text></text>
      </feedback>
    </answer>
    <answer fraction="100" format="moodle_auto_format">
      <text>dix</text>
      <feedback format="html">
        <text></text>
      </feedback>
    </answer>
  </question>

<!-- question: 128216  -->
  <question type="truefalse">
    <name>
      <text>Question 1</text>
    </name>
    <questiontext format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">The vector space \( \mathbb{R}^3 \) has just four subspaces.<br></p>]]></text>
    </questiontext>
    <generalfeedback format="html">
      <text><![CDATA[<p dir="ltr" style="text-align: left;">General feedback: Subspace means subvector space.<br></p>]]></text>
    </generalfeedback>
    <defaultgrade>1.0000000</defaultgrade>
    <penalty>1.0000000</penalty>
    <hidden>0</hidden>
    <idnumber></idnumber>
    <answer fraction="0" format="moodle_auto_format">
      <text>true</text>
      <feedback format="html">
        <text><![CDATA[<p>This space has infinitely many subspace!</p><br>]]></text>
      </feedback>
    </answer>
    <answer fraction="100" format="moodle_auto_format">
      <text>false</text>
      <feedback format="html">
        <text><![CDATA[<p>Bravo.<br></p>]]></text>
      </feedback>
    </answer>
  </question>

</quiz>
4
  • It could be more or less possible to have an usable source, but it's difficult - the macro that have been processed can't be undone. But for what I know there is no tool already made...
    – Rmano
    May 10, 2022 at 8:52
  • Do you have sample of such XML that we could test? LuaXML can translate XML to LaTeX, see the luaxml-transform library.
    – michal.h21
    May 10, 2022 at 10:14
  • 1
    @michal.h21 I added a sample xml file.
    – Name
    May 10, 2022 at 14:17
  • @Name, probably it would be useful if you also add the "source" LaTeX file.
    – Rmano
    May 10, 2022 at 16:25

3 Answers 3

5

Here is a solution that uses LuaXML. The main library is named moodle-transform.lua:

local transform = require "luaxml-transform"
local domobject = require "luaxml-domobject"

local transformer = transform.new()

transformer:add_action("p", "@<.>\n\n", {verbatim=true})
transformer:add_action("div", "@<.>\n", {verbatim=true})
transformer:add_action("*", "@<.>", {verbatim=true})

-- ********************
--   helper functions
-- ********************
local function get_cdata(element, selector)
  local t = {}
  local selector = selector or "text"
  for _, el in ipairs(element:query_selector(selector)) do
    for _, child in ipairs(el:get_children()) do
      if child._type == "CDATA" then
        local handle_greater = child._text:gsub("<%s", "&lt; "):gsub(">%s", "&gt; ")
        -- we pack everything in <div>, to force verbatim processing
        t[#t+1] =  transformer:parse_xml("<div>" .. handle_greater .. "</div>")
        -- for x,y in pairs(child) do print(x,y) end
      else
        t[#t+1] = child:get_text()
      end
    end
  end
  -- remove spurious spaces
  for k,v in ipairs(t) do t[k] = v:gsub("^%s*", ""):gsub("%s*$", "") end
  return table.concat(t, " ")
end

local function number_to_boolean(el)
  local number = el:get_text()
  if number == "1" then return true end
  return false
end

-- map from XML elements to Moodle fields
local basic_data_mapping = {
  penalty = "penalty",
  defaultgrade = "default grade",
  tags = "tags",
  generalfeedback = {"feedback", get_cdata},
  name = {"name", get_cdata},
  questiontext = {"question", get_cdata}
}

function add_mapping(basic, additional)
  local t = {}
  for k, v in pairs(basic) do t[k] = v end
  for k,v in pairs(additional) do t[k] = v end
  return t
end

local function map(data, el, mapping)
  local name = el._name
  local map = mapping[name]
  if map then
    if type(map) == "table" then
      local newname = map[1]
      local fn = map[2]
      data[newname] = fn(el)
    else
      data[map] = el:get_text()
    end
  end
end


local function get_basic_data(question, mapping)
  local mapping = mapping or basic_data_mapping
  local data = {}
  -- data.name = get_cdata(question, "name text")
  -- data.question = get_cdata(question, "questiontext text")
  for _, el in ipairs(question:get_children()) do
    if el:is_element() then
      map(data, el, mapping)
    end
  end
  return data
end

local function get_answers(question)
  -- process particular answers
  local answers = {}
  for _, answer in ipairs(question:query_selector("answer")) do
    local fraction = answer:get_attribute("fraction")
    local item = get_cdata(answer, "answer > text")
    local feedback = get_cdata(answer, "feedback > text")
    local tolerance = get_cdata(answer, "tolerance")
    if feedback == "" then feedback = nil end
    if tolerance == "" then tolerance = nil end
    answers[#answers+1] = {item = item, feedback = feedback, fraction = fraction, tolerance = tolerance}
  end
  return answers
end

local function make_field(field, value)
  if value then
    return string.format("%s=%s", field, value)
  end
  return nil
end

local basic_question_fields = {"points", "default grade", "penalty", "fraction", "feedback", "tags"}

local function add_question_fields(fields)
  local t = {}
  for _, v in ipairs(basic_question_fields) do t[#t+1] = v end
  for _, v in ipairs(fields) do t[#t+1] = v end
  return t
end
    
local function make_begin(name, data, fields)
  local expanded_fields = {}
  local fields = fields or basic_question_fields

  for _, field in ipairs(fields) do
    local value = data[field]
    if value and value ~= "" then
      if value:match("[%s]") then value = "{" .. value .. "}" end
      expanded_fields[#expanded_fields+1] = make_field(field, value)
    end
  end
  return string.format("\\begin{%s}[%s]{%s}", name, table.concat(expanded_fields, ","), data.name)
end

local function format_answers(answers, extra_fields, mark_fraction) 
  local extra_fields = extra_fields or {}
  local t  = {}
  for _, v in ipairs(answers) do
    local options = {}
    options[1] = make_field("fraction", v.fraction) 
    local item = "\\item"
    if mark_fraction then
      if v.fraction == "100" then item = item .. "*" end
    end
    options[#options+1] = make_field("feedback", v.feedback)
    for _,y in ipairs(extra_fields) do options[#options+1] = make_field(y, v[y]) end
    t[#t+1] =  item  .. "[" .. table.concat(options, ",") .. "]" ..  v.item
  end
  return table.concat(t, "\n")
end

local function make_end(environment)
  return "\\end{" .. environment .. "}"
end


-- ********************
--  question handlers
-- ********************

local function handle_category(question)
  local category = get_cdata(question, "category text") or ""
  -- category = category:gsub("%$course%$%/", "")
  -- get top level of the category hiearchy
  category = category:match("[^%/]+$")
  -- local info     = get_cdata(question, "info text") or ""
  -- it seems that category info isn't supported inside quiz environment
  local info     = "" 
  return string.format("\\setcategory{%s}\n%s\n", category, info)
end



multichoice_begin = add_question_fields {"single","numbering","shuffle", "fraction",}

local multichoice_mapping = add_mapping(basic_data_mapping,
  {
    single = "single",
    shuffleanswers = {"shuffle", number_to_boolean} ,
    answernumbering = "numbering",
  }
)


local function handle_multichoice(question)
  local data = get_basic_data(question, multichoice_mapping)
  data.answers = get_answers(question)
  local result = {}
  result[#result+1] =  make_begin("multi", data, multichoice_begin)
  result[#result+1] =  data.question
  result[#result+1] =  format_answers(data.answers)
  result[#result+1] =  make_end("multi")
  return table.concat(result, "\n")-- data.question
end


local numerical_mapping = add_mapping(basic_data_mapping,
{
  tolerance = "tolerance",
})

local numeric_begin = add_question_fields {"tolerance"}

local function handle_numerical(question)
  local result = {}
  local data = get_basic_data(question, numerical_mapping)
  data.answers = get_answers(question)
  result[#result+1] =  make_begin("numerical", data, numeric_begin)
  result[#result+1] =  data.question
  result[#result+1] =  format_answers(data.answers,{"tolerance"})
  result[#result+1] =  make_end("numerical")
  return table.concat(result, "\n")
end

local truefalse_mapping = add_mapping(basic_data_mapping,
{})

local function handle_truefalse(question)
  local result = {}
  local data = get_basic_data(question, truefalse_mapping)
  data.answers = get_answers(question)
  result[#result+1] =  make_begin("truefalse", data)
  result[#result+1] =  data.question
  result[#result+1] =  format_answers(data.answers, nil, true)
  result[#result+1] =  make_end("truefalse")
  return table.concat(result, "\n")
end

local function handle_shortanswer(question)
  local result = {}
  local data = get_basic_data(question)
  data.answers = get_answers(question)
  result[#result+1] =  make_begin("shortanswer", data)
  result[#result+1] =  data.question
  result[#result+1] =  format_answers(data.answers)
  result[#result+1] =  make_end("shortanswer")
  return table.concat(result, "\n")
end

local function handle_essay(question)
  local result = {}
  local data = get_basic_data(question)
  data.answers = get_answers(question)
  result[#result+1] =  make_begin("essay", data)
  result[#result+1] =  data.question
  result[#result+1] =  format_answers(data.answers)
  result[#result+1] =  make_end("essay")
  return table.concat(result, "\n")
end

local actions = {
  category  = handle_category,
  multichoice = handle_multichoice,
  numerical = handle_numerical,
  truefalse = handle_truefalse,
  shortanswer = handle_shortanswer,
  essay     = handle_essay,
}

local function transform(dom)
  -- buffer for tranformed LaTeX code
  local result = {"\\begin{quiz}{Unnamed}"}
  -- loop over all questions, and convert them to LaTeX
  for _, question in ipairs(dom:query_selector("question")) do
    -- we have a special handler for each question type
    local qtype = question:get_attribute("type")
    -- find action for the current question  in the table of actions
    local action = actions[qtype]
    if action then
      -- transform current question to LaTeX
      result[#result+1] = action(question)
    end
  end
  result[#result+1] = "\\end{quiz}"
  return table.concat(result, "\n")
end

local function process(text)
  local dom = domobject.parse(text)
  return transform(dom)
end

local function process_file(filename)
  local f = io.open(filename, "r")
  if f then 
    local text = f:read("*all")
    f:close()
    return process(text)
  end
end


return {
  transform = transform,
  process = process,
  process_file = process_file
}

What it does is that it uses LuaXML's DOM functions to retrieve Moodle elements and attributes to a Lua table, which is then transformed to the syntax used by the Moodle package.

It can be used from the command line, using for example the following script, moodletotex.lua:

kpse.set_program_name "luatex"
local moodle = require "moodle-transform"
local text = io.read("*all")

print(moodle.process(text))

It can be used with this command:

texlua moodletotex.lua < test.xml > output.tex

It will save TeX code to the output.tex file:

\begin{quiz}{Unnamed}
\setcategory{Default for my course}


\begin{multi}[default grade=10.0000000,penalty=0.3333333,feedback={\( A_4 \) is the subgroup of the symmetric group \( S_4 \) consisting of even permutations.},single=true,numbering=abc]{Querstion 2}
The number of the subgroups of the alternating group \( A_4 \) is:
\item[fraction=0,feedback=oops!]8
\item[fraction=0,feedback=oops!]9
\item[fraction=100,feedback=Yes!]10
\item[fraction=0,feedback=oops!]11
\end{multi}
\begin{multi}[default grade=3.0000000,penalty=0.3333333,single=true,numbering=abc]{Question 6}
What is \( \lim_{n\to\infty}(1+\frac{1}{n})^n \)?
\item[fraction=100,feedback=Yes.]e
\item[fraction=100]exp(1)
\item[fraction=80,feedback=Good.]2.7
\item[fraction=50,feedback=near!]2.9
\end{multi}
\begin{numerical}[default grade=7.0000000,penalty=0.3333333]{Question 5}
What is the approximate value of \( \int_1^2\frac{1}{x}dx? \)
\item[fraction=100,tolerance=0.001]0.693147
\item[fraction=50,tolerance=0.01]0.693147
\end{numerical}
\begin{shortanswer}[default grade=2.0000000,penalty=0.3333333,feedback={You should specify a number.}]{Question 3}
What is the number of the subgroups of the alternating group \( A_4 \)?
\item[fraction=100]10
\item[fraction=100]ten
\item[fraction=100]dix
\end{shortanswer}
\begin{truefalse}[default grade=1.0000000,penalty=1.0000000,feedback={General feedback: Subspace means subvector space.}]{Question 1}
The vector space \( \mathbb{R}^3 \) has just four subspaces.
\item[fraction=0,feedback=This space has infinitely many subspace!]true
\item*[fraction=100,feedback=Bravo.]false
\end{truefalse}
\end{quiz}

You can also make a simple LaTeX package, moodleimport.sty:

\ProvidesPackage{moodletotex}
\RequirePackage{moodle}
\RequirePackage{luacode}
\begin{luacode*}
moodleimport = require "moodle-transform"

function moodle_print(result)
  for line in result:gmatch("([^\n]+)") do
    tex.sprint(line)
  end
end
\end{luacode*}

\newcommand\importmoodle[1]{%
  \luaexec{%
    local result = moodleimport.process_file("#1")
    moodle_print(result)
  }

}%

It can be used like this:

\documentclass{article}
\usepackage{moodleimport}
\begin{document}
\importmoodle{test.xml}
\end{document}

You need to compile this document using lualatex.

This is the result: enter image description here

Here is a result for essay:

enter image description here

15
  • Wow....Don't have words to explain my feelings...Hope this is the best suggestion....Fantastic....
    – MadyYuvi
    May 20, 2022 at 14:28
  • 1
    @MadyYuvi thank you for your kind words. There are some differences in available data between the XML format and the LaTeX package, so I am sure that there are some bugs.
    – michal.h21
    May 20, 2022 at 14:37
  • Thank you very much. When I run the command texlua moodletotex sample.xml output.tex I get the error ./moodle-transform.lua:4: attempt to call a nil value (field 'new'). My LuaTeX, Version 1.13.0 (TeX Live 2021/W32TeX).
    – Name
    May 21, 2022 at 14:31
  • @Name it seems that your distro doesn't have the luaxml-transform library yet. Can the kpsewhich luaxml-transform.lua command find it?
    – michal.h21
    May 21, 2022 at 16:33
  • @michal.h21 Yes it has it in c:/texlive/2021/texmf-dist/tex/luatex/luaxml/luaxml-transform.lua.
    – Name
    May 21, 2022 at 16:35
2

The solution worked out by @michal.h21 is very neat. As the current maintainer the LaTeX package moodle, I have been looking for a solution like this in order to bring its test suite to an other level.

If it can help someone, I just published a very modest Python solution here. It is still in a "work in progress" state and contributions would be welcome (I do not intend to work on this in the near future).

This Python script seems to cleanly convert the OP's XML file. It is meant to deal with pictures as well.

3
  • For completeness, I added an "essay" type question to my sample xml file. It would be great if your code could handle this type as well.
    – Name
    Jun 30, 2022 at 21:05
  • @mgk Does your Python file complete? Can I just start a factorization of it from "now" because "cela pique les yeux"? :-)
    – projetmbc
    Jun 30, 2022 at 21:44
  • @projetmbc I am not sure if I understand your questions correctly. This was my very first Python project, and it is a very modest and incomplete contribution to the community. Sorry if my code makes your eyes bleed! :-) You are welcome to refactor and improve it the way you want.
    – mgk
    Jul 2, 2022 at 16:02
2

I'm actually working on the project started by @mgk, in a fork:

  • I've corrected some bugs related to image files (were not allways saved properly)
  • I've "supported" the "calculatedsimple" question type, by converting it "shortanswer" type in order to be able to "compile" the generated code.
  • I've set several options to generate the code in separated files
  • ...

I will certainly make an important refactoring to the code on the following weeks, but I'll be gratefull to know if someone have been doing similar things. @projetmbc you proposed the refactoring, but did you do something finally?

3
  • Welcome to TeX.SE!
    – Mensch
    Jul 24, 2023 at 8:57
  • Hello. For the moment, I am working on publishing my math packages.
    – projetmbc
    Jul 25, 2023 at 10:37
  • Hello. Just to end the conversation here. I've worked on the project during the summer, and I think the project is now in another stage: I've implemented automatic tests (not all passing yet), I've made an important refactoring (even if we could do better), I've implemented Clozes, Descriptions and Essays... I've made a pull request to @mgk, in order to this work to be integrated in the main repository. In the meantime, you can use the code that is in my repository.
    – Ion Iturbe
    Sep 7, 2023 at 13:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .