Last active
July 31, 2022 17:13
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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` | |
// 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 = `` | |
} | |
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 \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