dopry/netlify-cms

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

Summary

Maintainability
A
1 hr
Test Coverage
import { concat, last, nth, isEmpty, set } from 'lodash';
import visitParents from 'unist-util-visit-parents';

/**
 * remarkUnwrapInvalidNest
 *
 * Some MDAST node types can only be nested within specific node types - for
 * example, a paragraph can't be nested within another paragraph, and a heading
 * can't be nested in a "strong" type node. This kind of invalid MDAST can be
 * generated by rehype-remark from invalid HTML.
 *
 * This plugin finds instances of invalid nesting, and unwraps the invalidly
 * nested nodes as far up the parental line as necessary, splitting parent nodes
 * along the way. The resulting node has no invalidly nested nodes, and all
 * validly nested nodes retain their ancestry. Nodes that are emptied as a
 * result of unnesting nodes are removed from the tree.
 */
export default function remarkUnwrapInvalidNest() {
  return transform;

  function transform(tree) {
    const invalidNest = findInvalidNest(tree);

    if (!invalidNest) return tree;

    splitTreeAtNest(tree, invalidNest);

    return transform(tree);
  }

  /**
   * visitParents uses unist-util-visit-parent to check every node in the
   * tree while having access to every ancestor of the node. This is ideal
   * for determining whether a block node has an ancestor that should not
   * contain a block node. Note that it operates in a mutable fashion.
   */
  function findInvalidNest(tree) {
    /**
     * Node types that are considered "blocks".
     */
    const blocks = ['paragraph', 'heading', 'code', 'blockquote', 'list', 'table', 'thematicBreak'];

    /**
     * Node types that can contain "block" nodes as direct children. We check
     */
    const canContainBlocks = ['root', 'blockquote', 'listItem', 'tableCell'];

    let invalidNest;

    visitParents(tree, (node, parents) => {
      const parentType = !isEmpty(parents) && last(parents).type;
      const isInvalidNest = blocks.includes(node.type) && !canContainBlocks.includes(parentType);

      if (isInvalidNest) {
        invalidNest = concat(parents, node);
        return false;
      }
    });

    return invalidNest;
  }

  function splitTreeAtNest(tree, nest) {
    const grandparent = nth(nest, -3) || tree;
    const parent = nth(nest, -2);
    const node = last(nest);

    const splitIndex = grandparent.children.indexOf(parent);
    const splitChildren = grandparent.children;
    const splitChildIndex = parent.children.indexOf(node);

    const childrenBefore = parent.children.slice(0, splitChildIndex);
    const childrenAfter = parent.children.slice(splitChildIndex + 1);
    const nodeBefore = !isEmpty(childrenBefore) && { ...parent, children: childrenBefore };
    const nodeAfter = !isEmpty(childrenAfter) && { ...parent, children: childrenAfter };

    const childrenToInsert = [nodeBefore, node, nodeAfter].filter(val => !isEmpty(val));
    const beforeChildren = splitChildren.slice(0, splitIndex);
    const afterChildren = splitChildren.slice(splitIndex + 1);
    const newChildren = concat(beforeChildren, childrenToInsert, afterChildren);
    grandparent.children = newChildren;
  }
}