summaryrefslogtreecommitdiff
path: root/static/quiz-parser.js
diff options
context:
space:
mode:
authorSivaldo <sivaldodavi@disroot.org>2026-06-13 19:20:23 -0300
committerSivaldo <sivaldodavi@disroot.org>2026-06-13 19:20:23 -0300
commit7316238f16655bd127b04476bbf7864b27f940f9 (patch)
tree61f25290b3e5fd14894e52754cfe6dd4e85845ef /static/quiz-parser.js
Commit inicialHEADmain
Diffstat (limited to 'static/quiz-parser.js')
-rw-r--r--static/quiz-parser.js144
1 files changed, 144 insertions, 0 deletions
diff --git a/static/quiz-parser.js b/static/quiz-parser.js
new file mode 100644
index 0000000..c62beb5
--- /dev/null
+++ b/static/quiz-parser.js
@@ -0,0 +1,144 @@
+/**
+ * Quiz Parser - Converte sintaxe customizada em JSON estruturado
+ * Sintaxe suportada:
+ * ? Enunciado da questão
+ * - Opção A / - Opção B
+ * = Opção correta (incluída nas opções automaticamente se ausente)
+ * > Comentário (pode repetir; linha vazia separa parágrafos)
+ *
+ * ? Questão aberta
+ * [ Resposta de referência (pode repetir)
+ * > Comentário
+ *
+ * --- ou linha em branco separam questões
+ * # linha ignorada
+ */
+
+function parseQuizBody(raw) {
+ if (!raw || !raw.trim()) return [];
+ const lines = raw.replace(/\r\n/g, '\n').split('\n');
+ const result = [];
+ let enunciado = '', tipo = '', opcoes = [], gabarito = '', referencia = '', comentario = '';
+
+ function flush() {
+ if (!enunciado.trim()) return;
+ const q = { type: tipo || 'mc', text: enunciado.trim() };
+ if (tipo === 'mc') {
+ q.options = opcoes;
+ q.answer = gabarito;
+ } else {
+ q.reference = referencia.trim();
+ }
+ if (comentario.trim()) q.comment = comentario.trim();
+ result.push(q);
+ enunciado = '';
+ tipo = '';
+ opcoes = [];
+ gabarito = '';
+ referencia = '';
+ comentario = '';
+ }
+
+ for (const rawLine of lines) {
+ const line = rawLine.replace(/\r$/, '').trimEnd();
+ if (/^---+\s*$/.test(line)) {
+ flush();
+ continue;
+ }
+ if (line.trim() === '') continue;
+ const mark = line[0];
+ const content = line.length > 1 ? line.slice(2) : '';
+ switch (mark) {
+ case '#':
+ break;
+ case '?':
+ flush();
+ enunciado = content;
+ break;
+ case '-':
+ opcoes.push(content);
+ tipo = 'mc';
+ break;
+ case '=':
+ gabarito = content;
+ tipo = 'mc';
+ if (content && !opcoes.includes(content)) opcoes.push(content);
+ break;
+ case '[':
+ referencia += (referencia ? '\n' : '') + content;
+ tipo = 'open';
+ break;
+ case '>':
+ comentario += (comentario ? '\n' : '') + content;
+ break;
+ default:
+ if (enunciado && !tipo) enunciado += '\n' + line;
+ else if (tipo === 'open' && referencia) referencia += '\n' + line;
+ }
+ }
+ flush();
+ return result;
+}
+
+/* ── UTILITÁRIOS BASE ── */
+const LETRAS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+function shuffle(arr) {
+ const a = [...arr];
+ for (let i = a.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [a[i], a[j]] = [a[j], a[i]];
+ }
+ return a;
+}
+
+function prepareQuizQuestions(questions) {
+ const letterCounts = [];
+ let previousCorrectIndex = -1;
+ return questions.map((q) => {
+ if (!q || q.type !== 'mc' || !Array.isArray(q.options) || q.options.length < 2) {
+ return q;
+ }
+ const answerIndex = q.options.findIndex((o) => o === q.answer);
+ if (answerIndex < 0) {
+ return { ...q, options: shuffle(q.options) };
+ }
+ const optionCount = q.options.length;
+ const availableIndexes = Array.from({ length: optionCount }, (_, i) => i);
+ const lowestCount = Math.min(...availableIndexes.map((i) => letterCounts[i] || 0));
+ let candidates = availableIndexes.filter((i) => (letterCounts[i] || 0) === lowestCount);
+ if (optionCount > 1 && candidates.length > 1) {
+ candidates = candidates.filter((i) => i !== previousCorrectIndex);
+ }
+ const correctIndex = shuffle(candidates)[0] ?? 0;
+ const otherOptions = shuffle(q.options.filter((_, i) => i !== answerIndex));
+ const arrangedOptions = [...otherOptions];
+ arrangedOptions.splice(correctIndex, 0, q.options[answerIndex]);
+ letterCounts[correctIndex] = (letterCounts[correctIndex] || 0) + 1;
+ previousCorrectIndex = correctIndex;
+ return { ...q, options: arrangedOptions };
+ });
+}
+
+/* Para uso em atributos HTML (onclick="...") */
+function esc(s) {
+ return String(s ?? '')
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;');
+}
+
+/* Para innerHTML: escapa HTML e renderiza Markdown inline.
+ Suporta: **negrito**, *itálico*, `código`, \n→<br> */
+function md(s) {
+ return String(s ?? '')
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
+ .replace(/`(.+?)`/g, '<code>$1</code>')
+ .replace(/\n/g, '<br>');
+}