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 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..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 @@ -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}) 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: +

- 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/__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..354534a84fa --- /dev/null +++ b/packages/eui/src/components/markdown_editor/markdown_format.test.tsx @@ -0,0 +1,190 @@ +/* + * 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('
'), + }); + }); + }); + + 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'), + }); + }); + }); + + 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..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 @@ -31,39 +31,58 @@ 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 = { + /** + * 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; +}; + 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 9a8c3001d61..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,10 +13,12 @@ import { import { getDefaultEuiMarkdownParsingPlugins, DefaultEuiMarkdownParsingPlugins, + type DefaultParsingPluginsConfig, } from './parsing_plugins'; import { getDefaultEuiMarkdownProcessingPlugins, DefaultEuiMarkdownProcessingPlugins, + type DefaultProcessingPluginsConfig, } from './processing_plugins'; export type ExcludableDefaultPlugins = @@ -31,13 +33,27 @@ export type DefaultPluginsConfig = | { exclude?: ExcludableDefaultPlugins[] }; export const getDefaultEuiMarkdownPlugins = ( - config?: DefaultPluginsConfig + config: DefaultPluginsConfig & { + processingConfig?: DefaultProcessingPluginsConfig; + parsingConfig?: DefaultParsingPluginsConfig; + uiConfig?: {}; // No customizations currently supported, but we may add this in the future + } = {} ): { 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 }), + }; +}; 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) || 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..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,23 @@ interface LinkOrTextNode { children?: Array<{ value: string }>; } -export interface EuiMarkdownLinkValidatorOptions { - allowRelative: boolean; - allowProtocols: string[]; -} +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, + allowProtocols: ['https:', 'http:', 'mailto:'], +}; export function euiMarkdownLinkValidator( options: EuiMarkdownLinkValidatorOptions @@ -57,7 +70,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('/');