Skip to content

Commit 1623da9

Browse files
committed
CCM-10917 MarkdownContent component
1 parent a87451a commit 1623da9

File tree

7 files changed

+142
-11
lines changed

7 files changed

+142
-11
lines changed

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"jsonwebtoken": "^9.0.2",
1010
"jwt-decode": "^4.0.0",
1111
"markdown-it": "^13.0.2",
12+
"markdown-to-jsx": "^7.7.10",
1213
"next": "^15.2.3",
1314
"next-client-cookies": "^2.0.1",
1415
"nhs-notify-backend-client": "^0.0.1",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { MarkdownContent } from '@molecules/MarkdownContent/MarkdownContent';
4+
5+
describe('MarkdownContent', () => {
6+
it('renders nothing if segments is empty array', () => {
7+
const { container } = render(<MarkdownContent segments={[]} />);
8+
expect(container).toBeEmptyDOMElement();
9+
});
10+
11+
it('renders multiple segments in correct order', () => {
12+
const segments = ['First paragraph', 'Second [link](https://example.com)'];
13+
14+
render(<MarkdownContent segments={segments} />);
15+
16+
expect(screen.getByText('First paragraph')).toBeInTheDocument();
17+
expect(screen.getByRole('link')).toHaveAttribute(
18+
'href',
19+
'https://example.com'
20+
);
21+
expect(screen.getByRole('link')).toHaveTextContent('link');
22+
});
23+
24+
it('adds correct attributes to links', () => {
25+
const segments = ['Click [here](https://example.com)'];
26+
27+
render(<MarkdownContent segments={segments} />);
28+
29+
const link = screen.getByRole('link', { name: 'here' });
30+
expect(link).toHaveAttribute('target', '_blank');
31+
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
32+
});
33+
34+
it('renders markdown paragraphs and links correctly (snapshot)', () => {
35+
const segments = [
36+
'This is a paragraph',
37+
'Here is a [link](https://example.com)',
38+
];
39+
40+
const container = render(<MarkdownContent segments={segments} />);
41+
expect(container.asFragment()).toMatchSnapshot();
42+
});
43+
44+
it('escapes dangerous HTML, scripts, and iframes', () => {
45+
const segments = [
46+
'<script>alert("hacked!")</script>',
47+
'<img src=x onerror=alert(1)>',
48+
'<iframe src="https://malicious-site.com"></iframe>',
49+
];
50+
51+
const { container } = render(<MarkdownContent segments={segments} />);
52+
53+
expect(container.querySelector('script')).toBeNull();
54+
expect(container.querySelector('img')).toBeNull();
55+
expect(container.querySelector('iframe')).toBeNull();
56+
});
57+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`MarkdownContent renders markdown paragraphs and links correctly (snapshot) 1`] = `
4+
<DocumentFragment>
5+
<p>
6+
This is a paragraph
7+
</p>
8+
<p>
9+
Here is a
10+
<a
11+
href="https://example.com"
12+
rel="noopener noreferrer"
13+
target="_blank"
14+
>
15+
link
16+
</a>
17+
</p>
18+
</DocumentFragment>
19+
`;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// import ReactMarkdown from 'react-markdown';
2+
import Markdown from 'markdown-to-jsx';
3+
import React from 'react';
4+
5+
type MarkdownContentProps = { segments: string[] };
6+
7+
export function MarkdownContent({ segments }: MarkdownContentProps) {
8+
return (
9+
<>
10+
{segments.map((content, index) => {
11+
return (
12+
// <ReactMarkdown
13+
// key={index}
14+
// children={content}
15+
// allowedElements={['a', 'p']}
16+
// components={{
17+
// a(props) {
18+
// const { node, ...rest } = props;
19+
// return (
20+
// <a {...rest} target='_blank' rel='noopener noreferrer' />
21+
// );
22+
// },
23+
// }}
24+
// />
25+
<Markdown
26+
key={index}
27+
options={{
28+
forceBlock: true,
29+
wrapper: React.Fragment,
30+
disableParsingRawHTML: true,
31+
overrides: {
32+
a: {
33+
component: 'a',
34+
props: {
35+
rel: 'noopener noreferrer',
36+
target: '_blank',
37+
},
38+
},
39+
},
40+
}}
41+
>
42+
{content}
43+
</Markdown>
44+
);
45+
})}
46+
</>
47+
);
48+
}

frontend/src/components/molecules/TemplateSubmitted/TemplateSubmitted.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import Link from 'next/link';
44
import { NHSNotifyMain } from '@atoms/NHSNotifyMain/NHSNotifyMain';
55
import content from '@content/content';
6-
import { withEmbeddedLink } from '@utils/markdownit';
6+
// import { withEmbeddedLink } from '@utils/markdownit';
77
import React from 'react';
8+
import { MarkdownContent } from '@molecules/MarkdownContent/MarkdownContent';
89

910
type TemplateSubmittedProps = { templateId: string; templateName: string };
1011

@@ -61,16 +62,7 @@ export const TemplateSubmitted = ({
6162
<h3 className='nhsuk-u-margin-top-6 nhsuk-u-margin-bottom-3'>
6263
{heading}
6364
</h3>
64-
{text.map((textContent, i) => {
65-
return (
66-
<p
67-
key={`do-next-${index}-${i}`}
68-
dangerouslySetInnerHTML={{
69-
__html: withEmbeddedLink(textContent),
70-
}}
71-
/>
72-
);
73-
})}
65+
<MarkdownContent segments={text} />
7466
</React.Fragment>
7567
);
7668
})}

frontend/src/content/content.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ const templateSubmitted = {
686686
],
687687
},
688688
],
689+
// doNext: "### If you've sent messages using NHS Notify\n\n[Raise a request with the Service Desk (opens in a new tab)](https://nhsdigitallive.service-now.com/csm?id=sc_cat_item&sys_id=ce81c3ae1b1c5190892d4046b04bcb83) once you've submitted all your templates.\n\nIf you replaced a template by submitting a new one, tell us which template you want to use in your Service Desk request.",
689690
backLinkText: backToAllTemplates,
690691
};
691692

package-lock.json

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

0 commit comments

Comments
 (0)