diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js index de2761735..b465a02d9 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.js @@ -32,10 +32,11 @@ export class OLXParser { alwaysCreateTextNode: true, // preserveOrder: true }; + this.olxString = olxString; const parser = new XMLParser(options); - this.parsedOLX = parser.parse(olxString); - if (_.has(this.parsedOLX, 'problem')) { - this.problem = this.parsedOLX.problem; + const parsedOLX = parser.parse(olxString); + if (_.has(parsedOLX, 'problem')) { + this.problem = parsedOLX.problem; } } @@ -270,37 +271,67 @@ export class OLXParser { } parseQuestions(problemType) { - const builder = new XMLBuilder(); - const problemObject = _.get(this.problem, problemType); - let questionObject = {}; - /* TODO: How do we uniquely identify the label and description? - In order to parse label and description, there should be two states - and settings should be introduced to edit the label and description. - In turn editing the settings update the state and then it can be added to - the parsed OLX. - */ - const tagMap = { - label: 'strong', - description: 'em', + const getQuestionObject = (parsedOlx) => { + const hasNonQuestionKeys = (obj) => !(_.some(nonQuestionKeys, (key) => _.has(obj, key))); + const isWhiteSpace = (obj) => _.get(obj, '#text', 'nonemptystring').trim() !== ''; + const transformIntoQuestionObject = (root) => _.filter(_.filter(root, isWhiteSpace), hasNonQuestionKeys); + let questionObject = {}; + + // Try to find question within problemType node; odd path because of `preserveOrder` option used for parsing. + const problemTypeObject = _.get( + _.find(parsedOlx[0].problem, (obj) => _.has(obj, problemType)), + problemType, + {}, + ); + questionObject = transformIntoQuestionObject(problemTypeObject); + + // If haven't found any question nodes, check if question is stored in the problem node. + if (_.isEmpty(questionObject)) { + questionObject = transformIntoQuestionObject(parsedOlx[0].problem); + } + return questionObject; }; - /* Only numerical response has different ways to generate OLX, test with - numericInputWithFeedbackAndHintsOLXException and numericInputWithFeedbackAndHintsOLX - shows the different ways the olx can be generated. - */ - if (_.isArray(problemObject)) { - questionObject = _.omitBy(problemObject[0], (value, key) => _.includes(nonQuestionKeys, key)); - } else { - questionObject = _.omitBy(problemObject, (value, key) => _.includes(nonQuestionKeys, key)); - } - // Check if problem tag itself will have question and descriptions. - if (_.isEmpty(questionObject)) { - questionObject = _.omitBy(this.problem, (value, key) => _.includes(nonQuestionKeys, key)); - } - const serializedQuestion = _.mapKeys(questionObject, (value, key) => _.get(tagMap, key, key)); + const mapQuestionTags = (questionObject) => { + /* TODO: How do we uniquely identify the label and description? + In order to parse label and description, there should be two states + and settings should be introduced to edit the label and description. + In turn editing the settings update the state and then it can be added to + the parsed OLX. + */ + const tagMap = { + label: 'strong', + description: 'em', + }; - const questionString = builder.build(serializedQuestion); - return questionString; + return _.map(questionObject, (obj) => _.mapKeys(obj, (value, key) => _.get(tagMap, key, key))); + }; + + // To be able to generate question's HTML, such that it respects the initial + // order of elements and styles stored in the OLX, we parse and build + // original `olxString` with `preserveOrder: true`. We parse and build the + // question separately, because the JSON structure produced with + // `preserveOrder: true` option is more complex and hard to work with, and + // since the order of nested elements is not important in other places, we + // restrict the complexity of the produced JSON within the scope of this + // method. + const parser = new XMLParser({ + preserveOrder: true, + trimValues: false, + ignoreAttributes: false, + }); + const parsedOlx = parser.parse(this.olxString); + + const questionObject = getQuestionObject(parsedOlx); + const serializedQuestion = mapQuestionTags(questionObject); + + const builder = new XMLBuilder({ + preserveOrder: true, + attributeNamePrefix: '@_', + ignoreAttributes: false, + format: false, + }); + return builder.build(serializedQuestion); } getHints() { diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.test.js b/src/editors/containers/ProblemEditor/data/OLXParser.test.js index eade6422f..e7244f95d 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.test.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.test.js @@ -153,4 +153,14 @@ describe('Check OLXParser for question parsing', () => { const question = olxparser.parseQuestions('numericalresponse'); expect(question).toEqual(numericInputWithFeedbackAndHintsOLXException.question); }); + test.each([ + '', + '

Hello, World!

', + ])('Test blank problem', (expectedQuestion) => { + const olxparser = new OLXParser( + `${expectedQuestion}`, + ); + const question = olxparser.parseQuestions(null); + expect(question).toEqual(expectedQuestion); + }); }); diff --git a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js index 559e1c527..f9c7c5b3b 100644 --- a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js +++ b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js @@ -1,22 +1,20 @@ import _ from 'lodash-es'; -import { XMLParser, XMLBuilder } from 'fast-xml-parser'; +import { XMLBuilder } from 'fast-xml-parser'; import { ProblemTypeKeys } from '../../../data/constants/problem'; class ReactStateOLXParser { constructor(problemState) { - const parserOptions = { + this.parserOptions = { ignoreAttributes: false, alwaysCreateTextNode: true, - // preserveOrder: true }; - const builderOptions = { + this.builderOptions = { ignoreAttributes: false, attributeNamePrefix: '@_', suppressBooleanAttributes: false, format: true, }; - this.parser = new XMLParser(parserOptions); - this.builder = new XMLBuilder(builderOptions); + this.builder = new XMLBuilder(this.builderOptions); this.problemState = problemState.problem; } @@ -96,42 +94,53 @@ class ReactStateOLXParser { return compoundhint; } - addQuestion() { + buildOLXWithQuestion(problemObject, problemType) { + const olxWithoutQuestion = this.builder.build(problemObject); + + return this.insertQuestionIntoOLX(olxWithoutQuestion, problemType); + } + + insertQuestionIntoOLX(olx, tagName) { const { question } = this.problemState; - const questionObject = this.parser.parse(question); - return questionObject; + + const tagPattern = new RegExp(`<${tagName}[^>]*>`); + const match = olx.match(tagPattern); + const insertIndex = match.index + match[0].length; + + const olxWithQuestion = `${olx.slice(0, insertIndex)}\n${question}${olx.slice(insertIndex)}`; + + return olxWithQuestion; } buildMultiSelectProblem(problemType, widget, option) { - const question = this.addQuestion(); const widgetObject = this.addMultiSelectAnswers(option); const demandhint = this.addHints(); const problemObject = { problem: { [problemType]: { - ...question, [widget]: widgetObject, }, ...demandhint, }, }; - return this.builder.build(problemObject); + + return this.buildOLXWithQuestion(problemObject, problemType); } buildTextInput() { - const question = this.addQuestion(); + const problemType = ProblemTypeKeys.TEXTINPUT; const demandhint = this.addHints(); const answerObject = this.buildTextInputAnswersFeedback(); const problemObject = { problem: { - [ProblemTypeKeys.TEXTINPUT]: { - ...question, + [problemType]: { ...answerObject, }, ...demandhint, }, }; - return this.builder.build(problemObject); + + return this.buildOLXWithQuestion(problemObject, problemType); } buildTextInputAnswersFeedback() { @@ -175,17 +184,18 @@ class ReactStateOLXParser { } buildNumericInput() { - const question = this.addQuestion(); const demandhint = this.addHints(); const answerObject = this.buildNumericalResponse(); const problemObject = { problem: { - ...question, [ProblemTypeKeys.NUMERIC]: answerObject, ...demandhint, }, }; - return this.builder.build(problemObject); + + // Passing 'problem' as problem type, because for this type of problems the question has to be inserted under + // tag, instead of <${problemType}> tag. + return this.buildOLXWithQuestion(problemObject, 'problem'); } buildNumericalResponse() { diff --git a/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js b/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js index 5cae017f9..0c8a004f4 100644 --- a/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js +++ b/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js @@ -3,7 +3,7 @@ export const checkboxesOLXWithFeedbackAndHintsOLX = {

You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.

-You can add an optional tip or note related to the prompt like this. +You can add an optional tip or note related to the prompt like this. a correct answer You can specify optional feedback that appears after the learner selects and submits this answer. @@ -82,12 +82,10 @@ export const checkboxesOLXWithFeedbackAndHintsOLX = { }, ], }, - question: '

You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.

Add the question text, or prompt, here. This text is required.You can add an optional tip or note related to the prompt like this.', + question: '

You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.

Add the question text, or prompt, here. This text is required.You can add an optional tip or note related to the prompt like this. ', buildOLX: ` -

You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.

- Add the question text, or prompt, here. This text is required. - You can add an optional tip or note related to the prompt like this. +

You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.

Add the question text, or prompt, here. This text is required.You can add an optional tip or note related to the prompt like this. a correct answer You can specify optional feedback that appears after the learner selects and submits this answer. @@ -160,12 +158,10 @@ export const dropdownOLXWithFeedbackAndHintsOLX = { }, ], }, - question: '

You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.

Add the question text, or prompt, here. This text is required.You can add an optional tip or note related to the prompt like this.', + question: '

You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.

Add the question text, or prompt, here. This text is required.You can add an optional tip or note related to the prompt like this. ', buildOLX: ` -

You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.

- Add the question text, or prompt, here. This text is required. - You can add an optional tip or note related to the prompt like this. +

You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.

Add the question text, or prompt, here. This text is required.You can add an optional tip or note related to the prompt like this.