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:
arsam
2026-09-18 23:19:22 +03:30
parent 1bd2d027ec
commit 229101fca1
+70
View File
@@ -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();
}