diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js index 3a06e026..4d891424 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/components.js @@ -33,17 +33,10 @@ export const NODE_COMPONENTS = { 'table-row': props => {props.children}, 'table-cell': props => {props.children}, 'thematic-break': props =>
, - 'shortcode-wrapper': props =>
{props.children}
, - link: props => { - const data = props.node.get('data'); - const url = data.get('url'); - const title = data.get('title'); - return {props.children}; - }, shortcode: props => { const { attributes, node, state: editorState } = props; const isSelected = editorState.selection.hasFocusIn(node); const className = cn(styles.shortcode, { [styles.shortcodeSelected]: isSelected }); - return {node.data.get('shortcode')}; + return
{node.data.get('shortcode')}
; }, }; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css index 6c40e7c4..6a13efba 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css @@ -96,7 +96,7 @@ .shortcode { border: 2px solid black; padding: 8px; - margin: 2px 0; + margin: 16px 0; cursor: pointer; } diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 70c067e2..ebdaf6b1 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -26,7 +26,7 @@ export default class Editor extends Component { marks: MARK_COMPONENTS, rules: RULES, }, - shortcodes: registry.getEditorComponents(), + shortcodePlugins: registry.getEditorComponents(), }; } @@ -45,7 +45,8 @@ export default class Editor extends Component { handleDocumentChange = (doc, editorState) => { const raw = Raw.serialize(editorState, { terse: true }); - const mdast = slateToRemark(raw); + const plugins = this.state.shortcodePlugins; + const mdast = slateToRemark(raw, plugins); this.props.onChange(mdast); }; @@ -136,12 +137,11 @@ export default class Editor extends Component { const { editorState } = this.state; const data = { shortcode: plugin.id, - shortcodeValue: plugin.toBlock(shortcodeData.toJS()), shortcodeData, }; const nodes = [Text.createFromString('')]; const block = { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; - const resolvedState = editorState.transform().insertBlock(block).apply(); + const resolvedState = editorState.transform().insertBlock(block).focus().apply(); this.ref.onChange(resolvedState); this.setState({ editorState: resolvedState }); }; @@ -180,7 +180,7 @@ export default class Editor extends Component { codeBlock: this.getButtonProps('code', { isBlock: true }), }} onToggleMode={this.handleToggle} - plugins={this.state.shortcodes} + plugins={this.state.shortcodePlugins} onSubmit={this.handlePluginSubmit} onAddAsset={onAddAsset} onRemoveAsset={onRemoveAsset} diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js index 261f9e43..a367b875 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/rules.js @@ -25,6 +25,21 @@ const enforceNeverEmpty = { }, }; -const rules = [ enforceNeverEmpty ]; +/** + * Ensure that shortcodes are children of the root node. + */ +const shortcodesAtRoot = { + match: object => object.kind === 'document', + validate: doc => { + return doc.findDescendant(node => { + return node.type === 'shortcode' && doc.getParent(node.key).key !== doc.key; + }); + }, + normalize: (transform, doc, node) => { + return transform.unwrapNodeByKey(node.key); + }, +}; + +const rules = [ enforceNeverEmpty, shortcodesAtRoot ]; export default rules; diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js index 2683850b..6a5071ba 100644 --- a/src/components/Widgets/Markdown/unified.js +++ b/src/components/Widgets/Markdown/unified.js @@ -143,17 +143,18 @@ const remarkShortcodes = ({ plugins }) => { if (!nodeMayContainShortcode(node)) return node; /** - * Combine the text values of all children to a single string, then - * check that string for a shortcode pattern match. + * Combine the text values of all children to a single string, check the + * string for a shortcode pattern match, and validate the match. */ - const text = mdastToString(node); + const text = mdastToString(node).trim(); const { plugin, match } = matchTextToPlugin(text); + const matchIsValid = validateMatch(text, match); /** - * If a matching shortcode plugin is found, return a new node with shortcode - * data included. Otherwise, return the original node. + * If a valid match is found, return a new node with shortcode data + * included. Otherwise, return the original node. */ - return plugin ? createShortcodeNode(text, plugin, match) : node; + return matchIsValid ? createShortcodeNode(text, plugin, match) : node; }; /** @@ -185,6 +186,13 @@ const remarkShortcodes = ({ plugins }) => { return { plugin, match }; } + /** + * A match is only valid if it takes up the entire paragraph. + */ + function validateMatch(text, match) { + return match && match[0].length === text.length; + } + /** * Create a new node with shortcode data included. Use an 'html' node instead * of a 'text' node as the child to ensure the node content is not parsed by @@ -242,7 +250,9 @@ const remarkToRehypeShortcodes = ({ plugins, getAsset }) => { /** * Return a new 'html' type node containing the shortcode preview markup. */ - return u('html', valueHtml); + const textNode = u('html', valueHtml); + const children = [ textNode ]; + return { ...node, children }; } }; @@ -471,7 +481,7 @@ export const remarkToSlate = mdast => { return result; }; -export const slateToRemark = raw => { +export const slateToRemark = (raw, shortcodePlugins) => { const typeMap = { 'paragraph': 'paragraph', 'heading-one': 'heading', @@ -532,7 +542,9 @@ export const slateToRemark = raw => { if (node.type === 'shortcode') { const { data } = node; - const textNode = u('html', data.shortcodeValue); + const plugin = shortcodePlugins.get(data.shortcode); + const text = plugin.toBlock(data.shortcodeData); + const textNode = u('html', text); return u('paragraph', { data }, [ textNode ]); }