+ EuiMarkdownEditor provides a markdown authoring
+ experience for the user. The component consists of a toolbar, text
+ area, and a drag-and-drop zone to accept files (if configured to do
+ so). There are two modes: a textarea that keeps track of cursor
+ position, and a rendered preview mode that is powered by{' '}
+
+ EuiMarkdownFormat
+
+ . State is maintained between the two and it is possible to pass
+ changes from the preview area to the textarea and vice versa.
+
- This component renders a markdown editor, including buttons for
- quickly inserting common markdown elements and a preview mode.
+ The base editor can render basic markdown along with some built-in
+ plugins.
+ The errors prop allows you to pass an array of
+ errors if syntax is malformed. The below example starts with an
+ incomplete tooltip tag, showing this error message by default. These
+ errors are meant to be ephemeral and part of the editing experience.
+ They should not be a substitute for{' '}
+ form validation.
+
+ EuiMarkdownFormat is a read-only way to render
+ markdown-style content in a page. It is a peer component to{' '}
+
+ EuiMarkdownEditor
+ {' '}
+ and has the ability to be modified by additional{' '}
+ markdown plugins.
+
+ EuiMarkdownFormat is a wrapper that will render
+ Markdown provided. EuiMarkdownFormat uses{' '}
+ Remark by
+ default. The translation layer automatically substitutes raw HTML
+ output with their EUI equivilant. This means anchor and code blocks
+ will become EuiLink and EuiCodeBlock{' '}
+ components respectively.
+
+ {isAstShowing && {ast}}
+
+
+ {value}
+
+ >
+ );
};
diff --git a/src-docs/src/views/markdown_editor/markdown_format.js b/src-docs/src/views/markdown_editor/markdown_format.js
new file mode 100644
index 00000000000..a5b58e00a42
--- /dev/null
+++ b/src-docs/src/views/markdown_editor/markdown_format.js
@@ -0,0 +1,30 @@
+import React from 'react';
+
+import { EuiMarkdownFormat } from '../../../../src';
+
+const markdownContent = `Beyond Remark's base syntax, **EuiMarkdownFormat** bundles these abilities by default:
+
+\`:smile:\` we support emojis :smile:!
+
+\`!{tooltip[anchor text](Tooltip content)}\` syntax can render !{tooltip[tooltips like this](I am Jack's helpful tooltip content)}
+
+We also support checkboxes so that
+
+\`\`\`
+- [ ] Checkboxes
+- [x] Can be filled
+- [ ] Or empty
+\`\`\`
+
+turns into
+
+- [ ] Checkboxes
+- [x] Can be filled
+- [ ] Or empty
+
+Note that you'll need to use *EuiMarkdownEditor* to make those checkboxes dynamic.
+`;
+
+export default () => {
+ return {markdownContent};
+};
diff --git a/src-docs/src/views/markdown_editor/markdown-example.md b/src-docs/src/views/markdown_editor/markdown_format_sink.js
similarity index 88%
rename from src-docs/src/views/markdown_editor/markdown-example.md
rename to src-docs/src/views/markdown_editor/markdown_format_sink.js
index 56f9fbdd166..9cb5d06af7f 100644
--- a/src-docs/src/views/markdown_editor/markdown-example.md
+++ b/src-docs/src/views/markdown_editor/markdown_format_sink.js
@@ -1,4 +1,8 @@
-# h1 Heading
+import React from 'react';
+
+import { EuiMarkdownFormat } from '../../../../src';
+
+const markdownContent = `# h1 Heading
## h2 Heading
### h3 Heading
#### h4 Heading
@@ -69,27 +73,27 @@ Ordered
## Code
-Inline `` is awesome!
+Inline \`\` is awesome!
Block code "fences"
-```
+\`\`\`
Sample text here...
-```
+\`\`\`
Syntax highlighting JS
-``` js
+\`\`\` js
var foo = function (bar) {
return bar++;
};
console.log(foo(5));
-```
+\`\`\`
Syntax highlighting Java
-``` java
+\`\`\` java
package l2f.gameserver.model;
public abstract class L2Char extends L2Object {
@@ -103,7 +107,7 @@ public abstract class L2Char extends L2Object {
}
}
}
-```
+\`\`\`
## Tables
@@ -139,3 +143,8 @@ Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
### [Emojies](https://github.com/markdown-it/markdown-it-emoji)
> Classic markup: :wink: :cry: :laughing: :yum:
+`;
+
+export default () => {
+ return {markdownContent};
+};
diff --git a/src-docs/src/views/markdown_editor/markdown_plugin_example.js b/src-docs/src/views/markdown_editor/markdown_plugin_example.js
new file mode 100644
index 00000000000..627c34d78a3
--- /dev/null
+++ b/src-docs/src/views/markdown_editor/markdown_plugin_example.js
@@ -0,0 +1,379 @@
+import React, { Fragment } from 'react';
+
+import { renderToHtml } from '../../services';
+
+import { GuideSectionTypes } from '../../components';
+
+import {
+ EuiMarkdownEditor,
+ EuiText,
+ EuiTitle,
+ EuiSpacer,
+ EuiDescriptionList,
+ EuiHorizontalRule,
+ EuiCodeBlock,
+ EuiCode,
+ EuiLink,
+} from '../../../../src/components';
+
+import { Link } from 'react-router-dom';
+
+import MarkdownEditorWithPlugins from './markdown_editor_with_plugins';
+const markdownEditorWithPluginsSource = require('!!raw-loader!./markdown_editor_with_plugins');
+const markdownEditorWithPluginsHtml = renderToHtml(MarkdownEditorWithPlugins);
+
+const pluginSnippet = `
+
+
+
+`;
+
+const uiPluginSnippet = `const myPluginUI = {
+ name: 'myPlugin',
+ button: {
+ label: 'Chart',
+ iconType: 'visArea',
+ },
+ helpText: (
A node that explains how the syntax works
),
+ editor: function editor({ node, onSave, onCancel }) { return ('something'); },
+}; `;
+
+const pluginConcepts = [
+ {
+ title: 'uiPlugin',
+ description: (
+
+ Provides the UI for the button in the toolbar as well
+ as any modals or extra UI that provides content to the editor.
+
+ ),
+ },
+ {
+ title: 'parsingPluginList',
+ description: (
+
+ Provides the logic to identify the new syntax and parse it into an{' '}
+ AST node.
+
+ ),
+ },
+ {
+ title: 'processingPluginList',
+ description: (
+
+ Provides the logic to process the new AST node into a{' '}
+ React node.
+
+ ),
+ },
+];
+
+const uiPluginConcepts = [
+ {
+ title: 'name',
+ description: (
+
+ The name of your plugin. Use the button.label listed
+ below if you need a more friendly display name. The button can be
+ ommitted if you wish the user to only utilize syntax to author the
+ content.
+
+ ),
+ },
+ {
+ title: 'button',
+ description: (
+
+ Takes a label and an icon type. This
+ forms the button that appear in the toolbar. Clicking the button will
+ trigger either the editor or formatter
+ .
+
+ ),
+ },
+ {
+ title: 'editor',
+ description: (
+
+ Provides UI controls (like an interactive modal) for how to build the
+ inital content. Must exist if formatting does not.
+
+ ),
+ },
+ {
+ title: 'formatter',
+ description: (
+
+ If no editor is provided, this is an object defining
+ how the plugins markdown tag is styled.
+
+ ),
+ },
+ {
+ title: 'helpText',
+ description: (
+
+ Contains a React node. Should contain some information and an example
+ for how to utlize the syntax. Appears when the markdown icon is clicked
+ on the bottom of the editor.
+
+ ),
+ },
+];
+
+export const MarkdownPluginExample = {
+ title: 'Markdown plugins',
+ intro: (
+
+
+
+ Both{' '}
+
+ EuiMarkdownEditor
+ {' '}
+ and{' '}
+
+ EuiMarkdownFormat
+ {' '}
+ utilize the same underlying plugin architecture to transform string
+ based syntax into React components. At a high level{' '}
+
+ Unified JS
+ {' '}
+ is used in combination with{' '}
+
+ Remark
+ {' '}
+ to provide EUI's markdown components, which are separated into a{' '}
+ parsing and processing layer. These
+ two concepts are kept distinct in EUI components to provide concrete
+ locations for your plugins to be injected, be it editing or rendering.
+ Finally you provide UI to the component to handle
+ interactions with the editor.
+
+
+ In addition to running the full pipeline,{' '}
+ EuiMarkdownEditor uses just the parsing configuration
+ to determine the input's validity, provide messages back to the
+ application, and allow the toolbar buttons to interact with existing
+ markdown tags.
+
+
+
+
+
Plugin development
+
+
+
+
+ An EuiMarkdown plugin is comprised of three major
+ pieces, which are passed searpately as props.
+
+
+
+
+ {pluginSnippet}
+
+
+
+
+
+
+
uiPlugin
+
+
+
+ {uiPluginSnippet}
+
+
+
+
+
+
+
parsingPluginList
+
+
+
+
+
+
+ Remark-parse
+ {' '}
+ is used to parse the input text into markdown AST nodes. Its
+ documentation for{' '}
+
+ writing parsers
+ {' '}
+ is under the Extending the Parser section, but highlights are
+ included below.
+
+
+
+ A parser is comprised of three pieces. There is a wrapping function
+ which is provided to remark-parse and injects the parser, the parser
+ method itself, and a locator function if the markdown tag is inline.
+
+
+
+ The parsing method is called at locations where its markdown down
+ might be found at. The method is responsible for determining if the
+ location is a valid tag, process the tag, and mark report the
+ result.
+
+
+
Inline vs block
+
+ Inline tags are allowed at any point in text, and will be rendered
+ somewhere within a {'
'}
element. For better
+ performance, inline parsers must provide a locate method which
+ reports the location where their next tag might be found. They are
+ not allowed to span multiple lines of the input.
+
+
+
+ Block tags are rendered inside {''}{' '}
+ elements, and do not have a locate method. They can consume as much
+ input text as desired, across multiple lines.
+
+ After parsing the input into an AST, the nodes need to be transformed
+ into React elements. This is performed by a list of processors, the
+ default set converts remark AST into rehype and then into React.
+ Plugins need to define themselves within this transformation process,
+ identifying with the same type its parser uses in its{' '}
+ eat call.
+
+
+
+ {`// example plugin processor
+
+// convert remark nodes to rehype, basically a pass through
+const emojiMarkdownHandler = (h, node) => {
+ return h(node.position, 'emojiPlugin', node, []);
+};
+// receives the configuration from the parser and renders
+const EmojiMarkdownRenderer = ({ emoji }) => {
+ return {emoji};
+};
+
+// add the handler & renderer for \`emojiPlugin\`
+processingList[0][1].handlers.emojiPlugin = emojiMarkdownHandler;
+processingList[1][1].components.emojiPlugin = EmojiMarkdownRenderer;`}
+
+
+ ),
+ sections: [
+ {
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: markdownEditorWithPluginsSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: markdownEditorWithPluginsHtml,
+ },
+ ],
+ 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
+ toolbar.
+
+
+ Note that the EuiMarkdownEditor and{' '}
+ EuiMarkdownFormat examples utilize the same prop
+ list. The editor manages additional controls through the{' '}
+ uiPlugins prop.
+
+
+ ),
+ props: {
+ EuiMarkdownEditor,
+ },
+ demo: ,
+ },
+ ],
+};
diff --git a/src/components/index.js b/src/components/index.js
index 4add95e006d..34556c5f1c9 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -217,8 +217,9 @@ export {
export {
EuiMarkdownEditor,
EuiMarkdownContext,
- defaultProcessingPlugins,
- defaultParsingPlugins,
+ EuiMarkdownFormat,
+ EuiMarkdownDefaultParsingPlugins,
+ EuiMarkdownDefaultProcessingPlugins,
} from './markdown_editor';
export { EuiMark } from './mark';
diff --git a/src/components/markdown_editor/_index.scss b/src/components/markdown_editor/_index.scss
index 194529e7f04..dd5dfac5390 100644
--- a/src/components/markdown_editor/_index.scss
+++ b/src/components/markdown_editor/_index.scss
@@ -4,4 +4,5 @@
@import 'markdown_editor_footer';
@import 'markdown_editor_preview';
@import 'markdown_editor_text_area';
-@import 'markdown_editor_toolbar';
\ No newline at end of file
+@import 'markdown_editor_toolbar';
+@import 'plugins/markdown_tooltip';
diff --git a/src/components/markdown_editor/_markdown_format.scss b/src/components/markdown_editor/_markdown_format.scss
index 51a3cd25244..8aecdb7d956 100644
--- a/src/components/markdown_editor/_markdown_format.scss
+++ b/src/components/markdown_editor/_markdown_format.scss
@@ -12,8 +12,8 @@
$browserDefaultFontSize: 16px;
-// We're setting a function o transform px in em
-// because it's easier to think in px
+// We're setting a function o transform px in em
+// because it's easier to think in px
@function em($pixels, $context: $browserDefaultFontSize) {
@return #{$pixels/$context}em;
}
@@ -22,7 +22,7 @@ $browserDefaultFontSize: 16px;
@include euiFont;
@include euiText;
- // We're using `em` values to support apps where consumers might adjust sizes
+ // We're using `em` values to support apps where consumers might adjust sizes
// and consequently the markdown needs to adjust to these changes
// Font size variables
@@ -41,7 +41,7 @@ $browserDefaultFontSize: 16px;
$euiMarkdownSize: em(16px);
$euiMarkdownSizeL: em(24px);
- // We're using alpha values to support apps that
+ // We're using alpha values to support apps that
// display markdown on backgrounds of various colors
// Grayscale variables
@@ -58,23 +58,33 @@ $browserDefaultFontSize: 16px;
color: $euiColorLightestShade;
}
- > *:first-child {
+ > div > *:first-child {
// sass-lint:disable-block no-important
margin-top: 0 !important;
}
- > *:last-child {
+ > div > * {
+ margin-top: 0;
+ margin-bottom: $euiMarkdownSize;
+ }
+
+ > div > *:last-child,
+ .euiCheckbox {
// sass-lint:disable-block no-important
margin-bottom: 0 !important;
}
+ .euiCheckbox + *:not(.euiCheckbox) {
+ margin-top: $euiMarkdownSize;
+ }
+
p,
blockquote,
ul,
ol,
dl,
- table,
- pre {
+ pre,
+ table {
margin-top: 0;
margin-bottom: $euiMarkdownSize;
line-height: 1.5em;
@@ -263,26 +273,4 @@ $browserDefaultFontSize: 16px;
background-color: transparent;
border-top: 1px solid $euiMarkdownAlphaLightShade;
}
-
- // 8. Code
- // the markdown editor adds a EuiCodeBlock when consumers specify the language
- // when no language is specified it gets the .euiMarkdownFormat__code styles
- &__code {
- @include euiCodeFont;
- color: $euiCodeBlockColor;
- font-size: $euiMarkdownFontSizeXS;
- padding: $euiMarkdownSizeXXS 0;
- margin-bottom: $euiMarkdownSizeXS;
- background-color: $euiMarkdownAlphaLightestShade;
- }
-
- // default styles for code blocks
- pre &__code {
- display: block;
- padding: $euiMarkdownFontSizeL;
- overflow: visible;
- line-height: inherit;
- word-wrap: normal;
- white-space: pre;
- }
}
diff --git a/src/components/markdown_editor/index.ts b/src/components/markdown_editor/index.ts
index 5c1f4b4a7f4..299fb9f9357 100644
--- a/src/components/markdown_editor/index.ts
+++ b/src/components/markdown_editor/index.ts
@@ -17,13 +17,13 @@
* under the License.
*/
+export { EuiMarkdownEditor, EuiMarkdownEditorProps } from './markdown_editor';
export {
- EuiMarkdownEditor,
- EuiMarkdownEditorProps,
- defaultParsingPlugins,
- defaultProcessingPlugins,
-} from './markdown_editor';
+ EuiMarkdownDefaultParsingPlugins,
+ EuiMarkdownDefaultProcessingPlugins,
+} from './plugins/markdown_default_plugins';
export { EuiMarkdownContext } from './markdown_context';
+export { EuiMarkdownFormat } from './markdown_format';
export {
EuiMarkdownParseError,
EuiMarkdownAstNode,
diff --git a/src/components/markdown_editor/markdown_editor.tsx b/src/components/markdown_editor/markdown_editor.tsx
index d63ae78229c..b0771cf4af4 100644
--- a/src/components/markdown_editor/markdown_editor.tsx
+++ b/src/components/markdown_editor/markdown_editor.tsx
@@ -33,11 +33,6 @@ import React, {
import unified, { PluggableList, Processor } from 'unified';
import { VFileMessage } from 'vfile-message';
import classNames from 'classnames';
-import emoji from 'remark-emoji';
-import markdown from 'remark-parse';
-import remark2rehype from 'remark-rehype';
-import highlight from 'remark-highlight.js';
-import rehype2react from 'rehype-react';
import { CommonProps, OneOf } from '../common';
import MarkdownActions, { insertText } from './markdown_actions';
@@ -46,8 +41,7 @@ import { EuiMarkdownEditorTextArea } from './markdown_editor_text_area';
import { EuiMarkdownFormat } from './markdown_format';
import { EuiMarkdownEditorDropZone } from './markdown_editor_drop_zone';
import { htmlIdGenerator } from '../../services/accessibility';
-import { EuiLink } from '../link';
-import { EuiCodeBlock, EuiCodeBlockProps } from '../code';
+
import { MARKDOWN_MODE, MODE_EDITING, MODE_VIEWING } from './markdown_modes';
import {
EuiMarkdownAstNode,
@@ -58,46 +52,10 @@ import { EuiOverlayMask } from '../overlay_mask';
import { EuiModal } from '../modal';
import { ContextShape, EuiMarkdownContext } from './markdown_context';
import * as MarkdownTooltip from './plugins/markdown_tooltip';
-import * as MarkdownCheckbox from './plugins/markdown_checkbox';
-
-export const defaultParsingPlugins: PluggableList = [
- [markdown, {}],
- [highlight, {}],
- [emoji, { emoticon: true }],
- [MarkdownTooltip.parser, {}],
- [MarkdownCheckbox.parser, {}],
-];
-
-export const defaultProcessingPlugins: PluggableList = [
- [
- remark2rehype,
- {
- allowDangerousHtml: true,
- handlers: {
- tooltipPlugin: MarkdownTooltip.handler,
- checkboxPlugin: MarkdownCheckbox.handler,
- },
- },
- ],
- [
- rehype2react,
- {
- createElement: createElement,
- components: {
- a: EuiLink,
- code: (props: EuiCodeBlockProps) =>
- // if has classNames is a codeBlock using highlight js
- props.className ? (
-
- ) : (
-
- ),
- tooltipPlugin: MarkdownTooltip.renderer,
- checkboxPlugin: MarkdownCheckbox.renderer,
- },
- },
- ],
-];
+import {
+ EuiMarkdownDefaultParsingPlugins,
+ EuiMarkdownDefaultProcessingPlugins,
+} from './plugins/markdown_default_plugins';
type CommonMarkdownEditorProps = HTMLAttributes &
CommonProps & {
@@ -150,8 +108,8 @@ export const EuiMarkdownEditor: FunctionComponent<
value,
onChange,
height = 150,
- parsingPluginList = defaultParsingPlugins,
- processingPluginList = defaultProcessingPlugins,
+ parsingPluginList = EuiMarkdownDefaultParsingPlugins,
+ processingPluginList = EuiMarkdownDefaultProcessingPlugins,
uiPlugins = [],
onParse,
errors = [],
@@ -205,14 +163,6 @@ export const EuiMarkdownEditor: FunctionComponent<
}
}, [parser, value]);
- const processor = useMemo(
- () =>
- unified()
- .use(parsingPluginList)
- .use(processingPluginList),
- [parsingPluginList, processingPluginList]
- );
-
const isPreviewing = viewMode === MODE_VIEWING;
const replaceNode = useCallback(
@@ -308,7 +258,9 @@ export const EuiMarkdownEditor: FunctionComponent<