diff --git a/app/routes/questions/$question.tsx b/app/routes/questions/$question.tsx index 672fa044..f42d6890 100644 --- a/app/routes/questions/$question.tsx +++ b/app/routes/questions/$question.tsx @@ -258,24 +258,31 @@ function Contents({pageid, html, glossary}: {pageid: PageId; html: string; gloss * Replace all known glossary words in the given `textNode` with: * - a span to mark it as a glossary item * - an on hover popup with a short explaination of the glossary item + * - use each glossary item only once */ + const unusedGlossaryEntries = Object.values(glossary) + .filter((item) => item.pageid != pageid) + .map(({term}) => term) + .sort((a, b) => b.length - a.length) + .map( + (term) => + [ + new RegExp(`(^|[^\\w-])(${term})($|[^\\w-])`, 'i'), + '$1$2$3', + ] as const + ) const insertGlossary = (textNode: Node) => { const html = (textNode.textContent || '').replace('’', "'").replace('—', '-') // The glossary items have to be injected somewhere, so this does it by manually wrapping any known // definitions with spans. This is done from the longest to the shortest to make sure that sub strings // of longer definitions don't override them. - const updated = Object.values(glossary) - .filter((item) => item.pageid != pageid) - .map(({term}) => term) - .sort((a, b) => b.length - a.length) - .reduce( - (html, entry) => - html.replace( - new RegExp(`(^|[^\\w-])(${entry})($|[^\\w-])`, 'gi'), - '$1$2$3' - ), - html - ) + const updated = unusedGlossaryEntries.reduce((html, [match, replacement], index) => { + if (html.match(match)) { + unusedGlossaryEntries.splice(index, 1) + return html.replace(match, replacement) + } + return html + }, html) if (updated == html) { return textNode }