From 18b6ce1e80c88afeb39695072bb855cdf2066a03 Mon Sep 17 00:00:00 2001 From: Harpal Singh Date: Tue, 27 Aug 2024 22:24:25 +0530 Subject: [PATCH 1/8] feat: [EuiMarkdownFormat]: Added Opetion to Open Link in New Tab --- .../markdown_editor/markdown_format.tsx | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/eui/src/components/markdown_editor/markdown_format.tsx b/packages/eui/src/components/markdown_editor/markdown_format.tsx index c626f7027f1..521713fb77b 100644 --- a/packages/eui/src/components/markdown_editor/markdown_format.tsx +++ b/packages/eui/src/components/markdown_editor/markdown_format.tsx @@ -31,8 +31,29 @@ export type EuiMarkdownFormatProps = CommonProps & * Determines the text size. Choose `relative` to control the `font-size` based on the value of a parent container. */ textSize?: EuiTextProps['size']; + /** + * Links target attribute. Set to `_blank` to open links in a new tab. Other values can be _blank|_self|_parent|_top|framename + */ + linkTarget: string; }; +const openLinkToNewTab = (target: string) => { + return () => { + return (tree: any) => { + const visit = (node: any) => { + if (node.tagName === 'a') { + node.properties = node.properties || {}; + node.properties.target = target; + } + if (node.children) { + node.children.forEach(visit); + } + }; + visit(tree); + }; + } +}; + export const EuiMarkdownFormat: FunctionComponent = ({ children, className, @@ -40,10 +61,14 @@ export const EuiMarkdownFormat: FunctionComponent = ({ processingPluginList = defaultProcessingPlugins, textSize = 'm', color = 'default', + linkTarget = '', ...rest }) => { const processor = useMemo( - () => unified().use(parsingPluginList).use(processingPluginList), + () => { + const result = unified().use(parsingPluginList).use(processingPluginList) + return (linkTarget === '') ? result : result.use(openLinkToNewTab(linkTarget)); + }, [parsingPluginList, processingPluginList] ); const result = useMemo(() => { From 22f6d020f29095963156ce5a3f7dbfa046efc56e Mon Sep 17 00:00:00 2001 From: Harpal Singh Date: Wed, 28 Aug 2024 00:56:16 +0530 Subject: [PATCH 2/8] feat(EuiMarkdownFormat): Open links in new tab --- .../src/views/markdown_editor/markdown_format_links.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eui/src-docs/src/views/markdown_editor/markdown_format_links.js b/packages/eui/src-docs/src/views/markdown_editor/markdown_format_links.js index 2b28d6bd188..0891f243172 100644 --- a/packages/eui/src-docs/src/views/markdown_editor/markdown_format_links.js +++ b/packages/eui/src-docs/src/views/markdown_editor/markdown_format_links.js @@ -7,8 +7,8 @@ const locationPathname = location.pathname; export const markdownContent = `**Links starting with http:, https:, mailto:, and / are valid:** -* https://elastic.com -* http://elastic.com +* https://elastic.co +* http://elastic.co * https link to [elastic.co](https://elastic.co) * http link to [elastic.co](http://elastic.co) * relative link to [eui doc's homepage](${locationPathname}) @@ -21,5 +21,5 @@ export const markdownContent = `**Links starting with http:, https:, mailto:, an `; export default () => { - return {markdownContent}; + return {markdownContent}; }; From 35fdbae545694347e8d18c797c22df4c03e2bb9b Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 28 Aug 2024 08:35:41 -0700 Subject: [PATCH 3/8] Revert changes to EuiMarkdownFormat --- .../markdown_editor/markdown_format_links.js | 2 +- .../markdown_editor/markdown_format.tsx | 27 +------------------ 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/packages/eui/src-docs/src/views/markdown_editor/markdown_format_links.js b/packages/eui/src-docs/src/views/markdown_editor/markdown_format_links.js index 0891f243172..3e8969b8f1a 100644 --- a/packages/eui/src-docs/src/views/markdown_editor/markdown_format_links.js +++ b/packages/eui/src-docs/src/views/markdown_editor/markdown_format_links.js @@ -21,5 +21,5 @@ export const markdownContent = `**Links starting with http:, https:, mailto:, an `; export default () => { - return {markdownContent}; + return {markdownContent}; }; diff --git a/packages/eui/src/components/markdown_editor/markdown_format.tsx b/packages/eui/src/components/markdown_editor/markdown_format.tsx index 521713fb77b..c626f7027f1 100644 --- a/packages/eui/src/components/markdown_editor/markdown_format.tsx +++ b/packages/eui/src/components/markdown_editor/markdown_format.tsx @@ -31,29 +31,8 @@ export type EuiMarkdownFormatProps = CommonProps & * Determines the text size. Choose `relative` to control the `font-size` based on the value of a parent container. */ textSize?: EuiTextProps['size']; - /** - * Links target attribute. Set to `_blank` to open links in a new tab. Other values can be _blank|_self|_parent|_top|framename - */ - linkTarget: string; }; -const openLinkToNewTab = (target: string) => { - return () => { - return (tree: any) => { - const visit = (node: any) => { - if (node.tagName === 'a') { - node.properties = node.properties || {}; - node.properties.target = target; - } - if (node.children) { - node.children.forEach(visit); - } - }; - visit(tree); - }; - } -}; - export const EuiMarkdownFormat: FunctionComponent = ({ children, className, @@ -61,14 +40,10 @@ export const EuiMarkdownFormat: FunctionComponent = ({ processingPluginList = defaultProcessingPlugins, textSize = 'm', color = 'default', - linkTarget = '', ...rest }) => { const processor = useMemo( - () => { - const result = unified().use(parsingPluginList).use(processingPluginList) - return (linkTarget === '') ? result : result.use(openLinkToNewTab(linkTarget)); - }, + () => unified().use(parsingPluginList).use(processingPluginList), [parsingPluginList, processingPluginList] ); const result = useMemo(() => { From b5c96b33a9e0c334fc753492a2e55baa0d7711e0 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 28 Aug 2024 11:09:36 -0700 Subject: [PATCH 4/8] [setup] Add optional config objs for default processing, parsing, and UI plugins + Add missing EuiMarkdownFormat unit tests + bogart test file for testing rendered output of `getDefaultEuiMarkdownPlugins` --- .../markdown_format.test.tsx.snap | 15 ++ .../markdown_editor/markdown_format.test.tsx | 151 ++++++++++++++++++ .../markdown_default_plugins/plugins.ts | 27 +++- 3 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 packages/eui/src/components/markdown_editor/__snapshots__/markdown_format.test.tsx.snap create mode 100644 packages/eui/src/components/markdown_editor/markdown_format.test.tsx diff --git a/packages/eui/src/components/markdown_editor/__snapshots__/markdown_format.test.tsx.snap b/packages/eui/src/components/markdown_editor/__snapshots__/markdown_format.test.tsx.snap new file mode 100644 index 00000000000..161cf4eb8c1 --- /dev/null +++ b/packages/eui/src/components/markdown_editor/__snapshots__/markdown_format.test.tsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiMarkdownFormat renders 1`] = ` +
+

+ + Hello world + +

+
+`; diff --git a/packages/eui/src/components/markdown_editor/markdown_format.test.tsx b/packages/eui/src/components/markdown_editor/markdown_format.test.tsx new file mode 100644 index 00000000000..66f21036583 --- /dev/null +++ b/packages/eui/src/components/markdown_editor/markdown_format.test.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render } from '../../test/rtl'; +import { shouldRenderCustomStyles } from '../../test/internal'; +import { requiredProps } from '../../test'; + +import { EuiMarkdownFormat, getDefaultEuiMarkdownPlugins } from './index'; + +describe('EuiMarkdownFormat', () => { + shouldRenderCustomStyles(test); + + it('renders', () => { + const { container } = render( + **Hello world** + ); + + expect(container.firstChild).toMatchSnapshot(); + }); + + describe('props', () => { + test('color', () => { + const { getByTestSubject } = render( + <> + + _Hello world_ + + + ~Hello world~ + + + ); + + expect(getByTestSubject('first')).toHaveStyle({ + color: 'rgb(189, 39, 30)', + }); + expect(getByTestSubject('second')).toHaveStyle({ + color: '#ffffff', + }); + }); + + test('textSize', () => { + const { getByTestSubject } = render( + <> + + _Hello world_ + + + ~Hello world~ + + + ); + + expect(getByTestSubject('first')).toHaveStyle({ + 'font-size': '0.8571rem', + }); + expect(getByTestSubject('second')).toHaveStyle({ + 'font-size': '1em', + }); + }); + }); + + describe('plugins config', () => { + // Test utils + const getComponent = () => document.querySelector('.euiMarkdownFormat')!; + const getLink = () => getComponent().querySelector('.euiLink'); + const getCheckbox = () => getComponent().querySelector('.euiCheckbox'); + const getToolTip = () => getComponent().querySelector('.euiToolTipAnchor'); + + const assertMarkdownBeforeAndAfter = (args: { + markdown: string; + config: Parameters[0]; + before: Function; + after: Function; + }) => { + const { markdown, config, before, after } = args; + + const { rerender } = render( + {markdown} + ); + before(); + + const { processingPlugins, parsingPlugins } = + getDefaultEuiMarkdownPlugins(config); + rerender( + + {markdown} + + ); + + after(); + }; + + describe('exclude', () => { + test('tooltip', () => { + assertMarkdownBeforeAndAfter({ + markdown: '!{tooltip[text](help)}', + config: { exclude: ['tooltip'] }, + before: () => expect(getToolTip()).toBeInTheDocument(), + after: () => expect(getToolTip()).not.toBeInTheDocument(), + }); + }); + + test('checkbox', () => { + assertMarkdownBeforeAndAfter({ + markdown: '- [ ] TODO', + config: { exclude: ['checkbox'] }, + before: () => expect(getCheckbox()).toBeInTheDocument(), + after: () => expect(getCheckbox()).not.toBeInTheDocument(), + }); + }); + + test('emoji', () => { + assertMarkdownBeforeAndAfter({ + markdown: ':smile:', + config: { exclude: ['emoji'] }, + before: () => expect(getComponent()).toHaveTextContent('😄'), + after: () => expect(getComponent()).toHaveTextContent(':smile:'), + }); + }); + + test('linkValidator', () => { + assertMarkdownBeforeAndAfter({ + markdown: '[Sus link](file://)', + config: { exclude: ['linkValidator'] }, + before: () => expect(getLink()).not.toBeInTheDocument(), + after: () => expect(getLink()).toBeInTheDocument(), + }); + }); + + test('lineBreaks', () => { + assertMarkdownBeforeAndAfter({ + markdown: `One + Two`, + config: { exclude: ['lineBreaks'] }, + before: () => expect(getComponent().innerHTML).toContain('
'), + after: () => expect(getComponent().innerHTML).not.toContain('
'), + }); + }); + }); + }); +}); diff --git a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts index 9a8c3001d61..c66591e7afb 100644 --- a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts +++ b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts @@ -31,13 +31,28 @@ export type DefaultPluginsConfig = | { exclude?: ExcludableDefaultPlugins[] }; export const getDefaultEuiMarkdownPlugins = ( - config?: DefaultPluginsConfig + config: DefaultPluginsConfig & { + // TODO + processingConfig?: {}; + parsingConfig?: {}; + uiConfig?: {}; + } = {} ): { parsingPlugins: DefaultEuiMarkdownParsingPlugins; processingPlugins: DefaultEuiMarkdownProcessingPlugins; uiPlugins: DefaultEuiMarkdownUiPlugins; -} => ({ - parsingPlugins: getDefaultEuiMarkdownParsingPlugins(config), - processingPlugins: getDefaultEuiMarkdownProcessingPlugins(config), - uiPlugins: getDefaultEuiMarkdownUiPlugins(config), -}); +} => { + const { exclude, processingConfig, parsingConfig, uiConfig } = config; + + return { + parsingPlugins: getDefaultEuiMarkdownParsingPlugins({ + exclude, + ...parsingConfig, + }), + processingPlugins: getDefaultEuiMarkdownProcessingPlugins({ + exclude, + ...processingConfig, + }), + uiPlugins: getDefaultEuiMarkdownUiPlugins({ exclude, ...uiConfig }), + }; +}; From 8ef7273c538f359a92794c42450cd23c0ebb7a67 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 28 Aug 2024 11:40:33 -0700 Subject: [PATCH 5/8] Add `processingConfig.linkProps` config - will allow passing `target: "_blank"` configs and more (e.g. rel, disabled, color, etc) --- .../markdown_editor/markdown_format.test.tsx | 13 +++++++++++++ .../markdown_default_plugins/plugins.ts | 4 ++-- .../processing_plugins.tsx | 19 ++++++++++++++++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/eui/src/components/markdown_editor/markdown_format.test.tsx b/packages/eui/src/components/markdown_editor/markdown_format.test.tsx index 66f21036583..78a33e5ccd6 100644 --- a/packages/eui/src/components/markdown_editor/markdown_format.test.tsx +++ b/packages/eui/src/components/markdown_editor/markdown_format.test.tsx @@ -147,5 +147,18 @@ describe('EuiMarkdownFormat', () => { }); }); }); + + describe('processingConfig', () => { + test('linkProps', () => { + assertMarkdownBeforeAndAfter({ + markdown: '[link](https://elastic.co)', + config: { + processingConfig: { linkProps: { target: '_blank' } }, + }, + before: () => expect(getLink()).not.toHaveAttribute('target'), + after: () => expect(getLink()).toHaveAttribute('target', '_blank'), + }); + }); + }); }); }); diff --git a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts index c66591e7afb..46ed10a6739 100644 --- a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts +++ b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts @@ -17,6 +17,7 @@ import { import { getDefaultEuiMarkdownProcessingPlugins, DefaultEuiMarkdownProcessingPlugins, + type DefaultProcessingPluginsConfig, } from './processing_plugins'; export type ExcludableDefaultPlugins = @@ -32,8 +33,7 @@ export type DefaultPluginsConfig = export const getDefaultEuiMarkdownPlugins = ( config: DefaultPluginsConfig & { - // TODO - processingConfig?: {}; + processingConfig?: DefaultProcessingPluginsConfig; parsingConfig?: {}; uiConfig?: {}; } = {} diff --git a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/processing_plugins.tsx b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/processing_plugins.tsx index 04c575d826a..e4aba601352 100644 --- a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/processing_plugins.tsx +++ b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/processing_plugins.tsx @@ -29,7 +29,7 @@ import all from 'mdast-util-to-hast/lib/all'; import rehype2react from 'rehype-react'; import remark2rehype from 'remark-rehype'; -import { EuiLink } from '../../../link'; +import { EuiLink, EuiLinkProps } from '../../../link'; import { EuiCodeBlock, EuiCode } from '../../../code'; import { EuiHorizontalRule } from '../../../horizontal_rule'; @@ -53,6 +53,15 @@ export type DefaultEuiMarkdownProcessingPlugins = [ ...PluggableList // any additional are generic ]; +export type DefaultProcessingPluginsConfig = { + /** + * Allows customizing all formatted links. + * Accepts any prop that [EuiLink](/#/navigation/link) or any anchor link tag accepts. + * Useful for, e.g. setting `target="_blank"` on all links + */ + linkProps?: Partial; +}; + const DEFAULT_COMPONENT_RENDERERS: Partial< Record> > = { @@ -62,7 +71,9 @@ const DEFAULT_COMPONENT_RENDERERS: Partial< export const getDefaultEuiMarkdownProcessingPlugins = ({ exclude, -}: DefaultPluginsConfig = {}): DefaultEuiMarkdownProcessingPlugins => { + linkProps, +}: DefaultPluginsConfig & + DefaultProcessingPluginsConfig = {}): DefaultEuiMarkdownProcessingPlugins => { const componentPluginsWithExclusions: Rehype2ReactOptions['components'] = {}; Object.entries(DEFAULT_COMPONENT_RENDERERS).forEach( @@ -89,7 +100,9 @@ export const getDefaultEuiMarkdownProcessingPlugins = ({ createElement, Fragment, components: { - a: EuiLink, + a: (props: any) => { + return ; + }, code: (props: any) => // If there are linebreaks use codeblock, otherwise code /\r|\n/.exec(props.children) || From 037874eeef8fa62acd49b32ca780fdc2f5c13c75 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 28 Aug 2024 13:28:30 -0700 Subject: [PATCH 6/8] Add `parsingConfig` & allow overriding parsing plugin defaults --- .../markdown_editor/markdown_format.test.tsx | 26 +++++++++++++++ .../parsing_plugins.ts | 33 ++++++++++++++----- .../markdown_default_plugins/plugins.ts | 5 +-- .../plugins/markdown_link_validator.tsx | 14 ++++++-- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/packages/eui/src/components/markdown_editor/markdown_format.test.tsx b/packages/eui/src/components/markdown_editor/markdown_format.test.tsx index 78a33e5ccd6..354534a84fa 100644 --- a/packages/eui/src/components/markdown_editor/markdown_format.test.tsx +++ b/packages/eui/src/components/markdown_editor/markdown_format.test.tsx @@ -160,5 +160,31 @@ describe('EuiMarkdownFormat', () => { }); }); }); + + describe('parsingConfig', () => { + it('emoji', () => { + assertMarkdownBeforeAndAfter({ + markdown: ':)', + config: { + parsingConfig: { emoji: { emoticon: true } }, + }, + before: () => expect(getComponent()).toHaveTextContent(':)'), + after: () => expect(getComponent()).toHaveTextContent('😃'), + }); + }); + + it('linkValidator', () => { + assertMarkdownBeforeAndAfter({ + markdown: '[relative](/), [protocol](ftp://test)', + config: { + parsingConfig: { + linkValidator: { allowRelative: false, allowProtocols: ['ftp:'] }, + }, + }, + before: () => expect(getLink()).toHaveTextContent('relative'), + after: () => expect(getLink()).toHaveTextContent('protocol'), + }); + }); + }); }); }); diff --git a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts index d0ebcb12d82..d0e5e8598c1 100644 --- a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts +++ b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts @@ -31,39 +31,54 @@ import * as MarkdownCheckbox from '../markdown_checkbox'; import { euiMarkdownLinkValidator, EuiMarkdownLinkValidatorOptions, + DEFAULT_OPTIONS as LINK_VALIDATOR_DEFAULTS, } from '../markdown_link_validator'; import type { ExcludableDefaultPlugins, DefaultPluginsConfig } from './plugins'; export type DefaultEuiMarkdownParsingPlugins = PluggableList; +export type DefaultParsingPluginsConfig = Partial< + // We may eventually add more optional configuration options for more default plugins + Record +> & { + // But for now, these are the ones that have typed configurations + emoji?: { emoticon?: boolean }; + linkValidator?: EuiMarkdownLinkValidatorOptions; +}; + const DEFAULT_PARSING_PLUGINS: Record< ExcludableDefaultPlugins, DefaultEuiMarkdownParsingPlugins[0] > = { emoji: [emoji, { emoticon: false }], lineBreaks: [breaks, {}], - linkValidator: [ - euiMarkdownLinkValidator, - { - allowRelative: true, - allowProtocols: ['https:', 'http:', 'mailto:'], - } as EuiMarkdownLinkValidatorOptions, - ], + linkValidator: [euiMarkdownLinkValidator, LINK_VALIDATOR_DEFAULTS], checkbox: [MarkdownCheckbox.parser, {}], tooltip: [MarkdownTooltip.parser, {}], }; export const getDefaultEuiMarkdownParsingPlugins = ({ exclude, -}: DefaultPluginsConfig = {}): DefaultEuiMarkdownParsingPlugins => { + ...parsingConfig +}: DefaultPluginsConfig & + DefaultParsingPluginsConfig = {}): DefaultEuiMarkdownParsingPlugins => { const parsingPlugins: PluggableList = [ [markdown, {}], [highlight, {}], ]; Object.entries(DEFAULT_PARSING_PLUGINS).forEach(([pluginName, plugin]) => { + // Check for plugin exclusions if (!exclude?.includes(pluginName as ExcludableDefaultPlugins)) { - parsingPlugins.push(plugin); + // Check for plugin configuration overrides + if (pluginName in parsingConfig) { + parsingPlugins.push([ + (plugin as any[])[0], + parsingConfig[pluginName as keyof DefaultParsingPluginsConfig], + ]); + } else { + parsingPlugins.push(plugin); + } } }); diff --git a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts index 46ed10a6739..d53e85c8b27 100644 --- a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts +++ b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/plugins.ts @@ -13,6 +13,7 @@ import { import { getDefaultEuiMarkdownParsingPlugins, DefaultEuiMarkdownParsingPlugins, + type DefaultParsingPluginsConfig, } from './parsing_plugins'; import { getDefaultEuiMarkdownProcessingPlugins, @@ -34,8 +35,8 @@ export type DefaultPluginsConfig = export const getDefaultEuiMarkdownPlugins = ( config: DefaultPluginsConfig & { processingConfig?: DefaultProcessingPluginsConfig; - parsingConfig?: {}; - uiConfig?: {}; + parsingConfig?: DefaultParsingPluginsConfig; + uiConfig?: {}; // No customizations currently supported, but we may add this in the future } = {} ): { parsingPlugins: DefaultEuiMarkdownParsingPlugins; diff --git a/packages/eui/src/components/markdown_editor/plugins/markdown_link_validator.tsx b/packages/eui/src/components/markdown_editor/plugins/markdown_link_validator.tsx index 4f23051fd13..59bd92716a0 100644 --- a/packages/eui/src/components/markdown_editor/plugins/markdown_link_validator.tsx +++ b/packages/eui/src/components/markdown_editor/plugins/markdown_link_validator.tsx @@ -17,10 +17,15 @@ interface LinkOrTextNode { } export interface EuiMarkdownLinkValidatorOptions { - allowRelative: boolean; - allowProtocols: string[]; + allowRelative?: boolean; + allowProtocols?: string[]; } +export const DEFAULT_OPTIONS = { + allowRelative: true, + allowProtocols: ['https:', 'http:', 'mailto:'], +}; + export function euiMarkdownLinkValidator( options: EuiMarkdownLinkValidatorOptions ) { @@ -57,7 +62,10 @@ export function mutateLinkToText(node: LinkOrTextNode) { export function validateUrl( url: string, - { allowRelative, allowProtocols }: EuiMarkdownLinkValidatorOptions + { + allowRelative = DEFAULT_OPTIONS.allowRelative, + allowProtocols = DEFAULT_OPTIONS.allowProtocols, + }: EuiMarkdownLinkValidatorOptions ) { // relative captures both relative paths `/` and protocols `//` const isRelative = url.startsWith('/'); From 30fdb8a70ff0523c12a0c9a9a2cb0a8d17f59bea Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 28 Aug 2024 14:52:52 -0700 Subject: [PATCH 7/8] [docs] Documentation + prop docs pass - replace link validation example with this one, since it's now a better way of configuring the linkValidator plugin - add more defaults to props docs - clean up sidebar links/headings for Markdown plugins page --- .../markdown_link_validation.js | 37 ------ .../markdown_plugin_config.tsx | 37 ++++++ .../markdown_plugin_example.js | 123 ++++++++++-------- .../markdown_editor/markdown_plugin_props.tsx | 27 ++++ .../parsing_plugins.ts | 14 +- .../plugins/markdown_link_validator.tsx | 12 +- 6 files changed, 154 insertions(+), 96 deletions(-) delete mode 100644 packages/eui/src-docs/src/views/markdown_editor/markdown_link_validation.js create mode 100644 packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_config.tsx create mode 100644 packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_props.tsx diff --git a/packages/eui/src-docs/src/views/markdown_editor/markdown_link_validation.js b/packages/eui/src-docs/src/views/markdown_editor/markdown_link_validation.js deleted file mode 100644 index 0d7863e6ce5..00000000000 --- a/packages/eui/src-docs/src/views/markdown_editor/markdown_link_validation.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -import { - getDefaultEuiMarkdownParsingPlugins, - euiMarkdownLinkValidator, - EuiMarkdownFormat, -} from '../../../../src/components'; - -const parsingPlugins = [ - // Exclude the default validation plugin, we're configuring our own that excludes `http` as a protocol - ...getDefaultEuiMarkdownParsingPlugins({ - exclude: ['linkValidator'], - }), - [ - euiMarkdownLinkValidator, - { - allowProtocols: ['https:', 'mailto:'], - }, - ], -]; - -const markdown = `**Standalone links** -https://example.com -http://example.com -someone@example.com - -**As markdown syntax** -[example.com, https](https://example.com) -[example.com, http](http://example.com) -[email someone@example.com](mailto:someone@example.com) -`; - -export default () => ( - - {markdown} - -); diff --git a/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_config.tsx b/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_config.tsx new file mode 100644 index 00000000000..713887aef24 --- /dev/null +++ b/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_config.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { + EuiMarkdownFormat, + getDefaultEuiMarkdownPlugins, +} from '../../../../src'; + +export const markdownContent = ` +- :cry: Automatic emoji formatting has been excluded from this markdown. +- In the example below, only \`https:\` and \`mailto:\` protocols should turn into links. +- Links should open in a new tab. + +https://elastic.co +http://elastic.co +someone@elastic.co +`; + +export default () => { + const { processingPlugins, parsingPlugins } = getDefaultEuiMarkdownPlugins({ + exclude: ['emoji'], + processingConfig: { + linkProps: { target: '_blank' }, + }, + parsingConfig: { + linkValidator: { allowProtocols: ['https:', 'mailto:'] }, + }, + }); + + return ( + + {markdownContent} + + ); +}; diff --git a/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_example.js b/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_example.js index b839bfbdb34..eb362e4e53e 100644 --- a/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_example.js +++ b/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_example.js @@ -1,4 +1,5 @@ -import React, { Fragment } from 'react'; +import React from 'react'; +import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../components'; @@ -14,13 +15,18 @@ import { EuiLink, } from '../../../../src/components'; -import { Link } from 'react-router-dom'; +import { + DefaultPluginsConfig, + DefaultParsingPluginsConfig, + DefaultProcessingPluginsConfig, + EuiMarkdownLinkValidatorOptions, +} from './markdown_plugin_props'; import MarkdownEditorWithPlugins from './markdown_editor_with_plugins'; const markdownEditorWithPluginsSource = require('!!raw-loader!./markdown_editor_with_plugins'); -const linkValidationSource = require('!!raw-loader!./markdown_link_validation'); -import LinkValidation from './markdown_link_validation'; +const pluginConfigSource = require('!!raw-loader!./markdown_plugin_config'); +import PluginConfig from './markdown_plugin_config'; const pluginSnippet = `getDefaultEuiMarkdownParsingPlugins,{' '} getDefaultEuiMarkdownProcessingPlugins, and{' '} - getDefaultEuiMarkdownUiPlugins respectively. Each - of these three functions take an optional configuration object with - an exclude key, an array of EUI-defaulted plugins - to disable. Currently the only option this configuration can take is{' '} - 'tooltip'. + getDefaultEuiMarkdownUiPlugins respectively.

), @@ -306,56 +308,73 @@ export const MarkdownPluginExample = { source: [ { type: GuideSectionTypes.JS, - code: linkValidationSource, + code: pluginConfigSource, }, ], - title: 'Link validation & security', + title: 'Configuring the default plugins', text: ( - + <>

- To enhance user and application security, the default behavior - removes links to URLs that aren't relative (beginning with{' '} - /) and don't use the{' '} - https:, http:, or{' '} - mailto: protocols. This validation can be further - configured or removed altogether. + The above plugin utils, as well as{' '} + getDefaultEuiMarkdownPlugins, accept an optional + configuration object of: +

    +
  • + exclude: an array of default plugins to{' '} + + unregister + +
  • +
  • + parsingConfig: allows overriding the + configuration of any default parsing plugin +
  • +
  • + processingConfig: currently only accepts a{' '} + linkProps key, which accepts any prop that{' '} + EuiLink accepts +
  • +

- In this example only https: and{' '} - mailto: links are allowed. + The below example has the emoji plugin excluded, + and custom configuration on the link validator parsing plugin and + link processing plugin. See the Props table for all + plugin config options.

-
+ ), - snippet: [ - `// customize what link protocols are allowed -const parsingPlugins = [ - ...getDefaultEuiMarkdownParsingPlugins({ - // Exclude the default validation plugin - we're configuring our own - exclude: ['linkValidator'], - }), - [ - euiMarkdownLinkValidator, - { - // Customize what link protocols are allowed - allowProtocols: ['https:', 'mailto:'], - }, - ] -]; + snippet: `const { parsingPlugins, processingPlugins } = getDefaultEuiMarkdownPlugins({ + // Exclude plugins as necessary + exclude: ['emoji'], + parsingConfig: { + // Customize what link protocols are allowed + linkValidator: { allowProtocols: ['https:', 'mailto:'] }, + }, + processingConfig: { + // Configure all links to open in new tabs/windows + linkProps: { target: '_blank' }, + }, +}); -// Pass the customized parsing plugins to your markdown component - -`, - ], - demo: , +// Pass the customized plugins to your markdown component +`, + demo: , + props: { + DefaultPluginsConfig, + DefaultParsingPluginsConfig, + DefaultProcessingPluginsConfig, + EuiMarkdownLinkValidatorOptions, + }, }, { + title: 'Plugin development', wrapText: false, text: ( <> - -

Plugin development

-
-

An EuiMarkdown plugin is comprised of three major @@ -374,7 +393,7 @@ const parsingPlugins = [ /> -

uiPlugin

+

uiPlugin

@@ -388,11 +407,11 @@ const parsingPlugins = [ /> -

parsingPluginList

+

parsingPluginList

- + <>

- + -

processingPluginList

+

processingPluginList

@@ -533,7 +552,7 @@ processingList[1][1].components.emojiPlugin = EmojiMarkdownRenderer;`} ], title: 'Putting it all together: a simple chart plugin', text: ( - + <>

The below example takes the concepts from above to construct a simple chart embed that is initiated from a new button in the editor @@ -545,7 +564,7 @@ processingList[1][1].components.emojiPlugin = EmojiMarkdownRenderer;`} list. The editor manages additional controls through the{' '} uiPlugins prop.

-
+ ), props: { EuiMarkdownEditor, diff --git a/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_props.tsx b/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_props.tsx new file mode 100644 index 00000000000..5f15f38ab5d --- /dev/null +++ b/packages/eui/src-docs/src/views/markdown_editor/markdown_plugin_props.tsx @@ -0,0 +1,27 @@ +import { FunctionComponent } from 'react'; + +import type { + ExcludableDefaultPlugins, + DefaultParsingPluginsConfig as DefaultParsingPluginsConfigProps, + DefaultProcessingPluginsConfig as DefaultProcessingPluginsConfigProps, +} from '../../../../src/components/markdown_editor/plugins/markdown_default_plugins'; + +import type { EuiMarkdownLinkValidatorOptions as EuiMarkdownLinkValidatorOptionsProps } from '../../../../src/components/markdown_editor'; + +export const DefaultPluginsConfig: FunctionComponent<{ + exclude?: ExcludableDefaultPlugins; + parsingConfig?: DefaultParsingPluginsConfigProps; + processingConfig?: DefaultProcessingPluginsConfigProps; +}> = () => null; + +export const DefaultParsingPluginsConfig: FunctionComponent< + DefaultParsingPluginsConfigProps +> = () => null; + +export const DefaultProcessingPluginsConfig: FunctionComponent< + DefaultProcessingPluginsConfigProps +> = () => null; + +export const EuiMarkdownLinkValidatorOptions: FunctionComponent< + EuiMarkdownLinkValidatorOptionsProps +> = () => null; diff --git a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts index d0e5e8598c1..ab936b3fd08 100644 --- a/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts +++ b/packages/eui/src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts @@ -37,12 +37,16 @@ import type { ExcludableDefaultPlugins, DefaultPluginsConfig } from './plugins'; export type DefaultEuiMarkdownParsingPlugins = PluggableList; -export type DefaultParsingPluginsConfig = Partial< - // We may eventually add more optional configuration options for more default plugins - Record -> & { - // But for now, these are the ones that have typed configurations +export type DefaultParsingPluginsConfig = { + /** + * Allows enabling emoji rendering for emoticons such as :) and :( + * @default { emoticon: false } + */ emoji?: { emoticon?: boolean }; + /** + * Allows configuring the `allowRelative` and `allowProtocols` of + * #EuiMarkdownLinkValidatorOptions + */ linkValidator?: EuiMarkdownLinkValidatorOptions; }; diff --git a/packages/eui/src/components/markdown_editor/plugins/markdown_link_validator.tsx b/packages/eui/src/components/markdown_editor/plugins/markdown_link_validator.tsx index 59bd92716a0..d621fd488c5 100644 --- a/packages/eui/src/components/markdown_editor/plugins/markdown_link_validator.tsx +++ b/packages/eui/src/components/markdown_editor/plugins/markdown_link_validator.tsx @@ -16,10 +16,18 @@ interface LinkOrTextNode { children?: Array<{ value: string }>; } -export interface EuiMarkdownLinkValidatorOptions { +export type EuiMarkdownLinkValidatorOptions = { + /** + * Allow or disallow relative links (links that begin with a `/`) + * @default true + */ allowRelative?: boolean; + /** + * Allow or disallow specific [URL protocols or schemes](https://developer.mozilla.org/en-US/docs/Web/URI/Schemes) + * @default ['https:', 'http:', 'mailto:'] + */ allowProtocols?: string[]; -} +}; export const DEFAULT_OPTIONS = { allowRelative: true, From 186393fd4e6747d526e269e4a8a62b718dcfe5b9 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Wed, 28 Aug 2024 14:58:14 -0700 Subject: [PATCH 8/8] changelog --- packages/eui/changelogs/upcoming/7985.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/eui/changelogs/upcoming/7985.md diff --git a/packages/eui/changelogs/upcoming/7985.md b/packages/eui/changelogs/upcoming/7985.md new file mode 100644 index 00000000000..d8d6636d747 --- /dev/null +++ b/packages/eui/changelogs/upcoming/7985.md @@ -0,0 +1,5 @@ +- Updated `getDefaultEuiMarkdownPlugins` to support the following new default plugin configurations: + - `parsingConfig.linkValidator`, which allows configuring `allowRelative` and `allowProtocols` + - `parsingConfig.emoji`, which allows configuring emoticon parsing + - `processingConfig.linkProps`, which allows configuring rendered links with any props that `EuiLink` accepts + - See our **Markdown plugins** documentation for example `EuiMarkdownFormat` and `EuiMarkdownEditor` usage