diff --git a/packaging/Package.appxmanifest b/packaging/Package.appxmanifest index 9b9d074..9cb3be1 100644 --- a/packaging/Package.appxmanifest +++ b/packaging/Package.appxmanifest @@ -1,6 +1,6 @@  - + Everything2Everything YunChan diff --git a/src/Everything2Everything.App/Everything2Everything.App.csproj b/src/Everything2Everything.App/Everything2Everything.App.csproj index 984ecea..4d141e9 100644 --- a/src/Everything2Everything.App/Everything2Everything.App.csproj +++ b/src/Everything2Everything.App/Everything2Everything.App.csproj @@ -1,7 +1,7 @@ - 1.0.22 + 1.0.21 WinExe net9.0-windows10.0.19041.0 enable @@ -35,29 +35,6 @@ - - - - PreserveNewest - PreserveNewest - - - Everything2Everything.Shell.dll - PreserveNewest - PreserveNewest - - - AppxManifest.xml - PreserveNewest - PreserveNewest - - - Assets\%(RecursiveDir)%(Filename)%(Extension) - PreserveNewest - PreserveNewest - - - @@ -67,4 +44,3 @@ - diff --git a/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs b/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs index 76da67f..0b0820d 100644 --- a/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs +++ b/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs @@ -10,69 +10,6 @@ public static class ContextMenuRegistrar private const string MainLabel = "Everything2Everything으로 변환"; private const string SubmenuKeyPrefix = "Everything2Everything.SubMenu."; - public const string ClassicContextMenuOverrideKey = @"Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32"; - - public sealed class ContextMenuVerbPlan - { - public string Extension { get; init; } = ""; - public string RootKeyPath { get; init; } = ""; - public string RootLabel { get; init; } = ""; - public string IconPath { get; init; } = ""; - public bool UsesExtendedSubCommands { get; init; } = true; // 의도적 true로 RED 유도 - public string? SubCommandsValue { get; init; } = null; - public IReadOnlyList Items { get; init; } = Array.Empty(); - } - - public sealed class ContextMenuItemPlan - { - public string SubKeyPath { get; init; } = ""; - public string VerbName { get; init; } = ""; - public string Label { get; init; } = ""; - public string IconPath { get; init; } = ""; - public string Command { get; init; } = ""; - } - - public static ContextMenuVerbPlan BuildVerbPlan(ConversionEngine engine, string ext, string exePath) - { - var availableOutputs = GetAvailableOutputs(engine, ext); - var icon = exePath + ",0"; - var rootKeyPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell\{MainVerb}"; - - var items = new List(); - foreach (var (outExt, outLabel, sortPrefix) in availableOutputs) - { - var subVerbName = $"{sortPrefix}_{outExt.TrimStart('.')}"; - var cliExt = outExt.TrimStart('.'); - items.Add(new ContextMenuItemPlan - { - SubKeyPath = $@"{rootKeyPath}\shell\{subVerbName}", - VerbName = subVerbName, - Label = outLabel, - IconPath = icon, - Command = $"\"{exePath}\" to {cliExt} \"%1\"" - }); - } - - items.Add(new ContextMenuItemPlan - { - SubKeyPath = $@"{rootKeyPath}\shell\98_dialog", - VerbName = "98_dialog", - Label = "변환… (옵션 선택)", - IconPath = icon, - Command = $"\"{exePath}\" dialog \"%1\"" - }); - - return new ContextMenuVerbPlan - { - Extension = ext, - RootKeyPath = rootKeyPath, - RootLabel = MainLabel, - IconPath = icon, - UsesExtendedSubCommands = false, // Shift 불필요: 마우스 일반 우클릭으로 즉시 펼쳐짐 - SubCommandsValue = "", - Items = items - }; - } private static readonly (string Ext, string Label, string SortPrefix)[] PopularOutputs = { @@ -107,16 +44,16 @@ public static class ContextMenuRegistrar public static void Register(ConversionEngine engine) { var exe = GetAppExecutablePath(); + var icon = exe + ",0"; foreach (var ext in CollectInputExtensions(engine)) { - var plan = BuildVerbPlan(engine, ext, exe); - if (plan.Items.Count == 0) continue; + var availableOutputs = GetAvailableOutputs(engine, ext); + if (availableOutputs.Count == 0) continue; - WriteCascade(plan, ext); + WriteCascade(ext, exe, icon, availableOutputs); } - TryRegisterWindows11SparsePackage(); NotifyShell(); } @@ -129,44 +66,9 @@ public static class ContextMenuRegistrar DeleteVerb(ext, "Everything2Everything.Dialog"); DeleteSubmenuTree(ext); } - - TryUnregisterWindows11SparsePackage(); NotifyShell(); } - public static bool IsClassicContextMenuEnabled() - { - try - { - using var key = Registry.CurrentUser.OpenSubKey(ClassicContextMenuOverrideKey); - return key != null; - } - catch - { - return false; - } - } - - public static void SetClassicContextMenuEnabled(bool enable) - { - try - { - if (enable) - { - using var key = Registry.CurrentUser.CreateSubKey(ClassicContextMenuOverrideKey, writable: true); - key?.SetValue(null, "", RegistryValueKind.String); - } - else - { - Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}", throwOnMissingSubKey: false); - } - NotifyShell(); - } - catch - { - } - } - private static IEnumerable CollectInputExtensions(ConversionEngine engine) { return engine.Providers.Implemented @@ -177,25 +79,28 @@ public static class ContextMenuRegistrar .Distinct(); } - private static void WriteCascade(ContextMenuVerbPlan plan, string ext) + private static void WriteCascade( + string ext, + string exe, + string icon, + IReadOnlyList<(string Ext, string Label, string SortPrefix)> availableOutputs) { - // 1. 루트 키 등록 (Shift 없는 일반 우클릭에서 즉시 서브메뉴 노출) - using (var verbKey = Registry.CurrentUser.CreateSubKey(plan.RootKeyPath, writable: true) - ?? throw new InvalidOperationException($"레지스트리 키 생성 실패: {plan.RootKeyPath}")) - { - verbKey.SetValue(null, plan.RootLabel, RegistryValueKind.String); - verbKey.SetValue("MUIVerb", plan.RootLabel, RegistryValueKind.String); - verbKey.SetValue("Icon", plan.IconPath, RegistryValueKind.String); - verbKey.SetValue("SubCommands", plan.SubCommandsValue ?? "", RegistryValueKind.String); + var submenuKeyName = SubmenuKeyPrefix + ext.TrimStart('.'); - // 구버전에서 Shift 키를 강제하던 ExtendedSubCommandsKey 제거 - try { verbKey.DeleteValue("ExtendedSubCommandsKey", throwOnMissingValue: false); } catch { } + var verbPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell\{MainVerb}"; + using (var verbKey = Registry.CurrentUser.CreateSubKey(verbPath, writable: true) + ?? throw new InvalidOperationException($"레지스트리 키 생성 실패: {verbPath}")) + { + verbKey.SetValue(null, MainLabel, RegistryValueKind.String); + verbKey.SetValue("MUIVerb", MainLabel, RegistryValueKind.String); + verbKey.SetValue("Icon", icon, RegistryValueKind.String); + verbKey.SetValue("SubCommands", "", RegistryValueKind.String); + verbKey.SetValue("ExtendedSubCommandsKey", submenuKeyName, RegistryValueKind.String); try { verbKey.DeleteSubKeyTree("command", throwOnMissingSubKey: false); } catch { } } - // 2. 루트 직하위 shell 키 정리 및 서브메뉴 항목 등록 - var shellRootPath = $@"{plan.RootKeyPath}\shell"; - using (var existing = Registry.CurrentUser.OpenSubKey(shellRootPath, writable: true)) + var submenuShellPath = $@"Software\Classes\{submenuKeyName}\shell"; + using (var existing = Registry.CurrentUser.OpenSubKey(submenuShellPath, writable: true)) { if (existing is not null) { @@ -206,22 +111,31 @@ public static class ContextMenuRegistrar } } - foreach (var item in plan.Items) + foreach (var (outExt, outLabel, sortPrefix) in availableOutputs) { - using var key = Registry.CurrentUser.CreateSubKey(item.SubKeyPath, writable: true) - ?? throw new InvalidOperationException($"서브메뉴 키 생성 실패: {item.SubKeyPath}"); - - key.SetValue(null, item.Label, RegistryValueKind.String); - key.SetValue("MUIVerb", item.Label, RegistryValueKind.String); - key.SetValue("Icon", item.IconPath, RegistryValueKind.String); - - using var commandKey = key.CreateSubKey("command", writable: true) - ?? throw new InvalidOperationException("command 하위 키 생성 실패"); - commandKey.SetValue(null, item.Command, RegistryValueKind.String); + var subVerbName = $"{sortPrefix}_{outExt.TrimStart('.')}"; + var cliExt = outExt.TrimStart('.'); + WriteSubmenuItem(submenuKeyName, subVerbName, outLabel, icon, + $"\"{exe}\" to {cliExt} \"%1\""); } - // 3. 구버전 독립 SubMenu 키 정리 (이전 버전 찌꺼기 제거) - DeleteSubmenuTree(ext); + WriteSubmenuItem(submenuKeyName, "98_dialog", "변환… (옵션 선택)", icon, + $"\"{exe}\" dialog \"%1\""); + } + + private static void WriteSubmenuItem(string submenuKeyName, string verbName, string label, string icon, string command) + { + var path = $@"Software\Classes\{submenuKeyName}\shell\{verbName}"; + using var key = Registry.CurrentUser.CreateSubKey(path, writable: true) + ?? throw new InvalidOperationException($"서브메뉴 키 생성 실패: {path}"); + + key.SetValue(null, label, RegistryValueKind.String); + key.SetValue("MUIVerb", label, RegistryValueKind.String); + key.SetValue("Icon", icon, RegistryValueKind.String); + + using var commandKey = key.CreateSubKey("command", writable: true) + ?? throw new InvalidOperationException("command 하위 키 생성 실패"); + commandKey.SetValue(null, command, RegistryValueKind.String); } private static void DeleteVerb(string ext, string verb) @@ -258,70 +172,6 @@ public static class ContextMenuRegistrar + Path.DirectorySeparatorChar + "Everything2Everything.exe"; } - private static void TryRegisterWindows11SparsePackage() - { - try - { - if (Environment.OSVersion.Version.Build < 22000) return; - - var manifestPath = FindAppxManifestPath(); - if (string.IsNullOrEmpty(manifestPath) || !File.Exists(manifestPath)) return; - - var psi = new System.Diagnostics.ProcessStartInfo - { - FileName = "powershell.exe", - Arguments = $"-NoProfile -NonInteractive -WindowStyle Hidden -Command \"Add-AppxPackage -Register '{manifestPath}'\"", - CreateNoWindow = true, - UseShellExecute = false - }; - using var proc = System.Diagnostics.Process.Start(psi); - proc?.WaitForExit(5000); - } - catch - { - // Sparse Package 등록 실패(서명/정책 등) 시에도 레지스트리 캐스케이드는 정상이므로 무시 - } - } - - private static void TryUnregisterWindows11SparsePackage() - { - try - { - if (Environment.OSVersion.Version.Build < 22000) return; - - var psi = new System.Diagnostics.ProcessStartInfo - { - FileName = "powershell.exe", - Arguments = "-NoProfile -NonInteractive -WindowStyle Hidden -Command \"Get-AppxPackage -Name '*Everything2Everything*' | Remove-AppxPackage\"", - CreateNoWindow = true, - UseShellExecute = false - }; - using var proc = System.Diagnostics.Process.Start(psi); - proc?.WaitForExit(5000); - } - catch - { - } - } - - public static string? FindAppxManifestPath() - { - var baseDir = AppContext.BaseDirectory; - var candidates = new[] - { - Path.Combine(baseDir, "AppxManifest.xml"), - Path.Combine(baseDir, "Package.appxmanifest"), - Path.Combine(baseDir, "..", "packaging", "Package.appxmanifest"), - Path.Combine(baseDir, "..", "..", "packaging", "Package.appxmanifest") - }; - - foreach (var candidate in candidates) - { - if (File.Exists(candidate)) return Path.GetFullPath(candidate); - } - return null; - } - private static void NotifyShell() { try { NativeMethods.SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero); } diff --git a/src/Everything2Everything.App/Views/MainWindow.xaml.cs b/src/Everything2Everything.App/Views/MainWindow.xaml.cs index f9c3468..0805fde 100644 --- a/src/Everything2Everything.App/Views/MainWindow.xaml.cs +++ b/src/Everything2Everything.App/Views/MainWindow.xaml.cs @@ -1213,7 +1213,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC { ContextMenuRegistrar.Register(_engine); MessageBox.Show(this, - "컨텍스트 메뉴를 성공적으로 등록했습니다.\n파일 우클릭 시 \"Everything2Everything으로 변환\" 메뉴가 즉시 표시됩니다.", + "컨텍스트 메뉴를 등록했습니다.\n파일 우클릭 → \"추가 옵션 표시\" 또는 \"JPEG로 빠른 변환/변환…\".", "Everything2Everything", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) diff --git a/src/Everything2Everything.Core/Converters/DocumentProvider.cs b/src/Everything2Everything.Core/Converters/DocumentProvider.cs index 26c001f..0be40b6 100644 --- a/src/Everything2Everything.Core/Converters/DocumentProvider.cs +++ b/src/Everything2Everything.Core/Converters/DocumentProvider.cs @@ -17,22 +17,17 @@ namespace Everything2Everything.Core.Converters; // txt ↔ md → trivial copy public sealed class DocumentProvider : IConverterProvider { - 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" }; + private static readonly string[] Inputs = + { ".html", ".htm", ".hwp", ".hwpx", ".docx", ".doc", ".md", ".markdown", ".txt" }; + + private static readonly string[] Outputs = { ".html", ".docx", ".md", ".txt" }; public ProviderCapability Capability { get; } = new( Id: "document", - 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(), + DisplayName: "문서 텍스트 변환 (HTML/HWP/DOCX/MD/TXT)", + SupportedConversions: ProviderCapability.PairsFromMatrix(Inputs, Outputs), Status: ProviderStatus.RequiresExternal, - Summary: "DOCX·HWP 문서를 HTML/DOCX/MD/TXT로 변환하거나 텍스트를 DOCX로 저장합니다 (LibreOffice 엔진).", + Summary: "HTML·HWP·DOCX·Markdown·TXT 사이의 양방향 텍스트 변환 (LibreOffice + Markdig + ReverseMarkdown).", ExternalDependencies: new[] { new ExternalDependency( diff --git a/src/Everything2Everything.Core/Converters/HtmlProvider.cs b/src/Everything2Everything.Core/Converters/HtmlProvider.cs index a5ebf50..9db4759 100644 --- a/src/Everything2Everything.Core/Converters/HtmlProvider.cs +++ b/src/Everything2Everything.Core/Converters/HtmlProvider.cs @@ -1,7 +1,6 @@ using System.Text.Json; using System.Threading; using System.Windows; -using System.Windows.Interop; using System.Windows.Threading; using Everything2Everything.Core.Providers; using ImageMagick; @@ -166,25 +165,7 @@ public sealed class HtmlProvider : IConverterProvider try { var dispatcher = Dispatcher.CurrentDispatcher; - 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); - } - }); - + _ = RunCaptureOnDispatcher(capture, dispatcher, sourcePath, options, progress, cancellationToken, tcs); Dispatcher.Run(); } catch (Exception ex) @@ -200,15 +181,16 @@ public sealed class HtmlProvider : IConverterProvider return tcs.Task; } - private static async Task RunCaptureOnDispatcher( + private static async Task RunCaptureOnDispatcher( Func?, Task> capture, + Dispatcher dispatcher, string sourcePath, ConvertOptions options, IProgress? progress, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + TaskCompletionSource tcs) { CoreWebView2Controller? controller = null; - HwndSource? hwndSource = null; try { var userDataFolder = Path.Combine( @@ -216,23 +198,15 @@ public sealed class HtmlProvider : IConverterProvider "Everything2Everything", "WebView2"); Directory.CreateDirectory(userDataFolder); - var env = await CoreWebView2Environment.CreateAsync(null, userDataFolder); + var env = await CoreWebView2Environment.CreateAsync(null, userDataFolder).ConfigureAwait(true); 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 = true; + controller.IsVisible = false; var web = controller.CoreWebView2; progress?.Report(0.3); @@ -243,31 +217,36 @@ 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(Path.GetFullPath(sourcePath)).AbsoluteUri; + var fileUri = new Uri(sourcePath).AbsoluteUri; web.Navigate(fileUri); using (cancellationToken.Register(() => navTcs.TrySetCanceled())) { - await navTcs.Task; + await navTcs.Task.ConfigureAwait(true); } progress?.Report(0.5); if (options.HtmlRender.WaitMilliseconds > 0) - await Task.Delay(options.HtmlRender.WaitMilliseconds, cancellationToken); + await Task.Delay(options.HtmlRender.WaitMilliseconds, cancellationToken).ConfigureAwait(true); progress?.Report(0.65); - var bytes = await capture(web, progress); - return bytes; + var bytes = await capture(web, progress).ConfigureAwait(true); + tcs.TrySetResult(bytes); + } + catch (Exception ex) + { + tcs.TrySetException(ex); } finally { try { controller?.Close(); } catch { } - try { hwndSource?.Dispose(); } catch { } + dispatcher.BeginInvokeShutdown(DispatcherPriority.Background); } } diff --git a/src/Everything2Everything.Core/Converters/MarkdownProvider.cs b/src/Everything2Everything.Core/Converters/MarkdownProvider.cs deleted file mode 100644 index 3af441f..0000000 --- a/src/Everything2Everything.Core/Converters/MarkdownProvider.cs +++ /dev/null @@ -1,117 +0,0 @@ -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(), - RoadmapNote: null); - - public Task CheckAvailabilityAsync(CancellationToken cancellationToken = default) - => Task.FromResult(ProviderAvailability.Ready); - - public async Task ConvertAsync( - string sourcePath, - string outputDirectory, - string outputExtension, - ConvertOptions options, - IProgress? 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 = $"{Path.GetFileNameWithoutExtension(mdPath)}{body}"; - 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", "
\n"); - var fullHtml = $"{Path.GetFileNameWithoutExtension(txtPath)}
{encoded}
"; - await File.WriteAllTextAsync(htmlPath, fullHtml, Encoding.UTF8, ct).ConfigureAwait(false); - } -} diff --git a/src/Everything2Everything.Tests/ContextMenuRegistrarTests.cs b/src/Everything2Everything.Tests/ContextMenuRegistrarTests.cs index ca8c9ee..136a53a 100644 --- a/src/Everything2Everything.Tests/ContextMenuRegistrarTests.cs +++ b/src/Everything2Everything.Tests/ContextMenuRegistrarTests.cs @@ -56,44 +56,4 @@ public class ContextMenuRegistrarTests Assert.DoesNotContain(outputs, o => o.Ext == ".gif"); Assert.DoesNotContain(outputs, o => o.Ext == ".mp4"); } - - [Fact] - public void BuildVerbPlan_Png_DoesNotUseExtendedSubCommands_ForNormalRightClick() - { - // Windows 10/11에서 Shift 키 없이 일반 마우스 우클릭만으로 컨텍스트 메뉴가 바로 노출되어야 한다. - // ExtendedSubCommandsKey가 사용되면 Shift를 누를 때만 서브메뉴가 나오는 심각한 버그가 발생한다. - var engine = CreateEngine(); - var plan = ContextMenuRegistrar.BuildVerbPlan(engine, ".png", @"C:\Apps\E2E\Everything2Everything.exe"); - - Assert.False(plan.UsesExtendedSubCommands, "일반 우클릭에서 바로 노출되려면 UsesExtendedSubCommands가 반드시 false여야 합니다."); - Assert.Equal("", plan.SubCommandsValue); - } - - [Fact] - public void BuildVerbPlan_Png_PlacesSubVerbsUnderRootShellKey() - { - // Windows 7+ 표준 정적 캐스케이드 서브메뉴는 루트 동사 키 직하위의 \shell\ 키에 배치되어야 한다. - var engine = CreateEngine(); - var plan = ContextMenuRegistrar.BuildVerbPlan(engine, ".png", @"C:\Apps\E2E\Everything2Everything.exe"); - - Assert.NotEmpty(plan.Items); - var expectedPrefix = @"Software\Classes\SystemFileAssociations\.png\shell\Everything2Everything\shell\"; - foreach (var item in plan.Items) - { - Assert.StartsWith(expectedPrefix, item.SubKeyPath); - } - - // 대화상자 선택 메뉴(98_dialog)가 포함되어 있는지 검증 - var dialogItem = plan.Items.FirstOrDefault(i => i.VerbName.EndsWith("dialog")); - Assert.NotNull(dialogItem); - Assert.Contains("dialog", dialogItem.Command); - } - - [Fact] - public void ClassicContextMenuOverrideKey_Matches_Windows11_Specification() - { - // Windows 11에서 모던 메뉴 대신 항상 1차 클래식 메뉴를 복원하는 레지스트리 키 검증 - Assert.Equal(@"Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32", - ContextMenuRegistrar.ClassicContextMenuOverrideKey); - } } diff --git a/src/Everything2Everything.Tests/DependencyInjectionTests.cs b/src/Everything2Everything.Tests/DependencyInjectionTests.cs index ee13078..0ff2f38 100644 --- a/src/Everything2Everything.Tests/DependencyInjectionTests.cs +++ b/src/Everything2Everything.Tests/DependencyInjectionTests.cs @@ -22,8 +22,7 @@ public class DependencyInjectionTests using var sp = services.BuildServiceProvider(); var providers = sp.GetServices().ToList(); - Assert.Equal(15, providers.Count); // MarkdownProvider 추가로 총 15개 Provider - Assert.NotNull(sp.GetRequiredService()); + Assert.Equal(14, providers.Count); // 하드코딩 14개와 동일 — Scrutor 누락/초과 방어 Assert.NotNull(sp.GetRequiredService()); Assert.NotNull(sp.GetRequiredService()); @@ -43,7 +42,6 @@ public class DependencyInjectionTests Assert.NotNull(sp.GetRequiredService()); // ← PdfProvider 주입 Assert.NotNull(sp.GetRequiredService()); // ← PdfProvider 주입 Assert.NotNull(sp.GetRequiredService()); // ← ISettingsStore 주입 - Assert.NotNull(sp.GetRequiredService()); // AsSelfWithInterfaces: 구체 타입과 인터페이스가 동일 싱글턴 인스턴스를 공유. var magickAsSelf = sp.GetRequiredService(); @@ -81,7 +79,7 @@ public class DependencyInjectionTests { // 파사드 하위호환: DI 내부전환 후에도 CreateDefault가 동일하게 엔진/그래프를 구성. var engine = Everything2EverythingBootstrap.CreateDefault(); - Assert.Equal(15, engine.Providers.All.Count); + Assert.Equal(14, engine.Providers.All.Count); Assert.NotNull(engine.Providers.Graph.FindBestPath(".png", ".jpg")); Assert.NotNull(engine.Providers.Graph.FindBestPath(".svg", ".jpg", maxHops: 3)); } diff --git a/src/Everything2Everything.Tests/HtmlEndToEndAllOutputsTests.cs b/src/Everything2Everything.Tests/HtmlEndToEndAllOutputsTests.cs deleted file mode 100644 index 4d27682..0000000 --- a/src/Everything2Everything.Tests/HtmlEndToEndAllOutputsTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -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, "

Test Page

Paragraph content

"); - - 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, "

Markdown Title

Bold text

"); - - 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}"); - } -} diff --git a/src/Everything2Everything.Tests/HtmlProviderTests.cs b/src/Everything2Everything.Tests/HtmlProviderTests.cs deleted file mode 100644 index bdc152c..0000000 --- a/src/Everything2Everything.Tests/HtmlProviderTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -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, "

Hello HTML Test

Testing HTML to PDF

"); - - 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, "

Hello HTML Test

Testing HTML to PNG

"); - - 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); - } -} diff --git a/src/Everything2Everything.Tests/MarkdownProviderTests.cs b/src/Everything2Everything.Tests/MarkdownProviderTests.cs deleted file mode 100644 index aebded2..0000000 --- a/src/Everything2Everything.Tests/MarkdownProviderTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -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, "

Title Here

Paragraph with bold text

"); - - 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", content); - Assert.Contains("test", 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, "

Section

Content

"); - - var engine = Everything2EverythingBootstrap.CreateDefault(); - var path = engine.Providers.Graph.FindBestPath(".html", ".md"); - Assert.NotNull(path); - Assert.Single(path); - Assert.IsType(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); - } -} diff --git a/src/Everything2Everything.Tests/MatrixIntegrityTests.cs b/src/Everything2Everything.Tests/MatrixIntegrityTests.cs deleted file mode 100644 index 3bb944e..0000000 --- a/src/Everything2Everything.Tests/MatrixIntegrityTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -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); - } -}