From 758ed411b08340680dba9bdb4ecef062043cb88c Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Sat, 4 Oct 2025 16:04:49 +0530 Subject: [PATCH] refactor: enhance markdown processing and code structure - Updated the ConduitMarkdown class to streamline markdown rendering, improving maintainability and clarity. - Refactored the markdown configuration to utilize new methods for building markdown blocks and handling LaTeX syntax. - Improved the StreamingMarkdownWidget to leverage the updated markdown processing logic, ensuring a cohesive user experience. - Enhanced the handling of Mermaid diagrams and LaTeX rendering, providing better support for complex markdown content. --- lib/core/auth/auth_state_manager.dart | 5 +- .../widgets/markdown/code_block_header.dart | 49 ++ .../widgets/markdown/latex_block_widget.dart | 41 ++ .../widgets/markdown/markdown_config.dart | 492 ++++++++++-------- .../widgets/markdown/markdown_latex.dart | 94 ++-- .../markdown/streaming_markdown_widget.dart | 30 +- 6 files changed, 453 insertions(+), 258 deletions(-) create mode 100644 lib/shared/widgets/markdown/code_block_header.dart create mode 100644 lib/shared/widgets/markdown/latex_block_widget.dart diff --git a/lib/core/auth/auth_state_manager.dart b/lib/core/auth/auth_state_manager.dart index ef0c12d..7feb4a9 100644 --- a/lib/core/auth/auth_state_manager.dart +++ b/lib/core/auth/auth_state_manager.dart @@ -610,12 +610,11 @@ class AuthStateManager extends _$AuthStateManager { /// Prime the conversations list so navigation drawers show real data after login. void _prefetchConversations() { - Future.microtask(() async { + Future.microtask(() { if (!ref.mounted) return; try { refreshConversationsCache(ref, includeFolders: true); - await ref.read(conversationsProvider.future); - DebugLogger.auth('Conversations prefetch requested'); + DebugLogger.auth('Conversations prefetch scheduled'); } catch (e) { if (!ref.mounted) return; DebugLogger.warning( diff --git a/lib/shared/widgets/markdown/code_block_header.dart b/lib/shared/widgets/markdown/code_block_header.dart new file mode 100644 index 0000000..1e0116b --- /dev/null +++ b/lib/shared/widgets/markdown/code_block_header.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import '../../theme/theme_extensions.dart'; + +class CodeBlockHeader extends StatelessWidget { + const CodeBlockHeader({ + super.key, + required this.language, + required this.onCopy, + }); + + final String language; + final VoidCallback onCopy; + + @override + Widget build(BuildContext context) { + final theme = context.conduitTheme; + final label = language.isEmpty ? 'code' : language; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + decoration: BoxDecoration( + color: theme.surfaceContainer.withValues(alpha: 0.35), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.sm), + ), + ), + child: Row( + children: [ + Text( + label, + style: AppTypography.codeStyle.copyWith( + color: theme.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.copy_rounded, size: 18), + color: theme.iconPrimary, + tooltip: 'Copy code', + onPressed: onCopy, + ), + ], + ), + ); + } +} diff --git a/lib/shared/widgets/markdown/latex_block_widget.dart b/lib/shared/widgets/markdown/latex_block_widget.dart new file mode 100644 index 0000000..d45c8d5 --- /dev/null +++ b/lib/shared/widgets/markdown/latex_block_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; + +import '../../theme/theme_extensions.dart'; + +class LatexBlockWidget extends StatelessWidget { + const LatexBlockWidget({ + super.key, + required this.content, + required this.isInline, + required this.style, + required this.isDark, + }); + + final String content; + final bool isInline; + final TextStyle style; + final bool isDark; + + @override + Widget build(BuildContext context) { + final mathWidget = Math.tex( + content, + mathStyle: MathStyle.text, + textStyle: style, + textScaleFactor: 1, + onErrorFallback: (error) { + return Text(content, style: style.copyWith(color: Colors.red)); + }, + ); + + if (isInline) { + return mathWidget; + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: Spacing.xs), + child: Center(child: mathWidget), + ); + } +} diff --git a/lib/shared/widgets/markdown/markdown_config.dart b/lib/shared/widgets/markdown/markdown_config.dart index 88a7f65..6a3c14a 100644 --- a/lib/shared/widgets/markdown/markdown_config.dart +++ b/lib/shared/widgets/markdown/markdown_config.dart @@ -6,53 +6,73 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_highlight/flutter_highlight.dart'; import 'package:flutter_highlight/themes/a11y-dark.dart'; import 'package:flutter_highlight/themes/a11y-light.dart'; -import 'package:markdown/markdown.dart' as m; import 'package:markdown_widget/markdown_widget.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../theme/color_tokens.dart'; import '../../theme/theme_extensions.dart'; +import 'code_block_header.dart'; import 'markdown_latex.dart'; -/// Callback invoked when a markdown link is tapped. typedef MarkdownLinkTapCallback = void Function(String url, String title); -/// Bundles markdown configuration and generator metadata for the app theme. -class ConduitMarkdownTheme { - const ConduitMarkdownTheme({ - required this.config, - required this.inlineSyntaxes, - required this.blockSyntaxes, - required this.generators, - required this.linesMargin, - }); +class ConduitMarkdown { + const ConduitMarkdown._(); - final MarkdownConfig config; - final List inlineSyntaxes; - final List blockSyntaxes; - final List generators; - final EdgeInsets linesMargin; - - MarkdownGenerator createGenerator() { - return MarkdownGenerator( - inlineSyntaxList: inlineSyntaxes, - blockSyntaxList: blockSyntaxes, - linesMargin: linesMargin, - generators: generators, + static MarkdownWidget build({ + required BuildContext context, + required String data, + MarkdownLinkTapCallback? onTapLink, + bool selectable = true, + bool shrinkWrap = false, + ScrollPhysics? physics, + }) { + final components = prepare(context, onTapLink: onTapLink); + return MarkdownWidget( + data: data, + selectable: selectable, + config: components.config, + markdownGenerator: components.generator, + shrinkWrap: shrinkWrap, + physics: physics, + padding: EdgeInsets.zero, ); } -} -class ConduitMarkdownConfig { - static ConduitMarkdownTheme resolve( + static MarkdownBlock buildBlock({ + required BuildContext context, + required String data, + MarkdownLinkTapCallback? onTapLink, + bool selectable = true, + }) { + final components = prepare(context, onTapLink: onTapLink); + return MarkdownBlock( + data: data, + selectable: selectable, + config: components.config, + generator: components.generator, + ); + } + + static ({MarkdownConfig config, MarkdownGenerator generator}) prepare( + BuildContext context, { + MarkdownLinkTapCallback? onTapLink, + }) { + final config = _buildConfig(context, onTapLink: onTapLink); + final generator = _buildGenerator(context); + return (config: config, generator: generator); + } + + static MarkdownConfig _buildConfig( BuildContext context, { MarkdownLinkTapCallback? onTapLink, }) { final theme = context.conduitTheme; - final materialTheme = Theme.of(context); - final isDark = materialTheme.brightness == Brightness.dark; + final material = Theme.of(context); + final isDark = material.brightness == Brightness.dark; final baseBody = AppTypography.bodyMediumStyle.copyWith( color: theme.textPrimary, @@ -65,132 +85,129 @@ class ConduitMarkdownConfig { final codeBackground = theme.surfaceContainer.withValues(alpha: 0.55); final borderColor = theme.cardBorder.withValues(alpha: 0.25); - final latex = const ConduitLatex(); + final highlightTheme = _codeHighlightTheme(theme, isDark: isDark); - final markdownConfig = - (isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig) - .copy( - configs: [ - PConfig(textStyle: baseBody), - H1Config( - style: AppTypography.headlineLargeStyle.copyWith( - color: theme.textPrimary, - ), - ), - H2Config( - style: AppTypography.headlineMediumStyle.copyWith( - color: theme.textPrimary, - ), - ), - H3Config( - style: AppTypography.headlineSmallStyle.copyWith( - color: theme.textPrimary, - ), - ), - H4Config( - style: AppTypography.bodyLargeStyle.copyWith( - color: theme.textPrimary, - ), - ), - H5Config(style: baseBody.copyWith(fontWeight: FontWeight.w600)), - H6Config(style: secondaryBody), - LinkConfig( - style: baseBody.copyWith( - color: materialTheme.colorScheme.primary, - decoration: TextDecoration.underline, - decorationColor: materialTheme.colorScheme.primary, - ), - onTap: (url) => onTapLink?.call(url, url), - ), - CodeConfig( - style: AppTypography.codeStyle.copyWith( - color: theme.codeText, - backgroundColor: codeBackground, - ), - ), - PreConfig( - padding: const EdgeInsets.all(Spacing.sm), - margin: const EdgeInsets.symmetric(vertical: Spacing.xs), - decoration: BoxDecoration( - color: codeBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - border: Border.all( - color: borderColor, - width: BorderWidth.micro, - ), - ), - textStyle: AppTypography.codeStyle.copyWith( - color: theme.codeText, - ), - styleNotMatched: AppTypography.codeStyle.copyWith( - color: theme.codeText, - ), - theme: _codeHighlightTheme(theme, isDark: isDark), - language: 'plaintext', - ), - BlockquoteConfig( - sideColor: materialTheme.colorScheme.primary.withValues( - alpha: 0.35, - ), - textColor: theme.textSecondary, - sideWith: BorderWidth.micro, - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - margin: const EdgeInsets.symmetric(vertical: Spacing.sm), - ), - ListConfig(marginLeft: Spacing.lg, marginBottom: Spacing.xs), - TableConfig( - border: TableBorder.all( - color: borderColor, - width: BorderWidth.micro, - ), - headPadding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), - bodyPadding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), - headerStyle: secondaryBody.copyWith( - fontWeight: FontWeight.w600, - ), - bodyStyle: secondaryBody, - headerRowDecoration: BoxDecoration( - color: theme.surfaceBackground.withValues(alpha: 0.35), - ), - bodyRowDecoration: BoxDecoration( - color: theme.surfaceContainer.withValues(alpha: 0.2), - ), - ), - HrConfig(color: theme.dividerColor, height: BorderWidth.small), - ImgConfig( - builder: (url, _) { - return Builder( - builder: (context) { - final uri = Uri.tryParse(url); - if (uri == null) { - return _buildImageError( - context, - context.conduitTheme, - ); - } - return _buildImage(context, uri); - }, - ); - }, - ), - ], + return MarkdownConfig( + configs: [ + PConfig(textStyle: baseBody), + H1Config( + style: AppTypography.headlineLargeStyle.copyWith( + color: theme.textPrimary, + ), + ), + H2Config( + style: AppTypography.headlineMediumStyle.copyWith( + color: theme.textPrimary, + ), + ), + H3Config( + style: AppTypography.headlineSmallStyle.copyWith( + color: theme.textPrimary, + ), + ), + H4Config( + style: AppTypography.bodyLargeStyle.copyWith( + color: theme.textPrimary, + ), + ), + H5Config(style: baseBody.copyWith(fontWeight: FontWeight.w600)), + H6Config(style: secondaryBody), + LinkConfig( + style: baseBody.copyWith( + color: material.colorScheme.primary, + decoration: TextDecoration.underline, + decorationColor: material.colorScheme.primary, + ), + onTap: (url) => onTapLink?.call(url, url), + ), + CodeConfig( + style: AppTypography.codeStyle.copyWith( + color: theme.codeText, + backgroundColor: codeBackground, + ), + ), + PreConfig( + textStyle: AppTypography.codeStyle.copyWith(color: theme.codeText), + styleNotMatched: AppTypography.codeStyle.copyWith( + color: theme.codeText, + ), + theme: highlightTheme, + builder: (code, language) { + final normalizedLanguage = language.trim().isEmpty + ? 'plaintext' + : language.trim(); + final highlight = HighlightView( + code, + language: normalizedLanguage == 'plaintext' + ? null + : normalizedLanguage, + theme: highlightTheme, + textStyle: AppTypography.codeStyle.copyWith( + color: theme.codeText, + ), + padding: EdgeInsets.zero, ); + return _buildCodeWrapper( + context: context, + child: highlight, + backgroundColor: codeBackground, + borderColor: borderColor, + language: normalizedLanguage, + rawCode: code, + ); + }, + ), + BlockquoteConfig( + sideColor: material.colorScheme.primary.withValues(alpha: 0.35), + textColor: theme.textSecondary, + sideWith: BorderWidth.small, + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + margin: const EdgeInsets.symmetric(vertical: Spacing.sm), + ), + ListConfig(marginLeft: Spacing.lg, marginBottom: Spacing.xs), + TableConfig( + border: TableBorder.all(color: borderColor, width: BorderWidth.micro), + headPadding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + bodyPadding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + headerStyle: secondaryBody.copyWith(fontWeight: FontWeight.w600), + bodyStyle: secondaryBody, + headerRowDecoration: BoxDecoration( + color: theme.surfaceBackground.withValues(alpha: 0.35), + ), + bodyRowDecoration: BoxDecoration( + color: theme.surfaceContainer.withValues(alpha: 0.2), + ), + ), + HrConfig(color: theme.dividerColor, height: BorderWidth.small), + ImgConfig( + builder: (url, attributes) { + final uri = Uri.tryParse(url); + if (uri == null) { + return _buildImageError(context, theme); + } + return _buildImage(context, uri); + }, + ), + ], + ); + } - return ConduitMarkdownTheme( - config: markdownConfig, - inlineSyntaxes: [latex.syntax()], - blockSyntaxes: const [], + static MarkdownGenerator _buildGenerator(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final latex = ConduitLatex(); + return MarkdownGenerator( + inlineSyntaxList: latex.syntaxes(), generators: [latex.generator(isDark: isDark)], - linesMargin: const EdgeInsets.only(bottom: Spacing.sm), + linesMargin: const EdgeInsets.symmetric(vertical: Spacing.xs), ); } @@ -212,6 +229,67 @@ class ConduitMarkdownConfig { }; } + static Widget _buildCodeWrapper({ + required BuildContext context, + required Widget child, + required Color backgroundColor, + required Color borderColor, + required String language, + required String rawCode, + }) { + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth.isFinite + ? constraints.maxWidth + : MediaQuery.sizeOf(context).width; + + return Container( + margin: const EdgeInsets.symmetric(vertical: Spacing.xs), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(AppBorderRadius.sm), + border: Border.all(color: borderColor, width: BorderWidth.micro), + ), + child: ClipRect( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CodeBlockHeader( + language: language, + onCopy: () async { + await Clipboard.setData(ClipboardData(text: rawCode)); + if (!context.mounted) { + return; + } + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Code copied to clipboard.'), + ), + ); + }, + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: IntrinsicWidth( + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: width), + child: Padding( + padding: const EdgeInsets.all(Spacing.sm), + child: child, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + static Widget buildMermaidBlock(BuildContext context, String code) { final conduitTheme = context.conduitTheme; final materialTheme = Theme.of(context); @@ -299,6 +377,13 @@ class ConduitMarkdownConfig { ), ), errorWidget: (context, url, error) => _buildImageError(context, theme), + imageBuilder: (context, imageProvider) => Container( + margin: const EdgeInsets.symmetric(vertical: Spacing.sm), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + image: DecorationImage(image: imageProvider, fit: BoxFit.contain), + ), + ), ); } @@ -486,73 +571,70 @@ class _MermaidDiagramState extends State { String _buildHtml(String code, String script) { final theme = widget.brightness == Brightness.dark ? 'dark' : 'default'; - final encoded = jsonEncode(code); final primary = _toHex(widget.tokens.brandTone60); final secondary = _toHex(widget.tokens.accentTeal60); final background = _toHex(widget.tokens.codeBackground); final onBackground = _toHex(widget.tokens.codeText); - final lineColor = _toHex(widget.tokens.codeAccent); - final errorColor = _toHex(widget.tokens.statusError60); return ''' - - - - - - - -
- - + + + + + +
+
$code
+
+ + + '''; } String _toHex(Color color) { - final value = color.toARGB32(); - return '#' - '${((value >> 16) & 0xFF).toRadixString(16).padLeft(2, '0')}' - '${((value >> 8) & 0xFF).toRadixString(16).padLeft(2, '0')}' - '${(value & 0xFF).toRadixString(16).padLeft(2, '0')}' - .toUpperCase(); + int channel(double value) { + final scaled = (value * 255).round(); + if (scaled < 0) { + return 0; + } + if (scaled > 255) { + return 255; + } + return scaled; + } + + final argb = + (channel(color.a) << 24) | + (channel(color.r) << 16) | + (channel(color.g) << 8) | + channel(color.b); + return '#${argb.toRadixString(16).padLeft(8, '0')}'; } } diff --git a/lib/shared/widgets/markdown/markdown_latex.dart b/lib/shared/widgets/markdown/markdown_latex.dart index a04debc..6dcf04a 100644 --- a/lib/shared/widgets/markdown/markdown_latex.dart +++ b/lib/shared/widgets/markdown/markdown_latex.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_math_fork/flutter_math.dart'; import 'package:markdown/markdown.dart' as m; import 'package:markdown_widget/markdown_widget.dart'; -import '../../theme/theme_extensions.dart'; +import 'latex_block_widget.dart'; const String _latexTag = 'latex'; @@ -12,7 +11,10 @@ class ConduitLatex { const ConduitLatex(); /// Returns the inline syntax used to identify LaTeX segments. - m.InlineSyntax syntax() => _LatexSyntax(); + List syntaxes() => [ + _LatexDollarSyntax(), + _LatexEscapedSyntax(), + ]; /// Returns the span generator that renders LaTeX expressions. SpanNodeGeneratorWithTag generator({required bool isDark}) { @@ -30,28 +32,57 @@ class ConduitLatex { } } -class _LatexSyntax extends m.InlineSyntax { - _LatexSyntax() : super(r'(\$\$[\s\S]+?\$\$)|(\$[^\n]+?\$)'); +class _LatexDollarSyntax extends m.InlineSyntax { + _LatexDollarSyntax() + : super( + r'(\$\$[\s\S]+?\$\$)|(\$[^\n]+?\$)', + startCharacter: r'$'.codeUnitAt(0), + ); @override bool onMatch(m.InlineParser parser, Match match) { - final raw = match.input.substring(match.start, match.end); - final element = m.Element.text(_latexTag, raw); - if (raw.startsWith(r'$$') && raw.endsWith(r'$$') && raw.length > 4) { - element.attributes['content'] = raw.substring(2, raw.length - 2); - element.attributes['isInline'] = 'false'; - } else if (raw.startsWith(r'$') && raw.endsWith(r'$') && raw.length > 2) { - element.attributes['content'] = raw.substring(1, raw.length - 1); - element.attributes['isInline'] = 'true'; - } else { - element.attributes['content'] = raw; - element.attributes['isInline'] = 'true'; - } - parser.addNode(element); - return true; + return _handleMatch(parser, match.input.substring(match.start, match.end)); } } +class _LatexEscapedSyntax extends m.InlineSyntax { + _LatexEscapedSyntax() + : super( + r'(\\\\\([\s\S]+?\\\\\))|(\\\\\[[\s\S]+?\\\\\])', + startCharacter: r'\'.codeUnitAt(0), + ); + + @override + bool onMatch(m.InlineParser parser, Match match) { + return _handleMatch(parser, match.input.substring(match.start, match.end)); + } +} + +bool _handleMatch(m.InlineParser parser, String raw) { + final element = m.Element.text(_latexTag, raw); + String content = raw; + var isInline = true; + + if (raw.startsWith(r'$$') && raw.endsWith(r'$$') && raw.length > 4) { + content = raw.substring(2, raw.length - 2); + isInline = false; + } else if (raw.startsWith(r'$') && raw.endsWith(r'$') && raw.length > 2) { + content = raw.substring(1, raw.length - 1); + isInline = true; + } else if (raw.startsWith(r'\\(') && raw.endsWith(r'\\)') && raw.length > 4) { + content = raw.substring(2, raw.length - 2); + isInline = true; + } else if (raw.startsWith(r'\\[') && raw.endsWith(r'\\]') && raw.length > 4) { + content = raw.substring(2, raw.length - 2); + isInline = false; + } + + element.attributes['content'] = content; + element.attributes['isInline'] = '$isInline'; + parser.addNode(element); + return true; +} + class _LatexNode extends SpanNode { _LatexNode({ required this.attributes, @@ -79,23 +110,14 @@ class _LatexNode extends SpanNode { return TextSpan(text: rawText, style: baseStyle); } - final latexWidget = Math.tex( - content, - mathStyle: MathStyle.text, - textStyle: baseStyle, - textScaleFactor: 1, - onErrorFallback: (error) { - return Text(rawText, style: baseStyle.copyWith(color: Colors.red)); - }, + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: LatexBlockWidget( + content: content, + isInline: isInline, + style: baseStyle, + isDark: isDark, + ), ); - - final widget = isInline - ? latexWidget - : Padding( - padding: const EdgeInsets.symmetric(vertical: Spacing.xs), - child: Center(child: latexWidget), - ); - - return WidgetSpan(alignment: PlaceholderAlignment.middle, child: widget); } } diff --git a/lib/shared/widgets/markdown/streaming_markdown_widget.dart b/lib/shared/widgets/markdown/streaming_markdown_widget.dart index aa5b50c..a583b3b 100644 --- a/lib/shared/widgets/markdown/streaming_markdown_widget.dart +++ b/lib/shared/widgets/markdown/streaming_markdown_widget.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:markdown_widget/markdown_widget.dart'; + import '../../theme/theme_extensions.dart'; import 'markdown_config.dart'; import 'markdown_preprocessor.dart'; @@ -23,20 +25,23 @@ class StreamingMarkdownWidget extends StatelessWidget { } final normalized = ConduitMarkdownPreprocessor.normalize(content); - final markdownTheme = ConduitMarkdownConfig.resolve( + final mermaidRegex = RegExp(r'```mermaid\s*([\s\S]*?)```', multiLine: true); + final matches = mermaidRegex.allMatches(normalized).toList(); + final renderComponents = ConduitMarkdown.prepare( context, onTapLink: onTapLink, ); - final generator = markdownTheme.createGenerator(); - final mermaidRegex = RegExp(r'```mermaid\s*([\s\S]*?)```', multiLine: true); - final matches = mermaidRegex.allMatches(normalized).toList(); - List buildMarkdownBlocks(String data) { - return generator.buildWidgets(data, config: markdownTheme.config); + Widget buildMarkdown(String data) { + return MarkdownBlock( + data: data, + selectable: false, + config: renderComponents.config, + generator: renderComponents.generator, + ); } if (matches.isEmpty) { - final blocks = buildMarkdownBlocks(normalized); return SelectionArea( child: Theme( data: Theme.of(context).copyWith( @@ -44,10 +49,7 @@ class StreamingMarkdownWidget extends StatelessWidget { cursorColor: context.conduitTheme.buttonPrimary, ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: blocks, - ), + child: buildMarkdown(normalized), ), ); } @@ -57,12 +59,12 @@ class StreamingMarkdownWidget extends StatelessWidget { for (final match in matches) { final before = normalized.substring(currentIndex, match.start); if (before.trim().isNotEmpty) { - children.addAll(buildMarkdownBlocks(before)); + children.add(buildMarkdown(before)); } final code = match.group(1)?.trim() ?? ''; if (code.isNotEmpty) { - children.add(ConduitMarkdownConfig.buildMermaidBlock(context, code)); + children.add(ConduitMarkdown.buildMermaidBlock(context, code)); } currentIndex = match.end; @@ -70,7 +72,7 @@ class StreamingMarkdownWidget extends StatelessWidget { final tail = normalized.substring(currentIndex); if (tail.trim().isNotEmpty) { - children.addAll(buildMarkdownBlocks(tail)); + children.add(buildMarkdown(tail)); } return SelectionArea(