Replace words in html with links, but prefer longer words if search term collide

Viewed 48

I am having trouble with the following task:

I need to enrich a string containing html by linking some of the words it contains to "read more" pages. Unfortunately when the search terms I want to replace begin to overlap it becomes tricky. I either end up with nested links or, like in the example below, with longer terms not being replaced.

let html = `<p>So there is this Text in HTML containing words.</p>
<p>I want to link some special words to their read more pages.</p>
<p>But if search terms contain each other they re not linked correctly. Like the ones above.</p>`;

const linkTo = [
    {term: 'words', link: 'https://example.com/words'},
    {term: 'special words', link: 'https://example.com/special_words'},
];

for(const pair of linkTo){
    const re = new RegExp(pair.term, "g");
    html = html.replace(re, `<a href="${pair.link}">${pair.term}</a>`);
}

document.body.innerHTML = html;
<html><body></body></html>

In the example above the "special words" in line 2 should link to its own page.

Any Ideas?

Thanks in advance!

2 Answers

You need to sort the keys by length in the descending order and create a single regex from the terms:

let html = `<p>So there is this Text in HTML containing words.</p>
<p>I want to link some special words to their read more pages.</p>
<p>But if search terms contain each other they re not linked correctly. Like the ones above.</p>`;

const linkTo = [
    {term: 'words', link: 'https://example.com/words'},
    {term: 'special words', link: 'https://example.com/special_words'},
];
const keys = linkTo.map(x => x.term).sort((a, b) => b.length-a.length)
const re = new RegExp("\\b(?:" + keys.join("|") + ")\\b", "g");
html = html.replace(re, (m) => `<a href="${linkTo.find(x => m == x.term).link}">${m}</a>`);
document.body.innerHTML = html;
<html><body></body></html>

Note I am not escaping special chars for the time being since all your terms consist of word chars, and that is why I add \b on both ends, word boundaries.

In case you need to search for terms that may contain special chars you will need to use

const keys = linkTo.map(x => x.term.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')).sort((a, b) => b.length-a.length)
const re = new RegExp("(?<!\\w)(?:" + keys.join("|") + ")(?!\\w)", "g");

Some more details:

  • linkTo.map(x => x.term).sort((a, b) => b.length-a.length) - gets the terms from the linkTo items and sorts them by length in descending order, the longer come first. It is necessary because The Regex Engine Is Eager and the first alternative "wins" and make the regex engine stop processing other alternatives
  • new RegExp("\\b(?:" + keys.join("|") + ")\\b", "g") build a regex like \b(?:special words|words)\b
  • .replace(re, (m) => `<a href="${linkTo.find(x => m == x.term).link}">${m}</a>`) - parses a string only once, and replaces each match with the corresponding link.

Special chars supporting solution:

let html = `<p>So there is this Text in HTML containing words.</p>
<p>I want to link some special words to their read more pages.</p>
<p>But if search terms contain each other they re not linked correctly. Like the ones above.</p>`;

const linkTo = [
    {term: 'words', link: 'https://example.com/words'},
    {term: 'special words', link: 'https://example.com/special_words'},
];
const keys = linkTo.map(x => x.term.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')).sort((a, b) => b.length-a.length)
const re = new RegExp("(\\W|^)(" + keys.join("|") + ")(?!\\w)", "g");
html = html.replace(re, (x,y,z) => `${y}<a href="${linkTo.find(i => z == i.term).link}">${x}</a>`);
document.body.innerHTML = html;

You can just use the string.replace() function:

let html = `<p>So there is this Text in HTML containing words.</p>
<p>I want to link some special words to their read more pages.</p>
<p>But if search terms contain each other they re not linked correctly. Like the ones above.</p>`;

const linkTo = [
    {term: 'words', link: 'https://example.com/words'},
    {term: 'special words', link: 'https://example.com/special_words'},
];

for(const pair of linkTo){
   html = html.replace(pair.term, `<a href="${pair.link}">${pair.term}</a>`);
}

document.body.innerHTML = html;
<html><body></body></html>

Related