Steve W
Steve W

Reputation: 1128

Regex selects adjacent pattern matching items as single item

I'm trying to write a script that highlights everything on a webpage that matches the input to a search box whilst ignoring case in the search for matches.

My html page looks like -

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <title>Index Page</title>
    <!--<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">-->
    <!-- Bootstrap core CSS -->
    <link href="Content/bootstrap.min.css" rel="stylesheet">
    <!-- Your custom styles (optional) -->
    <link href="Content/style.css" rel="stylesheet">
    <!-- custom javascript -->
    <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.quicksearch/2.3.1/jquery.quicksearch.js"></script>
    <script src="Scripts/script.js"></script>
</head>
<body>

    <!--Main Navigation-->
    <header>
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <form class="form-inline my-2 my-lg-0">
                    <input class="form-control mr-sm-2" id="txt_query" type="search" placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
                </form>
        </nav>
    </header>
    <!--Main Navigation-->
    <div class="container-fluid">
        <div class="searchable">
            Hello
            <div>
                test1
            </div>
            <div>
                test2
            </div>
            <div>
                Test3
                <div>
                    teest4
                </div>
            </div>
        </div>
    </div>
</body>
</html>

And the javascript (JQuery) code is -

$(function () {
    $('input#txt_query').on('input', function () {
        if (this.value !== null) {
            var search_value = this.value;
            var search_regexp;
            if (this.value !== "") {
                search_regexp = new RegExp(search_value + "+(?![^<]*\>)", "gi");
            }
            $('.searchable').each(function () {
                $(this).find('*').each(function () {
                    if ($(this).data('old-state') == null) {
                        $(this).data('old-state', $(this).html());
                    }
                    if (this.value !== "") {
                        var html = $(this).data('old-state').replace(search_regexp, "<span class = 'highlight'>" + search_value + "</span>");
                        //alert(html);
                        $(this).html(html);
                    }
                });
            });
        }
    });
});

And the CSS to highlight the word is -

.highlight {
    font-weight: bold;
    color: green;
}

This currently has 2 problems.

  1. Where matching terms are adjacent, the items are treated as a single item. So when searching for 'e', the 'ee' gets replaced with a highlighted 'e'.

  2. A search for 't' finds the capital 'T' but replaces it with a highlighted 't' (small t).

I think both problems could be solved by modification of the following line of code:

var html = $(this).data('old-state').replace(
              search_regexp, 
              "<span class = 'highlight'>" + search_value + "</span>"
);

Instead of 'search_value' I'd like to use the text found by the regex pattern, but I don't know how to access that text.

Upvotes: 0

Views: 83

Answers (2)

ewwink
ewwink

Reputation: 19154

Try replace callback function to capture matches and replace with it so e become ee and t keep original case or T

var html = $(this).data('old-state').replace(search_regexp,
  function(match, contents, offset) {
    return "<span class = 'highlight'>" + match + "</span>";
  });

demo:

$(function() {
  $('input#txt_query').on('input', function() {
    if (this.value !== null) {
      var search_value = this.value;
      var search_regexp;
      if (this.value !== "") {
        search_regexp = new RegExp(search_value + "+(?![^<]*\>)", "gi");
      }
      $('.searchable').each(function() {
        $(this).find('*').each(function() {
          if ($(this).data('old-state') == null) {
            $(this).data('old-state', $(this).html());
          }
          if (this.value !== "") {
            var html = $(this).data('old-state').replace(search_regexp, function(match, contents, offset) {
              return "<span class = 'highlight'>" + match + "</span>";
            });
            //alert(html);
            $(this).html(html);
          }
        });
      });
    }
  });
});
.highlight {font-weight: bold;color: green;}
.searchable {font-size: 24px}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<input id="txt_query" type="search">
<div class="container-fluid">
  <div class="searchable">
    Hello
    <div>
      test1
    </div>
    <div>
      test2
    </div>
    <div>
      Test3
      <div>
        teest4
      </div>
    </div>
  </div>
</div>

Upvotes: 1

zer00ne
zer00ne

Reputation: 44068

Meta Sequence Word Boundary \b

Put a \b at both ends of you word and you will match only what is between them. The otherside of \b must be a non-word like a space.

Dynamic Regex

In order to use a variable in a Regex pattern use the RegExp constructor not the Regex literal.

👍 var keyword = new RegExp(variable, 'gi');

not

👎 var keyword = /variable/gi;

If you do use a variable, you need to escape it first. Note the \bs are escaped: \\b

var escaped = `(?!(?:[^<]+>|[^>]+<\\/a>))\\b(${keyword})\\b`;

I noticed a lack of template literals in the question so if you aren't familiar with them, it's worth getting to know them, they are strings with a better syntax.


Demo

This demo will highlight what it matches by wrapping them in <mark> tags.

document.getElementById('search').addEventListener('change', function(e) {
  highlight(this.value, '#content');
});

function highlight(keyword, selector) {
  var node = document.querySelector(selector);
  var html = node.innerHTML;
  var clean = html.replace(/(<mark>|<\/mark>)/, '');
  var escaped = `(?!(?:[^<]+>|[^>]+<\\/a>))\\b(${keyword})\\b`;
  var regex = new RegExp(escaped, `gi`);
  var hits = clean.replace(regex, `<mark>$1</mark>`);
  node.innerHTML = hits;
}
<input id='search' type='search'><input type='button' value='search'>

<article id='content'>
  <ol>
    <li>aircrew</li>
    <li><u>air</u> crew</li>
    <li>playground</li>
    <li><u>play</u> ground</li>
    <li>underground</li>
    <li><u>under</u> ground</li>
    <li>hacksaw</li>
    <li><u>hack</u> saw</li>
    <li>toothpaste</li>
    <li><u>tooth</u> paste</li>
  </ol>

</article>

Upvotes: 0

Related Questions