Convert Markdown components to TS (#38081) (#38389)

* Convert Markdown components to TS

* Fix jest snapshots
This commit is contained in:
Tim Roes 2019-06-07 13:05:45 +02:00 committed by GitHub
parent 59d1424989
commit 654a306f36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 189 additions and 145 deletions

View file

@ -306,6 +306,7 @@
"@types/listr": "^0.14.0",
"@types/lodash": "^3.10.1",
"@types/lru-cache": "^5.1.0",
"@types/markdown-it": "^0.0.7",
"@types/minimatch": "^2.0.29",
"@types/mocha": "^5.2.6",
"@types/moment-timezone": "^0.5.8",

View file

@ -1,108 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import classNames from 'classnames';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import MarkdownIt from 'markdown-it';
import { memoize } from 'lodash';
/**
* Return a memoized markdown rendering function that use the specified
* whiteListedRules and openLinksInNewTab configurations.
* @param {Array of Strings} whiteListedRules - white list of markdown rules
* list of rules can be found at https://github.com/markdown-it/markdown-it/issues/361
* @param {Boolean} openLinksInNewTab
* @return {Function} Returns an Object to use with dangerouslySetInnerHTML
* with the rendered markdown HTML
*/
export const markdownFactory = memoize((whiteListedRules = [], openLinksInNewTab = false) => {
let markdownIt;
// It is imperative that the html config property be set to false, to mitigate XSS: the output of markdown-it is
// fed directly to the DOM via React's dangerouslySetInnerHTML below.
if (whiteListedRules && whiteListedRules.length > 0) {
markdownIt = new MarkdownIt('zero', { html: false, linkify: true });
markdownIt.enable(whiteListedRules);
} else {
markdownIt = new MarkdownIt({ html: false, linkify: true });
}
if (openLinksInNewTab) {
// All links should open in new browser tab.
// Define custom renderer to add 'target' attribute
// https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
const originalLinkRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
markdownIt.renderer.rules.link_open = function (tokens, idx, options, env, self) {
tokens[idx].attrPush(['target', '_blank']);
// https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/
tokens[idx].attrPush(['rel', 'noopener noreferrer']);
return originalLinkRender(tokens, idx, options, env, self);
};
}
/**
* This method is used to render markdown from the passed parameter
* into HTML. It will just return an empty string when the markdown is empty.
* @param {String} markdown - The markdown String
* @return {String} - Returns the rendered HTML as string.
*/
return (markdown) => {
return markdown ? markdownIt.render(markdown) : '';
};
}, (whiteListedRules = [], openLinksInNewTab = false) => {
return whiteListedRules.join('_').concat(openLinksInNewTab);
});
export class Markdown extends PureComponent {
render() {
const {
className,
markdown,
openLinksInNewTab,
whiteListedRules,
...rest
} = this.props;
const classes = classNames('kbnMarkdown__body', className);
const markdownRenderer = markdownFactory(whiteListedRules, openLinksInNewTab);
const renderedMarkdown = markdownRenderer(markdown);
return (
<div
{...rest}
className={classes}
/*
* Justification for dangerouslySetInnerHTML:
* The Markdown Visualization is, believe it or not, responsible for rendering Markdown.
* This relies on `markdown-it` to produce safe and correct HTML.
*/
dangerouslySetInnerHTML={{ __html: renderedMarkdown }} //eslint-disable-line react/no-danger
/>
);
}
}
Markdown.propTypes = {
className: PropTypes.string,
markdown: PropTypes.string,
openLinksInNewTab: PropTypes.bool,
whiteListedRules: PropTypes.arrayOf(PropTypes.string),
};

View file

@ -20,74 +20,101 @@
import React from 'react';
import { shallow } from 'enzyme';
import {
Markdown,
} from './markdown';
import { Markdown } from './markdown';
test('render', () => {
const component = shallow(<Markdown/>);
const component = shallow(<Markdown />);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should never render html tags', () => {
const component = shallow(<Markdown
markdown="<div>I may be dangerous if rendered as html</div>"
/>);
const component = shallow(
<Markdown markdown="<div>I may be dangerous if rendered as html</div>" />
);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should render links with parentheses correctly', () => {
const component = shallow(
<Markdown markdown="[link](https://example.com/foo/bar?group=(()filters:!t))" />
);
expect(
component
.render()
.find('a')
.prop('href')
).toBe('https://example.com/foo/bar?group=(()filters:!t)');
});
test('should add `noreferrer` and `nooopener` to unknown links in new tabs', () => {
const component = shallow(
<Markdown
openLinksInNewTab={true}
markdown="[link](https://example.com/foo/bar?group=(()filters:!t))"
/>
);
expect(component.render().find('a').prop('href')).toBe('https://example.com/foo/bar?group=(()filters:!t)');
expect(
component
.render()
.find('a')
.prop('rel')
).toBe('noopener noreferrer');
});
test('should only add `nooopener` to known links in new tabs', () => {
const component = shallow(
<Markdown openLinksInNewTab={true} markdown="[link](https://www.elastic.co/cool/path" />
);
expect(
component
.render()
.find('a')
.prop('rel')
).toBe('noopener');
});
describe('props', () => {
const markdown = 'I am *some* [content](https://en.wikipedia.org/wiki/Content) with `markdown`';
test('markdown', () => {
const component = shallow(<Markdown
markdown={markdown}
/>);
const component = shallow(<Markdown markdown={markdown} />);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('openLinksInNewTab', () => {
const component = shallow(<Markdown
markdown={markdown}
openLinksInNewTab={true}
/>);
const component = shallow(<Markdown markdown={markdown} openLinksInNewTab={true} />);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('whiteListedRules', () => {
const component = shallow(<Markdown
markdown={markdown}
whiteListedRules={['backticks', 'emphasis']}
/>);
const component = shallow(
<Markdown markdown={markdown} whiteListedRules={['backticks', 'emphasis']} />
);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should update markdown when openLinksInNewTab prop change', () => {
const component = shallow(<Markdown
markdown={markdown}
openLinksInNewTab={false}
/>);
expect(component.render().find('a').prop('target')).not.toBe('_blank');
const component = shallow(<Markdown markdown={markdown} openLinksInNewTab={false} />);
expect(
component
.render()
.find('a')
.prop('target')
).not.toBe('_blank');
component.setProps({ openLinksInNewTab: true });
expect(component.render().find('a').prop('target')).toBe('_blank');
expect(
component
.render()
.find('a')
.prop('target')
).toBe('_blank');
});
test('should update markdown when whiteListedRules prop change', () => {
const markdown = '*emphasis* `backticks`';
const component = shallow(<Markdown
markdown={markdown}
whiteListedRules={['emphasis', 'backticks']}
/>);
const md = '*emphasis* `backticks`';
const component = shallow(
<Markdown markdown={md} whiteListedRules={['emphasis', 'backticks']} />
);
expect(component.render().find('em')).toHaveLength(1);
expect(component.render().find('code')).toHaveLength(1);
component.setProps({ whiteListedRules: ['backticks'] });

View file

@ -0,0 +1,113 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import classNames from 'classnames';
import React, { PureComponent } from 'react';
import MarkdownIt from 'markdown-it';
import { memoize } from 'lodash';
import { getSecureRelForTarget } from '@elastic/eui';
/**
* Return a memoized markdown rendering function that use the specified
* whiteListedRules and openLinksInNewTab configurations.
* @param {Array of Strings} whiteListedRules - white list of markdown rules
* list of rules can be found at https://github.com/markdown-it/markdown-it/issues/361
* @param {Boolean} openLinksInNewTab
* @return {Function} Returns an Object to use with dangerouslySetInnerHTML
* with the rendered markdown HTML
*/
export const markdownFactory = memoize(
(whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => {
let markdownIt: MarkdownIt;
// It is imperative that the html config property be set to false, to mitigate XSS: the output of markdown-it is
// fed directly to the DOM via React's dangerouslySetInnerHTML below.
if (whiteListedRules && whiteListedRules.length > 0) {
markdownIt = new MarkdownIt('zero', { html: false, linkify: true });
markdownIt.enable(whiteListedRules);
} else {
markdownIt = new MarkdownIt({ html: false, linkify: true });
}
if (openLinksInNewTab) {
// All links should open in new browser tab.
// Define custom renderer to add 'target' attribute
// https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
const originalLinkRender =
markdownIt.renderer.rules.link_open ||
function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
markdownIt.renderer.rules.link_open = function(tokens, idx, options, env, self) {
const href = tokens[idx].attrGet('href');
const target = '_blank';
const rel = getSecureRelForTarget({ href: href === null ? undefined : href, target });
// https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/
tokens[idx].attrPush(['target', target]);
if (rel) {
tokens[idx].attrPush(['rel', rel]);
}
return originalLinkRender(tokens, idx, options, env, self);
};
}
/**
* This method is used to render markdown from the passed parameter
* into HTML. It will just return an empty string when the markdown is empty.
* @param {String} markdown - The markdown String
* @return {String} - Returns the rendered HTML as string.
*/
return (markdown: string) => {
return markdown ? markdownIt.render(markdown) : '';
};
},
(whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => {
return `${whiteListedRules.join('_')}${openLinksInNewTab}`;
}
);
interface MarkdownProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
markdown?: string;
openLinksInNewTab?: boolean;
whiteListedRules?: string[];
}
export class Markdown extends PureComponent<MarkdownProps> {
render() {
const { className, markdown = '', openLinksInNewTab, whiteListedRules, ...rest } = this.props;
const classes = classNames('kbnMarkdown__body', className);
const markdownRenderer = markdownFactory(whiteListedRules, openLinksInNewTab);
const renderedMarkdown = markdownRenderer(markdown);
return (
<div
{...rest}
className={classes}
/*
* Justification for dangerouslySetInnerHTML:
* The Markdown Visualization is, believe it or not, responsible for rendering Markdown.
* This relies on `markdown-it` to produce safe and correct HTML.
*/
dangerouslySetInnerHTML={{ __html: renderedMarkdown }} // eslint-disable-line react/no-danger
/>
);
}
}

View file

@ -18,18 +18,17 @@
*/
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import ReactMarkdown from 'react-markdown';
const markdownRenderers = {
root: Fragment,
};
interface MarkdownSimpleProps {
children: string;
}
// Render markdown string into JSX inside of a Fragment.
export const MarkdownSimple = ({ children }) => (
export const MarkdownSimple = ({ children }: MarkdownSimpleProps) => (
<ReactMarkdown renderers={markdownRenderers}>{children}</ReactMarkdown>
);
MarkdownSimple.propTypes = {
children: PropTypes.string,
};

View file

@ -3596,6 +3596,11 @@
resolved "https://registry.yarnpkg.com/@types/license-checker/-/license-checker-15.0.0.tgz#685d69e2cf61ffd862320434601f51c85e28bba1"
integrity sha512-dHZdn+VxvPGwKyKUlqi6Dj/t3Q4KFmbmPpsCOwagytr8P98jmz/nXzyxzz9wbfgpw72mVMx7PMlR/PT0xNsF7A==
"@types/linkify-it@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
"@types/listr@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@types/listr/-/listr-0.14.0.tgz#55161177ed5043987871bca5f66d87ca0a63a0b7"
@ -3658,6 +3663,13 @@
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.12.0.tgz#acf14294d18e6eba427a5e5d7dfce0f5cd2a9400"
integrity sha512-+UzPmwHSEEyv7aGlNkVpuFxp/BirXgl8NnPGCtmyx2KXIzAapoW3IqSVk87/Z3PUk8vEL8Pe1HXEMJbNBOQgtg==
"@types/markdown-it@^0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
dependencies:
"@types/linkify-it" "*"
"@types/memoize-one@^4.1.0":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.1.tgz#41dd138a4335b5041f7d8fc038f9d593d88b3369"