RegExp Javascript per applicare tag span su stringhe con sottostringhe nidificate

Sep 11 2020

Stringa di esempio:

'There is a red car parked in front of a blue house with a fence painted red.'

Le stringhe da evidenziare con gli span sono:

['red car', 'blue house', 'red'].

Stringa prevista:

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

Tuttavia, quando eseguo una sostituzione iterando sull'array, finisco con i tag span nidificati.

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

Codice:

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

Qualche suggerimento su come evitare di ricodificare le stringhe tramite RegEx o qualsiasi altro metodo?

EDIT: ogni stringa deve essere evidenziata con classi di stile differenti. Modifica dell'array strToMatch in un array di oggetti che contengono il nome della classe CSS da applicare.

Risposte

4 WiktorStribiżew Sep 11 2020 at 21:59

È necessario ordinare in base alla stringsToMatchlunghezza in ordine decrescente e utilizzare un singolo modello basato sull'alternanza (con eventuali confini di parole per far corrispondere solo parole intere) per assicurarsi che la sostituzione venga eseguita in una volta:

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

Produzione:

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

Qui,

  • stringsToMatch.sort((a, b) => b.length - a.length);ordina le stringhe nell'array in base alla lunghezza in ordine decrescente. Scopri perché è importante in Remember That The Regex Engine Is Eager
  • new RegExp(String.raw`\b(?:${stringsToMatch.join('|')})\b`,'g')crea un oggetto RegExp, con il \b(?:blue house|red car|red)\bpattern (guarda la sua demo )
  • .replace(regEx, "<span class='highlight_text'>$&</span>")sostituisce le corrispondenze con se stesse racchiuse tra spantag ( $&è un riferimento all'intero valore della corrispondenza).
1 Mr.Polywhirl Sep 11 2020 at 22:13

Potresti sostituire (ridurre) tutte le parole corrispondenti con flag posizionali e quindi sostituire tutti quei flag con versioni incartate.

Questo può essere utilizzato in Node JS, poiché c'è il rilevamento del browser tramite typeof window !== 'undefined'durante il controllo di un 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>


Estensibilità

Se vuoi un mix di testo ed espressioni regolari nel tuo "elenco di parole", puoi modificare la funzione sopra per memorizzare nella cache ogni corrispondenza. In questo esempio, stai effettuando le sostituzioni tramite la cache, piuttosto che tramite l'elenco di parole in ordine inverso.

/**
 * @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>