diff --git a/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart b/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart
index e6d437af2..503633121 100644
--- a/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart
+++ b/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart
@@ -27,7 +27,7 @@ abstract class DomTransformer {
Tuple2? findImageUrlFromStyleTag(String style) {
try {
- final regExp = RegExp(r'background-image:\s*url\(([^)]+)\).*?');
+ final regExp = RegExp(r'''\bbackground-image\s*:\s*url\(\s*['"]?([^' ")]+)['"]?\s*\)''');
final match = regExp.firstMatch(style);
if (match == null) {
return null;
diff --git a/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart b/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart
index 07119143c..6bf21bae2 100644
--- a/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart
+++ b/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart
@@ -15,8 +15,9 @@ class NormalizeLineHeightInStyleTransformer extends DomTransformer {
Map? mapUrlDownloadCID,
}) async {
try {
+ final alternation = _removableLineHeights.map(RegExp.escape).join('|');
final pattern = RegExp(
- r'line-height\s*:\s*(?:' + _removableLineHeights.join('|') + r')\s*;?',
+ r'line-height\s*:\s*(?:' + alternation + r')\s*;?',
caseSensitive: false,
);
@@ -29,7 +30,7 @@ class NormalizeLineHeightInStyleTransformer extends DomTransformer {
var updatedStyle = originalStyle.replaceAll(pattern, '').trim();
// Remove extra spaces (>=2 spaces → 1 space)
- updatedStyle = updatedStyle.replaceAll(RegExp(r'\s{2,}'), ' ');
+ updatedStyle = updatedStyle.replaceAll(RegExp(r' {2,}'), ' ');
if (updatedStyle != originalStyle) {
if (updatedStyle.isEmpty) {
diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart
index c87a23135..f232d87d3 100644
--- a/core/lib/utils/html/html_utils.dart
+++ b/core/lib/utils/html/html_utils.dart
@@ -700,8 +700,8 @@ class HtmlUtils {
}
static String addQuoteToggle(String htmlString) {
- final likelyHtml = htmlString.contains(RegExp(r'<[a-zA-Z][^>]*>')) && // Contains a start tag
- htmlString.contains(RegExp(r'[a-zA-Z][^>]*>')); // Contains an end tag
+ final likelyHtml = htmlString.contains(RegExp(r'<[a-zA-Z][^>\s]*[^>]*>')) && // Contains a start tag
+ htmlString.contains(RegExp(r'[a-zA-Z][^>]{0,128}>')); // Contains an end tag
if (!likelyHtml) {
return htmlString; // Not likely HTML, return original
@@ -850,8 +850,9 @@ class HtmlUtils {
} while (decoded != cleaned && iterations < maxIterations);
// Delete all remaining HTML tags → replace tag with space to avoid text sticking
+ final String tagsPipe = validTags.map(RegExp.escape).join('|');
final tagRegex = RegExp(
- '?(${validTags.join('|')})(\\s+[^>]*)?>',
+ '?(?:$tagsPipe)(?:\\s+[^>]*)?>',
caseSensitive: false,
);
cleaned = cleaned.replaceAll(tagRegex, ' ');
@@ -870,10 +871,9 @@ class HtmlUtils {
if (container == null) return htmlString;
final urlRegex = RegExp(
- r'''(?:(?:https?:\/\/)|(?:ftp:\/\/)|(?:mailto:)|(?:file:\/\/)|(?:www\.))(?!\.)(?!.*\.\.)([^\s<]+[^<.,:;\"\'\)\]\s!?])''',
+ r'''(?:https?://|ftp://|mailto:|file://|www\.)[^\s<.]+(?:\.[^\s<.]+)*(? extractStrings(String input, String separatorPattern) {
try {
- // Check if the input is URL encoded
+ // 1. URL Decoding
if (input.contains('%')) {
- input = Uri.decodeComponent(input); // Decode URL encoding
+ input = Uri.decodeComponent(input);
}
- // Efficient Base64 validation: Check length and minimal regex match
- if (input.length % 4 == 0 && input.contains(RegExp(r'^[A-Za-z0-9+/=]+$'))) {
- try {
- input = utf8.decode(base64.decode(input)); // Decode Base64 encoding
- } catch (_) {
- // Ignore if decoding fails
+ // 2. Base64 Check - Using a non-regex check first is faster
+ if (input.length % 4 == 0 && !input.contains(' ')) {
+ // Only run regex if basic length/whitespace checks pass
+ if (RegExp(r'^[A-Za-z0-9+/=]+$').hasMatch(input)) {
+ try {
+ input = utf8.decode(base64.decode(input));
+ } catch (_) {
+ // Ignore if decoding fails
+ }
}
}
+ // 3. Optimized Split
+ // We use the pattern directly and skip the redundant .replaceAll() and .trim()
final RegExp separator = RegExp(separatorPattern);
final listStrings = input
- .replaceAll('\n', ' ')
.split(separator)
- .map((value) => value.trim())
.where((value) => value.isNotEmpty)
.toList();
log('StringConvert::extractStrings:listStrings = $listStrings');
@@ -128,16 +132,32 @@ class StringConvert {
text.split('\n').where((line) => line.trim().isNotEmpty).toList();
if (lines.length < 2) return false;
- // Check for Markdown table
- final mdSeparatorRegex = RegExp(r'^\|?(\s*:?-+:?\s*\|)+(\s*:?-+:?\s*)\|?$');
- final isMarkdown = lines.any((line) => mdSeparatorRegex.hasMatch(line));
+ // 1. Hardened Markdown Separator Regex
+ // We use [ \t]* instead of \s to be specific and avoid newline issues
+ // We ensure the dash sequence is clearly delimited
+ final mdSeparatorRegex = RegExp(
+ r'^\|?(?:[ \t]*:?-+:?[ \t]*\|)+[ \t]*:?-+:?[ \t]*\|?$',
+ multiLine: false);
- // Check for ASCII art table (with borders made of +, -, |, /, \, =)
- final asciiArtRegex = RegExp(r'[\+\-\|/\\=]');
- final isAsciiArt = lines.every((line) =>
- line.contains(asciiArtRegex) && line.split(asciiArtRegex).length > 1);
- log('StringConvert::isTextTable:isMarkdown = $isMarkdown | isAscii = $isAsciiArt');
- return isMarkdown || isAsciiArt;
+ // 2. Optimized ASCII Check
+ // We use hasMatch instead of split().length for better performance
+ final asciiArtRegex = RegExp(r'[+\-|/\\=]');
+
+ bool isMarkdown = false;
+ bool allLinesHaveAscii = true;
+
+ for (final line in lines) {
+ if (!isMarkdown && mdSeparatorRegex.hasMatch(line)) {
+ isMarkdown = true;
+ }
+ if (allLinesHaveAscii && !asciiArtRegex.hasMatch(line)) {
+ allLinesHaveAscii = false;
+ }
+ // Early exit if we found a table but also know it's not ASCII art
+ if (isMarkdown && !allLinesHaveAscii) break;
+ }
+
+ return isMarkdown || (allLinesHaveAscii && lines.length >= 2);
}
static List extractNamedAddresses(String input) {
@@ -156,12 +176,11 @@ class StringConvert {
input = input.replaceAll('\n', ' ');
final results = [];
- final pattern = RegExp(r'''(?:"([^"]+)"|'([^']+)'|)\s*<([^>]+)>''');
+ final pattern = RegExp(r'''(?:(?:"([^"]+)"|'([^']+)')\s*)?<([^>]+)>''');
int currentIndex = 0;
- final matches = pattern.allMatches(input).toList();
-
- for (final match in matches) {
+ // Use a for-in loop directly on the iterable to save memory
+ for (final match in pattern.allMatches(input)) {
if (match.start > currentIndex) {
final between = input.substring(currentIndex, match.start);
results.addAll(_splitPlainAddresses(between, emailSeparatorPattern));
diff --git a/lib/main/utils/app_utils.dart b/lib/main/utils/app_utils.dart
index 2194a8eed..f082b060b 100644
--- a/lib/main/utils/app_utils.dart
+++ b/lib/main/utils/app_utils.dart
@@ -54,7 +54,7 @@ class AppUtils {
static bool isEmailLocalhost(String email) {
final normalized = email.trim();
return RegExp(
- r'^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@localhost$'
+ r'^(?:"[^"\r\n]+"|[^<>()[\]\\.,;:\s@"]+(?:\.[^<>()[\]\\.,;:\s@"]+)*)@localhost$'
).hasMatch(normalized);
}