fix(converters): fix HtmlProvider WebView2 thread crash and add MarkdownProvider for pure .NET conversions
This commit is contained in:
parent
63d50c165d
commit
4e5285e27c
7 changed files with 447 additions and 28 deletions
|
|
@ -17,17 +17,22 @@ namespace Everything2Everything.Core.Converters;
|
|||
// txt ↔ md → trivial copy
|
||||
public sealed class DocumentProvider : IConverterProvider
|
||||
{
|
||||
private static readonly string[] Inputs =
|
||||
{ ".html", ".htm", ".hwp", ".hwpx", ".docx", ".doc", ".md", ".markdown", ".txt" };
|
||||
|
||||
private static readonly string[] Outputs = { ".html", ".docx", ".md", ".txt" };
|
||||
private static readonly string[] OfficeInputs = { ".docx", ".doc", ".hwp", ".hwpx" };
|
||||
private static readonly string[] OfficeOutputs = { ".html", ".docx", ".md", ".txt" };
|
||||
private static readonly string[] TextInputsToDocx = { ".html", ".htm", ".md", ".markdown", ".txt" };
|
||||
|
||||
public ProviderCapability Capability { get; } = new(
|
||||
Id: "document",
|
||||
DisplayName: "문서 텍스트 변환 (HTML/HWP/DOCX/MD/TXT)",
|
||||
SupportedConversions: ProviderCapability.PairsFromMatrix(Inputs, Outputs),
|
||||
DisplayName: "문서 텍스트 변환 (DOCX/HWP/오피스)",
|
||||
SupportedConversions: ProviderCapability.PairsFromMatrix(OfficeInputs, OfficeOutputs)
|
||||
.Concat(ProviderCapability.PairsFromMatrix(TextInputsToDocx, new[] { ".docx" }))
|
||||
.Concat(new[]
|
||||
{
|
||||
new ConversionPair(".html", ".txt", LossClass.Recode),
|
||||
new ConversionPair(".htm", ".txt", LossClass.Recode),
|
||||
}).ToList(),
|
||||
Status: ProviderStatus.RequiresExternal,
|
||||
Summary: "HTML·HWP·DOCX·Markdown·TXT 사이의 양방향 텍스트 변환 (LibreOffice + Markdig + ReverseMarkdown).",
|
||||
Summary: "DOCX·HWP 문서를 HTML/DOCX/MD/TXT로 변환하거나 텍스트를 DOCX로 저장합니다 (LibreOffice 엔진).",
|
||||
ExternalDependencies: new[]
|
||||
{
|
||||
new ExternalDependency(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Threading;
|
||||
using Everything2Everything.Core.Providers;
|
||||
using ImageMagick;
|
||||
|
|
@ -165,7 +166,25 @@ public sealed class HtmlProvider : IConverterProvider
|
|||
try
|
||||
{
|
||||
var dispatcher = Dispatcher.CurrentDispatcher;
|
||||
_ = RunCaptureOnDispatcher(capture, dispatcher, sourcePath, options, progress, cancellationToken, tcs);
|
||||
SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(dispatcher));
|
||||
|
||||
dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = await RunCaptureOnDispatcher(capture, sourcePath, options, progress, cancellationToken);
|
||||
tcs.TrySetResult(bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
|
||||
}
|
||||
});
|
||||
|
||||
Dispatcher.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -181,16 +200,15 @@ public sealed class HtmlProvider : IConverterProvider
|
|||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static async Task RunCaptureOnDispatcher(
|
||||
private static async Task<byte[]> RunCaptureOnDispatcher(
|
||||
Func<CoreWebView2, IProgress<double>?, Task<byte[]>> capture,
|
||||
Dispatcher dispatcher,
|
||||
string sourcePath,
|
||||
ConvertOptions options,
|
||||
IProgress<double>? progress,
|
||||
CancellationToken cancellationToken,
|
||||
TaskCompletionSource<byte[]> tcs)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
CoreWebView2Controller? controller = null;
|
||||
HwndSource? hwndSource = null;
|
||||
try
|
||||
{
|
||||
var userDataFolder = Path.Combine(
|
||||
|
|
@ -198,15 +216,23 @@ public sealed class HtmlProvider : IConverterProvider
|
|||
"Everything2Everything", "WebView2");
|
||||
Directory.CreateDirectory(userDataFolder);
|
||||
|
||||
var env = await CoreWebView2Environment.CreateAsync(null, userDataFolder).ConfigureAwait(true);
|
||||
var env = await CoreWebView2Environment.CreateAsync(null, userDataFolder);
|
||||
progress?.Report(0.15);
|
||||
|
||||
controller = await env.CreateCoreWebView2ControllerAsync(new IntPtr(-3)).ConfigureAwait(true);
|
||||
|
||||
int width = options.HtmlRender.ViewportWidth > 0 ? options.HtmlRender.ViewportWidth : 1280;
|
||||
int height = options.HtmlRender.ViewportHeight ?? 720;
|
||||
|
||||
var parameters = new HwndSourceParameters("WebView2HiddenHost")
|
||||
{
|
||||
WindowStyle = 0x800000,
|
||||
Width = width,
|
||||
Height = height,
|
||||
};
|
||||
hwndSource = new HwndSource(parameters);
|
||||
|
||||
controller = await env.CreateCoreWebView2ControllerAsync(hwndSource.Handle);
|
||||
controller.Bounds = new System.Drawing.Rectangle(0, 0, width, height);
|
||||
controller.IsVisible = false;
|
||||
controller.IsVisible = true;
|
||||
|
||||
var web = controller.CoreWebView2;
|
||||
progress?.Report(0.3);
|
||||
|
|
@ -217,36 +243,31 @@ public sealed class HtmlProvider : IConverterProvider
|
|||
{
|
||||
web.NavigationCompleted -= navHandler!;
|
||||
if (e.IsSuccess) navTcs.TrySetResult(true);
|
||||
else navTcs.TrySetException(new InvalidOperationException(
|
||||
$"내비게이션 실패: {e.WebErrorStatus}"));
|
||||
else navTcs.TrySetException(new InvalidOperationException($"내비게이션 실패: {e.WebErrorStatus}"));
|
||||
};
|
||||
web.NavigationCompleted += navHandler;
|
||||
|
||||
var fileUri = new Uri(sourcePath).AbsoluteUri;
|
||||
var fileUri = new Uri(Path.GetFullPath(sourcePath)).AbsoluteUri;
|
||||
web.Navigate(fileUri);
|
||||
|
||||
using (cancellationToken.Register(() => navTcs.TrySetCanceled()))
|
||||
{
|
||||
await navTcs.Task.ConfigureAwait(true);
|
||||
await navTcs.Task;
|
||||
}
|
||||
progress?.Report(0.5);
|
||||
|
||||
if (options.HtmlRender.WaitMilliseconds > 0)
|
||||
await Task.Delay(options.HtmlRender.WaitMilliseconds, cancellationToken).ConfigureAwait(true);
|
||||
await Task.Delay(options.HtmlRender.WaitMilliseconds, cancellationToken);
|
||||
|
||||
progress?.Report(0.65);
|
||||
|
||||
var bytes = await capture(web, progress).ConfigureAwait(true);
|
||||
tcs.TrySetResult(bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
var bytes = await capture(web, progress);
|
||||
return bytes;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { controller?.Close(); } catch { }
|
||||
dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
|
||||
try { hwndSource?.Dispose(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
117
src/Everything2Everything.Core/Converters/MarkdownProvider.cs
Normal file
117
src/Everything2Everything.Core/Converters/MarkdownProvider.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Everything2Everything.Core.Providers;
|
||||
using Markdig;
|
||||
|
||||
namespace Everything2Everything.Core.Converters;
|
||||
|
||||
public sealed class MarkdownProvider : IConverterProvider
|
||||
{
|
||||
private static readonly string[] MdInputs = { ".html", ".htm", ".md", ".markdown", ".txt" };
|
||||
|
||||
public ProviderCapability Capability { get; } = new(
|
||||
Id: "markdown",
|
||||
DisplayName: "마크다운 / HTML 텍스트",
|
||||
SupportedConversions: new[]
|
||||
{
|
||||
new ConversionPair(".html", ".md", LossClass.Recode),
|
||||
new ConversionPair(".htm", ".md", LossClass.Recode),
|
||||
new ConversionPair(".md", ".html", LossClass.Recode),
|
||||
new ConversionPair(".markdown", ".html", LossClass.Recode),
|
||||
new ConversionPair(".md", ".txt", LossClass.Container),
|
||||
new ConversionPair(".txt", ".md", LossClass.Container),
|
||||
new ConversionPair(".txt", ".html", LossClass.Recode),
|
||||
},
|
||||
Status: ProviderStatus.Available,
|
||||
Summary: "HTML·Markdown·TXT 사이의 순수 .NET 양방향 변환 (Markdig + ReverseMarkdown).",
|
||||
ExternalDependencies: Array.Empty<ExternalDependency>(),
|
||||
RoadmapNote: null);
|
||||
|
||||
public Task<ProviderAvailability> CheckAvailabilityAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(ProviderAvailability.Ready);
|
||||
|
||||
public async Task<ConvertResult> ConvertAsync(
|
||||
string sourcePath,
|
||||
string outputDirectory,
|
||||
string outputExtension,
|
||||
ConvertOptions options,
|
||||
IProgress<double>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inExt = Path.GetExtension(sourcePath).ToLowerInvariant();
|
||||
var outExt = ConversionPair.Normalize(outputExtension);
|
||||
var baseName = Path.GetFileNameWithoutExtension(sourcePath);
|
||||
var outPath = OutputPathHelper.ResolveOutputPath(outputDirectory, baseName, null, outExt, options.OnCollision);
|
||||
|
||||
if (OutputPathHelper.ShouldSkip(outPath, options.OnCollision))
|
||||
return ConvertResult.Skip(sourcePath, "기존 파일이 있어 건너뜁니다.");
|
||||
|
||||
progress?.Report(0.1);
|
||||
|
||||
try
|
||||
{
|
||||
if (inExt is ".html" or ".htm" && outExt == ".md")
|
||||
{
|
||||
await HtmlToMdAsync(sourcePath, outPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (inExt is ".md" or ".markdown" && outExt == ".html")
|
||||
{
|
||||
await MdToHtmlAsync(sourcePath, outPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (inExt == ".txt" && outExt == ".html")
|
||||
{
|
||||
await TxtToHtmlAsync(sourcePath, outPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if ((inExt is ".md" or ".markdown" && outExt == ".txt") || (inExt == ".txt" && outExt == ".md"))
|
||||
{
|
||||
await Task.Run(() => File.Copy(sourcePath, outPath, overwrite: true), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ConvertResult.Fail(sourcePath, $"지원하지 않는 변환: {inExt} → {outExt}");
|
||||
}
|
||||
|
||||
progress?.Report(1.0);
|
||||
return ConvertResult.Ok(sourcePath, new[] { outPath });
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ConvertResult.Fail(sourcePath, $"변환 실패: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task MdToHtmlAsync(string mdPath, string htmlPath, CancellationToken ct)
|
||||
{
|
||||
var md = await File.ReadAllTextAsync(mdPath, Encoding.UTF8, ct).ConfigureAwait(false);
|
||||
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
|
||||
var body = Markdown.ToHtml(md, pipeline);
|
||||
var fullHtml = $"<!DOCTYPE html><html lang=\"ko\"><head><meta charset=\"utf-8\"><title>{Path.GetFileNameWithoutExtension(mdPath)}</title></head><body>{body}</body></html>";
|
||||
await File.WriteAllTextAsync(htmlPath, fullHtml, Encoding.UTF8, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task HtmlToMdAsync(string htmlPath, string mdPath, CancellationToken ct)
|
||||
{
|
||||
var html = await File.ReadAllTextAsync(htmlPath, Encoding.UTF8, ct).ConfigureAwait(false);
|
||||
var converter = new ReverseMarkdown.Converter(new ReverseMarkdown.Config
|
||||
{
|
||||
UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.PassThrough,
|
||||
GithubFlavored = true,
|
||||
RemoveComments = true,
|
||||
SmartHrefHandling = true,
|
||||
});
|
||||
var md = converter.Convert(html);
|
||||
await File.WriteAllTextAsync(mdPath, md, Encoding.UTF8, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task TxtToHtmlAsync(string txtPath, string htmlPath, CancellationToken ct)
|
||||
{
|
||||
var txt = await File.ReadAllTextAsync(txtPath, Encoding.UTF8, ct).ConfigureAwait(false);
|
||||
var encoded = System.Net.WebUtility.HtmlEncode(txt).Replace("\n", "<br/>\n");
|
||||
var fullHtml = $"<!DOCTYPE html><html lang=\"ko\"><head><meta charset=\"utf-8\"><title>{Path.GetFileNameWithoutExtension(txtPath)}</title></head><body><pre style=\"font-family: 'Segoe UI', sans-serif; white-space: pre-wrap;\">{encoded}</pre></body></html>";
|
||||
await File.WriteAllTextAsync(htmlPath, fullHtml, Encoding.UTF8, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Converters;
|
||||
using Everything2Everything.Core.Providers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class HtmlEndToEndAllOutputsTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public HtmlEndToEndAllOutputsTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
private static string TempDir()
|
||||
{
|
||||
var d = Path.Combine(Path.GetTempPath(), "e2e_html_all_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(".pdf")]
|
||||
[InlineData(".png")]
|
||||
[InlineData(".jpg")]
|
||||
[InlineData(".webp")]
|
||||
[InlineData(".avif")]
|
||||
[InlineData(".bmp")]
|
||||
[InlineData(".tiff")]
|
||||
[InlineData(".gif")]
|
||||
public async Task Html_To_ImagesAndPdf_Via_Engine_Succeeds(string targetExt)
|
||||
{
|
||||
var dir = TempDir();
|
||||
var html = Path.Combine(dir, "sample.html");
|
||||
await File.WriteAllTextAsync(html, "<html><body><h1 style='color:red;'>Test Page</h1><p>Paragraph content</p></body></html>");
|
||||
|
||||
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||
var opt = new ConvertOptions
|
||||
{
|
||||
OutputLocation = OutputLocation.Custom,
|
||||
CustomOutputDirectory = dir
|
||||
};
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var result = await engine.ConvertOneAsync(html, targetExt, opt, null, cts.Token);
|
||||
|
||||
_output.WriteLine($"Convert .html -> {targetExt}: Status={result.Status}, Msg={result.Message}");
|
||||
Assert.Equal(ConvertStatus.Success, result.Status);
|
||||
Assert.NotEmpty(result.OutputPaths);
|
||||
Assert.True(File.Exists(result.OutputPaths[0]));
|
||||
Assert.True(new FileInfo(result.OutputPaths[0]).Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Html_To_Markdown_Via_Engine_TestsAvailability()
|
||||
{
|
||||
var dir = TempDir();
|
||||
var html = Path.Combine(dir, "sample.html");
|
||||
await File.WriteAllTextAsync(html, "<html><body><h1>Markdown Title</h1><p>Bold <b>text</b></p></body></html>");
|
||||
|
||||
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||
var opt = new ConvertOptions
|
||||
{
|
||||
OutputLocation = OutputLocation.Custom,
|
||||
CustomOutputDirectory = dir
|
||||
};
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
var result = await engine.ConvertOneAsync(html, ".md", opt, null, cts.Token);
|
||||
|
||||
_output.WriteLine($"Convert .html -> .md: Status={result.Status}, Msg={result.Message}");
|
||||
}
|
||||
}
|
||||
60
src/Everything2Everything.Tests/HtmlProviderTests.cs
Normal file
60
src/Everything2Everything.Tests/HtmlProviderTests.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Converters;
|
||||
using Everything2Everything.Core.Providers;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class HtmlProviderTests
|
||||
{
|
||||
private static string TempDir()
|
||||
{
|
||||
var d = Path.Combine(Path.GetTempPath(), "e2e_html_test_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HtmlProvider_ConvertAsync_HtmlToPdf_Succeeds()
|
||||
{
|
||||
var dir = TempDir();
|
||||
var html = Path.Combine(dir, "test.html");
|
||||
await File.WriteAllTextAsync(html, "<html><body><h1>Hello HTML Test</h1><p>Testing HTML to PDF</p></body></html>");
|
||||
|
||||
var provider = new HtmlProvider();
|
||||
var availability = await provider.CheckAvailabilityAsync();
|
||||
if (!availability.IsReady) return;
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var result = await provider.ConvertAsync(html, dir, ".pdf", new ConvertOptions(), null, cts.Token);
|
||||
|
||||
Assert.Equal(ConvertStatus.Success, result.Status);
|
||||
Assert.NotEmpty(result.OutputPaths);
|
||||
Assert.True(File.Exists(result.OutputPaths[0]));
|
||||
Assert.True(new FileInfo(result.OutputPaths[0]).Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HtmlProvider_ConvertAsync_HtmlToPng_Succeeds()
|
||||
{
|
||||
var dir = TempDir();
|
||||
var html = Path.Combine(dir, "test.html");
|
||||
await File.WriteAllTextAsync(html, "<html><body><h1>Hello HTML Test</h1><p>Testing HTML to PNG</p></body></html>");
|
||||
|
||||
var provider = new HtmlProvider();
|
||||
var availability = await provider.CheckAvailabilityAsync();
|
||||
if (!availability.IsReady) return;
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
var result = await provider.ConvertAsync(html, dir, ".png", new ConvertOptions(), null, cts.Token);
|
||||
|
||||
Assert.Equal(ConvertStatus.Success, result.Status);
|
||||
Assert.NotEmpty(result.OutputPaths);
|
||||
Assert.True(File.Exists(result.OutputPaths[0]));
|
||||
Assert.True(new FileInfo(result.OutputPaths[0]).Length > 0);
|
||||
}
|
||||
}
|
||||
79
src/Everything2Everything.Tests/MarkdownProviderTests.cs
Normal file
79
src/Everything2Everything.Tests/MarkdownProviderTests.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Converters;
|
||||
using Everything2Everything.Core.Providers;
|
||||
using Xunit;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class MarkdownProviderTests
|
||||
{
|
||||
private static string TempDir()
|
||||
{
|
||||
var d = Path.Combine(Path.GetTempPath(), "e2e_md_test_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkdownProvider_HtmlToMd_ConvertsProperly()
|
||||
{
|
||||
var dir = TempDir();
|
||||
var html = Path.Combine(dir, "input.html");
|
||||
await File.WriteAllTextAsync(html, "<h1>Title Here</h1><p>Paragraph with <strong>bold</strong> text</p>");
|
||||
|
||||
var provider = new MarkdownProvider();
|
||||
var res = await provider.ConvertAsync(html, dir, ".md", new ConvertOptions(), null, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ConvertStatus.Success, res.Status);
|
||||
var content = await File.ReadAllTextAsync(res.OutputPaths[0]);
|
||||
Assert.Contains("# Title Here", content);
|
||||
Assert.Contains("**bold**", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkdownProvider_MdToHtml_ConvertsProperly()
|
||||
{
|
||||
var dir = TempDir();
|
||||
var md = Path.Combine(dir, "input.md");
|
||||
await File.WriteAllTextAsync(md, "# Hello Markdown\n\nThis is a *test*.");
|
||||
|
||||
var provider = new MarkdownProvider();
|
||||
var res = await provider.ConvertAsync(md, dir, ".html", new ConvertOptions(), null, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ConvertStatus.Success, res.Status);
|
||||
var content = await File.ReadAllTextAsync(res.OutputPaths[0]);
|
||||
Assert.Contains("Hello Markdown</h1>", content);
|
||||
Assert.Contains("<em>test</em>", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkdownProvider_Availability_IsAlwaysReady()
|
||||
{
|
||||
var provider = new MarkdownProvider();
|
||||
var avail = await provider.CheckAvailabilityAsync();
|
||||
Assert.True(avail.IsReady);
|
||||
Assert.Equal(ProviderStatus.Available, provider.Capability.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Engine_HtmlToMd_RoutesToMarkdownProvider()
|
||||
{
|
||||
var dir = TempDir();
|
||||
var html = Path.Combine(dir, "input.html");
|
||||
await File.WriteAllTextAsync(html, "<h2>Section</h2><p>Content</p>");
|
||||
|
||||
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||
var path = engine.Providers.Graph.FindBestPath(".html", ".md");
|
||||
Assert.NotNull(path);
|
||||
Assert.Single(path);
|
||||
Assert.IsType<MarkdownProvider>(path![0].Provider);
|
||||
|
||||
var opt = new ConvertOptions { OutputLocation = OutputLocation.Custom, CustomOutputDirectory = dir };
|
||||
var res = await engine.ConvertOneAsync(html, ".md", opt);
|
||||
Assert.Equal(ConvertStatus.Success, res.Status);
|
||||
}
|
||||
}
|
||||
56
src/Everything2Everything.Tests/MatrixIntegrityTests.cs
Normal file
56
src/Everything2Everything.Tests/MatrixIntegrityTests.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Providers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Everything2Everything.Tests;
|
||||
|
||||
public class MatrixIntegrityTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public MatrixIntegrityTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Audit_AllRegisteredProviders_AndPrintMatrix()
|
||||
{
|
||||
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||
var providers = engine.Providers.All.ToList();
|
||||
|
||||
_output.WriteLine($"=== Total Registered Providers: {providers.Count} ===");
|
||||
foreach (var p in providers)
|
||||
{
|
||||
var cap = p.Capability;
|
||||
_output.WriteLine($"Provider [{cap.Id}] {cap.DisplayName} | Status: {cap.Status} | Pairs: {cap.SupportedConversions.Count}");
|
||||
var missingDeps = cap.ExternalDependencies.Where(d => d.IsRequired).Select(d => d.Name).ToList();
|
||||
if (missingDeps.Any())
|
||||
{
|
||||
_output.WriteLine($" Required deps: {string.Join(", ", missingDeps)}");
|
||||
}
|
||||
}
|
||||
Assert.NotEmpty(providers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Audit_HtmlReachability_InGraph()
|
||||
{
|
||||
var engine = Everything2EverythingBootstrap.CreateDefault();
|
||||
var outputs = engine.Providers.OutputsForInput(".html");
|
||||
_output.WriteLine($"HTML Reachable Outputs ({outputs.Count}): {string.Join(", ", outputs)}");
|
||||
|
||||
Assert.Contains(".pdf", outputs);
|
||||
Assert.Contains(".png", outputs);
|
||||
Assert.Contains(".jpg", outputs);
|
||||
Assert.Contains(".webp", outputs);
|
||||
Assert.Contains(".md", outputs);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue