Skip to content

Commit 5a581b0

Browse files
vladislavkeblysharbrandes
authored andcommitted
feat: add MathJax support for formula rendering in ORA responses
- Add better-react-mathjax dependency - Wrap ResponseDisplay content with MathJax component - Add MathJaxContext to App with MathJax v3 config - Support inline (\(...\), $...$, [mathjaxinline]) and display (\\[...\\], $$...$$, [mathjax]) math delimiters - Update tests
1 parent 8a2c6aa commit 5a581b0

7 files changed

Lines changed: 162 additions & 11 deletions

File tree

package-lock.json

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@testing-library/user-event": "^14.0.0",
4444
"@zip.js/zip.js": "^2.4.6",
4545
"axios": "^0.28.0",
46+
"better-react-mathjax": "^2.0.3",
4647
"classnames": "^2.3.1",
4748
"core-js": "3.35.1",
4849
"dompurify": "^2.3.1",

src/App.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { selectors } from 'data/redux';
1010

1111
import DemoWarning from 'containers/DemoWarning';
1212
import ListView from 'containers/ListView';
13+
import { MathJaxContext } from 'better-react-mathjax';
1314

1415
import './App.scss';
1516
import Head from './components/Head';
17+
import { mathJaxConfig } from './utils';
1618

1719
export const App = ({ courseMetadata, isEnabled }) => (
1820
<Router>
@@ -26,7 +28,9 @@ export const App = ({ courseMetadata, isEnabled }) => (
2628
/>
2729
{!isEnabled && <DemoWarning />}
2830
<main data-testid="main">
29-
<ListView />
31+
<MathJaxContext config={mathJaxConfig}>
32+
<ListView />
33+
</MathJaxContext>
3034
</main>
3135
<FooterSlot />
3236
</div>

src/containers/ResponseDisplay/index.jsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { connect } from 'react-redux';
4+
import { MathJax } from 'better-react-mathjax';
45

56
import { Card } from '@openedx/paragon';
67

@@ -62,12 +63,14 @@ export class ResponseDisplay extends React.Component {
6263
{
6364
/* eslint-disable react/no-array-index-key */
6465
this.textContents.map((textContent, index) => (
65-
<>
66+
<MathJax key={index}>
6667
{ multiPrompt && <PromptDisplay prompt={prompts[index]} /> }
67-
<Card className="response-display-card" key={index}>
68-
<Card.Section className="response-display-text-content" data-testid="response-display-text-content">{textContent}</Card.Section>
68+
<Card className="response-display-card">
69+
<Card.Section className="response-display-text-content" data-testid="response-display-text-content">
70+
{textContent}
71+
</Card.Section>
6972
</Card>
70-
</>
73+
</MathJax>
7174
))
7275
}
7376
</div>

src/containers/ResponseDisplay/index.test.jsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import { MathJaxContext } from 'better-react-mathjax';
12
import { render, screen } from '@testing-library/react';
23
import { fileUploadResponseOptions } from 'data/services/lms/constants';
34
import { selectors } from 'data/redux';
45
import { ResponseDisplay, mapStateToProps } from '.';
56

7+
jest.mock('better-react-mathjax', () => ({
8+
MathJax: ({ children }) => <div data-testid="mathjax">{children}</div>,
9+
MathJaxContext: ({ children }) => <div>{children}</div>,
10+
}));
11+
612
jest.mock('data/redux', () => ({
713
selectors: {
814
grading: {
@@ -68,41 +74,65 @@ describe('ResponseDisplay', () => {
6874

6975
describe('behavior', () => {
7076
it('renders response display container', () => {
71-
const { container } = render(<ResponseDisplay {...defaultProps} />);
77+
const { container } = render(
78+
<MathJaxContext>
79+
<ResponseDisplay {...defaultProps} />
80+
</MathJaxContext>,
81+
);
7282
const responseDisplay = container.querySelector('.response-display');
7383
expect(responseDisplay).toBeInTheDocument();
7484
});
7585

7686
it('displays text content in cards', () => {
77-
const { container } = render(<ResponseDisplay {...defaultProps} />);
87+
const { container } = render(
88+
<MathJaxContext>
89+
<ResponseDisplay {...defaultProps} />
90+
</MathJaxContext>,
91+
);
7892
const textContents = container.querySelectorAll('.response-display-text-content');
7993
expect(textContents).toHaveLength(defaultProps.response.text.length);
8094
expect(textContents[0]).toHaveTextContent('some text response here');
8195
expect(textContents[1]).toHaveTextContent('another text response');
8296
});
8397

8498
it('displays submission files when file upload is allowed', () => {
85-
render(<ResponseDisplay {...defaultProps} />);
99+
render(
100+
<MathJaxContext>
101+
<ResponseDisplay {...defaultProps} />
102+
</MathJaxContext>,
103+
);
86104
const submissionFiles = screen.getByTestId('submission-files');
87105
expect(submissionFiles).toBeInTheDocument();
88106
expect(submissionFiles).toHaveTextContent('Files: 2');
89107
});
90108

91109
it('displays preview display when file upload is allowed', () => {
92-
render(<ResponseDisplay {...defaultProps} />);
110+
render(
111+
<MathJaxContext>
112+
<ResponseDisplay {...defaultProps} />
113+
</MathJaxContext>,
114+
);
93115
const previewDisplay = screen.getByTestId('preview-display');
94116
expect(previewDisplay).toBeInTheDocument();
95117
expect(previewDisplay).toHaveTextContent('Preview: 2');
96118
});
97119

98120
it('does not display file components when file upload is disabled', () => {
99-
render(<ResponseDisplay {...defaultProps} fileUploadResponseConfig={fileUploadResponseOptions.none} />);
121+
render(
122+
<MathJaxContext>
123+
<ResponseDisplay {...defaultProps} fileUploadResponseConfig={fileUploadResponseOptions.none} />
124+
</MathJaxContext>,
125+
);
100126
expect(screen.queryByTestId('submission-files')).not.toBeInTheDocument();
101127
expect(screen.queryByTestId('preview-display')).not.toBeInTheDocument();
102128
});
103129

104130
it('renders empty content when no text response provided', () => {
105-
const { container } = render(<ResponseDisplay {...defaultProps} response={{ text: [], files: [] }} />);
131+
const { container } = render(
132+
<MathJaxContext>
133+
<ResponseDisplay {...defaultProps} response={{ text: [], files: [] }} />
134+
</MathJaxContext>,
135+
);
106136
const textContents = container.querySelectorAll('.response-display-text-content');
107137
expect(textContents).toHaveLength(0);
108138
});

src/utils/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { default as StrictDict } from './StrictDict';
22
export { default as keyStore } from './keyStore';
3+
export { default as mathJaxConfig } from './mathJaxConfig';

src/utils/mathJaxConfig.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const mathJaxConfig = {
2+
tex: {
3+
inlineMath: [
4+
['$', '$'],
5+
['\\(', '\\)'],
6+
['[mathjaxinline]', '[/mathjaxinline]'],
7+
],
8+
displayMath: [
9+
['$$', '$$'],
10+
['\\[', '\\]'],
11+
['[mathjax]', '[/mathjax]'],
12+
],
13+
processEscapes: true,
14+
},
15+
options: {
16+
enableMenu: false,
17+
},
18+
chtml: {
19+
linebreaks: { automatic: true },
20+
},
21+
svg: {
22+
linebreaks: { automatic: true },
23+
},
24+
};
25+
26+
export default mathJaxConfig;

0 commit comments

Comments
 (0)