6

I have some HTML in my DOM and I want to replace some strings in it, but only if that was not already replaced or that is not a TAG.

All that is based on an Array that contains the string I want to find and the new string I want this to be replace with.

Work in progress: https://jsfiddle.net/u2Lyaab1/23/

UPDATE: The HTML markup is just for simplicity written with ULs in the sample code, BUT it can contain different tags, event different nesting levels

Basically the desiredReplcement works nice (except that it looks in tags too), but I want that to happen on the DOM, not the new string because I want to maintain any other HTML markup in the DOM.

SNIPPET:

var list = [{
    original: 'This is',
    new: 'New this is'
  },
  {
    original: 'A list',
    new: 'New A list'
  },
  {
    original: 'And I want',
    new: 'New And I want'
  },
  {
    original: 'To wrap',
    new: 'New To wrap'
  },
  {
    original: 'li',
    new: 'bold'
  },
  {
    original: 'This',
    new: 'New This'
  },
  {
    original: 'strong',
    new: 'bold'
  },  {
original: 'This is another random tag',
new: 'This is another random tag that should be bold'
  }

];


var div = $('.wrap');
var htmlString = div.html();
var index = 0;
list.forEach(function(item, index) {

  console.log(index + ' Should replace: "' + item.original + '" with "' + item.new + '"');

  //I know that there is something here, but not sure what
  index = htmlString.indexOf(item.original);
  var expressionLength = index + item.original.length;
  var substring = htmlString.substring(index, expressionLength);
  var desiredReplcement = substring.replace(item.original, '<strong>' + item.new + '</strong>');
  console.log('index', index);
  console.log('substring', substring);
  console.log('desiredReplcement', desiredReplcement);

  //Current implementation in replace looks in the full div, but I just want to replace in the substring mathced above;
  var replacement = '<strong>' + item.new + '</strong>';
  var newHTML = div.html().replace(item.original, replacement);
  div.html(newHTML);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrap">
  <ul>
    <li>This is</li>
    <li>A list</li>
    <li>And I want</li>
    <li>This should not be bold</li>
    <li>To wrap</li>
    <li>This</li>
    <li>strong</li>
    <li>li</li>
  </ul>
<span><p><em>This is another random tag</em></p></span>
</div>
Adrian Florescu
  • 4,454
  • 8
  • 50
  • 74
  • https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags – epascarello Sep 18 '17 at 15:12
  • 1
    An alternative by converting the list into a Map: https://jsfiddle.net/u2Lyaab1/12/ – Me.Name Sep 18 '17 at 17:20
  • I updated my question. The answer must be applicable for all sort of HTML content. – Adrian Florescu Sep 18 '17 at 18:02
  • 1
    So, you want to check the entire DOM for these strings? What happens if there are multiples, or if one of those stings happen to make up part of other words? What are the target use cases, and what is the ultimate goal you are looking to achieve? – IzzyCooper Sep 25 '17 at 05:31

4 Answers4

3

Your div variable is referencing <div class="wrap">...</div>, therefore your htmlString value is a group of html tags instead of string.

That is the main reason your code is not working as expected.

And therefore I rewrote your implementation.

var list = [
  {
    original: 'This is',
    new: 'New this is'
  },
  {
    original: 'A list',
    new: 'New A list'
  },
  {
    original: 'And I want',
    new: 'New And I want'
  },
  {
    original: 'To wrap',
    new: 'New To wrap'
  },
  {
    original: 'li',
    new: 'bold'
  },
  {
    original: 'This',
    new: 'New This'
  },
  {
    original: 'strong',
    new: 'bold'
  }
  
];

var div = document.getElementsByClassName('wrap')[0].getElementsByTagName('li');  // Getting all <li> elements within <div class="wrap">

Array.prototype.forEach.call(div, function(li, x){  // Borrowing Array's forEach method to be used on HTMLCollection

    list.forEach(function(value, i){                // Looping through list
        if (value.original === li.innerHTML)        // if list[i]['original'] === li[x].innerHTML
            li.innerHTML = '<strong>' + value.new + '</strong>';
            
    });
    
    
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrap">
  <ul>
     <li>This is</li>
     <li>A list</li>
     <li>And I want</li>
     <li>This should not be bold</li>
     <li>To wrap</li>
     <li>This</li>
     <li>strong</li>
     <li>li</li>
  </ul>
</div>
yqlim
  • 6,898
  • 3
  • 19
  • 43
  • You can improve performance here by having your map be `var list = {'This is':'New this is', ...}` - ie. a "search=>replace" map. That way you don't need the inner loop, just get `list[li.innerHTML]` (or, better, `li.textContent`) – Niet the Dark Absol Sep 18 '17 at 15:53
  • @YongQuan. Thank you forr your answer! The DOM might contain any TAG.... so your solution is good just for this particular case, any thoungs if I had some random tags instead of list items? – Adrian Florescu Sep 18 '17 at 16:27
  • 1
    @AdrianFlorescu sure, you can get some idea from [here](https://www.w3schools.com/jsref/prop_node_childnodes.asp) and [here](https://www.w3schools.com/jsref/prop_element_children.asp) – yqlim Sep 18 '17 at 16:35
  • Wow, you are a clever man! :D, but what if there is no chidren, and it's directly text? – Adrian Florescu Sep 18 '17 at 16:36
  • @AdrianFlorescu you can find any kinds of contents using the `childNode` property from the first link. To filter or find certain type of nodes, refer [here](https://www.w3schools.com/jsref/prop_node_nodetype.asp). For element, `nodeType === 1`. For text, `nodeType === 3`. – yqlim Sep 19 '17 at 01:24
1

The following code will not replace tags and will do only one replacement for one text node (if there is any match). It looks through the whole structure in a recursive manner and checks the text of the elements.(and it uses the same list you described in your question)

Requirements:

  1. Replace text just in case of exact match => use === instead of indexOf

  2. Replace text only once => remove item from list after use

    var div = $('.wrap');
    
    function substitute(htmlElement, substituteStrings){
      var childrenElements = htmlElement.children;
      if(childrenElements.length !== 0){
        for (let i=0;i<childrenElements.length;i++){
            substitute(childrenElements[i], substituteStrings);
        }
      } else {
        var htmlString = htmlElement.innerText;
        substituteStrings.some(function(item){
            if(htmlString == item.original){
                htmlElement.innerHTML = htmlString.replace(item.original, '<strong>' + item.new + '</strong>');
                substituteStrings.splice(index,1);
                return true;
            }
        });
      }
    }
    substitute(div[0],list);
    
Zsolt V
  • 517
  • 3
  • 8
  • Thank you for your answer, it works almost perfect: https://jsfiddle.net/gLz7w5ax/ but as you can see, the last sentence is replaced differently than the expected in the list – Adrian Florescu Sep 23 '17 at 09:37
  • That's true. Could you define the desired behavior when multiple (maybe partial) matching occur? – Zsolt V Sep 23 '17 at 12:33
  • Basically the replacement needs to happen only one time and if it's not an exact match do not add the wrapper tags replacements – Adrian Florescu Sep 23 '17 at 14:39
1

I don't think that jQuery is necessary here.

First, you want to retrieve your container, which in your case will be the .wrap div.

var container = document.querySelector('.wrap');

Then you want to create a recursive function that will loop through an array to search and replace the data provided.

function replacement(containers, data){

    if(!data || !data.length)
        return;

    for(let i=0; i<containers.length; i++){

        var container = containers[i];

        // Trigger the recursion on the childrens of the current container
        if(container.children.length)
            replacement(container.children, data);

        // Perform the replacement on the actual container
        for(let j=0; j<data.length; j++){
            var index = container.textContent.indexOf(data[j].original);

            // Data not found
            if(index === -1)
                continue;

            // Remove the data from the list
            var replace = data.splice(j, 1)[0];
            container.innerHTML = container.innerHTML.replace(replace.original, '<strong>' + replace.new + '</strong>');

            // Lower the j by 1 since the data array length has been updated
            j--;

            // Only want to perform one rule
            break;

        }
    }
}

Demo: https://jsfiddle.net/u2Lyaab1/25/

Chin Leung
  • 14,621
  • 3
  • 34
  • 58
  • Very nice! One thing I noticed is that the one sentence "This should not be bold" is't been made bold in the first part of the string – Adrian Florescu Sep 27 '17 at 18:18
  • @AdrianFlorescu What do you mean? In the fiddle I see "New This should not be bold" and the "New This" is in bold. – Chin Leung Sep 27 '17 at 18:38
  • yes, the that full sentance should not be replaced at all – Adrian Florescu Sep 27 '17 at 18:39
  • Also, in the result panel, I noticed a string that reads: "New A boldst" – Adrian Florescu Sep 27 '17 at 18:49
  • @AdrianFlorescu Hmm... why should that sentence not be replaced tho? Isn't it a simple element like all the other li? Also, the "New a boldst" is because it replaced the "li" from the "list". Because one of the object is original set to `li` and new set to `bold`. So the `li` of `list` becomes `boldst`. – Chin Leung Sep 27 '17 at 18:56
  • You can remove the "
  • This should not be bold
  • " from the initial DOM, but the "boldst" should not be there -- the script should replace the original strings in the DOM with the new ones from the object in JS – Adrian Florescu Sep 27 '17 at 18:59
  • @AdrianFlorescu That's what it's doing. It replaced the first line with "New a list", then replaced the "li" from the "list" to "bold". Do you want the rules to not overwrite each other? – Chin Leung Sep 27 '17 at 19:02
  • Yes - I mentioned in my question: "but only if that was not already replaced or that is not a TAG" -- not sure if that was clear enough in my question. – Adrian Florescu Sep 27 '17 at 19:05
  • @AdrianFlorescu Alright, I've updated my answer to not have rules overwriting each other. – Chin Leung Sep 27 '17 at 19:10
  • You are awesome! Thank you very much for your time! It's perfect! – Adrian Florescu Sep 27 '17 at 19:18