huridocs/uwazi

View on GitHub
app/react/Markdown/markdownToReact.js

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import React from 'react';

import HtmlToReact, { Parser } from 'html-to-react';
import instanceMarkdownIt from 'markdown-it';
import mdContainer from 'markdown-it-container';
import * as CustomComponents from './components';

const components = Object.keys(CustomComponents).reduce(
  (map, key) => ({ ...map, [key.toLowerCase()]: CustomComponents[key] }),
  {}
);
const availableComponents = Object.keys(components);

const myParser = new Parser({ xmlMode: true });
const processNodeDefinitions = new HtmlToReact.ProcessNodeDefinitions(React);
const customComponentMatcher = '{\\w+}\\(.+\\)\\(.+\\)|{\\w+}\\(.+\\)';

const dynamicCustomContainersConfig = {
  validate() {
    return true;
  },
  render(tokens, idx) {
    const token = tokens[idx];

    if (token.type === 'container_dynamic_open') {
      return `<div class="${token.info.trim()}">`;
    }
    return '</div>';
  },
};

const markdownIt = instanceMarkdownIt({ xhtmlOut: true }).use(
  mdContainer,
  'dynamic',
  dynamicCustomContainersConfig
);
const markdownItWithHtml = instanceMarkdownIt({ html: true, xhtmlOut: true }).use(
  mdContainer,
  'dynamic',
  dynamicCustomContainersConfig
);
const customComponentTypeMatcher = /{(.+)}\(/;

const getConfig = string => {
  const customComponentOptionsMatcher = /{\w+}(\(.+\)\(.+\))|{\w+}(\(.+\))/g;
  let config;
  const configMatch = customComponentOptionsMatcher.exec(string);
  if (configMatch) {
    config = configMatch[1] || configMatch[2];
  }

  return config;
};

const removeWhitespacesInsideTableTags = html =>
  html
    .replace(
      /((\/)?(table|thead|tbody|tr|th|td)>)[\s\n]+(<(\/)?(table|thead|tbody|tr|th|td))/g,
      '$1$4'
    )
    .replace(
      /((\/)?(table|thead|tbody|tr|th|td)>)[\s\n]+(<(\/)?(table|thead|tbody|tr|th|td))/g,
      '$1$4'
    );

const getNodeTypeAndConfig = (_config, node, isCustomComponentPlaceholder, isCustomComponent) => {
  let type;
  let config = _config;

  if (isCustomComponentPlaceholder) {
    [, type] = node.children[0].data.match(customComponentTypeMatcher);
    config = getConfig(node.children[0].data);
  }

  if (isCustomComponent) {
    [, type] = node.data.match(customComponentTypeMatcher);
    config = getConfig(node.data);
  }

  type = availableComponents.includes(node.name ? node.name.toLowerCase() : '')
    ? components[node.name.toLowerCase()]
    : type;

  return { type, config };
};

export default (_markdown, callback, withHtml = false) => {
  let renderer = markdownIt;
  if (withHtml) {
    renderer = markdownItWithHtml;
  }

  const markdown = _markdown.replace(new RegExp(`(${customComponentMatcher})`, 'g'), '$1\n');

  const html = removeWhitespacesInsideTableTags(
    renderer
      .render(markdown)
      .replace(
        new RegExp(`<p>(${customComponentMatcher})</p>`, 'g'),
        '<placeholder>$1</placeholder>'
      )
  );

  const isValidNode = node => {
    const isBadNode = node.type === 'tag' && node.name.match(/<|>/g);
    if (isBadNode) {
      return false;
    }
    return true;
  };

  const processingInstructions = [
    {
      shouldProcessNode() {
        return true;
      },

      processNode: (node, children, index) => {
        if (
          node.name &&
          (node.name.toLowerCase() === 'dataset' || node.name.toLowerCase() === 'query')
        ) {
          return false;
        }
        const isCustomComponentPlaceholder =
          node.name === 'placeholder' &&
          node.children &&
          node.children[0] &&
          node.children[0].data &&
          node.children[0].data.match(customComponentMatcher);

        const isCustomComponent =
          node &&
          (!node.parent || (node.parent && node.parent.name !== 'placeholder')) &&
          node.data &&
          node.data.match(customComponentMatcher);

        const { type, config } = getNodeTypeAndConfig(
          node.attribs,
          node,
          isCustomComponentPlaceholder,
          isCustomComponent
        );

        const newNode = callback(type, config, index, children);

        return newNode || processNodeDefinitions.processDefaultNode(node, children, index);
      },
    },
  ];

  return myParser.parseWithInstructions(html, isValidNode, processingInstructions);
};