direito

Repositório direito
git clone git://git.sivaldodavi.com/direito.git
Log | Files | Refs

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, '&amp;')
    127     .replace(/</g, '&lt;')
    128     .replace(/>/g, '&gt;')
    129     .replace(/"/g, '&quot;')
    130     .replace(/'/g, '&#39;');
    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, '&amp;')
    138     .replace(/</g, '&lt;')
    139     .replace(/>/g, '&gt;')
    140     .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
    141     .replace(/\*(.+?)\*/g, '<em>$1</em>')
    142     .replace(/`(.+?)`/g, '<code>$1</code>')
    143     .replace(/\n/g, '<br>');
    144 }