Search code examples
javascriptregexdommarkdowntextnode

How to query text-nodes from DOM, find markdown-patterns, replace matches with HTML-markup and replace the original text-node with the new content?


Markdown-like functionality for tooltips

Problem:

Using Vanilla JavaScript I want to:

Change this:

<div>
   <p>
        Hello [world]{big round planet we live on}, how is it [going]{verb that means walking}? 
   </p>
   <p>
        It is [fine]{a word that expresses gratitude}.
   </p>
</div> 

To this:

<div>
    <p>
        Hello <mark data-toggle="tooltip" data-placement="top" title="big round planet we live on">world</mark>, how is it <mark data-toggle="tooltip" data-placement="top" title="verb means walking">world</mark>?
    </p>
    <p>
        It is fine <mark data-toggle="tooltip" data-placement="top" title="a word that expresses gratitude">thanks</mark>.
    </p>
</div> 

so it looks visually like this:

enter image description here

is somehow similar to "markdown" edit functionalities.

Solution:

  1. Mark the strings to replace in a different way:
<p>It is fine *[thanks]{a word that expresses gratitude}*!</p>
  1. Initiate Bootstrap and tooltip functionality.
  2. Grab all paragraphs
var p = document.getElementsByTagName('p')
  1. Apply REGEX
tooltip = original.match(/(\{)(.*?)(\})/gi)[0].slice(1, -1);
hint = original.match(/(\[)(.*?)(\])/gi)[0].slice(1, -1);
  1. Change their inside-text
replaced = original.replace(/(\*)(.*?)(\*)/gi, 
        `<mark data-toggle="tooltip" data-placement="top" title="${tooltip}">${hint}</mark>`);
elem.innerHTML = replaced;
  1. Alltogether in one function:
[].forEach.call(p, elem => {
    let original = elem.innerHTML;
    let replaced, tooltip, hint
    tooltip = original.match(/(\{)(.*?)(\})/gi)[0].slice(1, -1);
    hint = original.match(/(\[)(.*?)(\])/gi)[0].slice(1, -1);
    replaced = original.replace(/(\*)(.*?)(\*)/gi, 
        `<mark data-toggle="tooltip" data-placement="top" title="${tooltip}">${hint}</mark>`);
      elem.innerHTML = replaced;
});

but I fail

Miserable when there is more paragraphs or when I just want to do it in an easy way with 2 pair of brackets instead of additional asterix. Fails also hen the innerTEXT has more phrases / words that should have the tooltip. Any ideas? Do you have any suggestions? Existing ways of doing it? Libraries? Scripts?


Solution

  • One very easily can stumble at coming up with the right approach of how to replace a text node with other unknown HTML content.

    A generic solution takes into account a more complex HTML content.

    Thus, starting from a source-node, one stepwise needs to insert each of its child-nodes (either text- or element-nodes) before the targeted text-node. Once all nodes got inserted, one finally removes the targeted text-node.

    Regarding the regex and the markup template, one can create the markup-string within a single replace call from a single regex and a single template string both making use of Capturing Groups.

    // text node detection helper
    function isNonEmptyTextNode(node) {
      return (
           (node.nodeType === 3)
        && (node.nodeValue.trim() !== '')
        && (node.parentNode.tagName.toLowerCase() !== 'script')
      );
    }
    
    // text node reducer functionality
    function collectNonEmptyTextNode(list, node) {
      if (isNonEmptyTextNode(node)) {
        list.push(node);
      }
      return list;
    }
    function collectTextNodeList(list, elmNode) {
      return Array.from(
        elmNode.childNodes
      ).reduce(
        collectNonEmptyTextNode,
        list
      );
    }
    
    // final dom render function
    function replaceTargetNodeWithSourceNodeContent(targetNode, sourceNode) {
      const parentNode = targetNode.parentNode;
    
      Array.from(sourceNode.childNodes).forEach(function (node) {
        parentNode.insertBefore(node, targetNode);
      });
      parentNode.removeChild(targetNode);    
    }
    
    // template and dom fragment render function
    function findMarkdownCreateMarkupAndReplaceTextNode(node) {
      const regX = (/\[([^\]]+)\]\{([^\}]+)\}/g);
      const text = node.nodeValue;
    
      if (regX.test(text)) {
        const template = '<mark data-toggle="tooltip" data-placement="top" title="$2">$1</mark>'
    
        const renderNode = document.createElement('div');
        renderNode.innerHTML = text.replace(regX, template);
    
        replaceTargetNodeWithSourceNodeContent(node, renderNode);
      }
    }
    
    
    const elementNodeList = Array.from(document.body.getElementsByTagName('*'));
    
    const textNodeList = elementNodeList.reduce(collectTextNodeList, []);
    
    textNodeList.forEach(findMarkdownCreateMarkupAndReplaceTextNode);
    .as-console-wrapper { min-height: 100%!important; top: 0; }
    <div>
      <p>
        <span>Hello [world]{big round planet we live on}, how is it [going]{verb that means walking}?</span>
        <span>Hello [world]{big round planet we live on}, how is it [going]{verb that means walking}?</span>
      </p>
      <p>
        <span>It is [fine]{a word that expresses gratitude}.</span>
        It is [fine]{a word that expresses gratitude}.
        <span>It is [fine]{a word that expresses gratitude}.</span>
      </p>
    </div>
    <!--
    
      // does get rerendered into:
    
      <div>
        <p>
          <span>
            Hello
            <mark data-toggle="tooltip" data-placement="top" title="big round planet we live on">
              world
            </mark>
            , how is it
            <mark data-toggle="tooltip" data-placement="top" title="verb that means walking">
              going
            </mark>
            ?
          </span>
          <span>
            Hello
            <mark data-toggle="tooltip" data-placement="top" title="big round planet we live on">
              world
            </mark>
            , how is it
            <mark data-toggle="tooltip" data-placement="top" title="verb that means walking">
              going
            </mark>
            ?
          </span>
        </p>
        <p>
          <span>
            It is
            <mark data-toggle="tooltip" data-placement="top" title="a word that expresses gratitude">
              fine
            </mark>
            .
          </span>
          It is
          <mark data-toggle="tooltip" data-placement="top" title="a word that expresses gratitude">
            fine
          </mark>
          .
          <span>
            It is
            <mark data-toggle="tooltip" data-placement="top" title="a word that expresses gratitude">
              fine
            </mark>
            .
          </span>
        </p>
      </div>
    
    //-->