Home > other >  Search and put a sub HTML text inside a newly created span, preserving the HTML structure as much as
Search and put a sub HTML text inside a newly created span, preserving the HTML structure as much as

Time:01-24

Consider the following example HTML which I need to highlight a search text 192.168.1.1. This is just an example, the text search and HTML structure are totally arbitrary because it will be run on end-user websites with their inputs. The program will also perform additional operations on the inserted span, not just highlighting (for example displaying a tooltip when hovering but this is not an issue as long as I have a span inserted).

EDIT: I just found out a new line would result in space in between which textContent won't be correct anymore. However I will keep them there for better visual. You can check the snippet below for correct HTML code

<div>
    <span >The computer address</span>
    <em>is</em>
    <span >192</span>.
    <span >168</span>.
    <span >1</span>.
    <span >1</span>
</div>

Using textContent or innerText, it's easy to find the closest element with the subtext I need (the div in the example has the full 192.168.1.1 text). However the next step I want to do wrap those parts without breaking the original structure or other HTML/CSS class or properties. Ideally, I'd like to have the final HTML to be:

<div>
    <span >The computer address</span>
    <em>is</em>
    <span ><span >192</span>.
    <span >168</span>.
    <span >1</span>.
    <span >1</span></span>
</div>

Using deep-first search I can easily get to the div, but then I have no idea how to proceed from there. There's also this tricky case:

<div>
    <span >The computer address</span>
    <em>is
    <span >192</span>.</em>
    <span >168</span>.
    <span >1</span>.
    <span >1</span>
</div>

I think if there is a solution, this should be the easiest way to achieve it:

<div>
    <span >The computer address</span>
    <em>is
    <span ><span >192</span>.</span></em><span >
    <span >168</span>.
    <span >1</span>.
    <span >1</span></span>
</div>

The code will be executed in Chrome extension environment if it's relevance but I believe this should be a pure browser Javascript problem.


Here's a snippet so you can try out:

function transform(el, text) {
  // Transform this Element to highlight text
}

const text = "192.168.1.1";
transform(document.querySelector("#case-1"), text);
transform(document.querySelector("#case-2"), text);
transform(document.querySelector("#case-simple"), text);
.my-highlight-span {
  background-color: cornflowerblue;
}
<p>Case 1:</p>
<div id="case-1">
  <span >The computer address</span>
  <em>is</em>
  <span >192</span>.<span >168</span>.<span >1</span>.<span >1</span>
</div>

<p>Case 2:</p>
<div id="case-2">
  <span >The computer address</span>
  <em>is
        <span >192</span>.</em><span >168</span>.<span >1</span>.<span >1</span>
</div>

<p>Simple case:</p>

<div id="case-simple">
  The computer address is 192.168.1.1
</div>

<hr />

<p>Desired Result:</p>

<div id="case-1-result">
  <span >The computer address</span>
  <em>is</em>
  <span ><span >192</span>.<span >168</span>.<span >1</span>.<span >1</span></span>
</div>
<div id="case-2-result">
  <span >The computer address</span>
  <em>is
        <span ><span >192</span>.</span></em><span >
        <span >168</span>.<span >1</span>.<span >1</span></span>
</div>

<div id="case-simple-result">
  The computer address is <span >192.168.1.1</span>
</div>

CodePudding user response:

I am finally get the answer using Range API, credit to awesome explanation here and a subtle workaround of Range.surroundContents().

  • Using scanForText method I recursively scan for textContent of TEXT_NODEs to determine the start and end of a search term.

  • I then use Range API to select and extract the content and insert it back after surrounding it with my span.

There is some limitation obviously but this should work for most cases. For example, in case 2, my code would generate an extra empty <span > inside <em>.

function scanForText(el, expectingStart, expectingEnd, result) {
  if (el.nodeType === Node.TEXT_NODE) {
    const nodeContent = el.textContent;

    result[4]  = nodeContent;
    const currText = result[4];

    if (expectingStart < currText.length && !result[0]) {
      result[0] = el;
      result[1] = expectingStart - (currText.length - nodeContent.length);
    }

    if (expectingEnd <= currText.length && !result[2]) {
      result[2] = el;
      result[3] = expectingEnd - (currText.length - nodeContent.length);

      return;
    }
  } else {
    for (let childEl of el.childNodes) {
      scanForText(childEl, expectingStart, expectingEnd, result);

      if (result[2]) { // When already found ending, return
        return;
      }
    }

  }
}

function transform(el, text) {
  const fullText = el.textContent;
  const startIndex = fullText.indexOf(text);
  const endIndex = startIndex   text.length;

  const scanState = [null, -1, null, -1, ""];
  scanForText(el, startIndex, endIndex, scanState);

  const [startNode, startRangeIndex, endNode, endRangeIndex] = scanState;
  if (!startNode || !endNode) {
    console.warn("This should not be happening");
    return;
  }

  const range = new Range();
  range.setStart(startNode, startRangeIndex);
  range.setEnd(endNode, endRangeIndex);

  const surrounding = document.createElement("span");
  surrounding.className = "my-highlight-span";

  // This can't be used when cutting one text boundary.
  // Alternative is offered at https://developer.mozilla.org/en-US/docs/Web/API/Range/surroundContents
  // range.surroundContents(surrounding)

  const extracted = range.extractContents();
  surrounding.appendChild(extracted);
  range.insertNode(surrounding);
}

const text = "192.168.1.1";
transform(document.querySelector("#case-1"), text);
transform(document.querySelector("#case-2"), text);
transform(document.querySelector("#case-simple"), text);
.my-highlight-span {
  background-color: cornflowerblue;
}
<p>Case 1:</p>
<div id="case-1">
  <span >The computer address</span>
  <em>is</em>
  <span >192</span>.<span >168</span>.<span >1</span>.<span >1</span>
</div>

<p>Case 2:</p>
<div id="case-2">
  <span >The computer address</span>
  <em>is
        <span >192</span>.</em><span >168</span>.<span >1</span>.<span >1</span>
</div>

<p>Simple case:</p>

<div id="case-simple">
  The computer address is 192.168.1.1
</div>

<hr />

<p>Desired Result:</p>

<div id="case-1-result">
  <span >The computer address</span>
  <em>is</em>
  <span ><span >192</span>.<span >168</span>.<span >1</span>.<span >1</span></span>
</div>
<div id="case-2-result">
  <span >The computer address</span>
  <em>is
        <span ><span >192</span>.</span></em><span >
        <span >168</span>.<span >1</span>.<span >1</span></span>
</div>

<div id="case-simple-result">
  The computer address is <span >192.168.1.1</span>
</div>

  •  Tags:  
  • Related