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>.

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>.

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>'
  );
}

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;
  });
}

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

15