import _ from 'lodash-es'; import { XMLParser, XMLBuilder } from 'fast-xml-parser'; import { ProblemTypeKeys } from '../../../data/constants/problem'; class ReactStateOLXParser { constructor(problemState) { const parserOptions = { ignoreAttributes: false, alwaysCreateTextNode: true, }; const questionParserOptions = { ignoreAttributes: false, alwaysCreateTextNode: true, preserveOrder: true, }; const questionBuilderOptions = { ignoreAttributes: false, attributeNamePrefix: '@_', suppressBooleanAttributes: false, format: true, preserveOrder: true, }; const builderOptions = { ignoreAttributes: false, attributeNamePrefix: '@_', suppressBooleanAttributes: false, format: true, }; this.questionParser = new XMLParser(questionParserOptions); this.parser = new XMLParser(parserOptions); this.builder = new XMLBuilder(builderOptions); this.questionBuilder = new XMLBuilder(questionBuilderOptions); this.editorObject = problemState.editorObject; this.problemState = problemState.problem; } addHints() { const hintsArray = []; const { hints } = this.editorObject; hints.forEach(hint => { if (hint.length > 0) { const parsedHint = this.parser.parse(hint); hintsArray.push({ ...parsedHint, }); } }); const demandhint = { demandhint: { hint: hintsArray, }, }; return demandhint; } addSolution() { const { solution } = this.editorObject; if (!solution || solution.length <= 0) { return {}; } const solutionTitle = { '#text': 'Explanation' }; const parsedSolution = this.parser.parse(solution); const paragraphs = parsedSolution.p; const withWrapper = _.isArray(paragraphs) ? [solutionTitle, ...paragraphs] : [solutionTitle, paragraphs]; const solutionObject = { solution: { div: { '@_class': 'detailed-solution', p: withWrapper, }, }, }; return solutionObject; } addMultiSelectAnswers(option) { const choice = []; let compoundhint = []; let widget = {}; // eslint-disable-next-line prefer-const let { answers, problemType } = this.problemState; const answerTitles = this.editorObject?.answers; const { selectedFeedback, unselectedFeedback } = this.editorObject; /* todo */ /* * the logic for general feedback is ot current being used. * when component is updated will need to return to this code. * general feedback replaces selected feedback if all incorrect selected feedback is the same. * ****************************************** if (generalFeedback !== '' && answers.every( answer => ( answer.correct ? true : answer?.selectedFeedback === answers.find(a => a.correct === false).selectedFeedback ), )) { answers = answers.map(answer => (!answer?.correct ? { ...answer, selectedFeedback: generalFeedback } : answer)); } */ answers.forEach((answer) => { const feedback = []; let singleAnswer = {}; const title = answerTitles ? this.parser.parse(answerTitles[answer.id]) : { '#text': answer.title }; const currentSelectedFeedback = selectedFeedback?.[answer.id] || null; const currentUnselectedFeedback = unselectedFeedback?.[answer.id] || null; let isEmpty; if (answerTitles) { isEmpty = Object.keys(title)?.length <= 0; } else { isEmpty = title['#text']?.length <= 0; } if (title && !isEmpty) { if (currentSelectedFeedback && problemType === ProblemTypeKeys.MULTISELECT) { const parsedSelectedFeedback = this.parser.parse(currentSelectedFeedback); feedback.push({ ...parsedSelectedFeedback, '@_selected': true, }); } if (currentSelectedFeedback && problemType !== ProblemTypeKeys.MULTISELECT) { const parsedSelectedFeedback = this.parser.parse(currentSelectedFeedback); feedback.push({ ...parsedSelectedFeedback, }); } if (currentUnselectedFeedback && problemType === ProblemTypeKeys.MULTISELECT) { const parsedUnselectedFeedback = this.parser.parse(currentUnselectedFeedback); feedback.push({ ...parsedUnselectedFeedback, '@_selected': false, }); } if (feedback.length) { singleAnswer[`${option}hint`] = feedback; } singleAnswer = { '@_correct': answer.correct, ...title, ...singleAnswer, }; choice.push(singleAnswer); } }); widget = { [option]: choice }; if (_.has(this.problemState, 'groupFeedbackList') && problemType === ProblemTypeKeys.MULTISELECT) { compoundhint = this.addGroupFeedbackList(); widget = { ...widget, compoundhint, }; } return widget; } addGroupFeedbackList() { const compoundhint = []; const { groupFeedbackList } = this.problemState; const { groupFeedback } = this.editorObject; groupFeedbackList.forEach((element) => { const feedbackString = groupFeedback?.[element.id]; const parsedFeedback = this.parser.parse(feedbackString); compoundhint.push({ ...parsedFeedback, '@_value': element.answers.join(' '), }); }); return compoundhint; } addQuestion() { const { question } = this.editorObject; const questionObject = this.questionParser.parse(question); return questionObject; } buildMultiSelectProblem(problemType, widget, option) { const question = this.addQuestion(); const widgetObject = this.addMultiSelectAnswers(option); const demandhint = this.addHints(); const solution = this.addSolution(); const problemObject = { problem: { [problemType]: { [widget]: widgetObject, ...solution, }, ...demandhint, }, }; const problem = this.builder.build(problemObject); const questionString = this.questionBuilder.build(question); let problemTypeTag; switch (problemType) { case ProblemTypeKeys.MULTISELECT: [problemTypeTag] = problem.match(/|]+>/); break; case ProblemTypeKeys.DROPDOWN: [problemTypeTag] = problem.match(/|]+>/); break; case ProblemTypeKeys.SINGLESELECT: [problemTypeTag] = problem.match(/|]+>/); break; default: break; } const updatedString = `${problemTypeTag}\n${questionString}`; const problemString = problem.replace(problemTypeTag, updatedString); return problemString; } buildTextInput() { const question = this.addQuestion(); const demandhint = this.addHints(); const answerObject = this.buildTextInputAnswersFeedback(); const solution = this.addSolution(); const problemObject = { problem: { [ProblemTypeKeys.TEXTINPUT]: { ...answerObject, ...solution, }, ...demandhint, }, }; const problem = this.builder.build(problemObject); const questionString = this.questionBuilder.build(question); const [problemTypeTag] = problem.match(/|]+>/); const updatedString = `${problemTypeTag}\n${questionString}`; const problemString = problem.replace(problemTypeTag, updatedString); return problemString; } buildTextInputAnswersFeedback() { const { answers } = this.problemState; const { selectedFeedback } = this.editorObject; let answerObject = {}; const additionAnswers = []; const wrongAnswers = []; let firstCorrectAnswerParsed = false; answers.forEach((answer) => { const correcthint = this.getAnswerHints(selectedFeedback?.[answer.id]); if (this.hasAttributeWithValue(answer, 'title')) { if (answer.correct && firstCorrectAnswerParsed) { additionAnswers.push({ '@_answer': answer.title, ...correcthint, }); } else if (answer.correct && !firstCorrectAnswerParsed) { firstCorrectAnswerParsed = true; answerObject = { '@_answer': answer.title, ...correcthint, }; } else if (!answer.correct) { const wronghint = correcthint.correcthint; wrongAnswers.push({ '@_answer': answer.title, ...wronghint, }); } } }); answerObject = { ...answerObject, additional_answer: additionAnswers, stringequalhint: wrongAnswers, '@_type': _.get(this.problemState, 'additionalAttributes.type', 'ci'), textline: { '@_size': _.get(this.problemState, 'additionalAttributes.textline.size', 20), }, }; return answerObject; } buildNumericInput() { const question = this.addQuestion(); const demandhint = this.addHints(); const answerObject = this.buildNumericalResponse(); const solution = this.addSolution(); const problemObject = { problem: { [ProblemTypeKeys.NUMERIC]: { ...answerObject, ...solution, }, ...demandhint, }, }; const problem = this.builder.build(problemObject); const questionString = this.questionBuilder.build(question); const [problemTypeTag] = problem.match(/|]+>/); const updatedString = `${questionString}\n${problemTypeTag}`; const problemString = problem.replace(problemTypeTag, updatedString); return problemString; } buildNumericalResponse() { const { answers } = this.problemState; const { selectedFeedback } = this.editorObject; let answerObject = {}; const additionalAnswers = []; let firstCorrectAnswerParsed = false; answers.forEach((answer) => { const correcthint = this.getAnswerHints(selectedFeedback?.[answer.id]); if (this.hasAttributeWithValue(answer, 'title')) { if (answer.correct && !firstCorrectAnswerParsed) { firstCorrectAnswerParsed = true; let responseParam = {}; if (_.has(answer, 'tolerance')) { responseParam = { responseparam: { '@_type': 'tolerance', '@_default': _.get(answer, 'tolerance', 0), }, }; } answerObject = { '@_answer': answer.title, ...responseParam, ...correcthint, }; } else if (answer.correct && firstCorrectAnswerParsed) { additionalAnswers.push({ '@_answer': answer.title, ...correcthint, }); } } }); answerObject = { ...answerObject, additional_answer: additionalAnswers, formulaequationinput: { '#text': '', }, }; return answerObject; } getAnswerHints(feedback) { let correcthint = {}; if (feedback !== undefined && feedback !== '') { const parsedFeedback = this.parser.parse(feedback); correcthint = { correcthint: { ...parsedFeedback, }, }; } return correcthint; } hasAttributeWithValue(obj, attr) { return _.has(obj, attr) && _.get(obj, attr, '').toString().trim() !== ''; } buildOLX() { const { problemType } = this.problemState; let problemString = ''; switch (problemType) { case ProblemTypeKeys.MULTISELECT: problemString = this.buildMultiSelectProblem(ProblemTypeKeys.MULTISELECT, 'checkboxgroup', 'choice'); break; case ProblemTypeKeys.DROPDOWN: problemString = this.buildMultiSelectProblem(ProblemTypeKeys.DROPDOWN, 'optioninput', 'option'); break; case ProblemTypeKeys.SINGLESELECT: problemString = this.buildMultiSelectProblem(ProblemTypeKeys.SINGLESELECT, 'choicegroup', 'choice'); break; case ProblemTypeKeys.TEXTINPUT: problemString = this.buildTextInput(); break; case ProblemTypeKeys.NUMERIC: problemString = this.buildNumericInput(); break; default: break; } return problemString; } } export default ReactStateOLXParser;