Fix up markdown rendering to be scrollable

This commit is contained in:
2025-03-19 00:28:59 +01:00
parent 5f20368fe2
commit 09b7fe539e
2 changed files with 296 additions and 104 deletions

View File

@@ -1,11 +1,22 @@
/// Utility class to convert BBCode to Markdown for RimWorld mod descriptions
import 'dart:math' as math;
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');
// First, normalize line endings and escape any literal backslashes
String result = bbcode.replaceAll('\r\n', '\n').replaceAll('\\', '\\\\');
// Ensure paragraphs - double newlines between paragraphs
// First normalize any consecutive newlines to a single one
result = result.replaceAll(RegExp(r'\n+'), '\n');
// Then add empty line after each paragraph where needed
result = result.replaceAll('.\n', '.\n\n');
result = result.replaceAll('!\n', '!\n\n');
result = result.replaceAll('?\n', '?\n\n');
// Fix unclosed tags - RimWorld descriptions often have unclosed tags
final List<String> tagTypes = ['b', 'i', 'color', 'size', 'url', 'code', 'quote'];
@@ -42,30 +53,29 @@ class BBCodeConverter {
// Italic
result = result.replaceAll('[i]', '_').replaceAll('[/i]', '_');
// Headers
// Headers - ensure they start on their own line
result = result.replaceAllMapped(
RegExp(r'\[h1\](.*?)\[/h1\]', dotAll: true),
(match) => '# ${match.group(1)}'
(match) => '\n\n# ${match.group(1)?.trim()}\n\n'
);
result = result.replaceAllMapped(
RegExp(r'\[h2\](.*?)\[/h2\]', dotAll: true),
(match) => '## ${match.group(1)}'
(match) => '\n\n## ${match.group(1)?.trim()}\n\n'
);
result = result.replaceAllMapped(
RegExp(r'\[h3\](.*?)\[/h3\]', dotAll: true),
(match) => '### ${match.group(1)}'
(match) => '\n\n### ${match.group(1)?.trim()}\n\n'
);
// 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';
return '\n* $content\n';
}
);
@@ -102,35 +112,64 @@ class BBCodeConverter {
final tableContent = tableMatch.group(1) ?? '';
final rows = RegExp(r'\[tr\](.*?)\[/tr\]', dotAll: true).allMatches(tableContent);
final markdownTable = StringBuffer();
// Only process tables that have rows
if (rows.isEmpty) {
result = result.replaceFirst(tableMatch.group(0)!, '');
continue;
}
final markdownTable = StringBuffer('\n');
var isFirstRow = true;
// First determine the number of columns by examining all rows
int maxColumns = 0;
for (final rowMatch in rows) {
final rowContent = rowMatch.group(1) ?? '';
final cellCount = RegExp(r'\[td\]').allMatches(rowContent).length;
maxColumns = math.max(maxColumns, cellCount);
}
// Ensure we have at least 1 column
maxColumns = math.max(1, maxColumns);
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('|');
final rowBuffer = StringBuffer('| ');
int cellsAdded = 0;
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 |');
rowBuffer.write('$cleanCell | ');
cellsAdded++;
}
// Add empty cells if needed to maintain table structure
while (cellsAdded < maxColumns) {
rowBuffer.write(' | ');
cellsAdded++;
}
markdownTable.writeln(rowBuffer.toString());
// Add header separator after first row
if (isFirstRow) {
final cellCount = RegExp(r'\[td\]').allMatches(rowContent).length;
markdownTable.writeln('|${' --- |' * cellCount}');
final headerRow = StringBuffer('| ');
for (int i = 0; i < maxColumns; i++) {
headerRow.write('--- | ');
}
markdownTable.writeln(headerRow.toString());
isFirstRow = false;
}
}
result = result.replaceFirst(tableMatch.group(0)!, '\n${markdownTable.toString()}\n');
markdownTable.write('\n');
result = result.replaceFirst(tableMatch.group(0)!, markdownTable.toString());
}
}
@@ -149,7 +188,7 @@ class BBCodeConverter {
(match) {
final content = match.group(1)?.trim() ?? '';
if (content.isEmpty) return '';
return '\n> ${content.replaceAll('\n', '\n> ')}\n';
return '\n> ${content.replaceAll('\n', '\n> ')}\n\n';
}
);
@@ -163,12 +202,25 @@ class BBCodeConverter {
// [h1] without closing tag is common
result = result.replaceAllMapped(
RegExp(r'\[h1\]([^\[]+)'),
(match) => '# ${match.group(1)}\n'
(match) => '\n\n# ${match.group(1)?.trim()}\n\n'
);
// Replace multiple newlines with at most two newlines
// Convert simple newlines to double newlines for proper markdown rendering
// But avoid doing this for lines that are already marked up as headings, lists, or tables
result = result.replaceAllMapped(
RegExp(r'([^\n#*>|])\n([^\n#*>|-])'),
(match) => '${match.group(1)}\n\n${match.group(2)}'
);
// Normalize multiple spaces
result = result.replaceAll(RegExp(r' {2,}'), ' ');
// Remove excessive newlines (more than 2 consecutive)
result = result.replaceAll(RegExp(r'\n{3,}'), '\n\n');
// Ensure document starts and ends cleanly
result = result.trim();
return result;
}
}