diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js index 984c3429b..8c44dc28b 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.js @@ -28,6 +28,18 @@ export const nonQuestionKeys = [ 'textline', ]; +export const richTextFormats = [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'div', + 'p', + 'pre', +]; + export const responseKeys = [ 'multiplechoiceresponse', 'numericalresponse', @@ -62,57 +74,52 @@ export const stripNonTextTags = ({ input, tag }) => { export class OLXParser { constructor(olxString) { - this.problem = {}; - this.questionData = {}; - this.richTextProblem = {}; - const richTextOptions = { + // There are two versions of the parsed XLM because the fields using tinymce require the order + // of the parsed data and spacing values to be preserved. However, all the other widgets need + // the data grouped by the wrapping tag. Examples of the parsed format can be found here: + // https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/docs/v4/2.XMLparseOptions.md + const baseParserOptions = { ignoreAttributes: false, - alwaysCreateTextNode: true, numberParseOptions: { leadingZeros: false, hex: false, }, - preserveOrder: true, processEntities: false, }; + + // Base Parser + this.problem = {}; const parserOptions = { - ignoreAttributes: false, + ...baseParserOptions, alwaysCreateTextNode: true, - numberParseOptions: { - leadingZeros: false, - hex: false, - }, - processEntities: false, }; const builderOptions = { - ignoreAttributes: false, - numberParseOptions: { - leadingZeros: false, - hex: false, - }, - processEntities: false, + ...baseParserOptions, }; - const richTextBuilderOptions = { - ignoreAttributes: false, - numberParseOptions: { - leadingZeros: false, - hex: false, - }, - preserveOrder: true, - processEntities: false, - }; - // There are two versions of the parsed XLM because the fields using tinymce require the order - // of the parsed data to be preserved. However, all the other widgets need the data grouped by - // the wrapping tag. - const richTextParser = new XMLParser(richTextOptions); const parser = new XMLParser(parserOptions); this.builder = new XMLBuilder(builderOptions); - this.richTextBuilder = new XMLBuilder(richTextBuilderOptions); this.parsedOLX = parser.parse(olxString); - this.richTextOLX = richTextParser.parse(olxString); if (_.has(this.parsedOLX, 'problem')) { this.problem = this.parsedOLX.problem; - this.questionData = this.richTextOLX[0].problem; + } + + // Parser with `preservedOrder: true` and `trimValues: false` + this.richTextProblem = []; + const richTextOptions = { + ...baseParserOptions, + alwaysCreateTextNode: true, + preserveOrder: true, + trimValues: false, + }; + const richTextBuilderOptions = { + ...baseParserOptions, + preserveOrder: true, + trimValues: false, + }; + const richTextParser = new XMLParser(richTextOptions); + this.richTextBuilder = new XMLBuilder(richTextBuilderOptions); + this.richTextOLX = richTextParser.parse(olxString); + if (_.has(this.parsedOLX, 'problem')) { this.richTextProblem = this.richTextOLX[0].problem; } } @@ -462,7 +469,7 @@ export class OLXParser { * @return {string} string of OLX */ parseQuestions(problemType) { - const problemArray = _.get(this.questionData[0], problemType) || this.questionData; + const problemArray = _.get(this.richTextProblem[0], problemType) || this.richTextProblem; const questionArray = []; problemArray.forEach(tag => { @@ -478,7 +485,7 @@ export class OLXParser { */ tag[tagName].forEach(subTag => { const subTagName = Object.keys(subTag)[0]; - if (subTagName === 'label' || subTagName === 'description') { + if (subTagName === 'label' || subTagName === 'description' || richTextFormats.includes(subTagName)) { questionArray.push(subTag); } }); @@ -503,11 +510,13 @@ export class OLXParser { if (objKeys.includes('demandhint')) { const currentDemandHint = obj.demandhint; currentDemandHint.forEach(hint => { - const hintValue = this.richTextBuilder.build(hint.hint); - hintsObject.push({ - id: hintsObject.length, - value: hintValue, - }); + if (Object.keys(hint).includes('hint')) { + const hintValue = this.richTextBuilder.build(hint.hint); + hintsObject.push({ + id: hintsObject.length, + value: hintValue, + }); + } }); } }); @@ -526,21 +535,17 @@ export class OLXParser { getSolutionExplanation(problemType) { if (!_.has(this.problem, `${problemType}.solution`) && !_.has(this.problem, 'solution')) { return null; } const [problemBody] = this.richTextProblem.filter(section => Object.keys(section).includes(problemType)); - let { solution } = problemBody[problemType].pop(); - const { div } = solution[0]; - if (solution.length === 1 && div) { - div.forEach((block) => { - const [key] = Object.keys(block); - const [value] = block[key]; - if ((key === 'p' || key === 'h2') - && (_.get(value, '#text', null) === 'Explanation') - ) { - div.shift(); + const [solutionBody] = problemBody[problemType].filter(section => Object.keys(section).includes('solution')); + const [divBody] = solutionBody.solution.filter(section => Object.keys(section).includes('div')); + const solutionArray = []; + if (divBody && divBody.div) { + divBody.div.forEach(tag => { + if (_.get(Object.values(tag)[0][0], '#text', null) !== 'Explanation') { + solutionArray.push(tag); } }); - solution = div; } - const solutionString = this.richTextBuilder.build(solution); + const solutionString = this.richTextBuilder.build(solutionArray); return solutionString; } diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.test.js b/src/editors/containers/ProblemEditor/data/OLXParser.test.js index 6f785c906..d3922330a 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.test.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.test.js @@ -1,7 +1,6 @@ import { OLXParser } from './OLXParser'; import { checkboxesOLXWithFeedbackAndHintsOLX, - getCheckboxesOLXWithFeedbackAndHintsOLX, dropdownOLXWithFeedbackAndHintsOLX, numericInputWithFeedbackAndHintsOLX, textInputWithFeedbackAndHintsOLX, @@ -21,6 +20,7 @@ import { labelDescriptionQuestionOLX, htmlEntityTestOLX, numberParseTestOLX, + solutionExplanationTest, } from './mockData/olxTestData'; import { ProblemTypeKeys } from '../../../data/constants/problem'; @@ -261,13 +261,13 @@ describe('OLXParser', () => { const problemType = olxparser.getProblemType(); const question = olxparser.parseQuestions(problemType); it('should return an empty string for question', () => { - expect(question).toBe(blankQuestionOLX.question); + expect(question.trim()).toBe(blankQuestionOLX.question); }); }); describe('given a simple problem olx', () => { const question = textInputOlxParser.parseQuestions('stringresponse'); it('should return a string of HTML', () => { - expect(question).toEqual(textInputWithFeedbackAndHintsOLX.question); + expect(question.trim()).toEqual(textInputWithFeedbackAndHintsOLX.question); }); }); describe('given olx with html entities', () => { @@ -275,7 +275,7 @@ describe('OLXParser', () => { const problemType = olxparser.getProblemType(); const question = olxparser.parseQuestions(problemType); it('should not encode html entities', () => { - expect(question).toEqual(htmlEntityTestOLX.question); + expect(question.trim()).toEqual(htmlEntityTestOLX.question); }); }); describe('given olx with styled content', () => { @@ -283,7 +283,7 @@ describe('OLXParser', () => { const problemType = olxparser.getProblemType(); const question = olxparser.parseQuestions(problemType); it('should pase/build correct styling', () => { - expect(question).toBe(styledQuestionOLX.question); + expect(question.trim()).toBe(styledQuestionOLX.question); }); }); describe('given olx with label and description tags inside response tag', () => { @@ -291,20 +291,25 @@ describe('OLXParser', () => { const problemType = olxparser.getProblemType(); const question = olxparser.parseQuestions(problemType); it('should append the label/description to the question', () => { - expect(question).toBe(labelDescriptionQuestionOLX.question); + expect(question.trim()).toBe(labelDescriptionQuestionOLX.question); }); }); }); describe('getSolutionExplanation()', () => { describe('for checkbox questions', () => { test('should parse text in p tags', () => { - const { rawOLX } = getCheckboxesOLXWithFeedbackAndHintsOLX(); - const olxparser = new OLXParser(rawOLX); + const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX); const problemType = olxparser.getProblemType(); const explanation = olxparser.getSolutionExplanation(problemType); - const expected = getCheckboxesOLXWithFeedbackAndHintsOLX().solutionExplanation; + const expected = checkboxesOLXWithFeedbackAndHintsOLX.solutionExplanation; expect(explanation.replace(/\s/g, '')).toBe(expected.replace(/\s/g, '')); }); }); + it('should parse text with proper spacing', () => { + const olxparser = new OLXParser(solutionExplanationTest.rawOLX); + const problemType = olxparser.getProblemType(); + const explanation = olxparser.getSolutionExplanation(problemType); + expect(explanation).toBe(solutionExplanationTest.solutionExplanation); + }); }); }); diff --git a/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js b/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js index 43a723582..ae3fd2d58 100644 --- a/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js +++ b/src/editors/containers/ProblemEditor/data/mockData/olxTestData.js @@ -1,4 +1,7 @@ -export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({ +/* eslint-disable */ +// lint is disabled for this file due to strict spacing + +export const checkboxesOLXWithFeedbackAndHintsOLX = { rawOLX: `

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.

@@ -8,12 +11,12 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({

a correct answer

You can specify optional feedback that appears after the learner selects and submits this answer.

You can specify optional feedback that appears after the learner clears and submits this answer.

-
+

an incorrect answer

an incorrect answer

You can specify optional feedback for none, all, or a subset of the answers.

You can specify optional feedback for selected answers, cleared answers, or both.

-
+

a correct answer

You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted. You can specify optional feedback for one, several, or all answer combinations. @@ -56,7 +59,7 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({ answers: [ { id: 'A', - title: '

a correct answer

', + title: `

a correct answer

\n \n \n `, correct: true, selectedFeedback: '

You can specify optional feedback that appears after the learner selects and submits this answer.

', unselectedFeedback: '

You can specify optional feedback that appears after the learner clears and submits this answer.

', @@ -68,7 +71,7 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({ }, { id: 'C', - title: '

an incorrect answer

', + title: `

an incorrect answer

\n \n \n `, correct: false, selectedFeedback: '

You can specify optional feedback for none, all, or a subset of the answers.

', unselectedFeedback: '

You can specify optional feedback for selected answers, cleared answers, or both.

', @@ -103,45 +106,43 @@ export const getCheckboxesOLXWithFeedbackAndHintsOLX = () => ({ }, 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.

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.

- - 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.

-

You can specify optional feedback that appears after the learner clears and submits this answer.

-
-

an incorrect answer

- -

an incorrect answer

You can specify optional feedback for none, all, or a subset of the answers.

-

You can specify optional feedback for selected answers, cleared answers, or both.

-
-

a correct answer

- You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted. - You can specify optional feedback for one, several, or all answer combinations. -
- -
-

Explanation

-

+ +

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. + + +

a correct answer

You can specify optional feedback that appears after the learner selects and submits this answer.

+

You can specify optional feedback that appears after the learner clears and submits this answer.

+
+

an incorrect answer

+ +

an incorrect answer

You can specify optional feedback for none, all, or a subset of the answers.

+

You can specify optional feedback for selected answers, cleared answers, or both.

+
+

a correct answer

+ You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted. + You can specify optional feedback for one, several, or all answer combinations. +
+ +
+

Explanation

+

You can form a voltage divider that evenly divides the input voltage with two identically valued resistors, with the sampled voltage taken in between the two. -

-

-
-
- - -

You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.

-

If you add more than one hint, a different hint appears each time learners select the hint button.

-
- -`, -}); - -export const checkboxesOLXWithFeedbackAndHintsOLX = getCheckboxesOLXWithFeedbackAndHintsOLX({}); +

+

+
+
+
+ +

You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.

+

If you add more than one hint, a different hint appears each time learners select the hint button.

+
+
+ `, +}; export const multipleChoiceWithoutAnswers = { rawOLX: ` @@ -199,7 +200,7 @@ export const multipleChoiceSingleAnswer = { answers: [ { id: 'A', - title: '

a correct answer

image withcaption.
', + title: `

a correct answer

image with caption.
\n \n \n `, correct: true, selectedFeedback: '

You can specify optional feedback that appears after the learner selects and submits this answer.

', unselectedFeedback: '

You can specify optional feedback that appears after the learner clears and submits this answer.

', @@ -314,11 +315,9 @@ export const multipleChoiceWithFeedbackAndHintsOLX = { You can add an optional tip or note related to the prompt like this. -

an incorrect answer

You can specify optional feedback like this, which appears after this answer is submitted.

-
+

an incorrect answer

You can specify optional feedback like this, which appears after this answer is submitted.

the correct answer

-

an incorrect answer

You can specify optional feedback for none, a subset, or all of the answers. - +

an incorrect answer

You can specify optional feedback for none, a subset, or all of the answers.

You can add a solution

@@ -540,7 +539,7 @@ export const textInputWithFeedbackAndHintsOLX = { }, }, }, - question: '

You can use this template as a guide to the simple editor markdown and OLX markup to use for text input 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.', + question: '

You can use this template as a guide to the simple editor markdown and OLX markup to use for text input 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. ', buildOLX: `

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

@@ -726,7 +725,13 @@ export const styledQuestionOLX = {
`, - question: '

test

', + question: `

+ + + test + + +

`, }; export const shuffleProblemOLX = { @@ -763,7 +768,8 @@ export const labelDescriptionQuestionOLX = {
`, - question: '

boiling eggs: water system

Watch out, boiling water is hot', + question: `

boiling eggs: water system

+ Watch out, boiling water is hot`, }; export const htmlEntityTestOLX = { @@ -798,9 +804,7 @@ export const htmlEntityTestOLX = { }, ], }, - // eslint-disable-next-line - question: `

What is the content of the register x2 after executing the following three lines of instructions?

Address          assembly instructions

0x0              addi x1, x0, 1

0x4              slli x2, x1, 4

0x8              sub x1, x2, x1

`, - // eslint-disable-next-line + question: `

What is the content of the register x2 after executing the following three lines of instructions?

Address          assembly instructions

0x0              addi x1, x0, 1

0x4              slli x2, x1, 4

0x8              sub x1, x2, x1

`, solutionExplanation: `

Address          assembly instructions    comment

0x0              addi x1, x0, 1           x1 = 0x1

0x4              slli x2, x1, 4           x2 = x1 << 4 = 0x10

0x8              sub x1, x2, x1           x1 = x2 - x1 = 0x10 - 0x01 = 0xf

`, }; @@ -820,22 +824,22 @@ export const numberParseTestOLX = { answers: [ { id: 'A', - title: `0x10`, // eslint-disable-line + title: `0x10`, correct: false, }, { id: 'B', - title: `0x0f`, // eslint-disable-line + title: `0x0f`, correct: true, }, { id: 'C', - title: `0x07`, // eslint-disable-line + title: `0x07`, correct: false, }, { id: 'D', - title: `0009`, // eslint-disable-line + title: `0009`, correct: false, }, ], @@ -853,3 +857,26 @@ export const numberParseTestOLX = {
`, }; + +export const solutionExplanationTest = { + rawOLX: ` + How 99 long is the array q after the following loop runs? +
for i = 1:99
+      q(2*i - 1) = i;
+      end
+ + +
`, + solutionExplanation: `\n + This loop will iterate 99 times, but the length of q will not be 99 due to indexing with the value 2*i -1. On the last iteration, i = 99, so 2*i - 1 = 2*78 - 1 = 197. This will be the last position filled in q, so the answer is 197.\n `, +}; diff --git a/src/editors/data/services/cms/mockApi.js b/src/editors/data/services/cms/mockApi.js index bb407f4e0..bea7fc463 100644 --- a/src/editors/data/services/cms/mockApi.js +++ b/src/editors/data/services/cms/mockApi.js @@ -36,7 +36,21 @@ export const fetchBlockById = ({ blockId, studioEndpointUrl }) => { } else if (blockId === 'problem-block-id') { data = { data: ` - `, + +

What is the content of the register x2 after executing the following three lines of instructions?

+

Address          assembly instructions
0x0              addi x1, x0, 1
0x4              slli x2, x1, 4
0x8              sub x1, x2, x1

+ + answerA + answerB + + +
+

Explanation

+

Address          assembly instructions    comment
0x0              addi x1, x0, 1           x1 = 0x1
0x4              slli x2, x1, 4           x2 = x1 << 4 = 0x10
0x8              sub x1, x2, x1           x1 = x2 - x1 = 0x10 - 0x01 = 0xf

+
+
+
+ `, display_name: 'Dropdown', metadata: { markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.