From fdaa12ab5bfbcd35284a4f8dce6f9d2d5026a33b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 23 Sep 2026 13:11:53 +0200 Subject: [PATCH] Map code fence languages to export formats in one place --- .../Chat/ContentBlockComponent.razor.cs | 2 +- .../Tools/FileExportFormatExtensions.cs | 30 +++++++++++++++++ .../Tools/PlainFileExport.cs | 12 ++----- app/Tests/Tools/FileExportFormatTests.cs | 33 +++++++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 0b003c53..4dc68e92 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -873,4 +873,4 @@ public partial class ContentBlockComponent : MSGComponentBase await this.DisposeMathContainerIfNeededAsync(); } -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs index 2fde6b79..5a22f93b 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -103,6 +103,36 @@ public static class FileExportFormatExtensions _ => string.Empty, }; + /// + /// Reads which format a model means when it names a language behind the opening fence of a + /// code block. + /// + /// + /// A model which answers with a finished file puts it into a code block and names its language, + /// as in ```html. Models do not agree on the spelling, so we accept the usual names of a format + /// in any case. Only formats which are plain text appear here: a code block holds text, never + /// a Word document. + /// + /// The language behind the opening fence, as Markdig reads it into + /// FencedCodeBlock.Info. + /// The format the language names, or NONE when it names none of ours. + /// True, when the language names a format AI Studio writes. + public static bool TryFromCodeFenceLanguage(string? language, out FileExportFormat format) + { + format = language?.Trim().ToLowerInvariant() switch + { + "html" => FileExportFormat.HTML, + "latex" or "tex" => FileExportFormat.LATEX, + "markdown" or "md" => FileExportFormat.MARKDOWN, + "csv" => FileExportFormat.CSV, + "tsv" => FileExportFormat.TSV, + + _ => FileExportFormat.NONE, + }; + + return format is not FileExportFormat.NONE; + } + /// /// Returns the file name the save dialog starts with. /// diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index e2e1e788..a3ccf1fa 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -94,15 +94,7 @@ public static class PlainFileExport /// private static (string Fallback, FileExportFormat Format, string Text)? ToContent(FencedCodeBlock block) { - var format = block.Info?.Trim() switch - { - "csv" => FileExportFormat.CSV, - "tsv" => FileExportFormat.TSV, - - _ => FileExportFormat.NONE, - }; - - if (format is FileExportFormat.NONE) + if (!FileExportFormatExtensions.TryFromCodeFenceLanguage(block.Info, out var format) || format is not (FileExportFormat.CSV or FileExportFormat.TSV)) return null; var content = block.Lines.ToString(); @@ -206,4 +198,4 @@ public static class PlainFileExport return false; } } -} +} \ No newline at end of file diff --git a/app/Tests/Tools/FileExportFormatTests.cs b/app/Tests/Tools/FileExportFormatTests.cs index 46e8cb9f..a15f0671 100644 --- a/app/Tests/Tools/FileExportFormatTests.cs +++ b/app/Tests/Tools/FileExportFormatTests.cs @@ -36,4 +36,37 @@ public sealed class FileExportFormatTests FileExportFormat.HTML, }), "A format was added to or removed from the export menu: say in FollowsPageAnchors whether its reader follows a page in a local link, then name it here."); } + + [TestCase("html", FileExportFormat.HTML)] + [TestCase("latex", FileExportFormat.LATEX)] + [TestCase("tex", FileExportFormat.LATEX)] + [TestCase("markdown", FileExportFormat.MARKDOWN)] + [TestCase("md", FileExportFormat.MARKDOWN)] + [TestCase("csv", FileExportFormat.CSV)] + [TestCase("tsv", FileExportFormat.TSV)] + [TestCase("HTML", FileExportFormat.HTML, Description = "Models do not agree on the case.")] + [TestCase("LaTeX", FileExportFormat.LATEX)] + [TestCase("CSV", FileExportFormat.CSV)] + [TestCase(" md ", FileExportFormat.MARKDOWN, Description = "Space around the name is no part of it.")] + public void AFenceLanguageNamesItsFormat(string language, FileExportFormat expectedFormat) + { + Assert.Multiple(() => + { + Assert.That(FileExportFormatExtensions.TryFromCodeFenceLanguage(language, out var format), Is.True); + Assert.That(format, Is.EqualTo(expectedFormat)); + }); + } + + [TestCase("css", TestName = "A language AI Studio writes no file for")] + [TestCase("docx", TestName = "A format no code block can hold")] + [TestCase("", TestName = "A fence without a language")] + [TestCase(null, TestName = "A fence Markdig read no language for")] + public void AnyOtherFenceLanguageNamesNoFormat(string? language) + { + Assert.Multiple(() => + { + Assert.That(FileExportFormatExtensions.TryFromCodeFenceLanguage(language, out var format), Is.False); + Assert.That(format, Is.EqualTo(FileExportFormat.NONE)); + }); + } } \ No newline at end of file