No stupid questions
No stupid questions

Reputation: 485

React: Replace multiple, different parts of a html string with JSX components

I have a description stored as html I want to render in my component. However, before it can be rendered I need to replace parts of the description with JSX components. However, unlike other questions I've seen that ask this I need to replace more than one type of thing in the description with JSX components. This requires multiple regex statements. Take the following description as an example:

<div style="white-space: pre-line;">
    This is my video.

    0:00 Intro
    4:12 Point 1
    9:12 Point 2
    14:12 Closing Point

    Check out my website at https://example.com

    #tag #tag2 #tag3
</div>

In this description all links need to be wrapped in an link element, timestamps need to be converted into a link that changes the video time and hashtags need to be converted into a link that takes the user to the search page.

This is how I formatted the description when I was using jQuery:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="description" style="white-space: pre-line;"></div>
<script>
    var description = `
This is my video.

0:00 Intro
4:12 Point 1
9:12 Point 2 
14:12 Closing Point

Check out my website at https://example.com

#tag #tag2 #tag3
    `;
    $('#description').html(createLinks(createHashtagLinks(formatTimestamps(description))));
    
    function createLinks(text) {
        return text.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>')
    }
    function createHashtagLinks(text) {
        return text.replace(/#\w*[a-zA-Z]\w*/g, '<a href="/videos?search=$&">$&</a>');
    }
    function formatTimestamps(text) {
        return text.replace(/^[0-5]?\d(?::(?:[0-5]?\d)){1,2}/gm, function (match) {
            var timeArray = match.split(':').reverse();
            var seconds = 0;
            var i = 1;
            for (let unit of timeArray) {
                seconds += unit * i;
                i *= 60;
            }
            return `<a data-seconds="${seconds}" href="#">${match}</a>`
        });
    }
</script>
Since I am using React Router, instead of replacing matches in the description html with <a> elements I need to instead replace them with <Link> components. How can I do this in React?

Upvotes: 0

Views: 2937

Answers (1)

Lionel Rowe
Lionel Rowe

Reputation: 5926

Consider the following approach:

  1. Escape any stray &<>'" characters to their respective XML entities.
  2. Wrap the relevant spans with XML tags.
  3. Parse to XML DOM.
  4. Replace XML nodes with React components and render them.

For example:

const customParse = (rawStr) => {
  const str = rawStr.replace(/[&<>'"]/g, (m) => `&#${m.codePointAt(0)};`);

  const wrapped = str
    .replace(/https?:\/\/\S+/g, (m) => `<Link>${m}</Link>`)
    .replace(/#\w*[a-zA-Z]\w*/g, (m) => `<Tag>${m}</Tag>`);

  const dom = new DOMParser().parseFromString(
    `<root>${wrapped}</root>`,
    "application/xml"
  );

  return [...dom.documentElement.childNodes];
};

const RenderedOutput = ({ text }) => (
  <pre>
    {customParse(text).map((node, idx) => {
      if (node.nodeType === Node.TEXT_NODE) {
        return <React.Fragment key={idx}>{node.data}</React.Fragment>;
      } else {
        switch (node.nodeName) {
          case "Link":
            return <Link key={idx} url={node.textContent} />;
          case "Tag":
            return <Tag key={idx} tag={node.textContent} />;
          default:
            throw new Error("not implemented");
        }
      }
    })}
  </pre>
);

CodeSandbox demo

You could implement additional custom logic as needed if you also want to create and read from attribute lists and so on.

Upvotes: 1

Related Questions