Skip to content

Instantly share code, notes, and snippets.

@Chaphasilor
Last active July 31, 2022 17:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Chaphasilor/3842a8964c0cb01c4b9df2d0fb5c2d17 to your computer and use it in GitHub Desktop.
Save Chaphasilor/3842a8964c0cb01c4b9df2d0fb5c2d17 to your computer and use it in GitHub Desktop.
Convert completed moodle quizzes to markdown for testing yourself again. Just open a quiz in "review" mode, copy the whole gist text and paste it into your browser's console.
(async () => {
const zeroWidthSpace = `​`;
async function convertQuizToMarkdown() {
const quizTitle = document.querySelector("#page-navbar > nav > ol > li:last-child > a").innerText;
const quizContent = (await Promise.all([...document.querySelectorAll(`.que`)].map(async (x, index) => {
if (index !== 5) {
return null
}
const content = x.querySelector(`.content`)
const question = content.querySelector(`.formulation .qtext`)
let aBlock = content.querySelector(`.formulation .ablock, .formulation .answercontainer, .formulation .ddarea`)
let feedback = content.querySelector(`.outcome .feedback`)
let questionParsed
let questionType
let optionText
console.log(`x:`, x)
if (x.classList.contains(`truefalse`)) {
questionType = `True/False`
let options = [...aBlock.querySelectorAll(`.answer > div > label`)].map(y => y.cloneNode(true)).map(y => y.innerText.replaceAll(`\n\n`, ` `).replaceAll(`\n`, ` \n`).trim())
optionText = options.reduce((acc, cur) => acc + `\n - [ ] ${cur}`, ``)
questionParsed = question
} else if (x.classList.contains(`multichoice`)) {
if (aBlock.querySelector(`input[type="radio"]`)) {
questionType = `Multiple Choice - **Single Answer**`
} else {
questionType = `Multiple Choice - **Multiple Answers**`
}
let options = [...aBlock.querySelectorAll(`.answer > div > div`)].map(y => y.cloneNode(true)).map(y => y.innerText.replaceAll(`\n\n`, ` `).replaceAll(`\n`, ` \n`).trim())
optionText = options.reduce((acc, cur) => acc + `\n - [ ] ${cur}`, ``)
questionParsed = question
} else if (x.classList.contains(`multianswer`)) { // dropdowns
questionType = `Select`
feedback = document.createElement(`div`)
let forumlationCensored = content.querySelector(`.formulation`).cloneNode(true);
[...forumlationCensored.querySelectorAll(`.subquestion > select`)].forEach((y, index) => {
console.log(`y:`, y)
let values = [...y.querySelectorAll(`option`)].filter(x => x.innerText.trim() !== ``)
let valueText = values.reduce((acc, cur, ind) => {
if (cur.selected) {
feedback.innerHTML += index === 0 ? `${index + 1}. ${cur.innerText}` : ` \n${index + 1}. ${cur.innerText}`
}
acc += ind === 0 ? `${cur.innerText.trim()}` : ` | ${cur.innerText.trim()}`
return acc
}, `[ `)
y.parentNode.innerHTML = `${valueText} ]`
})
optionText = ` \n`
questionParsed = forumlationCensored
console.log(`questionParsed:`, questionParsed)
} else if (x.classList.contains(`ddwtos`)) { // drag and drop
let censoredQuestion = question.cloneNode(true)
censoredQuestion.querySelectorAll(`.drop`).forEach(x => x.remove())
censoredQuestion.querySelectorAll(`.draghome`).forEach(y => y.innerText = `${zeroWidthSpace}_______${zeroWidthSpace}`)
questionType = `Drag&Drop / Fill-in-the-Blanks`
let options = [...new Set([...aBlock.querySelectorAll(`span.draghome`)].map(y => y.cloneNode(true)).map(y => y.innerHTML.trim()))]
optionText = `Available options: ${options.reduce((acc, cur) => acc + `\n - ${cur}`, ``)}`
questionParsed = censoredQuestion
} else if (x.classList.contains(`shortanswer`) || x.classList.contains(`numerical`)) {
let censoredQuestion = question.cloneNode(true)
censoredQuestion.querySelectorAll(`label`).forEach(y => y.innerHTML = `${zeroWidthSpace}_______${zeroWidthSpace}`)
censoredQuestion.querySelectorAll(`input`).forEach(y => y.remove())
console.log(`censoredQuestion:`, censoredQuestion)
questionType = `Short Answer`
optionText = ` \n`
questionParsed = censoredQuestion
} else if (x.classList.contains(`ddmarker`)) {
if (!window.html2canvas) {
window.html2canvas = (await import(`https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.esm.min.js`)).default // load helper script
}
const droparea = aBlock.querySelector(`.droparea`)
const originalImage = droparea.querySelector(`img`)
questionType = `Drag&Drop Image`
// add original image to question as base64
const originalCanvas = await window.html2canvas(originalImage)
const originalImageBase64 = originalCanvas.toDataURL("image/png")
questionParsed = question.cloneNode(true)
optionText = `\n\n![](${originalImageBase64})`
// add `markertext`s to options
const markerTexts = [...new Set([...aBlock.querySelectorAll(`.draghomes .markertext`)].map(y => y.cloneNode(true)).map(y => y.innerText.replaceAll(`\n\n`, ` `).replaceAll(`\n`, ` \n`).trim()))]
optionText += `Options/Markers: \n`
optionText += markerTexts.reduce((acc, cur) => acc + `\n - ${cur}`, ``)
// convert `droparea` to an image using canvas
const canvas = await window.html2canvas(droparea)
// convert canvas to data url
const dataUrl = canvas.toDataURL(`image/png`)
// add image to feedback/answer as base64 encoded image
feedback = document.createElement(`div`)
feedback.innerHTML = `![](${dataUrl})`
}
let answerParagraphs = [...feedback.querySelectorAll(`p, li`)].filter(x => x.querySelector(`img.img-responsive`) || x.innerText.trim() !== ``)
if (answerParagraphs.length === 0) {
answerParagraphs = [feedback]
}
let answerString = `${await answerParagraphs.reduce(async (acc, cur, ind) => {
let textContent
const image = cur.querySelector(`img.img-responsive`)
if (image) {
console.log(`detected image`)
textContent = await extractImageFromParagraph(cur, image, ``)
} else {
textContent = cur.innerText.trim()
}
if (ind === 0) {
return (await acc) + `${textContent}`
} else {
return (await acc) + ` \n\n${textContent}`
}
}, ``)}`
let questionParagraphs = [...questionParsed.querySelectorAll(`p, li`)].filter(x => x.querySelector(`img.img-responsive`) || x.innerText.trim() !== ``)
if (questionParagraphs.length === 0) {
questionParagraphs = [questionParsed]
}
questionString = `${await questionParagraphs.reduce(async (acc, cur, ind) => {
let textContent
const image = cur.querySelector(`img.img-responsive`)
if (image) {
console.log(`detected image`)
// textContent = await extractImageFromParagraph(cur, image, `.formulation .qtext`)
textContent = await extractImageFromParagraph(cur, image, ``)
} else {
textContent = cur.innerText.trim()
}
if (ind === 0) {
return (await acc) + `${textContent}`
} else {
return (await acc) + ` \n\n${textContent}`
}
}, ``)} \n\n *(${questionType})*`
console.log(`questionString:`, questionString)
let outputString = `${questionString.split(`\n`).reduce((acc, cur, ind) => { acc += ind === 0 ? `**${cur.trim()}**` : `\n ${cur}`; return acc }, ``)}
${optionText.split(`\n`).map(x => ` ${x}`).join(`\n`)}
<details>
<summary>Answer</summary>
${answerString.split(`\n`).map(x => ` ${x}`).join(`\n`)}
</details> `
return outputString
})))
.filter(x => x !== null)
.reduce((acc, cur) => acc + `\n\n1. ${cur}`, ``)
return {
title: quizTitle,
markdown: `# ${quizTitle}
${quizContent}
`,
}
}
async function extractImageFromParagraph(paragraph, image, basepath) {
if (!window.html2canvas) {
window.html2canvas = (await import(`https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.esm.min.js`)).default // load helper script
}
let base64
try {
console.log('${basepath} + getSelector(image):', `${basepath} ` + getSelector(image))
// console.log('document.querySelector(`${basepath} ` + getSelector(image):', document.querySelector(`${basepath} ` + getSelector(image)))
console.log('document.querySelector(getSelector(image):', document.querySelector(getSelector(image)))
const canvasImage = (await window.html2canvas(document.querySelector(`${basepath} ` + getSelector(image))))
console.log(`canvasImage:`, canvasImage)
base64 = canvasImage.toDataURL(`image/png`)
// console.log(`base64:`, base64)
} catch (err) {
console.warn(`Error while extracting image:`, err);
console.log(`paragraph:`, paragraph)
console.log(`image:`, image)
return paragraph.innerText
}
try {
const clonedParagraph = paragraph.cloneNode(true)
const clonedImage = clonedParagraph.querySelector(`img.img-responsive`)
const imageDiv = document.createElement(`div`)
imageDiv.innerHTML = ` \n\n![](${base64}) \n\n`
clonedImage.parentNode.replaceChild(imageDiv, clonedImage)
return clonedParagraph.innerText
} catch (err) {
console.warn(`Error while embedding image:`, err);
return paragraph.innerText
}
}
function slugify(text) {
return text.toString().toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, ''); // Trim - from end of text
}
function getSelector(elm) {
if (elm.tagName === "BODY") return "BODY";
const names = [];
while (elm.parentElement && elm.tagName !== "BODY") {
if (elm.id) {
names.unshift("#" + elm.getAttribute("id")); // getAttribute, because `elm.id` could also return a child element with name "id"
break; // Because ID should be unique, no more is needed. Remove the break, if you always want a full path.
} else {
let c = 1, e = elm;
for (; e.previousElementSibling; e = e.previousElementSibling, c++) ;
names.unshift(elm.tagName + ":nth-child(" + c + ")");
}
elm = elm.parentElement;
}
return names.join(">");
}
async function downloadQuiz() {
const quiz = await convertQuizToMarkdown()
const markdown = quiz.markdown
const blob = new Blob([markdown], { type: "text/plain;charset=utf-8" })
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = `${slugify(quiz.title)}.md`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
downloadQuiz()
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment