import 'dart:math' as math; /// Utility class to convert mixed format content (BBCode, Markdown, and HTML) to HTML class FormatConverter { /// Converts mixed format text (BBCode, Markdown, HTML) to pure HTML static String toHtml(String content) { if (content.isEmpty) return ''; // First, normalize line endings and escape any literal backslashes that aren't already escaped String result = content.replaceAll('\r\n', '\n'); // Handle BBCode format result = _convertBBCodeToHtml(result); // Handle Markdown format result = _convertMarkdownToHtml(result); // Sanitize HTML result = _sanitizeHtml(result); // Wrap the final content in a container with styles result = '
$result
'; return result; } /// Converts BBCode to HTML static String _convertBBCodeToHtml(String bbcode) { String result = bbcode; // Fix unclosed tags - RimWorld descriptions often have unclosed BBCode tags final List tagTypes = [ 'b', 'i', 'color', 'size', 'url', 'code', 'quote', 'list', 'table', 'tr', 'td', ]; for (final tag in tagTypes) { final openCount = '[${tag}'.allMatches(result).length; final closeCount = '[/$tag]'.allMatches(result).length; if (openCount > closeCount) { result = result + '[/$tag]' * (openCount - closeCount); } } // URLs // [url=http://example.com]text[/url] -> text result = RegExp( r'\[url=([^\]]+)\](.*?)\[/url\]', dotAll: true, ).allMatches(result).fold(result, (prev, match) { final url = match.group(1); final text = match.group(2); return prev.replaceFirst( match.group(0)!, '$text', ); }); // Simple URL [url]http://example.com[/url] -> http://example.com result = result.replaceAllMapped( RegExp(r'\[url\](.*?)\[/url\]', dotAll: true), (match) => '${match.group(1)}', ); // Bold result = result .replaceAll('[b]', '') .replaceAll('[/b]', ''); // Italic result = result.replaceAll('[i]', '').replaceAll('[/i]', ''); // Headers result = result.replaceAllMapped( RegExp(r'\[h1\](.*?)\[/h1\]', dotAll: true), (match) => '

${match.group(1)?.trim()}

', ); result = result.replaceAllMapped( RegExp(r'\[h2\](.*?)\[/h2\]', dotAll: true), (match) => '

${match.group(1)?.trim()}

', ); result = result.replaceAllMapped( RegExp(r'\[h3\](.*?)\[/h3\]', dotAll: true), (match) => '

${match.group(1)?.trim()}

', ); // Lists result = result .replaceAll( '[list]', ''); // List items result = result.replaceAllMapped( RegExp(r'\[\*\](.*?)(?=\[\*\]|\[/list\]|$)', dotAll: true), (match) { final content = match.group(1)?.trim() ?? ''; return '
  • $content
  • '; }, ); // Color result = result.replaceAllMapped( RegExp(r'\[color=([^\]]+)\](.*?)\[/color\]', dotAll: true), (match) { final color = match.group(1) ?? ''; final content = match.group(2) ?? ''; if (content.trim().isEmpty) return ''; return '$content'; }, ); // Images result = result.replaceAllMapped( RegExp(r'\[img\](.*?)\[/img\]', dotAll: true), (match) => 'Image', ); // Image with size result = result.replaceAllMapped( RegExp(r'\[img[^\]]*width=(\d+)[^\]]*\](.*?)\[/img\]', dotAll: true), (match) { final width = match.group(1) ?? ''; final url = match.group(2) ?? ''; return 'Image'; }, ); // Tables result = result .replaceAll( '[table]', '', ) .replaceAll('[/table]', '
    '); result = result.replaceAll('[tr]', '').replaceAll('[/tr]', ''); result = result .replaceAll('[td]', '') .replaceAll('[/td]', ''); // Size result = result.replaceAllMapped( RegExp(r'\[size=([^\]]+)\](.*?)\[/size\]', dotAll: true), (match) { final size = match.group(1) ?? ''; final content = match.group(2) ?? ''; return '$content'; }, ); // Code result = result .replaceAll( '[code]', '
    ',
            )
            .replaceAll('[/code]', '
    '); // Quote result = result.replaceAllMapped( RegExp(r'\[quote\](.*?)\[/quote\]', dotAll: true), (match) { final content = match.group(1)?.trim() ?? ''; if (content.isEmpty) return ''; return '
    $content
    '; }, ); // Handle any remaining custom BBCode tags result = result.replaceAllMapped( RegExp(r'\[([a-zA-Z0-9_]+)(?:=[^\]]+)?\](.*?)\[/\1\]', dotAll: true), (match) => match.group(2) ?? '', ); // Handle RimWorld-specific patterns // [h1] without closing tag is common result = result.replaceAllMapped( RegExp(r'\[h1\]([^\[]+)'), (match) => '

    ${match.group(1)?.trim()}

    ', ); return result; } /// Converts Markdown to HTML static String _convertMarkdownToHtml(String markdown) { String result = markdown; // Headers // Convert # Header to

    Header

    result = result.replaceAllMapped( RegExp(r'^#\s+(.*?)$', multiLine: true), (match) => '

    ${match.group(1)?.trim()}

    ', ); // Convert ## Header to

    Header

    result = result.replaceAllMapped( RegExp(r'^##\s+(.*?)$', multiLine: true), (match) => '

    ${match.group(1)?.trim()}

    ', ); // Convert ### Header to

    Header

    result = result.replaceAllMapped( RegExp(r'^###\s+(.*?)$', multiLine: true), (match) => '

    ${match.group(1)?.trim()}

    ', ); // Bold - **text** to text result = result.replaceAllMapped( RegExp(r'\*\*(.*?)\*\*'), (match) => '${match.group(1)}', ); // Italic - *text* or _text_ to text result = result.replaceAllMapped( RegExp(r'\*(.*?)\*|_(.*?)_'), (match) => '${match.group(1) ?? match.group(2)}', ); // Inline code - `code` to code result = result.replaceAllMapped( RegExp(r'`(.*?)`'), (match) => '${match.group(1)}', ); // Links - [text](url) to text result = result.replaceAllMapped( RegExp(r'\[(.*?)\]\((.*?)\)'), (match) => '${match.group(1)}', ); // Images - ![alt](url) to alt result = result.replaceAllMapped( RegExp(r'!\[(.*?)\]\((.*?)\)'), (match) => '${match.group(1)}', ); // Lists - Convert Markdown bullet lists to HTML lists // This is a simple implementation and might not handle all cases result = result.replaceAllMapped( RegExp(r'^(\s*)\*\s+(.*?)$', multiLine: true), (match) => '
  • ${match.group(2)}
  • ', ); // Wrap adjacent list items in