Clean up code
This commit is contained in:
@@ -5,31 +5,44 @@ 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>';
|
||||
|
||||
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'];
|
||||
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;
|
||||
@@ -37,75 +50,88 @@ class FormatConverter {
|
||||
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>'
|
||||
);
|
||||
});
|
||||
|
||||
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>'
|
||||
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>');
|
||||
|
||||
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>'
|
||||
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>'
|
||||
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>'
|
||||
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>');
|
||||
|
||||
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),
|
||||
RegExp(r'\[\*\](.*?)(?=\[\*\]|\[/list\]|$)', dotAll: true),
|
||||
(match) {
|
||||
final content = match.group(1)?.trim() ?? '';
|
||||
return '<li style="margin-bottom: 4px;">$content</li>';
|
||||
}
|
||||
return '<li style="margin-bottom: 4px;">$content</li>';
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
// Color
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[color=([^\]]+)\](.*?)\[/color\]', dotAll: true),
|
||||
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%;" />'
|
||||
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),
|
||||
@@ -113,130 +139,155 @@ class FormatConverter {
|
||||
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(
|
||||
'[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>');
|
||||
|
||||
result = result
|
||||
.replaceAll('[td]', '<td style="padding: 8px;">')
|
||||
.replaceAll('[/td]', '</td>');
|
||||
|
||||
// Size
|
||||
result = result.replaceAllMapped(
|
||||
RegExp(r'\[size=([^\]]+)\](.*?)\[/size\]', dotAll: true),
|
||||
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>');
|
||||
|
||||
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),
|
||||
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) ?? ''
|
||||
(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>'
|
||||
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>'
|
||||
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>'
|
||||
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>'
|
||||
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>'
|
||||
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>'
|
||||
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>'
|
||||
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>'
|
||||
RegExp(r'\[(.*?)\]\((.*?)\)'),
|
||||
(match) =>
|
||||
'<a href="${match.group(2)}" target="_blank">${match.group(1)}</a>',
|
||||
);
|
||||
|
||||
|
||||
// Images -  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%;" />'
|
||||
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>'
|
||||
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>',
|
||||
'<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;">', '');
|
||||
|
||||
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 />'
|
||||
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
|
||||
@@ -247,11 +298,11 @@ class FormatConverter {
|
||||
.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>', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user