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]', '').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 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 '
';
}
);
// 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 -  to
result = result.replaceAllMapped(
RegExp(r'!\[(.*?)\]\((.*?)\)'),
(match) => '
'
);
// 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 tags (simple approach)
result = result.replaceAll('\n- ', '
- ');
result = result.replaceAll('
- ', '
- ');
result = result.replaceAll('
', '
');
// Remove duplicated tags
result = result.replaceAll('
', '');
// Paragraphs - Convert newlines to
, but skipping where tags already exist
result = result.replaceAllMapped(
RegExp(r'(?)\n(?!<)'),
(match) => '
'
);
return result;
}
/// Performs basic sanitization and fixes for the HTML
static String _sanitizeHtml(String html) {
// Remove potentially dangerous elements and attributes
final String result = html
// Remove any script tags
.replaceAll(RegExp(r'.*?', dotAll: true), '')
// Remove on* event handlers
.replaceAll(RegExp(r'\son\w+=".*?"'), '')
// Ensure newlines are converted to
if not already handled
.replaceAll(RegExp(r'(?)\n(?!<)'), '
');
// Fix double paragraph or break issues
return result
.replaceAll('
', '
')
.replaceAll('
', '
')
.replaceAll('', '');
}
}