quiz-parser.js (4360B)
1 /** 2 * Quiz Parser - Converte sintaxe customizada em JSON estruturado 3 * Sintaxe suportada: 4 * ? Enunciado da questão 5 * - Opção A / - Opção B 6 * = Opção correta (incluída nas opções automaticamente se ausente) 7 * > Comentário (pode repetir; linha vazia separa parágrafos) 8 * 9 * ? Questão aberta 10 * [ Resposta de referência (pode repetir) 11 * > Comentário 12 * 13 * --- ou linha em branco separam questões 14 * # linha ignorada 15 */ 16 17 function parseQuizBody(raw) { 18 if (!raw || !raw.trim()) return []; 19 const lines = raw.replace(/\r\n/g, '\n').split('\n'); 20 const result = []; 21 let enunciado = '', tipo = '', opcoes = [], gabarito = '', referencia = '', comentario = ''; 22 23 function flush() { 24 if (!enunciado.trim()) return; 25 const q = { type: tipo || 'mc', text: enunciado.trim() }; 26 if (tipo === 'mc') { 27 q.options = opcoes; 28 q.answer = gabarito; 29 } else { 30 q.reference = referencia.trim(); 31 } 32 if (comentario.trim()) q.comment = comentario.trim(); 33 result.push(q); 34 enunciado = ''; 35 tipo = ''; 36 opcoes = []; 37 gabarito = ''; 38 referencia = ''; 39 comentario = ''; 40 } 41 42 for (const rawLine of lines) { 43 const line = rawLine.replace(/\r$/, '').trimEnd(); 44 if (/^---+\s*$/.test(line)) { 45 flush(); 46 continue; 47 } 48 if (line.trim() === '') continue; 49 const mark = line[0]; 50 const content = line.length > 1 ? line.slice(2) : ''; 51 switch (mark) { 52 case '#': 53 break; 54 case '?': 55 flush(); 56 enunciado = content; 57 break; 58 case '-': 59 opcoes.push(content); 60 tipo = 'mc'; 61 break; 62 case '=': 63 gabarito = content; 64 tipo = 'mc'; 65 if (content && !opcoes.includes(content)) opcoes.push(content); 66 break; 67 case '[': 68 referencia += (referencia ? '\n' : '') + content; 69 tipo = 'open'; 70 break; 71 case '>': 72 comentario += (comentario ? '\n' : '') + content; 73 break; 74 default: 75 if (enunciado && !tipo) enunciado += '\n' + line; 76 else if (tipo === 'open' && referencia) referencia += '\n' + line; 77 } 78 } 79 flush(); 80 return result; 81 } 82 83 /* ── UTILITÁRIOS BASE ── */ 84 const LETRAS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 85 86 function shuffle(arr) { 87 const a = [...arr]; 88 for (let i = a.length - 1; i > 0; i--) { 89 const j = Math.floor(Math.random() * (i + 1)); 90 [a[i], a[j]] = [a[j], a[i]]; 91 } 92 return a; 93 } 94 95 function prepareQuizQuestions(questions) { 96 const letterCounts = []; 97 let previousCorrectIndex = -1; 98 return questions.map((q) => { 99 if (!q || q.type !== 'mc' || !Array.isArray(q.options) || q.options.length < 2) { 100 return q; 101 } 102 const answerIndex = q.options.findIndex((o) => o === q.answer); 103 if (answerIndex < 0) { 104 return { ...q, options: shuffle(q.options) }; 105 } 106 const optionCount = q.options.length; 107 const availableIndexes = Array.from({ length: optionCount }, (_, i) => i); 108 const lowestCount = Math.min(...availableIndexes.map((i) => letterCounts[i] || 0)); 109 let candidates = availableIndexes.filter((i) => (letterCounts[i] || 0) === lowestCount); 110 if (optionCount > 1 && candidates.length > 1) { 111 candidates = candidates.filter((i) => i !== previousCorrectIndex); 112 } 113 const correctIndex = shuffle(candidates)[0] ?? 0; 114 const otherOptions = shuffle(q.options.filter((_, i) => i !== answerIndex)); 115 const arrangedOptions = [...otherOptions]; 116 arrangedOptions.splice(correctIndex, 0, q.options[answerIndex]); 117 letterCounts[correctIndex] = (letterCounts[correctIndex] || 0) + 1; 118 previousCorrectIndex = correctIndex; 119 return { ...q, options: arrangedOptions }; 120 }); 121 } 122 123 /* Para uso em atributos HTML (onclick="...") */ 124 function esc(s) { 125 return String(s ?? '') 126 .replace(/&/g, '&') 127 .replace(/</g, '<') 128 .replace(/>/g, '>') 129 .replace(/"/g, '"') 130 .replace(/'/g, '''); 131 } 132 133 /* Para innerHTML: escapa HTML e renderiza Markdown inline. 134 Suporta: **negrito**, *itálico*, `código`, \n→<br> */ 135 function md(s) { 136 return String(s ?? '') 137 .replace(/&/g, '&') 138 .replace(/</g, '<') 139 .replace(/>/g, '>') 140 .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') 141 .replace(/\*(.+?)\*/g, '<em>$1</em>') 142 .replace(/`(.+?)`/g, '<code>$1</code>') 143 .replace(/\n/g, '<br>'); 144 }