From 661a32866f0788a7cbba2c763a9b77e4ab77c71c Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Sun, 5 Oct 2025 23:36:14 +0530 Subject: [PATCH] refactor: enhance markdown parsing for
tags - Implemented custom block syntax for
tags in the markdown parser to prevent rendering issues during streaming. - Updated the assistant message widget to leverage the new
handling, eliminating the need for manual tag management. - Added a details builder to ensure
elements are processed correctly without causing character flashing. --- .../widgets/assistant_message_widget.dart | 11 +-- .../widgets/markdown/markdown_config.dart | 80 +++++++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index b159d32..1a614be 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -753,14 +753,9 @@ class _AssistantMessageWidgetState extends ConsumerState '', ); - // If there's an unclosed
, drop the tail to avoid raw tags. - final lastOpen = cleaned.lastIndexOf('= 0) { - final tail = cleaned.substring(lastOpen); - if (!tail.contains('
')) { - cleaned = cleaned.substring(0, lastOpen); - } - } + // Note: The markdown parser now handles
tags via a custom block syntax, + // so they won't be rendered as plain text during streaming. This prevents the + // character flashing issue. // Process images in the remaining text final processedContent = _processContentForImages(cleaned); diff --git a/lib/shared/widgets/markdown/markdown_config.dart b/lib/shared/widgets/markdown/markdown_config.dart index 8432643..6a974d9 100644 --- a/lib/shared/widgets/markdown/markdown_config.dart +++ b/lib/shared/widgets/markdown/markdown_config.dart @@ -42,6 +42,7 @@ class ConduitMarkdown { : null, syntaxHighlighter: _CodeSyntaxHighlighter(context), inlineSyntaxes: _buildInlineSyntaxes(), + blockSyntaxes: _buildBlockSyntaxes(), ); } @@ -143,6 +144,7 @@ class ConduitMarkdown { 'img': _ImageBuilder(context), 'mermaid': _MermaidBuilder(context), 'latex': _LatexBuilder(context), + 'details': _DetailsBuilder(context), }; } @@ -150,6 +152,10 @@ class ConduitMarkdown { return [_LatexInlineSyntax()]; } + static List _buildBlockSyntaxes() { + return [_DetailsBlockSyntax()]; + } + static Widget buildMermaidBlock(BuildContext context, String code) { final conduitTheme = context.conduitTheme; final materialTheme = Theme.of(context); @@ -689,3 +695,77 @@ class _MermaidDiagramState extends State { return '#${argb.toRadixString(16).padLeft(8, '0')}'; } } + +// Details block syntax for parsing
tags +class _DetailsBlockSyntax extends md.BlockSyntax { + @override + RegExp get pattern => RegExp(r'^]*)?>$'); + + @override + md.Node? parse(md.BlockParser parser) { + final match = pattern.firstMatch(parser.current.content); + if (match == null) { + return null; + } + + // Parse attributes from the opening tag + final attributesString = match.group(1) ?? ''; + final attributes = _parseAttributes(attributesString); + + parser.advance(); + + // Find the matching closing tag + String summary = ''; + final contentLines = []; + while (!parser.isDone) { + final line = parser.current.content; + + // Check for closing tag + if (line.trim() == '
') { + parser.advance(); + break; + } + + // Check for summary tag + final summaryMatch = RegExp(r'^(.*?)<\/summary>$').firstMatch(line); + if (summaryMatch != null) { + summary = summaryMatch.group(1) ?? ''; + parser.advance(); + continue; + } + + contentLines.add(line); + parser.advance(); + } + + final element = md.Element('details', [md.Text(contentLines.join('\n'))]); + element.attributes['summary'] = summary; + element.attributes.addAll(attributes); + + return element; + } + + Map _parseAttributes(String attributesString) { + final attributes = {}; + final attrRegex = RegExp(r'(\w+)="([^"]*)"'); + for (final match in attrRegex.allMatches(attributesString)) { + attributes[match.group(1)!] = match.group(2) ?? ''; + } + return attributes; + } +} + +// Details builder for rendering
elements +class _DetailsBuilder extends MarkdownElementBuilder { + _DetailsBuilder(this.context); + + final BuildContext context; + + @override + Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) { + // The details element should not be rendered as markdown during streaming. + // Instead, it's handled by the ReasoningParser in assistant_message_widget. + // Return empty widget to prevent flashing. + return const SizedBox.shrink(); + } +}