Javascript RegExp para aplicar tags span em strings com substrings aninhados
String de exemplo:
'There is a red car parked in front of a blue house with a fence painted red.'
As strings que devem ser destacadas com extensões são:
['red car', 'blue house', 'red'].
String esperada:
There is a <span class='redHighlight'>red car</span> parked in front of a <span class='blueHighlight'>blue house</span> with a fence painted <span class='redHighlight'>red</span>.
No entanto, quando faço uma substituição ao iterar o array, acabo com tags de span aninhadas.
> "There is a <span class='<span class='redHighlight'>red</span>Highlight'><span class='redHighlight'>red</span> car</span> parked in front of a <span class='blueHighlight'>blue house</span> with a fence painted <span class='redHighlight'>red</span>.
Código:
let strToHighlight = 'There is a red car parked in front of a blue house with a fence painted red.';
let stringsToMatch = [{'strVal': 'red car',
'cssClass': 'redHighlight'},
{'strVal': 'blue house',
'cssClass': 'blueHighlight'},
{'strVal': 'red',
'cssClass':'redHighlight'}
];
stringsToMatch.forEach(el => {
let regEx = new RegExp(el.strVal,'g') // replace all occurances
strToHighlight = strToHighlight.replace(regEx, `<span class='${el.cssClass}'>${el.strVal}</span>}`);
console.log(strToHighlight);
})
Alguma sugestão sobre como evitar a reclassificação de strings via RegEx ou qualquer outro método?
EDITAR: Cada string deve ser destacada com diferentes classes de estilo. Editando o array strToMatch para um array de objetos contendo o nome da classe CSS a ser aplicada.
Respostas
Você precisa classificar o stringsToMatchpor comprimento em ordem decrescente e usar um único padrão baseado em alternância (com limites de palavra eventuais para corresponder apenas a palavras inteiras) para garantir que a substituição seja realizada de uma vez:
let strToHighlight = 'There is a red car parked in front of a blue house with a fence painted red.';
let stringsToMatch = [{'strVal': 'red car', 'cssClass': 'redHighlight'},
{'strVal': 'blue house', 'cssClass': 'blueHighlight'},
{'strVal': 'red','cssClass':'redHighlight'}
];
const searchTerms = stringsToMatch.map(x => x.strVal);
searchTerms.sort((a, b) => b.length - a.length);
let regEx = new RegExp(String.raw`\b(?:${searchTerms.join('|')})\b`,'g'); // => /\b(?:blue house|red car|red)\b/g strToHighlight = strToHighlight.replace(regEx, (m) => `<span class='${stringsToMatch.find(x => x.strVal == m).cssClass}'>${m}</span>`);
console.log(strToHighlight);
Resultado:
There is a <span class='redHighlight'>red car</span> parked in front of a <span class='blueHighlight'>blue house</span> with a fence painted <span class='redHighlight'>red</span>.
Aqui,
stringsToMatch.sort((a, b) => b.length - a.length);classifica as strings na matriz por comprimento na ordem decrescente. Veja por que isso é importante em Lembre-se de que o motor Regex está ansiosonew RegExp(String.raw`\b(?:${stringsToMatch.join('|')})\b`,'g')cria um objeto RegExp, com o\b(?:blue house|red car|red)\bpadrão (veja sua demonstração ).replace(regEx, "<span class='highlight_text'>$&</span>")substitui correspondências com eles próprios entrespantags ($&é uma referência anterior para todo o valor de correspondência).
Você pode substituir (reduzir) todas as palavras correspondentes por sinalizadores de posição e, em seguida, substituir todos esses sinalizadores por versões agrupadas.
Isso pode ser usado no Node JS, uma vez que há detecção do navegador por meio da typeof window !== 'undefined'verificação de um elemento.
/**
* @param {String|Node} strOrElement - String or element (browser only)
* @param {String[]} words - List of words to wrap
* @param {function} replacerFn - Replacer function for each word
*/
const wrapWords = (strOrElement, words, replacerFn) => {
const isEl = typeof window !== 'undefined' && strOrElement instanceof Node;
if (!isEl && typeof strOrElement !== 'string') {
throw new Error('must be text or an element');
}
const sorted = words.slice().sort().reverse();
const text = isEl ? strOrElement.textContent : strOrElement;
const result = sorted
.reduce((curr, word, idx) => curr.replace(word, `$\{${idx}\}`), text)
.replace(/\$\{(\d+)\}/g, (m, p1) => replacerFn(sorted[parseInt(p1, 10)])); if (isEl) strOrElement.innerHTML = result; return result; }; const words = [ 'red car', 'blue house', 'red' ]; const el = document.querySelector('p') const fn = word => `<span class="highlight_text">${word}</span>`;
wrapWords(el, words, fn);
.highlight_text {
background: #FFA;
border: thin dashed red;
padding: 0.125em 0.25em;
}
<p>There is a red car parked in front of a blue house with a fence painted red.</p>
Extensibilidade
Se quiser uma mistura de texto e expressões regulares em sua "lista de palavras", você pode modificar a função acima para armazenar em cache cada correspondência. Neste exemplo, você está fazendo substituições por meio do cache, em vez da lista de palavras classificada reversamente.
/**
* @param {String|Node} strOrElement - String or element (browser only)
* @param {String[]} words - List of words to wrap
* @param {function} replacerFn - Replacer function for each word
*/
const wrapWords = (strOrElement, words, replacerFn) => {
const isEl = typeof window !== 'undefined' && strOrElement instanceof Node;
if (!isEl && typeof strOrElement !== 'string') {
throw new Error('must be text or an element');
}
const text = isEl ? strOrElement.textContent : strOrElement;
const cache = [];
const result = words.slice().sort().reverse()
.reduce((curr, word, idx) => curr.replace(word, (m) => {
cache[idx] = [ ...(cache[idx] || []), m ];
return `$\{${idx}\}`;
}), text)
.replace(/\$\{(\d+)\}/g, (m, p1) => { return replacerFn(cache[parseInt(p1, 10)].pop()); }); if (isEl) strOrElement.innerHTML = result; return result; }; const wl1 = [ 'red car', 'blue house', 'red' ]; const el1 = document.querySelector('p:nth-child(1)') wrapWords(el1, wl1, word => `<span class="highlight_text">${word}</span>`);
const wl2 = [ /\$\d+.\d+/g ]; const el2 = document.querySelector('p:nth-child(2)') wrapWords(el2, wl2, word => `<span class="highlight_text">${word}</span>`);
.highlight_text {
background: #FFA;
border: thin dashed red;
padding: 0.125em 0.25em;
}
<p>There is a red car parked in front of a blue house with a fence painted red.</p>
<p>The price of the watch was reduced from $500.00 down to $199.99.</p>