feat: support Telegram HTML formatting in privacy/offer content

- Fix formatContent to handle mixed content (inline HTML + newlines)
- Only skip paragraph conversion for content with block-level HTML
- Add sanitizer support for <s>, <del>, <ins>, <tg-spoiler>
- Add prose styles for underline, strikethrough, and spoiler tags
- Light theme variants included
This commit is contained in:
Fringg
2026-02-07 07:02:24 +03:00
parent f36ee60c0b
commit 3e70008b81
2 changed files with 46 additions and 16 deletions

View File

@@ -93,39 +93,47 @@ const sanitizeHtml = (html: string): string => {
'blockquote', 'blockquote',
'code', 'code',
'pre', 'pre',
's',
'del',
'ins',
'span', 'span',
'div', 'div',
'tg-spoiler',
], ],
ALLOWED_ATTR: ['href', 'target', 'rel', 'class'], ALLOWED_ATTR: ['href', 'target', 'rel', 'class'],
ALLOW_DATA_ATTR: false, ALLOW_DATA_ATTR: false,
}); });
}; };
// Convert plain text to HTML with proper formatting // Convert content to formatted HTML (handles Telegram HTML + plain text)
const formatContent = (content: string): string => { const formatContent = (content: string): string => {
if (!content) return ''; if (!content) return '';
// Check if content already has HTML tags // Check if content has block-level HTML (full HTML document)
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(content); const hasBlockHtml = /<(p|div|h[1-6]|ul|ol|blockquote)\b/i.test(content);
if (hasHtmlTags) { if (hasBlockHtml) {
return sanitizeHtml(content); return sanitizeHtml(content);
} }
// Convert plain text to formatted HTML // Content may have inline Telegram HTML (<b>, <i>, <u>, <code>, <a>) but uses
// newlines for structure. Convert newlines to paragraphs while preserving inline tags.
const result = content const result = content
.split(/\n\n+/) // Split by double newlines (paragraphs) .split(/\n\n+/)
.map((paragraph) => { .map((paragraph) => {
// Check if it's a header (starts with # or numeric like "1.") const trimmed = paragraph.trim();
if (/^#{1,4}\s/.test(paragraph)) { if (!trimmed) return '';
const level = paragraph.match(/^(#{1,4})/)?.[1].length || 1;
const text = paragraph.replace(/^#{1,4}\s*/, ''); // Check if it's a markdown header
if (/^#{1,4}\s/.test(trimmed)) {
const level = trimmed.match(/^(#{1,4})/)?.[1].length || 1;
const text = trimmed.replace(/^#{1,4}\s*/, '');
return `<h${level}>${text}</h${level}>`; return `<h${level}>${text}</h${level}>`;
} }
// Check for list items // Check for list items
if (/^[-•]\s/.test(paragraph) || /^\d+[.)]\s/.test(paragraph)) { if (/^[-•]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) {
const lines = paragraph.split('\n'); const lines = trimmed.split('\n');
const isOrdered = /^\d+[.)]\s/.test(lines[0]); const isOrdered = /^\d+[.)]\s/.test(lines[0]);
const listItems = lines const listItems = lines
.map((line) => line.replace(/^[-•]\s*/, '').replace(/^\d+[.)]\s*/, '')) .map((line) => line.replace(/^[-•]\s*/, '').replace(/^\d+[.)]\s*/, ''))
@@ -135,11 +143,11 @@ const formatContent = (content: string): string => {
return isOrdered ? `<ol>${listItems}</ol>` : `<ul>${listItems}</ul>`; return isOrdered ? `<ol>${listItems}</ol>` : `<ul>${listItems}</ul>`;
} }
// Regular paragraph - handle single line breaks within // Regular paragraph single newlines become <br/>
const formattedParagraph = paragraph.split('\n').join('<br/>'); const formatted = trimmed.split('\n').join('<br/>');
return `<p>${formatted}</p>`;
return `<p>${formattedParagraph}</p>`;
}) })
.filter(Boolean)
.join(''); .join('');
return sanitizeHtml(result); return sanitizeHtml(result);

View File

@@ -853,6 +853,19 @@ img.twemoji {
@apply italic text-dark-300; @apply italic text-dark-300;
} }
.prose u {
@apply underline underline-offset-2;
}
.prose s,
.prose del {
@apply text-dark-400 line-through;
}
.prose tg-spoiler {
@apply cursor-pointer rounded bg-dark-600 px-1 text-transparent transition-colors hover:bg-transparent hover:text-dark-200;
}
.prose blockquote { .prose blockquote {
@apply my-4 rounded-r-lg border-l-4 border-accent-500/50 bg-dark-800/30 py-2 pl-4 italic text-dark-300; @apply my-4 rounded-r-lg border-l-4 border-accent-500/50 bg-dark-800/30 py-2 pl-4 italic text-dark-300;
} }
@@ -929,6 +942,15 @@ img.twemoji {
@apply text-champagne-700; @apply text-champagne-700;
} }
.light .prose s,
.light .prose del {
@apply text-champagne-500;
}
.light .prose tg-spoiler {
@apply bg-champagne-300 text-transparent hover:bg-transparent hover:text-champagne-800;
}
.light .prose blockquote { .light .prose blockquote {
@apply border-champagne-400 bg-champagne-100/50 text-champagne-700; @apply border-champagne-400 bg-champagne-100/50 text-champagne-700;
} }