| import * as pdfjsLib from 'pdfjs-dist'; |
| import mammoth from 'mammoth'; |
| import { Question } from '../types'; |
|
|
| |
| pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs`; |
|
|
| export class DocumentParserService { |
| |
| |
| |
| static async extractTextFromFile(file: File): Promise<string> { |
| const extension = file.name.split('.').pop()?.toLowerCase(); |
|
|
| if (extension === 'txt') { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = () => resolve(reader.result as string); |
| reader.onerror = () => reject(new Error("Unable to parse TXT document.")); |
| reader.readAsText(file); |
| }); |
| } |
|
|
| if (extension === 'docx') { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = async () => { |
| try { |
| const arrayBuffer = reader.result as ArrayBuffer; |
| const result = await mammoth.extractRawText({ arrayBuffer }); |
| resolve(result.value); |
| } catch (err: any) { |
| reject(new Error("Unable to parse Microsoft Word Document (.docx): " + err.message)); |
| } |
| }; |
| reader.onerror = () => reject(new Error("File load error in Word parser.")); |
| reader.readAsArrayBuffer(file); |
| }); |
| } |
|
|
| if (extension === 'pdf') { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = async () => { |
| try { |
| const arrayBuffer = reader.result as ArrayBuffer; |
| const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); |
| const pdf = await loadingTask.promise; |
| |
| let extractedText = ""; |
| for (let i = 1; i <= pdf.numPages; i++) { |
| const page = await pdf.getPage(i); |
| const textContent = await page.getTextContent(); |
| const pageText = textContent.items |
| .map((item: any) => item.str) |
| .join(" "); |
| extractedText += pageText + "\n"; |
| } |
| resolve(extractedText); |
| } catch (err: any) { |
| reject(new Error("Unable to parse PDF document (.pdf): " + err.message)); |
| } |
| }; |
| reader.onerror = () => reject(new Error("File load error in PDF parser.")); |
| reader.readAsArrayBuffer(file); |
| }); |
| } |
|
|
| throw new Error("Unsupported file schema! Please upload .TXT, .PDF, or .DOCX files."); |
| } |
|
|
| |
| |
| |
| static parseLocalRegex(text: string): Question[] { |
| const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0); |
| const questions: Question[] = []; |
| let currentQuestion: Partial<Question> | null = null; |
|
|
| lines.forEach((line) => { |
| |
| const qMatch = line.match(/^(\d+)[\.\)]\s*(.*)$/i) || line.match(/^Question\s*(\d+)[:\.]?\s*(.*)$/i) || line.match(/^Q(\d+)[:\.]?\s*(.*)$/i); |
| |
| if (qMatch) { |
| if (currentQuestion && currentQuestion.text) { |
| questions.push(this.normalizeCleanQuestion(currentQuestion)); |
| } |
| currentQuestion = { |
| id: `manual-regex-${Date.now()}-${qMatch[1]}`, |
| text: qMatch[2], |
| options: [], |
| correctOptionId: 'opt-1', |
| category: 'General', |
| difficulty: 'medium', |
| tags: [] |
| }; |
| return; |
| } |
|
|
| |
| const oMatch = line.match(/^([A-D])[\.\)]\s*(.*)$/i) || line.match(/^\(([A-D])\)\s*(.*)$/i); |
| if (oMatch && currentQuestion) { |
| const letterIndex = oMatch[1].toUpperCase(); |
| const optionIdMap: Record<string, string> = { A: 'opt-1', B: 'opt-2', C: 'opt-3', D: 'opt-4' }; |
| currentQuestion.options?.push({ |
| id: optionIdMap[letterIndex] || `opt-${letterIndex.toLowerCase()}`, |
| text: oMatch[2] |
| }); |
| return; |
| } |
|
|
| |
| const aMatch = line.match(/^(?:Answer|ANS)[:\s]+([A-D])/i); |
| if (aMatch && currentQuestion) { |
| const letterIndex = aMatch[1].toUpperCase(); |
| const optionIdMap: Record<string, string> = { A: 'opt-1', B: 'opt-2', C: 'opt-3', D: 'opt-4' }; |
| currentQuestion.correctOptionId = optionIdMap[letterIndex] || 'opt-1'; |
| return; |
| } |
|
|
| |
| if (currentQuestion && !oMatch && !aMatch) { |
| currentQuestion.text += ' ' + line; |
| } |
| }); |
|
|
| if (currentQuestion && currentQuestion.text) { |
| questions.push(this.normalizeCleanQuestion(currentQuestion)); |
| } |
|
|
| return questions; |
| } |
|
|
| private static normalizeCleanQuestion(q: Partial<Question>): Question { |
| const baseOptIds = ['opt-1', 'opt-2', 'opt-3', 'opt-4']; |
| |
| |
| const options = q.options || []; |
| while (options.length < 4) { |
| options.push({ id: baseOptIds[options.length], text: `Option ${String.fromCharCode(65 + options.length)} placeholder` }); |
| } |
| const slicedOptions = options.slice(0, 4); |
|
|
| return { |
| text: q.text || "Untitled Question", |
| options: slicedOptions, |
| correctOptionId: q.correctOptionId || 'opt-1', |
| category: q.category || 'General', |
| difficulty: q.difficulty || 'medium', |
| tags: q.tags || [] |
| } as Question; |
| } |
| } |
|
|