Import source: app/backend/src/lib/link-engine
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Autolink glossary terms in the provided HTML content using cheerio.
|
||||
* Safely traverses text nodes and wraps matching terms in anchor tags,
|
||||
* avoiding already linked text, code blocks, and headings.
|
||||
*/
|
||||
export async function autolinkGlossaryTerms(htmlContent: string): Promise<string> {
|
||||
if (!htmlContent) return htmlContent;
|
||||
|
||||
const glossaryTerms = await prisma.article.findMany({
|
||||
where: { type: 'glossary', status: 'منتشرشده' },
|
||||
select: { title: true, slug: true },
|
||||
});
|
||||
|
||||
if (glossaryTerms.length === 0) return htmlContent;
|
||||
|
||||
// Sort by length descending to match longest phrases first
|
||||
const sortedTerms = glossaryTerms.sort((a, b) => b.title.length - a.title.length);
|
||||
|
||||
const $ = cheerio.load(htmlContent, null, false);
|
||||
|
||||
// Exclude elements where we shouldn't inject links
|
||||
const excludedTags = ['a', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'script', 'style'];
|
||||
|
||||
// Find all text nodes that are not inside excluded tags
|
||||
const textNodes: any[] = [];
|
||||
$('*').contents().each(function() {
|
||||
if (this.type === 'text') {
|
||||
let parent = $(this).parent();
|
||||
let shouldExclude = false;
|
||||
while (parent.length > 0 && parent[0].name) {
|
||||
if (excludedTags.includes(parent[0].name.toLowerCase())) {
|
||||
shouldExclude = true;
|
||||
break;
|
||||
}
|
||||
parent = parent.parent();
|
||||
}
|
||||
if (!shouldExclude) {
|
||||
textNodes.push(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const textNode of textNodes) {
|
||||
let nodeText = $(textNode).text();
|
||||
let replaced = false;
|
||||
|
||||
for (const term of sortedTerms) {
|
||||
const escapedTerm = term.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
// Using word boundaries; since Farsi spaces/zwnj might be tricky, we can use a simpler regex
|
||||
// that matches the phrase separated by whitespace or punctuation.
|
||||
const regex = new RegExp(`(?<=^|[\\s،؛.!?()»«])(${escapedTerm})(?=[\\s،؛.!?()»«]|$)`, 'gi');
|
||||
|
||||
if (regex.test(nodeText)) {
|
||||
nodeText = nodeText.replace(regex, `<a href="/glossary/${term.slug}" class="glossary-link" title="${term.title}">$1</a>`);
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (replaced) {
|
||||
$(textNode).replaceWith(nodeText);
|
||||
}
|
||||
}
|
||||
|
||||
return $.html();
|
||||
}
|
||||
Reference in New Issue
Block a user