This commit is contained in:
C. Fuhrman 2025-01-26 09:33:42 -05:00
parent a59574a8b3
commit 910b83def2
11 changed files with 47 additions and 42 deletions

View file

@ -1,4 +1,4 @@
import { FormatTextTemplate } from "src/components/GiftTemplate/templates/TextTypeTemplate";
import { FormattedTextTemplate } from "src/components/GiftTemplate/templates/TextTypeTemplate";
import { TextFormat } from "gift-pegjs";
describe('TextType', () => {
@ -8,7 +8,7 @@ describe('TextType', () => {
format: 'moodle'
};
const expectedOutput = 'Hello, world! 5 > 3, right?';
expect(FormatTextTemplate(input)).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
it('should format text with newlines correctly', () => {
@ -17,7 +17,7 @@ describe('TextType', () => {
format: 'plain'
};
const expectedOutput = 'Hello,<br>world!<br>5 > 3, right?';
expect(FormatTextTemplate(input)).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
it('should format text with LaTeX correctly', () => {
@ -31,7 +31,7 @@ describe('TextType', () => {
// by running the test and copying the "Received string:" in jest output
// when it fails (assuming the output is correct)
const expectedOutput = '<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(FormatTextTemplate(input)).toContain(expectedOutput);
expect(FormattedTextTemplate(input)).toContain(expectedOutput);
});
it('should format text with two equations (inline and separate) correctly', () => {
@ -41,7 +41,7 @@ describe('TextType', () => {
};
// hint: katex-display is the class that indicates a separate equation
const expectedOutput = '<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">a + b = c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">c</span></span></span></span> ? <span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(FormatTextTemplate(input)).toContain(expectedOutput);
expect(FormattedTextTemplate(input)).toContain(expectedOutput);
});
it('should format text with a katex matrix correctly', () => {
@ -51,7 +51,7 @@ describe('TextType', () => {
format: 'plain'
};
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow></mrow><annotation encoding="application/x-tex"></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}';
expect(FormatTextTemplate(input)).toContain(expectedOutput);
expect(FormattedTextTemplate(input)).toContain(expectedOutput);
});
it('should format text with Markdown correctly', () => {
@ -61,7 +61,7 @@ describe('TextType', () => {
};
// TODO: investigate why the output has an extra newline
const expectedOutput = '<strong>Bold</strong>\n';
expect(FormatTextTemplate(input)).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
it('should format text with HTML correctly', () => {
@ -70,7 +70,7 @@ describe('TextType', () => {
format: 'html'
};
const expectedOutput = '<em>yes</em>';
expect(FormatTextTemplate(input)).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
it('should format plain text correctly', () => {
@ -79,7 +79,7 @@ describe('TextType', () => {
format: 'plain'
};
const expectedOutput = 'Just plain text';
expect(FormatTextTemplate(input)).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
// Add more tests for other formats if needed

View file

@ -1,5 +1,5 @@
import { TemplateOptions } from './types';
import {FormatTextTemplate} from './TextTypeTemplate';
import {FormattedTextTemplate} from './TextTypeTemplate';
import { state } from '.';
import { theme } from '../constants';
import { TextFormat } from 'gift-pegjs';
@ -20,7 +20,7 @@ export default function GlobalFeedbackTemplate({ format, text }: GlobalFeedbackO
return (format && text)
? `<div style="${Container}">
<p>${FormatTextTemplate({format: format, text: text})}</p>
<p>${FormattedTextTemplate({format: format, text: text})}</p>
</div>`
: ``;
}

View file

@ -1,7 +1,7 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import Title from './TitleTemplate';
import {FormatTextTemplate} from './TextTypeTemplate';
import {FormattedTextTemplate} from './TextTypeTemplate';
import GlobalFeedback from './GlobalFeedbackTemplate';
import { ParagraphStyle, SelectStyle } from '../constants';
import { state } from '.';
@ -67,7 +67,7 @@ function MatchAnswers({ choices }: MatchAnswerOptions): string {
.map(({ formattedSubquestion }) => {
return `
<div style="${OptionTable} ${ParagraphStyle(state.theme)}">
${FormatTextTemplate(formattedSubquestion)}
${FormattedTextTemplate(formattedSubquestion)}
</div>
<div>
<select class="gift-select" style="${SelectStyle(state.theme)} ${Dropdown}">

View file

@ -1,6 +1,6 @@
import { nanoid } from 'nanoid';
import { TemplateOptions } from './types';
import {FormatTextTemplate} from './TextTypeTemplate';
import {FormattedTextTemplate} from './TextTypeTemplate';
import AnswerIcon from './AnswerIconTemplate';
import { state } from '.';
import { ParagraphStyle, theme } from '../constants';
@ -42,7 +42,7 @@ export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoic
}" id="${inputId}" name="${id}">
${AnswerWeight({ correct: isCorrectOption, weight: weight })}
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">
${FormatTextTemplate(formattedText)}
${FormattedTextTemplate(formattedText)}
</label>
${AnswerIcon({ correct: isCorrectOption })}
${AnswerFeedback({ formattedFeedback: formattedFeedback })}
@ -86,5 +86,5 @@ function AnswerFeedback({ formattedFeedback }: AnswerFeedbackOptions): string {
color: ${theme(state.theme, 'teal700', 'gray700')};
`;
return formattedFeedback ? `<span style="${Container}">${FormatTextTemplate(formattedFeedback)}</span>` : ``;
return formattedFeedback ? `<span style="${Container}">${FormattedTextTemplate(formattedFeedback)}</span>` : ``;
}

View file

@ -1,7 +1,7 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import Title from './TitleTemplate';
import {FormatTextTemplate} from './TextTypeTemplate';
import {FormattedTextTemplate} from './TextTypeTemplate';
import GlobalFeedback from './GlobalFeedbackTemplate';
import { ParagraphStyle, InputStyle } from '../constants';
import { state } from './index';
@ -32,7 +32,7 @@ export default function ShortAnswerTemplate({
function Answers({ choices }: AnswerOptions): string {
const placeholder = choices
.map(({ text }) => FormatTextTemplate({ format: '', text: text }))
.map(({ text }) => FormattedTextTemplate({ format: '', text: text }))
.join(', ');
return `
<div>

View file

@ -2,7 +2,7 @@ import { TemplateOptions } from './types';
import { state } from '.';
import { ParagraphStyle } from '../constants';
import { BaseQuestion } from 'gift-pegjs';
import { FormatTextTemplate } from './TextTypeTemplate';
import { FormattedTextTemplate } from './TextTypeTemplate';
// Type is string to allow for custom question type text (e,g, "Multiple Choice")
interface StemOptions extends TemplateOptions {
@ -18,7 +18,7 @@ export default function StemTemplate({ formattedStem }: StemOptions): string {
<div style="${Container}">
<span>
<p style="${ParagraphStyle(state.theme)}" class="present-question-stem">
${FormatTextTemplate(formattedStem)}
${FormattedTextTemplate(formattedStem)}
</p>
</span>
</div>

View file

@ -25,22 +25,27 @@ export function formatLatex(text: string): string {
* @see marked
* @see katex
*/
export function FormatTextTemplate(formattedText: TextFormat): string {
export function FormattedTextTemplate(formattedText: TextFormat): string {
const formatText = formatLatex(formattedText.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
let parsedText = '';
let result = '';
switch (formattedText.format) {
case '':
case 'moodle':
case 'plain':
// Replace newlines with <br> tags
return DOMPurify.sanitize(formatText.replace(/(?:\r\n|\r|\n)/g, '<br>'));
result = formatText.replace(/(?:\r\n|\r|\n)/g, '<br>');
break;
case 'html':
// Strip outer paragraph tags (not a great approach with regex)
return DOMPurify.sanitize(formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2'));
result = formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
break;
case 'markdown':
parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
return DOMPurify.sanitize(parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2'));
parsedText = marked.parse(formatText, { breaks: true, gfm: true }) as string; // <br> for newlines
result = parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
break;
default:
throw new Error(`Unsupported text format: ${formattedText.format}`);
}
return DOMPurify.sanitize(result);
}

View file

@ -2,7 +2,7 @@
import React, { useEffect, useState } from 'react';
import '../questionStyle.css';
import { Button } from '@mui/material';
import { FormatTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { MultipleChoiceQuestion } from 'gift-pegjs';
interface Props {
@ -30,7 +30,7 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
return (
<div className="question-container">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedStem) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
<div className="choices-wrapper mb-1">
{question.choices.map((choice, i) => {
@ -47,13 +47,13 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
(choice.isCorrect ? '✅' : '❌')}
<div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}>
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(choice.formattedText) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedText) }} />
</div>
</Button>
{choice.formattedFeedback && showAnswer && (
<div className="feedback-container mb-1 mt-1/2">
{choice.isCorrect ? '✅' : '❌'}
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(choice.formattedFeedback) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedFeedback) }} />
</div>
)}
</div>
@ -62,7 +62,7 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
</div>
{question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedGlobalFeedback) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>
)}

View file

@ -2,7 +2,7 @@
import React, { useState } from 'react';
import '../questionStyle.css';
import { Button, TextField } from '@mui/material';
import { FormatTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { NumericalQuestion, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs';
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer } from 'gift-pegjs/typeGuards';
@ -40,13 +40,13 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
return (
<div className="question-wrapper">
<div>
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedStem) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
{showAnswer ? (
<>
<div className="correct-answer-text mb-2">{correctAnswer}</div>
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedGlobalFeedback) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>}
</>
) : (
@ -64,7 +64,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
</div>
{question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedGlobalFeedback) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>
)}
{handleOnSubmitAnswer && (

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import '../questionStyle.css';
import { Button, TextField } from '@mui/material';
import { FormatTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { ShortAnswerQuestion } from 'gift-pegjs';
interface Props {
@ -17,7 +17,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
return (
<div className="question-wrapper">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedStem) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
{showAnswer ? (
<>
@ -29,7 +29,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
))}
</div>
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedGlobalFeedback) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>}
</>
) : (

View file

@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import '../questionStyle.css';
import { Button } from '@mui/material';
import { TrueFalseQuestion } from 'gift-pegjs';
import { FormatTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate';
import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate';
interface Props {
question: TrueFalseQuestion;
@ -26,7 +26,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
return (
<div className="question-container">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedStem) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
<div className="choices-wrapper mb-1">
<Button
@ -51,18 +51,18 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
{/* selected TRUE, show True feedback if it exists */}
{showAnswer && answer && question.trueFormattedFeedback && (
<div className="true-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.trueFormattedFeedback) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
</div>
)}
{/* selected FALSE, show False feedback if it exists */}
{showAnswer && !answer && question.falseFormattedFeedback && (
<div className="false-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.falseFormattedFeedback) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
</div>
)}
{question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormatTextTemplate(question.formattedGlobalFeedback) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>
)}
{!showAnswer && handleOnSubmitAnswer && (