Jesus they're not BBCode... They're html and bbcode and markdown

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
This commit is contained in:
2025-03-19 00:44:14 +01:00
parent 09b7fe539e
commit 40d251f400
12 changed files with 818 additions and 9 deletions

257
lib/format_converter.dart Normal file
View File

@@ -0,0 +1,257 @@
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 = '<div style="line-height: 1.5; word-wrap: break-word;">$result</div>';
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<String> 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] -> <a href="http://example.com">text</a>
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)!,
'<a href="$url" target="_blank">$text</a>'
);
});
// Simple URL [url]http://example.com[/url] -> <a href="http://example.com">http://example.com</a>
result = result.replaceAllMapped(
RegExp(r'\[url\](.*?)\[/url\]', dotAll: true),
(match) => '<a href="${match.group(1)}" target="_blank">${match.group(1)}</a>'
);
// Bold
result = result.replaceAll('[b]', '<strong>').replaceAll('[/b]', '</strong>');
// Italic
result = result.replaceAll('[i]', '<em>').replaceAll('[/i]', '</em>');
// Headers
result = result.replaceAllMapped(
RegExp(r'\[h1\](.*?)\[/h1\]', dotAll: true),
(match) => '<h1 style="margin-top: 16px; margin-bottom: 8px;">${match.group(1)?.trim()}</h1>'
);
result = result.replaceAllMapped(
RegExp(r'\[h2\](.*?)\[/h2\]', dotAll: true),
(match) => '<h2 style="margin-top: 12px; margin-bottom: 6px;">${match.group(1)?.trim()}</h2>'
);
result = result.replaceAllMapped(
RegExp(r'\[h3\](.*?)\[/h3\]', dotAll: true),
(match) => '<h3 style="margin-top: 10px; margin-bottom: 4px;">${match.group(1)?.trim()}</h3>'
);
// Lists
result = result.replaceAll('[list]', '<ul style="padding-left: 20px; margin-top: 8px; margin-bottom: 8px;">').replaceAll('[/list]', '</ul>');
// List items
result = result.replaceAllMapped(
RegExp(r'\[\*\](.*?)(?=\[\*\]|\[/list\]|$)', dotAll: true),
(match) {
final content = match.group(1)?.trim() ?? '';
return '<li style="margin-bottom: 4px;">$content</li>';
}
);
// 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 '<span style="color:$color">$content</span>';
}
);
// Images
result = result.replaceAllMapped(
RegExp(r'\[img\](.*?)\[/img\]', dotAll: true),
(match) => '<img src="${match.group(1)}" alt="Image" style="max-width: 100%;" />'
);
// 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 '<img src="$url" alt="Image" width="$width" style="max-width: 100%;" />';
}
);
// Tables
result = result.replaceAll('[table]', '<table border="1" style="border-collapse: collapse; width: 100%; margin: 10px 0;">').replaceAll('[/table]', '</table>');
result = result.replaceAll('[tr]', '<tr>').replaceAll('[/tr]', '</tr>');
result = result.replaceAll('[td]', '<td style="padding: 8px;">').replaceAll('[/td]', '</td>');
// Size
result = result.replaceAllMapped(
RegExp(r'\[size=([^\]]+)\](.*?)\[/size\]', dotAll: true),
(match) {
final size = match.group(1) ?? '';
final content = match.group(2) ?? '';
return '<span style="font-size:${size}px">$content</span>';
}
);
// Code
result = result.replaceAll('[code]', '<pre style="background-color: rgba(0,0,0,0.1); padding: 8px; border-radius: 4px; overflow-x: auto;"><code>').replaceAll('[/code]', '</code></pre>');
// Quote
result = result.replaceAllMapped(
RegExp(r'\[quote\](.*?)\[/quote\]', dotAll: true),
(match) {
final content = match.group(1)?.trim() ?? '';
if (content.isEmpty) return '';
return '<blockquote style="border-left: 4px solid rgba(128,128,128,0.5); padding-left: 10px; margin: 10px 0; color: rgba(255,255,255,0.8);">$content</blockquote>';
}
);
// 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) => '<h1 style="margin-top: 16px; margin-bottom: 8px;">${match.group(1)?.trim()}</h1>'
);
return result;
}
/// Converts Markdown to HTML
static String _convertMarkdownToHtml(String markdown) {
String result = markdown;
// Headers
// Convert # Header to <h1>Header</h1>
result = result.replaceAllMapped(
RegExp(r'^#\s+(.*?)$', multiLine: true),
(match) => '<h1 style="margin-top: 16px; margin-bottom: 8px;">${match.group(1)?.trim()}</h1>'
);
// Convert ## Header to <h2>Header</h2>
result = result.replaceAllMapped(
RegExp(r'^##\s+(.*?)$', multiLine: true),
(match) => '<h2 style="margin-top: 12px; margin-bottom: 6px;">${match.group(1)?.trim()}</h2>'
);
// Convert ### Header to <h3>Header</h3>
result = result.replaceAllMapped(
RegExp(r'^###\s+(.*?)$', multiLine: true),
(match) => '<h3 style="margin-top: 10px; margin-bottom: 4px;">${match.group(1)?.trim()}</h3>'
);
// Bold - **text** to <strong>text</strong>
result = result.replaceAllMapped(
RegExp(r'\*\*(.*?)\*\*'),
(match) => '<strong>${match.group(1)}</strong>'
);
// Italic - *text* or _text_ to <em>text</em>
result = result.replaceAllMapped(
RegExp(r'\*(.*?)\*|_(.*?)_'),
(match) => '<em>${match.group(1) ?? match.group(2)}</em>'
);
// Inline code - `code` to <code>code</code>
result = result.replaceAllMapped(
RegExp(r'`(.*?)`'),
(match) => '<code style="background-color: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">${match.group(1)}</code>'
);
// Links - [text](url) to <a href="url">text</a>
result = result.replaceAllMapped(
RegExp(r'\[(.*?)\]\((.*?)\)'),
(match) => '<a href="${match.group(2)}" target="_blank">${match.group(1)}</a>'
);
// Images - ![alt](url) to <img src="url" alt="alt" />
result = result.replaceAllMapped(
RegExp(r'!\[(.*?)\]\((.*?)\)'),
(match) => '<img src="${match.group(2)}" alt="${match.group(1)}" style="max-width: 100%;" />'
);
// 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) => '<li style="margin-bottom: 4px;">${match.group(2)}</li>'
);
// Wrap adjacent list items in <ul> tags (simple approach)
result = result.replaceAll('</li>\n<li>', '</li><li>');
result = result.replaceAll('<li>', '<ul style="padding-left: 20px; margin-top: 8px; margin-bottom: 8px;"><li>');
result = result.replaceAll('</li>', '</li></ul>');
// Remove duplicated </ul><ul> tags
result = result.replaceAll('</ul><ul style="padding-left: 20px; margin-top: 8px; margin-bottom: 8px;">', '');
// Paragraphs - Convert newlines to <br>, but skipping where tags already exist
result = result.replaceAllMapped(
RegExp(r'(?<!>)\n(?!<)'),
(match) => '<br />'
);
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'<script.*?>.*?</script>', dotAll: true), '')
// Remove on* event handlers
.replaceAll(RegExp(r'\son\w+=".*?"'), '')
// Ensure newlines are converted to <br /> if not already handled
.replaceAll(RegExp(r'(?<!>)\n(?!<)'), '<br />');
// Fix double paragraph or break issues
return result
.replaceAll('<br /><br />', '<br />')
.replaceAll('<br></br>', '<br />')
.replaceAll('<p></p>', '');
}
}