diff --git a/package.json b/package.json
index a2405c4c..67d4629b 100644
--- a/package.json
+++ b/package.json
@@ -171,6 +171,7 @@
"textarea-caret-position": "^0.1.1",
"unified": "^6.1.4",
"unist-builder": "^1.0.2",
+ "unist-util-map": "^1.0.3",
"unist-util-modify-children": "^1.1.1",
"uuid": "^2.0.3",
"whatwg-fetch": "^1.0.0"
diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js
index c49cf597..ec2070fe 100644
--- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js
+++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js
@@ -139,6 +139,7 @@ const NODE_COMPONENTS = {
},
'shortcode': props => {
const { attributes, node, state: editorState } = props;
+ const { data } = node;
const isSelected = editorState.selection.hasFocusIn(node);
return (
- {getShortcodeId(props)}
+ {data.get('shortcode')}
);
},
diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js
index 434f2b34..139c485d 100644
--- a/src/components/Widgets/Markdown/unified.js
+++ b/src/components/Widgets/Markdown/unified.js
@@ -1,4 +1,5 @@
-import { get, find, isEmpty } from 'lodash';
+import { get, has, find, isEmpty } from 'lodash';
+import { renderToString } from 'react-dom/server';
import unified from 'unified';
import u from 'unist-builder';
import markdownToRemarkPlugin from 'remark-parse';
@@ -20,7 +21,6 @@ import { reduce, capitalize } from 'lodash';
// Remove the yaml tokenizer, as the rich text editor doesn't support frontmatter
delete markdownToRemarkPlugin.Parser.prototype.blockTokenizers.yamlFrontMatter;
-console.log(markdownToRemarkPlugin.Parser.prototype.blockTokenizers);
const shortcodeAttributePrefix = 'ncp';
@@ -150,6 +150,90 @@ function remarkPrecompileShortcodes() {
visitors.text = node => node.value;
};
+const remarkShortcodes = ({ plugins }) => {
+ return transform;
+
+ function transform(node) {
+ if (node.children) {
+ node.children = node.children.reduce(reducer, []);
+ }
+ return node;
+
+ function reducer(newChildren, childNode) {
+ if (!['text', 'html'].includes(childNode.type)) {
+ const processedNode = childNode.children ? transform(childNode) : childNode;
+ newChildren.push(processedNode);
+ return newChildren;
+ }
+
+ const text = childNode.value;
+ let lastPlugin;
+ let match;
+ const plugin = plugins.find(p => {
+ match = text.match(p.pattern);
+ return match;
+ });
+ if (!plugin) {
+ newChildren.push(childNode);
+ return newChildren;
+ }
+ const matchValue = match[0];
+ const matchLength = matchValue.length;
+ const matchAll = matchLength === text.length;
+
+ if (matchAll) {
+ const shortcodeNode = createShortcodeNode(text, plugin, match);
+ newChildren.push(shortcodeNode);
+ return newChildren;
+ }
+
+ const tempChildren = [];
+ const matchAtStart = match.index === 0;
+ const matchAtEnd = match.index + matchLength === text.length;
+
+ if (!matchAtStart) {
+ const textBeforeMatch = text.slice(0, match.index);
+ const result = reducer([], { type: 'text', value: textBeforeMatch });
+ tempChildren.push(...result);
+ }
+
+ const matchNode = createShortcodeNode(matchValue, plugin, match);
+ tempChildren.push(matchNode);
+
+ if (!matchAtEnd) {
+ const textAfterMatch = text.slice(match.index + matchLength);
+ const result = reducer([], { type: 'text', value: textAfterMatch });
+ tempChildren.push(...result);
+ }
+
+ newChildren.push(...tempChildren);
+ return newChildren;
+ }
+
+ function createShortcodeNode(text, plugin, match) {
+ const shortcode = plugin.id;
+ const shortcodeData = plugin.fromBlock(match);
+ return { type: 'html', value: text, data: { shortcode, shortcodeData } };
+ }
+ }
+};
+
+const remarkToRehypeShortcodes = ({ plugins }) => {
+ return transform;
+
+ function transform(node) {
+ const children = node.children ? node.children.map(transform) : node.children;
+ if (!has(node, ['data', 'shortcode'])) {
+ return { ...node, children };
+ }
+ const { shortcode, shortcodeData } = node.data;
+ const plugin = plugins.get(shortcode);
+ const value = plugin.toPreview(shortcodeData);
+ const valueHtml = typeof value === 'string' ? value : renderToString(value);
+ return { ...node, value: valueHtml };
+ }
+};
+
const parseShortcodesFromMarkdown = markdown => {
const plugins = registry.getEditorComponents();
const markdownLines = markdown.split('\n');
@@ -191,7 +275,7 @@ const remarkToSlatePlugin = () => {
delete: 'strikethrough',
inlineCode: 'code',
};
- const toTextNode = text => ({ kind: 'text', text });
+ const toTextNode = (text, data) => ({ kind: 'text', text, data });
const wrapText = (node, index, parent) => {
if (['text', 'html'].includes(node.type)) {
parent.children.splice(index, 1, u('paragraph', [node]));
@@ -199,11 +283,13 @@ const remarkToSlatePlugin = () => {
};
let getDefinition;
- const transform = node => {
+ const transform = (node, index, siblings, parent) => {
let nodes;
if (node.type === 'root') {
+ // Create definition getter for link and image references
getDefinition = mdastDefinitions(node);
+ // Ensure top level text nodes are wrapped in paragraphs
modifyChildren(wrapText)(node);
}
@@ -213,8 +299,10 @@ const remarkToSlatePlugin = () => {
// If a node returns a falsey value, exclude it. Some nodes do not
// translate from MDAST to Slate, such as definitions for link/image
// references or footnotes.
- nodes = node.children.reduce((acc, childNode) => {
- const transformed = transform(childNode);
+ //
+ // Consider using unist-util-remove instead for this.
+ nodes = node.children.reduce((acc, childNode, idx, sibs) => {
+ const transformed = transform(childNode, idx, sibs, node);
if (transformed) {
acc.push(transformed);
}
@@ -228,7 +316,17 @@ const remarkToSlatePlugin = () => {
// Process raw html as text, since it's valid markdown
if (['text', 'html'].includes(node.type)) {
- return toTextNode(node.value);
+ const { value, data } = node;
+ const shortcode = get(data, 'shortcode');
+ if (shortcode) {
+ const isBlock = parent.type === 'paragraph' && siblings.length === 1;
+ data.shortcodeValue = value;
+
+ if (isBlock) {
+ return { kind: 'block', type: 'shortcode', data, isVoid: true, nodes: [toTextNode('')] };
+ }
+ }
+ return toTextNode(value, data);
}
if (node.type === 'inlineCode') {
@@ -315,7 +413,10 @@ const remarkToSlatePlugin = () => {
return { kind: 'block', type: typeMap['image'], data };
}
};
- return transform;
+
+ // Since `transform` is used for recursive child mapping, ensure that only the
+ // first argument is supplied on the initial call.
+ return node => transform(node);
};
const slateToRemarkPlugin = () => {
@@ -327,9 +428,14 @@ const slateToRemarkPlugin = () => {
};
export const markdownToRemark = markdown => {
- const result = unified()
+ const parsed = unified()
.use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true })
.parse(markdown);
+
+ const result = unified()
+ .use(remarkShortcodes, { plugins: registry.getEditorComponents() })
+ .runSync(parsed);
+
return result;
};
@@ -399,6 +505,7 @@ export const slateToRemark = raw => {
}
});
} else {
+
acc.push(u('html', childNode.text));
}
return acc;
@@ -408,6 +515,10 @@ export const slateToRemark = raw => {
return u('root', children);
}
+ if (node.type === 'shortcode') {
+ return u('html', { data: node.data }, node.data.shortcodeValue);
+ }
+
if (node.type.startsWith('heading')) {
const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 };
const depth = node.type.split('-')[1];
@@ -448,19 +559,21 @@ export const slateToRemark = raw => {
}
}
raw.type = 'root';
- const result = transform(raw);
+ const mdast = transform(raw);
+
+ const result = unified()
+ .use(remarkShortcodes, { plugins: registry.getEditorComponents() })
+ .runSync(mdast);
+
return result;
};
export const remarkToHtml = mdast => {
const result = unified()
+ .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents() })
.use(remarkToRehype, { allowDangerousHTML: true })
- .use(rehypeReparse)
.use(rehypeRemoveEmpty)
.use(rehypeMinifyWhitespace)
- .use(() => node => {
- return node;
- })
.runSync(mdast);
const output = unified()
diff --git a/yarn.lock b/yarn.lock
index 848b2350..97a6a911 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5321,7 +5321,7 @@ mdast-util-compact@^1.0.0:
unist-util-modify-children "^1.0.0"
unist-util-visit "^1.1.0"
-mdast-util-definitions@^1.2.0:
+mdast-util-definitions@^1.2.0, mdast-util-definitions@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.2.tgz#673f4377c3e23d3de7af7a4fe2214c0e221c5ac7"
dependencies:
@@ -9010,7 +9010,13 @@ unist-util-is@^2.0.0, unist-util-is@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b"
-unist-util-modify-children@^1.0.0:
+unist-util-map@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/unist-util-map/-/unist-util-map-1.0.3.tgz#26a913d7cddb3cd3e9a886d135d37a3d1f54e514"
+ dependencies:
+ object-assign "^4.0.1"
+
+unist-util-modify-children@^1.0.0, unist-util-modify-children@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-1.1.1.tgz#66d7e6a449e6f67220b976ab3cb8b5ebac39e51d"
dependencies: