diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 0f4bacd78dd..cd0f513f65d 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -135,6 +135,10 @@ import { LoadingExample } from './views/loading/loading_example'; import { MarkdownEditorExample } from './views/markdown_editor/mardown_editor_example'; +import { MarkdownFormatExample } from './views/markdown_editor/mardown_format_example'; + +import { MarkdownPluginExample } from './views/markdown_editor/markdown_plugin_example'; + import { ModalExample } from './views/modal/modal_example'; import { MutationObserverExample } from './views/mutation_observer/mutation_observer_example'; @@ -366,7 +370,6 @@ const navigation = [ BadgeExample, CallOutExample, CardExample, - CodeExample, CommentListExample, DescriptionListExample, DragAndDropExample, @@ -396,11 +399,9 @@ const navigation = [ SuperSelectExample, ComboBoxExample, ColorPickerExample, - CodeEditorExample, DatePickerExample, ExpressionExample, FilterGroupExample, - MarkdownEditorExample, RangeControlExample, SearchBarExample, SelectableExample, @@ -408,6 +409,16 @@ const navigation = [ SuperDatePickerExample, ].map(example => createExample(example)), }, + { + name: 'Editors & syntax', + items: [ + MarkdownFormatExample, + MarkdownEditorExample, + MarkdownPluginExample, + CodeEditorExample, + CodeExample, + ].map(example => createExample(example)), + }, { name: 'Elastic Charts', items: [ diff --git a/src-docs/src/views/markdown_editor/mardown_editor_example.js b/src-docs/src/views/markdown_editor/mardown_editor_example.js index c6744de6bb0..02656a92af1 100644 --- a/src-docs/src/views/markdown_editor/mardown_editor_example.js +++ b/src-docs/src/views/markdown_editor/mardown_editor_example.js @@ -1,17 +1,47 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import { renderToHtml } from '../../services'; import { GuideSectionTypes } from '../../components'; -import { EuiMarkdownEditor } from '../../../../src/components'; +import { + EuiMarkdownEditor, + EuiText, + EuiSpacer, + EuiCode, +} from '../../../../src/components'; + +import { Link } from 'react-router-dom'; import MarkdownEditor from './markdown_editor'; const markdownEditorSource = require('!!raw-loader!./markdown_editor'); const markdownEditorHtml = renderToHtml(MarkdownEditor); +import MarkdownEditorErrors from './markdown_editor_errors'; +const markdownEditorErrorsSource = require('!!raw-loader!./markdown_editor_errors'); +const markdownEditorErrorsHtml = renderToHtml(MarkdownEditorErrors); + export const MarkdownEditorExample = { - title: 'Markdown Editor', + title: 'Markdown editor', + intro: ( + + +

+ 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. +

+
+ +
+ ), sections: [ { source: [ @@ -24,10 +54,11 @@ export const MarkdownEditorExample = { code: markdownEditorHtml, }, ], + title: 'Base editor', text: (

- 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.

), props: { @@ -35,5 +66,32 @@ export const MarkdownEditorExample = { }, demo: , }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: markdownEditorErrorsSource, + }, + { + type: GuideSectionTypes.HTML, + code: markdownEditorErrorsHtml, + }, + ], + title: 'Error handling and feedback', + text: ( +

+ 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. +

+ ), + props: { + EuiMarkdownEditor, + }, + demo: , + }, ], }; diff --git a/src-docs/src/views/markdown_editor/mardown_format_example.js b/src-docs/src/views/markdown_editor/mardown_format_example.js new file mode 100644 index 00000000000..352d45e844b --- /dev/null +++ b/src-docs/src/views/markdown_editor/mardown_format_example.js @@ -0,0 +1,94 @@ +import React, { Fragment } from 'react'; + +import { renderToHtml } from '../../services'; + +import { GuideSectionTypes } from '../../components'; + +import { + EuiMarkdownFormat, + EuiText, + EuiSpacer, +} from '../../../../src/components'; + +import { Link } from 'react-router-dom'; + +import MarkdownFormat from './markdown_format'; +const markdownFormatSource = require('!!raw-loader!./markdown_format'); +const markdownFormatHtml = renderToHtml(MarkdownFormat); + +import MarkdownFormatSink from './markdown_format_sink'; +const markdownFormatSinkSource = require('!!raw-loader!./markdown_format_sink'); +const markdownFormatSinkHtml = renderToHtml(MarkdownFormatSink); + +export const MarkdownFormatExample = { + title: 'Markdown format', + intro: ( + + +

+ 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. +

+
+ +
+ ), + sections: [ + { + source: [ + { + type: GuideSectionTypes.JS, + code: markdownFormatSource, + }, + { + type: GuideSectionTypes.HTML, + code: markdownFormatHtml, + }, + ], + title: 'Built in plugins', + text: ( +

+ 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. +

+ ), + props: { + EuiMarkdownFormat, + }, + demo: , + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: markdownFormatSinkSource, + }, + { + type: GuideSectionTypes.HTML, + code: markdownFormatSinkHtml, + }, + ], + title: 'Kitchen sink', + text: ( +

+ This example shows of all the styling and markup possibilities. It is + mostly used for testing. +

+ ), + props: { + EuiMarkdownFormat, + }, + demo: , + }, + ], +}; diff --git a/src-docs/src/views/markdown_editor/markdown_editor.js b/src-docs/src/views/markdown_editor/markdown_editor.js index 2dd2ce8ffc7..c2398c2328f 100644 --- a/src-docs/src/views/markdown_editor/markdown_editor.js +++ b/src-docs/src/views/markdown_editor/markdown_editor.js @@ -1,25 +1,25 @@ import React, { useCallback, useState } from 'react'; import { - defaultParsingPlugins, - defaultProcessingPlugins, EuiMarkdownEditor, EuiSpacer, EuiCodeBlock, EuiButtonToggle, -} from '../../../../src'; -import * as MarkdownChart from './plugins/markdown_chart'; +} from '../../../../src/components'; -const markdownExample = require('!!raw-loader!./markdown-example.md'); +const initialContent = `## Hello world! -const exampleParsingList = [...defaultParsingPlugins, MarkdownChart.parser]; +Basic "github flavored" markdown will work as you'd expect. -const exampleProcessingList = [...defaultProcessingPlugins]; // pretend mutation doesn't happen immediately next 😅 -exampleProcessingList[0][1].handlers.chartDemoPlugin = MarkdownChart.handler; -exampleProcessingList[1][1].components.chartDemoPlugin = MarkdownChart.renderer; +The editor also ships with some built in plugins. For example it can handle checkboxes. Notice how they toggle state even in the preview mode. + +- [ ] Checkboxes +- [x] Can be filled +- [ ] Or empty +`; export default () => { - const [value, setValue] = useState(markdownExample); + const [value, setValue] = useState(initialContent); const [messages, setMessages] = useState([]); const [ast, setAst] = useState(null); const [isAstShowing, setIsAstShowing] = useState(false); @@ -34,9 +34,6 @@ export default () => { value={value} onChange={setValue} height={400} - uiPlugins={[MarkdownChart.plugin]} - parsingPluginList={exampleParsingList} - processingPluginList={exampleProcessingList} onParse={onParse} errors={messages} /> diff --git a/src-docs/src/views/markdown_editor/markdown_editor_errors.js b/src-docs/src/views/markdown_editor/markdown_editor_errors.js new file mode 100644 index 00000000000..ef6c3546739 --- /dev/null +++ b/src-docs/src/views/markdown_editor/markdown_editor_errors.js @@ -0,0 +1,63 @@ +import React, { useCallback, useState } from 'react'; + +import { Link } from 'react-router-dom'; + +import { + EuiMarkdownEditor, + EuiSpacer, + EuiCodeBlock, + EuiButtonToggle, + EuiFormErrorText, +} from '../../../../src/components'; + +const initialContent = `## Errors + +The tooltip is empty and will error + +!{tooltip[]()} +`; + +export default () => { + const [value, setValue] = useState(initialContent); + const [messages, setMessages] = useState([]); + const [ast, setAst] = useState(null); + const [isAstShowing, setIsAstShowing] = useState(false); + const onParse = useCallback((err, { messages, ast }) => { + setMessages(err ? [err] : messages); + setAst(JSON.stringify(ast, null, 2)); + }, []); + return ( + <> + + + + + Utilize error text or{' '} + + EuiFormRow + {' '} + for more permanent error feedback + + +
+ setIsAstShowing(!isAstShowing)} + isSelected={isAstShowing} + /> +
+ + {isAstShowing && {ast}} + + ); +}; diff --git a/src-docs/src/views/markdown_editor/plugins/markdown_chart.js b/src-docs/src/views/markdown_editor/markdown_editor_with_plugins.js similarity index 67% rename from src-docs/src/views/markdown_editor/plugins/markdown_chart.js rename to src-docs/src/views/markdown_editor/markdown_editor_with_plugins.js index 081555924b7..7d86dfc9c64 100644 --- a/src-docs/src/views/markdown_editor/plugins/markdown_chart.js +++ b/src-docs/src/views/markdown_editor/markdown_editor_with_plugins.js @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; + import { Chart, Settings, @@ -6,8 +7,17 @@ import { BarSeries, DataGenerator, } from '@elastic/charts'; -import { EUI_CHARTS_THEME_LIGHT } from '../../../../../src/themes/charts/themes'; + +import { EUI_CHARTS_THEME_LIGHT } from '../../../../src/themes/charts/themes'; + import { + EuiMarkdownDefaultParsingPlugins, + EuiMarkdownDefaultProcessingPlugins, + EuiMarkdownEditor, + EuiMarkdownFormat, + EuiSpacer, + EuiCodeBlock, + EuiButtonToggle, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody, @@ -18,10 +28,9 @@ import { EuiFormRow, EuiSelect, EuiRange, - EuiCodeBlock, EuiText, - EuiSpacer, -} from '../../../../../src/components'; +} from '../../../../src/components'; + import { euiPaletteColorBlind, euiPaletteComplimentary, @@ -32,7 +41,7 @@ import { euiPaletteNegative, euiPalettePositive, euiPaletteWarm, -} from '../../../../../src/services/color'; +} from '../../../../src/services/color'; const paletteData = { euiPaletteColorBlind, @@ -45,6 +54,7 @@ const paletteData = { euiPaletteWarm, euiPaletteGray, }; + const paletteNames = Object.keys(paletteData); const dg = new DataGenerator(); @@ -112,7 +122,16 @@ const chartDemoPlugin = { /> - +
+ +
@@ -132,7 +151,7 @@ const chartDemoPlugin = { }, }; -function ChartParser() { +function ChartMarkdownParser() { const Parser = this.Parser; const tokenizers = Parser.prototype.blockTokenizers; const methods = Parser.prototype.blockMethods; @@ -229,9 +248,64 @@ const ChartMarkdownRenderer = ({ height = 200, palette = 5 }) => { ); }; -export { - chartDemoPlugin as plugin, - ChartParser as parser, - chartMarkdownHandler as handler, - ChartMarkdownRenderer as renderer, +const exampleParsingList = [ + ...EuiMarkdownDefaultParsingPlugins, + ChartMarkdownParser, +]; + +const exampleProcessingList = [...EuiMarkdownDefaultProcessingPlugins]; // pretend mutation doesn't happen immediately next 😅 +exampleProcessingList[0][1].handlers.chartDemoPlugin = chartMarkdownHandler; +exampleProcessingList[1][1].components.chartDemoPlugin = ChartMarkdownRenderer; + +const initialExample = `## Chart plugin + +Notice the toolbar above has a new chart button. Click it to add a chart. + +Once you finish it'll add some syntax that looks like the below. + +!{chart{"palette":4,"height":300}} +`; + +export default () => { + const [value, setValue] = useState(initialExample); + const [messages, setMessages] = useState([]); + const [ast, setAst] = useState(null); + const [isAstShowing, setIsAstShowing] = useState(false); + const onParse = useCallback((err, { messages, ast }) => { + setMessages(err ? [err] : messages); + setAst(JSON.stringify(ast, null, 2)); + }, []); + return ( + <> + + +
+ setIsAstShowing(!isAstShowing)} + isSelected={isAstShowing} + /> +
+ {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. +

+
+
+ + + {`// 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'); +}`} + + + +

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 + +// 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<
- + {value}
diff --git a/src/components/markdown_editor/markdown_format.tsx b/src/components/markdown_editor/markdown_format.tsx index decdac83182..4f942527cbc 100644 --- a/src/components/markdown_editor/markdown_format.tsx +++ b/src/components/markdown_editor/markdown_format.tsx @@ -18,23 +18,38 @@ */ import React, { FunctionComponent, useMemo } from 'react'; -import { Processor } from 'unified'; +import unified, { PluggableList } from 'unified'; +import { + EuiMarkdownDefaultProcessingPlugins, + EuiMarkdownDefaultParsingPlugins, +} from './plugins/markdown_default_plugins'; interface EuiMarkdownFormatProps { children: string; - processor: Processor; + /** array of unified plugins to parse content into an AST */ + parsingPluginList?: PluggableList; + /** array of unified plugins to convert the AST into a ReactNode */ + processingPluginList?: PluggableList; } export const EuiMarkdownFormat: FunctionComponent = ({ children, - processor, + parsingPluginList = EuiMarkdownDefaultParsingPlugins, + processingPluginList = EuiMarkdownDefaultProcessingPlugins, }) => { + const processor = useMemo( + () => + unified() + .use(parsingPluginList) + .use(processingPluginList), + [parsingPluginList, processingPluginList] + ); const result = useMemo(() => { try { return processor.processSync(children).contents; } catch (e) { return children; } - }, [processor, children]); + }, [children, processor]); return
{result}
; }; diff --git a/src/components/markdown_editor/plugins/markdown_checkbox.tsx b/src/components/markdown_editor/plugins/markdown_checkbox.tsx index 339fb702838..f1ec36d7e10 100644 --- a/src/components/markdown_editor/plugins/markdown_checkbox.tsx +++ b/src/components/markdown_editor/plugins/markdown_checkbox.tsx @@ -47,12 +47,12 @@ const CheckboxParser: Plugin = function CheckboxParser() { silent ) { /** - * optional leading whitespace & single dash mix + * optional leading whitespace & single (dash or asterisk) mix * square brackets, optionally containing whitespace and `x` * optional whitespace * remainder of the line is consumed as the textbox label */ - const checkboxMatch = value.match(/^(\s*-\s*)?\[([\sx]*)\](.+)/); + const checkboxMatch = value.match(/^(\s*[-*]\s*)?\[([\sx]*)\](.+)/); if (checkboxMatch == null) return false; if (silent) { diff --git a/src/components/markdown_editor/plugins/markdown_default_plugins.tsx b/src/components/markdown_editor/plugins/markdown_default_plugins.tsx new file mode 100644 index 00000000000..174bfce7f31 --- /dev/null +++ b/src/components/markdown_editor/plugins/markdown_default_plugins.tsx @@ -0,0 +1,69 @@ +/* + * 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 { PluggableList } from 'unified'; +import remark2rehype from 'remark-rehype'; +import rehype2react from 'rehype-react'; +import * as MarkdownTooltip from './markdown_tooltip'; +import * as MarkdownCheckbox from './markdown_checkbox'; +import React, { createElement } from 'react'; +import { EuiLink } from '../../link'; +import { EuiCodeBlock, EuiCode } from '../../code'; +import markdown from 'remark-parse'; +import highlight from 'remark-highlight.js'; +import emoji from 'remark-emoji'; + +export const EuiMarkdownDefaultParsingPlugins: PluggableList = [ + [markdown, {}], + [highlight, {}], + [emoji, { emoticon: true }], + [MarkdownTooltip.parser, {}], + [MarkdownCheckbox.parser, {}], +]; + +export const EuiMarkdownDefaultProcessingPlugins: PluggableList = [ + [ + remark2rehype, + { + allowDangerousHtml: true, + handlers: { + tooltipPlugin: MarkdownTooltip.handler, + checkboxPlugin: MarkdownCheckbox.handler, + }, + }, + ], + [ + rehype2react, + { + createElement: createElement, + components: { + a: EuiLink, + code: (props: any) => + // If there are linebreaks use codeblock, otherwise code + /\r|\n/.exec(props.children) ? ( + + ) : ( + + ), + tooltipPlugin: MarkdownTooltip.renderer, + checkboxPlugin: MarkdownCheckbox.renderer, + }, + }, + ], +]; diff --git a/src/components/markdown_editor/plugins/markdown_tooltip.scss b/src/components/markdown_editor/plugins/markdown_tooltip.scss new file mode 100644 index 00000000000..2fe1a191c83 --- /dev/null +++ b/src/components/markdown_editor/plugins/markdown_tooltip.scss @@ -0,0 +1,4 @@ +// This is to offset the tooltip icon, which isn't perfectly centered +.euiMarkdownTooltip__icon { + transform: translateY(-1px); +} diff --git a/src/components/markdown_editor/plugins/markdown_tooltip.tsx b/src/components/markdown_editor/plugins/markdown_tooltip.tsx index 9b215ebf861..fa94c5d2de6 100644 --- a/src/components/markdown_editor/plugins/markdown_tooltip.tsx +++ b/src/components/markdown_editor/plugins/markdown_tooltip.tsx @@ -25,6 +25,7 @@ import { RemarkTokenizer, } from '../markdown_types'; import { EuiToolTip } from '../../tool_tip'; +import { EuiIcon } from '../../icon'; import { EuiCodeBlock } from '../../code'; import { Plugin } from 'unified'; @@ -143,9 +144,17 @@ const tooltipMarkdownRenderer: FunctionComponent< TooltipNodeDetails & { position: EuiMarkdownAstNodePosition } > = ({ content, children }) => { return ( - - {children} - + + + + {children} + + + + ); };