Files
iiEsaywebUIapp/lib/shared/widgets/markdown/streaming_markdown_widget.dart
cogwheel0 e04b43949b refactor: migrate to flutter_math_fork and markdown_widget for enhanced markdown capabilities
- Replaced flutter_markdown_plus with flutter_math_fork to improve mathematical rendering within markdown content.
- Integrated markdown_widget for better markdown processing and rendering, enhancing overall user experience.
- Updated the markdown configuration to utilize the new packages, ensuring maintainability and adaptability of markdown features.
- Refactored the streaming markdown widget to accommodate the new markdown processing logic, improving code clarity and performance.
2025-10-04 13:37:47 +05:30

122 lines
3.2 KiB
Dart

import 'package:flutter/material.dart';
import '../../theme/theme_extensions.dart';
import 'markdown_config.dart';
import 'markdown_preprocessor.dart';
class StreamingMarkdownWidget extends StatelessWidget {
const StreamingMarkdownWidget({
super.key,
required this.content,
required this.isStreaming,
this.onTapLink,
});
final String content;
final bool isStreaming;
final MarkdownLinkTapCallback? onTapLink;
@override
Widget build(BuildContext context) {
if (content.trim().isEmpty) {
return const SizedBox.shrink();
}
final normalized = ConduitMarkdownPreprocessor.normalize(content);
final markdownTheme = ConduitMarkdownConfig.resolve(
context,
onTapLink: onTapLink,
);
final generator = markdownTheme.createGenerator();
final mermaidRegex = RegExp(r'```mermaid\s*([\s\S]*?)```', multiLine: true);
final matches = mermaidRegex.allMatches(normalized).toList();
List<Widget> buildMarkdownBlocks(String data) {
return generator.buildWidgets(data, config: markdownTheme.config);
}
if (matches.isEmpty) {
final blocks = buildMarkdownBlocks(normalized);
return SelectionArea(
child: Theme(
data: Theme.of(context).copyWith(
textSelectionTheme: TextSelectionThemeData(
cursorColor: context.conduitTheme.buttonPrimary,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: blocks,
),
),
);
}
final children = <Widget>[];
var currentIndex = 0;
for (final match in matches) {
final before = normalized.substring(currentIndex, match.start);
if (before.trim().isNotEmpty) {
children.addAll(buildMarkdownBlocks(before));
}
final code = match.group(1)?.trim() ?? '';
if (code.isNotEmpty) {
children.add(ConduitMarkdownConfig.buildMermaidBlock(context, code));
}
currentIndex = match.end;
}
final tail = normalized.substring(currentIndex);
if (tail.trim().isNotEmpty) {
children.addAll(buildMarkdownBlocks(tail));
}
return SelectionArea(
child: Theme(
data: Theme.of(context).copyWith(
textSelectionTheme: TextSelectionThemeData(
cursorColor: context.conduitTheme.buttonPrimary,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
),
);
}
}
extension StreamingMarkdownExtension on String {
Widget toMarkdown({
required BuildContext context,
bool isStreaming = false,
MarkdownLinkTapCallback? onTapLink,
}) {
return StreamingMarkdownWidget(
content: this,
isStreaming: isStreaming,
onTapLink: onTapLink,
);
}
}
class MarkdownWithLoading extends StatelessWidget {
const MarkdownWithLoading({super.key, this.content, required this.isLoading});
final String? content;
final bool isLoading;
@override
Widget build(BuildContext context) {
final value = content ?? '';
if (isLoading && value.trim().isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return StreamingMarkdownWidget(content: value, isStreaming: isLoading);
}
}