import { get, has } from 'lodash'; import { XMLParser, XMLBuilder } from 'fast-xml-parser'; import { ProblemTypeKeys } from '../../../data/constants/problem'; import { ToleranceTypes } from '../components/EditProblemView/SettingsWidget/settingsComponents/Tolerance/constants'; import { findNodesAndRemoveTheirParentNodes } from './reactStateOLXHelpers'; const HtmlBlockTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'pre', 'blockquote', 'ol', 'ul', 'li', 'dl', 'dt', 'dd', 'hr', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'colgroup', 'col', 'address', 'fieldset', 'legend']; class ReactStateOLXParser { constructor(problemState) { const richTextParserOptions = { ignoreAttributes: false, alwaysCreateTextNode: true, numberParseOptions: { leadingZeros: false, hex: false, }, preserveOrder: true, // Ensure whitespace inside
 tags is preserved
      trimValues: false,
      // Parse 
correctly unpairedTags: ['br'], }; const richTextBuilderOptions = { ignoreAttributes: false, attributeNamePrefix: '@_', suppressBooleanAttributes: false, // Avoid formatting as it adds unwanted newlines and whitespace, // breaking
 tags
      format: false,
      numberParseOptions: {
        leadingZeros: false,
        hex: false,
      },
      preserveOrder: true,
      unpairedTags: ['br'],
      // Output 
rather than
suppressUnpairedNode: false, }; this.richTextParser = new XMLParser(richTextParserOptions); this.richTextBuilder = new XMLBuilder(richTextBuilderOptions); this.editorObject = problemState.editorObject; this.problemState = problemState.problem; } /** addHints() * The editorObject saved to the class constuctor is parsed for the attribute hints. No hints returns an empty object. * The hints are parsed and appended to the hintsArray as object representations of the hint. The hints array is saved * to the hint key in the demandHint object and returned. * @return {object} demandhint object with atrribut hint with array of objects */ addHints() { const hintsArray = []; const { hints } = this.editorObject; if (hints.length < 1) { return hintsArray; } hints.forEach(hint => { if (hint.length > 0) { const parsedHint = this.richTextParser.parse(hint); hintsArray.push({ hint: [...parsedHint], }); } }); const demandhint = [{ demandhint: hintsArray }]; return demandhint; } /** addSolution() * The editorObject saved to the class constuctor is parsed for the attribute solution. If the soltuion is empty, it * returns an empty object. The solution is parsed and checked if paragraph key's value is a string or array. Studio * requires a div wrapper with a heading (Explanation). The heading is prepended to the parsed solution object. The * solution object is returned with the updated div wrapper. * @return {object} object representation of solution */ addSolution() { const { solution } = this.editorObject; if (!solution || solution.length <= 0) { return []; } const solutionTitle = { p: [{ '#text': 'Explanation' }] }; const parsedSolution = this.richTextParser.parse(solution); const withWrapper = [solutionTitle, ...parsedSolution]; const solutionObject = [{ solution: [{ ':@': { '@_class': 'detailed-solution' }, div: [...withWrapper], }], }]; return solutionObject; } /** addMultiSelectAnswers(option) * addMultiSelectAnswers takes option. Option is used to assign an answers to the * correct OLX tag. This function is used for multiple choice, checkbox, and * dropdown problems. The editorObject saved to the class constuctor is parsed for * answers (titles only), selectFeedback, and unselectedFeedback. The problemState * saved to the class constructor is parsed for the problemType and answers (full * object). The answers are looped through to pair feedback with its respective * OLX tags. While matching feedback tags, answers are also mapped to their * respective OLX tags. he object representation of the answers is returned with * the correct wrapping tags. For checkbox problems, compound hints are also returned. * @param {string} option - string of answer tag name * @return {object} object representation of answers */ addMultiSelectAnswers(option) { const choice = []; let compoundhint = []; // 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.richTextParser.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.richTextParser.parse(currentSelectedFeedback); feedback.push({ ':@': { '@_selected': true }, [`${option}hint`]: parsedSelectedFeedback, }); } if (currentSelectedFeedback && problemType !== ProblemTypeKeys.MULTISELECT) { const parsedSelectedFeedback = this.richTextParser.parse(currentSelectedFeedback); feedback.push({ [`${option}hint`]: parsedSelectedFeedback, }); } if (currentUnselectedFeedback && problemType === ProblemTypeKeys.MULTISELECT) { const parsedUnselectedFeedback = this.richTextParser.parse(currentUnselectedFeedback); feedback.push({ ':@': { '@_selected': false }, [`${option}hint`]: parsedUnselectedFeedback, }); } singleAnswer = { ':@': { '@_correct': answer.correct }, [option]: [...title, ...feedback], }; choice.push(singleAnswer); } }); if (has(this.problemState, 'groupFeedbackList') && problemType === ProblemTypeKeys.MULTISELECT) { compoundhint = this.addGroupFeedbackList(); choice.push(...compoundhint); } return choice; } /** addGroupFeedbackList() * The problemState saved to the class constuctor is parsed for the attribute groupFeedbackList. * No group feedback returns an empty array. Each groupFeedback in the groupFeedback list is * mapped to a new object and appended to the compoundhint array. * @return {object} object representation of compoundhints */ addGroupFeedbackList() { const compoundhint = []; const { groupFeedbackList } = this.problemState; groupFeedbackList.forEach((element) => { compoundhint.push({ compoundhint: [{ '#text': element.feedback }], ':@': { '@_value': element.answers.join(' ') }, }); }); return compoundhint; } /** addQuestion() * The editorObject saved to the class constuctor is parsed for the attribute question. The question is parsed and * checked for label tags. label tags are extracted from block-type tags like

or

, and the block-type tag is * deleted while label is kept. For example,

becomes , while *

Text

remains

Text

. The question is returned as an object representation. * @return {object} object representaion of question */ addQuestion() { const { question } = this.editorObject; const questionObjectArray = this.richTextParser.parse(question); /* Removes block tags like

or

that surround the