Implement a popup card that renders description markdown
This commit is contained in:
174
lib/bbcode_converter.dart
Normal file
174
lib/bbcode_converter.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
/// Utility class to convert BBCode to Markdown for RimWorld mod descriptions
|
||||
class BBCodeConverter {
|
||||
/// Converts BBCode formatted text to Markdown format
|
||||
static String toMarkdown(String bbcode) {
|
||||
if (bbcode.isEmpty) return '';
|
||||
|
||||
// First, normalize line endings
|
||||
String result = bbcode.replaceAll('\r\n', '\n');
|
||||
|
||||
// Fix unclosed tags - RimWorld descriptions often have unclosed tags
|
||||
final List<String> tagTypes = ['b', 'i', 'color', 'size', 'url', 'code', 'quote'];
|
||||
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](http://example.com)
|
||||
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]($url)'
|
||||
);
|
||||
});
|
||||
|
||||
// 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)}'
|
||||
);
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[h2\](.*?)\[/h2\]', dotAll: true),
|
||||
(match) => '## ${match.group(1)}'
|
||||
);
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[h3\](.*?)\[/h3\]', dotAll: true),
|
||||
(match) => '### ${match.group(1)}'
|
||||
);
|
||||
|
||||
// Lists - handle nested lists too
|
||||
result = result.replaceAll('[list]', '\n').replaceAll('[/list]', '\n');
|
||||
|
||||
// Handle list items - giving them proper indentation
|
||||
int listLevel = 0;
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[\*\](.*?)(?=\[\*\]|\[/list\]|$)', dotAll: true),
|
||||
(match) {
|
||||
final content = match.group(1)?.trim() ?? '';
|
||||
return '* $content\n';
|
||||
}
|
||||
);
|
||||
|
||||
// Color - convert to bold since Markdown doesn't support color
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[color=([^\]]+)\](.*?)\[/color\]', dotAll: true),
|
||||
(match) {
|
||||
final content = match.group(2) ?? '';
|
||||
if (content.trim().isEmpty) return '';
|
||||
return '**${match.group(2)}**';
|
||||
}
|
||||
);
|
||||
|
||||
// Images
|
||||
// [img]url[/img] -> 
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[img\](.*?)\[/img\]', dotAll: true),
|
||||
(match) => '})'
|
||||
);
|
||||
|
||||
// Image with size [img width=300]url[/img] -> 
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[img[^\]]*width=(\d+)[^\]]*\](.*?)\[/img\]', dotAll: true),
|
||||
(match) => '})'
|
||||
);
|
||||
|
||||
// Tables - convert tables to markdown tables
|
||||
if (result.contains('[table]')) {
|
||||
// Process tables
|
||||
final tableRegex = RegExp(r'\[table\](.*?)\[/table\]', dotAll: true);
|
||||
final tables = tableRegex.allMatches(result);
|
||||
|
||||
for (final tableMatch in tables) {
|
||||
final tableContent = tableMatch.group(1) ?? '';
|
||||
final rows = RegExp(r'\[tr\](.*?)\[/tr\]', dotAll: true).allMatches(tableContent);
|
||||
|
||||
final markdownTable = StringBuffer();
|
||||
var isFirstRow = true;
|
||||
|
||||
for (final rowMatch in rows) {
|
||||
final rowContent = rowMatch.group(1) ?? '';
|
||||
final cells = RegExp(r'\[td\](.*?)\[/td\]', dotAll: true).allMatches(rowContent);
|
||||
|
||||
if (cells.isEmpty) continue;
|
||||
|
||||
final rowBuffer = StringBuffer('|');
|
||||
|
||||
for (final cellMatch in cells) {
|
||||
final cellContent = cellMatch.group(1)?.trim() ?? '';
|
||||
// Clean up any newlines inside cell content
|
||||
final cleanCell = cellContent.replaceAll('\n', ' ').trim();
|
||||
rowBuffer.write(' $cleanCell |');
|
||||
}
|
||||
|
||||
markdownTable.writeln(rowBuffer.toString());
|
||||
|
||||
// Add header separator after first row
|
||||
if (isFirstRow) {
|
||||
final cellCount = RegExp(r'\[td\]').allMatches(rowContent).length;
|
||||
markdownTable.writeln('|${' --- |' * cellCount}');
|
||||
isFirstRow = false;
|
||||
}
|
||||
}
|
||||
|
||||
result = result.replaceFirst(tableMatch.group(0)!, '\n${markdownTable.toString()}\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Size - remove since Markdown doesn't directly support font size
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[size=[^\]]+\](.*?)\[/size\]', dotAll: true),
|
||||
(match) => match.group(1) ?? ''
|
||||
);
|
||||
|
||||
// Code
|
||||
result = result.replaceAll('[code]', '\n```\n').replaceAll('[/code]', '\n```\n');
|
||||
|
||||
// Quote
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[quote\](.*?)\[/quote\]', dotAll: true),
|
||||
(match) {
|
||||
final content = match.group(1)?.trim() ?? '';
|
||||
if (content.isEmpty) return '';
|
||||
return '\n> ${content.replaceAll('\n', '\n> ')}\n';
|
||||
}
|
||||
);
|
||||
|
||||
// Handle any remaining custom BBCode tags - just remove them
|
||||
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)}\n'
|
||||
);
|
||||
|
||||
// Replace multiple newlines with at most two newlines
|
||||
result = result.replaceAll(RegExp(r'\n{3,}'), '\n\n');
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user