ReactViews/Custom/parseCustomHtmlToReact.js

"use strict";

const React = require("react");
const HtmlToReact = require("html-to-react");
const combine = require("terriajs-cesium/Source/Core/combine").default;
const defined = require("terriajs-cesium/Source/Core/defined").default;
const utils = require("html-to-react/lib/utils");

const CustomComponents = require("./CustomComponents");

const htmlToReactParser = new HtmlToReact.Parser({
  decodeEntities: true
});
const processNodeDefinitions = new HtmlToReact.ProcessNodeDefinitions(React);

const isValidNode = function() {
  return true;
};

const shouldProcessEveryNodeExceptWhiteSpace = function(node) {
  // Use this to avoid white space between table elements, eg.
  //     <table> <tbody> <tr>\n<td>x</td> <td>3</td> </tr> </tbody> </table>
  // being rendered as empty <span> elements, and causing React errors.
  return node.type !== "text" || node.data.trim();
};

let keyIndex = 0;

/**
 * @private
 */
function getProcessingInstructions(context) {
  /**
   * @private
   */
  function boundProcessor(processor) {
    return {
      shouldProcessNode: processor.shouldProcessNode,
      processNode: processor.processNode.bind(null, context)
    };
  }

  // Process custom nodes specially.
  let processingInstructions = [];
  const customComponents = CustomComponents.values();
  for (let i = 0; i < customComponents.length; i++) {
    const customComponent = customComponents[i];
    processingInstructions.push({
      shouldProcessNode: node => node.name === customComponent.name,
      processNode: customComponent.processNode.bind(null, context)
    });
    const processors = customComponent.furtherProcessing;
    if (defined(processors)) {
      processingInstructions = processingInstructions.concat(
        processors.map(boundProcessor)
      );
    }
  }

  // Make sure any <a href> tags open in a new window
  processingInstructions.push({
    shouldProcessNode: node => node.name === "a",
    processNode: function(node, children, index) {
      // eslint-disable-line react/display-name
      const elementProps = {
        key: "anchor-" + keyIndex++,
        target: "_blank",
        rel: "noreferrer noopener"
      };
      node.attribs = combine(node.attribs, elementProps);
      return utils.createElement(node, index, node.data, children);
    }
  });

  // Process all other nodes as normal.
  processingInstructions.push({
    shouldProcessNode: shouldProcessEveryNodeExceptWhiteSpace,
    processNode: processNodeDefinitions.processDefaultNode
  });
  return processingInstructions;
}

/**
 * Return html as a React Element.
 * @param  {String} html
 * @param  {Object} [context] Provide any further information that custom components need to know here, eg. which feature and catalogItem they come from.
 * @return {ReactElement}
 */
function parseCustomHtmlToReact(html, context) {
  if (!defined(html) || html.length === 0) {
    return html;
  }
  return htmlToReactParser.parseWithInstructions(
    html,
    isValidNode,
    getProcessingInstructions(context || {})
  );
}

module.exports = parseCustomHtmlToReact;