diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs
index fce357fb..ea2d28af 100644
--- a/app/MindWork AI Studio/Tools/PlainFileExport.cs
+++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs
@@ -1,28 +1,67 @@
-using System.Diagnostics;
+using System.Text.RegularExpressions;
using AIStudio.Chat;
-using AIStudio.Dialogs;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
-using DialogOptions = MudBlazor.DialogOptions;
namespace AIStudio.Tools;
-public static class PlainFileExport
+public static partial class PlainFileExport
{
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PlainFileExport));
private sealed record ExportTarget(string DisplayName, FileTypeFilter FileType);
private static readonly ExportTarget MARKDOWN = new("Markdown (.md)", FileTypes.MARKDOWN);
+ private static readonly ExportTarget CSV = new("CSV (.csv)", FileTypes.CSV);
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PlainFileExport).Namespace, nameof(PlainFileExport));
+
+ ///
+ /// Extracts the content of the first complete Markdown code block marked as CSV.
+ ///
+ public static bool TryExtractCsvContent(IContent content, out string csvContent)
+ {
+ if (content is ContentText text)
+ {
+ var match = CsvCodeFenceRegex().Match(text.Text);
+ if (match.Success)
+ {
+ csvContent = match.Groups["content"].Value;
+ return true;
+ }
+ }
+
+ csvContent = string.Empty;
+ return false;
+ }
+
+ [GeneratedRegex(
+ """
+ # Matches an opening Markdown code fence and captures its delimiter.
+ ^(?`{3,}|~{3,})
+
+ # Matches the csv language identifier and the end of the opening fence line.
+ # Also allow tab-separated, pipe-separated and semicolon separated values at the code fence
+ [ \t]*(csv|tsv|psv|ssv)[ \t]*\r?\n
+
+ # Captures the content of the first matching fenced code block.
+ (?[\s\S]*?)
+
+ # Matches the corresponding closing fence followed by a line ending or end of input.
+ ^\k[ \t]*(?=\r?\n|$)
+ """,
+ RegexOptions.IgnoreCase |
+ RegexOptions.Multiline |
+ RegexOptions.IgnorePatternWhitespace)]
+ private static partial Regex CsvCodeFenceRegex();
public static async Task ToFile(RustService rustService, FileExportFormat format, IContent markdownContent)
{
var exportTarget = format switch
{
FileExportFormat.MARKDOWN => MARKDOWN,
+ FileExportFormat.CSV => CSV,
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
};
@@ -37,14 +76,19 @@ public static class PlainFileExport
try
{
- var markdownText = markdownContent switch
+ var fileContent = format switch
{
- ContentText text => text.Text,
- ContentImage _ => "Image export is not yet possible.",
- _ => "Unknown content type. Cannot export document."
+ FileExportFormat.MARKDOWN => markdownContent switch
+ {
+ ContentText text => text.Text,
+ ContentImage _ => "Image export is not yet possible.",
+ _ => "Unknown content type. Cannot export document."
+ },
+ FileExportFormat.CSV when TryExtractCsvContent(markdownContent, out var csvContent) => csvContent,
+ _ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
};
- await File.WriteAllTextAsync(response.SaveFilePath, markdownText);
+ await File.WriteAllTextAsync(response.SaveFilePath, fileContent);
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Document export successful")));
return true;