Skip to content

Commit

Permalink
Open links in new tab & disable links if needed
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasikas committed Oct 20, 2020
1 parent 239688f commit a73e977
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { memo } from 'react';
import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';

type MarkdownLinkProps = { disableLinks?: boolean } & EuiLinkAnchorProps;

const MarkdownLinkComponent: React.FC<MarkdownLinkProps> = ({
disableLinks,
href,
target,
children,
...props
}) => (
<>
{disableLinks ? (
<span>{children}</span>
) : (
<EuiLink
{...props}
target="_blank"
data-test-subj="markdown-link"
href={disableLinks ? undefined : href}
rel="nofollow"
>
{children}
</EuiLink>
)}
</>
);

export const MarkdownLink = memo(MarkdownLinkComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins();

parsingPlugins.push(timelineMarkdownPlugin.parser);

// This line of code is TS-compatible and it will break if [1][1] change in the future.
processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { mount } from 'enzyme';

import { MarkdownRenderer } from './renderer';

describe('Markdown', () => {
describe('markdown links', () => {
const markdownWithLink = 'A link to an external site [External Site](https://google.com)';

test('it renders the expected link text', () => {
const wrapper = mount(<MarkdownRenderer>{markdownWithLink}</MarkdownRenderer>);

expect(wrapper.find('[data-test-subj="markdown-link"]').first().text()).toEqual(
'External Site'
);
});

test('it renders the expected href', () => {
const wrapper = mount(<MarkdownRenderer>{markdownWithLink}</MarkdownRenderer>);

expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
'href',
'https://google.com/'
);
});

test('it does NOT render the href if links are disabled', () => {
const wrapper = mount(
<MarkdownRenderer disableLinks={true}>{markdownWithLink}</MarkdownRenderer>
);

expect(wrapper.find('[data-test-subj="markdown-link"]').exists()).toBeFalsy();
});

test('it opens links in a new tab via target="_blank"', () => {
const wrapper = mount(<MarkdownRenderer>{markdownWithLink}</MarkdownRenderer>);

expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
'target',
'_blank'
);
});

test('it sets the link `rel` attribute to `noopener` to prevent the new page from accessing `window.opener`, `nofollow` to note the link is not endorsed by us, and noreferrer to prevent the browser from sending the current address', () => {
const wrapper = mount(<MarkdownRenderer>{markdownWithLink}</MarkdownRenderer>);

expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty(
'rel',
'nofollow noopener noreferrer'
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { memo } from 'react';
import { EuiMarkdownFormat } from '@elastic/eui';
import React, { memo, useMemo } from 'react';
import { cloneDeep } from 'lodash/fp';
import { EuiMarkdownFormat, EuiLinkAnchorProps } from '@elastic/eui';

import { parsingPlugins, processingPlugins } from './plugins';
import { MarkdownLink } from './markdown_link';

interface Props {
children: string;
disableLinks?: boolean;
}

const MarkdownRendererComponent: React.FC<Props> = ({ children }) => {
const MarkdownRendererComponent: React.FC<Props> = ({ children, disableLinks }) => {
const MarkdownLinkProcessingComponent: React.FC<EuiLinkAnchorProps> = useMemo(
() => (props) => <MarkdownLink {...props} disableLinks={disableLinks} />,
[disableLinks]
);

// Deep clone of the processing plugins to prevent affecting the markdown editor.
const processingPluginList = cloneDeep(processingPlugins);
// This line of code is TS-compatible and it will break if [1][1] change in the future.
processingPluginList[1][1].components.a = MarkdownLinkProcessingComponent;

return (
<EuiMarkdownFormat parsingPluginList={parsingPlugins} processingPluginList={processingPlugins}>
<EuiMarkdownFormat
parsingPluginList={parsingPlugins}
processingPluginList={processingPluginList}
>
{children}
</EuiMarkdownFormat>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const RecentCasesComponent = ({ cases }: { cases: Case[] }) => {
{c.description && c.description.length && (
<MarkdownContainer>
<EuiText color="subdued" size="xs">
<MarkdownRenderer>{c.description}</MarkdownRenderer>
<MarkdownRenderer disableLinks={true}>{c.description}</MarkdownRenderer>
</EuiText>
</MarkdownContainer>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const NotePreview = React.memo<Pick<TimelineResultNote, 'note' | 'updated
</p>
</EuiText>
</NotePreviewHeader>
<MarkdownRenderer>{note || ''}</MarkdownRenderer>
<MarkdownRenderer>{note ?? ''}</MarkdownRenderer>
</EuiFlexItem>
</EuiFlexGroup>
</NotePreviewGroup>
Expand Down

0 comments on commit a73e977

Please sign in to comment.