dopry/netlify-cms

View on GitHub
src/components/Widgets/Markdown/serializers/remarkShortcodes.js

Summary

Maintainability
A
35 mins
Test Coverage
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;
  };

  /**
   * 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]);
  }
}