* Convert Markdown components to TS * Fix jest snapshots
This commit is contained in:
parent
59d1424989
commit
654a306f36
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
};
|
|
@ -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'] });
|
113
src/legacy/ui/public/markdown/markdown.tsx
Normal file
113
src/legacy/ui/public/markdown/markdown.tsx
Normal 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
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue