Negative lookbehind alternative in JavaScript
Piotr Sobuś
Posted on December 14, 2021
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.
Posted on December 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.