function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[ch])) } function formatDate(value) { if (!value) return '-' return new Date(value).toLocaleString('en-IN') } export function buildResultHtml(result) { const rows = (result.answers || []).map((answer, idx) => { const status = answer.is_correct === true ? 'Correct' : answer.is_correct === false ? 'Wrong' : 'Skipped' const options = (answer.options || []).map((option, i) => { const letter = 'ABCD'[i] || `${i + 1}` return `
  • ${letter}. ${escapeHtml(option)}
  • ` }).join('') return `
    Q${idx + 1} ${status} ${escapeHtml(answer.marks_awarded)} marks

    ${escapeHtml(answer.question_text)}

    ${options ? `
      ${options}
    ` : ''}
    Your answer: ${escapeHtml(answer.selected_answer || 'Skipped')} Correct answer: ${escapeHtml(answer.correct_answer)} Time: ${escapeHtml(answer.time_spent_seconds)}s
    ` }).join('') return ` ${escapeHtml(result.test_title)} result

    ${escapeHtml(result.test_title)}

    Attempt ${escapeHtml(result.attempt_number)} · ${result.counts_for_leaderboard ? 'Saved first attempt' : 'Practice attempt'} · ${escapeHtml(formatDate(result.submitted_at))}
    Score${escapeHtml(result.score?.toFixed?.(2) ?? result.score)} / ${escapeHtml(result.total_marks)}
    Percentage${escapeHtml(Math.round(result.percentage))}%
    Correct${escapeHtml(result.correct)}
    Wrong${escapeHtml(result.incorrect)}
    Skipped${escapeHtml(result.skipped)}
    ${rows} ` } export function downloadResultReport(result) { const slug = (result.test_title || 'result').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') const suffix = result.counts_for_leaderboard ? 'result' : 'practice-result' const blob = new Blob([buildResultHtml(result)], { type: 'text/html;charset=utf-8' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = `${slug || 'test'}-${suffix}.html` document.body.appendChild(link) link.click() link.remove() setTimeout(() => URL.revokeObjectURL(url), 0) }