DEV Community

Piotr Sobuś
Piotr Sobuś

Posted on

Negative lookbehind alternative in JavaScript

Task description

One of my recent assignment was to create a function that traverses through a parsed HTML text, finds placeholders that meet a certain condition and wraps them with specific HTML tags. The condition was if a placeholder is wrapped around mark + span tag, ignore it, otherwise wrap it with those tags.

Example:

My name is {{ first_name }}. I am <b>{{ age }}</b> years old and I love <mark><span>{{ interest }}</span></mark>.
Enter fullscreen mode Exit fullscreen mode

The function should find 2 occurrences and replace them. The last placeholder should be ignored.

My name is <mark><span>{{ first_name }}</span></mark>. I am <b><mark><span>{{ age }}</span></mark></b> years old and I love <mark><span>{{ interest }}</span></mark>.
Enter fullscreen mode Exit fullscreen mode

First solution

In my first solution I used a negative lookbehind assertion (?<!) that basically tries to find expression A where expression B does not precede. In simpler words - match every placeholder that does not start with a span tag.

markPlaceholders(html: string): string {
  return html.replace(
    /w*(?<!<span>){{([a-z0-9_]*)}}/g,
    '<mark><span>{{$1}}</span></mark>'
  );
}
Enter fullscreen mode Exit fullscreen mode

Easy.

The problem

Unfortunately, when I tried to open the application on Safari, it crashed with a following message:

SyntaxError: Invalid regular expression: invalid group specifier name

Turns out that Safari does not support negative lookbehind assertions. What a shame.

Final solution

The workaround for this problem was to pass a function as the second parameter in the replace method. This so called "replacer" will check if the placeholder starts with the mark and span tag. If it does not, we create a new element and replace it with the matched placeholder. Otherwise, we return what we have, because it already contains those tags. The function will be invoked after the match has been performed.

markPlaceholders(html: string): string {
  return html.replace(/{{[a-z0-9_]*}}/g, (match, _, idx) => {
    const hasTagsBefore =
      html.substring(idx - '<mark><span>'.length, idx) === '<mark><span>';

    if (!hasTagsBefore) {
      return `<mark><span>${match}</span></mark>`;
    }

    return match;
  });
}
Enter fullscreen mode Exit fullscreen mode

I hope this example will help you if you are struggling with the same problem. Feel free to ask questions.

Oldest comments (2)

Collapse
 
jamesthomson profile image
James Thomson

Turns out that Safari does not support negative lookbehind assertions.

Ah, of course. SafarIE strikes again.

Collapse
 
shakilmansuridream profile image
shakil-mansuri-dream • Edited

I have a long html string & I want to highlight some words in it but not html tag itself. So this regex is to ignore html tags. e.g. I have html string <mark>mark this as Read</mark>. In this HTML, I want to highlight "mark" word into inner html but not "mark" between < & >. using: /\b(?<!<[^>]*)test(?<![^>]*<)\b/ regex