Fix standalone HTML and LaTeX exports

This commit is contained in:
Peer Hogeterp committed 2026-09-22 15:11:45 +02:00
1 parent c95cc5bacc
commit a1d8169284
3 files changed
+71 -3

No files matched your search

@@ -1,6 +1,7 @@
using System.Text;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Markdig.Extensions.Tables;
@@ -15,6 +16,42 @@ public static class PlainFileExport
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PlainFileExport).Namespace, nameof(PlainFileExport));
/// <summary>
/// Reads a complete HTML or LaTeX source file out of a message which consists only of the
/// matching fenced code block.
/// </summary>
/// <param name="markdown">The Markdown text the model wrote.</param>
/// <param name="format">The source format to extract.</param>
/// <param name="source">The contents inside the fence, or an empty string when the message is
/// not a standalone source file.</param>
/// <returns>True, when the message is a complete source file for the selected format.</returns>
internal static bool TryExtractStandaloneSource(string markdown, FileExportFormat format, out string source)
{
source = string.Empty;
var languages = format switch
{
FileExportFormat.HTML => new[] { "html" },
FileExportFormat.LATEX => new[] { "latex", "tex" },
_ => [],
};
if (languages.Length is 0 || string.IsNullOrWhiteSpace(markdown))
return false;
var document = Markdig.Markdown.Parse(markdown, Markdown.SAFE_MARKDOWN_PIPELINE);
if (document.Count is not 1 || document[0] is not FencedCodeBlock block || block.ClosingFencedCharCount is 0)
return false;
var language = block.Info?.Trim();
if (!languages.Any(candidate => string.Equals(candidate, language, StringComparison.OrdinalIgnoreCase)))
return false;
source = block.Lines.ToString();
return true;
}
/// <summary>
/// Reads every table a message holds, in the order they appear in it.
/// </summary>
@@ -183,6 +220,30 @@ public static class PlainFileExport
if (format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter)
throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format itself.");
return await WriteFile(rustService, dialogTitle, format, fileContent, fileTypeFilter, fileName);
}
/// <summary>
/// Writes HTML or LaTeX source authored by the model without converting it.
/// </summary>
/// <param name="rustService">The Rust service, used for the save dialog.</param>
/// <param name="dialogTitle">The title of the save dialog.</param>
/// <param name="format">The matching HTML or LaTeX format.</param>
/// <param name="source">The source text inside the model's code fence.</param>
/// <returns>True, when the file was written.</returns>
internal static async Task<bool> ToSourceFile(RustService rustService, string dialogTitle, FileExportFormat format, string source)
{
if (format is not (FileExportFormat.HTML or FileExportFormat.LATEX) || format.ToFileTypeFilter() is not { } fileTypeFilter)
throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format as an authored source file.");
return await WriteFile(rustService, dialogTitle, format, source, fileTypeFilter);
}
/// <summary>
/// Lets the user choose a path and writes text with the encoding of its format.
/// </summary>
private static async Task<bool> WriteFile(RustService rustService, string dialogTitle, FileExportFormat format, string fileContent, FileTypeFilter fileTypeFilter, string? fileName = null)
{
var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName(fileName));
if (response.UserCancelled)
{
@@ -206,4 +267,4 @@ public static class PlainFileExport
return false;
}
}
}
}