mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
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:
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user