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;