Skip to content

Commit

Permalink
fix: fix problem editor displaying and editing styled problem questions
Browse files Browse the repository at this point in the history
#173

Fixed the issues of the problem editor related to displaying, editing
and saving of the problem questions which were styled:
* Fix the order in which the styled text was being displayed - now
  displayed in the correct order, instead of putting the styled text
  first
* Render the style attributes of the HTML tags correctly, instead of
  rendering them as HTML tags
  • Loading branch information
Cup0fCoffee committed Jan 31, 2023
1 parent 4a1bac3 commit f9fbebe
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 82 deletions.
93 changes: 62 additions & 31 deletions src/editors/containers/ProblemEditor/data/OLXParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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() {
Expand Down
10 changes: 10 additions & 0 deletions src/editors/containers/ProblemEditor/data/OLXParser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,14 @@ describe('Check OLXParser for question parsing', () => {
const question = olxparser.parseQuestions('numericalresponse');
expect(question).toEqual(numericInputWithFeedbackAndHintsOLXException.question);
});
test.each([
'',
'<p>Hello, World!</p>',
])('Test blank problem', (expectedQuestion) => {
const olxparser = new OLXParser(
`<problem>${expectedQuestion}</problem>`,
);
const question = olxparser.parseQuestions(null);
expect(question).toEqual(expectedQuestion);
});
});
48 changes: 29 additions & 19 deletions src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
// <problem> tag, instead of <${problemType}> tag.
return this.buildOLXWithQuestion(problemObject, 'problem');
}

buildNumericalResponse() {
Expand Down
Loading

0 comments on commit f9fbebe

Please sign in to comment.