$1' }
];
// Preprocess text to convert BBCode and Markdown to HTML
function preprocessFormatting(text: string): string {
let processedText = text;
// Apply simple BBCode patterns first (bold, italic, underline, strikethrough)
BB_CODE_SIMPLE_PATTERNS.forEach(({ pattern, replacement }) => {
processedText = processedText.replace(pattern, replacement);
});
// Apply BBCode patterns that require validation (color, size, font)
processedText = processBBCodeWithValidation(processedText);
// Apply Markdown patterns (but avoid conflicts with already processed BBCode)
MARKDOWN_PATTERNS.forEach(({ pattern, replacement }) => {
// Only apply if not already inside HTML tags
processedText = processedText.replace(pattern, (match, content) => {
// Simple check to avoid replacing inside existing HTML tags
const beforeMatch = processedText.substring(0, processedText.indexOf(match));
const openTags = (beforeMatch.match(/<[^/][^>]*>/g) || []).length;
const closeTags = (beforeMatch.match(/<\/[^>]*>/g) || []).length;
// If we're inside an unclosed tag, don't apply markdown
if (openTags > closeTags) {
return match;
}
return replacement.replace('$1', content);
});
});
// Note: Mention highlighting is applied after sanitization in formatText
// to ensure it's not stripped by the sanitizer
return processedText;
}
// Main text formatting function
export async function formatText(
text: string,
userStyle?: Partialtags if it's just a single paragraph if (formattedHtml.startsWith('
') && formattedHtml.endsWith('
')) { const pCount = (formattedHtml.match(//g) || []).length; if (pCount === 1) { formattedHtml = formattedHtml.slice(3, -4).trim(); } } // Final check - if we ended up with empty content, return escaped text if (!formattedHtml) { return escapeHtml(text); } // Apply mention highlighting after sanitization formattedHtml = highlightMentionsHtml(formattedHtml); return formattedHtml; } catch (error) { console.error('Text formatting error:', error); // Return escaped plain text as fallback return escapeHtml(text); } } // Utility function to escape HTML (isomorphic - works in SSR and browser) export function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Generate CSS style string excluding font-family (which should be handled by classes) function generateCSSStyleWithoutFont(style: TextStyle): string { const styles: string[] = []; // Font size styles.push(`font-size: ${style.fontSize}px`); // Color (not for gradients) if (style.color && !style.gradient) { styles.push(`color: ${style.color}`); } // Font weight if (style.bold) { styles.push(`font-weight: bold`); } // Font style if (style.italic) { styles.push(`font-style: italic`); } // Text decoration const decorations: string[] = []; if (style.underline) decorations.push('underline'); if (style.strikethrough) decorations.push('line-through'); if (decorations.length > 0) { styles.push(`text-decoration: ${decorations.join(' ')}`); } return styles.join('; '); } // Create gradient text effect export function createGradientText( text: string, colors: string[], className: string = 'gradient-text', style?: TextStyle ): string { if (colors.length < 2) { const fontClass = style?.fontFamily ? ` retro-font-${style.fontFamily}` : ''; const inlineStyle = style ? generateCSSStyleWithoutFont({ ...style, gradient: undefined }) : ''; const result = `${escapeHtml(text)}`; return result; } const chars = Array.from(text); const totalChars = chars.length; if (totalChars === 0) return ''; // Create spans for each character with interpolated colors const gradientSpans = chars.map((char, index) => { if (char === ' ') { return ' '; // Use non-breaking space to preserve spacing } // Calculate color position (0 to 1) const position = totalChars === 1 ? 0 : index / (totalChars - 1); // Find the two colors to interpolate between const colorIndex = position * (colors.length - 1); const lowerIndex = Math.floor(colorIndex); const upperIndex = Math.min(lowerIndex + 1, colors.length - 1); const t = colorIndex - lowerIndex; // Interpolate between colors const color1 = hexToRgb(colors[lowerIndex]); const color2 = hexToRgb(colors[upperIndex]); if (!color1 || !color2) { const fallbackStyle = style ? generateCSSStyleWithoutFont({ ...style, gradient: undefined }) : ''; return `${escapeHtml(char)}`; } const r = Math.round(color1.r + t * (color2.r - color1.r)); const g = Math.round(color1.g + t * (color2.g - color1.g)); const b = Math.round(color1.b + t * (color2.b - color1.b)); const interpolatedColor = `rgb(${r}, ${g}, ${b})`; // Combine interpolated color with other styles let inlineStyle = `color: ${interpolatedColor}`; if (style) { // Get styles without font-family and color if (style.fontSize !== 14) inlineStyle += `; font-size: ${style.fontSize}px`; if (style.bold) inlineStyle += `; font-weight: bold`; if (style.italic) inlineStyle += `; font-style: italic`; const decorations: string[] = []; if (style.underline) decorations.push('underline'); if (style.strikethrough) decorations.push('line-through'); if (decorations.length > 0) { inlineStyle += `; text-decoration: ${decorations.join(' ')}`; } } return `${escapeHtml(char)}`; }); // Wrap all spans in a container - use inline display to match text baseline const fontClass = style?.fontFamily ? ` retro-font-${style.fontFamily}` : ''; const containerStyle = style ? generateCSSStyleWithoutFont({ ...style, gradient: undefined, color: undefined }) : ''; const result = `${gradientSpans.join('')}`; return result; } // Helper function to convert hex to RGB function hexToRgb(hex: string): { r: number; g: number; b: number } | null { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } // Validate message content for security export function validateMessageContent(content: string): { isValid: boolean; sanitizedContent: string; errors: string[]; } { const errors: string[] = []; let sanitizedContent = content; // Check length if (content.length > 2000) { errors.push('Message too long (max 2000 characters)'); sanitizedContent = content.substring(0, 2000); } // Check for potential XSS patterns const dangerousPatterns = [ /