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']*>')); // Contains an end tag + final likelyHtml = htmlString.contains(RegExp(r'<[a-zA-Z][^>\s]*[^>]*>')) && // Contains a start tag + htmlString.contains(RegExp(r']{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( - ']*)?>', + ']*)?>', 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); }