Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
import '@testing-library/jest-dom'
import type { ElementType } from 'react'
import { readFileSync } from 'fs'
import { render, screen } from '@testing-library/react'
import remarkBreaks from 'remark-breaks'
Expand All @@ -9,6 +10,9 @@ import { MarkdownContent } from './MarkdownContent'

interface MarkdownRendererProps {
children: string
components: {
a: ElementType
}
remarkPlugins: unknown[]
skipHtml: boolean
}
Expand Down Expand Up @@ -65,6 +69,28 @@ describe('MarkdownContent', () => {
}))
})

it('opens GFM links in a safe new tab', () => {
const markdown = 'https://www.topcoder-dev.com/challenges'

render(<MarkdownContent markdown={markdown} />)
const markdownProps = mockReactMarkdown.mock.calls[0][0] as MarkdownRendererProps
const MarkdownLink = markdownProps.components.a

render(
<MarkdownLink href={markdown}>
{markdown}
</MarkdownLink>,
)
const link = screen.getByRole('link', { name: markdown })

expect(link)
.toHaveAttribute('href', markdown)
expect(link)
.toHaveAttribute('target', '_blank')
expect(link)
.toHaveAttribute('rel', 'noopener noreferrer')
})

it('keeps Markdown formatting visible after the platform style reset', () => {
expect(markdownStyles)
.toMatch(/a,[\s\S]*a:hover \{[\s\S]*color: \$link-blue-dark;[\s\S]*text-decoration: underline;/)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Safe Markdown renderer for user-authored support content. */
import { FC } from 'react'
import ReactMarkdown from 'react-markdown'
import type { FC } from 'react'
import ReactMarkdown, { type Components } from 'react-markdown'
import remarkBreaks from 'remark-breaks'
import remarkGfm from 'remark-gfm'

Expand All @@ -10,8 +10,28 @@ export interface MarkdownContentProps {
markdown: string
}

const markdownComponents: Components = {
/**
* Renders a Markdown link in a new tab without exposing the opener page.
*
* @param props anchor attributes and AST metadata produced by ReactMarkdown.
* @returns a safely targeted anchor used for links in support conversations.
* @throws Does not throw.
*/
a: props => (
<a
href={props.href}
rel='noopener noreferrer'
target='_blank'
title={props.title}
>
{props.children}
</a>
),
}

/**
* Renders GFM and line breaks while dropping raw HTML.
* Renders GFM and line breaks with links opening in new tabs while dropping raw HTML.
*
* @param props untrusted Markdown source.
* @returns safely rendered Markdown.
Expand All @@ -20,6 +40,7 @@ export interface MarkdownContentProps {
export const MarkdownContent: FC<MarkdownContentProps> = props => (
<div className={styles.markdown}>
<ReactMarkdown
components={markdownComponents}
remarkPlugins={[remarkGfm, remarkBreaks]}
skipHtml
>
Expand Down
Loading