refactor: replace HTML rendering with MarkdownHtmlWidget across various components for consistent markdown processing

This commit is contained in:
mso7nueh
2026-01-14 14:48:32 +03:00
parent c786821275
commit 4852d2cf10
8 changed files with 31 additions and 525 deletions

View File

@@ -13,12 +13,10 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class DietsWidget extends StatefulWidget {
final Diet diet;
@@ -117,94 +115,11 @@ class _DietsWidgetState extends State<DietsWidget> {
String details = widget.diet.details.length > 200
? '${widget.diet.details.substring(0, 200)}...'
: widget.diet.details;
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
details = details.replaceAllMapped(wrongLinkRegex, (match) {
final urlInText = match.group(1)!;
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(details)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = details;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(details)) {
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
details = result;
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
details = details.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return Html(
data: md.markdownToHtml(
details,
extensionSet: md.ExtensionSet.gitHubFlavored,
),
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"b, strong": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
if (url != null) {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
return MarkdownHtmlWidget(
markdownText: details,
fontSize: TextStyles.textSMedium.fontSize ?? 14,
textColor: appTheme.black,
);
},
),
@@ -275,8 +190,3 @@ class _DietsWidgetState extends State<DietsWidget> {
}
}
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -16,12 +16,10 @@ import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class RecipesWidget extends StatefulWidget {
final int index;
@@ -155,96 +153,11 @@ class _RecipesWidgetState extends State<RecipesWidget> {
String description = widget.recipe.description.length > 200
? widget.recipe.description.substring(0, 200)
: widget.recipe.description;
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
description = description.replaceAllMapped(wrongLinkRegex, (match) {
final urlInText = match.group(1)!;
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
// Находим все существующие markdown ссылки и их позиции
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(description)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = description;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(description)) {
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
description = result;
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
description = description.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return Html(
data: md.markdownToHtml(
description,
extensionSet: md.ExtensionSet.gitHubFlavored,
),
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"b, strong": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
if (url != null) {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
return MarkdownHtmlWidget(
markdownText: description,
fontSize: TextStyles.textSMedium.fontSize ?? 14,
textColor: appTheme.black,
);
},
),
@@ -338,8 +251,3 @@ class _RecipesWidgetState extends State<RecipesWidget> {
}
}
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}