mirror of
https://github.com/QL-Win/QuickLook.git
synced 2025-09-11 17:59:17 +00:00
Improved MarkdownViewer
#### Features - Uses the latest [github-markdown.css](https://github.com/sindresorhus/github-markdown-css/blob/main/github-markdown.css) file that contains styling support for both dark and light mode. - Table of Contents has an improved design, and: - The width can be resized. - TOC headings are automatically highlighted to help track your position in the file. - Improved fullscreen layout based on Github.com behavior. - External links clicked will open in default browser instead of doing nothing. - Uses [markdownItAnchor](https://github.com/valeriangalliat/markdown-it-anchor) to allow heading anchor links in the file to work. - Uses [highlight.js](https://github.com/highlightjs/highlight.js) to provide syntax highlighting to codeblocks. #### Changes - Made changes to allow the `md2html.html` file to use relative file imports for better maintainability. - MarkdownViewer can now easily be customized by users by modifying files in `<Quicklook data folder>/QuickLook.Plugin.MarkdownViewer/` - Caching and `localStorage` is now supported thanks to these changes. - Prevent default behavior of spacebar scrolling the page, while we use spacebar to dismiss the preview. - Sets `WebView` `DefaultBackgroundColor` to prevent white flash in dark mode. After the page has loaded, sets `DefaultBackgroundColor` back to white to have the expected default behavior on HTML pages that don't specify any background color. #### Clean up - Removed the need for `jQuery`. - Removed old polyfill code.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2021 Paddy Xu and Frank Becker
|
||||
// Copyright © 2021 Paddy Xu and Frank Becker
|
||||
//
|
||||
// This file is part of QuickLook program.
|
||||
//
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
@@ -29,8 +30,8 @@ namespace QuickLook.Plugin.HtmlViewer
|
||||
{
|
||||
public class WebpagePanel : UserControl
|
||||
{
|
||||
private Uri _currentUri;
|
||||
private WebView2 _webView;
|
||||
public Uri _currentUri;
|
||||
public WebView2 _webView;
|
||||
|
||||
public WebpagePanel()
|
||||
{
|
||||
@@ -44,10 +45,12 @@ namespace QuickLook.Plugin.HtmlViewer
|
||||
{
|
||||
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.NavigationCompleted += WebView_NavigationCompleted;
|
||||
Content = _webView;
|
||||
}
|
||||
}
|
||||
@@ -83,6 +86,11 @@ namespace QuickLook.Plugin.HtmlViewer
|
||||
if (newUri != _currentUri) e.Cancel = true;
|
||||
}
|
||||
|
||||
private void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
|
||||
{
|
||||
_webView.DefaultBackgroundColor = Color.White; // Reset to white after page load to match expected default behavior
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_webView?.Dispose();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2017 Paddy Xu
|
||||
// Copyright © 2017 Paddy Xu
|
||||
//
|
||||
// This file is part of QuickLook program.
|
||||
//
|
||||
@@ -16,12 +16,17 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using QuickLook.Common.Helpers;
|
||||
using QuickLook.Common.Plugin;
|
||||
using QuickLook.Plugin.HtmlViewer;
|
||||
using UtfUnknown;
|
||||
@@ -30,17 +35,90 @@ namespace QuickLook.Plugin.MarkdownViewer
|
||||
{
|
||||
public class Plugin : IViewer
|
||||
{
|
||||
private WebpagePanel _panel;
|
||||
private WebpagePanel? _panel;
|
||||
private static string _resourcePath;
|
||||
private static readonly string ResourcePrefix = "QuickLook.Plugin.MarkdownViewer.Resources.";
|
||||
private string? _currentHtmlPath;
|
||||
|
||||
private static bool OverrideFilesInDevelopment => true && Debugger.IsAttached; // Debug setting
|
||||
|
||||
static Plugin()
|
||||
{
|
||||
// Set up resource path in AppData
|
||||
_resourcePath = Path.Combine(SettingHelper.LocalDataPath, "QuickLook.Plugin.MarkdownViewer");
|
||||
}
|
||||
|
||||
public int Priority => 0;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
// Create directory if it doesn't exist
|
||||
if (!Directory.Exists(_resourcePath) || OverrideFilesInDevelopment)
|
||||
{
|
||||
Directory.CreateDirectory(_resourcePath);
|
||||
ExtractResources();
|
||||
}
|
||||
|
||||
// Clean up any temporary HTML files if QuickLook was forcibly terminated
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractResources()
|
||||
{
|
||||
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 (skip if it already exists, unless in debug mode)
|
||||
if (File.Exists(targetPath) && !OverrideFilesInDevelopment)
|
||||
continue;
|
||||
|
||||
using (var resourceStream = assembly.GetManifestResourceStream(resourceName))
|
||||
using (var fileStream = File.Create(targetPath))
|
||||
{
|
||||
resourceStream?.CopyTo(fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)}");
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanHandle(string path)
|
||||
{
|
||||
return !Directory.Exists(path) && new[] {".md", ".rmd", ".markdown"}.Any(path.ToLower().EndsWith);
|
||||
return !Directory.Exists(path) && new[] { ".md", ".rmd", ".markdown" }.Any(path.ToLower().EndsWith);
|
||||
}
|
||||
|
||||
public void Prepare(string path, ContextObject context)
|
||||
@@ -54,29 +132,180 @@ namespace QuickLook.Plugin.MarkdownViewer
|
||||
context.ViewerContent = _panel;
|
||||
context.Title = Path.GetFileName(path);
|
||||
|
||||
_panel.NavigateToHtml(GenerateMarkdownHtml(path));
|
||||
var htmlPath = GenerateMarkdownHtml(path);
|
||||
_panel.NavigateToFile(htmlPath);
|
||||
_panel.Dispatcher.Invoke(() => { context.IsBusy = false; }, DispatcherPriority.Loaded);
|
||||
|
||||
_panel._webView.NavigationStarting += NavigationStarting_CancelNavigation;
|
||||
}
|
||||
|
||||
private void NavigationStarting_CancelNavigation(object sender, CoreWebView2NavigationStartingEventArgs e)
|
||||
{
|
||||
if (e.Uri.StartsWith("data:")) // when using NavigateToString
|
||||
return;
|
||||
|
||||
var newUri = new Uri(e.Uri);
|
||||
if (newUri == _panel?._currentUri) return;
|
||||
e.Cancel = true;
|
||||
|
||||
// Open in default browser
|
||||
try
|
||||
{
|
||||
if (!Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri))
|
||||
{
|
||||
Debug.WriteLine($"Invalid URI format: {e.Uri}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Safe schemes can open directly
|
||||
if (uri.Scheme == Uri.UriSchemeHttp ||
|
||||
uri.Scheme == Uri.UriSchemeHttps ||
|
||||
uri.Scheme == Uri.UriSchemeMailto)
|
||||
{
|
||||
Process.Start(uri.AbsoluteUri);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask user for unsafe schemes. Use dispatcher to avoid blocking thread.
|
||||
var associatedApp = GetAssociatedAppForScheme(uri.Scheme);
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
!string.IsNullOrEmpty(associatedApp) ?
|
||||
$"The following link will open in {associatedApp}:\n{e.Uri}" : $"The following link will open:\n{e.Uri}",
|
||||
!string.IsNullOrEmpty(associatedApp) ?
|
||||
$"Open {associatedApp}?" : "Open custom URI?",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Information);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
Process.Start(e.Uri);
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to open URL in browser: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern uint AssocQueryString(
|
||||
AssocF flags,
|
||||
AssocStr str,
|
||||
string pszAssoc,
|
||||
string? pszExtra,
|
||||
[Out] StringBuilder? pszOut,
|
||||
ref uint pcchOut);
|
||||
|
||||
[Flags]
|
||||
private enum AssocF
|
||||
{
|
||||
None = 0,
|
||||
VerifyExists = 0x1
|
||||
}
|
||||
|
||||
private enum AssocStr
|
||||
{
|
||||
Command = 1,
|
||||
Executable = 2,
|
||||
FriendlyAppName = 4
|
||||
}
|
||||
|
||||
private string? GetAssociatedAppForScheme(string scheme)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to get friendly app name first
|
||||
uint pcchOut = 0;
|
||||
AssocQueryString(AssocF.None, AssocStr.FriendlyAppName, scheme, null, null, ref pcchOut);
|
||||
|
||||
if (pcchOut > 0)
|
||||
{
|
||||
StringBuilder pszOut = new StringBuilder((int)pcchOut);
|
||||
AssocQueryString(AssocF.None, AssocStr.FriendlyAppName, scheme, null, pszOut, ref pcchOut);
|
||||
|
||||
var appName = pszOut.ToString().Trim();
|
||||
if (!string.IsNullOrEmpty(appName))
|
||||
return appName;
|
||||
}
|
||||
|
||||
// Fall back to executable name if friendly name is not available
|
||||
pcchOut = 0;
|
||||
AssocQueryString(AssocF.None, AssocStr.Executable, scheme, null, null, ref pcchOut);
|
||||
|
||||
if (pcchOut > 0)
|
||||
{
|
||||
StringBuilder pszOut = new StringBuilder((int)pcchOut);
|
||||
AssocQueryString(AssocF.None, AssocStr.Executable, scheme, null, pszOut, ref pcchOut);
|
||||
|
||||
var exeName = pszOut.ToString().Trim();
|
||||
if (!string.IsNullOrEmpty(exeName))
|
||||
return Path.GetFileName(exeName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to get associated app: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void CleanupTempHtmlFile()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_currentHtmlPath) && File.Exists(_currentHtmlPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(_currentHtmlPath);
|
||||
}
|
||||
catch (IOException) { } // Ignore deletion errors
|
||||
}
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
CleanupTempHtmlFile();
|
||||
|
||||
_panel?.Dispose();
|
||||
_panel = null;
|
||||
}
|
||||
|
||||
private string GenerateMarkdownHtml(string path)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
var encoding = CharsetDetector.DetectFromBytes(bytes).Detected?.Encoding ?? Encoding.Default;
|
||||
|
||||
var md = encoding.GetString(bytes);
|
||||
md = WebUtility.HtmlEncode(md);
|
||||
|
||||
var html = Resources.md2html.Replace("{{content}}", md);
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
}
|
@@ -70,11 +70,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\**" />
|
||||
<EmbeddedResource Include="Resources\**\*">
|
||||
<LogicalName>QuickLook.Plugin.MarkdownViewer.Resources.%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
File diff suppressed because it is too large
Load Diff
1207
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/highlight.js/highlight.min.js
vendored
Normal file
1207
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/highlight.js/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
10
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/highlight.js/styles/github.min.css
vendored
Normal file
10
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/highlight.js/styles/github.min.css
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
2
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/js/markdown-it.min.js
vendored
Normal file
2
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/js/markdown-it.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user