mirror of
https://github.com/QL-Win/QuickLook.git
synced 2025-09-12 10:19:07 +00:00
@@ -31,10 +31,10 @@ namespace QuickLook.Plugin.HtmlViewer;
|
||||
|
||||
public class WebpagePanel : UserControl
|
||||
{
|
||||
private Uri _currentUri;
|
||||
private string _primaryPath;
|
||||
private string _fallbackPath;
|
||||
private WebView2 _webView;
|
||||
protected Uri _currentUri;
|
||||
protected string _primaryPath;
|
||||
protected string _fallbackPath;
|
||||
protected WebView2 _webView;
|
||||
|
||||
public string FallbackPath
|
||||
{
|
||||
@@ -54,11 +54,11 @@ public class WebpagePanel : UserControl
|
||||
{
|
||||
CreationProperties = new CoreWebView2CreationProperties
|
||||
{
|
||||
UserDataFolder = Path.Combine(SettingHelper.LocalDataPath, @"WebView2_Data\\"),
|
||||
UserDataFolder = Path.Combine(SettingHelper.LocalDataPath, @"WebView2_Data\"),
|
||||
},
|
||||
DefaultBackgroundColor = OSThemeHelper.AppsUseDarkTheme() ? Color.FromArgb(255, 32, 32, 32) : Color.White, // Prevent white flash in dark mode
|
||||
};
|
||||
_webView.NavigationStarting += NavigationStarting_CancelNavigation;
|
||||
_webView.NavigationStarting += Webview_NavigationStarting;
|
||||
_webView.NavigationCompleted += WebView_NavigationCompleted;
|
||||
_webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
|
||||
Content = _webView;
|
||||
@@ -97,7 +97,7 @@ public class WebpagePanel : UserControl
|
||||
.ContinueWith(_ => Dispatcher.Invoke(() => _webView?.NavigateToString(html)));
|
||||
}
|
||||
|
||||
private void NavigationStarting_CancelNavigation(object sender, CoreWebView2NavigationStartingEventArgs e)
|
||||
protected virtual void Webview_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
|
||||
{
|
||||
if (e.Uri.StartsWith("data:")) // when using NavigateToString
|
||||
return;
|
||||
@@ -163,12 +163,12 @@ public class WebpagePanel : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
|
||||
protected virtual void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
|
||||
{
|
||||
_webView.DefaultBackgroundColor = Color.White; // Reset to white after page load to match expected default behavior
|
||||
}
|
||||
|
||||
private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
|
||||
protected virtual void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
|
||||
{
|
||||
if (e.IsSuccess)
|
||||
{
|
||||
|
@@ -1,54 +0,0 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ResourcesDir,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputFile
|
||||
)
|
||||
|
||||
# Get all files in the Resources directory
|
||||
$files = Get-ChildItem -Path $ResourcesDir -Recurse -File | Sort-Object -Property FullName
|
||||
|
||||
# Create SHA256 hasher
|
||||
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
||||
|
||||
# Create memory stream to combine all file contents
|
||||
$memoryStream = New-Object System.IO.MemoryStream
|
||||
|
||||
# Add each file's path and contents to the hash
|
||||
foreach ($file in $files) {
|
||||
# Get relative path and convert to lowercase for consistent hashing
|
||||
$relativePath = $file.FullName.Substring($ResourcesDir.Length + 1).ToLowerInvariant()
|
||||
$pathBytes = [System.Text.Encoding]::UTF8.GetBytes("QuickLook.Plugin.MarkdownViewer.Resources.$relativePath".Replace("\", "/"))
|
||||
$memoryStream.Write($pathBytes, 0, $pathBytes.Length)
|
||||
|
||||
# Add file contents
|
||||
$fileBytes = [System.IO.File]::ReadAllBytes($file.FullName)
|
||||
$memoryStream.Write($fileBytes, 0, $fileBytes.Length)
|
||||
}
|
||||
|
||||
# Calculate final hash
|
||||
$hashBytes = $sha256.ComputeHash($memoryStream.ToArray())
|
||||
$hash = [BitConverter]::ToString($hashBytes).Replace("-", "").ToLowerInvariant()
|
||||
|
||||
# Generate C# code
|
||||
$code = @"
|
||||
// <auto-generated>
|
||||
// This file was generated during build to indicate changes to embedded resources.
|
||||
// </auto-generated>
|
||||
|
||||
namespace QuickLook.Plugin.MarkdownViewer
|
||||
{
|
||||
internal static class EmbeddedResourcesHash
|
||||
{
|
||||
internal const string Hash = "$hash";
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
# Write to output file
|
||||
[System.IO.File]::WriteAllText($OutputFile, $code)
|
||||
|
||||
# Cleanup
|
||||
$memoryStream.Dispose()
|
||||
$sha256.Dispose()
|
@@ -0,0 +1,172 @@
|
||||
// Copyright © 2017-2025 QL-Win Contributors
|
||||
//
|
||||
// This file is part of QuickLook program.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using QuickLook.Plugin.HtmlViewer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UtfUnknown;
|
||||
|
||||
namespace QuickLook.Plugin.MarkdownViewer;
|
||||
|
||||
public class MarkdownPanel : WebpagePanel
|
||||
{
|
||||
protected const string _resourcePrefix = "QuickLook.Plugin.MarkdownViewer.Resources.";
|
||||
protected internal static readonly Dictionary<string, byte[]> _resources = [];
|
||||
protected byte[] _homePage;
|
||||
|
||||
static MarkdownPanel()
|
||||
{
|
||||
InitializeResources();
|
||||
}
|
||||
|
||||
protected static void InitializeResources()
|
||||
{
|
||||
if (_resources.Any()) return;
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
foreach (var resourceName in assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (!resourceName.StartsWith(_resourcePrefix)) continue;
|
||||
|
||||
var relativePath = resourceName.Substring(_resourcePrefix.Length);
|
||||
if (relativePath.Equals("resources", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null) continue;
|
||||
var memoryStream = new MemoryStream();
|
||||
stream.CopyTo(memoryStream);
|
||||
_resources.Add($"/{relativePath.Replace('\\', '/')}", memoryStream.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public void PreviewMarkdown(string path)
|
||||
{
|
||||
FallbackPath = Path.GetDirectoryName(path);
|
||||
|
||||
var html = GenerateMarkdownHtml(path);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(html);
|
||||
_homePage = bytes;
|
||||
|
||||
NavigateToUri(new Uri("file://quicklook/"));
|
||||
}
|
||||
|
||||
protected string GenerateMarkdownHtml(string path)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
var encoding = CharsetDetector.DetectFromBytes(bytes).Detected?.Encoding ?? Encoding.Default;
|
||||
var content = encoding.GetString(bytes);
|
||||
|
||||
var template = ReadString("/md2html.html");
|
||||
var html = template.Replace("{{content}}", content);
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
protected override void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
|
||||
{
|
||||
if (e.IsSuccess)
|
||||
{
|
||||
_webView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All);
|
||||
|
||||
_webView.CoreWebView2.WebResourceRequested += (sender, args) =>
|
||||
{
|
||||
Debug.WriteLine($"[{args.Request.Method}] {args.Request.Uri}");
|
||||
|
||||
try
|
||||
{
|
||||
var requestedUri = new Uri(args.Request.Uri);
|
||||
|
||||
if (requestedUri.Scheme == "file")
|
||||
{
|
||||
if (requestedUri.AbsolutePath == "/")
|
||||
{
|
||||
var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
|
||||
new MemoryStream(_homePage), 200, "OK", MimeTypes.GetContentType(".html"));
|
||||
args.Response = response;
|
||||
}
|
||||
else if (ContainsKey(requestedUri.AbsolutePath))
|
||||
{
|
||||
var stream = ReadStream(requestedUri.AbsolutePath);
|
||||
var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
|
||||
stream, 200, "OK", MimeTypes.GetContentType(Path.GetExtension(requestedUri.AbsolutePath)));
|
||||
args.Response = response;
|
||||
}
|
||||
else
|
||||
{
|
||||
var localPath = _fallbackPath + requestedUri.AbsolutePath.Replace('/', '\\');
|
||||
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
var fileStream = File.OpenRead(localPath);
|
||||
var response = _webView.CoreWebView2.Environment.CreateWebResourceResponse(
|
||||
fileStream, 200, "OK", MimeTypes.GetContentType());
|
||||
args.Response = response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// We don't need to feel burdened by any exceptions
|
||||
Debug.WriteLine(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ContainsKey(string key)
|
||||
{
|
||||
return _resources.ContainsKey(key);
|
||||
}
|
||||
|
||||
public static Stream ReadStream(string key)
|
||||
{
|
||||
byte[] bytes = _resources[key];
|
||||
return new MemoryStream(bytes);
|
||||
}
|
||||
|
||||
public static string ReadString(string key)
|
||||
{
|
||||
using var reader = new StreamReader(ReadStream(key), Encoding.UTF8);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
|
||||
public static class MimeTypes
|
||||
{
|
||||
public const string Html = "text/html";
|
||||
public const string JavaScript = "application/javascript";
|
||||
public const string Css = "text/css";
|
||||
public const string Binary = "application/octet-stream";
|
||||
|
||||
public static string GetContentType(string extension = null) => $"Content-Type: {GetMimeType(extension)}";
|
||||
|
||||
public static string GetMimeType(string extension = null) => extension?.ToLowerInvariant() switch
|
||||
{
|
||||
".js" => JavaScript, // Only handle known extensions from resources
|
||||
".css" => Css,
|
||||
".html" => Html,
|
||||
_ => Binary,
|
||||
};
|
||||
}
|
||||
}
|
@@ -15,25 +15,17 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
using QuickLook.Common.Helpers;
|
||||
using QuickLook.Common.Plugin;
|
||||
using QuickLook.Plugin.HtmlViewer;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using UtfUnknown;
|
||||
|
||||
namespace QuickLook.Plugin.MarkdownViewer;
|
||||
|
||||
public class Plugin : IViewer
|
||||
{
|
||||
private bool _isInitialized = false;
|
||||
private WebpagePanel? _panel;
|
||||
private string? _currentHtmlPath;
|
||||
private MarkdownPanel? _panel;
|
||||
|
||||
/// <summary>
|
||||
/// Markdown and Markdown-like extensions
|
||||
@@ -52,25 +44,10 @@ public class Plugin : IViewer
|
||||
".mdtxt", ".mdtext", // Less common
|
||||
];
|
||||
|
||||
private static readonly string _resourcePath = Path.Combine(SettingHelper.LocalDataPath, "QuickLook.Plugin.MarkdownViewer");
|
||||
private static readonly string _resourcePrefix = "QuickLook.Plugin.MarkdownViewer.Resources.";
|
||||
private static readonly ResourceManager _resourceManager = new(_resourcePath, _resourcePrefix);
|
||||
|
||||
public int Priority => 0;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
// Delayed initialization can speed up startup
|
||||
_isInitialized = false;
|
||||
}
|
||||
|
||||
public void InitializeResources()
|
||||
{
|
||||
// Initialize resources and handle versioning
|
||||
_resourceManager.InitializeResources();
|
||||
|
||||
// Clean up any temporary HTML files if QuickLook was forcibly terminated
|
||||
CleanupTempFiles();
|
||||
}
|
||||
|
||||
public bool CanHandle(string path)
|
||||
@@ -85,97 +62,19 @@ public class Plugin : IViewer
|
||||
|
||||
public void View(string path, ContextObject context)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_isInitialized = true;
|
||||
InitializeResources();
|
||||
}
|
||||
_panel = new MarkdownPanel();
|
||||
_panel.PreviewMarkdown(path);
|
||||
|
||||
_panel = new WebpagePanel();
|
||||
context.ViewerContent = _panel;
|
||||
context.Title = Path.GetFileName(path);
|
||||
|
||||
var htmlPath = GenerateMarkdownHtml(path);
|
||||
_panel.FallbackPath = Path.GetDirectoryName(path);
|
||||
_panel.NavigateToFile(htmlPath);
|
||||
_panel.Dispatcher.Invoke(() => { context.IsBusy = false; }, DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private string GenerateMarkdownHtml(string path)
|
||||
{
|
||||
var templatePath = Path.Combine(_resourcePath, "md2html.html");
|
||||
|
||||
if (!File.Exists(templatePath))
|
||||
throw new FileNotFoundException($"Required template file md2html.html not found in extracted resources at {templatePath}");
|
||||
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
var encoding = CharsetDetector.DetectFromBytes(bytes).Detected?.Encoding ?? Encoding.Default;
|
||||
var content = encoding.GetString(bytes);
|
||||
|
||||
var template = File.ReadAllText(templatePath);
|
||||
var html = template.Replace("{{content}}", content);
|
||||
|
||||
// Generate unique filename and ensure it doesn't exist
|
||||
string outputPath;
|
||||
do
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||
var outputFileName = $"temp_{uniqueId}.html";
|
||||
outputPath = Path.Combine(_resourcePath, outputFileName);
|
||||
} while (File.Exists(outputPath));
|
||||
|
||||
// Clean up previous file if it exists
|
||||
CleanupTempHtmlFile();
|
||||
|
||||
File.WriteAllText(outputPath, html);
|
||||
_currentHtmlPath = outputPath;
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
#region Cleanup
|
||||
|
||||
private void CleanupTempHtmlFile()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_currentHtmlPath) && File.Exists(_currentHtmlPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(_currentHtmlPath);
|
||||
}
|
||||
catch (IOException) { } // Ignore deletion errors
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupTempFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempFiles = Directory.GetFiles(_resourcePath, "temp_*.html");
|
||||
foreach (var file in tempFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
catch (IOException) { } // Ignore deletion errors
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to clean up temporary HTML files: {ex.Message}");
|
||||
}
|
||||
context.IsBusy = false;
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
CleanupTempHtmlFile();
|
||||
|
||||
_panel?.Dispose();
|
||||
_panel = null;
|
||||
}
|
||||
|
||||
#endregion Cleanup
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<UseWPF>true</UseWPF>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
||||
@@ -77,30 +76,18 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3240.44">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**\*">
|
||||
<LogicalName>QuickLook.Plugin.MarkdownViewer.Resources.%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="GenerateEmbeddedResourcesHash" BeforeTargets="CoreCompile">
|
||||
<PropertyGroup>
|
||||
<HashFileDir>$(IntermediateOutputPath)Generated</HashFileDir>
|
||||
<HashFilePath>$(HashFileDir)\EmbeddedResourcesHash.cs</HashFilePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Create the output directory -->
|
||||
<MakeDir Directories="$(HashFileDir)" />
|
||||
|
||||
<!-- Run a script to calculate hash and generate code file -->
|
||||
<Exec Command="powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(MSBuildThisFileDirectory)GenerateEmbeddedResourcesHash.ps1" -ResourcesDir "$(MSBuildThisFileDirectory)Resources" -OutputFile "$(HashFilePath)"" />
|
||||
|
||||
<!-- Include the generated file in compilation -->
|
||||
<ItemGroup>
|
||||
<Compile Include="$(HashFilePath)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\GitVersion.cs">
|
||||
<Link>Properties\GitVersion.cs</Link>
|
||||
|
@@ -1,235 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
namespace QuickLook.Plugin.MarkdownViewer;
|
||||
|
||||
internal class ResourceManager
|
||||
{
|
||||
private readonly string _resourcePath;
|
||||
private readonly string _resourcePrefix;
|
||||
private readonly string _versionFilePath;
|
||||
private readonly string _noUpdateFilePath;
|
||||
private readonly string _embeddedHash;
|
||||
|
||||
public ResourceManager(string resourcePath, string resourcePrefix)
|
||||
{
|
||||
_resourcePath = resourcePath;
|
||||
_resourcePrefix = resourcePrefix;
|
||||
_versionFilePath = Path.Combine(_resourcePath, ".version");
|
||||
_noUpdateFilePath = Path.Combine(_resourcePath, ".noupdate");
|
||||
_embeddedHash = GetEmbeddedResourcesHash();
|
||||
}
|
||||
|
||||
public void InitializeResources()
|
||||
{
|
||||
// Extract resources for the first time
|
||||
if (!Directory.Exists(_resourcePath))
|
||||
{
|
||||
ExtractResources();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if updates are disabled
|
||||
if (File.Exists(_noUpdateFilePath))
|
||||
return;
|
||||
|
||||
// Check if resources need updating by comparing hashes
|
||||
var versionInfo = ReadVersionFile();
|
||||
|
||||
if (versionInfo == null)
|
||||
{
|
||||
// No version file exists, create it and extract resources
|
||||
ExtractResources();
|
||||
return;
|
||||
}
|
||||
|
||||
// If embedded hash matches stored hash, no update needed
|
||||
if (_embeddedHash == versionInfo.EmbeddedHash) return;
|
||||
|
||||
// Calculate current directory hash
|
||||
var currentDirectoryHash = CalculateDirectoryHash(_resourcePath);
|
||||
|
||||
// If current directory matches the stored extracted hash, user hasn't modified files
|
||||
if (currentDirectoryHash == versionInfo.ExtractedHash)
|
||||
{
|
||||
// Safe to update
|
||||
ExtractResources();
|
||||
return;
|
||||
}
|
||||
|
||||
// User has modified files, ask for permission to update
|
||||
var result = MessageBox.Show(
|
||||
"The MarkdownViewer resources have been updated. Would you like to update to the newest version?\n\n" +
|
||||
"Note: Your current resources appear to have been modified. Updating will overwrite your modifications.",
|
||||
"MarkdownViewer Update Available",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
ExtractResources();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update the embedded hash in the version file to prevent any more prompts for this version
|
||||
UpdateVersionFileEmbeddedHash();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractResources()
|
||||
{
|
||||
// Delete and recreate directory to ensure clean state
|
||||
if (Directory.Exists(_resourcePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(_resourcePath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to delete directory {_resourcePath}: {ex.Message}");
|
||||
// If we can't delete the directory, we'll try to continue with existing one
|
||||
}
|
||||
}
|
||||
Directory.CreateDirectory(_resourcePath);
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceNames = assembly.GetManifestResourceNames();
|
||||
|
||||
foreach (var resourceName in resourceNames)
|
||||
{
|
||||
if (!resourceName.StartsWith(_resourcePrefix)) continue;
|
||||
|
||||
var relativePath = resourceName.Substring(_resourcePrefix.Length);
|
||||
if (relativePath.Equals("resources", StringComparison.OrdinalIgnoreCase)) continue; // Skip 'resources' binary file
|
||||
|
||||
var targetPath = Path.Combine(_resourcePath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
var directory = Path.GetDirectoryName(targetPath);
|
||||
if (directory != null)
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// Extract the resource
|
||||
using (var stream = assembly.GetManifestResourceStream(resourceName))
|
||||
using (var fileStream = File.Create(targetPath))
|
||||
{
|
||||
if (stream == null) continue;
|
||||
stream.CopyTo(fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate version file after extracting all resources
|
||||
GenerateVersionFile();
|
||||
|
||||
// Verify that md2html.html was extracted
|
||||
var htmlPath = Path.Combine(_resourcePath, "md2html.html");
|
||||
if (!File.Exists(htmlPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Required template file md2html.html not found in resources. Available resources: {string.Join(", ", resourceNames)}");
|
||||
}
|
||||
}
|
||||
|
||||
private class VersionInfo(string embeddedHash, string extractedHash)
|
||||
{
|
||||
public string EmbeddedHash { get; set; } = embeddedHash;
|
||||
public string ExtractedHash { get; set; } = extractedHash;
|
||||
}
|
||||
|
||||
private static string CalculateDirectoryHash(string directory)
|
||||
{
|
||||
var files = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories)
|
||||
.Where(f => !f.EndsWith(".version"))
|
||||
.OrderBy(f => f)
|
||||
.ToList();
|
||||
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
var combinedBytes = new List<byte>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
var relativePath = file.Substring(directory.Length + 1);
|
||||
var pathBytes = Encoding.UTF8.GetBytes(relativePath.ToLowerInvariant());
|
||||
combinedBytes.AddRange(pathBytes);
|
||||
|
||||
var contentBytes = File.ReadAllBytes(file);
|
||||
combinedBytes.AddRange(contentBytes);
|
||||
}
|
||||
|
||||
var hash = sha256.ComputeHash([.. combinedBytes]);
|
||||
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetEmbeddedResourcesHash()
|
||||
{
|
||||
try
|
||||
{
|
||||
return EmbeddedResourcesHash.Hash;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Debug.WriteLine("QuickLook.Plugin.MarkdownViewer: Embedded resources hash file not found.");
|
||||
return CalculateEmbeddedResourcesHash();
|
||||
}
|
||||
}
|
||||
|
||||
private string CalculateEmbeddedResourcesHash()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
using var sha256 = SHA256.Create();
|
||||
var combinedBytes = new List<byte>();
|
||||
var resourceNames = assembly.GetManifestResourceNames()
|
||||
.Where(name => name.StartsWith(_resourcePrefix) &&
|
||||
!name.EndsWith(".version"))
|
||||
.OrderBy(name => name)
|
||||
.ToList();
|
||||
|
||||
foreach (var resourceName in resourceNames)
|
||||
{
|
||||
var nameBytes = Encoding.UTF8.GetBytes(resourceName.ToLowerInvariant());
|
||||
combinedBytes.AddRange(nameBytes);
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null) continue;
|
||||
var buffer = new byte[stream.Length];
|
||||
stream.Read(buffer, 0, buffer.Length);
|
||||
combinedBytes.AddRange(buffer);
|
||||
}
|
||||
|
||||
var hash = sha256.ComputeHash([.. combinedBytes]);
|
||||
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private void GenerateVersionFile()
|
||||
{
|
||||
var extractedHash = CalculateDirectoryHash(_resourcePath);
|
||||
var newVersionContent = $"{_embeddedHash}{Environment.NewLine}{extractedHash}";
|
||||
File.WriteAllText(_versionFilePath, newVersionContent);
|
||||
}
|
||||
|
||||
private void UpdateVersionFileEmbeddedHash()
|
||||
{
|
||||
var versionInfo = ReadVersionFile() ?? throw new InvalidOperationException("Cannot update version file: no existing version file found");
|
||||
|
||||
var newVersionContent = $"{_embeddedHash}{Environment.NewLine}{versionInfo.ExtractedHash}";
|
||||
File.WriteAllText(_versionFilePath, newVersionContent);
|
||||
}
|
||||
|
||||
private VersionInfo? ReadVersionFile()
|
||||
{
|
||||
if (!File.Exists(_versionFilePath)) return null;
|
||||
|
||||
var lines = File.ReadAllLines(_versionFilePath);
|
||||
if (lines.Length < 2) return null;
|
||||
|
||||
return new VersionInfo(lines[0], lines[1]);
|
||||
}
|
||||
}
|
@@ -1,79 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace QuickLook.Plugin.MarkdownViewer {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("QuickLook.Plugin.MarkdownViewer.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <!DOCTYPE html>
|
||||
///<html>
|
||||
/// <head>
|
||||
/// <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
/// </head>
|
||||
/// <body>
|
||||
/// <textarea id="text-input" style="display:none;">{{content}}</textarea>
|
||||
/// <style><!-- https://github.com/sindresorhus/github-markdown-css -->.markdown-body hr::after,.markdown-body::after{clear:both}@font-face{font-family:octicons-link;src:url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAA [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
internal static string md2html {
|
||||
get {
|
||||
return ResourceManager.GetString("md2html", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,124 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
<data name="md2html" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>Resources\md2html.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
|
||||
</data>
|
||||
</root>
|
Reference in New Issue
Block a user