2017-07-31 12:58:45 -04:00
|
|
|
import { map, every } from 'lodash';
|
|
|
|
import u from 'unist-builder';
|
|
|
|
import mdastToString from 'mdast-util-to-string';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse shortcodes from an MDAST.
|
|
|
|
*
|
|
|
|
* Shortcodes are plain text, and must be the lone content of a paragraph. The
|
|
|
|
* paragraph must also be a direct child of the root node. When a shortcode is
|
|
|
|
* found, we just need to add data to the node so the shortcode can be
|
|
|
|
* identified and processed when serializing to a new format. The paragraph
|
|
|
|
* containing the node is also recreated to ensure normalization.
|
|
|
|
*/
|
|
|
|
export default function remarkShortcodes({ plugins }) {
|
|
|
|
return transform;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map over children of the root node and convert any found shortcode nodes.
|
|
|
|
*/
|
|
|
|
function transform(root) {
|
|
|
|
const transformedChildren = map(root.children, processShortcodes);
|
|
|
|
return { ...root, children: transformedChildren };
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mapping function to transform nodes that contain shortcodes.
|
|
|
|
*/
|
|
|
|
function processShortcodes(node) {
|
|
|
|
/**
|
|
|
|
* If the node is not eligible to contain a shortcode, return the original
|
|
|
|
* node unchanged.
|
|
|
|
*/
|
|
|
|
if (!nodeMayContainShortcode(node)) return node;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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).trim();
|
|
|
|
const { plugin, match } = matchTextToPlugin(text);
|
|
|
|
const matchIsValid = validateMatch(text, match);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If a valid match is found, return a new node with shortcode data
|
|
|
|
* included. Otherwise, return the original node.
|
|
|
|
*/
|
|
|
|
return matchIsValid ? createShortcodeNode(text, plugin, match) : node;
|
2018-07-27 09:13:52 -06:00
|
|
|
}
|
2017-07-31 12:58:45 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensure that the node and it's children are acceptable types to contain
|
|
|
|
* shortcodes. Currently, only a paragraph containing text and/or html nodes
|
|
|
|
* may contain shortcodes.
|
|
|
|
*/
|
|
|
|
function nodeMayContainShortcode(node) {
|
|
|
|
const validNodeTypes = ['paragraph'];
|
|
|
|
const validChildTypes = ['text', 'html'];
|
|
|
|
|
|
|
|
if (validNodeTypes.includes(node.type)) {
|
|
|
|
return every(node.children, child => {
|
|
|
|
return validChildTypes.includes(child.type);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the plugin and RegExp.match result from the first plugin with a
|
|
|
|
* pattern that matches the given text.
|
|
|
|
*/
|
|
|
|
function matchTextToPlugin(text) {
|
|
|
|
let match;
|
|
|
|
const plugin = plugins.find(p => {
|
|
|
|
match = text.match(p.pattern);
|
|
|
|
return !!match;
|
|
|
|
});
|
|
|
|
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
|
|
|
|
* Remark or Rehype. Include the child as an array because an MDAST paragraph
|
|
|
|
* node must have it's children in an array.
|
|
|
|
*/
|
|
|
|
function createShortcodeNode(text, plugin, match) {
|
|
|
|
const shortcode = plugin.id;
|
|
|
|
const shortcodeData = plugin.fromBlock(match);
|
|
|
|
const data = { shortcode, shortcodeData };
|
|
|
|
const textNode = u('html', text);
|
|
|
|
return u('paragraph', { data }, [textNode]);
|
|
|
|
}
|
|
|
|
}
|