import sanitizeHtml from 'sanitize-html'; import { SITE_URL } from '../consts'; /** Sanitize и нормализация HTML тела поста для RSS ``. */ export function sanitizeForRss(html: string): string { const absolutized = html.replace(/(src|href)=["']\/uploads\//g, `$1="${SITE_URL}/uploads/`); return sanitizeHtml(absolutized, { allowedTags: [ 'p', 'br', 'strong', 'em', 'b', 'i', 'u', 's', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'img', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'span', 'hr', ], allowedAttributes: { a: ['href', 'title', 'rel', 'target'], img: ['src', 'alt', 'title', 'width', 'height'], div: ['align'], span: ['align'], p: ['align'], td: ['colspan', 'rowspan'], th: ['colspan', 'rowspan'], }, transformTags: { a: (tagName, attribs) => ({ tagName, attribs: { ...attribs, target: '_blank', rel: 'noopener noreferrer', }, }), }, allowedSchemes: ['http', 'https', 'mailto'], }); } /** Экранировать строку для XML-атрибутов и текстовых нод. */ export function xmlEscape(s: string): string { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** Завернуть в CDATA, нейтрализовав `]]>` внутри. */ export function cdata(s: string): string { return `/g, ']]]]>')}]]>`; } /** Текстовое описание из HTML (для ``). */ export function plainTextExcerpt(html: string, max = 500): string { const txt = html .replace(//gi, ' ') .replace(//gi, ' ') .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/\s+/g, ' ') .trim(); if (txt.length <= max) return txt; return txt.slice(0, max).replace(/\s+\S*$/, '') + '…'; } /** Построить полный RSS 2.0 XML с CDATA-завёрнутым content:encoded. */ export function buildRss(opts: { title: string; description: string; selfUrl: string; language: string; items: Array<{ title: string; link: string; pubDate: Date; description: string; contentHtml: string; author: string; categories: string[]; }>; }): string { const { title, description, selfUrl, language, items } = opts; const lastBuild = new Date().toUTCString(); const itemsXml = items .map((it) => ` ${cdata(it.title)} ${xmlEscape(it.link)} ${xmlEscape(it.link)} ${it.pubDate.toUTCString()} ${cdata(it.author)} ${it.categories.map((c) => ` ${cdata(c)}`).join('\n')} ${cdata(it.description)} ${cdata(it.contentHtml)} `) .join('\n'); return ` ${cdata(title)} ${xmlEscape(SITE_URL + '/')} ${cdata(description)} ${language} ${lastBuild} 60 ${itemsXml} `; }