diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js index c8c2ba7d123e..d73fbaa7ff60 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js @@ -26,6 +26,7 @@ import { markdownToSlate, slateToMarkdown } from '../serializers'; import withShortcodes from './plugins/shortcodes/withShortcodes'; import insertShortcode from './plugins/shortcodes/insertShortcode'; import defaultEmptyBlock from './plugins/blocks/defaultEmptyBlock'; +import withHtml from './plugins/html/withHtml'; function visualEditorStyles({ minimal }) { return ` @@ -97,7 +98,9 @@ function Editor(props) { const editor = useMemo( () => - withReact(withHistory(withShortcodes(withBlocks(withLists(withInlines(createEditor())))))), + withHtml( + withReact(withHistory(withShortcodes(withBlocks(withLists(withInlines(createEditor())))))), + ), [], ); diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js new file mode 100644 index 000000000000..a7f96cbec3e6 --- /dev/null +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js @@ -0,0 +1,98 @@ +// source: https://github.com/ianstormtaylor/slate/blob/main/site/examples/ts/paste-html.tsx +import { jsx } from 'slate-hyperscript'; +import { Transforms } from 'slate'; + +const ELEMENT_TAGS = { + A: el => ({ type: 'link', url: el.getAttribute('href') }), + BLOCKQUOTE: () => ({ type: 'quote' }), + H1: () => ({ type: 'heading-one' }), + H2: () => ({ type: 'heading-two' }), + H3: () => ({ type: 'heading-three' }), + H4: () => ({ type: 'heading-four' }), + H5: () => ({ type: 'heading-five' }), + H6: () => ({ type: 'heading-six' }), + IMG: el => ({ type: 'image', url: el.getAttribute('src') }), + LI: () => ({ type: 'list-item' }), + OL: () => ({ type: 'numbered-list' }), + P: () => ({ type: 'paragraph' }), + PRE: () => ({ type: 'code' }), + UL: () => ({ type: 'bulleted-list' }), +}; + +// COMPAT: `B` is omitted here because Google Docs uses `` in weird ways. +const TEXT_TAGS = { + CODE: () => ({ code: true }), + DEL: () => ({ strikethrough: true }), + EM: () => ({ italic: true }), + I: () => ({ italic: true }), + S: () => ({ strikethrough: true }), + STRONG: () => ({ bold: true }), + U: () => ({ underline: true }), +}; + +function deserialize(el) { + if (el.nodeType === 3) { + return el.textContent; + } else if (el.nodeType !== 1) { + return null; + } else if (el.nodeName === 'BR') { + return '\n'; + } + + const { nodeName } = el; + let parent = el; + + if (nodeName === 'PRE' && el.childNodes[0] && el.childNodes[0].nodeName === 'CODE') { + parent = el.childNodes[0]; + } + let children = Array.from(parent.childNodes).map(deserialize).flat(); + + if (children.length === 0) { + children = [{ text: '' }]; + } + + if (el.nodeName === 'BODY') { + return jsx('fragment', {}, children); + } + + if (ELEMENT_TAGS[nodeName]) { + const attrs = ELEMENT_TAGS[nodeName](el); + return jsx('element', attrs, children); + } + + if (TEXT_TAGS[nodeName]) { + const attrs = TEXT_TAGS[nodeName](el); + return children.map(child => jsx('text', attrs, child)); + } + + return children; +} + +function withHtml(editor) { + const { insertData, isInline, isVoid } = editor; + + editor.isInline = element => { + return element.type === 'link' ? true : isInline(element); + }; + + editor.isVoid = element => { + return element.type === 'image' ? true : isVoid(element); + }; + + editor.insertData = data => { + const html = data.getData('text/html'); + + if (html) { + const parsed = new DOMParser().parseFromString(html, 'text/html'); + const fragment = deserialize(parsed.body); + Transforms.insertFragment(editor, fragment); + return; + } + + insertData(data); + }; + + return editor; +} + +export default withHtml; diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js index fe825e3dbd0a..9bc0e4c810de 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js @@ -226,8 +226,8 @@ function NumberedList(props) { } function Link(props) { - const url = props.url; - const title = props.title || url; + const url = props.element.url; + const title = props.element.title || url; return ( diff --git a/packages/decap-cms-widget-markdown/src/serializers/slateRemark.js b/packages/decap-cms-widget-markdown/src/serializers/slateRemark.js index b1bae20ffb9f..c75fb69fee32 100644 --- a/packages/decap-cms-widget-markdown/src/serializers/slateRemark.js +++ b/packages/decap-cms-widget-markdown/src/serializers/slateRemark.js @@ -450,7 +450,7 @@ export default function slateToRemark(value, { voidCodeBlock }) { */ case 'link': { const { title, data } = node; - return u(typeMap[node.type], { url: data?.url, title, ...data }, children); + return u(typeMap[node.type], { url: node.url, title, ...data }, children); } /**