3

I need to highlight multiple words in a statement in HTML.

Ex: words: ['Hello', 'world', 'my']  
Statement: 'Hello, welcome to my beautiful world'

I'm able to do it using regex find and replace in normal cases (like replacing the occurrence of each word by some markup like

<span  class="highlight-span"> word </span>.    

But the problem is if any words like spa, span, class, lass, high which are part of my replace markup string I will run into issues.)

Any ideas on doing this in a better way?

Below is the code for your reference.

import {
    Pipe,
    PipeTransform
} from '@angular/core';
@Pipe({
    name: 'highlight'
})
export class HighlightSearch implements PipeTransform {
    transform(value: string, args: string): any {
        if (args && value) {
            let startIndex = value.toLowerCase().indexOf(args.toLowerCase());
            if (startIndex != -1) {
                let endLength = args.length;
                let matchingString = value.substr(startIndex, endLength);
                return value.replace(matchingString, "<span  class="highlight-span">" + matchingString + "</span>");
            }

        }
        return value;
    }
}

In this case, I cannot have my search words like mar, mark. How can I get rid of such problems?

Mahesh Bongani
  • 680
  • 7
  • 20
  • Can you create a StackBlitz for the same? I'm not able to replicate your issue. It seems to work fine even when I use mar, mark as the search string. – nash11 Sep 06 '19 at 04:25
  • That's a sample code for handling a single search string. If I send multiple strings and repeat with a loop, in case my second string is 'mar' by the time will be in the statement! Hope you understood it now. @nash11 – Mahesh Bongani Sep 06 '19 at 04:59

2 Answers2

3

Here is a much simpler way to do this using RegExp

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
    name: 'highlight'
})
export class HighlightPipe implements PipeTransform {

    transform(value: string, args: string[] | string): string {
        if (!args.length) { return value; }
        const pattern = Array.isArray(args) ? args.filter(arg => !!arg).join('|') : args;
        const regex = new RegExp(pattern.concat('|<[^>]*>'), 'gi');
        return value.replace(regex, (match) => /<[^>]*>/g.test(match) ? match: `<mark>${match}</mark>`);
    }

}

I have made the pipe such that you can highlight using both arrays or a single string if needed. <[^>]*> is the RegEx used for matching HTML tags.

In case you want to search with case sensitivity, simply remove the i when creating the RegExp like below

const regex = new RegExp(pattern.concat('|<[^>]*>'), 'g');

Then in your template, use the pipe like below

<span [innerHTML]="sentence | highlight: highlightWords"></span>

where sentence and highlightWords are

sentence: string = 'Hello, welcome to my beautiful world';
highlightWords: string[] = ['world', 'my'];

Update: It was brought to my notice that the pipe doesn't work when metacharacters are used. To fix this, use regex to escape the metacharacters as shown here.

const pattern = Array.isArray(args) ? args.filter(arg => !!arg).map(this.escapeRegex).join('|') : this.escapeRegex(args);

where the function escapeRegex is defined as

escapeRegex(word: string) {
    return word.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}

Here is a working example on StackBlitz.

nash11
  • 8,220
  • 3
  • 19
  • 55
  • It works partially because if the text has already a mark tag this fails. i.e sentence: string = 'Hello, welcome to mark my beautiful world'; – SGalea Sep 06 '19 at 14:35
  • Thank you @nash11, That works well for replacing it with tags. Is there a way to replace it with something like ` word `. I wasn't clear on my sample code. I have updated it now. – Mahesh Bongani Sep 08 '19 at 10:37
  • You can just replace `${match}` with `${match}` – nash11 Sep 08 '19 at 10:57
  • Yeah, just now verified it. Working like a charm. Awesome dude. Tons of Thanks. – Mahesh Bongani Sep 08 '19 at 11:16
  • Genious, I was strugling for a while with the RegExp and you just gave a nice solution, thank you! – Olek Feb 27 '23 at 12:10
1

A quick and crude way

import {
    Pipe,
    PipeTransform
} from '@angular/core';
import {
    DomSanitizer
} from '@angular/platform-browser'

    @Pipe({
        name: 'highlight'
    })
    export class HighlightSearch implements PipeTransform {

        constructor(private sanitizer: DomSanitizer){}

        transform(value: string, args: string): any {
            if (args && value) {
                let startIndex = value.toLowerCase().indexOf(args.toLowerCase());
                if (startIndex != -1) {
                    let endLength = args.length;
                    let matchingString = value.substr(startIndex, endLength);
                    let temp = value.replace("<mark>", "START_PLACEHOLDER");
                    temp = temp.replace("</mark>", "END_PLACEHOLDER");
                    temp = temp.replace(matchingString, "<mark>" + matchingString + "</mark>");
                    temp = temp.replace( "START_PLACEHOLDER","<mark>");
                    temp = temp.replace( "END_PLACEHOLDER","</mark>");
                    return this.sanitizer.bypassSecurityTrustHtml(temp)
                }

            }
            return value;
        }
    }

Also use Dom Sanitizer when you are inserting html like this

SGalea
  • 712
  • 9
  • 18