refactor: enhance markdown parsing for <details> tags
- Implemented custom block syntax for <details> tags in the markdown parser to prevent rendering issues during streaming. - Updated the assistant message widget to leverage the new <details> handling, eliminating the need for manual tag management. - Added a details builder to ensure <details> elements are processed correctly without causing character flashing.
This commit is contained in:
@@ -753,14 +753,9 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
// If there's an unclosed <details>, drop the tail to avoid raw tags.
|
// Note: The markdown parser now handles <details> tags via a custom block syntax,
|
||||||
final lastOpen = cleaned.lastIndexOf('<details');
|
// so they won't be rendered as plain text during streaming. This prevents the
|
||||||
if (lastOpen >= 0) {
|
// character flashing issue.
|
||||||
final tail = cleaned.substring(lastOpen);
|
|
||||||
if (!tail.contains('</details>')) {
|
|
||||||
cleaned = cleaned.substring(0, lastOpen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process images in the remaining text
|
// Process images in the remaining text
|
||||||
final processedContent = _processContentForImages(cleaned);
|
final processedContent = _processContentForImages(cleaned);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class ConduitMarkdown {
|
|||||||
: null,
|
: null,
|
||||||
syntaxHighlighter: _CodeSyntaxHighlighter(context),
|
syntaxHighlighter: _CodeSyntaxHighlighter(context),
|
||||||
inlineSyntaxes: _buildInlineSyntaxes(),
|
inlineSyntaxes: _buildInlineSyntaxes(),
|
||||||
|
blockSyntaxes: _buildBlockSyntaxes(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +144,7 @@ class ConduitMarkdown {
|
|||||||
'img': _ImageBuilder(context),
|
'img': _ImageBuilder(context),
|
||||||
'mermaid': _MermaidBuilder(context),
|
'mermaid': _MermaidBuilder(context),
|
||||||
'latex': _LatexBuilder(context),
|
'latex': _LatexBuilder(context),
|
||||||
|
'details': _DetailsBuilder(context),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +152,10 @@ class ConduitMarkdown {
|
|||||||
return [_LatexInlineSyntax()];
|
return [_LatexInlineSyntax()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<md.BlockSyntax> _buildBlockSyntaxes() {
|
||||||
|
return [_DetailsBlockSyntax()];
|
||||||
|
}
|
||||||
|
|
||||||
static Widget buildMermaidBlock(BuildContext context, String code) {
|
static Widget buildMermaidBlock(BuildContext context, String code) {
|
||||||
final conduitTheme = context.conduitTheme;
|
final conduitTheme = context.conduitTheme;
|
||||||
final materialTheme = Theme.of(context);
|
final materialTheme = Theme.of(context);
|
||||||
@@ -689,3 +695,77 @@ class _MermaidDiagramState extends State<MermaidDiagram> {
|
|||||||
return '#${argb.toRadixString(16).padLeft(8, '0')}';
|
return '#${argb.toRadixString(16).padLeft(8, '0')}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Details block syntax for parsing <details> tags
|
||||||
|
class _DetailsBlockSyntax extends md.BlockSyntax {
|
||||||
|
@override
|
||||||
|
RegExp get pattern => RegExp(r'^<details(\s+[^>]*)?>$');
|
||||||
|
|
||||||
|
@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 = <String>[];
|
||||||
|
while (!parser.isDone) {
|
||||||
|
final line = parser.current.content;
|
||||||
|
|
||||||
|
// Check for closing tag
|
||||||
|
if (line.trim() == '</details>') {
|
||||||
|
parser.advance();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for summary tag
|
||||||
|
final summaryMatch = RegExp(r'^<summary>(.*?)<\/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<String, String> _parseAttributes(String attributesString) {
|
||||||
|
final attributes = <String, String>{};
|
||||||
|
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 <details> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user