Markdown plugins
Both EuiMarkdownEditor and EuiMarkdownFormat utilize the same underlying plugin architecture to transform string based syntax into React components. At a high level Unified JS(external, opens in a new tab or window) is used in combination with Remark(external, opens in a new tab or window) 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.
Default plugins
EUI provides additional plugins by default, but these can be omitted or otherwise customized by providing the
parsingPluginList
, processingPluginList
, and uiPlugins
props to the editor and formatter components.The parsing plugins, responsible for parsing markdown are:
The above set provides an abstract syntax tree used by the editor to provide feedback, and the renderer passes that output to the set of processing plugins to allow it to be rendered:
The last set of plugin configuration - uiPlugins
- allows toolbar buttons to be defined and how they alter or inject markdown and returns with only one plugin:
These plugin definitions can be obtained by calling getDefaultEuiMarkdownParsingPlugins
, getDefaultEuiMarkdownProcessingPlugins
, and getDefaultEuiMarkdownUiPlugins
respectively.
Configuring the default plugins
The above plugin utils, as well as getDefaultEuiMarkdownPlugins
, accept an optional configuration object of:
exclude
: an array of default plugins to unregisterparsingConfig
: allows overriding the configuration of any default parsing pluginprocessingConfig
: currently only accepts alinkProps
key, which accepts any prop that EuiLink accepts
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.
Plugin development
An EuiMarkdown plugin is comprised of three major pieces, which are passed searpately as props.
<EuiMarkdownEditor
uiPlugin={myPluginUI}
parsingPluginList={myPluginParsingList}
processingPluginList={myPluginProcessingList}
{..otherProps}
/>
<!-- Note that the format component does not need a UI prop. -->
<EuiMarkdownFormat
parsingPluginList={myPluginParsingList}
processingPluginList={myPluginProcessingList}
/>
- uiPlugin
- Provides the UI for the button in the toolbar as well as any modals or extra UI that provides content to the editor.
- parsingPluginList
- Provides the logic to identify the new syntax and parse it into an AST node.
- processingPluginList
- Provides the logic to process the new AST node into a React node.
uiPlugin
const myPluginUI = {
name: 'myPlugin',
button: {
label: 'Chart',
iconType: 'visArea',
},
helpText: (<div>A node that explains how the syntax works</div>),
editor: function editor({ node, onSave, onCancel }) { return ('something'); },
};
- name
- 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.
- button
- 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.
- editor
- Provides UI controls (like an interactive modal) for how to build the inital content. Must exist if formatting does not.
- formatting
- If no editor is provided, this is an object defining how the plugins markdown tag is styled.
- helpText
- 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.
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 <p>
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 <span>
elements, and do not have a locate method. They can consume as much input text as desired, across multiple lines.
// example plugin parser
function EmojiMarkdownParser() {
const Parser = this.Parser;
const tokenizers = Parser.prototype.inlineTokenizers;
const methods = Parser.prototype.inlineMethods;
const emojiMap = {
wave: '👋',
smile: '😀',
plane: '🛩',
};
const emojiNames = Object.keys(emojiMap);
// function to parse a matching string
function tokenizeEmoji(eat, value, silent) {
const tokenMatch = value.match(/^:(.*?):/);
if (!tokenMatch) return false; // no match
const [, emojiName] = tokenMatch;
// ensure we know this one
if (emojiNames.indexOf(emojiName) === -1) return false;
if (silent) {
return true;
}
// must consume the exact & entire match string
return eat(`:${emojiName}:`)({
type: 'emojiPlugin',
emoji: emojiMap[emojiName], // configuration is passed to the renderer
});
}
// function to detect where the next emoji match might be found
tokenizeEmoji.locator = (value, fromIndex) => {
return value.indexOf(':', fromIndex);
};
// define the emoji plugin and inject it just before the existing text plugin
tokenizers.emoji = tokenizeEmoji;
methods.splice(methods.indexOf('text'), 0, 'emoji');
}
// add the parser for `emojiPlugin`
const parsingList = getDefaultEuiMarkdownParsingPlugins();
parsingList.push(EmojiMarkdownParser);
processingPluginList
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
// receives the configuration from the parser and renders
const EmojiMarkdownRenderer = ({ emoji }) => {
return <span>{emoji}</span>;
};
// add the renderer for `emojiPlugin`
const processingList = getDefaultEuiMarkdownProcessingPlugins();
processingList[1][1].components.emojiPlugin = EmojiMarkdownRenderer;
Putting it all together: a simple chart plugin
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.