From 3e70008b81a05781bff578328b4e96e2387278ab Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 7 Feb 2026 07:02:24 +0300 Subject: [PATCH] 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 , , , - Add prose styles for underline, strikethrough, and spoiler tags - Light theme variants included --- src/pages/Info.tsx | 40 ++++++++++++++++++++++++---------------- src/styles/globals.css | 22 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/pages/Info.tsx b/src/pages/Info.tsx index 8f65c01..b6717fd 100644 --- a/src/pages/Info.tsx +++ b/src/pages/Info.tsx @@ -93,39 +93,47 @@ const sanitizeHtml = (html: string): string => { 'blockquote', 'code', 'pre', + 's', + 'del', + 'ins', 'span', 'div', + 'tg-spoiler', ], ALLOWED_ATTR: ['href', 'target', 'rel', 'class'], 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 => { if (!content) return ''; - // Check if content already has HTML tags - const hasHtmlTags = /<[a-z][\s\S]*>/i.test(content); + // Check if content has block-level HTML (full HTML document) + const hasBlockHtml = /<(p|div|h[1-6]|ul|ol|blockquote)\b/i.test(content); - if (hasHtmlTags) { + if (hasBlockHtml) { return sanitizeHtml(content); } - // Convert plain text to formatted HTML + // Content may have inline Telegram HTML (, , , , ) but uses + // newlines for structure. Convert newlines to paragraphs while preserving inline tags. const result = content - .split(/\n\n+/) // Split by double newlines (paragraphs) + .split(/\n\n+/) .map((paragraph) => { - // Check if it's a header (starts with # or numeric like "1.") - if (/^#{1,4}\s/.test(paragraph)) { - const level = paragraph.match(/^(#{1,4})/)?.[1].length || 1; - const text = paragraph.replace(/^#{1,4}\s*/, ''); + const trimmed = paragraph.trim(); + if (!trimmed) return ''; + + // 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 `${text}`; } // Check for list items - if (/^[-•]\s/.test(paragraph) || /^\d+[.)]\s/.test(paragraph)) { - const lines = paragraph.split('\n'); + if (/^[-•]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) { + const lines = trimmed.split('\n'); const isOrdered = /^\d+[.)]\s/.test(lines[0]); const listItems = lines .map((line) => line.replace(/^[-•]\s*/, '').replace(/^\d+[.)]\s*/, '')) @@ -135,11 +143,11 @@ const formatContent = (content: string): string => { return isOrdered ? `
    ${listItems}
` : `
    ${listItems}
`; } - // Regular paragraph - handle single line breaks within - const formattedParagraph = paragraph.split('\n').join('
'); - - return `

${formattedParagraph}

`; + // Regular paragraph — single newlines become
+ const formatted = trimmed.split('\n').join('
'); + return `

${formatted}

`; }) + .filter(Boolean) .join(''); return sanitizeHtml(result); diff --git a/src/styles/globals.css b/src/styles/globals.css index 94fc357..3778807 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -853,6 +853,19 @@ img.twemoji { @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 { @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; } + .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 { @apply border-champagne-400 bg-champagne-100/50 text-champagne-700; }