re-implement shortcode parsing to/from mdast
This commit is contained in:
parent
c95f06138a
commit
b7379b019e
@ -171,6 +171,7 @@
|
|||||||
"textarea-caret-position": "^0.1.1",
|
"textarea-caret-position": "^0.1.1",
|
||||||
"unified": "^6.1.4",
|
"unified": "^6.1.4",
|
||||||
"unist-builder": "^1.0.2",
|
"unist-builder": "^1.0.2",
|
||||||
|
"unist-util-map": "^1.0.3",
|
||||||
"unist-util-modify-children": "^1.1.1",
|
"unist-util-modify-children": "^1.1.1",
|
||||||
"uuid": "^2.0.3",
|
"uuid": "^2.0.3",
|
||||||
"whatwg-fetch": "^1.0.0"
|
"whatwg-fetch": "^1.0.0"
|
||||||
|
@ -139,6 +139,7 @@ const NODE_COMPONENTS = {
|
|||||||
},
|
},
|
||||||
'shortcode': props => {
|
'shortcode': props => {
|
||||||
const { attributes, node, state: editorState } = props;
|
const { attributes, node, state: editorState } = props;
|
||||||
|
const { data } = node;
|
||||||
const isSelected = editorState.selection.hasFocusIn(node);
|
const isSelected = editorState.selection.hasFocusIn(node);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -146,7 +147,7 @@ const NODE_COMPONENTS = {
|
|||||||
{...attributes}
|
{...attributes}
|
||||||
draggable
|
draggable
|
||||||
>
|
>
|
||||||
{getShortcodeId(props)}
|
{data.get('shortcode')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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 unified from 'unified';
|
||||||
import u from 'unist-builder';
|
import u from 'unist-builder';
|
||||||
import markdownToRemarkPlugin from 'remark-parse';
|
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
|
// Remove the yaml tokenizer, as the rich text editor doesn't support frontmatter
|
||||||
delete markdownToRemarkPlugin.Parser.prototype.blockTokenizers.yamlFrontMatter;
|
delete markdownToRemarkPlugin.Parser.prototype.blockTokenizers.yamlFrontMatter;
|
||||||
console.log(markdownToRemarkPlugin.Parser.prototype.blockTokenizers);
|
|
||||||
|
|
||||||
const shortcodeAttributePrefix = 'ncp';
|
const shortcodeAttributePrefix = 'ncp';
|
||||||
|
|
||||||
@ -150,6 +150,90 @@ function remarkPrecompileShortcodes() {
|
|||||||
visitors.text = node => node.value;
|
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 parseShortcodesFromMarkdown = markdown => {
|
||||||
const plugins = registry.getEditorComponents();
|
const plugins = registry.getEditorComponents();
|
||||||
const markdownLines = markdown.split('\n');
|
const markdownLines = markdown.split('\n');
|
||||||
@ -191,7 +275,7 @@ const remarkToSlatePlugin = () => {
|
|||||||
delete: 'strikethrough',
|
delete: 'strikethrough',
|
||||||
inlineCode: 'code',
|
inlineCode: 'code',
|
||||||
};
|
};
|
||||||
const toTextNode = text => ({ kind: 'text', text });
|
const toTextNode = (text, data) => ({ kind: 'text', text, data });
|
||||||
const wrapText = (node, index, parent) => {
|
const wrapText = (node, index, parent) => {
|
||||||
if (['text', 'html'].includes(node.type)) {
|
if (['text', 'html'].includes(node.type)) {
|
||||||
parent.children.splice(index, 1, u('paragraph', [node]));
|
parent.children.splice(index, 1, u('paragraph', [node]));
|
||||||
@ -199,11 +283,13 @@ const remarkToSlatePlugin = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let getDefinition;
|
let getDefinition;
|
||||||
const transform = node => {
|
const transform = (node, index, siblings, parent) => {
|
||||||
let nodes;
|
let nodes;
|
||||||
|
|
||||||
if (node.type === 'root') {
|
if (node.type === 'root') {
|
||||||
|
// Create definition getter for link and image references
|
||||||
getDefinition = mdastDefinitions(node);
|
getDefinition = mdastDefinitions(node);
|
||||||
|
// Ensure top level text nodes are wrapped in paragraphs
|
||||||
modifyChildren(wrapText)(node);
|
modifyChildren(wrapText)(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,8 +299,10 @@ const remarkToSlatePlugin = () => {
|
|||||||
// If a node returns a falsey value, exclude it. Some nodes do not
|
// If a node returns a falsey value, exclude it. Some nodes do not
|
||||||
// translate from MDAST to Slate, such as definitions for link/image
|
// translate from MDAST to Slate, such as definitions for link/image
|
||||||
// references or footnotes.
|
// 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) {
|
if (transformed) {
|
||||||
acc.push(transformed);
|
acc.push(transformed);
|
||||||
}
|
}
|
||||||
@ -228,7 +316,17 @@ const remarkToSlatePlugin = () => {
|
|||||||
|
|
||||||
// Process raw html as text, since it's valid markdown
|
// Process raw html as text, since it's valid markdown
|
||||||
if (['text', 'html'].includes(node.type)) {
|
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') {
|
if (node.type === 'inlineCode') {
|
||||||
@ -315,7 +413,10 @@ const remarkToSlatePlugin = () => {
|
|||||||
return { kind: 'block', type: typeMap['image'], data };
|
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 = () => {
|
const slateToRemarkPlugin = () => {
|
||||||
@ -327,9 +428,14 @@ const slateToRemarkPlugin = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const markdownToRemark = markdown => {
|
export const markdownToRemark = markdown => {
|
||||||
const result = unified()
|
const parsed = unified()
|
||||||
.use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true })
|
.use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true })
|
||||||
.parse(markdown);
|
.parse(markdown);
|
||||||
|
|
||||||
|
const result = unified()
|
||||||
|
.use(remarkShortcodes, { plugins: registry.getEditorComponents() })
|
||||||
|
.runSync(parsed);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -399,6 +505,7 @@ export const slateToRemark = raw => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
acc.push(u('html', childNode.text));
|
acc.push(u('html', childNode.text));
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
@ -408,6 +515,10 @@ export const slateToRemark = raw => {
|
|||||||
return u('root', children);
|
return u('root', children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.type === 'shortcode') {
|
||||||
|
return u('html', { data: node.data }, node.data.shortcodeValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (node.type.startsWith('heading')) {
|
if (node.type.startsWith('heading')) {
|
||||||
const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 };
|
const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 };
|
||||||
const depth = node.type.split('-')[1];
|
const depth = node.type.split('-')[1];
|
||||||
@ -448,19 +559,21 @@ export const slateToRemark = raw => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
raw.type = 'root';
|
raw.type = 'root';
|
||||||
const result = transform(raw);
|
const mdast = transform(raw);
|
||||||
|
|
||||||
|
const result = unified()
|
||||||
|
.use(remarkShortcodes, { plugins: registry.getEditorComponents() })
|
||||||
|
.runSync(mdast);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const remarkToHtml = mdast => {
|
export const remarkToHtml = mdast => {
|
||||||
const result = unified()
|
const result = unified()
|
||||||
|
.use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents() })
|
||||||
.use(remarkToRehype, { allowDangerousHTML: true })
|
.use(remarkToRehype, { allowDangerousHTML: true })
|
||||||
.use(rehypeReparse)
|
|
||||||
.use(rehypeRemoveEmpty)
|
.use(rehypeRemoveEmpty)
|
||||||
.use(rehypeMinifyWhitespace)
|
.use(rehypeMinifyWhitespace)
|
||||||
.use(() => node => {
|
|
||||||
return node;
|
|
||||||
})
|
|
||||||
.runSync(mdast);
|
.runSync(mdast);
|
||||||
|
|
||||||
const output = unified()
|
const output = unified()
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -5321,7 +5321,7 @@ mdast-util-compact@^1.0.0:
|
|||||||
unist-util-modify-children "^1.0.0"
|
unist-util-modify-children "^1.0.0"
|
||||||
unist-util-visit "^1.1.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"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.2.tgz#673f4377c3e23d3de7af7a4fe2214c0e221c5ac7"
|
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.2.tgz#673f4377c3e23d3de7af7a4fe2214c0e221c5ac7"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9010,7 +9010,13 @@ unist-util-is@^2.0.0, unist-util-is@^2.1.0:
|
|||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b"
|
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"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-1.1.1.tgz#66d7e6a449e6f67220b976ab3cb8b5ebac39e51d"
|
resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-1.1.1.tgz#66d7e6a449e6f67220b976ab3cb8b5ebac39e51d"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user