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)
}