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('', '');
}
}