DEV Community

Piotr Sobuś
Piotr Sobuś

Posted on

4 2

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.

SurveyJS custom survey software

Simplify data collection in your JS app with a fully integrated form management platform. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more. Integrates with any backend system, giving you full control over your data and no user limits.

Learn more

Top comments (2)

Collapse
 
shakilmansuridream profile image

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

Collapse
 
jamesthomson profile image
James Thomson

Turns out that Safari does not support negative lookbehind assertions.

Ah, of course. SafarIE strikes again.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay